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
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts import QtQuick.Layouts
import ISC import ISC
import ISC.Tabs.components
import QtQuick.Dialogs import QtQuick.Dialogs
// ===== Home Workspace Shell ===== // ===== Home Workspace Shell =====
// Unified left rail: pill tab bar → context panel → status footer → utility buttons.
// Workspace fills the remaining area with the active tab's content.
Rectangle { Rectangle {
id: root id: root
anchors.fill: parent anchors.fill: parent
@@ -13,27 +17,8 @@ Rectangle {
border.color: Theme.outerFrameBorder border.color: Theme.outerFrameBorder
border.width: Theme.borderStrong border.width: Theme.borderStrong
// ===== Navigation Model =====
// ---------------------------------------------------------------------------
// TODO P1.1: Delete these 3 property blocks — superseded by mockup rail
//
// THINKING:
// The new rail uses a pill-tab-bar for navigation (Status/Data/Map) instead
// of sideActions[] + bottomTabs[]. The Repeaters below that consume these
// arrays are also deleted with the old layout (P1.1).
// selectedSideActionIndex is dead — the new rail has no concept of a
// globally-selected side action. selectedTabIndex stays, redefined below.
//
// Cross-ref: P1.2 (pill tabs), P1.3 (context panels), P1.4 (workspace)
// ---------------------------------------------------------------------------
property var sideActions: ["DETECT WAFER", "READ MEMORY", "ERASE MEMORY"]
// ===== Footer Tab Model =====
property var bottomTabs: ["Status", "Data", "Wafer Map", "Settings"]
// ===== View State ===== // ===== View State =====
property int selectedTabIndex: 0 property int selectedTabIndex: 0
property int selectedSideActionIndex: -1
property bool memoryRead: false property bool memoryRead: false
property bool waferDetected: false property bool waferDetected: false
@@ -41,31 +26,25 @@ Rectangle {
Connections { Connections {
target: deviceController target: deviceController
function onDetectResult(result){ function onDetectResult(result){
//result is a waferInfo dict on success, None on failure
root.waferDetected = (result != null && result != undefined) root.waferDetected = (result != null && result != undefined)
} }
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result.success && result.csv_path) { if (result.success && result.csv_path) {
// Load the freshly read CSV into the player
streamController.loadFile(result.csv_path) streamController.loadFile(result.csv_path)
root.selectedTabIndex = 2
// Automatically switch to the "Wafer Map Tab" (index 3)
root.selectedTabIndex = 3
} }
} }
} }
Connections { Connections {
target: deviceController target: deviceController
function onReadResult(result) { function onReadResult(result) {
// result has "success" key on success, "error" key on failure
root.memoryRead = (result !== null && root.memoryRead = (result !== null &&
result !== undefined && result !== undefined &&
result.success === true) result.success === true)
if (root.memoryRead) { if (root.memoryRead) {
console.log(`[P1.1] Memory read complete: ${result.bytes} bytes`) console.log("[P1.1] Memory read complete:", result.bytes, "bytes")
} else { } else {
console.log(`[P1.1] Read failed: ${result.error || "unknown"}`) console.log("[P1.1] Read failed:", result.error || "unknown")
} }
} }
} }
@@ -90,298 +69,573 @@ Rectangle {
} }
} }
// --------------------------------------------------------------------------- // ===== Settings Popup =====
// TODO P1.1: Delete importFileDialog + storedDataDialog (and the Popup {
// `selectedTabIndex = 3` lines inside them) id: settingsPopup
// modal: true
// THINKING: dim: true
// These dialogs were only opened from the old side-rail buttons closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
// "IMPORT DATA" (index 4) and "STORED DATA" (index 5). With the width: Math.min(root.width - 80, 800)
// mockup rail, the Map tab's file list (P1.3) replaces both — it height: Math.min(root.height - 80, 600)
// drives `streamController.loadFile()` directly from a file_browser anchors.centerIn: Overlay.overlay
// Repeater. Keeping these dead dialogs would bloat the QML and
// confuse future contributors. background: Rectangle {
// radius: Theme.radiusMd
// `selectedTabIndex = 3` is also stale (Map tab is index 2 now). color: Theme.cardBackground
// The replacement flow: file_row.onClicked → loadFile() → border.color: Theme.cardBorder
// selectedTabIndex = 2. border.width: 1
// ---------------------------------------------------------------------------
FileDialog {
id: importFileDialog
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
}
} }
FileDialog { contentItem: Rectangle {
id: storedDataDialog color: Theme.pageBackground
title: "Open Stored CSV File" radius: Theme.radiusMd
nameFilters: ["CSV files (*.csv)", "All files (*)"] clip: true
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))
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.panelPadding spacing: 0
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
Rectangle { Rectangle {
anchors.left: parent.left Layout.fillWidth: true
anchors.top: parent.top Layout.preferredHeight: 40
anchors.bottom: parent.bottom color: Theme.panelBackground
width: 3 border.color: Theme.cardBorder
visible: control.isActive border.width: 1
color: Theme.primaryAccent radius: Theme.radiusMd
radius: Theme.radiusXs
} 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 { Button {
text: control.text text: "\u2715"
color: control.isActive ? Theme.headingColor : Theme.bodyColor flat: true
font.bold: control.isActive Layout.preferredWidth: 32
font.pixelSize: 18 Layout.preferredHeight: 32
wrapMode: Text.WordWrap 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 horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter 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 { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.workspaceBackground color: Theme.workspaceBackground
border.color: Theme.workspaceBorder border.color: Theme.workspaceBorder
border.width: Theme.borderThin
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 border.width: 1
radius: Theme.radiusMd
StackLayout { StackLayout {
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.mainAreaPadding
currentIndex: root.selectedTabIndex currentIndex: root.selectedTabIndex
// ===== Tab Content Routing =====
Repeater {
model: root.bottomTabs
Item {
property string tabName: modelData
Loader { Loader {
anchors.fill: parent source: "Tabs/StatusTab.qml"
active: parent.tabName === "Settings" active: StackLayout.index === root.selectedTabIndex
source: parent.tabName === "Settings" ? "Tabs/SettingsTab.qml" : ""
} }
Loader { Loader {
anchors.fill: parent source: "Tabs/DataTab.qml"
active: parent.tabName === "Status" active: StackLayout.index === root.selectedTabIndex
source: parent.tabName === "Status" ? "Tabs/StatusTab.qml" : ""
} }
Loader { Loader {
anchors.fill: parent source: "Tabs/WaferMapTab.qml"
active: parent.tabName === "Data" active: StackLayout.index === root.selectedTabIndex
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
}
}
}
}
} }
} }
} }
+30 -20
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.TableView
import ISC import ISC
// ===== Data Tab ===== // ===== Data Tab =====
@@ -18,7 +17,7 @@ Item {
target: deviceController target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result && result.success) { if (result && result.success) {
dataTable.forceLayout() dataTable.forceLayout();
} }
} }
} }
@@ -31,7 +30,7 @@ Item {
Label { Label {
text: "No Data" text: "No Data"
font.pixelSize: 24 font.pixelSize: Theme.font2xl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -39,7 +38,7 @@ Item {
Label { Label {
text: "Read and parse wafer data to see temperature readings here." text: "Read and parse wafer data to see temperature readings here."
font.pixelSize: 14 font.pixelSize: Theme.fontMd
color: Theme.bodyColor color: Theme.bodyColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@@ -60,17 +59,18 @@ Item {
Label { Label {
text: "Temperature Data" text: "Temperature Data"
font.pixelSize: 18 font.pixelSize: Theme.fontXl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
Label { Label {
text: deviceController.dataRowCount + " rows × " + text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors"
deviceController.dataColCount + " sensors" font.pixelSize: Theme.fontSm
font.pixelSize: 13
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
@@ -82,7 +82,8 @@ Item {
Button { Button {
text: "Open in Excel" text: "Open in Excel"
icon.source: "icon/excel.svg" icon.source: "icons/excel.svg"
icon.color: Theme.headingColor
onClicked: deviceController.openCsvFile() onClicked: deviceController.openCsvFile()
} }
@@ -96,7 +97,9 @@ Item {
onClicked: splitDialog.open() onClicked: splitDialog.open()
} }
Item { Layout.fillWidth: true} Item {
Layout.fillWidth: true
}
} }
// --- Table container --- // --- Table container ---
Rectangle { Rectangle {
@@ -131,7 +134,7 @@ Item {
// 'display' is the Qt.DisplayRole value from headerData() // 'display' is the Qt.DisplayRole value from headerData()
text: display !== undefined ? display : "" text: display !== undefined ? display : ""
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
font.bold: true font.bold: true
elide: Text.ElideRight elide: Text.ElideRight
} }
@@ -148,13 +151,16 @@ Item {
reuseItems: true reuseItems: true
columnWidthProvider: function (col) { columnWidthProvider: function (col) {
if (col === 0) return 52 // row index column if (col === 0)
const sensorCols = Math.max(1, dataTable.columns - 1) return 52; // row index column
const available = dataTable.width - 52 const sensorCols = Math.max(1, dataTable.columns - 1);
return Math.max(52, Math.min(90, available / sensorCols)) const available = dataTable.width - 52;
return Math.max(52, Math.min(90, available / sensorCols));
} }
rowHeightProvider: function() { return 24 } rowHeightProvider: function () {
return 24;
}
delegate: Rectangle { delegate: Rectangle {
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
@@ -163,13 +169,17 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
text: display !== undefined ? display : "" text: display !== undefined ? display : ""
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
ScrollBar.horizontal: ScrollBar { policy: ScrollBar.AsNeeded } ScrollBar.horizontal: ScrollBar {
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded } policy: ScrollBar.AsNeeded
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
} }
} }
} }
+40 -37
View File
@@ -27,18 +27,20 @@ Item {
Label { Label {
text: "Line Chart" text: "Line Chart"
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 13 font.pixelSize: Theme.fontSm
font.bold: true font.bold: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
// Refresh button — pulls data from deviceController // Refresh button — pulls data from deviceController
Button { Button {
id: refreshBtn id: refreshBtn
text: "REFRESH" text: "REFRESH"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
implicitHeight: 26 implicitHeight: 26
implicitWidth: 72 implicitWidth: 72
@@ -68,7 +70,7 @@ Item {
Label { Label {
text: "Source:" text: "Source:"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -77,7 +79,7 @@ Item {
model: ["Parsed Data", "Live Trend"] model: ["Parsed Data", "Live Trend"]
currentIndex: 0 currentIndex: 0
implicitHeight: 26 implicitHeight: 26
font.pixelSize: 11 font.pixelSize: Theme.fontXs
background: Rectangle { background: Rectangle {
color: Theme.fieldBackground color: Theme.fieldBackground
border.color: Theme.fieldBorder border.color: Theme.fieldBorder
@@ -95,13 +97,15 @@ Item {
onCurrentIndexChanged: reloadData() onCurrentIndexChanged: reloadData()
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
Label { Label {
id: statusLabel id: statusLabel
text: "Ready" text: "Ready"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.italic: true font.italic: true
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
@@ -125,8 +129,7 @@ Item {
gridColor: Theme.softBorder gridColor: Theme.softBorder
axisColor: Theme.bodyColor axisColor: Theme.bodyColor
textColor: Theme.headingColor textColor: Theme.headingColor
seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, seriesColors: [Theme.sensorLow, Theme.sensorHigh, Theme.sensorInRange, "#FFA726", "#AB47BC", "#00BCD4"]
"#FFA726", "#AB47BC", "#00BCD4"]
showLegend: true showLegend: true
} }
} }
@@ -137,44 +140,44 @@ Item {
function reloadData() { function reloadData() {
if (sourceSelector.currentIndex === 0) { if (sourceSelector.currentIndex === 0) {
// Parsed data from DeviceController // Parsed data from DeviceController
loadParsedData() loadParsedData();
} else { } else {
// Live trend from SessionController // Live trend from SessionController
loadLiveTrend() loadLiveTrend();
} }
} }
function loadParsedData() { function loadParsedData() {
statusLabel.text = "Loading..." statusLabel.text = "Loading...";
var result = deviceController.getChartData() var result = deviceController.getChartData();
if (!result || !result.success) { if (!result || !result.success) {
graph.seriesData = [] graph.seriesData = [];
graph.sensorNames = [] graph.sensorNames = [];
graph.title = "Sensor Temperature Over Time" graph.title = "Sensor Temperature Over Time";
statusLabel.text = "No data — read a wafer first" statusLabel.text = "No data — read a wafer first";
return return;
} }
graph.seriesData = result.series || [] graph.seriesData = result.series || [];
graph.sensorNames = result.sensor_names || [] graph.sensorNames = result.sensor_names || [];
graph.title = "Sensor Temperature Over Time" graph.title = "Sensor Temperature Over Time";
statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded" statusLabel.text = (result.series ? result.series.length : 0) + " sensors loaded";
} }
function loadLiveTrend() { function loadLiveTrend() {
// Live trend: connects to streamController.trendData // Live trend: connects to streamController.trendData
// The trend data comes as a JSON array of per-frame averages // 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 // We need at least one data point
if (streamController.stats && streamController.stats.avg !== undefined) { if (streamController.stats && streamController.stats.avg !== undefined) {
// Build a single-series graph from the trend buffer // Build a single-series graph from the trend buffer
// For now show a placeholder — the trendData signal handles updates // For now show a placeholder — the trendData signal handles updates
graph.title = "Live Average Temperature" graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)" graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Time (s)" graph.xLabel = "Time (s)";
statusLabel.text = "Live trend — waiting for data..." statusLabel.text = "Live trend — waiting for data...";
} else { } 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 target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (sourceSelector.currentIndex === 0) { if (sourceSelector.currentIndex === 0) {
root.reloadData() root.reloadData();
} }
} }
} }
@@ -194,14 +197,14 @@ Item {
function onTrendData(avgsJson) { function onTrendData(avgsJson) {
if (sourceSelector.currentIndex === 1) { if (sourceSelector.currentIndex === 1) {
try { try {
var avgs = JSON.parse(avgsJson) var avgs = JSON.parse(avgsJson);
if (avgs && avgs.length > 0) { if (avgs && avgs.length > 0) {
graph.seriesData = [avgs] graph.seriesData = [avgs];
graph.sensorNames = ["Average"] graph.sensorNames = ["Average"];
graph.title = "Live Average Temperature" graph.title = "Live Average Temperature";
graph.yLabel = "Avg Temperature (°C)" graph.yLabel = "Avg Temperature (°C)";
graph.xLabel = "Frame" graph.xLabel = "Frame";
statusLabel.text = "Live: " + avgs.length + " data points" statusLabel.text = "Live: " + avgs.length + " data points";
} }
} catch (e) { } catch (e) {
// ignore parse errors // ignore parse errors
@@ -213,7 +216,7 @@ Item {
// On startup, try to load parsed data if available // On startup, try to load parsed data if available
Component.onCompleted: { Component.onCompleted: {
if (deviceController.dataRowCount > 0) { if (deviceController.dataRowCount > 0) {
reloadData() reloadData();
} }
} }
} }
+22 -14
View File
@@ -24,9 +24,11 @@ Dialog {
property int selectedRow: -1 property int selectedRow: -1
property int metadataEditRow: -1 property int metadataEditRow: -1
readonly property string masterTypeFieldText: { readonly property string masterTypeFieldText: {
if (metadataEditRow < 0) return ""; if (metadataEditRow < 0)
return "";
const rec = tableModel[metadataEditRow]; const rec = tableModel[metadataEditRow];
if (!rec) return ""; if (!rec)
return "";
return rec.masterType ?? ""; return rec.masterType ?? "";
} }
@@ -113,7 +115,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight elide: Text.ElideRight
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
@@ -129,6 +131,12 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
text: root.title text: root.title
font.pixelSize: 15 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -221,7 +229,7 @@ Dialog {
text: file_browser.currentDirectory text: file_browser.currentDirectory
color: Theme.fieldText color: Theme.fieldText
elide: Text.ElideMiddle elide: Text.ElideMiddle
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
} }
@@ -242,7 +250,7 @@ Dialog {
anchors.centerIn: parent anchors.centerIn: parent
text: root.headerTitles[index] ?? "" text: root.headerTitles[index] ?? ""
font.bold: true font.bold: true
font.pixelSize: 12 font.pixelSize: Theme.fontSm
color: Theme.headingColor color: Theme.headingColor
} }
} }
@@ -329,7 +337,7 @@ Dialog {
const mt = root.normalizedRows[row]?.masterType ?? ""; const mt = root.normalizedRows[row]?.masterType ?? "";
return mt || "—"; return mt || "—";
} }
font.pixelSize: 14 font.pixelSize: Theme.fontMd
font.bold: true font.bold: true
color: { color: {
const mt = root.normalizedRows[row]?.masterType ?? ""; const mt = root.normalizedRows[row]?.masterType ?? "";
@@ -346,7 +354,7 @@ Dialog {
sourceComponent: Button { sourceComponent: Button {
hoverEnabled: true hoverEnabled: true
text: "Edit…" text: "Edit…"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
onClicked: root.openMetadataEditor(row) onClicked: root.openMetadataEditor(row)
background: Rectangle { background: Rectangle {
@@ -361,7 +369,7 @@ Dialog {
color: Theme.buttonNeutralText color: Theme.buttonNeutralText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
} }
} }
@@ -388,7 +396,7 @@ Dialog {
return ""; return "";
} }
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
@@ -409,7 +417,7 @@ Dialog {
return record.notes ?? ""; return record.notes ?? "";
} }
elide: Text.ElideRight elide: Text.ElideRight
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: Theme.bodyColor color: Theme.bodyColor
} }
} }
@@ -456,7 +464,7 @@ Dialog {
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: root.selectedPath text: root.selectedPath
elide: Text.ElideLeft elide: Text.ElideLeft
font.pixelSize: 13 font.pixelSize: Theme.fontSm
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
} }
} }
@@ -504,7 +512,7 @@ Dialog {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 16 anchors.leftMargin: 16
text: metadataEditDialog.title text: metadataEditDialog.title
font.pixelSize: 15 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -530,7 +538,7 @@ Dialog {
maximumLineCount: 3 maximumLineCount: 3
elide: Text.ElideRight elide: Text.ElideRight
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
Label { Label {
+44 -12
View File
@@ -27,8 +27,7 @@ Item {
} }
// ===== Settings Data Helpers ===== // ===== 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) { function masterPath(family) {
const table = settingsModel.masters; const table = settingsModel.masters;
return table && table[family] ? table[family] : ""; return table && table[family] ? table[family] : "";
@@ -58,7 +57,7 @@ Item {
label: Label { label: Label {
text: settingsGroup.title text: settingsGroup.title
color: Theme.panelTitleText color: Theme.panelTitleText
font.pixelSize: 16 font.pixelSize: Theme.fontLg
font.bold: true font.bold: true
} }
} }
@@ -75,6 +74,12 @@ Item {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: textInput.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: textInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "✓" text: "✓"
font.pixelSize: 14 font.pixelSize: Theme.fontMd
font.bold: true font.bold: true
color: Theme.panelBackground 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 // Small nudge to center the checkmark perfectly
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -129,7 +144,7 @@ Item {
color: Theme.headingColor color: Theme.headingColor
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing leftPadding: toggle.indicator.width + toggle.spacing
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
} }
@@ -159,6 +174,7 @@ Item {
anchors.top: settingsScroll.top anchors.top: settingsScroll.top
anchors.right: settingsScroll.right anchors.right: settingsScroll.right
anchors.bottom: settingsScroll.bottom anchors.bottom: settingsScroll.bottom
anchors.leftMargin: 6
policy: ScrollBar.AsNeeded policy: ScrollBar.AsNeeded
contentItem: Rectangle { contentItem: Rectangle {
@@ -174,16 +190,19 @@ Item {
} }
} }
// Gap reserved between the centered content and the right-edge scrollbar.
readonly property int scrollGap: 100
ColumnLayout { ColumnLayout {
id: container id: container
width: Math.min(settingsScroll.availableWidth, Theme.settingsPanelMaxWidth) width: Math.min(settingsScroll.availableWidth - settingsScroll.scrollGap, Theme.settingsPanelMaxWidth)
x: (settingsScroll.availableWidth - width) / 2 x: (settingsScroll.availableWidth - width) / 2
spacing: Theme.settingsSectionSpacing spacing: Theme.settingsSectionSpacing
// ===== Page Title ===== // ===== Page Title =====
Label { Label {
text: "Settings" text: "Settings"
font.pixelSize: 30 font.pixelSize: Theme.font3xl
font.bold: true font.bold: true
color: Theme.headingColor color: Theme.headingColor
} }
@@ -241,11 +260,24 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: Theme.settingsGridSpacing 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 { Repeater {
model: root.masterFamilies model: masterModel
RowLayout { RowLayout {
property string family: modelData property string family: model.letter
Layout.fillWidth: true Layout.fillWidth: true
spacing: Theme.settingsRowSpacing spacing: Theme.settingsRowSpacing
@@ -361,7 +393,7 @@ Item {
Label { Label {
text: Theme.isDarkMode ? "Dark" : "Light" text: Theme.isDarkMode ? "Dark" : "Light"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 13 font.pixelSize: Theme.fontSm
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
Behavior on color { Behavior on color {
@@ -423,7 +455,7 @@ Item {
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent
text: "↓ Scroll for more" text: "↓ Scroll for more"
font.pixelSize: 13 font.pixelSize: Theme.fontSm
font.bold: true font.bold: true
color: Theme.bodyColor color: Theme.bodyColor
opacity: (settingsScroll.ScrollBar.vertical.position <= 0 && settingsScroll.contentHeight > settingsScroll.height) ? (scrollHintTimer.running ? 1 : 0) : 0 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 property bool dataParsed: false
function cleanFolderUrl(url) { function cleanFolderUrl(url) {
return decodeURIComponent(String(url).replace(/^file:\/\//, "")) return decodeURIComponent(String(url).replace(/^file:\/\//, ""));
} }
function currentFamilyCode() { function currentFamilyCode() {
var info = deviceController.lastWaferInfo var info = deviceController.lastWaferInfo;
return info && info.length > 0 ? (info[0] || "") : "" return info && info.length > 0 ? (info[0] || "") : "";
} }
function parseAndSavePendingRead() { function parseAndSavePendingRead() {
deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "") deviceController.parseAndSaveData(root.currentFamilyCode(), deviceController.selectedPort || "");
} }
FolderDialog { FolderDialog {
id: saveDirDialog id: saveDirDialog
title: "Choose a folder to save." title: "Choose a folder to save."
onAccepted: { onAccepted: {
deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder)) deviceController.setSaveDataDir(root.cleanFolderUrl(selectedFolder));
root.parseAndSavePendingRead() root.parseAndSavePendingRead();
} }
} }
@@ -59,9 +59,11 @@ ColumnLayout {
Layout.preferredHeight: 92 Layout.preferredHeight: 92
color: Theme.cardBackground color: Theme.cardBackground
border.color: { border.color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor if (deviceController.connectionStatus === "Connected")
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor return Theme.statusSuccessColor;
return Theme.cardBorder if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.cardBorder;
} }
radius: Theme.radiusSm radius: Theme.radiusSm
@@ -73,19 +75,21 @@ ColumnLayout {
Text { Text {
text: deviceController.connectionStatus text: deviceController.connectionStatus
color: { color: {
if (deviceController.connectionStatus === "Connected") return Theme.statusSuccessColor if (deviceController.connectionStatus === "Connected")
if (deviceController.connectionStatus === "Disconnected") return Theme.statusErrorColor return Theme.statusSuccessColor;
return Theme.bodyColor if (deviceController.connectionStatus === "Disconnected")
return Theme.statusErrorColor;
return Theme.bodyColor;
} }
font.bold: true font.bold: true
font.pixelSize: 16 font.pixelSize: Theme.fontLg
Layout.fillWidth: true Layout.fillWidth: true
} }
Text { Text {
text: "Port: " + (deviceController.selectedPort || "None selected") text: "Port: " + (deviceController.selectedPort || "None selected")
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 14 font.pixelSize: Theme.fontMd
Layout.fillWidth: true Layout.fillWidth: true
} }
@@ -97,7 +101,7 @@ ColumnLayout {
Text { Text {
text: "Save dir: " + (deviceController.saveDataDir || "None selected") text: "Save dir: " + (deviceController.saveDataDir || "None selected")
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
opacity: 0.7 opacity: 0.7
elide: Text.ElideMiddle elide: Text.ElideMiddle
Layout.fillWidth: true Layout.fillWidth: true
@@ -107,7 +111,7 @@ ColumnLayout {
Button { Button {
id: browseBtn id: browseBtn
text: "BROWSE..." text: "BROWSE..."
font.pixelSize: 9 font.pixelSize: Theme.font2xs
font.weight: Font.Medium font.weight: Font.Medium
implicitHeight: 20 implicitHeight: 20
implicitWidth: 65 implicitWidth: 65
@@ -149,22 +153,52 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Family Code"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferInfoFamily; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Serial Number"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferSerial; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Sensor Count"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferSensors; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Runtime"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferRuntime; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } text: "Runtime"
color: Theme.subheadingColor
font.pixelSize: Theme.fontSm
}
Text {
id: waferRuntime
text: "—"
color: Theme.bodyColor
font.pixelSize: Theme.fontLg
font.bold: true
}
} }
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Cycles"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { id: waferCycles; text: "—"; color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Rows"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: String(root.dataRows); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "Sensors"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: String(root.dataCols); color: Theme.bodyColor; font.pixelSize: 16; font.bold: true } 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 { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 4 spacing: 4
Text { text: "CSV"; color: Theme.subheadingColor; font.pixelSize: 12 } Text {
Text { text: root.csvPath; color: Theme.bodyColor; font.pixelSize: 12; elide: Text.ElideMiddle } 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 { Label {
text: "ACTIVITY LOG" text: "ACTIVITY LOG"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.letterSpacing: 1.8 font.letterSpacing: 1.8
font.weight: Font.Medium font.weight: Font.Medium
Layout.fillWidth: true Layout.fillWidth: true
@@ -259,7 +340,7 @@ ColumnLayout {
Button { Button {
text: "REFRESH SESSION" text: "REFRESH SESSION"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
@@ -278,7 +359,7 @@ ColumnLayout {
Button { Button {
text: "CLEAR LOG" text: "CLEAR LOG"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
implicitHeight: 24 implicitHeight: 24
hoverEnabled: true hoverEnabled: true
background: Rectangle { background: Rectangle {
@@ -317,7 +398,7 @@ ColumnLayout {
text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.") text: qsTr("Welcome — use the side rail to detect a wafer, or import data to review.")
readOnly: true readOnly: true
font.family: "monospace" font.family: "monospace"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
wrapMode: TextArea.WordWrap wrapMode: TextArea.WordWrap
color: Theme.bodyColor color: Theme.bodyColor
background: null background: null
@@ -328,7 +409,7 @@ ColumnLayout {
Connections { Connections {
target: deviceController target: deviceController
function onActivityLogUpdated(newLog) { function onActivityLogUpdated(newLog) {
activityLog.text = newLog activityLog.text = newLog;
} }
} }
} }
@@ -340,7 +421,7 @@ ColumnLayout {
function onLogMessage(message) { function onLogMessage(message) {
if (message == "CHOOSE_SAVE_DIR") { if (message == "CHOOSE_SAVE_DIR") {
saveDirDialog.open() saveDirDialog.open();
} }
} }
@@ -349,36 +430,35 @@ ColumnLayout {
// Status tab always visible now // Status tab always visible now
} }
function onDetectResult(result) { function onDetectResult(result) {
if (result && result.familyCode) { if (result && result.familyCode) {
root.waferDetected = true root.waferDetected = true;
waferInfoFamily.text = result.familyCode waferInfoFamily.text = result.familyCode;
waferSerial.text = result.serialNumber waferSerial.text = result.serialNumber;
waferSensors.text = String(result.sensorCount) waferSensors.text = String(result.sensorCount);
waferRuntime.text = result.runtime + "s" waferRuntime.text = result.runtime + "s";
waferCycles.text = String(result.cycleCount) waferCycles.text = String(result.cycleCount);
} }
// On failure, keep the last displayed wafer info and do not change waferDetected. // On failure, keep the last displayed wafer info and do not change waferDetected.
} }
function onStatusRestored() { function onStatusRestored() {
// Show restored wafer info if available // Show restored wafer info if available
var info = deviceController.lastWaferInfo var info = deviceController.lastWaferInfo;
if (info && info.length > 0) { if (info && info.length > 0) {
root.waferDetected = true root.waferDetected = true;
waferInfoFamily.text = info[0] || "—" waferInfoFamily.text = info[0] || "—";
waferSerial.text = info[1] || "—" waferSerial.text = info[1] || "—";
waferSensors.text = String(info[2] || 0) waferSensors.text = String(info[2] || 0);
waferRuntime.text = (info[3] || 0) + "s" waferRuntime.text = (info[3] || 0) + "s";
waferCycles.text = String(info[4] || 0) waferCycles.text = String(info[4] || 0);
} }
// Show data summary if data was previously parsed // Show data summary if data was previously parsed
if (deviceController.dataRowCount > 0) { if (deviceController.dataRowCount > 0) {
root.dataParsed = true root.dataParsed = true;
root.dataRows = deviceController.dataRowCount root.dataRows = deviceController.dataRowCount;
root.dataCols = deviceController.dataColCount root.dataCols = deviceController.dataColCount;
root.csvPath = "" // CSV path not persisted as full path, just show it was parsed root.csvPath = ""; // CSV path not persisted as full path, just show it was parsed
} }
} }
} }
@@ -386,13 +466,13 @@ ColumnLayout {
Connections { Connections {
target: deviceController target: deviceController
function onReadResult(result) { function onReadResult(result) {
root.dataParsed = false root.dataParsed = false;
root.dataRows = 0 root.dataRows = 0;
root.dataCols = 0 root.dataCols = 0;
root.csvPath = "" root.csvPath = "";
if (result && result.success === true) if (result && result.success === true)
saveDirDialog.open() saveDirDialog.open();
} }
} }
@@ -400,12 +480,12 @@ ColumnLayout {
target: deviceController target: deviceController
function onParsedDataReady(result) { function onParsedDataReady(result) {
if (result && result.success) { if (result && result.success) {
root.dataParsed = true root.dataParsed = true;
root.dataRows = result.rows root.dataRows = result.rows;
root.dataCols = result.cols root.dataCols = result.cols;
root.csvPath = result.csv_path || "" root.csvPath = result.csv_path || "";
} else { } else {
root.dataParsed = false root.dataParsed = false;
} }
} }
} }
+67 -61
View File
@@ -18,12 +18,13 @@ Item {
repeat: true repeat: true
running: streamController.mode === "live" && streamController.state !== "idle" running: streamController.mode === "live" && streamController.state !== "idle"
onTriggered: root._liveSecs++ onTriggered: root._liveSecs++
onRunningChanged: if (!running) root._liveSecs = 0 onRunningChanged: if (!running)
root._liveSecs = 0
} }
function fmtTime(s) { function fmtTime(s) {
var m = Math.floor(s / 60) var m = Math.floor(s / 60);
var ss = s % 60 var ss = s % 60;
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss;
} }
// Wire streamController.trendData (Signal(str) carrying JSON list of floats) // Wire streamController.trendData (Signal(str) carrying JSON list of floats)
// into the trend chart's data property. The slot parseJsonToData() is defined // into the trend chart's data property. The slot parseJsonToData() is defined
@@ -32,7 +33,7 @@ Item {
Connections { Connections {
target: streamController target: streamController
function onTrendData(avgsJson) { function onTrendData(avgsJson) {
trendChart.setDataFromJson(avgsJson) trendChart.setDataFromJson(avgsJson);
} }
} }
@@ -60,7 +61,8 @@ Item {
} }
TabButton { TabButton {
text: "Review" text: "Review"
implicitWidth: 64; implicitHeight: 28 implicitWidth: 64
implicitHeight: 28
background: Rectangle { background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent" color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1 radius: Theme.radiusSm - 1
@@ -68,7 +70,7 @@ Item {
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
@@ -78,7 +80,8 @@ Item {
text: "Live" text: "Live"
enabled: deviceController.connectionStatus === "Connected" enabled: deviceController.connectionStatus === "Connected"
opacity: enabled ? 1.0 : 0.4 opacity: enabled ? 1.0 : 0.4
implicitWidth: 58; implicitHeight: 28 implicitWidth: 58
implicitHeight: 28
background: Rectangle { background: Rectangle {
color: parent.checked ? Theme.tabActiveBackground : "transparent" color: parent.checked ? Theme.tabActiveBackground : "transparent"
radius: Theme.radiusSm - 1 radius: Theme.radiusSm - 1
@@ -86,29 +89,28 @@ Item {
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
color: parent.checked ? Theme.headingColor : Theme.bodyColor color: parent.checked ? Theme.headingColor : Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontSm
font.weight: parent.checked ? Font.Medium : Font.Normal font.weight: parent.checked ? Font.Medium : Font.Normal
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
} }
onCurrentIndexChanged: onCurrentIndexChanged: if (currentIndex === 1) {
if (currentIndex === 1){
// Guard: revert to Review if not connected // Guard: revert to Review if not connected
if (deviceController.connectionStatus !== "Connected") { if (deviceController.connectionStatus !== "Connected") {
currentIndex = 0 currentIndex = 0;
return return;
} }
// Entering Live mode // Entering Live mode
streamController.setMode("live") streamController.setMode("live");
var fc = "" var fc = "";
if (deviceController.lastWaferInfo && deviceController.lastWaferInfo.length > 0) { 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 { } else {
// Entering Review Mode (automatically calls stopStream in backend) // Entering Review Mode (automatically calls stopStream in backend)
streamController.setMode("review") streamController.setMode("review");
} }
} }
@@ -120,33 +122,44 @@ Item {
Label { Label {
id: liveIndicator id: liveIndicator
text: "◉" text: "◉"
color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") color: (deviceController.connectionStatus === "Connected" && streamController.state !== "idle") ? Theme.liveColor : Theme.bodyColor
? Theme.liveColor : Theme.bodyColor font.pixelSize: Theme.fontSm
font.pixelSize: 13
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle" running: deviceController.connectionStatus === "Connected" && streamController.state !== "idle"
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600; easing.type: Easing.InOutQuad } NumberAnimation {
NumberAnimation { to: 1.0; duration: 600; easing.type: Easing.InOutQuad } to: 0.2
duration: 600
easing.type: Easing.InOutQuad
}
NumberAnimation {
to: 1.0
duration: 600
easing.type: Easing.InOutQuad
}
onRunningChanged: { onRunningChanged: {
if (!running) liveIndicator.opacity = 1.0 if (!running)
liveIndicator.opacity = 1.0;
} }
} }
} }
Label { Label {
text: "Live stream" text: "Live stream"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
} }
} }
Item { Layout.fillWidth: true } Item {
Layout.fillWidth: true
}
// SET badge + avg temp // SET badge + avg temp
Rectangle { Rectangle {
visible: streamController.state === "set" visible: streamController.state === "set"
height: 22; radius: Theme.radiusSm height: 22
radius: Theme.radiusSm
color: "transparent" color: "transparent"
border.color: Theme.liveColor border.color: Theme.liveColor
border.width: 1 border.width: 1
@@ -154,10 +167,9 @@ Item {
Label { Label {
id: setLabel id: setLabel
anchors.centerIn: parent anchors.centerIn: parent
text: "SET " + (streamController.stats.avg !== undefined text: "SET " + (streamController.stats.avg !== undefined ? streamController.stats.avg + " avg" : "")
? streamController.stats.avg + " avg" : "")
color: Theme.liveColor color: Theme.liveColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
font.letterSpacing: 0.8 font.letterSpacing: 0.8
} }
@@ -168,19 +180,27 @@ Item {
spacing: 5 spacing: 5
visible: streamController.recording visible: streamController.recording
Rectangle { Rectangle {
width: 7; height: 7; radius: 4 width: 7
height: 7
radius: 4
color: Theme.recordColor color: Theme.recordColor
SequentialAnimation on opacity { SequentialAnimation on opacity {
running: streamController.recording running: streamController.recording
loops: Animation.Infinite loops: Animation.Infinite
NumberAnimation { to: 0.2; duration: 600 } NumberAnimation {
NumberAnimation { to: 1.0; duration: 600 } to: 0.2
duration: 600
}
NumberAnimation {
to: 1.0
duration: 600
}
} }
} }
Label { Label {
text: "REC" text: "REC"
color: Theme.recordColor color: Theme.recordColor
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.weight: Font.Medium
font.letterSpacing: 1.2 font.letterSpacing: 1.2
} }
@@ -197,7 +217,7 @@ Item {
// Label { // Label {
// text: "LIVE " + root.fmtTime(root._liveSecs) // text: "LIVE " + root.fmtTime(root._liveSecs)
// color: Theme.liveColor // color: Theme.liveColor
// font.pixelSize: 10 // font.pixelSize: Theme.fontXs
// font.weight: Font.Medium // font.weight: Font.Medium
// font.letterSpacing: 0.8 // font.letterSpacing: 0.8
// } // }
@@ -208,7 +228,7 @@ Item {
// visible: streamController.mode === "review" || streamController.state === "idle" // visible: streamController.mode === "review" || streamController.state === "idle"
// text: streamController.state.toUpperCas`e`() // text: streamController.state.toUpperCas`e`()
// color: Theme.bodyColor // color: Theme.bodyColor
// font.pixelSize: 11 // font.pixelSize: Theme.fontXs
// font.weight: Font.Medium // font.weight: Font.Medium
// font.letterSpacing: 1.2 // font.letterSpacing: 1.2
// } // }
@@ -248,21 +268,7 @@ Item {
Layout.fillHeight: true Layout.fillHeight: true
spacing: 16 spacing: 16
// Source panel — bordered card // Source panel — moved to side rail (P2.1)
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
}
}
ColumnLayout { ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
@@ -280,7 +286,7 @@ Item {
id: trendPane id: trendPane
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: 160 implicitHeight: 160
visible: trendChart.hasData visible: trendChart.hasData && streamController.mode === "live"
color: Theme.cardBackground color: Theme.cardBackground
border.color: Theme.cardBorder border.color: Theme.cardBorder
border.width: 1 border.width: 1
@@ -293,28 +299,28 @@ Item {
} }
} }
Item { Layout.fillWidth: true; implicitHeight: 16 } Item {
Layout.fillWidth: true
implicitHeight: 16
}
TransportBar { TransportBar {
Layout.fillWidth: true Layout.fillWidth: true
visible: streamController.mode !== "live" || trendChart.hasData
height: visible ? implicitHeight : 0
} }
} }
// Readout panel — bordered card // Readout panel — floating wrapper box
Rectangle { Rectangle {
Layout.preferredWidth: 220 Layout.preferredWidth: 220
Layout.fillHeight: true Layout.fillHeight: true
color: Theme.cardBackground color: "transparent"
border.color: Theme.cardBorder border.color: "transparent"
border.width: 1 border.width: 0
radius: Theme.radiusMd
clip: true clip: true
ReadoutPanel { ReadoutPanel {
id: readoutPanel id: readoutPanel
anchors.fill: parent 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 QtQuick.Layouts
import ISC import ISC
// ===== Readout Panel =====
// Right-rail panel with three bordered sections: READOUT, DISPLAY, THRESHOLDS.
// Cards match the left-rail SOURCE / CONNECTION FLOW style.
ColumnLayout { ColumnLayout {
spacing: 0 spacing: 6
property var s: streamController.stats property var s: streamController.stats
property alias showLabels: labelsToggle.checked property alias showLabels: labelsToggle.checked
property alias heatmapBlend: heatmapSlider.value property alias heatmapBlend: heatmapSlider.value
@@ -21,13 +24,23 @@ ColumnLayout {
border.width: Theme.borderThin border.width: Theme.borderThin
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
Behavior on border.color {
ColorAnimation { duration: Theme.durationFast }
}
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: "✓" text: "✓"
font.pixelSize: 13 font.pixelSize: Theme.fontSm
font.bold: true font.bold: true
color: Theme.panelBackground color: Theme.panelBackground
visible: toggle.checked opacity: toggle.checked ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: Theme.durationFast }
}
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
} }
@@ -36,150 +49,290 @@ ColumnLayout {
contentItem: Text { contentItem: Text {
text: toggle.text text: toggle.text
color: Theme.headingColor color: Theme.headingColor
font.family: Theme.uiFontFamily
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: toggle.indicator.width + toggle.spacing 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 { Label {
text: "READOUT" id: badgeLabel
anchors.centerIn: parent
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.font2xs
font.letterSpacing: 1.8
font.weight: Font.Medium font.weight: Font.Medium
topPadding: 6 }
bottomPadding: 8
} }
Rectangle { Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: statsLayout.implicitHeight + 20 implicitHeight: readoutCard.implicitHeight + 24
color: Theme.subtleSectionBackground color: Theme.sidePanelBackground
radius: Theme.radiusSm radius: Theme.sidePanelRadius
border.color: Theme.cardBorder border.color: Theme.sideBorder
border.width: 1 border.width: 1
ColumnLayout { ColumnLayout {
id: statsLayout id: readoutCard
anchors { fill: parent; margins: 10 } anchors {
spacing: 8 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 // Min Temp Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
RowLayout { RowLayout {
spacing: 4 spacing: 4
Label { Label {
text: s.min !== undefined ? s.min : "—" text: s.min !== undefined ? s.min : "—"
color: Theme.sensorLow color: Theme.sensorLow
font.pixelSize: 13; font.weight: Font.Bold font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
} }
Label { Label {
text: s.minIndex !== undefined ? "#" + s.minIndex : "" visible: (s.minIndex !== undefined) && s.minIndex >= 0
color: Theme.bodyColor text: "#" + (s.minIndex !== undefined ? s.minIndex : "")
font.pixelSize: 10 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 // Max Temp Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
RowLayout { RowLayout {
spacing: 4 spacing: 4
Label { Label {
text: s.max !== undefined ? s.max : "—" text: s.max !== undefined ? s.max : "—"
color: Theme.sensorHigh color: Theme.sensorHigh
font.pixelSize: 13; font.weight: Font.Bold font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
} }
Label { Label {
text: s.maxIndex !== undefined ? "#" + s.maxIndex : "" visible: (s.maxIndex !== undefined) && s.maxIndex >= 0
color: Theme.bodyColor text: "#" + (s.maxIndex !== undefined ? s.maxIndex : "")
font.pixelSize: 10 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 // Differential Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
Label { Label {
text: s.diff !== undefined ? s.diff : "—" text: s.diff !== undefined ? s.diff : "—"
color: Theme.headingColor color: Theme.isDarkMode ? "#A78BFA" : "#8B5CF6"
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
}
// Average Row // Average Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
Label { Label {
text: s.avg !== undefined ? s.avg : "—" text: s.avg !== undefined ? s.avg : "—"
color: Theme.headingColor 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 // Sigma Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
Label { Label {
text: s.sigma !== undefined ? s.sigma : "—" text: s.sigma !== undefined ? s.sigma : "—"
color: Theme.headingColor 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 // 3-Sigma Row
RowLayout { RowLayout {
Layout.fillWidth: true 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 } Item { Layout.fillWidth: true }
Label { Label {
text: s.threeSigma !== undefined ? s.threeSigma : "—" text: s.threeSigma !== undefined ? s.threeSigma : "—"
color: Theme.bodyColor color: Theme.headingColor
font.pixelSize: 12; font.weight: Font.Medium font.family: Theme.codeFontFamily
font.pixelSize: Theme.fontMd
font.bold: true
} }
} }
} }
} }
Item { height: 12 } Rectangle {
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } Layout.fillWidth: true
Item { height: 12 } 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 { Label {
text: "DISPLAY" text: "DISPLAY"
color: Theme.bodyColor color: Theme.sideMutedText
font.pixelSize: 10 font.family: Theme.uiFontFamily
font.letterSpacing: 1.8 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.letterSpacing: 1.5
topPadding: 6 font.bold: true
bottomPadding: 8 Layout.bottomMargin: 4
} }
PanelCheckBox { PanelCheckBox {
id: thicknessToggle id: thicknessToggle
text: "Show Thickness" text: "Show Thickness"
checked: false checked: false
font.pixelSize: 11 font.pixelSize: Theme.fontXs
onCheckedChanged: waferMapItem.showThickness = checked onCheckedChanged: waferMapItem.showThickness = checked
} }
@@ -187,61 +340,104 @@ ColumnLayout {
id: labelsToggle id: labelsToggle
text: "Labels" text: "Labels"
checked: true checked: true
font.pixelSize: 11 font.pixelSize: Theme.fontXs
} }
PanelCheckBox { PanelCheckBox {
id: clusterAverageToggle id: clusterAverageToggle
text: "Average Clusters" text: "Average Clusters"
checked: streamController.clusterAveragingEnabled checked: streamController.clusterAveragingEnabled
font.pixelSize: 11 font.pixelSize: Theme.fontXs
onCheckedChanged: streamController.clusterAveragingEnabled = checked onCheckedChanged: streamController.clusterAveragingEnabled = checked
} }
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 6 spacing: 6
Label { Label {
text: "Heatmap" text: "Heatmap"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
Layout.preferredWidth: 52 Layout.preferredWidth: 52
} }
Slider { Slider {
id: heatmapSlider id: heatmapSlider
from: 0; to: 1; value: 0 from: 0
to: 1
value: 0
Layout.fillWidth: true Layout.fillWidth: true
ToolTip.visible: hovered ToolTip.visible: hovered
ToolTip.text: Math.round(value * 100) + "%" 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 {
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder } Layout.fillWidth: true
Item { height: 12 } 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 { Label {
text: "THRESHOLDS" text: "THRESHOLDS"
color: Theme.bodyColor color: Theme.sideMutedText
font.pixelSize: 10 font.family: Theme.uiFontFamily
font.letterSpacing: 1.8 font.pixelSize: Theme.fontXs
font.weight: Font.Medium font.letterSpacing: 1.5
topPadding: 6 font.bold: true
bottomPadding: 8 Layout.bottomMargin: 4
} }
Label { Label {
text: "Set Point (°C)" text: "Set Point (°C)"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
bottomPadding: 3
} }
TextField { TextField {
id: spField id: spField
Layout.fillWidth: true Layout.fillWidth: true
text: "149.0" text: "149.0"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder placeholderTextColor: Theme.fieldPlaceholder
@@ -252,23 +448,26 @@ ColumnLayout {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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() onEditingFinished: pushThresholds()
} }
Item { height: 10 }
Label { Label {
text: "Margin (±°C)" text: "Margin (±°C)"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 11 font.pixelSize: Theme.fontXs
bottomPadding: 3
} }
TextField { TextField {
id: mgField id: mgField
Layout.fillWidth: true Layout.fillWidth: true
text: "1.0" text: "1.0"
font.pixelSize: 12 font.pixelSize: Theme.fontSm
inputMethodHints: Qt.ImhFormattedNumbersOnly inputMethodHints: Qt.ImhFormattedNumbersOnly
color: Theme.fieldText color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder placeholderTextColor: Theme.fieldPlaceholder
@@ -279,25 +478,33 @@ ColumnLayout {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: mgField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: mgField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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() onEditingFinished: pushThresholds()
} }
Item { height: 10 }
PanelCheckBox { PanelCheckBox {
id: autoCheck id: autoCheck
text: "Auto range (mean ± 1σ)" text: "Auto range (mean ± 1σ)"
checked: true checked: true
font.pixelSize: 11 font.pixelSize: Theme.fontXs
onCheckedChanged: pushThresholds() onCheckedChanged: pushThresholds()
} }
}
}
Item { Layout.fillHeight: true } Item {
Layout.fillHeight: true
}
function pushThresholds() { function pushThresholds() {
var sp = parseFloat(spField.text) || 149.0 var sp = parseFloat(spField.text) || 149.0;
var mg = parseFloat(mgField.text) || 1.0 var mg = parseFloat(mgField.text) || 1.0;
streamController.setThresholds(sp, mg, autoCheck.checked) streamController.setThresholds(sp, mg, autoCheck.checked);
} }
} }
@@ -17,14 +17,15 @@ Dialog {
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0 property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
function openFor(index) { function openFor(index) {
sensorIndex = index sensorIndex = index;
var dot = streamController.sensorDots[index] var dot = streamController.sensorDots[index];
if (!dot) return if (!dot)
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1) return;
currentValue = dot.value sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1);
valueField.text = "" currentValue = dot.value;
offsetField.text = "" valueField.text = "";
open() offsetField.text = "";
open();
} }
background: Rectangle { background: Rectangle {
@@ -42,11 +43,10 @@ Dialog {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
Label { Label {
text: "Sensor #" + (root.sensorIndex + 1) + text: "Sensor #" + (root.sensorIndex + 1) + (root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
color: Theme.headingColor color: Theme.headingColor
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
Layout.fillWidth: true Layout.fillWidth: true
} }
Rectangle { Rectangle {
@@ -61,7 +61,7 @@ Dialog {
anchors.centerIn: parent anchors.centerIn: parent
text: "OVERRIDE" text: "OVERRIDE"
color: "white" color: "white"
font.pixelSize: 10 font.pixelSize: Theme.fontXs
font.bold: true font.bold: true
font.letterSpacing: 1 font.letterSpacing: 1
} }
@@ -71,13 +71,21 @@ Dialog {
Label { Label {
text: "Live: " + root.currentValue.toFixed(2) text: "Live: " + root.currentValue.toFixed(2)
color: Theme.bodyColor 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 ───────────────────────────────────────── // ── 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 { TextField {
id: valueField id: valueField
Layout.fillWidth: true Layout.fillWidth: true
@@ -92,11 +100,21 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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 ────────────────────────────────────────── // ── 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 { TextField {
id: offsetField id: offsetField
Layout.fillWidth: true Layout.fillWidth: true
@@ -111,10 +129,20 @@ Dialog {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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 ─────────────────────────────────────────────── // ── buttons ───────────────────────────────────────────────
RowLayout { RowLayout {
@@ -140,20 +168,20 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: { onClicked: {
if (valueField.text !== "") { if (valueField.text !== "") {
var v = parseFloat(valueField.text) var v = parseFloat(valueField.text);
if (!isNaN(v)) if (!isNaN(v))
streamController.replaceSensor(root.sensorIndex, v) streamController.replaceSensor(root.sensorIndex, v);
} }
if (offsetField.text !== "") { if (offsetField.text !== "") {
var d = parseFloat(offsetField.text) var d = parseFloat(offsetField.text);
if (!isNaN(d)) if (!isNaN(d))
streamController.offsetSensor(root.sensorIndex, d) streamController.offsetSensor(root.sensorIndex, d);
} }
root.close() root.close();
} }
} }
Button { Button {
@@ -175,11 +203,11 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: { onClicked: {
streamController.clearSensorEdit(root.sensorIndex) streamController.clearSensorEdit(root.sensorIndex);
root.close() root.close();
} }
} }
Button { Button {
@@ -198,7 +226,7 @@ Dialog {
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.bold: true font.bold: true
font.pixelSize: 13 font.pixelSize: Theme.fontSm
} }
onClicked: root.close() onClicked: root.close()
} }
+121 -77
View File
@@ -1,6 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls.impl
import ISC import ISC
import ".." import ".."
@@ -14,10 +15,11 @@ ColumnLayout {
Label { Label {
text: "SOURCE" text: "SOURCE"
color: Theme.bodyColor color: Theme.sideMutedText
font.pixelSize: 10 font.family: Theme.uiFontFamily
font.letterSpacing: 1.8 font.pixelSize: Theme.fontSm
font.weight: Font.Medium font.letterSpacing: 1.5
font.bold: true
topPadding: 6 topPadding: 6
bottomPadding: 8 bottomPadding: 8
Layout.fillWidth: true Layout.fillWidth: true
@@ -26,51 +28,79 @@ ColumnLayout {
Button { Button {
id: editBtn id: editBtn
text: "✎" implicitWidth: 28
implicitWidth: 26 implicitHeight: 28
implicitHeight: 26
hoverEnabled: true hoverEnabled: true
font.pixelSize: 14
background: Rectangle { background: Rectangle {
color: editBtn.pressed ? Theme.buttonPressed color: editBtn.pressed ? Theme.buttonPressed : editBtn.hovered ? Theme.buttonNeutralHover : "transparent"
: editBtn.hovered ? Theme.buttonNeutralHover
: "transparent"
radius: Theme.radiusSm radius: Theme.radiusSm
} }
contentItem: Text { contentItem: IconImage {
text: parent.text source: "../icons/edit.svg"
sourceSize.width: 16
sourceSize.height: 16
color: Theme.bodyColor color: Theme.bodyColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
} }
onClicked: { onClicked: {
file_browser.refreshFiles() file_browser.refreshFiles();
csvEditorDialog.open() 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 { Button {
id: refreshBtn id: refreshBtn
text: "⟳" implicitWidth: 28
implicitWidth: 26 implicitHeight: 28
implicitHeight: 26
hoverEnabled: true hoverEnabled: true
font.pixelSize: 14
background: Rectangle { background: Rectangle {
color: refreshBtn.pressed ? Theme.buttonPressed color: refreshBtn.pressed ? Theme.buttonPressed : refreshBtn.hovered ? Theme.buttonNeutralHover : "transparent"
: refreshBtn.hovered ? Theme.buttonNeutralHover
: "transparent"
radius: Theme.radiusSm radius: Theme.radiusSm
} }
contentItem: Text { contentItem: IconImage {
text: parent.text source: "../icons/refresh.svg"
sourceSize.width: 16
sourceSize.height: 16
color: Theme.bodyColor color: Theme.bodyColor
font: parent.font
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
} }
onClicked: file_browser.refreshFiles() 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 id: dirBtn
Layout.fillWidth: true Layout.fillWidth: true
hoverEnabled: true hoverEnabled: true
implicitHeight: 32 implicitHeight: 36
leftPadding: 8 leftPadding: 8
rightPadding: 8 rightPadding: 8
background: Rectangle { background: Rectangle {
radius: Theme.radiusSm radius: Theme.radiusSm
color: dirBtn.pressed ? Theme.buttonNeutralPressed color: dirBtn.pressed ? Theme.buttonNeutralPressed : dirBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
: dirBtn.hovered ? Theme.buttonNeutralHover
: Theme.buttonNeutralBackground
border.width: Theme.borderThin border.width: Theme.borderThin
border.color: Theme.fieldBorder border.color: Theme.fieldBorder
} }
@@ -95,12 +123,12 @@ ColumnLayout {
Label { Label {
text: "▤" text: "▤"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 12 font.pixelSize: Theme.fontSm
} }
Label { Label {
text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 11 font.pixelSize: Theme.fontSm
elide: Text.ElideMiddle elide: Text.ElideMiddle
Layout.fillWidth: true Layout.fillWidth: true
} }
@@ -113,7 +141,7 @@ ColumnLayout {
id: filter id: filter
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: "Filter…" placeholderText: "Filter…"
font.pixelSize: 11 font.pixelSize: Theme.fontSm
color: Theme.fieldText color: Theme.fieldText
placeholderTextColor: Theme.fieldPlaceholder placeholderTextColor: Theme.fieldPlaceholder
selectedTextColor: Theme.fieldBackground selectedTextColor: Theme.fieldBackground
@@ -123,6 +151,12 @@ ColumnLayout {
color: Theme.fieldBackground color: Theme.fieldBackground
border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin
border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder 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 focusPolicy: Qt.NoFocus
readonly property bool matchesFilter: { readonly property bool matchesFilter: {
if (filter.text === "") return true if (filter.text === "")
var q = filter.text.toLowerCase() return true;
return (modelData.baseName + modelData.waferType + modelData.date) var q = filter.text.toLowerCase();
.toLowerCase().indexOf(q) >= 0 return (modelData.baseName + modelData.waferType + modelData.date).toLowerCase().indexOf(q) >= 0;
} }
// Active only in review mode; no highlight during live streaming // Active only in review mode; no highlight during live streaming
readonly property bool isActive: readonly property bool isActive: streamController.mode === "review" && streamController.loadedFile !== "" && modelData.fileName === streamController.loadedFile
streamController.mode === "review" &&
streamController.loadedFile !== "" &&
modelData.fileName === streamController.loadedFile
visible: matchesFilter visible: matchesFilter
height: matchesFilter ? implicitHeight : 0 height: matchesFilter ? implicitHeight : 0
@@ -164,10 +195,11 @@ ColumnLayout {
onClicked: streamController.loadFile(modelData.fileName) onClicked: streamController.loadFile(modelData.fileName)
background: Rectangle { background: Rectangle {
color: fileItem.isActive color: fileItem.isActive ? Qt.rgba(1, 1, 1, 0.06) : fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
? Qt.rgba(1, 1, 1, 0.06)
: fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
radius: Theme.radiusSm radius: Theme.radiusSm
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
// Left accent bar — shown only when active // Left accent bar — shown only when active
Rectangle { Rectangle {
@@ -175,15 +207,21 @@ ColumnLayout {
width: 3 width: 3
radius: 1.5 radius: 1.5
anchors { anchors {
top: parent.top; bottom: parent.bottom; left: parent.left top: parent.top
topMargin: 5; bottomMargin: 5 bottom: parent.bottom
left: parent.left
topMargin: 5
bottomMargin: 5
} }
color: { color: {
var t = modelData.waferType var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P") return "#3B82F6" if (t === "A" || t === "E" || t === "P")
if (t === "B" || t === "C" || t === "D") return "#10B981" return "#3B82F6";
if (t === "Z") return "#8B5CF6" if (t === "B" || t === "C" || t === "D")
return Theme.headingColor return "#10B981";
if (t === "Z")
return "#8B5CF6";
return Theme.headingColor;
} }
} }
} }
@@ -193,22 +231,26 @@ ColumnLayout {
// Wafer-type avatar (circle) // Wafer-type avatar (circle)
Rectangle { Rectangle {
implicitWidth: 28; implicitHeight: 28 implicitWidth: 28
implicitHeight: 28
Layout.preferredWidth: 28 Layout.preferredWidth: 28
Layout.preferredHeight: 28 Layout.preferredHeight: 28
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
radius: 14 radius: 14
color: { color: {
var t = modelData.waferType var t = modelData.waferType;
if (t === "A" || t === "E" || t === "P") return "#1D4ED8" if (t === "A" || t === "E" || t === "P")
if (t === "B" || t === "C" || t === "D") return "#065F46" return "#1D4ED8";
if (t === "Z") return "#7C3AED" if (t === "B" || t === "C" || t === "D")
return "#374151" return "#065F46";
if (t === "Z")
return "#7C3AED";
return "#374151";
} }
Label { Label {
anchors.centerIn: parent anchors.centerIn: parent
text: modelData.waferType || "?" text: modelData.waferType || "?"
font.pixelSize: 12 font.pixelSize: Theme.fontMd
font.weight: Font.Bold font.weight: Font.Bold
color: "#FFFFFF" color: "#FFFFFF"
} }
@@ -223,7 +265,7 @@ ColumnLayout {
Label { Label {
text: modelData.serialNumber ? (modelData.waferType + modelData.serialNumber) : modelData.baseName text: modelData.serialNumber ? (modelData.waferType + modelData.serialNumber) : modelData.baseName
color: Theme.headingColor color: Theme.headingColor
font.pixelSize: 13 font.pixelSize: Theme.fontMd
font.weight: Font.Bold font.weight: Font.Bold
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
@@ -235,24 +277,24 @@ ColumnLayout {
Layout.fillWidth: true Layout.fillWidth: true
Label { Label {
text: { text: {
var d = modelData.date || "" var d = modelData.date || "";
return d.length >= 10 ? d.substring(0, 10) : (d || "—") return d.length >= 10 ? d.substring(0, 10) : (d || "—");
} }
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontSm
} }
Label { Label {
visible: (modelData.timeStr || "") !== "" visible: (modelData.timeStr || "") !== ""
text: "·" text: "·"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontSm
opacity: 0.45 opacity: 0.45
} }
Label { Label {
visible: (modelData.timeStr || "") !== "" visible: (modelData.timeStr || "") !== ""
text: modelData.timeStr || "" text: modelData.timeStr || ""
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontSm
opacity: 0.65 opacity: 0.65
} }
} }
@@ -260,18 +302,20 @@ ColumnLayout {
// Tertiary: edited metadata (chamber · notes) if any // Tertiary: edited metadata (chamber · notes) if any
Label { Label {
visible: { visible: {
var c = modelData.chamber || "" var c = modelData.chamber || "";
var n = modelData.notes || "" var n = modelData.notes || "";
return c !== "" || n !== "" return c !== "" || n !== "";
} }
text: { text: {
var parts = [] var parts = [];
if (modelData.chamber) parts.push(modelData.chamber) if (modelData.chamber)
if (modelData.notes) parts.push(modelData.notes) parts.push(modelData.chamber);
return parts.join(" · ") if (modelData.notes)
parts.push(modelData.notes);
return parts.join(" · ");
} }
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 9 font.pixelSize: Theme.fontXs
opacity: 0.7 opacity: 0.7
elide: Text.ElideRight elide: Text.ElideRight
Layout.fillWidth: true Layout.fillWidth: true
@@ -285,7 +329,7 @@ ColumnLayout {
Label { Label {
text: file_browser.files.length + " files · CSV only" text: file_browser.files.length + " files · CSV only"
color: Theme.bodyColor color: Theme.bodyColor
font.pixelSize: 10 font.pixelSize: Theme.fontSm
} }
SelectFileDialog { SelectFileDialog {
+296 -60
View File
@@ -3,101 +3,337 @@ import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ISC import ISC
RowLayout { // ===== Control Bar =====
spacing: 2 // 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 { // Hide entirely when in review mode with no file loaded
implicitWidth: 44 readonly property bool hasContent: {
implicitHeight: 32 if (streamController.mode === "live") return true;
hoverEnabled: true return streamController.loadedFile !== "";
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
} }
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 { 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 anchors.centerIn: parent
text: "Frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal implicitWidth: liveRow.implicitWidth + 48
color: Theme.headingColor implicitHeight: 68
font.pixelSize: 11 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 font.weight: Font.Medium
} }
// Separator
Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: 1
height: 36
color: Theme.sideBorder
} }
// Left fill — centers the button cluster // Errors counter
Item { Layout.fillWidth: true } 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) } // Separator
TBtn { text: "⏹"; onClicked: streamController.stop() } Rectangle {
TBtn { text: "⏸"; onClicked: streamController.pause() } anchors.verticalCenter: parent.verticalCenter
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset width: 1
TBtn { height: 36
text: "▶" color: Theme.sideBorder
implicitWidth: 52 }
// 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 { contentItem: Text {
text: parent.text text: parent.text
color: Theme.headingColor color: Theme.headingColor
font: parent.font font: parent.font
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: 2
} }
onClicked: streamController.play() onClicked: streamController.stopStream()
}
}
}
} }
TBtn { text: "⏭"; onClicked: streamController.step(1) }
// Right fill — balances centering // ── REVIEW MODE ──────────────────────────────────────────────
Item { Layout.fillWidth: true } 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 { Button {
id: speedBtn id: speedBtn
property var speeds: [1, 2, 5] anchors.verticalCenter: parent.verticalCenter
property var speeds: [1, 2, 4]
property int idx: 0 property int idx: 0
implicitWidth: 44 implicitWidth: 72
implicitHeight: 32 implicitHeight: 48
hoverEnabled: true hoverEnabled: true
text: speeds[idx] + "×" text: speeds[idx] + "x"
font.pixelSize: 11 font.family: Theme.uiFontFamily
font.pixelSize: Theme.fontSm
font.weight: Font.Medium font.weight: Font.Medium
background: Rectangle { background: Rectangle {
color: parent.pressed ? Theme.transportButtonHover radius: 8
: parent.hovered ? Theme.transportButtonBg color: speedBtn.pressed ? Theme.transportButtonHover
: Theme.transportBackground : Theme.transportButtonBg
radius: Theme.radiusSm border.color: Theme.sideBorder
border.width: 1
Behavior on color {
ColorAnimation { duration: Theme.durationFast }
}
} }
contentItem: Text { contentItem: Text {
text: parent.text text: parent.text
color: Theme.bodyColor color: Theme.headingColor
font: parent.font font: parent.font
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
onClicked: { onClicked: {
idx = (idx + 1) % speeds.length idx = (idx + 1) % speeds.length;
streamController.setSpeed(speeds[idx]) streamController.setSpeed(speeds[idx]);
}
}
}
} }
} }
} }
+8 -10
View File
@@ -3,7 +3,6 @@ import QtQuick.Controls
import ISC import ISC
import ISC.Wafer import ISC.Wafer
Item { Item {
id: root id: root
property real blend: 0.0 property real blend: 0.0
@@ -29,21 +28,20 @@ Item {
highColor: Theme.sensorHigh highColor: Theme.sensorHigh
textColor: Theme.headingColor textColor: Theme.headingColor
TapHandler { TapHandler {
onTapped: (ev) => { onTapped: ev => {
var idx = map.which_marker(ev.position.x, ev.position.y) var idx = map.which_marker(ev.position.x, ev.position.y);
if (idx >= 0) replaceDialog.openFor(idx) if (idx >= 0)
replaceDialog.openFor(idx);
} }
} }
} }
ReplaceSensorDialog { id: replaceDialog} ReplaceSensorDialog {
id: replaceDialog
}
function exportImage(filePath) { 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 TransportBar 1.0 TransportBar.qml
WaferMapView 1.0 WaferMapView.qml WaferMapView 1.0 WaferMapView.qml
ReplaceSensorDialog 1.0 ReplaceSensorDialog.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: // Sections:
// 1. Mode flag // 1. Mode flag
// 2. Tone palette (raw tones; everything else derives from these) // 2. Typography
// 3. Surfaces (page / card / panel / workspace backgrounds) // 3. Motion
// 4. Borders // 4. Tone palette (raw tones; everything else derives from these)
// 5. Text (headings, body, panel titles) // 5. Surfaces (page / card / panel / workspace backgrounds)
// 6. Controls (fields, buttons, checkbox) // 6. Borders
// 7. Tracks (pill toggle + scrollbar) // 7. Text (headings, body, panel titles)
// 8. Tabs (footer tab bar) // 8. Controls (fields, buttons, checkbox)
// 9. Side rail // 9. Tracks (pill toggle + scrollbar)
// 10. Status (success / warning / error) // 10. Tabs (footer tab bar)
// 11. Geometry (radius / border width / spacing / sizing) // 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 { QtObject {
// ── 1. Mode ────────────────────────────────────────────────────────────── // ── 1. Mode ──────────────────────────────────────────────────────────────
property bool isDarkMode: true property bool isDarkMode: true
// ── 2. Tone palette (base values; consume via semantic tokens below) ───── // ── 2. Typography ──────────────────────────────────────────────────────
readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA" // pixelSize scale — map to closest token, tune globally
readonly property color tone150: isDarkMode ? "#161616" : "#F6F6F6" readonly property int font2xs: 9 // tiny metadata
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F0F0F0" readonly property int fontXs: 11 // captions, micro labels
readonly property color tone250: isDarkMode ? "#212121" : "#EAEAEA" readonly property int fontSm: 13 // sidebar labels, secondary
readonly property color tone300: isDarkMode ? "#282828" : "#E2E2E2" readonly property int fontMd: 14 // body, field values
readonly property color tone350: isDarkMode ? "#303030" : "#D6D6D6" readonly property int fontLg: 16 // section headers
readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111" readonly property int fontXl: 20 // panel / dialog titles
readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A" readonly property int font2xl: 24 // hero titles
readonly property color toneBorder: isDarkMode ? "#2E2E2E" : "#D4D4D4" 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 ────────────────────────────────────────────────────────── // ── 3. Surfaces ──────────────────────────────────────────────────────────
readonly property color pageBackground: tone100 readonly property color pageBackground: tone100
readonly property color cardBackground: tone200 readonly property color cardBackground: tone150
readonly property color panelBackground: tone200 readonly property color panelBackground: tone150
readonly property color workspaceBackground: tone200 readonly property color workspaceBackground: tone150
readonly property color responseBackground: tone200 readonly property color responseBackground: tone150
readonly property color subtleSectionBackground: tone300 readonly property color subtleSectionBackground: tone250
// ── 4. Borders ─────────────────────────────────────────────────────────── // ── 4. Borders ───────────────────────────────────────────────────────────
readonly property color cardBorder: toneBorder 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 responseBorder: toneBorder
readonly property color workspaceBorder: toneBorder readonly property color workspaceBorder: toneBorder
readonly property color innerFrameBorder: toneBorder readonly property color innerFrameBorder: toneBorder
readonly property color outerFrameBorder: isDarkMode ? "#383838" : "#CACACA" readonly property color outerFrameBorder: isDarkMode ? "#3A3A42" : "#C7C7C2"
readonly property color softBorder: isDarkMode ? "#252525" : "#E0E0E0" readonly property color softBorder: isDarkMode ? "#24242A" : "#E4E4E1"
// ── 5. Text ────────────────────────────────────────────────────────────── // ── 5. Text ──────────────────────────────────────────────────────────────
readonly property color headingColor: toneText readonly property color headingColor: toneText
@@ -60,53 +100,61 @@ QtObject {
// ── 6. Controls ────────────────────────────────────────────────────────── // ── 6. Controls ──────────────────────────────────────────────────────────
// Fields // Fields
readonly property color fieldBackground: tone100 readonly property color fieldBackground: isDarkMode ? "#101014" : "#FFFFFF"
readonly property color fieldText: toneText readonly property color fieldText: toneText
readonly property color fieldPlaceholder: toneMute readonly property color fieldPlaceholder: toneMute
readonly property color fieldBorder: toneBorder readonly property color fieldBorder: toneBorder
readonly property color fieldBorderFocus: toneText readonly property color fieldBorderFocus: themeAccent
// Neutral buttons // Neutral buttons
readonly property color buttonNeutralBackground: tone300 readonly property color buttonNeutralBackground: tone250
readonly property color buttonNeutralHover: isDarkMode ? "#303030" : "#DDDDDD" readonly property color buttonNeutralHover: tone300
readonly property color buttonNeutralPressed: tone100 readonly property color buttonNeutralPressed: isDarkMode ? "#0F0F13" : "#FFFFFF"
readonly property color buttonNeutralText: toneText readonly property color buttonNeutralText: toneText
readonly property color buttonPressed: tone300 readonly property color buttonPressed: tone300
readonly property color primaryAccent: toneText readonly property color primaryAccent: themeAccent
// Checkbox // Checkbox
readonly property color checkboxText: toneText readonly property color checkboxText: toneText
// ── 7. Tracks (pill toggle, scrollbar thumb) ───────────────────────────── // ── 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) ───────────────────────────────────────────── // ── 8. Tabs (footer tab bar) ─────────────────────────────────────────────
readonly property color tabBarBackground: tone200 readonly property color tabBarBackground: tone200
readonly property color tabBackground: "transparent" readonly property color tabBackground: "transparent"
readonly property color tabActiveBackground: isDarkMode ? "#333333" : "#FFFFFF" readonly property color tabActiveBackground: isDarkMode ? "#25252B" : "#FFFFFF"
readonly property color tabHoverBackground: tone300 readonly property color tabHoverBackground: tone250
readonly property color tabBorder: "transparent" readonly property color tabBorder: "transparent"
readonly property color tabText: toneMute readonly property color tabText: toneMute
readonly property color tabActiveText: toneText readonly property color tabActiveText: toneText
// ── 9. Side rail ───────────────────────────────────────────────────────── // ── 9. Side rail ─────────────────────────────────────────────────────────
readonly property color sideRailBackground: tone200 readonly property color sideRailBackground: tone150
readonly property color sideActiveBackground: tone300 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 ──────────────────────────────────── // ── 10a. Transport / toolbar surfaces ────────────────────────────────────
readonly property color transportBackground: isDarkMode ? "#0D0D0D" : "#E8E8E8" readonly property color transportBackground: isDarkMode ? "#101014" : "#EFEFED"
readonly property color transportButtonBg: isDarkMode ? "#2A2A2A" : "#D4D4D4" readonly property color transportButtonBg: tone250
readonly property color transportButtonHover: isDarkMode ? "#3A3A3A" : "#C4C4C4" readonly property color transportButtonHover: tone300
readonly property color liveColor: isDarkMode ? "#22C55E" : "#16A34A" readonly property color liveColor: themeAdded
readonly property color recordColor: isDarkMode ? "#EF4444" : "#DC2626" readonly property color recordColor: themeRemoved
// ── 10. Status ─────────────────────────────────────────────────────────── // ── 10. Status ───────────────────────────────────────────────────────────
readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44" readonly property color statusSuccessColor: themeAdded
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18" readonly property color statusWarningColor: isDarkMode ? "#E8A817" : "#B8860B" // amber/yellow, separate from accent
readonly property color statusErrorColor: isDarkMode ? "#FF6B6B" : "#D64545" readonly property color statusErrorColor: themeRemoved
// -- 10b. Sensor bands (wafer map dots) // -- 10b. Sensor bands (wafer map dots)
readonly property color sensorInRange: statusSuccessColor 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 sensorHigh: statusErrorColor
readonly property color sensorLow: isDarkMode ? "#5B9DF5" : "#2F6FE0" readonly property color sensorLow: themeSkill
readonly property color waferRingColor: toneBorder readonly property color waferRingColor: toneBorder
readonly property color waferAxisColor: softBorder readonly property color waferAxisColor: softBorder
@@ -124,16 +172,20 @@ QtObject {
readonly property int rightPaneGap: 6 readonly property int rightPaneGap: 6
readonly property int panelPadding: 12 readonly property int panelPadding: 12
// Side rail layout // Side rail layout
readonly property int sideRailWidth: 190 readonly property int sideRailWidth: 280
readonly property int sideRailSpacing: 14 readonly property int sideRailMargin: 16
readonly property int sideButtonHeight: 146 readonly property int sideRailSpacing: 16
readonly property int sideButtonMinHeight: 88 readonly property int sideButtonHeight: 44
readonly property int sidePanelRadius: 10
readonly property int sideFooterConnection: 100
readonly property int sideFooterUtility: 46
// Tab bar layout // Tab bar layout
readonly property int tabBarHeight: 34 readonly property int tabBarHeight: 34
readonly property int sidePillHeight: 54
readonly property int sidePillButtonHeight: 36
readonly property int tabBarPadding: 6 readonly property int tabBarPadding: 6
readonly property int tabSpacing: 2 readonly property int tabSpacing: 2
readonly property int tabRadius: 8 readonly property int tabRadius: 8
readonly property int tabFontSize: 12
readonly property int tabButtonMinWidth: 80 readonly property int tabButtonMinWidth: 80
// Settings page layout // Settings page layout
readonly property int settingsPanelMaxWidth: 760 readonly property int settingsPanelMaxWidth: 760
+1
View File
@@ -4,6 +4,7 @@ module ISC
# ===== Root Components ===== # ===== Root Components =====
Main 1.0 Main.qml Main 1.0 Main.qml
HomePage 1.0 HomePage.qml HomePage 1.0 HomePage.qml
AboutDialog 1.0 AboutDialog.qml
# ===== Singleton Theme ===== # ===== Singleton Theme =====
singleton Theme 1.0 Theme.qml singleton Theme 1.0 Theme.qml
@@ -37,10 +37,12 @@ class SessionController(QObject):
loadedFileChanged = Signal() loadedFileChanged = Signal()
clusterAveragingEnabledChanged = Signal() clusterAveragingEnabledChanged = Signal()
settingsChanged = Signal() # emitted when session state changes that should be persisted 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 # trend: per-frame avg for live graph
trendData = Signal(str) # JSON array of floats trendData = Signal(str) # JSON array of floats
# private: marshal a worker-thread frame onto the main thread # private: marshal a worker-thread frame onto the main thread
_liveFrame = Signal(object) # Frame _liveFrame = Signal(object) # Frame
_liveError = Signal() # worker-thread error ping
def __init__(self, parent: QObject | None = None, def __init__(self, parent: QObject | None = None,
settings: dict | None = None) -> None: settings: dict | None = None) -> None:
@@ -72,6 +74,7 @@ class SessionController(QObject):
self._dirty = False self._dirty = False
self._liveFrame.connect(self._on_live_frame, Qt.ConnectionType.QueuedConnection) 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._sensor_editor = SensorEditor()
self._last_raw_frame: Frame | None = None self._last_raw_frame: Frame | None = None
@@ -79,6 +82,8 @@ class SessionController(QObject):
self._cluster_averaging_enabled = False self._cluster_averaging_enabled = False
self._active_clusters: list[list[int]] = [] self._active_clusters: list[list[int]] = []
self._stats_tracker = ReplayStatsTracker() self._stats_tracker = ReplayStatsTracker()
self._received_count: int = 0
self._error_count: int = 0
# Restore persisted state if available # Restore persisted state if available
self._load_settings(settings or {}) self._load_settings(settings or {})
@@ -105,6 +110,9 @@ class SessionController(QObject):
@Property(int, notify=frameUpdated) @Property(int, notify=frameUpdated)
def frameTotal(self) -> int: return self._player.total 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) @Property(str, notify=stateChanged)
def state(self) -> str: def state(self) -> str:
if self._last: if self._last:
@@ -210,6 +218,14 @@ class SessionController(QObject):
self.clusterAveragingEnabledChanged.emit() self.clusterAveragingEnabledChanged.emit()
self._reprocess_current() 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 ---- # ---- mode + thresholds ----
@Slot(str) @Slot(str)
@slot_error_boundary @slot_error_boundary
@@ -292,11 +308,13 @@ class SessionController(QObject):
@slot_error_boundary @slot_error_boundary
def play(self) -> None: def play(self) -> None:
self._play_timer.start(self._next_interval_ms()) self._play_timer.start(self._next_interval_ms())
self.stateChanged.emit()
@Slot() @Slot()
@slot_error_boundary @slot_error_boundary
def pause(self) -> None: def pause(self) -> None:
self._play_timer.stop() self._play_timer.stop()
self.stateChanged.emit()
@Slot() @Slot()
@slot_error_boundary @slot_error_boundary
@@ -304,6 +322,7 @@ class SessionController(QObject):
self._play_timer.stop() self._play_timer.stop()
self._player.seek(0) self._player.seek(0)
self._emit_current() self._emit_current()
self.stateChanged.emit()
@Slot(int) @Slot(int)
@slot_error_boundary @slot_error_boundary
@@ -311,6 +330,7 @@ class SessionController(QObject):
self._play_timer.stop() self._play_timer.stop()
self._player.step(delta) self._player.step(delta)
self._emit_current() self._emit_current()
self.stateChanged.emit()
@Slot(int) @Slot(int)
@slot_error_boundary @slot_error_boundary
@@ -335,6 +355,7 @@ class SessionController(QObject):
def _advance(self) -> None: def _advance(self) -> None:
if self._player.at_end: if self._player.at_end:
self._play_timer.stop() self._play_timer.stop()
self.stateChanged.emit()
return return
self._player.step(1) self._player.step(1)
self._emit_current() self._emit_current()
@@ -419,6 +440,9 @@ class SessionController(QObject):
self._stats_tracker.reset() self._stats_tracker.reset()
self._last = None self._last = None
self._last_raw_frame = None self._last_raw_frame = None
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
transport = SerialPort.open_port(port, timeout=1) transport = SerialPort.open_port(port, timeout=1)
# Send 'D2' command padded to 512 bytes to start the stream # Send 'D2' command padded to 512 bytes to start the stream
@@ -427,6 +451,7 @@ class SessionController(QObject):
def on_error(exc: Exception): def on_error(exc: Exception):
log.error("Live stream error: %s", exc) log.error("Live stream error: %s", exc)
self._liveError.emit()
self._reader = StreamReader( self._reader = StreamReader(
transport, parse_binary_frame, transport, parse_binary_frame,
@@ -444,6 +469,9 @@ class SessionController(QObject):
def stopStream(self) -> None: def stopStream(self) -> None:
self._repaint_timer.stop() self._repaint_timer.stop()
self._trend_timer.stop() self._trend_timer.stop()
self._received_count = 0
self._error_count = 0
self.liveStatsChanged.emit()
if self._reader: if self._reader:
transport = self._reader._transport transport = self._reader._transport
self._reader.stop() self._reader.stop()
@@ -465,6 +493,8 @@ class SessionController(QObject):
@slot_error_boundary @slot_error_boundary
def _on_live_frame(self, frame: Frame) -> None: def _on_live_frame(self, frame: Frame) -> None:
import json import json
self._received_count += 1
self.liveStatsChanged.emit()
self._stats_tracker.track(frame.seq, frame.values) self._stats_tracker.track(frame.seq, frame.values)
# Main thread (queued). Record raw, process edited; mark dirty for repaint. # Main thread (queued). Record raw, process edited; mark dirty for repaint.
self._last_raw_frame = frame self._last_raw_frame = frame
@@ -498,6 +528,11 @@ class SessionController(QObject):
self.frameUpdated.emit() self.frameUpdated.emit()
self.stateChanged.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 ---- # ---- recording ----
@Slot(str, str) @Slot(str, str)
@slot_error_boundary @slot_error_boundary