feat: add file comparison, data splitting, and sensor modification functionality to SessionController

This commit is contained in:
jack
2026-07-06 11:58:26 -07:00
parent ecab3d81b1
commit 3cc10ea7fe
9 changed files with 1361 additions and 167 deletions
+746 -122
View File
@@ -2,183 +2,807 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ISC
import ISC.Wafer
// ===== Data Tab =====
// Displays parsed temperature data in a scrollable table.
// Column 0 = Row index, remaining columns = Sensor1, Sensor2, ...
// ===== Data Tab (Compare Mode Only) =====
// Focuses exclusively on comparing two wafer runs side-by-side using DTW.
// Clicking Box 1 or Box 2 activates selection mode, and clicking a file in the
// left rail's file browser populates that slot.
Item {
id: root
anchors.fill: parent
readonly property int rowCount: deviceController.dataRowCount
// Properties for DTW Comparison
property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive
property string compareFileA: ""
property string compareFileB: ""
property real warpingDistance: -1
property real maxSensorDeviation: -1
property bool comparing: false
property var trendDataA: []
property var trendDataB: []
property string errorMessage: ""
// Re-layout columns whenever the model is reset
// Wafer overlap properties
property var compareSensorLayout: []
property var compareSensorDiff: []
property string compareWaferShape: "round"
property real compareWaferSize: 300.0
function diffBands(diffs) {
return diffs.map(d => d > 1.0 ? "high" : d < -1.0 ? "low" : "in_range")
}
function resetComparison() {
root.compareFileA = ""
root.compareFileB = ""
root.warpingDistance = -1
root.maxSensorDeviation = -1
root.comparing = false
root.trendDataA = []
root.trendDataB = []
root.compareSensorLayout = []
root.compareSensorDiff = []
root.errorMessage = ""
root.activeBox = 1
}
// Connections to handle clicked/selected files on left rail file browser
Connections {
target: deviceController
function onParsedDataReady(result) {
target: streamController
function onLoadedFileChanged() {
if (streamController.loadedFile && root.activeBox > 0) {
if (root.activeBox === 1) {
root.compareFileA = streamController.loadedFile
root.activeBox = 2 // Auto-advance to box 2
} else if (root.activeBox === 2) {
root.compareFileB = streamController.loadedFile
root.activeBox = 0 // Finished selecting
}
}
}
function onComparisonResult(result) {
root.comparing = false
if (result && result.success) {
dataTable.forceLayout();
root.warpingDistance = result.distance
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
root.trendDataA = result.series_a || []
root.trendDataB = result.series_b || []
root.compareSensorLayout = result.sensor_layout || []
root.compareSensorDiff = result.sensor_diff || []
root.compareWaferShape = result.wafer_shape || "round"
root.compareWaferSize = result.wafer_size || 300.0
root.errorMessage = ""
} else {
root.warpingDistance = -1
root.maxSensorDeviation = -1
root.trendDataA = []
root.trendDataB = []
root.compareSensorLayout = []
root.compareSensorDiff = []
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
}
}
}
// ===== Empty State =====
Column {
anchors.centerIn: parent
spacing: 16
visible: root.rowCount === 0
Label {
text: "No Data"
font.pixelSize: Theme.font2xl
font.bold: true
color: Theme.headingColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
text: "Read and parse wafer data to see temperature readings here."
font.pixelSize: Theme.fontMd
color: Theme.bodyColor
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Text.WordWrap
}
}
// ===== Data Table =====
// ── Main Layout ───────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: Theme.rightPaneGap
visible: root.rowCount > 0
// --- Header row ---
// ── Title / Header ─────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: Theme.rightPaneGap
spacing: 8
Label {
text: "Temperature Data"
text: "Run Comparison (DTW)"
font.pixelSize: Theme.fontXl
font.bold: true
color: Theme.headingColor
Layout.fillWidth: true
elide: Text.ElideRight
}
Item {
Layout.fillWidth: true
// Reset button
Button {
id: resetBtn
flat: true
implicitWidth: 32
implicitHeight: 32
visible: root.compareFileA !== "" || root.compareFileB !== ""
onClicked: root.resetComparison()
background: Rectangle {
radius: Theme.radiusSm
color: resetBtn.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
}
contentItem: Label {
text: "\u21BA"
font.pixelSize: Theme.fontLg
color: Theme.bodyColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
ToolTip.visible: resetBtn.hovered
ToolTip.text: "Reset comparison"
}
Label {
text: deviceController.dataRowCount + " rows × " + deviceController.dataColCount + " sensors"
text: "Dynamic Time Warping Profile Matching"
font.pixelSize: Theme.fontSm
color: Theme.bodyColor
}
}
// --- Toolbar ---
// ── Selection Boxes (Box 1 and Box 2) ──────────────────────
RowLayout {
Layout.fillWidth: true
spacing: 8
spacing: 16
Button {
text: "Open in Excel"
icon.source: "icons/excel.svg"
icon.color: Theme.headingColor
onClicked: deviceController.openCsvFile()
}
Button {
text: "Compare"
onClicked: compareDialog.open()
}
Button {
text: "Split"
onClicked: splitDialog.open()
}
Item {
// --- Box 1: Reference CSV (Run A) ---
Rectangle {
id: box1
Layout.fillWidth: true
}
}
// --- Table container ---
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.panelBackground
border.color: Theme.cardBorder
border.width: Theme.borderThin
radius: Theme.radiusSm
clip: true
Layout.preferredHeight: 70
radius: Theme.radiusMd
color: Theme.cardBackground
border.width: root.activeBox === 1 ? 2 : 1
border.color: root.activeBox === 1 ? Theme.primaryAccent : Theme.cardBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 1
spacing: 0
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
// Column header strip
HorizontalHeaderView {
id: headerView
Layout.fillWidth: true
syncView: dataTable
clip: true
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.activeBox = 1
}
delegate: Rectangle {
implicitHeight: 28
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
RowLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 10
Text {
anchors.centerIn: parent
// 'display' is the Qt.DisplayRole value from headerData()
text: display !== undefined ? display : ""
color: Theme.headingColor
// Color indicator dot
Rectangle {
width: 10; height: 10; radius: 5
color: Theme.primaryAccent
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
text: "REFERENCE (RUN A)"
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.0
color: root.activeBox === 1 ? Theme.primaryAccent : Theme.sideMutedText
}
Label {
text: root.compareFileA !== ""
? root.compareFileA.split("/").pop()
: (root.activeBox === 1 ? "← Select file from left panel..." : "Click to select")
font.pixelSize: Theme.fontSm
color: root.compareFileA !== "" ? Theme.headingColor : Theme.bodyColor
opacity: root.compareFileA !== "" ? 1.0 : 0.6
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
Button {
text: "✕"
visible: root.compareFileA !== ""
flat: true
implicitWidth: 24; implicitHeight: 24
onClicked: {
root.compareFileA = ""
root.activeBox = 1
root.warpingDistance = -1
root.trendDataA = []
root.trendDataB = []
root.compareSensorLayout = []
root.compareSensorDiff = []
}
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
}
contentItem: Label {
text: "✕"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
// --- Box 2: Session CSV (Run B) ---
Rectangle {
id: box2
Layout.fillWidth: true
Layout.preferredHeight: 70
radius: Theme.radiusMd
color: Theme.cardBackground
border.width: root.activeBox === 2 ? 2 : 1
border.color: root.activeBox === 2 ? Theme.themeSkill : Theme.cardBorder
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.activeBox = 2
}
RowLayout {
anchors.fill: parent
anchors.margins: 14
spacing: 10
// Color indicator dot
Rectangle {
width: 10; height: 10; radius: 5
color: Theme.themeSkill
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
text: "SESSION (RUN B)"
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.0
color: root.activeBox === 2 ? Theme.themeSkill : Theme.sideMutedText
}
Label {
text: root.compareFileB !== ""
? root.compareFileB.split("/").pop()
: (root.activeBox === 2 ? "← Select file from left panel..." : "Click to select")
font.pixelSize: Theme.fontSm
color: root.compareFileB !== "" ? Theme.headingColor : Theme.bodyColor
opacity: root.compareFileB !== "" ? 1.0 : 0.6
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
Button {
text: "✕"
visible: root.compareFileB !== ""
flat: true
implicitWidth: 24; implicitHeight: 24
onClicked: {
root.compareFileB = ""
root.activeBox = 2
root.warpingDistance = -1
root.trendDataA = []
root.trendDataB = []
root.compareSensorLayout = []
root.compareSensorDiff = []
}
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent"
}
contentItem: Label {
text: "✕"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
// ── Compare Button ─────────────────────────────────────────
Button {
id: compareBtn
Layout.fillWidth: true
Layout.preferredHeight: 40
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
onClicked: {
root.comparing = true
root.warpingDistance = -1
root.maxSensorDeviation = -1
root.trendDataA = []
root.trendDataB = []
root.errorMessage = ""
streamController.compareFiles(root.compareFileA, root.compareFileB)
}
background: Rectangle {
radius: Theme.radiusSm
color: compareBtn.enabled
? (compareBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent)
: Theme.buttonNeutralBackground
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
}
contentItem: Row {
spacing: 8
anchors.centerIn: parent
Label {
anchors.verticalCenter: parent.verticalCenter
text: compareBtn.enabled
? (root.comparing ? "Comparing…" : "▶ RUN DTW COMPARISON")
: "SELECT BOTH RUNS ABOVE"
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
font.pixelSize: Theme.fontSm
font.bold: true
font.letterSpacing: 0.8
}
BusyIndicator {
running: root.comparing
visible: root.comparing
width: 16; height: 16
anchors.verticalCenter: parent.verticalCenter
}
}
}
// ── Error Banner ───────────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: errorLabel.implicitHeight + 16
visible: root.errorMessage !== ""
color: Qt.rgba(0.93, 0.27, 0.27, 0.15)
border.color: "#ef4444"
border.width: 1
radius: Theme.radiusSm
Label {
id: errorLabel
anchors.fill: parent
anchors.margins: 8
text: "⚠ " + root.errorMessage
color: "#fca5a5"
font.pixelSize: Theme.fontSm
wrapMode: Text.WordWrap
}
}
// ── DTW Chart + Overlap Wafer Map Row ──────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
// --- Left Column: Aligned Curves Chart ---
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumWidth: 350
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 10
RowLayout {
Layout.fillWidth: true
Label {
text: "Aligned Temperature Curves (DTW)"
font.pixelSize: Theme.fontSm
font.bold: true
color: Theme.headingColor
Layout.fillWidth: true
elide: Text.ElideRight
}
// Legend
Row {
spacing: 16
visible: root.trendDataA.length > 1
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.fillHeight: true
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
clip: true
readonly property bool hasSeries: root.trendDataA.length > 1 && root.trendDataB.length > 1
Canvas {
id: overlayCanvas
anchors.fill: parent
anchors.margins: 0
visible: parent.hasSeries
readonly property real padLeft: 50
readonly property real padRight: 16
readonly property real padTop: 16
readonly property real padBottom: 28
function drawGrid(ctx, minV, maxV, dataLen) {
var plotW = width - padLeft - padRight
var plotH = height - padTop - padBottom
var range = (maxV - minV) || 1
ctx.strokeStyle = Qt.rgba(1, 1, 1, 0.08)
ctx.lineWidth = 1
ctx.fillStyle = Qt.rgba(1, 1, 1, 0.35)
ctx.font = "10px sans-serif"
ctx.textAlign = "right"
// Y-axis gridlines + labels
var ySteps = 5
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(1), padLeft - 6, yPx + 3)
}
// X-axis labels
ctx.textAlign = "center"
var xSteps = Math.min(5, dataLen - 1)
for (var xi = 0; xi <= xSteps; xi++) {
var xFrac = xi / xSteps
var xPx = padLeft + plotW * xFrac
var xIdx = Math.round(xFrac * (dataLen - 1))
ctx.fillText(xIdx.toString(), xPx, height - 6)
}
// Y-axis title
ctx.save()
ctx.translate(12, padTop + plotH / 2)
ctx.rotate(-Math.PI / 2)
ctx.textAlign = "center"
ctx.fillText("°C", 0, 0)
ctx.restore()
// X-axis title
ctx.textAlign = "center"
ctx.fillText("Frame", padLeft + plotW / 2, height - 2)
}
function drawSeries(ctx, series, minV, range, color) {
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 / (series.length - 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()
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
if (!parent.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
var range = (maxV - minV) || 1
drawGrid(ctx, minV, maxV, Math.max(root.trendDataA.length, root.trendDataB.length))
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent)
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill)
}
Connections {
target: root
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
}
}
// Empty state
ColumnLayout {
anchors.centerIn: parent
visible: !parent.hasSeries
spacing: 8
Label {
Layout.alignment: Qt.AlignHCenter
text: root.comparing ? "" : "📊"
font.pixelSize: 32
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.comparing ? "Running DTW comparison…" : "Select two files and run comparison"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: !root.comparing
text: "Click Box 1, then a file. Click Box 2, then a file."
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
}
}
}
}
}
// --- Right Column: Wafer Map Overlap & Readouts ---
ColumnLayout {
Layout.fillWidth: false
Layout.preferredWidth: 280
Layout.minimumWidth: 280
Layout.maximumWidth: 280
Layout.fillHeight: true
spacing: 12
// ── DTW Readout Cards ─────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: dtwReadoutCol.implicitHeight + 24
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
ColumnLayout {
id: dtwReadoutCol
anchors.fill: parent
anchors.margins: 12
spacing: 8
Label {
text: "COMPARISON RESULTS"
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.2
color: Theme.sideMutedText
Layout.fillWidth: true
}
// DTW Distance
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.panelBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 2
Label {
text: "DTW Alignment Distance"
font.pixelSize: Theme.fontXs
color: Theme.bodyColor
}
Label {
text: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
font.pixelSize: Theme.fontLg
font.bold: true
color: root.warpingDistance >= 0
? (root.warpingDistance < 50 ? "#6ee7b7" : root.warpingDistance < 100 ? "#fde047" : "#ef4444")
: Theme.sideMutedText
}
}
}
// Max Sensor Deviation
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.panelBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 2
Label {
text: "Max Sensor Deviation"
font.pixelSize: Theme.fontXs
color: Theme.bodyColor
}
Label {
text: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "\u00B0C" : "--"
font.pixelSize: Theme.fontLg
font.bold: true
color: root.maxSensorDeviation >= 0
? (root.maxSensorDeviation < 1.0 ? "#6ee7b7" : root.maxSensorDeviation < 3.0 ? "#fde047" : "#ef4444")
: Theme.sideMutedText
}
}
}
// Match Quality Badge
Rectangle {
Layout.fillWidth: true
height: 28
visible: root.warpingDistance >= 0
radius: Theme.radiusSm
color: root.warpingDistance < 50 ? Qt.rgba(0.43, 0.91, 0.72, 0.12)
: root.warpingDistance < 100 ? Qt.rgba(0.99, 0.88, 0.28, 0.12)
: Qt.rgba(0.94, 0.27, 0.27, 0.12)
Label {
anchors.centerIn: parent
text: root.warpingDistance < 50 ? "✓ Excellent Match"
: root.warpingDistance < 100 ? "⚠ Moderate Deviation"
: "✗ Poor Match"
font.pixelSize: Theme.fontXs
font.bold: true
color: root.warpingDistance < 50 ? "#6ee7b7"
: root.warpingDistance < 100 ? "#fde047"
: "#ef4444"
}
}
}
}
// Data rows
TableView {
id: dataTable
// ── Wafer Overlap View ────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
model: deviceController.dataModel
clip: true
reuseItems: true
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
columnWidthProvider: function (col) {
if (col === 0)
return 52; // row index column
const sensorCols = Math.max(1, dataTable.columns - 1);
const available = dataTable.width - 52;
return Math.max(52, Math.min(90, available / sensorCols));
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
rowHeightProvider: function () {
return 24;
}
delegate: Rectangle {
color: row % 2 === 0 ? Theme.panelBackground : Theme.cardBackground
Text {
anchors.centerIn: parent
text: display !== undefined ? display : ""
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
elide: Text.ElideRight
RowLayout {
Layout.fillWidth: true
Label {
text: "SENSOR DEVIATION MAP"
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.2
color: Theme.sideMutedText
Layout.fillWidth: true
}
CheckBox {
id: enableOverlapCheck
checked: true
implicitWidth: 18; implicitHeight: 18
ToolTip.visible: hovered
ToolTip.text: "Toggle overlap heatmap"
}
}
}
ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AsNeeded
}
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
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: root.compareSensorDiff
bands: root.diffBands(root.compareSensorDiff)
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
spacing: 4
Label {
Layout.alignment: Qt.AlignHCenter
text: "🗺"
font.pixelSize: 24
}
Label {
Layout.alignment: Qt.AlignHCenter
text: "No sensor map available"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
}
// Blend slider
RowLayout {
Layout.fillWidth: true
spacing: 6
Label {
text: "Blend"
font.pixelSize: Theme.fontXs
color: Theme.bodyColor
}
Slider {
id: blendSlider
Layout.fillWidth: true
from: 0; to: 100; value: 50
stepSize: 1
}
Label {
text: Math.round(blendSlider.value) + "%"
font.pixelSize: Theme.fontXs
color: Theme.sideMutedText
Layout.preferredWidth: 30
}
}
// Color legend
Row {
Layout.fillWidth: true
spacing: 8
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Cool"; font.pixelSize: 9; 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: 9; color: Theme.sideMutedText }
}
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Hot"; font.pixelSize: 9; color: Theme.sideMutedText }
}
}
}
}
}