6import QGroundControl.Controls
7import QGroundControl.FactControls
8import QGroundControl.PlanView
10/// Unified plan tree view showing Mission Items, GeoFence, and Rally Points
11/// as collapsible sections using a real TreeView with type-discriminating delegates.
14 model: _missionController.visualItemsTree
16 boundsBehavior: Flickable.StopAtBounds
18 pointerNavigationEnabled: false
19 selectionBehavior: TableView.SelectionDisabled
22 required property var editorMap
23 required property var planMasterController
25 signal editingLayerChangeRequested(int layer)
27 readonly property int _layerMission: 1
28 readonly property int _layerFence: 2
29 readonly property int _layerRally: 3
30 readonly property bool _createNewPlanMode: planMasterController.showCreateFromTemplate
32 on_CreateNewPlanModeChanged: {
33 if (_createNewPlanMode) {
34 var planFileRow = _rowFor(_missionController.planFileGroupIndex)
35 if (!root.isExpanded(planFileRow)) {
36 root.expand(planFileRow)
42 property var _missionController: planMasterController.missionController
43 property var _geoFenceController: planMasterController.geoFenceController
44 property var _rallyPointController: planMasterController.rallyPointController
46 // Helper: convert a persistent model index to the current visual row
47 function _rowFor(modelIndex) { return root.rowAtIndex(modelIndex) }
49 // QGCFlickableScrollIndicator expects parent to have indicatorColor (provided by QGCFlickable/QGCListView)
50 property color indicatorColor: qgcPal.text
52 QGCPalette { id: qgcPal; colorGroupEnabled: enabled }
54 QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Horizontal }
55 QGCFlickableScrollIndicator { parent: root; orientation: QGCFlickableScrollIndicator.Vertical }
57 property int _lastMissionItemCount: 0
60 target: root._missionController.visualItems
61 function onCountChanged() {
62 var newCount = _missionController.visualItems ? _missionController.visualItems.count : 0
63 if (newCount > root._lastMissionItemCount) {
64 // First waypoint added — collapse Plan Info and Defaults
65 if (root._lastMissionItemCount <= 1 && newCount > 1) {
66 var planFileRow = _rowFor(_missionController.planFileGroupIndex)
67 if (root.isExpanded(planFileRow)) {
68 root.collapse(planFileRow)
70 var defaultsRow = _rowFor(_missionController.defaultsGroupIndex)
71 if (root.isExpanded(defaultsRow)) {
72 root.collapse(defaultsRow)
75 // Expand mission group and scroll to the new item
76 var missionRow = _rowFor(_missionController.missionGroupIndex)
77 if (!root.isExpanded(missionRow)) {
78 root.expand(missionRow)
80 // Scroll happens when the editor signals editorExpandedAndLoaded
82 root._lastMissionItemCount = newCount
87 target: root._missionController
88 function onVisualItemsReset() {
89 root.collapseRecursively()
90 if (_missionController.containsItems) {
91 // Non-empty plan: expand mission group
92 root.expand(_rowFor(_missionController.missionGroupIndex))
94 // Empty plan: expand Plan Info and Defaults, scroll to top
95 root.expand(_rowFor(_missionController.planFileGroupIndex))
96 root.expand(_rowFor(_missionController.defaultsGroupIndex))
99 root._lastMissionItemCount = _missionController.visualItems ? _missionController.visualItems.count : 0
100 root.editingLayerChangeRequested(root._layerMission)
102 function onPlanViewStateChanged() {
103 // Current item changed — bring it on-screen if completely off-screen.
104 // Fine-tuned scroll happens later via editorExpandedAndLoaded.
105 var item = _missionController.currentPlanViewItem
107 var modelIndex = _missionController.visualItemsTree.indexForObject(item)
108 var row = root.rowAtIndex(modelIndex)
111 root.positionViewAtRow(row, TableView.Visible)
117 // Public API: select a layer and expand its group. Called by the layer tool buttons.
118 function selectLayer(nodeType) {
122 targetRow = _rowFor(_missionController.missionGroupIndex)
123 editingLayerChangeRequested(_layerMission)
126 targetRow = _rowFor(_missionController.fenceGroupIndex)
127 editingLayerChangeRequested(_layerFence)
130 targetRow = _rowFor(_missionController.rallyGroupIndex)
131 editingLayerChangeRequested(_layerRally)
135 if (targetRow >= 0) {
136 if (!root.isExpanded(targetRow))
137 root.expand(targetRow)
139 root.positionViewAtRow(targetRow, TableView.AlignTop)
143 // Toggle expand/collapse for a group header. Does not affect the editing layer.
144 // Caller is responsible for calling allowViewSwitch() before invoking this.
145 function _toggleGroup(row) {
146 if (root.isExpanded(row)) {
154 // Subtitle text shown on group headers, varies by node type
155 function _groupSubtitle(nodeType) {
157 case "planFileGroup": return planMasterController.currentPlanFileName === "" ? qsTr("<Untitled>") : planMasterController.currentPlanFileName
158 case "missionGroup": return _missionController.visualItems ? (_missionController.visualItems.count - 1) + qsTr(" items") : ""
159 case "rallyGroup": return _rallyPointController.points ? _rallyPointController.points.count + qsTr(" points") : ""
164 // Coalesces multiple delegate height changes into a single forceLayout() call
170 onTriggered: root.forceLayout()
173 // Called by MissionItemEditor delegates when their editor height has settled.
174 function _scrollToMissionItem(delegateItem) {
176 var bottomY = delegateItem.mapToItem(root.contentItem, 0, delegateItem.height).y
177 var neededContentY = bottomY - root.height
178 if (neededContentY > root.contentY) {
179 root.contentY = neededContentY
185 implicitWidth: root.width
186 implicitHeight: (loader.item ? loader.item.height : 1) + (separatorLine.visible ? separatorLine.height + root.rowSpacing : 0)
187 visible: !root._createNewPlanMode || _visibleInCreateMode
188 height: visible ? implicitHeight : 0
191 required property TreeView treeView
192 required property bool isTreeNode
193 required property bool expanded
194 required property bool hasChildren
195 required property int depth
196 required property int row
197 required property var model
199 readonly property var nodeObject: model.object
200 readonly property string nodeType: model.nodeType
201 readonly property bool separator: model.separator ?? false
203 // In create-new-plan mode, only show Plan Info and Defaults groups and their children
204 readonly property bool _visibleInCreateMode: nodeType === "planFileGroup" || nodeType === "planFileInfo"
205 || nodeType === "defaultsGroup" || nodeType === "defaultsInfo"
207 onImplicitHeightChanged: layoutTimer.restart()
209 readonly property string _qrcBase: "qrc:/qml/QGroundControl/PlanView/"
211 // We use setSource() instead of sourceComponent so that required properties
212 // (e.g. missionItem) are injected before internal bindings activate,
213 // preventing "Cannot read property of null" warnings.
218 Component.onCompleted: {
219 switch (delegateRoot.nodeType) {
220 case "planFileGroup":
221 case "defaultsGroup":
225 case "transformGroup":
226 sourceComponent = groupHeaderComponent
229 setSource(delegateRoot._qrcBase + "PlanInfoEditor.qml", {
230 width: Qt.binding(() => delegateRoot.width),
231 planMasterController: root.planMasterController,
232 missionController: root._missionController,
233 editorMap: root.editorMap
237 setSource(delegateRoot._qrcBase + "MissionDefaultsEditor.qml", {
238 width: Qt.binding(() => delegateRoot.width),
239 missionController: root._missionController,
240 planMasterController: root.planMasterController
244 if (delegateRoot.nodeObject) {
245 setSource(delegateRoot._qrcBase + "MissionItemEditor.qml", {
246 width: Qt.binding(() => delegateRoot.width),
248 missionItem: delegateRoot.nodeObject
253 if (delegateRoot.nodeObject) {
254 setSource(delegateRoot._qrcBase + "GeoFenceEditor.qml", {
255 width: Qt.binding(() => delegateRoot.width),
256 myGeoFenceController: root._geoFenceController,
257 flightMap: root.editorMap
262 if (delegateRoot.nodeObject) {
263 setSource(delegateRoot._qrcBase + "RallyPointEditorHeader.qml", {
264 width: Qt.binding(() => delegateRoot.width),
265 controller: root._rallyPointController
270 if (delegateRoot.nodeObject) {
271 setSource(delegateRoot._qrcBase + "RallyPointItemEditor.qml", {
272 width: Qt.binding(() => delegateRoot.width),
273 rallyPoint: delegateRoot.nodeObject,
274 controller: root._rallyPointController
278 case "transformEditor":
279 setSource(delegateRoot._qrcBase + "TransformEditor.qml", {
280 width: Qt.binding(() => delegateRoot.width),
281 missionController: root._missionController
288 if (delegateRoot.nodeType === "missionItem" && item) {
289 item.clicked.connect(function() {
290 root._missionController.setCurrentPlanViewSeqNum(delegateRoot.nodeObject.sequenceNumber, false)
292 item.remove.connect(function() {
293 var viIndex = root._missionController.visualItemIndexForObject(delegateRoot.nodeObject)
295 root._missionController.removeVisualItem(viIndex)
298 item.selectNextNotReadyItem.connect(function() {
299 for (var i = 0; i < root._missionController.visualItems.count; i++) {
300 var vmi = root._missionController.visualItems.get(i)
301 if (vmi.readyForSaveState === VisualMissionItem.NotReadyForSaveData) {
302 root._missionController.setCurrentPlanViewSeqNum(vmi.sequenceNumber, true)
307 item.editorExpandedAndLoaded.connect(function() {
308 root._scrollToMissionItem(delegateRoot)
316 anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5
317 anchors.topMargin: root.rowSpacing
318 anchors.top: loader.bottom
319 anchors.left: parent.left
320 anchors.right: parent.right
322 color: qgcPal.groupBorder
323 visible: delegateRoot.separator
326 // ── Group header (Mission Items / GeoFence / Rally Points) ──
328 id: groupHeaderComponent
331 width: delegateRoot.width
332 height: ScreenTools.implicitComboBoxHeight + ScreenTools.defaultFontPixelWidth
333 color: qgcPal.windowShade
337 spacing: ScreenTools.defaultFontPixelWidth * 0.5
338 anchors.verticalCenter: parent.verticalCenter
339 anchors.left: parent.left
340 anchors.right: parent.right
341 anchors.margins: ScreenTools.defaultFontPixelWidth * 0.5
344 Layout.alignment: Qt.AlignVCenter
345 Layout.preferredWidth: ScreenTools.defaultFontPixelHeight * 0.75
346 Layout.preferredHeight: Layout.preferredWidth
347 source: "/InstrumentValueIcons/cheveron-right.svg"
349 rotation: delegateRoot.expanded ? 90 : 0
353 Layout.alignment: Qt.AlignBaseline
354 text: delegateRoot.nodeObject ? delegateRoot.nodeObject.objectName : ""
359 Layout.alignment: Qt.AlignBaseline
360 Layout.fillWidth: true
361 text: root._groupSubtitle(delegateRoot.nodeType)
362 elide: Text.ElideRight
363 font.pointSize: ScreenTools.smallFontPointSize
364 color: qgcPal.colorGrey
371 if (!mainWindow.allowViewSwitch()) {
374 root._toggleGroup(delegateRoot.row)