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