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