feat: Add Wafer Map tab and associated components
- Introduced a new Wafer Map tab in the HomePage.qml, which loads WaferMapTab.qml when selected. - Created WaferMapTab.qml to display wafer map with live data and controls. - Added ReadoutPanel.qml for displaying sensor statistics and thresholds. - Implemented SourcePanel.qml for file selection and filtering. - Developed TransportBar.qml for playback controls. - Added WaferMapView.qml to visualize wafer data with interactive features. - Created ReplaceSensorDialog.qml for sensor value overrides. - Updated Theme.qml for new color schemes and UI adjustments. - Modified qmldir files to include new components in the ISC.Tabs and ISC.Tabs.components modules.
This commit is contained in:
@@ -158,11 +158,17 @@ Rectangle {
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data"
|
||||
visible: parent.tabName !== "Settings" && parent.tabName !== "Status" && parent.tabName !== "Data" && parent.tabName !== "Wafer Map"
|
||||
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" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
import ISC.Tabs.components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
// Live elapsed-time counter
|
||||
property int _liveSecs: 0
|
||||
Timer {
|
||||
id: liveTimer
|
||||
interval: 1000
|
||||
repeat: true
|
||||
running: streamController.mode === "live" && streamController.state !== "idle"
|
||||
onTriggered: root._liveSecs++
|
||||
onRunningChanged: if (!running) root._liveSecs = 0
|
||||
}
|
||||
function fmtTime(s) {
|
||||
var m = Math.floor(s / 60)
|
||||
var ss = s % 60
|
||||
return (m < 10 ? "0" : "") + m + ":" + (ss < 10 ? "0" : "") + ss
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.panelPadding
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
// ── Toolbar ───────────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
|
||||
// Mode toggle
|
||||
TabBar {
|
||||
id: modeBar
|
||||
spacing: 2
|
||||
padding: 2
|
||||
background: Rectangle {
|
||||
color: Theme.subtleSectionBackground
|
||||
radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder
|
||||
border.width: Theme.borderThin
|
||||
}
|
||||
TabButton {
|
||||
text: "Live"
|
||||
implicitWidth: 58; implicitHeight: 28
|
||||
background: Rectangle {
|
||||
color: parent.checked ? Theme.buttonNeutralBackground : "transparent"
|
||||
radius: Theme.radiusSm - 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
TabButton {
|
||||
text: "Review"
|
||||
implicitWidth: 64; implicitHeight: 28
|
||||
background: Rectangle {
|
||||
color: parent.checked ? Theme.buttonNeutralBackground : "transparent"
|
||||
radius: Theme.radiusSm - 1
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: parent.checked ? Theme.headingColor : Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: parent.checked ? Font.Medium : Font.Normal
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
onCurrentIndexChanged:
|
||||
streamController.setMode(currentIndex === 0 ? "live" : "review")
|
||||
}
|
||||
|
||||
// Live source info: WiFi icon + port · serial
|
||||
RowLayout {
|
||||
visible: streamController.mode === "live"
|
||||
spacing: 5
|
||||
// WiFi icon (Unicode approximation; swap for SVG if available)
|
||||
Label {
|
||||
text: "◉"
|
||||
color: streamController.state !== "idle"
|
||||
? Theme.liveColor : Theme.bodyColor
|
||||
font.pixelSize: 13
|
||||
}
|
||||
Label {
|
||||
text: "Live stream"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// SET badge + avg temp
|
||||
Rectangle {
|
||||
visible: streamController.state === "set"
|
||||
height: 22; radius: Theme.radiusSm
|
||||
color: "transparent"
|
||||
border.color: Theme.liveColor
|
||||
border.width: 1
|
||||
implicitWidth: setLabel.implicitWidth + 16
|
||||
Label {
|
||||
id: setLabel
|
||||
anchors.centerIn: parent
|
||||
text: "SET " + (streamController.stats.avg !== undefined
|
||||
? streamController.stats.avg + " avg" : "")
|
||||
color: Theme.liveColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// Record indicator
|
||||
RowLayout {
|
||||
spacing: 5
|
||||
visible: streamController.recording
|
||||
Rectangle {
|
||||
width: 7; height: 7; radius: 4
|
||||
color: Theme.recordColor
|
||||
SequentialAnimation on opacity {
|
||||
running: streamController.recording
|
||||
loops: Animation.Infinite
|
||||
NumberAnimation { to: 0.2; duration: 600 }
|
||||
NumberAnimation { to: 1.0; duration: 600 }
|
||||
}
|
||||
}
|
||||
Label {
|
||||
text: "REC"
|
||||
color: Theme.recordColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// LIVE timer
|
||||
RowLayout {
|
||||
spacing: 5
|
||||
visible: streamController.mode === "live" && streamController.state !== "idle"
|
||||
Rectangle {
|
||||
width: 7; height: 7; radius: 4
|
||||
color: Theme.liveColor
|
||||
}
|
||||
Label {
|
||||
text: "LIVE " + root.fmtTime(root._liveSecs)
|
||||
color: Theme.liveColor
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 0.8
|
||||
}
|
||||
}
|
||||
|
||||
// IDLE label (review / not connected)
|
||||
Label {
|
||||
visible: streamController.mode === "review" || streamController.state === "idle"
|
||||
text: streamController.state.toUpperCase()
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
font.letterSpacing: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
// ── Body: 3 zones ─────────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
// Source panel — bordered card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
SourcePanel {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 8
|
||||
|
||||
WaferMapView {
|
||||
id: waferView
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
blend: readoutPanel.heatmapBlend
|
||||
showLabels: readoutPanel.showLabels
|
||||
}
|
||||
|
||||
TransportBar { Layout.fillWidth: true }
|
||||
}
|
||||
|
||||
// Readout panel — bordered card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: 220
|
||||
Layout.fillHeight: true
|
||||
color: Theme.cardBackground
|
||||
border.color: Theme.cardBorder
|
||||
border.width: 1
|
||||
radius: Theme.radiusMd
|
||||
clip: true
|
||||
|
||||
ReadoutPanel {
|
||||
id: readoutPanel
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 0
|
||||
property var s: streamController.stats
|
||||
property alias showLabels: labelsToggle.checked
|
||||
property alias heatmapBlend: heatmapSlider.value
|
||||
|
||||
// ── READOUT ───────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "READOUT"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
bottomPadding: 4
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 2
|
||||
rowSpacing: 4
|
||||
columnSpacing: 4
|
||||
Layout.fillWidth: true
|
||||
|
||||
// MIN TEMP
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 52
|
||||
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder; border.width: 1
|
||||
ColumnLayout {
|
||||
anchors { fill: parent; margins: 7 }
|
||||
spacing: 1
|
||||
Label { text: "MIN TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
Label {
|
||||
text: s.min !== undefined ? s.min + " #" + s.minIndex : "—"
|
||||
color: Theme.sensorLow
|
||||
font.pixelSize: 15; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MAX TEMP
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 52
|
||||
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder; border.width: 1
|
||||
ColumnLayout {
|
||||
anchors { fill: parent; margins: 7 }
|
||||
spacing: 1
|
||||
Label { text: "MAX TEMP"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
Label {
|
||||
text: s.max !== undefined ? s.max + " #" + s.maxIndex : "—"
|
||||
color: Theme.sensorHigh
|
||||
font.pixelSize: 15; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DIFFERENTIAL
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 52
|
||||
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder; border.width: 1
|
||||
ColumnLayout {
|
||||
anchors { fill: parent; margins: 7 }
|
||||
spacing: 1
|
||||
Label { text: "DIFFERENTIAL"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
Label {
|
||||
text: s.diff !== undefined ? s.diff : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 15; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AVERAGE
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 52
|
||||
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder; border.width: 1
|
||||
ColumnLayout {
|
||||
anchors { fill: parent; margins: 7 }
|
||||
spacing: 1
|
||||
Label { text: "AVERAGE"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
Label {
|
||||
text: s.avg !== undefined ? s.avg : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 15; font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SIGMA — full-width
|
||||
Rectangle {
|
||||
Layout.fillWidth: true; height: 44
|
||||
color: Theme.subtleSectionBackground; radius: Theme.radiusSm
|
||||
border.color: Theme.cardBorder; border.width: 1
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 7; rightMargin: 7; topMargin: 4; bottomMargin: 4 }
|
||||
ColumnLayout {
|
||||
spacing: 1
|
||||
Label { text: "SIGMA (σ)"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
Label { text: "3σ"; color: Theme.bodyColor; font.pixelSize: 9; font.letterSpacing: 1 }
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
ColumnLayout {
|
||||
spacing: 1
|
||||
Label {
|
||||
text: s.sigma !== undefined ? s.sigma : "—"
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 15; font.weight: Font.Bold
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
Label {
|
||||
text: s.threeSigma !== undefined ? s.threeSigma : "—"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12; font.weight: Font.Medium
|
||||
Layout.alignment: Qt.AlignRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 8 }
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
Item { height: 8 }
|
||||
|
||||
// ── DISPLAY ───────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "DISPLAY"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
bottomPadding: 4
|
||||
}
|
||||
|
||||
CheckBox {
|
||||
id: labelsToggle
|
||||
text: "Labels"
|
||||
checked: true
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "Heatmap"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
Layout.preferredWidth: 52
|
||||
}
|
||||
Slider {
|
||||
id: heatmapSlider
|
||||
from: 0; to: 1; value: 0
|
||||
Layout.fillWidth: true
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: Math.round(value * 100) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 8 }
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
Item { height: 8 }
|
||||
|
||||
// ── THRESHOLDS ────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "THRESHOLDS"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
bottomPadding: 4
|
||||
}
|
||||
|
||||
Label { text: "Set Point (°C)"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
TextField {
|
||||
id: spField
|
||||
Layout.fillWidth: true
|
||||
text: "149.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: spField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: spField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
onEditingFinished: pushThresholds()
|
||||
}
|
||||
|
||||
Item { height: 4 }
|
||||
|
||||
Label { text: "Margin (±°C)"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
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: 4 }
|
||||
|
||||
CheckBox {
|
||||
id: autoCheck
|
||||
text: "Auto range (mean ± 1σ)"
|
||||
checked: true
|
||||
font.pixelSize: 11
|
||||
onCheckedChanged: pushThresholds()
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
|
||||
function pushThresholds() {
|
||||
var sp = parseFloat(spField.text) || 149.0
|
||||
var mg = parseFloat(mgField.text) || 1.0
|
||||
streamController.setThresholds(sp, mg, autoCheck.checked)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
Dialog {
|
||||
id: root
|
||||
title: "Sensor Override"
|
||||
modal: true
|
||||
standardButtons: Dialog.NoButton
|
||||
width: 300
|
||||
|
||||
// called by WaferMapView's TapHandler
|
||||
property int sensorIndex: -1
|
||||
property string sensorLabel: ""
|
||||
property real currentValue: 0.0
|
||||
property bool hasOverride: streamController.overriddenSensors.indexOf(sensorIndex) >= 0
|
||||
|
||||
function openFor(index) {
|
||||
sensorIndex = index
|
||||
var dot = streamController.sensorDots[index]
|
||||
if (!dot) return
|
||||
sensorLabel = dot.label !== undefined ? String(dot.label) : String(index + 1)
|
||||
currentValue = dot.value
|
||||
valueField.text = ""
|
||||
offsetField.text = ""
|
||||
open()
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.cardBackground
|
||||
radius: 8
|
||||
border.color: Theme.cardBorder
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.rightPaneGap
|
||||
|
||||
// ── header ───────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: "Sensor #" + (root.sensorIndex + 1) +
|
||||
(root.sensorLabel ? " (" + root.sensorLabel + ")" : "")
|
||||
color: Theme.headingColor
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
visible: root.hasOverride
|
||||
width: overrideTag.implicitWidth + 12
|
||||
height: overrideTag.implicitHeight + 6
|
||||
radius: 4
|
||||
color: Theme.statusWarningColor
|
||||
opacity: 0.85
|
||||
Label {
|
||||
id: overrideTag
|
||||
anchors.centerIn: parent
|
||||
text: "OVERRIDE"
|
||||
color: "white"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.letterSpacing: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Live: " + root.currentValue.toFixed(2)
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// ── replace field ─────────────────────────────────────────
|
||||
Label { text: "Replace with value"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
TextField {
|
||||
id: valueField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "100"
|
||||
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: valueField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: valueField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ── offset field ──────────────────────────────────────────
|
||||
Label { text: "Or add offset (± delta)"; color: Theme.bodyColor; font.pixelSize: 11 }
|
||||
TextField {
|
||||
id: offsetField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "e.g. +0.5 or -0.5 (blank = skip)"
|
||||
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: offsetField.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: offsetField.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: Theme.cardBorder }
|
||||
|
||||
// ── buttons ───────────────────────────────────────────────
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
Button {
|
||||
id: applyBtn
|
||||
text: "Apply"
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
enabled: valueField.text !== "" || offsetField.text !== ""
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: applyBtn.down ? Theme.buttonNeutralPressed : applyBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: applyBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: {
|
||||
if (valueField.text !== "") {
|
||||
var v = parseFloat(valueField.text)
|
||||
if (!isNaN(v))
|
||||
streamController.replaceSensor(root.sensorIndex, v)
|
||||
}
|
||||
if (offsetField.text !== "") {
|
||||
var d = parseFloat(offsetField.text)
|
||||
if (!isNaN(d))
|
||||
streamController.offsetSensor(root.sensorIndex, d)
|
||||
}
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: clearBtn
|
||||
text: "Clear"
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
enabled: root.hasOverride
|
||||
opacity: enabled ? 1.0 : 0.4
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: clearBtn.down ? Theme.buttonNeutralPressed : clearBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: clearBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: {
|
||||
streamController.clearSensorEdit(root.sensorIndex)
|
||||
root.close()
|
||||
}
|
||||
}
|
||||
Button {
|
||||
id: cancelBtn
|
||||
text: "Cancel"
|
||||
hoverEnabled: true
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: cancelBtn.down ? Theme.buttonNeutralPressed : cancelBtn.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: Text {
|
||||
text: cancelBtn.text
|
||||
color: Theme.buttonNeutralText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.bold: true
|
||||
font.pixelSize: 13
|
||||
}
|
||||
onClicked: root.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 6
|
||||
|
||||
// ── Section label ─────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: "SOURCE"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
font.letterSpacing: 1.8
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// ── Directory button ──────────────────────────────────────────────────
|
||||
Button {
|
||||
id: dirBtn
|
||||
Layout.fillWidth: true
|
||||
hoverEnabled: true
|
||||
implicitHeight: 32
|
||||
leftPadding: 8
|
||||
rightPadding: 8
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusSm
|
||||
color: dirBtn.pressed ? Theme.buttonNeutralPressed
|
||||
: dirBtn.hovered ? Theme.buttonNeutralHover
|
||||
: Theme.buttonNeutralBackground
|
||||
border.width: Theme.borderThin
|
||||
border.color: Theme.fieldBorder
|
||||
}
|
||||
contentItem: RowLayout {
|
||||
spacing: 6
|
||||
Label {
|
||||
text: "▤"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 12
|
||||
}
|
||||
Label {
|
||||
text: file_browser.currentDirectory.split("/").pop() || file_browser.currentDirectory
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideMiddle
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
onClicked: file_browser.chooseDirectory()
|
||||
}
|
||||
|
||||
// ── Filter ────────────────────────────────────────────────────────────
|
||||
TextField {
|
||||
id: filter
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Filter…"
|
||||
font.pixelSize: 11
|
||||
color: Theme.fieldText
|
||||
placeholderTextColor: Theme.fieldPlaceholder
|
||||
selectedTextColor: Theme.fieldBackground
|
||||
selectionColor: Theme.fieldText
|
||||
background: Rectangle {
|
||||
radius: Theme.radiusXs
|
||||
color: Theme.fieldBackground
|
||||
border.width: filter.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||
border.color: filter.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||
}
|
||||
}
|
||||
|
||||
// ── File list ─────────────────────────────────────────────────────────
|
||||
ListView {
|
||||
id: fileList
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
spacing: 2
|
||||
highlight: null
|
||||
highlightFollowsCurrentItem: false
|
||||
|
||||
model: file_browser.files
|
||||
|
||||
delegate: ItemDelegate {
|
||||
id: fileItem
|
||||
width: ListView.view.width
|
||||
padding: 0
|
||||
highlighted: false
|
||||
focusPolicy: Qt.NoFocus
|
||||
|
||||
readonly property bool matchesFilter: {
|
||||
if (filter.text === "") return true
|
||||
var q = filter.text.toLowerCase()
|
||||
return (modelData.baseName + modelData.waferType + modelData.date)
|
||||
.toLowerCase().indexOf(q) >= 0
|
||||
}
|
||||
|
||||
// Active only in review mode; no highlight during live streaming
|
||||
readonly property bool isActive:
|
||||
streamController.mode === "review" &&
|
||||
streamController.loadedFile !== "" &&
|
||||
modelData.fileName === streamController.loadedFile
|
||||
|
||||
visible: matchesFilter
|
||||
height: matchesFilter ? implicitHeight : 0
|
||||
|
||||
onClicked: streamController.loadFile(modelData.fileName)
|
||||
|
||||
background: Rectangle {
|
||||
color: fileItem.isActive
|
||||
? Qt.rgba(1, 1, 1, 0.06)
|
||||
: fileItem.hovered ? Qt.rgba(1, 1, 1, 0.04) : "transparent"
|
||||
radius: Theme.radiusSm
|
||||
|
||||
// Left accent bar — shown only when active
|
||||
Rectangle {
|
||||
visible: fileItem.isActive
|
||||
width: 3
|
||||
radius: 1.5
|
||||
anchors {
|
||||
top: parent.top; bottom: parent.bottom; left: parent.left
|
||||
topMargin: 5; bottomMargin: 5
|
||||
}
|
||||
color: {
|
||||
var t = modelData.waferType
|
||||
if (t === "A" || t === "E" || t === "P") return "#3B82F6"
|
||||
if (t === "B" || t === "C" || t === "D") return "#10B981"
|
||||
if (t === "Z") return "#8B5CF6"
|
||||
return Theme.headingColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 8
|
||||
anchors.margins: 8
|
||||
|
||||
// Wafer-type avatar (circle)
|
||||
Rectangle {
|
||||
width: 28; height: 28
|
||||
radius: 14
|
||||
color: {
|
||||
var t = modelData.waferType
|
||||
if (t === "A" || t === "E" || t === "P") return "#1D4ED8"
|
||||
if (t === "B" || t === "C" || t === "D") return "#065F46"
|
||||
if (t === "Z") return "#7C3AED"
|
||||
return "#374151"
|
||||
}
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.waferType || "?"
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
color: "#FFFFFF"
|
||||
}
|
||||
}
|
||||
|
||||
// Typography hierarchy
|
||||
ColumnLayout {
|
||||
spacing: 1
|
||||
Layout.fillWidth: true
|
||||
|
||||
// Primary: serial number
|
||||
Label {
|
||||
text: modelData.serialNumber || modelData.baseName
|
||||
color: Theme.headingColor
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Bold
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Secondary: date · time
|
||||
RowLayout {
|
||||
spacing: 3
|
||||
Layout.fillWidth: true
|
||||
Label {
|
||||
text: {
|
||||
var d = modelData.date || ""
|
||||
return d.length >= 10 ? d.substring(0, 10) : (d || "—")
|
||||
}
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
Label {
|
||||
visible: (modelData.timeStr || "") !== ""
|
||||
text: "·"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
opacity: 0.45
|
||||
}
|
||||
Label {
|
||||
visible: (modelData.timeStr || "") !== ""
|
||||
text: modelData.timeStr || ""
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
opacity: 0.65
|
||||
}
|
||||
}
|
||||
|
||||
// Tertiary: edited metadata (chamber · notes) if any
|
||||
Label {
|
||||
visible: {
|
||||
var c = modelData.chamber || ""
|
||||
var n = modelData.notes || ""
|
||||
return c !== "" || n !== ""
|
||||
}
|
||||
text: {
|
||||
var parts = []
|
||||
if (modelData.chamber) parts.push(modelData.chamber)
|
||||
if (modelData.notes) parts.push(modelData.notes)
|
||||
return parts.join(" · ")
|
||||
}
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 9
|
||||
opacity: 0.7
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Footer ────────────────────────────────────────────────────────────
|
||||
Label {
|
||||
text: file_browser.files.length + " files · CSV only"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
RowLayout {
|
||||
spacing: 2
|
||||
|
||||
component TBtn: Button {
|
||||
implicitWidth: 44
|
||||
implicitHeight: 32
|
||||
hoverEnabled: true
|
||||
font.pixelSize: 13
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? Theme.transportButtonHover
|
||||
: parent.hovered ? Theme.transportButtonBg
|
||||
: Theme.transportBackground
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.headingColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Left fill — centers the button cluster
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
TBtn { text: "⏮"; onClicked: streamController.step(-1) }
|
||||
TBtn { text: "⏹"; onClicked: streamController.stop() }
|
||||
TBtn { text: "⏸"; onClicked: streamController.pause() }
|
||||
// Play: slightly wider; leftPadding nudge compensates for ▶ optical offset
|
||||
TBtn {
|
||||
text: "▶"
|
||||
implicitWidth: 52
|
||||
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
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// Speed cycle
|
||||
Button {
|
||||
id: speedBtn
|
||||
property var speeds: [1, 2, 5]
|
||||
property int idx: 0
|
||||
implicitWidth: 44
|
||||
implicitHeight: 32
|
||||
hoverEnabled: true
|
||||
text: speeds[idx] + "×"
|
||||
font.pixelSize: 11
|
||||
font.weight: Font.Medium
|
||||
background: Rectangle {
|
||||
color: parent.pressed ? Theme.transportButtonHover
|
||||
: parent.hovered ? Theme.transportButtonBg
|
||||
: Theme.transportBackground
|
||||
radius: Theme.radiusSm
|
||||
}
|
||||
contentItem: Text {
|
||||
text: parent.text
|
||||
color: Theme.bodyColor
|
||||
font: parent.font
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
idx = (idx + 1) % speeds.length
|
||||
streamController.setSpeed(speeds[idx])
|
||||
}
|
||||
}
|
||||
|
||||
Item { width: 8 }
|
||||
|
||||
Label {
|
||||
text: "frame " + (streamController.frameIndex + 1) + " / " + streamController.frameTotal
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import ISC
|
||||
import ISC.Wafer
|
||||
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property real blend: 0.0
|
||||
property bool showLabels: true
|
||||
|
||||
WaferMapItem {
|
||||
id: map
|
||||
anchors.fill: parent
|
||||
sensors: streamController.sensorLayout // [{label,x,y}]
|
||||
values: streamController.sensorValues // [float]
|
||||
bands: streamController.sensorBands // ["in_range"|"high"|"low"]
|
||||
target: streamController.target
|
||||
margin: streamController.margin
|
||||
blend: root.blend
|
||||
showLabels: root.showLabels
|
||||
// Bind to Theme so colors update on dark/light mode switch
|
||||
ringColor: Theme.waferRingColor
|
||||
axisColor: Theme.waferAxisColor
|
||||
lowColor: Theme.sensorLow
|
||||
inRangeColor: Theme.sensorInRange
|
||||
highColor: Theme.sensorHigh
|
||||
textColor: Theme.headingColor
|
||||
|
||||
|
||||
TapHandler {
|
||||
onTapped: (ev) => {
|
||||
var idx = map.which_marker(ev.position.x, ev.position.y)
|
||||
if (idx >= 0) replaceDialog.openFor(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceSensorDialog { id: replaceDialog}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# ===== ISC Tabs Components Module =====
|
||||
module ISC.Tabs.components
|
||||
SourcePanel 1.0 SourcePanel.qml
|
||||
ReadoutPanel 1.0 ReadoutPanel.qml
|
||||
TransportBar 1.0 TransportBar.qml
|
||||
WaferMapView 1.0 WaferMapView.qml
|
||||
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
||||
@@ -1,5 +1,8 @@
|
||||
# ===== ISC Tabs Module =====
|
||||
module ISC.Tabs
|
||||
|
||||
# ===== Tab Components =====
|
||||
WaferMapTab 1.0 WaferMapTab.qml
|
||||
DataTab 1.0 DataTab.qml
|
||||
SettingsTab 1.0 SettingsTab.qml
|
||||
StatusTab 1.0 StatusTab.qml
|
||||
SelectFileDialog 1.0 SelectFileDialog.qml
|
||||
|
||||
+22
-11
@@ -21,15 +21,18 @@ import QtQuick
|
||||
|
||||
QtObject {
|
||||
// ── 1. Mode ──────────────────────────────────────────────────────────────
|
||||
property bool isDarkMode: false
|
||||
property bool isDarkMode: true
|
||||
|
||||
// ── 2. Tone palette (base values; consume via semantic tokens below) ─────
|
||||
readonly property color tone100: isDarkMode ? "#111111" : "#FAFAFA"
|
||||
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F2F2F2"
|
||||
readonly property color tone300: isDarkMode ? "#242424" : "#E8E8E8"
|
||||
readonly property color tone150: isDarkMode ? "#161616" : "#F6F6F6"
|
||||
readonly property color tone200: isDarkMode ? "#1A1A1A" : "#F0F0F0"
|
||||
readonly property color tone250: isDarkMode ? "#212121" : "#EAEAEA"
|
||||
readonly property color tone300: isDarkMode ? "#282828" : "#E2E2E2"
|
||||
readonly property color tone350: isDarkMode ? "#303030" : "#D6D6D6"
|
||||
readonly property color toneText: isDarkMode ? "#F2F2F2" : "#111111"
|
||||
readonly property color toneMute: isDarkMode ? "#A8A8A8" : "#8A8A8A"
|
||||
readonly property color toneBorder: isDarkMode ? "#2B2B2B" : "#D8D8D8"
|
||||
readonly property color toneBorder: isDarkMode ? "#2E2E2E" : "#D4D4D4"
|
||||
|
||||
// ── 3. Surfaces ──────────────────────────────────────────────────────────
|
||||
readonly property color pageBackground: tone100
|
||||
@@ -41,11 +44,12 @@ QtObject {
|
||||
|
||||
// ── 4. Borders ───────────────────────────────────────────────────────────
|
||||
readonly property color cardBorder: toneBorder
|
||||
readonly property color cardSurfaceBorder: isDarkMode ? "#3C3C3C" : "#C8C8C8"
|
||||
readonly property color responseBorder: toneBorder
|
||||
readonly property color workspaceBorder: toneBorder
|
||||
readonly property color innerFrameBorder: toneBorder
|
||||
readonly property color outerFrameBorder: isDarkMode ? "#343434" : "#CECECE"
|
||||
readonly property color softBorder: isDarkMode ? "#222222" : "#E2E2E2"
|
||||
readonly property color outerFrameBorder: isDarkMode ? "#383838" : "#CACACA"
|
||||
readonly property color softBorder: isDarkMode ? "#252525" : "#E0E0E0"
|
||||
|
||||
// ── 5. Text ──────────────────────────────────────────────────────────────
|
||||
readonly property color headingColor: toneText
|
||||
@@ -87,6 +91,13 @@ QtObject {
|
||||
readonly property color sideRailBackground: tone200
|
||||
readonly property color sideActiveBackground: tone300
|
||||
|
||||
// ── 10a. Transport / toolbar surfaces ────────────────────────────────────
|
||||
readonly property color transportBackground: isDarkMode ? "#0D0D0D" : "#E8E8E8"
|
||||
readonly property color transportButtonBg: isDarkMode ? "#2A2A2A" : "#D4D4D4"
|
||||
readonly property color transportButtonHover: isDarkMode ? "#3A3A3A" : "#C4C4C4"
|
||||
readonly property color liveColor: isDarkMode ? "#22C55E" : "#16A34A"
|
||||
readonly property color recordColor: isDarkMode ? "#EF4444" : "#DC2626"
|
||||
|
||||
// ── 10. Status ───────────────────────────────────────────────────────────
|
||||
readonly property color statusSuccessColor: isDarkMode ? "#63D471" : "#2E9E44"
|
||||
readonly property color statusWarningColor: isDarkMode ? "#F5C15C" : "#C88A18"
|
||||
@@ -95,16 +106,16 @@ QtObject {
|
||||
// -- 10b. Sensor bands (wafer map dots)
|
||||
readonly property color sensorInRange: statusSuccessColor
|
||||
readonly property color sensorHigh: statusErrorColor
|
||||
readonly property color sensorlow: isDarkMode ? "#589DF5" : "#2F6FE0"
|
||||
readonly property color sensorLow: isDarkMode ? "#5B9DF5" : "#2F6FE0"
|
||||
readonly property color waferRingColor: toneBorder
|
||||
readonly property color waferAxisColor: softBorder
|
||||
|
||||
// ── 11. Geometry ─────────────────────────────────────────────────────────
|
||||
// Radius
|
||||
readonly property int radiusXs: 4 // fields, tight elements
|
||||
readonly property int radiusSm: 6 // buttons
|
||||
readonly property int radiusMd: 10 // cards, group boxes
|
||||
readonly property int radiusLg: 14 // large panels / dialogs
|
||||
readonly property int radiusXs: 6 // fields, tight elements
|
||||
readonly property int radiusSm: 8 // buttons
|
||||
readonly property int radiusMd: 12 // cards, group boxes
|
||||
readonly property int radiusLg: 18 // large panels / dialogs
|
||||
// Border width
|
||||
readonly property int borderThin: 1
|
||||
readonly property int borderStrong: 2
|
||||
|
||||
Reference in New Issue
Block a user