fix(qml): resolve runtime reference errors, missing imports, and port change notifications
This commit is contained in:
+35
-758
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
|||||||
import QtQuick.Controls.impl
|
import QtQuick.Controls.impl
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ISC
|
import ISC
|
||||||
|
import ISC.Tabs.components
|
||||||
import ISC.Wafer
|
import ISC.Wafer
|
||||||
|
|
||||||
// ===== Data Tab (Compare Content Only) =====
|
// ===== Data Tab (Compare Content Only) =====
|
||||||
@@ -184,8 +185,8 @@ Item {
|
|||||||
root.compareWaferShape = result.wafer_shape || "round"
|
root.compareWaferShape = result.wafer_shape || "round"
|
||||||
root.compareWaferSize = result.wafer_size || 300.0
|
root.compareWaferSize = result.wafer_size || 300.0
|
||||||
root.errorMessage = ""
|
root.errorMessage = ""
|
||||||
scrubber.value = Math.floor(root.seriesLen / 2)
|
chartCard.frameIndex = Math.floor(root.seriesLen / 2)
|
||||||
scrubber.forceActiveFocus()
|
chartCard.focusScrubber()
|
||||||
if (root.frameCountA !== root.frameCountB) {
|
if (root.frameCountA !== root.frameCountB) {
|
||||||
var timeA = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
|
var timeA = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
|
||||||
var timeB = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
|
var timeB = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
|
||||||
@@ -204,250 +205,6 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Reusable pieces ────────────────────────────────────────────
|
|
||||||
component SectionTitle: Label {
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
font.letterSpacing: 1.5
|
|
||||||
font.bold: true
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
|
|
||||||
component PanelBox: Rectangle {
|
|
||||||
color: Theme.sidePanelBackground
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.sidePanelRadius
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checkbox styled to match MapTab's right-rail ReadoutPanel (square accent
|
|
||||||
// indicator + check icon) instead of the platform-default CheckBox look.
|
|
||||||
component PanelCheckBox: CheckBox {
|
|
||||||
id: toggle
|
|
||||||
indicator: Rectangle {
|
|
||||||
implicitWidth: 18
|
|
||||||
implicitHeight: 18
|
|
||||||
x: toggle.leftPadding
|
|
||||||
y: parent.height / 2 - height / 2
|
|
||||||
radius: Theme.radiusXs
|
|
||||||
color: toggle.checked ? Theme.primaryAccent : "transparent"
|
|
||||||
border.width: Theme.borderThin
|
|
||||||
border.color: toggle.checked ? Theme.primaryAccent : Theme.fieldBorder
|
|
||||||
|
|
||||||
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
|
|
||||||
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
source: "icons/check.svg"
|
|
||||||
width: 12; height: 12
|
|
||||||
sourceSize.width: 12
|
|
||||||
sourceSize.height: 12
|
|
||||||
color: Theme.panelBackground
|
|
||||||
opacity: toggle.checked ? 1.0 : 0.0
|
|
||||||
Behavior on opacity { NumberAnimation { duration: Theme.durationFast } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Text {
|
|
||||||
text: toggle.text
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
leftPadding: toggle.indicator.width + toggle.spacing
|
|
||||||
font.pixelSize: toggle.font.pixelSize || Theme.fontXs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slider styled to match MapTab's right-rail heatmap slider (circular
|
|
||||||
// accent handle that grows slightly on press).
|
|
||||||
component PanelSlider: Slider {
|
|
||||||
id: panelSlider
|
|
||||||
handle: Rectangle {
|
|
||||||
x: panelSlider.leftPadding + panelSlider.visualPosition * (panelSlider.availableWidth - width)
|
|
||||||
y: panelSlider.topPadding + panelSlider.availableHeight / 2 - height / 2
|
|
||||||
implicitWidth: 18
|
|
||||||
implicitHeight: 18
|
|
||||||
radius: 9
|
|
||||||
color: Theme.primaryAccent
|
|
||||||
scale: panelSlider.pressed ? 1.15 : 1.0
|
|
||||||
Behavior on scale { NumberAnimation { duration: Theme.durationFast; easing.type: Theme.easeStandard } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mini stat box under the alignment scrubber (styled to match DTW ReadoutCard)
|
|
||||||
component ScrubStat: Rectangle {
|
|
||||||
id: scrubStat
|
|
||||||
property string label
|
|
||||||
property string value
|
|
||||||
property color valueColor: Theme.headingColor
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: 52
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
color: Theme.subtleSectionBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 2
|
|
||||||
Label {
|
|
||||||
text: scrubStat.label.toUpperCase()
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 0.2
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: scrubStat.value
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
color: scrubStat.valueColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Editable stat box for scrubber readout (styled to match DTW ReadoutCard)
|
|
||||||
component EditableScrubStat: Rectangle {
|
|
||||||
id: edStat
|
|
||||||
property string label
|
|
||||||
property real value
|
|
||||||
property string unit
|
|
||||||
property string extraText: ""
|
|
||||||
property bool editable: true
|
|
||||||
signal valueEdited(real newValue)
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: edStatRow.implicitHeight + 16
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
color: Theme.subtleSectionBackground
|
|
||||||
border.color: (edStat.editable && inputField.activeFocus) ? Theme.fieldBorderFocus : Theme.cardBorder
|
|
||||||
border.width: (edStat.editable && inputField.activeFocus) ? Theme.borderStrong : Theme.borderThin
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: edStatRow
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 2
|
|
||||||
Label {
|
|
||||||
text: edStat.label.toUpperCase()
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 0.2
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
RowLayout {
|
|
||||||
spacing: 4
|
|
||||||
TextField {
|
|
||||||
id: inputField
|
|
||||||
readOnly: !edStat.editable
|
|
||||||
hoverEnabled: edStat.editable
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
color: edStat.editable ? Theme.headingColor : Theme.sideMutedText
|
|
||||||
verticalAlignment: TextInput.AlignVCenter
|
|
||||||
horizontalAlignment: TextInput.AlignHCenter
|
|
||||||
background: Rectangle {
|
|
||||||
visible: edStat.editable
|
|
||||||
color: Theme.fieldBackground
|
|
||||||
radius: Theme.radiusXs
|
|
||||||
border.color: inputField.activeFocus ? Theme.fieldBorderFocus : (inputField.hovered ? Theme.fieldBorderFocus : Theme.fieldBorder)
|
|
||||||
border.width: 1
|
|
||||||
}
|
|
||||||
padding: 0
|
|
||||||
leftPadding: edStat.editable ? 6 : 0
|
|
||||||
rightPadding: edStat.editable ? 6 : 0
|
|
||||||
topPadding: edStat.editable ? 5 : 0
|
|
||||||
bottomPadding: edStat.editable ? 5 : 0
|
|
||||||
selectByMouse: edStat.editable
|
|
||||||
inputMethodHints: edStat.label === "Time" ? Qt.ImhFormattedNumbersOnly : Qt.ImhDigitsOnly
|
|
||||||
Layout.preferredWidth: edStat.editable ? Math.max(36, contentWidth + 12) : contentWidth
|
|
||||||
Layout.preferredHeight: edStat.editable ? 32 : implicitHeight
|
|
||||||
|
|
||||||
text: edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
|
|
||||||
|
|
||||||
onEditingFinished: {
|
|
||||||
var val = parseFloat(text)
|
|
||||||
if (!isNaN(val)) {
|
|
||||||
edStat.valueEdited(val)
|
|
||||||
}
|
|
||||||
text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
|
|
||||||
scrubber.forceActiveFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: edStat
|
|
||||||
function onValueChanged() {
|
|
||||||
if (!inputField.activeFocus) {
|
|
||||||
inputField.text = edStat.value.toFixed(edStat.label === "Time" ? 1 : 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: edStat.unit
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
color: edStat.editable ? Theme.headingColor : Theme.sideMutedText
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
visible: edStat.extraText !== ""
|
|
||||||
text: edStat.extraText
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
color: Theme.headingColor
|
|
||||||
Layout.alignment: Qt.AlignVCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Large labeled value card for the DTW readout panel
|
|
||||||
component ReadoutCard: Rectangle {
|
|
||||||
id: readoutCard
|
|
||||||
property string label
|
|
||||||
property string value
|
|
||||||
property color valueColor: Theme.headingColor
|
|
||||||
Layout.fillWidth: true
|
|
||||||
implicitHeight: 52
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
color: Theme.subtleSectionBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 8
|
|
||||||
spacing: 2
|
|
||||||
Label {
|
|
||||||
text: readoutCard.label
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 0.2
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: readoutCard.value
|
|
||||||
font.pixelSize: Theme.fontLg
|
|
||||||
font.bold: true
|
|
||||||
font.family: Theme.codeFontFamily
|
|
||||||
color: readoutCard.valueColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Main Layout ───────────────────────────────────────────────
|
// ── Main Layout ───────────────────────────────────────────────
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -506,533 +263,53 @@ Item {
|
|||||||
spacing: 12
|
spacing: 12
|
||||||
|
|
||||||
// Wafer Map Overlap View card
|
// Wafer Map Overlap View card
|
||||||
PanelBox {
|
OverlapMapCard {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
Layout.minimumHeight: 220
|
Layout.minimumHeight: 220
|
||||||
|
sensorLayout: root.compareSensorLayout
|
||||||
ColumnLayout {
|
diffValues: streamController.getSensorDiffAt(Math.round(chartCard.frameIndex))
|
||||||
anchors.fill: parent
|
diffBands: root.diffBands
|
||||||
anchors.margins: 12
|
waferShape: root.compareWaferShape
|
||||||
spacing: 8
|
waferSize: root.compareWaferSize
|
||||||
|
overlapEnabled: sidePanel.overlapEnabled
|
||||||
RowLayout {
|
blendAmount: sidePanel.blendAmount
|
||||||
Layout.fillWidth: true
|
|
||||||
SectionTitle {
|
|
||||||
text: "WAFER OVERLAP"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
color: Theme.fieldBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
WaferMapItem {
|
|
||||||
id: overlapMapItem
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.compareSensorLayout.length > 0 && enableOverlapCheck.checked
|
|
||||||
sensors: root.compareSensorLayout
|
|
||||||
values: streamController.getSensorDiffAt(scrubReadout.scrubIdx)
|
|
||||||
bands: root.diffBands(overlapMapItem.values)
|
|
||||||
shape: root.compareWaferShape
|
|
||||||
size: root.compareWaferSize
|
|
||||||
target: 0
|
|
||||||
margin: 1.0
|
|
||||||
blend: blendSlider.value / 100
|
|
||||||
showLabels: true
|
|
||||||
ringColor: Theme.waferRingColor
|
|
||||||
axisColor: Theme.waferAxisColor
|
|
||||||
lowColor: Theme.sensorLow
|
|
||||||
inRangeColor: Theme.sensorInRange
|
|
||||||
highColor: Theme.sensorHigh
|
|
||||||
textColor: Theme.headingColor
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: root.compareSensorLayout.length === 0 || !enableOverlapCheck.checked
|
|
||||||
spacing: 4
|
|
||||||
IconImage {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
source: "icons/map.svg"
|
|
||||||
width: 24; height: 24
|
|
||||||
sourceSize.width: 24
|
|
||||||
sourceSize.height: 24
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: !enableOverlapCheck.checked ? "Overlap map disabled"
|
|
||||||
: "No sensor map available"
|
|
||||||
color: Theme.bodyColor
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chart card with alignment scrubber
|
// Chart card with alignment scrubber
|
||||||
PanelBox {
|
ComparisonChartCard {
|
||||||
|
id: chartCard
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: chartCol.implicitHeight + 24
|
trendDataA: root.trendDataA
|
||||||
|
trendDataB: root.trendDataB
|
||||||
ColumnLayout {
|
seriesLen: root.seriesLen
|
||||||
id: chartCol
|
hasSeries: root.hasSeries
|
||||||
anchors.fill: parent
|
comparing: root.comparing
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
SectionTitle {
|
|
||||||
text: "CURVE OVERLAY"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
spacing: 16
|
|
||||||
visible: root.hasSeries
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.primaryAccent; anchors.verticalCenter: parent.verticalCenter }
|
|
||||||
Label { text: "Run A"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
|
|
||||||
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
spacing: 4
|
|
||||||
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.themeSkill; anchors.verticalCenter: parent.verticalCenter }
|
|
||||||
Label { text: "Run B"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: 200
|
|
||||||
color: Theme.fieldBackground
|
|
||||||
border.color: Theme.cardBorder
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.radiusSm
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Canvas {
|
|
||||||
id: overlayCanvas
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: root.hasSeries
|
|
||||||
|
|
||||||
readonly property real padLeft: 50
|
|
||||||
readonly property real padRight: 16
|
|
||||||
readonly property real padTop: 24
|
|
||||||
readonly property real padBottom: 36
|
|
||||||
|
|
||||||
// Nice tick step (1/2/5 × 10^k) — same scheme as TrendChartItem,
|
|
||||||
// so labels land on round numbers instead of raw data min/max.
|
|
||||||
function niceStep(rawStep) {
|
|
||||||
var mag = Math.pow(10, Math.floor(Math.log10(rawStep)))
|
|
||||||
return [1, 2, 5, 10].map(function (s) { return s * mag })
|
|
||||||
.find(function (s) { return s >= rawStep })
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawGrid(ctx, minV, maxV, yStep, dataLen) {
|
|
||||||
var plotW = width - padLeft - padRight
|
|
||||||
var plotH = height - padTop - padBottom
|
|
||||||
var range = (maxV - minV) || 1
|
|
||||||
|
|
||||||
ctx.strokeStyle = Theme.chartGridLine
|
|
||||||
ctx.lineWidth = 1
|
|
||||||
ctx.fillStyle = Theme.chartAxisText
|
|
||||||
ctx.font = "10px " + Theme.uiFontFamily
|
|
||||||
ctx.textAlign = "right"
|
|
||||||
|
|
||||||
var ySteps = Math.max(1, Math.round(range / yStep))
|
|
||||||
var yDec = Math.max(0, -Math.floor(Math.log10(yStep)))
|
|
||||||
for (var yi = 0; yi <= ySteps; yi++) {
|
|
||||||
var yFrac = yi / ySteps
|
|
||||||
var yPx = padTop + plotH * yFrac
|
|
||||||
var yVal = maxV - yFrac * range
|
|
||||||
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(padLeft, yPx)
|
|
||||||
ctx.lineTo(padLeft + plotW, yPx)
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
ctx.fillText(yVal.toFixed(yDec), padLeft - 6, yPx + 3)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.textAlign = "center"
|
|
||||||
var xMax = dataLen - 1
|
|
||||||
if (xMax > 0) {
|
|
||||||
var xStep = niceStep(xMax / 5 || 1)
|
|
||||||
for (var xIdx = 0; xIdx <= xMax; xIdx += xStep) {
|
|
||||||
var xPx = padLeft + (xIdx / xMax) * plotW
|
|
||||||
ctx.fillText(xIdx.toString(), xPx, height - 20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(14, padTop + plotH / 2)
|
|
||||||
ctx.rotate(-Math.PI / 2)
|
|
||||||
ctx.textAlign = "center"
|
|
||||||
ctx.font = "12px " + Theme.uiFontFamily
|
|
||||||
ctx.fillText("Celcius (°C)", 0, 0)
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
ctx.textAlign = "center"
|
|
||||||
ctx.font = "12px " + Theme.uiFontFamily
|
|
||||||
ctx.fillText("Frame", padLeft + plotW / 2, height - 4)
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawSeries(ctx, series, minV, range, color, dataLen) {
|
|
||||||
// x is mapped against the shared frame domain (dataLen), not the
|
|
||||||
// series' own length — otherwise a shorter run's line stretches to
|
|
||||||
// fill the whole plot instead of stopping where its data ends.
|
|
||||||
if (series.length < 2) return
|
|
||||||
var plotW = width - padLeft - padRight
|
|
||||||
var plotH = height - padTop - padBottom
|
|
||||||
|
|
||||||
ctx.strokeStyle = color
|
|
||||||
ctx.lineWidth = 2
|
|
||||||
ctx.beginPath()
|
|
||||||
for (var i = 0; i < series.length; i++) {
|
|
||||||
var x = padLeft + (i / (dataLen - 1)) * plotW
|
|
||||||
var y = padTop + plotH - ((series[i] - minV) / range) * plotH
|
|
||||||
if (i === 0) ctx.moveTo(x, y)
|
|
||||||
else ctx.lineTo(x, y)
|
|
||||||
}
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
|
|
||||||
function drawScrubMarker(ctx) {
|
|
||||||
if (root.seriesLen < 2) return
|
|
||||||
var plotW = width - padLeft - padRight
|
|
||||||
var x = padLeft + (scrubber.value / (root.seriesLen - 1)) * plotW
|
|
||||||
ctx.strokeStyle = Theme.primaryAccent
|
|
||||||
ctx.lineWidth = 1.5
|
|
||||||
ctx.setLineDash([3, 3])
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(x, padTop)
|
|
||||||
ctx.lineTo(x, height - padBottom)
|
|
||||||
ctx.stroke()
|
|
||||||
ctx.setLineDash([])
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaint: {
|
|
||||||
var ctx = getContext("2d")
|
|
||||||
ctx.reset()
|
|
||||||
if (!root.hasSeries) return
|
|
||||||
var all = root.trendDataA.concat(root.trendDataB)
|
|
||||||
var minV = Math.min.apply(null, all)
|
|
||||||
var maxV = Math.max.apply(null, all)
|
|
||||||
var padding = (maxV - minV) * 0.05
|
|
||||||
minV -= padding
|
|
||||||
maxV += padding
|
|
||||||
// Snap bounds to nice-step multiples -> stable round-number axis
|
|
||||||
var yStep = niceStep((maxV - minV) / 5 || 1)
|
|
||||||
minV = Math.floor(minV / yStep) * yStep
|
|
||||||
maxV = Math.ceil(maxV / yStep) * yStep
|
|
||||||
var range = (maxV - minV) || 1
|
|
||||||
|
|
||||||
drawGrid(ctx, minV, maxV, yStep, root.seriesLen)
|
|
||||||
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent, root.seriesLen)
|
|
||||||
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill, root.seriesLen)
|
|
||||||
drawScrubMarker(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: root
|
|
||||||
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
|
|
||||||
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
|
|
||||||
}
|
|
||||||
Connections {
|
|
||||||
target: scrubber
|
|
||||||
function onValueChanged() { overlayCanvas.requestPaint() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty state
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
visible: !root.hasSeries
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
visible: !root.comparing
|
|
||||||
source: "icons/bar-chart.svg"
|
|
||||||
width: 32; height: 32
|
|
||||||
sourceSize.width: 32
|
|
||||||
sourceSize.height: 32
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
text: root.comparing ? "Running DTW comparison…" : "Select two runs and run comparison"
|
|
||||||
color: Theme.bodyColor
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
visible: !root.comparing
|
|
||||||
text: "Click a run box on the right, then select a file from the left panel."
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Alignment Timeline Scrubber ────────────
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 4
|
|
||||||
visible: root.hasSeries
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: Theme.cardBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
SectionTitle {
|
|
||||||
text: "TIMELINE SCRUBBER"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.topMargin: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
Slider {
|
|
||||||
id: scrubber
|
|
||||||
Layout.fillWidth: true
|
|
||||||
leftPadding: overlayCanvas.padLeft
|
|
||||||
rightPadding: overlayCanvas.padRight
|
|
||||||
from: 0
|
|
||||||
to: Math.max(1, root.seriesLen - 1)
|
|
||||||
stepSize: 1
|
|
||||||
value: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Right Column: run selection, overlap settings, readout ---
|
// --- Right Column: run selection, overlap settings, readout ---
|
||||||
ColumnLayout {
|
ComparisonSidePanel {
|
||||||
Layout.fillWidth: false
|
id: sidePanel
|
||||||
|
Layout.fillWidth: false
|
||||||
Layout.preferredWidth: 280
|
Layout.preferredWidth: 280
|
||||||
Layout.minimumWidth: 280
|
Layout.minimumWidth: 280
|
||||||
Layout.maximumWidth: 280
|
Layout.maximumWidth: 280
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
spacing: 12
|
warpingDistance: root.warpingDistance
|
||||||
|
frameOffset: root.frameOffset
|
||||||
// Bento 3: DTW Comparison Readout
|
frameOffsetSeconds: root.frameOffsetSeconds
|
||||||
PanelBox {
|
maxSensorDeviation: root.maxSensorDeviation
|
||||||
Layout.fillWidth: true
|
seriesLen: root.seriesLen
|
||||||
Layout.preferredHeight: readoutCol.implicitHeight + 24
|
hasSeries: root.hasSeries
|
||||||
|
trendDataA: root.trendDataA
|
||||||
ColumnLayout {
|
trendDataB: root.trendDataB
|
||||||
id: readoutCol
|
timeDataA: root.timeDataA
|
||||||
anchors.fill: parent
|
timeDataB: root.timeDataB
|
||||||
anchors.margins: 12
|
frameForTime: root.frameForTime
|
||||||
spacing: 8
|
frameIndex: chartCard.frameIndex
|
||||||
|
onFrameIndexRequested: (idx) => chartCard.frameIndex = idx
|
||||||
SectionTitle { text: "DTW READOUT" }
|
onScrubberFocusRequested: chartCard.focusScrubber()
|
||||||
|
|
||||||
ReadoutCard {
|
|
||||||
label: "DTW ALIGNMENT DISTANCE"
|
|
||||||
value: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
|
|
||||||
valueColor: root.warpingDistance >= 0
|
|
||||||
? (root.warpingDistance < 50 ? Theme.metricGood : root.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad)
|
|
||||||
: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
ReadoutCard {
|
|
||||||
label: "TEMPORAL FRAME OFFSET"
|
|
||||||
value: root.warpingDistance >= 0
|
|
||||||
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames / "
|
|
||||||
+ (root.frameOffsetSeconds >= 0 ? "+" : "") + root.frameOffsetSeconds.toFixed(1) + "s")
|
|
||||||
: "--"
|
|
||||||
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
|
|
||||||
}
|
|
||||||
ReadoutCard {
|
|
||||||
label: "MAX SENSOR DEVIATION"
|
|
||||||
value: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "°C" : "--"
|
|
||||||
valueColor: root.maxSensorDeviation >= 0
|
|
||||||
? (root.maxSensorDeviation < 1.0 ? Theme.metricGood : root.maxSensorDeviation < 3.0 ? Theme.metricWarn : Theme.metricBad)
|
|
||||||
: Theme.sideMutedText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bento 2: Overlap Map Settings
|
|
||||||
PanelBox {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: overlapCol.implicitHeight + 24
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: overlapCol
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
SectionTitle { text: "OVERLAP SETTINGS" }
|
|
||||||
|
|
||||||
PanelCheckBox {
|
|
||||||
id: enableOverlapCheck
|
|
||||||
Layout.fillWidth: true
|
|
||||||
checked: true
|
|
||||||
text: "Enable Overlap Map"
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 6
|
|
||||||
visible: enableOverlapCheck.checked
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
height: 1
|
|
||||||
color: Theme.cardBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 6
|
|
||||||
Label {
|
|
||||||
text: "Blend"
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
color: Theme.bodyColor
|
|
||||||
Layout.preferredWidth: 40
|
|
||||||
}
|
|
||||||
PanelSlider {
|
|
||||||
id: blendSlider
|
|
||||||
Layout.fillWidth: true
|
|
||||||
from: 0; to: 100; value: 50
|
|
||||||
stepSize: 1
|
|
||||||
}
|
|
||||||
Label {
|
|
||||||
text: Math.round(blendSlider.value) + "%"
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
Layout.preferredWidth: 30
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Diff color legend
|
|
||||||
Row {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
spacing: 10
|
|
||||||
Row {
|
|
||||||
spacing: 3
|
|
||||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
|
|
||||||
Label { text: "B cooler"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
spacing: 3
|
|
||||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter }
|
|
||||||
Label { text: "Match"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
|
|
||||||
}
|
|
||||||
Row {
|
|
||||||
spacing: 3
|
|
||||||
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
|
|
||||||
Label { text: "B hotter"; font.pixelSize: Theme.fontSm; color: Theme.sideMutedText }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bento: Frame / Time Controls
|
|
||||||
PanelBox {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: frameTimeCol.implicitHeight + 32
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: frameTimeCol
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 16
|
|
||||||
spacing: 20
|
|
||||||
|
|
||||||
SectionTitle { text: "FRAME / TIME" }
|
|
||||||
|
|
||||||
EditableScrubStat {
|
|
||||||
label: "Frame"
|
|
||||||
value: scrubReadout.scrubIdx
|
|
||||||
unit: "/ " + Math.max(0, root.seriesLen - 1) + (Math.max(0, root.seriesLen - 1) <= 1 ? " frame" : " frames")
|
|
||||||
editable: root.hasSeries
|
|
||||||
onValueEdited: (newValue) => {
|
|
||||||
var maxVal = Math.max(0, root.seriesLen - 1)
|
|
||||||
var val = Math.max(0, Math.min(newValue, maxVal))
|
|
||||||
scrubber.value = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditableScrubStat {
|
|
||||||
label: "Time"
|
|
||||||
value: scrubReadout.scrubTime
|
|
||||||
unit: "/ " + scrubReadout.maxTime.toFixed(1) + "s"
|
|
||||||
editable: root.hasSeries
|
|
||||||
onValueEdited: (newValue) => {
|
|
||||||
var closestIdx = root.frameForTime(newValue)
|
|
||||||
scrubber.value = closestIdx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bento: Scrub Readout
|
|
||||||
PanelBox {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: scrubReadout.implicitHeight + 24
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: scrubReadout
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 12
|
|
||||||
spacing: 8
|
|
||||||
|
|
||||||
readonly property int scrubIdx: Math.round(scrubber.value)
|
|
||||||
readonly property bool hasA: scrubIdx < root.trendDataA.length
|
|
||||||
readonly property bool hasB: scrubIdx < root.trendDataB.length
|
|
||||||
readonly property real tempA: hasA ? root.trendDataA[scrubIdx] : 0
|
|
||||||
readonly property real tempB: hasB ? root.trendDataB[scrubIdx] : 0
|
|
||||||
readonly property real scrubTime: {
|
|
||||||
var t = scrubIdx < root.timeDataA.length ? root.timeDataA[scrubIdx]
|
|
||||||
: (scrubIdx < root.timeDataB.length ? root.timeDataB[scrubIdx] : 0)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
readonly property real maxTime: {
|
|
||||||
var ta = root.timeDataA.length > 0 ? root.timeDataA[root.timeDataA.length - 1] : 0
|
|
||||||
var tb = root.timeDataB.length > 0 ? root.timeDataB[root.timeDataB.length - 1] : 0
|
|
||||||
return Math.max(ta, tb)
|
|
||||||
}
|
|
||||||
|
|
||||||
SectionTitle { text: "SCRUBBER READOUT" }
|
|
||||||
|
|
||||||
ScrubStat {
|
|
||||||
label: "Temp (Run A)"
|
|
||||||
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
|
|
||||||
valueColor: Theme.primaryAccent
|
|
||||||
}
|
|
||||||
ScrubStat {
|
|
||||||
label: "Temp (Run B)"
|
|
||||||
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
|
|
||||||
valueColor: Theme.themeSkill
|
|
||||||
}
|
|
||||||
ScrubStat {
|
|
||||||
label: "Difference"
|
|
||||||
value: (scrubReadout.hasA && scrubReadout.hasB)
|
|
||||||
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillHeight: true }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Small labeled value tile — DTW readout panel, scrubber readout, and any
|
||||||
|
// other compact label/value stat display share this one shape.
|
||||||
|
Rectangle {
|
||||||
|
id: readoutStat
|
||||||
|
property string label
|
||||||
|
property string value
|
||||||
|
property color valueColor: Theme.headingColor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
implicitHeight: 52
|
||||||
|
radius: Theme.radiusSm
|
||||||
|
color: Theme.subtleSectionBackground
|
||||||
|
border.color: Theme.cardBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 2
|
||||||
|
Label {
|
||||||
|
text: readoutStat.label.toUpperCase()
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.bold: true
|
||||||
|
font.letterSpacing: 0.2
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: readoutStat.value
|
||||||
|
font.pixelSize: Theme.fontLg
|
||||||
|
font.bold: true
|
||||||
|
font.family: Theme.codeFontFamily
|
||||||
|
color: readoutStat.valueColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,619 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import Qt.labs.qmlmodels
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: root
|
||||||
|
title: "Select File"
|
||||||
|
width: 980
|
||||||
|
height: 640
|
||||||
|
modal: true
|
||||||
|
padding: 16
|
||||||
|
topPadding: 0
|
||||||
|
parent: Overlay.overlay
|
||||||
|
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
||||||
|
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
||||||
|
|
||||||
|
signal fileChosen(string path)
|
||||||
|
|
||||||
|
// ===== Dialog State =====
|
||||||
|
property string selectedPath: ""
|
||||||
|
property var tableModel: []
|
||||||
|
property int selectedRow: -1
|
||||||
|
property int metadataEditRow: -1
|
||||||
|
readonly property string masterTypeFieldText: {
|
||||||
|
if (metadataEditRow < 0)
|
||||||
|
return "";
|
||||||
|
const rec = tableModel[metadataEditRow];
|
||||||
|
if (!rec)
|
||||||
|
return "";
|
||||||
|
return rec.masterType ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMetadataEditor(row) {
|
||||||
|
metadataEditRow = row;
|
||||||
|
const rec = tableModel[row];
|
||||||
|
if (!rec) {
|
||||||
|
metadataEditDialog.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
waferField.text = rec.wafer ?? "";
|
||||||
|
dateField.text = rec.date ?? "";
|
||||||
|
chamberField.text = rec.chamber ?? "";
|
||||||
|
if (!chamberField.text && settingsModel.chamberId)
|
||||||
|
chamberField.text = settingsModel.chamberId;
|
||||||
|
notesField.text = rec.notes ?? "";
|
||||||
|
filePathLabel.text = rec.fileName ?? "";
|
||||||
|
metadataEditDialog.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMetadataFromEditor() {
|
||||||
|
if (metadataEditRow < 0)
|
||||||
|
return;
|
||||||
|
const rec = tableModel[metadataEditRow];
|
||||||
|
if (!rec) {
|
||||||
|
metadataEditDialog.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file_browser.saveMetadata(rec.fileName ?? "", waferField.text, dateField.text, chamberField.text, notesField.text, false, "");
|
||||||
|
metadataEditDialog.close();
|
||||||
|
metadataEditRow = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var headerTitles: ["Master", "Wafer", "Date", "Chamber", "Notes", "File", "Edit"]
|
||||||
|
// Build a path→family reverse map from settingsModel.masters.
|
||||||
|
// settingsModel is a QML context property; accessing it here makes the
|
||||||
|
// binding reactive — any mastersChanged signal re-evaluates normalizedRows.
|
||||||
|
function masterFamilyForPath(filePath) {
|
||||||
|
const m = settingsModel.masters;
|
||||||
|
for (var fam in m) {
|
||||||
|
if (m[fam] && m[fam] === filePath)
|
||||||
|
return fam;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property var normalizedRows: {
|
||||||
|
// Touch settingsModel.masters so QML tracks this dependency.
|
||||||
|
const _ = settingsModel.masters;
|
||||||
|
return (tableModel || []).map(function (row) {
|
||||||
|
row = row || {};
|
||||||
|
// settingsModel is the authoritative source; fall back to sidecar masterType.
|
||||||
|
const masterType = root.masterFamilyForPath(row.fileName ?? "") || row.masterType || "";
|
||||||
|
return {
|
||||||
|
masterType: masterType,
|
||||||
|
wafer: row.wafer ?? "",
|
||||||
|
date: row.date ?? "",
|
||||||
|
chamber: row.chamber ?? "",
|
||||||
|
edit: "",
|
||||||
|
notes: row.notes ?? "",
|
||||||
|
fileName: row.fileName ?? "",
|
||||||
|
highlight: row.highlight ?? false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Reusable Controls =====
|
||||||
|
component DialogActionButton: Button {
|
||||||
|
id: dialogButton
|
||||||
|
hoverEnabled: true
|
||||||
|
implicitHeight: 36
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusSm
|
||||||
|
color: dialogButton.down ? Theme.buttonNeutralPressed : dialogButton.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
border.color: Theme.fieldBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: dialogButton.text
|
||||||
|
color: Theme.buttonNeutralText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component DialogTextInput: TextField {
|
||||||
|
id: dialogTextInput
|
||||||
|
color: Theme.fieldText
|
||||||
|
placeholderTextColor: Theme.fieldPlaceholder
|
||||||
|
selectedTextColor: Theme.fieldBackground
|
||||||
|
selectionColor: Theme.fieldText
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
color: Theme.fieldBackground
|
||||||
|
border.width: dialogTextInput.activeFocus ? Theme.borderStrong : Theme.borderThin
|
||||||
|
border.color: dialogTextInput.activeFocus ? Theme.fieldBorderFocus : Theme.fieldBorder
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.durationFast }
|
||||||
|
}
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.durationFast }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Dialog Chrome =====
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusLg
|
||||||
|
color: Theme.panelBackground
|
||||||
|
border.color: Theme.outerFrameBorder
|
||||||
|
border.width: Theme.borderStrong
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom header — replaces the native dark title bar
|
||||||
|
header: Rectangle {
|
||||||
|
width: root.width
|
||||||
|
height: 48
|
||||||
|
radius: Theme.radiusLg
|
||||||
|
color: Theme.subtleSectionBackground
|
||||||
|
border.color: Theme.innerFrameBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
|
||||||
|
// Square off the bottom corners
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: parent.radius
|
||||||
|
color: parent.color
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
text: root.title
|
||||||
|
font.pixelSize: Theme.fontLg
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close button
|
||||||
|
DialogActionButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
implicitWidth: 32
|
||||||
|
implicitHeight: 32
|
||||||
|
onClicked: root.close()
|
||||||
|
contentItem: IconImage {
|
||||||
|
source: "../icons/x.svg"
|
||||||
|
width: 14; height: 14
|
||||||
|
sourceSize.width: 14
|
||||||
|
sourceSize.height: 14
|
||||||
|
color: Theme.buttonNeutralText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Content =====
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
// ===== Browser Toolbar =====
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 12
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
DialogActionButton {
|
||||||
|
text: "Choose Folder"
|
||||||
|
Layout.preferredWidth: 140
|
||||||
|
onClicked: file_browser.chooseDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogActionButton {
|
||||||
|
text: "Refresh"
|
||||||
|
Layout.preferredWidth: 100
|
||||||
|
onClicked: file_browser.refreshFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
Layout.minimumWidth: 80
|
||||||
|
clip: true
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
color: Theme.fieldBackground
|
||||||
|
border.color: Theme.fieldBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: file_browser.currentDirectory
|
||||||
|
color: Theme.fieldText
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== File Table =====
|
||||||
|
HorizontalHeaderView {
|
||||||
|
id: headerView
|
||||||
|
syncView: tableView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
implicitHeight: 34
|
||||||
|
color: Theme.subtleSectionBackground
|
||||||
|
border.color: Theme.innerFrameBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.headerTitles[index] ?? ""
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TableView {
|
||||||
|
id: tableView
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
columnSpacing: 0
|
||||||
|
rowSpacing: 0
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
|
columnWidthProvider: function (col) {
|
||||||
|
// [master, wafer, date, chamber, notes, file (variable), edit]
|
||||||
|
const fixed = {
|
||||||
|
0: 48,
|
||||||
|
1: 90,
|
||||||
|
2: 170,
|
||||||
|
3: 120,
|
||||||
|
4: 180,
|
||||||
|
6: 56
|
||||||
|
};
|
||||||
|
if (col in fixed)
|
||||||
|
return fixed[col];
|
||||||
|
// col 5 (File) takes remaining width
|
||||||
|
const w = Math.max(400, tableView.width);
|
||||||
|
const used = 48 + 90 + 170 + 120 + 180 + 56;
|
||||||
|
return Math.max(120, w - used);
|
||||||
|
}
|
||||||
|
|
||||||
|
model: TableModel {
|
||||||
|
TableModelColumn {
|
||||||
|
display: "masterType"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "wafer"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "date"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "chamber"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "notes"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "fileName"
|
||||||
|
}
|
||||||
|
TableModelColumn {
|
||||||
|
display: "edit"
|
||||||
|
}
|
||||||
|
rows: root.normalizedRows
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property bool current
|
||||||
|
required property int row
|
||||||
|
required property int column
|
||||||
|
|
||||||
|
implicitHeight: 40
|
||||||
|
border.color: Theme.softBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
color: {
|
||||||
|
if (root.selectedRow === row)
|
||||||
|
return Theme.sideActiveBackground;
|
||||||
|
if (root.tableModel[row]?.highlight)
|
||||||
|
return Theme.subtleSectionBackground;
|
||||||
|
return row % 2 === 0 ? Theme.cardBackground : Theme.tone100;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Master type indicator (col 0)
|
||||||
|
// Reads normalizedRows (which merges settingsModel.masters + sidecar).
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: column === 0
|
||||||
|
sourceComponent: Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: {
|
||||||
|
const mt = root.normalizedRows[row]?.masterType ?? "";
|
||||||
|
return mt || "—";
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontMd
|
||||||
|
font.bold: true
|
||||||
|
color: {
|
||||||
|
const mt = root.normalizedRows[row]?.masterType ?? "";
|
||||||
|
return mt ? Theme.primaryAccent : Theme.fieldPlaceholder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edit button (col 6)
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
active: column === 6
|
||||||
|
sourceComponent: Button {
|
||||||
|
hoverEnabled: true
|
||||||
|
text: "Edit…"
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
onClicked: root.openMetadataEditor(row)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
color: parent.down ? Theme.buttonNeutralPressed : parent.hovered ? Theme.buttonNeutralHover : Theme.buttonNeutralBackground
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
border.color: Theme.fieldBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: parent.text
|
||||||
|
color: Theme.buttonNeutralText
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text columns: Wafer (1), Date (2), File (5)
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: column === 1 || column === 2 || column === 5
|
||||||
|
sourceComponent: Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: {
|
||||||
|
const record = root.tableModel[row] || {};
|
||||||
|
if (column === 1)
|
||||||
|
return record.wafer ?? "";
|
||||||
|
if (column === 2)
|
||||||
|
return record.date ?? "";
|
||||||
|
if (column === 5) {
|
||||||
|
const p = record.fileName ?? "";
|
||||||
|
return p.split("/").pop();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
color: Theme.bodyColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text columns: Chamber (3), Notes (4)
|
||||||
|
Loader {
|
||||||
|
anchors.fill: parent
|
||||||
|
active: column === 3 || column === 4
|
||||||
|
sourceComponent: Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: {
|
||||||
|
const record = root.tableModel[row] || {};
|
||||||
|
if (column === 3)
|
||||||
|
return record.chamber ?? "";
|
||||||
|
return record.notes ?? "";
|
||||||
|
}
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
color: Theme.bodyColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: column !== 6
|
||||||
|
onClicked: {
|
||||||
|
root.selectedRow = row;
|
||||||
|
root.selectedPath = root.tableModel[row]?.fileName ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Bottom Bar =====
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
DialogActionButton {
|
||||||
|
text: "OK"
|
||||||
|
Layout.preferredWidth: 90
|
||||||
|
onClicked: {
|
||||||
|
root.fileChosen(root.selectedPath);
|
||||||
|
root.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
Layout.minimumWidth: 120
|
||||||
|
clip: true
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
color: Theme.fieldBackground
|
||||||
|
border.color: Theme.fieldBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: root.selectedPath
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
color: root.selectedPath ? Theme.fieldText : Theme.fieldPlaceholder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Metadata Editor Dialog =====
|
||||||
|
Dialog {
|
||||||
|
id: metadataEditDialog
|
||||||
|
parent: Overlay.overlay
|
||||||
|
modal: true
|
||||||
|
title: qsTr("Edit Metadata")
|
||||||
|
width: 520
|
||||||
|
height: 520
|
||||||
|
padding: 16
|
||||||
|
topPadding: 0
|
||||||
|
x: Math.round(((parent ? parent.width : width) - width) / 2)
|
||||||
|
y: Math.round(((parent ? parent.height : height) - height) / 2)
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: Theme.radiusLg
|
||||||
|
color: Theme.panelBackground
|
||||||
|
border.color: Theme.outerFrameBorder
|
||||||
|
border.width: Theme.borderStrong
|
||||||
|
}
|
||||||
|
|
||||||
|
header: Rectangle {
|
||||||
|
width: metadataEditDialog.width
|
||||||
|
height: 48
|
||||||
|
radius: Theme.radiusLg
|
||||||
|
color: Theme.subtleSectionBackground
|
||||||
|
border.color: Theme.innerFrameBorder
|
||||||
|
border.width: Theme.borderThin
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: parent.radius
|
||||||
|
color: parent.color
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 16
|
||||||
|
text: metadataEditDialog.title
|
||||||
|
font.pixelSize: Theme.fontLg
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
spacing: Theme.settingsGridSpacing
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("File")
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.headingColor
|
||||||
|
Layout.topMargin: 8
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: filePathLabel
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.maximumHeight: 56
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
wrapMode: Text.WrapAnywhere
|
||||||
|
maximumLineCount: 3
|
||||||
|
elide: Text.ElideRight
|
||||||
|
color: Theme.bodyColor
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Wafer")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: waferField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Date")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: dateField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Chamber")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: chamberField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Notes")
|
||||||
|
color: Theme.headingColor
|
||||||
|
}
|
||||||
|
DialogTextInput {
|
||||||
|
id: notesField
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 36
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: 16
|
||||||
|
Layout.bottomMargin: 16
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogActionButton {
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
Layout.preferredWidth: 90
|
||||||
|
onClicked: {
|
||||||
|
root.metadataEditRow = -1;
|
||||||
|
metadataEditDialog.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogActionButton {
|
||||||
|
text: qsTr("Save")
|
||||||
|
Layout.preferredWidth: 90
|
||||||
|
onClicked: root.applyMetadataFromEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-16
@@ -9,12 +9,22 @@ from PySide6.QtWidgets import QApplication
|
|||||||
|
|
||||||
from pygui.backend.controllers.device_controller import DeviceController
|
from pygui.backend.controllers.device_controller import DeviceController
|
||||||
from pygui.backend.controllers.session_controller import SessionController
|
from pygui.backend.controllers.session_controller import SessionController
|
||||||
|
from pygui.backend.data.constants import default_data_dir
|
||||||
from pygui.backend.data.file_browser import FileBrowser
|
from pygui.backend.data.file_browser import FileBrowser
|
||||||
from pygui.backend.data.local_settings import LocalSettings
|
from pygui.backend.data.local_settings import LocalSettings
|
||||||
from pygui.backend.data.local_settings_model import LocalSettingsModel
|
from pygui.backend.data.local_settings_model import LocalSettingsModel
|
||||||
from pygui.backend.license.license_model import LicenseModel
|
from pygui.backend.license.license_model import LicenseModel
|
||||||
|
|
||||||
# Importing wafer_map_item + trend_chart_item registers @QmlElement types (QML: import ISC.Wafer).
|
# Importing wafer_map_item + trend_chart_item registers @QmlElement types (QML: import ISC.Wafer).
|
||||||
|
from pygui.backend.visualization import (
|
||||||
|
graph_quick_item as graph_quick_item, # noqa: F401
|
||||||
|
)
|
||||||
|
from pygui.backend.visualization import (
|
||||||
|
trend_chart_item as trend_chart_item, # noqa: F401
|
||||||
|
)
|
||||||
|
from pygui.backend.visualization import (
|
||||||
|
wafer_map_item as wafer_map_item, # noqa: F401
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ===== Application Entry Point =====
|
# ===== Application Entry Point =====
|
||||||
@@ -32,8 +42,15 @@ def main() -> int:
|
|||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
engine = QQmlApplicationEngine()
|
engine = QQmlApplicationEngine()
|
||||||
|
|
||||||
# ===== Shared Models =====
|
# ===== Shared Settings =====
|
||||||
settings_model = LocalSettingsModel()
|
# One LocalSettings instance, one disk read. LocalSettingsModel and
|
||||||
|
# DeviceController both hold this same object, so a save from the
|
||||||
|
# Settings tab is visible to the device layer immediately — no reload,
|
||||||
|
# no second copy to go stale.
|
||||||
|
data_dir = str(default_data_dir())
|
||||||
|
raw_settings = LocalSettings.read_settings(data_dir)
|
||||||
|
|
||||||
|
settings_model = LocalSettingsModel(raw_settings, data_dir)
|
||||||
select_file_dialog_model = FileBrowser()
|
select_file_dialog_model = FileBrowser()
|
||||||
settings_model.loadSettings()
|
settings_model.loadSettings()
|
||||||
engine.rootContext().setContextProperty("settingsModel", settings_model)
|
engine.rootContext().setContextProperty("settingsModel", settings_model)
|
||||||
@@ -54,8 +71,6 @@ def main() -> int:
|
|||||||
# read/erase time (P2.1); a bound Callable[[str], str] avoids a
|
# read/erase time (P2.1); a bound Callable[[str], str] avoids a
|
||||||
# LicenseModel import in the controller and keeps it testable with a
|
# LicenseModel import in the controller and keeps it testable with a
|
||||||
# lambda. See docs/pending/license-gating-plan.md §1.1.
|
# lambda. See docs/pending/license-gating-plan.md §1.1.
|
||||||
data_dir = str(settings_model._data_dir)
|
|
||||||
raw_settings = LocalSettings.read_settings(data_dir)
|
|
||||||
device_controller = DeviceController(raw_settings, data_dir)
|
device_controller = DeviceController(raw_settings, data_dir)
|
||||||
engine.rootContext().setContextProperty("deviceController", device_controller)
|
engine.rootContext().setContextProperty("deviceController", device_controller)
|
||||||
|
|
||||||
@@ -64,20 +79,9 @@ def main() -> int:
|
|||||||
engine.rootContext().setContextProperty("licenseModel", license_model)
|
engine.rootContext().setContextProperty("licenseModel", license_model)
|
||||||
|
|
||||||
# ===== Session Controller (live/review wafer dashboard) =====
|
# ===== Session Controller (live/review wafer dashboard) =====
|
||||||
raw_settings_dict = raw_settings.__dict__.copy() if hasattr(raw_settings, '__dict__') else {}
|
stream_controller = SessionController()
|
||||||
stream_controller = SessionController(settings=raw_settings_dict)
|
|
||||||
engine.rootContext().setContextProperty("streamController", stream_controller)
|
engine.rootContext().setContextProperty("streamController", stream_controller)
|
||||||
|
|
||||||
# Persist session state back to settings when it changes
|
|
||||||
def _persist_session_settings():
|
|
||||||
session_state = stream_controller.collect_settings()
|
|
||||||
for k, v in session_state.items():
|
|
||||||
if hasattr(raw_settings, k):
|
|
||||||
setattr(raw_settings, k, v)
|
|
||||||
LocalSettings.save_settings(data_dir, raw_settings)
|
|
||||||
|
|
||||||
stream_controller.settingsChanged.connect(_persist_session_settings)
|
|
||||||
|
|
||||||
# ===== QML Startup =====
|
# ===== QML Startup =====
|
||||||
# The "ISC" QML module lives alongside this file (src/pygui/ISC), so the
|
# The "ISC" QML module lives alongside this file (src/pygui/ISC), so the
|
||||||
# package directory is the import path the engine searches for qmldir.
|
# package directory is the import path the engine searches for qmldir.
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ from typing import Any, Optional
|
|||||||
|
|
||||||
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
from PySide6.QtCore import Property, QObject, Qt, Signal, Slot
|
||||||
|
|
||||||
from pygui.backend.controllers.session_controller import SessionController
|
|
||||||
from pygui.backend.data.local_settings import LocalSettings
|
from pygui.backend.data.local_settings import LocalSettings
|
||||||
from pygui.backend.models.data_model import TemperatureTableModel
|
from pygui.backend.models.data_model import TemperatureTableModel
|
||||||
from pygui.backend.utils import slot_error_boundary
|
from pygui.backend.utils import slot_error_boundary
|
||||||
@@ -26,11 +25,6 @@ from pygui.serialcomm.data_parser import (
|
|||||||
)
|
)
|
||||||
from pygui.serialcomm.serial_port import WaferInfo
|
from pygui.serialcomm.serial_port import WaferInfo
|
||||||
|
|
||||||
# import pygui.backend.wafer_map_item
|
|
||||||
|
|
||||||
stream_controller = SessionController()
|
|
||||||
# engine.rootContext().setContextProperty("streamController", stream_controller)
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user