6import QGroundControl.Controls
8/// Shared chart base for the Log Viewer.
10/// Provides: GraphsView with QGC-themed GraphsTheme, a HH:MM:SS x-axis
11/// labelDelegate, zoom/cursor interaction, position marker line, and
12/// LogViewerCursorPopup.
14/// Derived charts (LogViewerChart, LogViewerAltChart) embed this as an Item,
15/// connect to zoomRangeSet / cursorPositionSet to refresh their series data,
16/// and supply popup extra rows as declarative children (forwarded via the
17/// default property alias to the popup's extraContent).
21 // -------------------------------------------------------------------------
23 // -------------------------------------------------------------------------
24 required property var logParser
26 // -------------------------------------------------------------------------
28 // -------------------------------------------------------------------------
29 property bool xAxisShowLocalTime: false
30 property string yAxisTitle: ""
31 property real popupYOffset: 0 ///< added to popup y (alt chart offsets slightly)
33 // -------------------------------------------------------------------------
34 // Read-only aliases to child items — derived charts access these
35 // -------------------------------------------------------------------------
36 readonly property alias graphsView: _chart
37 readonly property alias xAxis: _xAxis
38 readonly property alias yAxis: _yAxis
39 readonly property alias colorBlockWidth: _popup.colorBlockWidth
40 readonly property real popupWidth: _popup.implicitWidth
42 // -------------------------------------------------------------------------
43 // Default property — children declared inside LogViewerBaseChart { } go
44 // into the popup's extraContent column (chart-specific popup rows).
45 // -------------------------------------------------------------------------
46 default property alias content: _popup.extraContent
48 // -------------------------------------------------------------------------
50 // -------------------------------------------------------------------------
52 /// Emitted only on user-driven cursor interaction.
53 /// Do NOT re-emit this when handling an external sync (setSharedCursor).
54 signal cursorMoved(real t)
56 /// Emitted only on user-driven zoom.
57 /// Do NOT re-emit this when handling an external sync (setSharedZoom).
58 signal zoomApplied(real minX, real maxX)
60 /// Emitted whenever the zoom range changes (user-driven or external sync).
61 /// Derived charts connect here to refresh their series data.
62 signal zoomRangeSet(real minX, real maxX)
64 /// Emitted whenever the cursor position changes (user-driven or external sync).
65 /// Derived charts connect here to update popup values (e.g. _markerAltValue).
66 signal cursorPositionSet(real t)
68 // -------------------------------------------------------------------------
69 // State — writable by base, readable by derived charts
70 // -------------------------------------------------------------------------
71 property bool markerVisible: false
72 property real markerPixelX: 0
73 property real markerXValue: 0
74 property real fullMinX: 0
75 property real fullMaxX: 1
76 property real zoomMinX: 0
77 property real zoomMaxX: 1
79 // -------------------------------------------------------------------------
81 // -------------------------------------------------------------------------
83 /// Set the full and zoom range (called on log load).
84 /// Emits zoomRangeSet so derived charts can refresh their series.
85 function initRange(minX, maxX) {
92 zoomRangeSet(minX, maxX)
95 /// User-driven zoom — also emits zoomApplied for cross-chart sync.
96 function applyZoomRange(minX, maxX) {
97 _applyZoomInternal(minX, maxX)
98 zoomApplied(minX, maxX)
101 /// External zoom sync — does not re-emit zoomApplied.
102 function setSharedZoom(minX, maxX) {
103 _applyZoomInternal(minX, maxX)
106 /// Place the cursor at axis value t and emit cursorPositionSet.
107 /// Does NOT emit cursorMoved — use for both external sync and initial placement.
108 function setCursor(t) {
109 if (_xAxis.max <= _xAxis.min) return
111 markerPixelX = _axisXToPixel(t)
116 /// External cursor sync — delegates to setCursor (no cursorMoved emitted).
117 function setSharedCursor(t) {
121 function resetZoom() {
122 applyZoomRange(fullMinX, fullMaxX)
125 function clearCursor() {
126 markerVisible = false
129 // -------------------------------------------------------------------------
131 // -------------------------------------------------------------------------
132 function _pixelToAxisX(pixelX) {
133 const plotX = _chart.plotArea.x
134 const plotW = _chart.plotArea.width
135 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return _xAxis.min
136 const ratio = (Math.max(plotX, Math.min(plotX + plotW, pixelX)) - plotX) / plotW
137 return _xAxis.min + ratio * (_xAxis.max - _xAxis.min)
140 function _axisXToPixel(axisX) {
141 const plotX = _chart.plotArea.x
142 const plotW = _chart.plotArea.width
143 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return plotX
144 const ratio = (axisX - _xAxis.min) / (_xAxis.max - _xAxis.min)
145 return plotX + Math.max(0, Math.min(plotW, ratio * plotW))
148 function _applyZoomInternal(minX, maxX) {
149 if (maxX <= minX) return
154 if (markerVisible && (markerXValue < minX || markerXValue > maxX)) {
155 markerXValue = (minX + maxX) / 2
157 markerPixelX = _axisXToPixel(markerXValue)
158 zoomRangeSet(minX, maxX)
161 function _updateCursorFromInteraction(pixelX) {
162 if (_xAxis.max <= _xAxis.min) return
163 const plotX = _chart.plotArea.x
164 const plotW = _chart.plotArea.width
166 markerPixelX = Math.max(plotX, Math.min(plotX + plotW, pixelX))
167 markerXValue = _pixelToAxisX(markerPixelX)
168 cursorPositionSet(markerXValue)
169 cursorMoved(markerXValue)
173 const plotMidX = _chart.plotArea.x + _chart.plotArea.width / 2
174 if (markerPixelX < plotMidX) {
175 const rightX = _chart.plotArea.x + _chart.plotArea.width - _popup.width
176 return Math.max(0, Math.min(rightX, control.width - _popup.width))
178 return _chart.plotArea.x + _popup._margin
182 // -------------------------------------------------------------------------
184 // -------------------------------------------------------------------------
185 QGCPalette { id: qgcPal }
196 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
197 backgroundColor: qgcPal.windowShadeDark
198 backgroundVisible: true
199 plotAreaBackgroundColor: qgcPal.windowShadeDark
200 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.25)
201 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.12)
203 labelBackgroundVisible: false
204 labelTextColor: qgcPal.text
205 axisXLabelFont.family: ScreenTools.fixedFontFamily
206 axisXLabelFont.pointSize: ScreenTools.smallFontPointSize
207 axisYLabelFont.family: ScreenTools.fixedFontFamily
208 axisYLabelFont.pointSize: ScreenTools.smallFontPointSize
213 titleText: xAxisShowLocalTime ? qsTr("Time (local)") : qsTr("Elapsed")
218 labelDelegate: Component {
220 property string text: "" // raw seconds value, assigned by axis
223 anchors.centerIn: parent
224 color: _chart.theme.labelTextColor
225 font: _chart.theme.axisXLabelFont
226 horizontalAlignment: Text.AlignHCenter
228 const secs = parseFloat(parent.text)
229 if (isNaN(secs)) { return parent.text }
230 if (xAxisShowLocalTime) {
231 const st = logParser.startTime
232 if (st && !isNaN(st.getTime()) && st.getTime() > 0) {
233 const use12h = Qt.locale().timeFormat(Locale.ShortFormat).indexOf("a") >= 0
234 || Qt.locale().timeFormat(Locale.ShortFormat).indexOf("A") >= 0
235 return Qt.formatTime(new Date(st.getTime() + secs * 1000),
236 use12h ? "h:mm:ss AP" : "HH:mm:ss")
239 const wholeSecs = Math.floor(secs)
240 const hh = Math.floor(wholeSecs / 3600)
241 const mm = Math.floor((wholeSecs % 3600) / 60)
242 const ss = wholeSecs % 60
244 return hh + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0")
246 return mm + ":" + String(ss).padStart(2, "0")
257 titleText: yAxisTitle
263 // Recalculate marker pixel position when the chart is resized or laid out.
266 function onPlotAreaChanged() {
267 if (control.markerVisible) {
268 control.markerPixelX = control._axisXToPixel(control.markerXValue)
273 // Zoom selection rectangle
277 color: Qt.rgba(1, 1, 1, 0.2)
278 border.color: qgcPal.buttonHighlight
285 acceptedButtons: Qt.LeftButton
286 enabled: _xAxis.max > _xAxis.min
289 property real _dragStartX: 0
290 property bool _isDraggingCursor: false
291 property bool _isDraggingZoom: false
292 property bool _wasDoubleClick: false
294 onPressed: (mouse) => {
295 _isDraggingCursor = false
296 _isDraggingZoom = false
297 _wasDoubleClick = false
298 if (mouse.modifiers & Qt.ShiftModifier) {
299 // Shift pressed — start cursor drag
300 _isDraggingCursor = true
301 control._updateCursorFromInteraction(mouse.x)
303 // Plain press — start zoom selection
304 _dragStartX = mouse.x
305 _zoomRect.x = mouse.x
306 _zoomRect.y = _chart.plotArea.y
308 _zoomRect.height = _chart.plotArea.height
309 _zoomRect.visible = false // only show once drag is meaningful
313 onPositionChanged: (mouse) => {
315 if (_isDraggingCursor) {
316 control._updateCursorFromInteraction(mouse.x)
318 // Plain drag — update zoom selection rect
319 const left = Math.min(_dragStartX, mouse.x)
320 const right = Math.max(_dragStartX, mouse.x)
322 _zoomRect.width = Math.max(0, right - left)
323 if (_zoomRect.width >= ScreenTools.defaultFontPixelWidth * 0.5) {
324 _isDraggingZoom = true
325 _zoomRect.visible = true
330 onReleased: (mouse) => {
331 if (_isDraggingCursor) {
332 _isDraggingCursor = false
335 _zoomRect.visible = false
336 if (_isDraggingZoom) {
337 _isDraggingZoom = false
338 const leftX = control._pixelToAxisX(_zoomRect.x)
339 const rightX = control._pixelToAxisX(_zoomRect.x + _zoomRect.width)
340 control.applyZoomRange(Math.min(leftX, rightX), Math.max(leftX, rightX))
341 } else if (!_wasDoubleClick) {
342 // Tap — place cursor (suppressed on the second click of a double-click)
343 control._updateCursorFromInteraction(mouse.x)
347 onDoubleClicked: (mouse) => {
348 // onDoubleClicked fires before onReleased for the second click,
349 // so this flag prevents onReleased from repositioning the cursor.
350 _wasDoubleClick = true
352 const centerT = (control.fullMinX + control.fullMaxX) / 2
353 control.setCursor(centerT)
354 control.cursorMoved(centerT)
358 // Position marker line
360 visible: markerVisible
364 height: _chart.plotArea.height
369 // Cursor popup — chart-specific rows are injected via default property alias
370 LogViewerCursorPopup {
372 visible: markerVisible
374 y: _chart.plotArea.y + popupYOffset
376 cursorXValue: markerXValue
377 logParser: control.logParser
378 xAxisShowLocalTime: control.xAxisShowLocalTime
379 zoomMinX: control.zoomMinX
380 zoomMaxX: control.zoomMaxX
381 plotAreaWidth: _chart.plotArea.width