refactor(qml): extract sidebar panels and data tab comparison cards to modular components
This commit is contained in:
+17
-349
@@ -17,20 +17,6 @@ Rectangle {
|
|||||||
border.color: Theme.outerFrameBorder
|
border.color: Theme.outerFrameBorder
|
||||||
border.width: Theme.borderStrong
|
border.width: Theme.borderStrong
|
||||||
|
|
||||||
// ===== Global ToolTip Styling =====
|
|
||||||
ToolTip.toolTip.background: Rectangle {
|
|
||||||
color: Theme.cardBackground
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
radius: Theme.radiusXs
|
|
||||||
}
|
|
||||||
ToolTip.toolTip.contentItem: Text {
|
|
||||||
text: ToolTip.toolTip.text
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== View State =====
|
// ===== View State =====
|
||||||
property int selectedTabIndex: 0
|
property int selectedTabIndex: 0
|
||||||
onSelectedTabIndexChanged: {
|
onSelectedTabIndexChanged: {
|
||||||
@@ -420,43 +406,6 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: "Tabs/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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: runCompCol
|
id: runCompCol
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
@@ -498,8 +447,10 @@ Rectangle {
|
|||||||
color: Theme.bodyColor
|
color: Theme.bodyColor
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
}
|
}
|
||||||
ToolTip.visible: resetRunBtn.hovered
|
AppToolTip {
|
||||||
ToolTip.text: "Reset comparison"
|
visible: resetRunBtn.hovered
|
||||||
|
text: "Reset comparison"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,11 +513,13 @@ Rectangle {
|
|||||||
readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB
|
readonly property bool sameFile: !!dt && fileA !== "" && fileA === dt.compareFileB
|
||||||
readonly property string gateError: !!dt ? dt.familyGateError : ""
|
readonly property string gateError: !!dt ? dt.familyGateError : ""
|
||||||
enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing && gateError === ""
|
enabled: !!dt && fileA !== "" && dt.compareFileB !== "" && !sameFile && !dt.comparing && !masterMissing && gateError === ""
|
||||||
ToolTip.visible: (sameFile || masterMissing || gateError !== "") && hovered
|
AppToolTip {
|
||||||
ToolTip.text: masterMissing
|
visible: (compareRunBtn.sameFile || compareRunBtn.masterMissing || compareRunBtn.gateError !== "") && compareRunBtn.hovered
|
||||||
? "No master file registered for this wafer type (Settings → Master Files)"
|
text: compareRunBtn.masterMissing
|
||||||
: gateError !== "" ? gateError
|
? "No master file registered for this wafer type (Settings → Master Files)"
|
||||||
: "Choose two different runs to compare"
|
: compareRunBtn.gateError !== "" ? compareRunBtn.gateError
|
||||||
|
: "Choose two different runs to compare"
|
||||||
|
}
|
||||||
onClicked: {
|
onClicked: {
|
||||||
dt.comparing = true
|
dt.comparing = true
|
||||||
dt.clearResults()
|
dt.clearResults()
|
||||||
@@ -617,100 +570,9 @@ Rectangle {
|
|||||||
currentIndex: root.selectedTabIndex === 0 ? 0 : 1
|
currentIndex: root.selectedTabIndex === 0 ? 0 : 1
|
||||||
|
|
||||||
// ── Status tab: Hardware Actions + Log Actions ──────────
|
// ── Status tab: Hardware Actions + Log Actions ──────────
|
||||||
ColumnLayout {
|
StatusActionsPanel {
|
||||||
spacing: Theme.sideRailSpacing
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: hwActionsCol.implicitHeight + 28
|
|
||||||
radius: Theme.sidePanelRadius
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
color: Theme.sidePanelBackground
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: hwActionsCol
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 14
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "HARDWARE ACTIONS"
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
font.letterSpacing: 1.5
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
RailActionButton {
|
|
||||||
label: "DETECT WAFER"
|
|
||||||
iconSource: "../icons/detect.svg"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
enabled: !deviceController.operationInProgress
|
|
||||||
onClicked: root._doDetect()
|
|
||||||
}
|
|
||||||
|
|
||||||
RailActionButton {
|
|
||||||
label: "READ MEMORY"
|
|
||||||
iconSource: "../icons/read.svg"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
enabled: root.waferDetected
|
|
||||||
onClicked: deviceController.readMemoryAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
RailActionButton {
|
|
||||||
label: "ERASE MEMORY"
|
|
||||||
iconSource: "../icons/erase.svg"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
destructive: true
|
|
||||||
enabled: root.waferDetected
|
|
||||||
onClicked: deviceController.eraseMemory(
|
|
||||||
deviceController.selectedPort)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.preferredHeight: logActionsCol.implicitHeight + 28
|
|
||||||
radius: Theme.sidePanelRadius
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
color: Theme.sidePanelBackground
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: logActionsCol
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 14
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
Label {
|
|
||||||
text: "LOG ACTIONS"
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
font.letterSpacing: 1.5
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
RailActionButton {
|
|
||||||
label: "REFRESH SESSION"
|
|
||||||
iconSource: "../icons/refresh.svg"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
onClicked: deviceController.clearSession()
|
|
||||||
}
|
|
||||||
|
|
||||||
RailActionButton {
|
|
||||||
label: "CLEAR LOG"
|
|
||||||
iconSource: "../icons/x.svg"
|
|
||||||
Layout.fillWidth: true
|
|
||||||
onClicked: deviceController.clearActivityLog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillHeight: true }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Map tab: Source file browser ──────────────
|
// ── Map tab: Source file browser ──────────────
|
||||||
@@ -728,208 +590,14 @@ Rectangle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── BOX 3: Hardware Status Footer ──────────────────────────
|
// ── BOX 3: Hardware Status Footer ──────────────────────────
|
||||||
// Not relevant on the Data tab — Run Comparison takes its place above.
|
ConnectionFooter {
|
||||||
Rectangle {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: Theme.sideFooterConnection
|
|
||||||
visible: root.selectedTabIndex !== 1
|
|
||||||
radius: Theme.sidePanelRadius
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
color: Theme.sidePanelBackground
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 14
|
|
||||||
spacing: 6
|
|
||||||
|
|
||||||
// Row 1: Title
|
|
||||||
Label {
|
|
||||||
text: "CONNECTION FLOW"
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
font.letterSpacing: 1.5
|
|
||||||
font.bold: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Row 2: Port / descriptive text
|
|
||||||
Label {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
text: {
|
|
||||||
if (deviceController.connectionStatus === "Connected")
|
|
||||||
return deviceController.selectedPort || "Connected"
|
|
||||||
if (deviceController.selectedPort)
|
|
||||||
return deviceController.selectedPort
|
|
||||||
return "No port selected"
|
|
||||||
}
|
|
||||||
color: Theme.bodyColor
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontSm
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillHeight: true }
|
|
||||||
|
|
||||||
// Row 3: Mode chip left, status badge bottom-right
|
|
||||||
RowLayout {
|
|
||||||
spacing: 6
|
|
||||||
Layout.fillWidth: true
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: modeChipLabel.implicitWidth + 12
|
|
||||||
height: 18
|
|
||||||
radius: 4
|
|
||||||
color: Theme.fieldBackground
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: modeChipLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: streamController.mode
|
|
||||||
? streamController.mode.toUpperCase() : "REVIEW"
|
|
||||||
color: Theme.sideMutedText
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { Layout.fillWidth: true }
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
height: 18
|
|
||||||
width: badgeLabel.implicitWidth + 14
|
|
||||||
radius: 9
|
|
||||||
color: {
|
|
||||||
var s = deviceController.connectionStatus
|
|
||||||
if (s === "Connected") return Theme.statusSuccessColor
|
|
||||||
if (s === "Disconnected") return Theme.statusErrorColor
|
|
||||||
return Theme.statusWarningColor
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
id: badgeLabel
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: deviceController.connectionStatus || "Disconnected"
|
|
||||||
color: Theme.statusBadgeText
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── BOX 4: Utility Buttons ─────────────────────────────────
|
// ── BOX 4: Utility Buttons ─────────────────────────────────
|
||||||
Rectangle {
|
UtilityFooter {
|
||||||
|
id: utilFooter
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: Theme.sideFooterUtility
|
|
||||||
radius: Theme.sidePanelRadius
|
|
||||||
border.color: Theme.sideBorder
|
|
||||||
border.width: 1
|
|
||||||
color: Theme.sidePanelBackground
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 4
|
|
||||||
spacing: 4
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: settingsBtn
|
|
||||||
flat: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
icon.source: "Tabs/icons/settings.svg"
|
|
||||||
onClicked: settingsPopup.open()
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: 8
|
|
||||||
color: settingsBtn.hovered
|
|
||||||
? Theme.sideActiveBackground : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Row {
|
|
||||||
spacing: 6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
source: "Tabs/icons/settings.svg"
|
|
||||||
sourceSize.width: 16
|
|
||||||
sourceSize.height: 16
|
|
||||||
color: Theme.headingColor
|
|
||||||
opacity: 0.75
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "SETTINGS"
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 1.2
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 1
|
|
||||||
height: 24
|
|
||||||
color: Theme.sideBorder
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
id: aboutBtn
|
|
||||||
flat: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
icon.source: "Tabs/icons/about.svg"
|
|
||||||
onClicked: aboutDialog.open()
|
|
||||||
|
|
||||||
background: Rectangle {
|
|
||||||
radius: 8
|
|
||||||
color: aboutBtn.hovered
|
|
||||||
? Theme.sideActiveBackground : "transparent"
|
|
||||||
}
|
|
||||||
|
|
||||||
contentItem: Row {
|
|
||||||
spacing: 6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
IconImage {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 16
|
|
||||||
height: 16
|
|
||||||
source: "Tabs/icons/about.svg"
|
|
||||||
sourceSize.width: 16
|
|
||||||
sourceSize.height: 16
|
|
||||||
color: Theme.headingColor
|
|
||||||
opacity: 0.75
|
|
||||||
}
|
|
||||||
|
|
||||||
Label {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "ABOUT"
|
|
||||||
color: Theme.headingColor
|
|
||||||
font.family: Theme.uiFontFamily
|
|
||||||
font.pixelSize: Theme.fontXs
|
|
||||||
font.bold: true
|
|
||||||
font.letterSpacing: 1.2
|
|
||||||
opacity: 0.8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,619 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Single seam for tooltip chrome — every hover tip in the app instantiates
|
||||||
|
// this instead of styling QtQuick.Controls ToolTip.background/contentItem
|
||||||
|
// inline at each call site.
|
||||||
|
ToolTip {
|
||||||
|
id: root
|
||||||
|
delay: 400
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
|
||||||
|
contentItem: Text {
|
||||||
|
text: root.text
|
||||||
|
color: Theme.headingColor
|
||||||
|
font: root.font
|
||||||
|
}
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.cardBackground
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
radius: Theme.radiusXs
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Curve overlay chart card with the alignment timeline scrubber.
|
||||||
|
PanelBox {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
required property var trendDataA
|
||||||
|
required property var trendDataB
|
||||||
|
required property int seriesLen
|
||||||
|
required property bool hasSeries
|
||||||
|
required property bool comparing
|
||||||
|
|
||||||
|
// Scrubber position — the internal Slider is private; consumers
|
||||||
|
// read/write frameIndex and call focusScrubber().
|
||||||
|
property alias frameIndex: scrubber.value
|
||||||
|
|
||||||
|
function focusScrubber() { scrubber.forceActiveFocus() }
|
||||||
|
|
||||||
|
implicitHeight: chartCol.implicitHeight + 24
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: chartCol
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
SectionTitle {
|
||||||
|
text: "CURVE OVERLAY"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
spacing: 16
|
||||||
|
visible: card.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: card.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 (card.seriesLen < 2) return
|
||||||
|
var plotW = width - padLeft - padRight
|
||||||
|
var x = padLeft + (scrubber.value / (card.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 (!card.hasSeries) return
|
||||||
|
var all = card.trendDataA.concat(card.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, card.seriesLen)
|
||||||
|
drawSeries(ctx, card.trendDataA, minV, range, Theme.primaryAccent, card.seriesLen)
|
||||||
|
drawSeries(ctx, card.trendDataB, minV, range, Theme.themeSkill, card.seriesLen)
|
||||||
|
drawScrubMarker(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: card
|
||||||
|
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
|
||||||
|
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: scrubber
|
||||||
|
function onValueChanged() { overlayCanvas.requestPaint() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !card.hasSeries
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: !card.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: card.comparing ? "Running DTW comparison…" : "Select two runs and run comparison"
|
||||||
|
color: Theme.bodyColor
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: !card.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: card.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, card.seriesLen - 1)
|
||||||
|
stepSize: 1
|
||||||
|
value: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Right-column bento stack: DTW readout, overlap settings, frame/time
|
||||||
|
// controls, and scrubber readout. Pure view — all comparison state lives
|
||||||
|
// on DataTab; scrubber writes go up via frameIndexRequested.
|
||||||
|
ColumnLayout {
|
||||||
|
id: panel
|
||||||
|
|
||||||
|
required property real warpingDistance
|
||||||
|
required property int frameOffset
|
||||||
|
required property real frameOffsetSeconds
|
||||||
|
required property real maxSensorDeviation
|
||||||
|
required property int seriesLen
|
||||||
|
required property bool hasSeries
|
||||||
|
required property var trendDataA
|
||||||
|
required property var trendDataB
|
||||||
|
required property var timeDataA
|
||||||
|
required property var timeDataB
|
||||||
|
required property var frameForTime // function(seconds) -> frame index
|
||||||
|
required property real frameIndex // bound from ComparisonChartCard.frameIndex
|
||||||
|
|
||||||
|
// Overlap settings outputs (consumed by OverlapMapCard via DataTab)
|
||||||
|
property alias overlapEnabled: enableOverlapCheck.checked
|
||||||
|
property alias blendAmount: blendSlider.value
|
||||||
|
|
||||||
|
signal frameIndexRequested(real idx)
|
||||||
|
signal scrubberFocusRequested()
|
||||||
|
|
||||||
|
spacing: 12
|
||||||
|
|
||||||
|
// Bento 3: DTW Comparison Readout
|
||||||
|
PanelBox {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: readoutCol.implicitHeight + 24
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: readoutCol
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SectionTitle { text: "DTW READOUT" }
|
||||||
|
|
||||||
|
ReadoutStat {
|
||||||
|
label: "DTW ALIGNMENT DISTANCE"
|
||||||
|
value: panel.warpingDistance >= 0 ? panel.warpingDistance.toFixed(2) : "--"
|
||||||
|
valueColor: panel.warpingDistance >= 0
|
||||||
|
? (panel.warpingDistance < 50 ? Theme.metricGood : panel.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad)
|
||||||
|
: Theme.sideMutedText
|
||||||
|
}
|
||||||
|
ReadoutStat {
|
||||||
|
label: "TEMPORAL FRAME OFFSET"
|
||||||
|
value: panel.warpingDistance >= 0
|
||||||
|
? ((panel.frameOffset >= 0 ? "+" : "") + panel.frameOffset + " frames / "
|
||||||
|
+ (panel.frameOffsetSeconds >= 0 ? "+" : "") + panel.frameOffsetSeconds.toFixed(1) + "s")
|
||||||
|
: "--"
|
||||||
|
valueColor: panel.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
|
||||||
|
}
|
||||||
|
ReadoutStat {
|
||||||
|
label: "MAX SENSOR DEVIATION"
|
||||||
|
value: panel.maxSensorDeviation >= 0 ? panel.maxSensorDeviation.toFixed(2) + "°C" : "--"
|
||||||
|
valueColor: panel.maxSensorDeviation >= 0
|
||||||
|
? (panel.maxSensorDeviation < 1.0 ? Theme.metricGood : panel.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, panel.seriesLen - 1) + (Math.max(0, panel.seriesLen - 1) <= 1 ? " frame" : " frames")
|
||||||
|
editable: panel.hasSeries
|
||||||
|
onValueEdited: (newValue) => {
|
||||||
|
var maxVal = Math.max(0, panel.seriesLen - 1)
|
||||||
|
var val = Math.max(0, Math.min(newValue, maxVal))
|
||||||
|
panel.frameIndexRequested(val)
|
||||||
|
}
|
||||||
|
onEditingDone: panel.scrubberFocusRequested()
|
||||||
|
}
|
||||||
|
|
||||||
|
EditableScrubStat {
|
||||||
|
label: "Time"
|
||||||
|
value: scrubReadout.scrubTime
|
||||||
|
unit: "/ " + scrubReadout.maxTime.toFixed(1) + "s"
|
||||||
|
editable: panel.hasSeries
|
||||||
|
onValueEdited: (newValue) => {
|
||||||
|
var closestIdx = panel.frameForTime(newValue)
|
||||||
|
panel.frameIndexRequested(closestIdx)
|
||||||
|
}
|
||||||
|
onEditingDone: panel.scrubberFocusRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(panel.frameIndex)
|
||||||
|
readonly property bool hasA: scrubIdx < panel.trendDataA.length
|
||||||
|
readonly property bool hasB: scrubIdx < panel.trendDataB.length
|
||||||
|
readonly property real tempA: hasA ? panel.trendDataA[scrubIdx] : 0
|
||||||
|
readonly property real tempB: hasB ? panel.trendDataB[scrubIdx] : 0
|
||||||
|
readonly property real scrubTime: {
|
||||||
|
var t = scrubIdx < panel.timeDataA.length ? panel.timeDataA[scrubIdx]
|
||||||
|
: (scrubIdx < panel.timeDataB.length ? panel.timeDataB[scrubIdx] : 0)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
readonly property real maxTime: {
|
||||||
|
var ta = panel.timeDataA.length > 0 ? panel.timeDataA[panel.timeDataA.length - 1] : 0
|
||||||
|
var tb = panel.timeDataB.length > 0 ? panel.timeDataB[panel.timeDataB.length - 1] : 0
|
||||||
|
return Math.max(ta, tb)
|
||||||
|
}
|
||||||
|
|
||||||
|
SectionTitle { text: "SCRUBBER READOUT" }
|
||||||
|
|
||||||
|
ReadoutStat {
|
||||||
|
label: "Temp (Run A)"
|
||||||
|
value: scrubReadout.hasA ? scrubReadout.tempA.toFixed(1) + "°C" : "none"
|
||||||
|
valueColor: Theme.primaryAccent
|
||||||
|
}
|
||||||
|
ReadoutStat {
|
||||||
|
label: "Temp (Run B)"
|
||||||
|
value: scrubReadout.hasB ? scrubReadout.tempB.toFixed(1) + "°C" : "none"
|
||||||
|
valueColor: Theme.themeSkill
|
||||||
|
}
|
||||||
|
ReadoutStat {
|
||||||
|
label: "Difference"
|
||||||
|
value: (scrubReadout.hasA && scrubReadout.hasB)
|
||||||
|
? Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C" : "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// ── BOX 3: Hardware Status Footer ──────────────────────────
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Theme.sideFooterConnection
|
||||||
|
visible: root.selectedTabIndex !== 1
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 14
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
// Row 1: Title
|
||||||
|
Label {
|
||||||
|
text: "CONNECTION FLOW"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Row 2: Port / descriptive text
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: {
|
||||||
|
if (deviceController.connectionStatus === "Connected")
|
||||||
|
return deviceController.selectedPort || "Connected"
|
||||||
|
if (deviceController.selectedPort)
|
||||||
|
return deviceController.selectedPort
|
||||||
|
return "No port selected"
|
||||||
|
}
|
||||||
|
color: Theme.bodyColor
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
|
||||||
|
// Row 3: Mode chip left, status badge bottom-right
|
||||||
|
RowLayout {
|
||||||
|
spacing: 6
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: modeChipLabel.implicitWidth + 12
|
||||||
|
height: 18
|
||||||
|
radius: 4
|
||||||
|
color: Theme.fieldBackground
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: modeChipLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: streamController.mode
|
||||||
|
? streamController.mode.toUpperCase() : "REVIEW"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.bold: true
|
||||||
|
font.letterSpacing: 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
height: 18
|
||||||
|
width: badgeLabel.implicitWidth + 14
|
||||||
|
radius: 9
|
||||||
|
color: {
|
||||||
|
var s = deviceController.connectionStatus
|
||||||
|
if (s === "Connected") return Theme.statusSuccessColor
|
||||||
|
if (s === "Disconnected") return Theme.statusErrorColor
|
||||||
|
return Theme.statusWarningColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
id: badgeLabel
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: deviceController.connectionStatus || "Disconnected"
|
||||||
|
color: Theme.statusBadgeText
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Editable stat box for scrubber readout (styled to match DTW ReadoutCard)
|
||||||
|
Rectangle {
|
||||||
|
id: edStat
|
||||||
|
property string label
|
||||||
|
property real value
|
||||||
|
property string unit
|
||||||
|
property string extraText: ""
|
||||||
|
property bool editable: true
|
||||||
|
signal valueEdited(real newValue)
|
||||||
|
// Fires when the input field commits (Enter/focus loss), even if the
|
||||||
|
// entered text was invalid — callers use it to hand focus back to the
|
||||||
|
// timeline scrubber, matching the pre-extraction behavior.
|
||||||
|
signal editingDone()
|
||||||
|
|
||||||
|
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)
|
||||||
|
edStat.editingDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
import ISC.Wafer
|
||||||
|
|
||||||
|
// Wafer overlap map card — renders per-sensor Run A/Run B temperature
|
||||||
|
// differences at the current scrub position.
|
||||||
|
PanelBox {
|
||||||
|
id: card
|
||||||
|
|
||||||
|
required property var sensorLayout
|
||||||
|
required property var diffValues
|
||||||
|
required property var diffBands // function(values) -> band thresholds
|
||||||
|
required property string waferShape
|
||||||
|
required property real waferSize
|
||||||
|
required property bool overlapEnabled
|
||||||
|
required property real blendAmount // 0–100, percent
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 12
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
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: card.sensorLayout.length > 0 && card.overlapEnabled
|
||||||
|
sensors: card.sensorLayout
|
||||||
|
values: card.diffValues
|
||||||
|
bands: card.diffBands(overlapMapItem.values)
|
||||||
|
shape: card.waferShape
|
||||||
|
size: card.waferSize
|
||||||
|
target: 0
|
||||||
|
margin: 1.0
|
||||||
|
blend: card.blendAmount / 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: card.sensorLayout.length === 0 || !card.overlapEnabled
|
||||||
|
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: !card.overlapEnabled ? "Overlap map disabled"
|
||||||
|
: "No sensor map available"
|
||||||
|
color: Theme.bodyColor
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import QtQuick
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Card container for the Data tab bento boxes.
|
||||||
|
Rectangle {
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Checkbox styled to match MapTab's right-rail ReadoutPanel (square accent
|
||||||
|
// indicator + check icon) instead of the platform-default CheckBox look.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Slider styled to match MapTab's right-rail heatmap slider (circular
|
||||||
|
// accent handle that grows slightly on press).
|
||||||
|
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 } }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// Uppercase card heading — shared by the Data tab cards.
|
||||||
|
Label {
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
import ISC.Tabs.components
|
||||||
|
|
||||||
|
// ── Status tab: Hardware Actions + Log Actions ──────────
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Theme.sideRailSpacing
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: hwActionsCol.implicitHeight + 28
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: hwActionsCol
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 14
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "HARDWARE ACTIONS"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RailActionButton {
|
||||||
|
label: "DETECT WAFER"
|
||||||
|
iconSource: "../icons/detect.svg"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: !deviceController.operationInProgress
|
||||||
|
onClicked: root._doDetect()
|
||||||
|
}
|
||||||
|
|
||||||
|
RailActionButton {
|
||||||
|
label: "READ MEMORY"
|
||||||
|
iconSource: "../icons/read.svg"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: root.waferDetected
|
||||||
|
onClicked: deviceController.readMemoryAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
RailActionButton {
|
||||||
|
label: "ERASE MEMORY"
|
||||||
|
iconSource: "../icons/erase.svg"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
destructive: true
|
||||||
|
enabled: root.waferDetected
|
||||||
|
onClicked: deviceController.eraseMemory(
|
||||||
|
deviceController.selectedPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: logActionsCol.implicitHeight + 28
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: logActionsCol
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 14
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: "LOG ACTIONS"
|
||||||
|
color: Theme.sideMutedText
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontSm
|
||||||
|
font.letterSpacing: 1.5
|
||||||
|
font.bold: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RailActionButton {
|
||||||
|
label: "REFRESH SESSION"
|
||||||
|
iconSource: "../icons/refresh.svg"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
onClicked: deviceController.clearSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
RailActionButton {
|
||||||
|
label: "CLEAR LOG"
|
||||||
|
iconSource: "../icons/x.svg"
|
||||||
|
Layout.fillWidth: true
|
||||||
|
onClicked: deviceController.clearActivityLog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item { Layout.fillHeight: true }
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.impl
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ISC
|
||||||
|
|
||||||
|
// ── BOX 4: Utility Buttons ─────────────────────────────────
|
||||||
|
Rectangle {
|
||||||
|
id: utilFooter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Theme.sideFooterUtility
|
||||||
|
radius: Theme.sidePanelRadius
|
||||||
|
border.color: Theme.sideBorder
|
||||||
|
border.width: 1
|
||||||
|
color: Theme.sidePanelBackground
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 4
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: settingsBtn
|
||||||
|
flat: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
icon.source: "../icons/settings.svg"
|
||||||
|
onClicked: utilFooter.parent.settingsPopup.open()
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 8
|
||||||
|
color: settingsBtn.hovered
|
||||||
|
? Theme.sideActiveBackground : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Row {
|
||||||
|
spacing: 6
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
source: "../icons/settings.svg"
|
||||||
|
sourceSize.width: 16
|
||||||
|
sourceSize.height: 16
|
||||||
|
color: Theme.headingColor
|
||||||
|
opacity: 0.75
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "SETTINGS"
|
||||||
|
color: Theme.headingColor
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.bold: true
|
||||||
|
font.letterSpacing: 1.2
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 1
|
||||||
|
height: 24
|
||||||
|
color: Theme.sideBorder
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
id: aboutBtn
|
||||||
|
flat: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
icon.source: "../icons/about.svg"
|
||||||
|
onClicked: utilFooter.parent.aboutDialog.open()
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 8
|
||||||
|
color: aboutBtn.hovered
|
||||||
|
? Theme.sideActiveBackground : "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Row {
|
||||||
|
spacing: 6
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
IconImage {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
source: "../icons/about.svg"
|
||||||
|
sourceSize.width: 16
|
||||||
|
sourceSize.height: 16
|
||||||
|
color: Theme.headingColor
|
||||||
|
opacity: 0.75
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: "ABOUT"
|
||||||
|
color: Theme.headingColor
|
||||||
|
font.family: Theme.uiFontFamily
|
||||||
|
font.pixelSize: Theme.fontXs
|
||||||
|
font.bold: true
|
||||||
|
font.letterSpacing: 1.2
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,5 +6,19 @@ TransportBar 1.0 TransportBar.qml
|
|||||||
WaferMapView 1.0 WaferMapView.qml
|
WaferMapView 1.0 WaferMapView.qml
|
||||||
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
|
||||||
RailActionButton 1.0 RailActionButton.qml
|
RailActionButton 1.0 RailActionButton.qml
|
||||||
|
SelectFileDialog 1.0 SelectFileDialog.qml
|
||||||
SplitDialog 1.0 SplitDialog.qml
|
SplitDialog 1.0 SplitDialog.qml
|
||||||
StreamStatsDialog 1.0 StreamStatsDialog.qml
|
StreamStatsDialog 1.0 StreamStatsDialog.qml
|
||||||
|
StatusActionsPanel 1.0 StatusActionsPanel.qml
|
||||||
|
ConnectionFooter 1.0 ConnectionFooter.qml
|
||||||
|
UtilityFooter 1.0 UtilityFooter.qml
|
||||||
|
PanelCheckBox 1.0 PanelCheckBox.qml
|
||||||
|
ReadoutStat 1.0 ReadoutStat.qml
|
||||||
|
AppToolTip 1.0 AppToolTip.qml
|
||||||
|
SectionTitle 1.0 SectionTitle.qml
|
||||||
|
PanelBox 1.0 PanelBox.qml
|
||||||
|
PanelSlider 1.0 PanelSlider.qml
|
||||||
|
EditableScrubStat 1.0 EditableScrubStat.qml
|
||||||
|
ComparisonChartCard 1.0 ComparisonChartCard.qml
|
||||||
|
OverlapMapCard 1.0 OverlapMapCard.qml
|
||||||
|
ComparisonSidePanel 1.0 ComparisonSidePanel.qml
|
||||||
@@ -5,4 +5,3 @@ WaferMapTab 1.0 WaferMapTab.qml
|
|||||||
DataTab 1.0 DataTab.qml
|
DataTab 1.0 DataTab.qml
|
||||||
SettingsTab 1.0 SettingsTab.qml
|
SettingsTab 1.0 SettingsTab.qml
|
||||||
StatusTab 1.0 StatusTab.qml
|
StatusTab 1.0 StatusTab.qml
|
||||||
SelectFileDialog 1.0 SelectFileDialog.qml
|
|
||||||
|
|||||||
Reference in New Issue
Block a user