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