QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
VehicleConfigView.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5import QGroundControl
6import QGroundControl.Controls
7
8Rectangle {
9 id: vehicleConfigView
10 color: qgcPal.window
11 z: QGroundControl.zOrderTopMost
12
13 // This need to block click event leakage to underlying map.
14 DeadMouseArea {
15 anchors.fill: parent
16 }
17
18 QGCPalette { id: qgcPal; colorGroupEnabled: true }
19
20 readonly property real _defaultTextHeight: ScreenTools.defaultFontPixelHeight
21 readonly property real _defaultTextWidth: ScreenTools.defaultFontPixelWidth
22 readonly property real _horizontalMargin: _defaultTextWidth / 2
23 readonly property real _verticalMargin: _defaultTextHeight / 2
24 readonly property real _buttonWidth: _defaultTextWidth * 18
25 readonly property string _armedVehicleText: qsTr("This operation cannot be performed while the vehicle is armed.")
26
27 property var _activeVehicle: QGroundControl.multiVehicleManager.activeVehicle
28 property bool _vehicleArmed: _activeVehicle ? _activeVehicle.armed : false
29 property string _messagePanelText: qsTr("missing message panel text")
30 property bool _fullParameterVehicleAvailable: _activeVehicle && QGroundControl.multiVehicleManager.parameterReadyVehicleAvailable && !_activeVehicle.parameterManager.missingParameters
31 property var _corePlugin: QGroundControl.corePlugin
32
33 // Tree view state
34 property int _selectedComponentIndex: -1 // -1 = summary or special button
35 property int _selectedSectionIndex: -1
36 property string _selectedSpecial: "" // "summary", "parameters", "firmware", "opticalflow"
37 property var _expandedComponents: ({})
38 property int _expandedRevision: 0
39 property string _searchQuery: ""
40
41 function _setExpanded(compIndex, value) {
42 _expandedComponents[compIndex] = value
43 _expandedRevision++
44 }
45
46 function _isExpanded(compIndex) {
47 void _expandedRevision
48 return !!_expandedComponents[compIndex]
49 }
50
51 /// Translate a section name using the component's JSON filename as context.
52 /// Falls back to the raw name when no vehicleConfigJson is set.
53 function _translateSection(component, name) {
54 var context = _translationContext(component)
55 if (!context) return name
56 return qsTranslate(context, name)
57 }
58
59 /// Get the section name for a sidebar entry.
60 function _sectionName(compIndex, sectionIndex) {
61 if (sectionIndex < 0 || !_fullParameterVehicleAvailable) return ""
62 var components = _activeVehicle.autopilotPlugin.vehicleComponents
63 if (compIndex < 0 || compIndex >= components.length) return ""
64 var secs = components[compIndex].sections
65 if (sectionIndex < secs.length) return secs[sectionIndex]
66 return ""
67 }
68
69 /// Extract the translation context (JSON filename) from a component.
70 function _translationContext(component) {
71 if (!component || !component.vehicleConfigJson) return ""
72 var path = component.vehicleConfigJson.toString()
73 var slash = path.lastIndexOf("/")
74 return slash >= 0 ? path.substring(slash + 1) : path
75 }
76
77 function _componentMatchesSearch(component) {
78 if (_searchQuery.trim() === "") return true
79 var query = _searchQuery.toLowerCase().trim()
80 if (component.name.toLowerCase().indexOf(query) !== -1) return true
81 var context = _translationContext(component)
82 var secs = component.sections
83 if (secs) {
84 for (var i = 0; i < secs.length; i++) {
85 if (secs[i].toLowerCase().indexOf(query) !== -1) return true
86 if (context && qsTranslate(context, secs[i]).toLowerCase().indexOf(query) !== -1) return true
87 }
88 }
89 var keywords = component.sectionKeywords
90 if (keywords) {
91 for (var key in keywords) {
92 var terms = keywords[key]
93 for (var j = 0; j < terms.length; j++) {
94 if (terms[j].toLowerCase().indexOf(query) !== -1) return true
95 if (context && qsTranslate(context, terms[j]).toLowerCase().indexOf(query) !== -1) return true
96 }
97 }
98 }
99 return false
100 }
101
102 function _sectionMatchesSearch(component, sectionName) {
103 if (_searchQuery.trim() === "") return true
104 var query = _searchQuery.toLowerCase().trim()
105 if (sectionName.toLowerCase().indexOf(query) !== -1) return true
106 var context = _translationContext(component)
107 if (context && qsTranslate(context, sectionName).toLowerCase().indexOf(query) !== -1) return true
108 var keywords = component.sectionKeywords
109 if (keywords && keywords[sectionName]) {
110 var terms = keywords[sectionName]
111 for (var i = 0; i < terms.length; i++) {
112 if (terms[i].toLowerCase().indexOf(query) !== -1) return true
113 if (context && qsTranslate(context, terms[i]).toLowerCase().indexOf(query) !== -1) return true
114 }
115 }
116 return false
117 }
118
119 function showSummaryPanel() {
120 if (mainWindow.allowViewSwitch()) {
121 _showSummaryPanel()
122 }
123 }
124
125 function _showSummaryPanel() {
126 _selectedSpecial = "summary"
127 _selectedComponentIndex = -1
128 _selectedSectionIndex = -1
129 if (_fullParameterVehicleAvailable) {
130 if (_activeVehicle.autopilotPlugin.vehicleComponents.length === 0) {
131 panelLoader.setSourceComponent(noComponentsVehicleSummaryComponent)
132 } else {
133 panelLoader.setSource("qrc:/qml/QGroundControl/VehicleSetup/VehicleSummary.qml")
134 }
135 } else if (QGroundControl.multiVehicleManager.parameterReadyVehicleAvailable) {
136 panelLoader.setSourceComponent(missingParametersVehicleSummaryComponent)
137 } else {
138 panelLoader.setSourceComponent(disconnectedVehicleAndParamsSummaryComponent)
139 }
140 }
141
142 function showPanel(specialName, qmlSource) {
143 if (mainWindow.allowViewSwitch()) {
144 _selectedSpecial = specialName
145 _selectedComponentIndex = -1
146 _selectedSectionIndex = -1
147 panelLoader.setSource(qmlSource)
148 }
149 }
150
151 function _navigateToComponent(compIndex, sectionIndex) {
152 if (!mainWindow.allowViewSwitch()) return
153 if (!_fullParameterVehicleAvailable) return
154
155 var components = _activeVehicle.autopilotPlugin.vehicleComponents
156 if (compIndex < 0 || compIndex >= components.length) return
157 var vehicleComponent = components[compIndex]
158
159 var autopilotPlugin = _activeVehicle.autopilotPlugin
160 var prereq = autopilotPlugin.prerequisiteSetup(vehicleComponent)
161 if (prereq !== "") {
162 _messagePanelText = qsTr("%1 setup must be completed prior to %2 setup.").arg(prereq).arg(vehicleComponent.name)
163 panelLoader.setSourceComponent(messagePanelComponent)
164 return
165 }
166
167 _selectedSpecial = ""
168
169 // If component opts in and root was clicked, auto-select first section
170 if (sectionIndex < 0 && vehicleComponent.showFirstSectionOnRootClick && vehicleComponent.sections.length > 0) {
171 sectionIndex = 0
172 }
173 _selectedSectionIndex = sectionIndex
174
175 if (_selectedComponentIndex !== compIndex) {
176 _selectedComponentIndex = compIndex
177 panelLoader.setSource(vehicleComponent.setupSource, vehicleComponent)
178 }
179
180 // Apply section filter
181 if (panelLoader.item && typeof panelLoader.item.sectionNameFilter !== "undefined") {
182 panelLoader.item.sectionNameFilter = _sectionName(compIndex, sectionIndex)
183 }
184 }
185
186 function showParametersPanel() {
187 showPanel("parameters", "qrc:/qml/QGroundControl/VehicleSetup/SetupParameterEditor.qml")
188 }
189
190 function showVehicleComponentPanel(vehicleComponent) {
191 if (!mainWindow.allowViewSwitch()) return
192 if (!_fullParameterVehicleAvailable) return
193
194 var components = _activeVehicle.autopilotPlugin.vehicleComponents
195 for (var i = 0; i < components.length; i++) {
196 if (components[i] === vehicleComponent) {
197 _navigateToComponent(i, -1)
198 return
199 }
200 }
201 }
202
203 Component.onCompleted: _showSummaryPanel()
204
205 Connections {
206 target: QGroundControl.corePlugin
207 function onShowAdvancedUIChanged(showAdvancedUI) {
208 if (!showAdvancedUI) {
209 _showSummaryPanel()
210 }
211 }
212 }
213
214 Connections {
215 target: QGroundControl.multiVehicleManager
216 function onParameterReadyVehicleAvailableChanged(parametersReady) {
217 if (parametersReady || _selectedSpecial === "summary" || _selectedSpecial !== "firmware") {
218 _showSummaryPanel()
219 }
220 }
221 }
222
223 Connections {
224 target: panelLoader
225 function onLoaded() {
226 if (panelLoader.item && typeof panelLoader.item.sectionNameFilter !== "undefined") {
227 panelLoader.item.sectionNameFilter = _sectionName(_selectedComponentIndex, _selectedSectionIndex)
228 }
229 }
230 }
231
232 Component {
233 id: noComponentsVehicleSummaryComponent
234 Rectangle {
235 color: qgcPal.windowShade
236 QGCLabel {
237 anchors.margins: _defaultTextWidth * 2
238 anchors.fill: parent
239 verticalAlignment: Text.AlignVCenter
240 horizontalAlignment: Text.AlignHCenter
241 wrapMode: Text.WordWrap
242 font.pointSize: ScreenTools.mediumFontPointSize
243 text: qsTr("%1 does not currently support configuration of your vehicle. ").arg(QGroundControl.appName) +
244 "If your vehicle is already configured you can still Fly."
245 }
246 }
247 }
248
249 Component {
250 id: disconnectedVehicleAndParamsSummaryComponent
251 Rectangle {
252 id: disconnectedRect
253 color: qgcPal.windowShade
254 Column {
255 anchors.centerIn: parent
256 spacing: ScreenTools.defaultFontPixelHeight
257 QGCLabel {
258 anchors.horizontalCenter: parent.horizontalCenter
259 width: disconnectedRect.width - _defaultTextWidth * 4
260 horizontalAlignment: Text.AlignHCenter
261 wrapMode: Text.WordWrap
262 font.pointSize: ScreenTools.largeFontPointSize
263 text: !_activeVehicle
264 ? qsTr("Vehicle configuration pages will display after you connect your vehicle and parameters have been downloaded.")
265 : (_activeVehicle.parameterManager.parameterDownloadSkipped
266 ? qsTr("Parameter download was skipped because the vehicle is flying. Configuration pages will be available after parameters are downloaded.")
267 : qsTr("Waiting for vehicle parameters to download…"))
268 }
269 QGCButton {
270 anchors.horizontalCenter: parent.horizontalCenter
271 text: qsTr("Download Parameters")
272 visible: _activeVehicle && _activeVehicle.parameterManager.parameterDownloadSkipped
273 enabled: _activeVehicle && _activeVehicle.parameterManager.parameterDownloadSkipped && _activeVehicle.parameterManager.loadProgress === 0
274 onClicked: _activeVehicle.parameterManager.refreshAllParameters()
275 }
276 }
277 }
278 }
279
280 Component {
281 id: missingParametersVehicleSummaryComponent
282
283 Rectangle {
284 color: qgcPal.windowShade
285
286 QGCLabel {
287 anchors.margins: _defaultTextWidth * 2
288 anchors.fill: parent
289 verticalAlignment: Text.AlignVCenter
290 horizontalAlignment: Text.AlignHCenter
291 wrapMode: Text.WordWrap
292 font.pointSize: ScreenTools.mediumFontPointSize
293 text: qsTr("Vehicle did not return the full parameter list. ") +
294 qsTr("As a result, the configuration pages are not available.")
295 }
296 }
297 }
298
299 Component {
300 id: messagePanelComponent
301
302 Item {
303 QGCLabel {
304 anchors.margins: _defaultTextWidth * 2
305 anchors.fill: parent
306 verticalAlignment: Text.AlignVCenter
307 horizontalAlignment: Text.AlignHCenter
308 wrapMode: Text.WordWrap
309 font.pointSize: ScreenTools.mediumFontPointSize
310 text: _messagePanelText
311 }
312 }
313 }
314
315 ColumnLayout {
316 id: leftPanel
317 width: Math.max(buttonColumn.implicitWidth + _horizontalMargin, ScreenTools.defaultFontPixelWidth * 22)
318 anchors.topMargin: _verticalMargin
319 anchors.top: parent.top
320 anchors.bottom: parent.bottom
321 anchors.leftMargin: _horizontalMargin
322 anchors.left: parent.left
323 spacing: _verticalMargin / 2
324
325 QGCTextField {
326 id: searchField
327 Layout.fillWidth: true
328 placeholderText: qsTr("Search configuration...")
329 visible: _fullParameterVehicleAvailable
330
331 onTextChanged: {
332 vehicleConfigView._searchQuery = text
333 }
334 }
335
336 QGCFlickable {
337 Layout.fillWidth: true
338 Layout.fillHeight: true
339 contentHeight: buttonColumn.height + _verticalMargin
340 flickableDirection: Flickable.VerticalFlick
341 clip: true
342
343 ColumnLayout {
344 id: buttonColumn
345 width: parent.width
346 spacing: 0
347
348 // Summary button
349 ConfigButton {
350 id: summaryButton
351 icon.source: "/qmlimages/VehicleSummaryIcon.png"
352 checked: vehicleConfigView._selectedSpecial === "summary"
353 text: qsTr("Summary")
354 Layout.fillWidth: true
355 visible: vehicleConfigView._searchQuery.trim() === ""
356
357 onClicked: showSummaryPanel()
358 }
359
360 Item {
361 Layout.fillWidth: true
362 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight / 2
363 visible: vehicleConfigView._searchQuery.trim() === ""
364 }
365
366 // Vehicle component tree
367 Repeater {
368 id: componentRepeater
369 model: _fullParameterVehicleAvailable ? _activeVehicle.autopilotPlugin.vehicleComponents : 0
370
371 ColumnLayout {
372 id: compColumn
373 spacing: 0
374 Layout.fillWidth: true
375
376 required property int index
377 required property var modelData
378
379 property var comp: modelData
380 property string compName: comp ? comp.name : ""
381 property var compSections: comp ? comp.sections : []
382 property bool isSelected: vehicleConfigView._selectedComponentIndex === index && vehicleConfigView._selectedSpecial === ""
383 property bool hasSections: compSections.length > 1
384 property bool isSearching: vehicleConfigView._searchQuery.trim() !== ""
385 property bool matchesSearch: comp ? vehicleConfigView._componentMatchesSearch(comp) : false
386 property bool isExpanded: hasSections && (isSearching ? matchesSearch : vehicleConfigView._isExpanded(index))
387
388 visible: {
389 if (!comp) return false
390 if (comp.setupSource.toString() === "") return false
391 if (isSearching) return matchesSearch
392 return true
393 }
394
395 ConfigButton {
396 Layout.fillWidth: true
397 icon.source: compColumn.comp ? compColumn.comp.iconResource : ""
398 setupComplete: compColumn.comp ? compColumn.comp.setupComplete : true
399 text: compColumn.compName
400 expandable: compColumn.hasSections
401 expanded: compColumn.isExpanded
402 checked: compColumn.isSelected && vehicleConfigView._selectedSectionIndex === -1
403
404 onClicked: {
405 vehicleConfigView._navigateToComponent(compColumn.index, -1)
406 if (compColumn.hasSections) {
407 if (compColumn.isSelected && compColumn.isExpanded) {
408 vehicleConfigView._setExpanded(compColumn.index, false)
409 } else if (!compColumn.isExpanded) {
410 vehicleConfigView._setExpanded(compColumn.index, true)
411 }
412 }
413 }
414
415 onToggleExpand: {
416 if (!mainWindow.allowViewSwitch()) return
417 var expanding = !compColumn.isExpanded
418 vehicleConfigView._setExpanded(compColumn.index, expanding)
419 if (!expanding && compColumn.isSelected) {
420 vehicleConfigView._navigateToComponent(compColumn.index, -1)
421 }
422 }
423 }
424
425 // Section sub-items
426 Repeater {
427 model: compColumn.isExpanded ? compColumn.compSections : []
428
429 Button {
430 id: sectionBtn
431 Layout.fillWidth: true
432 padding: ScreenTools.defaultFontPixelWidth * 0.75
433 leftPadding: ScreenTools.defaultFontPixelWidth * 3
434 hoverEnabled: !ScreenTools.isMobile
435
436 property int sectionIndex: index
437 property bool sectionChecked: compColumn.isSelected && vehicleConfigView._selectedSectionIndex === sectionIndex
438 property bool sectionMatchesSearch: {
439 if (!compColumn.isSearching) return true
440 return vehicleConfigView._sectionMatchesSearch(compColumn.comp, modelData)
441 }
442 property bool sectionContentVisible: {
443 if (!compColumn.isSelected) return true
444 if (!panelLoader.item) return true
445 if (typeof panelLoader.item.sectionVisible !== "function") return true
446 return panelLoader.item.sectionVisible(modelData)
447 }
448 property color textColor: sectionChecked || pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText
449 visible: sectionMatchesSearch && sectionContentVisible
450
451 background: Rectangle {
452 color: qgcPal.buttonHighlight
453 opacity: sectionBtn.sectionChecked || sectionBtn.pressed ? 1 : sectionBtn.enabled && sectionBtn.hovered ? 0.2 : 0
454 radius: ScreenTools.defaultFontPixelWidth / 2
455 }
456
457 contentItem: RowLayout {
458 spacing: ScreenTools.defaultFontPixelWidth * 0.5
459
460 Rectangle {
461 width: ScreenTools.defaultFontPixelWidth
462 height: width
463 radius: width / 2
464 color: compColumn.comp && typeof compColumn.comp.sectionSetupComplete === "function"
465 ? (compColumn.comp.sectionSetupComplete(modelData) ? qgcPal.colorGreen : qgcPal.colorOrange)
466 : "transparent"
467 visible: compColumn.comp && typeof compColumn.comp.sectionSetupComplete === "function"
468 && !compColumn.comp.sectionSetupComplete(modelData)
469 }
470
471 QGCLabel {
472 text: vehicleConfigView._translateSection(compColumn.comp, modelData)
473 color: sectionBtn.textColor
474 font.pointSize: ScreenTools.defaultFontPointSize * 0.9
475 horizontalAlignment: Text.AlignLeft
476 Layout.fillWidth: true
477 }
478 }
479
480 onClicked: {
481 vehicleConfigView._navigateToComponent(compColumn.index, sectionIndex)
482 }
483 }
484 }
485 }
486 }
487
488 // Optical Flow (special)
489 ConfigButton {
490 visible: _activeVehicle ? _activeVehicle.flowImageIndex > 0 : false
491 text: qsTr("Optical Flow")
492 Layout.fillWidth: true
493 checked: vehicleConfigView._selectedSpecial === "opticalflow"
494 onClicked: showPanel("opticalflow", "qrc:/qml/QGroundControl/VehicleSetup/OpticalFlowSensor.qml")
495 }
496
497 Item {
498 Layout.fillWidth: true
499 Layout.preferredHeight: ScreenTools.defaultFontPixelHeight / 2
500 visible: vehicleConfigView._searchQuery.trim() === ""
501 }
502
503 ConfigButton {
504 id: parametersButton
505 visible: QGroundControl.multiVehicleManager.parameterReadyVehicleAvailable &&
506 !_activeVehicle.usingHighLatencyLink &&
507 _corePlugin.showAdvancedUI &&
508 vehicleConfigView._searchQuery.trim() === ""
509 text: qsTr("Parameters")
510 Layout.fillWidth: true
511 icon.source: "/qmlimages/subMenuButtonImage.png"
512 checked: vehicleConfigView._selectedSpecial === "parameters"
513 onClicked: showPanel("parameters", "qrc:/qml/QGroundControl/VehicleSetup/SetupParameterEditor.qml")
514 }
515
516 ConfigButton {
517 id: firmwareButton
518 icon.source: "/qmlimages/FirmwareUpgradeIcon.png"
519 visible: !ScreenTools.isMobile && _corePlugin.options.showFirmwareUpgrade &&
520 vehicleConfigView._searchQuery.trim() === ""
521 text: qsTr("Firmware")
522 Layout.fillWidth: true
523 checked: vehicleConfigView._selectedSpecial === "firmware"
524
525 onClicked: showPanel("firmware", "qrc:/qml/QGroundControl/VehicleSetup/FirmwareUpgrade.qml")
526 }
527 }
528 }
529 }
530
531 Rectangle {
532 id: divider
533 anchors.topMargin: _verticalMargin
534 anchors.bottomMargin: _verticalMargin
535 anchors.leftMargin: _horizontalMargin
536 anchors.left: leftPanel.right
537 anchors.top: parent.top
538 anchors.bottom: parent.bottom
539 width: 1
540 color: qgcPal.windowShade
541 }
542
543 Loader {
544 id: panelLoader
545 anchors.topMargin: _verticalMargin
546 anchors.bottomMargin: _verticalMargin
547 anchors.leftMargin: _horizontalMargin
548 anchors.rightMargin: _horizontalMargin
549 anchors.left: divider.right
550 anchors.right: parent.right
551 anchors.top: parent.top
552 anchors.bottom: parent.bottom
553
554 function setSource(source, vehicleComponent) {
555 panelLoader.source = ""
556 panelLoader.vehicleComponent = vehicleComponent
557 panelLoader.source = source
558 }
559
560 function setSourceComponent(sourceComponent, vehicleComponent) {
561 panelLoader.sourceComponent = undefined
562 panelLoader.vehicleComponent = vehicleComponent
563 panelLoader.sourceComponent = sourceComponent
564 }
565
566 property var vehicleComponent
567 }
568}