QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
AppLogging.qml
Go to the documentation of this file.
1import QGroundControl
2import QGroundControl.Controls
3import QGroundControl.LogManager
4import QtQuick
5import QtQuick.Layouts
6
7Item {
8 id: root
9 objectName: "settingsPage_AppLogViewer"
10
11 readonly property real _indicatorWidth: ScreenTools.defaultFontPixelWidth * 0.4
12 readonly property real _margin: ScreenTools.defaultFontPixelWidth
13 readonly property real _cellLeftMargin: _margin * 0.3
14 readonly property real _cellRightMargin: _cellLeftMargin
15 readonly property real _rowHeight: ScreenTools.defaultFontPixelHeight * 1.6
16
17 function _levelColor(lvl) {
18 switch (lvl) {
19 case LogEntry.Debug: return qgcPal.colorGrey
20 case LogEntry.Info: return qgcPal.text
21 case LogEntry.Warning: return qgcPal.colorOrange
22 case LogEntry.Critical: return qgcPal.colorRed
23 case LogEntry.Fatal: return qgcPal.colorRed
24 default: return qgcPal.text
25 }
26 }
27
28 QGCPalette {
29 id: qgcPal
30
31 colorGroupEnabled: enabled
32 }
33
34 ColumnLayout {
35 anchors.fill: parent
36 spacing: 0
37
38 // ── Column header ────────────────────────────────────────
39 HorizontalHeaderView {
40 id: headerView
41
42 Layout.fillWidth: true
43 clip: true
44 syncView: tableView
45
46 delegate: Rectangle {
47 required property var display
48
49 color: qgcPal.windowShade
50 implicitHeight: headerLabel.contentHeight + ScreenTools.defaultFontPixelHeight * 0.4
51 implicitWidth: headerLabel.contentWidth + ScreenTools.defaultFontPixelWidth * 2
52
53 QGCLabel {
54 id: headerLabel
55
56 anchors.left: parent.left
57 anchors.leftMargin: _margin * 0.5
58 anchors.verticalCenter: parent.verticalCenter
59 font.bold: true
60 font.family: ScreenTools.fixedFontFamily
61 font.pointSize: ScreenTools.defaultFontPointSize
62 text: parent.display ?? ""
63 }
64
65 Rectangle {
66 anchors.bottom: parent.bottom
67 color: qgcPal.groupBorder
68 height: 1
69 width: parent.width
70 }
71
72 Rectangle {
73 anchors.right: parent.right
74 color: qgcPal.groupBorder
75 height: parent.height
76 width: 1
77 }
78 }
79 }
80
81 // ── Log table ────────────────────────────────────────────
82 TableView {
83 id: tableView
84 Layout.fillHeight: true
85 Layout.fillWidth: true
86 boundsBehavior: Flickable.StopAtBounds
87 clip: true
88 columnSpacing: 0
89 rowSpacing: 0
90 model: LogManager.model
91 resizableColumns: true
92 reuseItems: true
93
94 readonly property real minMessageColumnWidthInChars: 40
95
96 columnWidthProvider: function (col) {
97 const explicit_ = tableView.explicitColumnWidth(col)
98 if (explicit_ > 0) {
99 return explicit_;
100 }
101 const charWidth = ScreenTools.defaultFontPixelWidth
102 const compact = _cachedWidth < charWidth * 60
103 const catW = compact ? charWidth * 14 : charWidth * 22
104 const tsW = compact ? charWidth * 9 : charWidth * 12
105 const srcW = compact ? charWidth * 16 : charWidth * 22
106 switch (col) {
107 case LogEntry.MessageColumn:
108 return Math.max(charWidth * minMessageColumnWidthInChars, _cachedWidth - catW - tsW - srcW)
109 case LogEntry.CategoryColumn:
110 return catW;
111 case LogEntry.TimestampColumn:
112 return tsW;
113 case LogEntry.SourceColumn:
114 return srcW;
115 }
116 return -1;
117 }
118
119 property bool _loadCompleted: false
120 property real _cachedWidth: 0
121 property bool _following: false
122 property bool _atBottom: false
123
124 function _updateAtBottom() {
125 if (!_loadCompleted) {
126 return
127 }
128 const maxY = contentHeight - height
129 if (maxY <= 0) {
130 _atBottom = true
131 } else {
132 _atBottom = (contentY >= maxY - _rowHeight)
133 }
134 // Stop following if user scrolls away from bottom
135 if (!_atBottom) {
136 _following = false
137 }
138 }
139
140 onWidthChanged: {
141 _cachedWidth = width;
142 _layoutTimer.restart();
143 }
144
145 onContentYChanged: _updateAtBottom()
146 onContentHeightChanged: _updateAtBottom()
147 onHeightChanged: _updateAtBottom()
148
149 Component.onCompleted: {
150 _cachedWidth = width;
151 _loadCompleted = true;
152 }
153
154 Timer {
155 id: _layoutTimer
156
157 interval: 0
158
159 onTriggered: tableView.forceLayout()
160 }
161
162 delegate: Rectangle {
163 required property int column
164 required property var display
165 required property int level
166 required property int row
167
168 color: row % 2 === 0 ? qgcPal.window : qgcPal.windowShade
169 implicitHeight: _rowHeight
170
171 Rectangle {
172 anchors.bottom: parent.bottom
173 anchors.left: parent.left
174 anchors.top: parent.top
175 color: root._levelColor(parent.level)
176 visible: parent.column === 0 && parent.level >= LogEntry.Warning
177 width: _indicatorWidth
178 }
179
180 QGCLabel {
181 anchors.left: parent.left
182 anchors.leftMargin: parent.column === 0 ? _indicatorWidth + _cellLeftMargin : _cellLeftMargin
183 anchors.right: parent.right
184 anchors.rightMargin: _cellRightMargin
185 anchors.verticalCenter: parent.verticalCenter
186 color: root._levelColor(parent.level)
187 elide: Text.ElideRight
188 font.family: ScreenTools.fixedFontFamily
189 font.pointSize: ScreenTools.defaultFontPointSize
190 text: parent.display ?? ""
191 }
192 }
193
194 QGCLabel {
195 x: tableView.contentX + (tableView.width - width) / 2
196 y: tableView.contentY + (tableView.height - height) / 2
197 color: qgcPal.colorGrey
198 text: qsTr("No log entries")
199 visible: tableView.rows === 0
200 }
201
202 Timer {
203 id: scrollTimer
204 interval: 50
205
206 onTriggered: {
207 if (tableView.rows > 0)
208 tableView.positionViewAtRow(tableView.rows - 1, TableView.AlignBottom);
209 }
210 }
211
212 Connections {
213 function onRowsInserted() {
214 if (tableView._loadCompleted && tableView._following) {
215 scrollTimer.restart();
216 }
217 }
218
219 target: LogManager.model
220 }
221 }
222
223 // ── Separator ─────────────────────────────────────────────
224 Rectangle {
225 Layout.fillWidth: true
226 Layout.preferredHeight: 1
227 color: qgcPal.groupBorder
228 }
229
230 // ── Filter bar ────────────────────────────────────────────
231 Rectangle {
232 Layout.fillWidth: true
233 Layout.preferredHeight: filterRow.implicitHeight + _margin
234 color: qgcPal.windowShade
235
236 QGCFlickable {
237 id: filterFlickable
238
239 anchors.fill: parent
240 anchors.leftMargin: _margin
241 anchors.rightMargin: _margin
242 contentWidth: filterRow.width
243 flickableDirection: Flickable.HorizontalFlick
244
245 RowLayout {
246 id: filterRow
247
248 height: filterFlickable.height
249 spacing: _margin * 0.75
250 width: Math.max(filterFlickable.width, implicitWidth)
251
252 QGCButton {
253 text: qsTr("Categories")
254
255 onClicked: filtersDialogFactory.open()
256 }
257
258 QGCComboBox {
259 id: levelCombo
260
261 model: [qsTr("All Levels"), qsTr("Debug"), qsTr("Info"), qsTr("Warning"), qsTr("Critical"), qsTr("Fatal")]
262 sizeToContents: true
263
264 Component.onCompleted: currentIndex = LogManager.model.filterLevel + 1
265 onActivated: index => {
266 LogManager.model.filterLevel = index - 1;
267 }
268 }
269
270 QGCComboBox {
271 id: categoryCombo
272
273 model: [qsTr("All Categories")].concat(LogManager.model.categoriesList)
274 sizeToContents: true
275
276 onActivated: index => {
277 LogManager.model.filterCategory = index === 0 ? "" : model[index];
278 }
279 }
280
281 QGCTextField {
282 id: searchField
283
284 Layout.fillWidth: true
285 Layout.minimumWidth: _margin * 10
286 placeholderText: qsTr("Search…")
287
288 onTextChanged: LogManager.model.setFilterTextDeferred(text)
289 }
290
291 QGCButton {
292 ToolTip.text: qsTr("Regex search")
293 ToolTip.visible: hovered
294 checkable: true
295 checked: LogManager.model.filterRegex
296 text: qsTr(".*")
297
298 onClicked: LogManager.model.filterRegex = checked
299 }
300
301 QGCLabel {
302 color: qgcPal.colorRed
303 font.bold: true
304 text: qsTr("\u26A0 Disk Error")
305 visible: LogManager.hasError
306
307 QGCMouseArea {
308 anchors.fill: parent
309
310 onClicked: LogManager.clearError()
311 }
312 }
313
314 QGCButton {
315 text: qsTr("Save")
316
317 onClicked: saveFileDialog.openForSave()
318 }
319
320 QGCButton {
321 text: qsTr("Clear")
322
323 onClicked: LogManager.model.clear()
324 }
325 }
326 }
327 }
328 }
329
330 QGCButton {
331 anchors.horizontalCenter: parent.horizontalCenter
332 anchors.topMargin: headerView.height + _margin
333 anchors.top: parent.top
334 text: qsTr("Show Latest")
335 visible: tableView._loadCompleted && !tableView._atBottom && tableView.rows > 0
336 opacity: 0.75
337
338 onClicked: {
339 tableView._following = true
340 tableView.positionViewAtRow(tableView.rows - 1, TableView.AlignBottom)
341 }
342 }
343
344 QGCPopupDialogFactory {
345 id: filtersDialogFactory
346
347 dialogComponent: Component {
348 LoggingCategoriesDialog {
349 }
350 }
351 }
352
353 QGCFileDialog {
354 id: saveFileDialog
355
356 readonly property var _suffixes: ["txt", "csv"]
357
358 defaultSuffix: _suffixes[QGroundControl.settingsManager.logManagerSettings.saveFormat.rawValue]
359 folder: QGroundControl.settingsManager.appSettings.logSavePath
360 title: qsTr("Save app log")
361
362 onAcceptedForSave: file => {
363 LogManager.writeMessages(file)
364 }
365 }
366}