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