360 lines
14 KiB
QML
360 lines
14 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import QtQuick.Dialogs
|
|
import ISC
|
|
|
|
// ===== Split Dialog (Threshold Segmentation) =====
|
|
// Modal popup for segmenting temperature profile into Ramp/Soak/Cool phases.
|
|
// Backend: splitter.segment_profile() returns list of DataSegment objects.
|
|
|
|
Popup {
|
|
id: root
|
|
modal: true
|
|
dim: true
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
width: Math.min(parent.width - 100, 800)
|
|
height: Math.min(parent.height - 100, 600)
|
|
anchors.centerIn: Overlay.overlay
|
|
|
|
property real threshold: 40.0
|
|
property bool segmenting: false
|
|
property var segments: []
|
|
property string currentFile: streamController.loadedFile || ""
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusMd
|
|
color: Theme.cardBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 20
|
|
spacing: 16
|
|
|
|
// ── Header ────────────────────────────────────────────────
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
|
|
Label {
|
|
text: "SPLIT DATA"
|
|
font.pixelSize: Theme.fontLg
|
|
font.bold: true
|
|
color: Theme.headingColor
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Button {
|
|
text: "\u2715"
|
|
flat: true
|
|
Layout.preferredWidth: 32
|
|
Layout.preferredHeight: 32
|
|
onClicked: root.close()
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusXs
|
|
color: parent.hovered ? Theme.buttonNeutralHover : "transparent"
|
|
}
|
|
contentItem: Label {
|
|
text: parent.text
|
|
color: Theme.bodyColor
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Threshold Input ───────────────────────────────────────
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 16
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
|
|
Label {
|
|
text: "THRESHOLD TEMPERATURE (°C)"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
font.letterSpacing: 1.0
|
|
}
|
|
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 8
|
|
|
|
SpinBox {
|
|
id: thresholdSpin
|
|
from: 0
|
|
to: 200
|
|
value: root.threshold
|
|
stepSize: 1
|
|
editable: true
|
|
Layout.fillWidth: true
|
|
background: Rectangle {
|
|
color: Theme.fieldBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
radius: Theme.radiusSm
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: "°C"
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontMd
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 4
|
|
|
|
Label {
|
|
text: "SESSION CSV"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
font.letterSpacing: 1.0
|
|
}
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
text: root.currentFile !== ""
|
|
? root.currentFile.split("/").pop()
|
|
: "No file loaded"
|
|
color: root.currentFile !== "" ? Theme.bodyColor : Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
elide: Text.ElideMiddle
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Split Button ──────────────────────────────────────────
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
spacing: 10
|
|
|
|
Button {
|
|
id: splitBtn
|
|
text: "SPLIT"
|
|
Layout.fillWidth: true
|
|
enabled: !root.segmenting && root.currentFile !== ""
|
|
onClicked: {
|
|
root.threshold = thresholdSpin.value;
|
|
root.segmenting = true;
|
|
streamController.splitData(root.currentFile, root.threshold);
|
|
}
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusSm
|
|
color: splitBtn.enabled ? Theme.primaryAccent : Theme.buttonNeutralBackground
|
|
border.color: splitBtn.enabled ? Theme.primaryAccent : "transparent"
|
|
border.width: 1
|
|
}
|
|
contentItem: Row {
|
|
spacing: 8
|
|
anchors.centerIn: parent
|
|
|
|
Label {
|
|
text: splitBtn.enabled ? (root.segmenting ? "Segmenting..." : "SPLIT") : "LOAD DATA FIRST"
|
|
color: splitBtn.enabled ? Theme.tone100 : Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
font.bold: true
|
|
font.letterSpacing: 1.0
|
|
}
|
|
|
|
BusyIndicator {
|
|
running: root.segmenting
|
|
visible: root.segmenting
|
|
width: 16
|
|
height: 16
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Segments Table ────────────────────────────────────────
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
radius: Theme.radiusSm
|
|
color: Theme.panelBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 0
|
|
spacing: 0
|
|
|
|
// Table header
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 36
|
|
color: Theme.subtleSectionBackground
|
|
border.color: Theme.cardBorder
|
|
border.width: 1
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 8
|
|
|
|
Label { text: "PHASE"; Layout.preferredWidth: 100; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
|
Label { text: "START (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
|
Label { text: "END (s)"; Layout.preferredWidth: 120; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
|
Label { text: "AVG TEMP (°C)"; Layout.fillWidth: true; color: Theme.sideMutedText; font.bold: true; font.pixelSize: Theme.fontSm }
|
|
Item { Layout.preferredWidth: 80 }
|
|
}
|
|
}
|
|
|
|
// Table body (scrollable)
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
ColumnLayout {
|
|
width: parent.width - 32
|
|
spacing: 0
|
|
|
|
Repeater {
|
|
model: root.segments.length > 0 ? root.segments : []
|
|
delegate: Rectangle {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 40
|
|
color: index % 2 === 0 ? Theme.cardBackground : "transparent"
|
|
border.color: Theme.cardBorder
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.margins: 12
|
|
spacing: 8
|
|
|
|
Label {
|
|
text: modelData.label || ""
|
|
Layout.preferredWidth: 100
|
|
color: modelData.label === "Soak" ? Theme.statusSuccessColor :
|
|
modelData.label === "Ramp" ? Theme.statusWarningColor : Theme.bodyColor
|
|
font.bold: true
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
Label {
|
|
text: (modelData.start_frame || 0).toString()
|
|
Layout.preferredWidth: 120
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
Label {
|
|
text: (modelData.end_frame || 0).toString()
|
|
Layout.preferredWidth: 120
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
Label {
|
|
text: (modelData.avg_temp || 0).toFixed(2) + " °C"
|
|
Layout.fillWidth: true
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
Button {
|
|
id: exportBtn
|
|
text: "Export"
|
|
Layout.preferredWidth: 80
|
|
Layout.preferredHeight: 26
|
|
hoverEnabled: true
|
|
onClicked: streamController.exportSegment(index)
|
|
|
|
background: Rectangle {
|
|
radius: Theme.radiusXs
|
|
color: exportBtn.pressed ? Theme.buttonNeutralPressed
|
|
: exportBtn.hovered ? Theme.buttonNeutralHover
|
|
: Theme.buttonNeutralBackground
|
|
border.color: Theme.fieldBorder
|
|
border.width: 1
|
|
}
|
|
contentItem: Label {
|
|
text: exportBtn.text
|
|
color: Theme.buttonNeutralText
|
|
font.pixelSize: Theme.fontXs
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Empty state
|
|
Label {
|
|
visible: root.segments.length === 0
|
|
text: root.currentFile !== "" ? "Click SPLIT to segment data" : "Load a CSV file first"
|
|
color: Theme.sideMutedText
|
|
font.pixelSize: Theme.fontSm
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 100
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Summary line ──────────────────────────────────────────
|
|
Label {
|
|
visible: root.segments.length > 0
|
|
text: root.segments.length + " segment" + (root.segments.length === 1 ? "" : "s")
|
|
+ " found above " + root.threshold.toFixed(0) + "°C threshold."
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
}
|
|
|
|
// ── Export feedback ───────────────────────────────────────
|
|
Label {
|
|
id: exportStatus
|
|
visible: text !== ""
|
|
color: Theme.bodyColor
|
|
font.pixelSize: Theme.fontSm
|
|
elide: Text.ElideMiddle
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
// ── Backend Connection ────────────────────────────────────────
|
|
Connections {
|
|
target: streamController
|
|
function onSplitResult(result) {
|
|
root.segmenting = false;
|
|
exportStatus.text = "";
|
|
if (result && result.success) {
|
|
root.segments = result.segments || [];
|
|
console.log("[SplitDialog] Segmented into", root.segments.length, "phases");
|
|
} else {
|
|
root.segments = [];
|
|
console.log("[SplitDialog] Split failed:", result.error || "unknown");
|
|
}
|
|
}
|
|
|
|
function onSegmentExported(result) {
|
|
if (result && result.success)
|
|
exportStatus.text = "Exported: " + result.path.split("/").pop();
|
|
else
|
|
exportStatus.text = "Export failed: " + (result.error || "unknown");
|
|
}
|
|
}
|
|
|
|
}
|