refactor(qml): extract sidebar panels and data tab comparison cards to modular components

This commit is contained in:
jack
2026-07-09 12:49:10 -07:00
parent 637b51b999
commit ca158a7946
16 changed files with 1156 additions and 970 deletions
@@ -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 // 0100, 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
}
}
}
}
}
+15 -1
View File
@@ -6,5 +6,19 @@ TransportBar 1.0 TransportBar.qml
WaferMapView 1.0 WaferMapView.qml
ReplaceSensorDialog 1.0 ReplaceSensorDialog.qml
RailActionButton 1.0 RailActionButton.qml
SelectFileDialog 1.0 SelectFileDialog.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