QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MissionItemTreeView.qml
Go to the documentation of this file.
1import QtQuick
2import QtQuick.Controls
3import QtQuick.Layouts
4
5import QGroundControl
6import QGroundControl.Controls
7import QGroundControl.FactControls
8
9/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points
10/// as collapsible sections using a real TreeView with type-discriminating delegates.
11TreeView {
12 id: root
13
14 required property var editorMap
15 required property var planMasterController
16
17 signal editingLayerChangeRequested(int layer)
18
19 readonly property int _layerMission: 1
20 readonly property int _layerFence: 2
21 readonly property int _layerRally: 3
22
23 property var _missionController: planMasterController.missionController
24 property var _geoFenceController: planMasterController.geoFenceController
25 property var _rallyPointController: planMasterController.rallyPointController
26
27 model: _missionController.visualItemsTree
28 clip: true
29 boundsBehavior: Flickable.StopAtBounds
30 reuseItems: false
31 pointerNavigationEnabled: false
32 selectionBehavior: TableView.SelectionDisabled
33 rowSpacing: 2
34
35 // Helper: convert a persistent model index to the current visual row
36 function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) }
37
38 // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView)
39 property color indicatorColor: qgcPal.text
40
41 QGCPalette { id: qgcPal; colorGroupEnabled: enabled }
42
43 QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal }
44 QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical }
45
46 Connections {
47 target: root._missionController
48 function onVisualItemsChanged() {
49 // Mission group always expanded after rebuild (clear / load)
50 root.collapseRecursively()
51 root.expand(_rowFor(_missionController.missionGroupIndex))
52 root.editingLayerChangeRequested(root._layerMission)
53 }
54 }
55
56 // Public API: select a layer and expand its group. Called by the layer tool buttons.
57 function selectLayer(nodeType) {
58 let targetRow = -1
59 switch (nodeType) {
60 case "missionGroup":
61 targetRow = _rowFor(_missionController.missionGroupIndex)
62 editingLayerChangeRequested(_layerMission)
63 break
64 case "fenceGroup":
65 targetRow = _rowFor(_missionController.fenceGroupIndex)
66 editingLayerChangeRequested(_layerFence)
67 break
68 case "rallyGroup":
69 targetRow = _rowFor(_missionController.rallyGroupIndex)
70 editingLayerChangeRequested(_layerRally)
71 break
72 }
73
74 if (targetRow >= 0) {
75 if (!root.isExpanded(targetRow))
76 root.expand(targetRow)
77 root.forceLayout()
78 root.positionViewAtRow(targetRow, TableView.AlignTop)
79 }
80 }
81
82 // Toggle expand/collapse for a group header. Does not affect the editing layer.
83 function _toggleGroup(row) {
84 if (root.isExpanded(row))
85 root.collapse(row)
86 else
87 root.expand(row)
88 root.forceLayout()
89 }
90
91 // Coalesces multiple delegate height changes into a single forceLayout() call
92 Timer {
93 id: layoutTimer
94 interval: 0
95 running: false
96 repeat: false
97 onTriggered: root.forceLayout()
98 }
99
100 delegate: Item {
101 id: delegateRoot
102
103 required property TreeView treeView
104 required property bool isTreeNode
105 required property bool expanded
106 required property bool hasChildren
107 required property int depth
108 required property int row
109 required property var model
110
111 readonly property var nodeObject: model.object
112 readonly property string nodeType: model.nodeType
113
114 implicitWidth: root.width
115 implicitHeight: loader.item ? loader.item.height : 1
116 width: root.width
117 height: implicitHeight
118
119 onImplicitHeightChanged: layoutTimer.restart()
120
121 Loader {
122 id: loader
123 width: parent.width
124 sourceComponent: {
125 // Guard: non-group delegates need a valid object. During model
126 // row removal the role data goes null before the delegate is
127 // destroyed, which would cause "Cannot read property of null"
128 // warnings in every downstream editor binding.
129 switch (delegateRoot.nodeType) {
130 case "planFileGroup": return groupHeaderComponent
131 case "defaultsGroup": return groupHeaderComponent
132 case "missionGroup": return groupHeaderComponent
133 case "fenceGroup": return groupHeaderComponent
134 case "rallyGroup": return groupHeaderComponent
135 case "planFileInfo": return planFileInfoComponent
136 case "defaultsInfo": return defaultsEditorComponent
137 case "missionItem": return delegateRoot.nodeObject ? missionItemComponent : null
138 case "fenceEditor": return delegateRoot.nodeObject ? fenceEditorComponent : null
139 case "rallyHeader": return delegateRoot.nodeObject ? rallyHeaderComponent : null
140 case "rallyItem": return delegateRoot.nodeObject ? rallyItemComponent : null
141 default: return null
142 }
143 }
144 }
145
146 // ── Group header (Mission Items / GeoFence / Rally Points) ──
147 Component {
148 id: groupHeaderComponent
149
150 Rectangle {
151 width: delegateRoot.width
152 height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth
153 color: qgcPal.windowShade
154
155 Row {
156 id: groupHeaderRow
157 spacing: ScreenTools.defaultFontPixelWidth * 0.5
158 anchors.verticalCenter: parent.verticalCenter
159 anchors.left: parent.left
160 anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 0.5
161
162 QGCColoredImage {
163 width: ScreenTools.defaultFontPixelHeight * 0.75
164 height: width
165 source: "/InstrumentValueIcons/cheveron-right.svg"
166 color: qgcPal.text
167 anchors.verticalCenter: parent.verticalCenter
168 rotation: delegateRoot.expanded ? 90 : 0
169 }
170
171 QGCLabel {
172 text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : ""
173 font.bold: true
174 anchors.verticalCenter: parent.verticalCenter
175 }
176 }
177
178 MouseArea {
179 anchors.fill: parent
180 onClicked: root._toggleGroup(delegateRoot.row)
181 }
182 }
183 }
184
185 // ── Plan file info delegate ──
186 Component {
187 id: planFileInfoComponent
188
189 Rectangle {
190 width: delegateRoot.width
191 height: planFileColumn.height + ScreenTools.defaultFontPixelHeight
192 color: qgcPal.windowShadeDark
193
194 Column {
195 id: planFileColumn
196 anchors.left: parent.left
197 anchors.right: parent.right
198 anchors.verticalCenter: parent.verticalCenter
199 anchors.margins: ScreenTools.defaultFontPixelWidth
200 spacing: ScreenTools.defaultFontPixelHeight * 0.25
201
202 QGCTextField {
203 id: planNameField
204 placeholderText: qsTr("Untitled")
205 width: parent.width
206
207 Component.onCompleted: text = root.planMasterController.currentPlanFileName
208
209 Connections {
210 target: root.planMasterController
211 function onCurrentPlanFileNameChanged() {
212 if (!planNameField.activeFocus) {
213 planNameField.text = root.planMasterController.currentPlanFileName
214 }
215 }
216 }
217
218 onEditingFinished: root.planMasterController.currentPlanFileName = text
219 }
220 }
221 }
222 }
223
224 // ── Defaults editor delegate ──
225 Component {
226 id: defaultsEditorComponent
227
228 Rectangle {
229 id: defaultsRect
230 width: delegateRoot.width
231 height: defaultsColumn.height + ScreenTools.defaultFontPixelHeight
232 color: qgcPal.windowShadeDark
233
234 property var _missionController: root._missionController
235 property var _controllerVehicle: root.planMasterController.controllerVehicle
236 property var _visualItems: root._missionController.visualItems
237 property bool _noMissionItemsAdded: _visualItems ? _visualItems.count <= 1 : true
238 property var _settingsItem: _visualItems && _visualItems.count > 0 ? _visualItems.get(0) : null
239 property bool _multipleFirmware: !QGroundControl.singleFirmwareSupport
240 property bool _multipleVehicleTypes: !QGroundControl.singleVehicleSupport
241 property bool _allowFWVehicleTypeSelection: _noMissionItemsAdded && !globals.activeVehicle
242 property bool _showCruiseSpeed: _controllerVehicle ? !_controllerVehicle.multiRotor : false
243 property bool _showHoverSpeed: _controllerVehicle ? (_controllerVehicle.multiRotor || _controllerVehicle.vtol) : false
244 property bool _vehicleHasHomePosition: _controllerVehicle ? _controllerVehicle.homePosition.isValid : false
245 property bool _waypointsOnlyMode: QGroundControl.corePlugin.options.missionWaypointsOnly
246 property real _fieldWidth: ScreenTools.defaultFontPixelWidth * 16
247 readonly property real _margin: ScreenTools.defaultFontPixelWidth / 2
248
249 Connections {
250 target: defaultsRect._controllerVehicle
251 function onFirmwareTypeChanged() {
252 if (!defaultsRect._controllerVehicle.supports.terrainFrame
253 && defaultsRect._missionController.globalAltitudeMode === QGroundControl.AltitudeModeTerrainFrame) {
254 defaultsRect._missionController.globalAltitudeMode = QGroundControl.AltitudeModeCalcAboveTerrain
255 }
256 }
257 }
258
259 Component { id: altModeDialogComponent; AltModeDialog { } }
260
261 QGCPopupDialogFactory {
262 id: defaultsAltModeDialogFactory
263 dialogComponent: altModeDialogComponent
264 }
265
266 ColumnLayout {
267 id: defaultsColumn
268 anchors.left: parent.left
269 anchors.right: parent.right
270 anchors.verticalCenter: parent.verticalCenter
271 anchors.margins: ScreenTools.defaultFontPixelWidth
272 spacing: ScreenTools.defaultFontPixelHeight * 0.5
273
274 LabelledButton {
275 Layout.fillWidth: true
276 label: qsTr("Altitude Mode")
277 buttonText: QGroundControl.altitudeModeShortDescription(defaultsRect._missionController.globalAltitudeMode)
278
279 onClicked: {
280 let removeModes = []
281 let updateFunction = function(altMode) { defaultsRect._missionController.globalAltitudeMode = altMode }
282 if (!defaultsRect._controllerVehicle.supports.terrainFrame) {
283 removeModes.push(QGroundControl.AltitudeModeTerrainFrame)
284 }
285 if (!defaultsRect._noMissionItemsAdded) {
286 if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeRelative) {
287 removeModes.push(QGroundControl.AltitudeModeRelative)
288 }
289 if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeAbsolute) {
290 removeModes.push(QGroundControl.AltitudeModeAbsolute)
291 }
292 if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeCalcAboveTerrain) {
293 removeModes.push(QGroundControl.AltitudeModeCalcAboveTerrain)
294 }
295 if (defaultsRect._missionController.globalAltitudeMode !== QGroundControl.AltitudeModeTerrainFrame) {
296 removeModes.push(QGroundControl.AltitudeModeTerrainFrame)
297 }
298 }
299 defaultsAltModeDialogFactory.open({ rgRemoveModes: removeModes, updateAltModeFn: updateFunction })
300 }
301 }
302
303 FactTextFieldSlider {
304 Layout.fillWidth: true
305 label: qsTr("Waypoints Altitude")
306 fact: QGroundControl.settingsManager.appSettings.defaultMissionItemAltitude
307 }
308
309 FactTextFieldSlider {
310 Layout.fillWidth: true
311 label: qsTr("Flight Speed")
312 fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.flightSpeed : null
313 showEnableCheckbox: true
314 enableCheckBoxChecked: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.specifyFlightSpeed : false
315 visible: defaultsRect._settingsItem ? defaultsRect._settingsItem.speedSection.available : false
316
317 onEnableCheckboxClicked: {
318 if (defaultsRect._settingsItem) {
319 defaultsRect._settingsItem.speedSection.specifyFlightSpeed = enableCheckBoxChecked
320 }
321 }
322 }
323
324 // ── Vehicle Info ──
325 SectionHeader {
326 id: vehicleInfoSectionHeader
327 Layout.fillWidth: true
328 text: qsTr("Vehicle Info")
329 visible: !defaultsRect._waypointsOnlyMode
330 checked: false
331 }
332
333 GridLayout {
334 Layout.fillWidth: true
335 columnSpacing: ScreenTools.defaultFontPixelWidth
336 rowSpacing: columnSpacing
337 columns: 2
338 visible: vehicleInfoSectionHeader.visible && vehicleInfoSectionHeader.checked
339
340 QGCLabel {
341 text: qsTr("Firmware")
342 Layout.fillWidth: true
343 visible: defaultsRect._multipleFirmware
344 }
345 FactComboBox {
346 fact: QGroundControl.settingsManager.appSettings.offlineEditingFirmwareClass
347 indexModel: false
348 Layout.preferredWidth: defaultsRect._fieldWidth
349 visible: defaultsRect._multipleFirmware && defaultsRect._allowFWVehicleTypeSelection
350 }
351 QGCLabel {
352 text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.firmwareTypeString : ""
353 visible: defaultsRect._multipleFirmware && !defaultsRect._allowFWVehicleTypeSelection
354 }
355
356 QGCLabel {
357 text: qsTr("Vehicle")
358 Layout.fillWidth: true
359 visible: defaultsRect._multipleVehicleTypes
360 }
361 FactComboBox {
362 fact: QGroundControl.settingsManager.appSettings.offlineEditingVehicleClass
363 indexModel: false
364 Layout.preferredWidth: defaultsRect._fieldWidth
365 visible: defaultsRect._multipleVehicleTypes && defaultsRect._allowFWVehicleTypeSelection
366 }
367 QGCLabel {
368 text: defaultsRect._controllerVehicle ? defaultsRect._controllerVehicle.vehicleTypeString : ""
369 visible: defaultsRect._multipleVehicleTypes && !defaultsRect._allowFWVehicleTypeSelection
370 }
371
372 QGCLabel {
373 Layout.columnSpan: 2
374 Layout.alignment: Qt.AlignHCenter
375 Layout.fillWidth: true
376 wrapMode: Text.WordWrap
377 font.pointSize: ScreenTools.smallFontPointSize
378 text: qsTr("The following speed values are used to calculate total mission time. They do not affect the flight speed for the mission.")
379 visible: defaultsRect._showCruiseSpeed || defaultsRect._showHoverSpeed
380 }
381
382 QGCLabel {
383 text: qsTr("Cruise speed")
384 visible: defaultsRect._showCruiseSpeed
385 Layout.fillWidth: true
386 }
387 FactTextField {
388 fact: QGroundControl.settingsManager.appSettings.offlineEditingCruiseSpeed
389 visible: defaultsRect._showCruiseSpeed
390 Layout.preferredWidth: defaultsRect._fieldWidth
391 }
392
393 QGCLabel {
394 text: qsTr("Hover speed")
395 visible: defaultsRect._showHoverSpeed
396 Layout.fillWidth: true
397 }
398 FactTextField {
399 fact: QGroundControl.settingsManager.appSettings.offlineEditingHoverSpeed
400 visible: defaultsRect._showHoverSpeed
401 Layout.preferredWidth: defaultsRect._fieldWidth
402 }
403 }
404
405 // ── Launch Position ──
406 SectionHeader {
407 id: plannedHomePositionSection
408 Layout.fillWidth: true
409 text: qsTr("Launch Position")
410 visible: !defaultsRect._vehicleHasHomePosition
411 checked: false
412 }
413
414 GridLayout {
415 Layout.fillWidth: true
416 columnSpacing: ScreenTools.defaultFontPixelWidth
417 rowSpacing: columnSpacing
418 columns: 2
419 visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition
420
421 QGCLabel {
422 text: qsTr("Altitude")
423 }
424 FactTextField {
425 fact: defaultsRect._settingsItem ? defaultsRect._settingsItem.plannedHomePositionAltitude : null
426 Layout.fillWidth: true
427 }
428 }
429
430 QGCLabel {
431 Layout.fillWidth: true
432 wrapMode: Text.WordWrap
433 font.pointSize: ScreenTools.smallFontPointSize
434 text: qsTr("Actual position set by vehicle at flight time.")
435 horizontalAlignment: Text.AlignHCenter
436 visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition
437 }
438
439 QGCButton {
440 text: qsTr("Set To Map Center")
441 Layout.alignment: Qt.AlignHCenter
442 visible: plannedHomePositionSection.checked && !defaultsRect._vehicleHasHomePosition
443 onClicked: {
444 if (defaultsRect._settingsItem) {
445 defaultsRect._settingsItem.coordinate = root.editorMap.center
446 }
447 }
448 }
449 }
450 }
451 }
452
453 // ── Mission item delegate ──
454 Component {
455 id: missionItemComponent
456
457 MissionItemEditor {
458 width: delegateRoot.width
459 map: root.editorMap
460 masterController: root.planMasterController
461 missionItem: delegateRoot.nodeObject
462 readOnly: false
463
464 onClicked: root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false)
465
466 onRemove: {
467 var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject)
468 if (viIndex > 0) {
469 root._missionController.removeVisualItem(viIndex)
470 }
471 }
472
473 onSelectNextNotReadyItem: {
474 for (var i = 0; i < root._missionController.visualItems.count; i++) {
475 var vmi = root._missionController.visualItems.get(i)
476 if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) {
477 root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true)
478 break
479 }
480 }
481 }
482 }
483 }
484
485 // ── GeoFence editor (single child of fence group) ──
486 Component {
487 id: fenceEditorComponent
488
489 GeoFenceEditor {
490 width: delegateRoot.width
491 myGeoFenceController: root._geoFenceController
492 flightMap: root.editorMap
493 }
494 }
495
496 // ── Rally header / instructions ──
497 Component {
498 id: rallyHeaderComponent
499
500 RallyPointEditorHeader {
501 width: delegateRoot.width
502 controller: root._rallyPointController
503 }
504 }
505
506 // ── Rally point item editor ──
507 Component {
508 id: rallyItemComponent
509
510 RallyPointItemEditor {
511 width: delegateRoot.width
512 rallyPoint: delegateRoot.nodeObject
513 controller: root._rallyPointController
514 }
515 }
516 }
517}