QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QmlObjectTreeModel.cc
Go to the documentation of this file.
1/****************************************************************************
2 *
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 *
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
7 *
8 ****************************************************************************/
9
10#include "QmlObjectTreeModel.h"
11
12#include <QtCore/QMetaMethod>
13#include <QtQml/QQmlEngine>
14
15#include "QGCLoggingCategory.h"
16
17QGC_LOGGING_CATEGORY(QmlObjectTreeModelLog, "API.QmlObjectTreeModel")
18
19namespace {
20
21constexpr const char* kDirtyChangedSignature = "dirtyChanged(bool)";
22constexpr const char* kChildDirtyChangedSlotSignature = "_childDirtyChanged(bool)";
23
24QMetaMethod childDirtyChangedSlot()
25{
26 const QMetaObject* metaObject = &QmlObjectTreeModel::staticMetaObject;
27 const int slotIndex = metaObject->indexOfSlot(kChildDirtyChangedSlotSignature);
28 Q_ASSERT_X(slotIndex >= 0, "childDirtyChangedSlot", "slot signature mismatch — update kChildDirtyChangedSlotSignature");
29 return (slotIndex >= 0) ? metaObject->method(slotIndex) : QMetaMethod();
30}
31
32QMetaMethod dirtyChangedSignal(const QObject* object)
33{
34 if (!object) {
35 return QMetaMethod();
36 }
37
38 const int signalIndex = object->metaObject()->indexOfSignal(kDirtyChangedSignature);
39 return (signalIndex >= 0) ? object->metaObject()->method(signalIndex) : QMetaMethod();
40}
41
42} // namespace
43
44//-----------------------------------------------------------------------------
45// TreeNode
46//-----------------------------------------------------------------------------
47
48int QmlObjectTreeModel::TreeNode::row() const
49{
50 if (parentNode) {
51 return parentNode->children.indexOf(const_cast<TreeNode*>(this));
52 }
53 return 0;
54}
55
56//-----------------------------------------------------------------------------
57// Construction / Destruction
58//-----------------------------------------------------------------------------
59
61 : ObjectItemModelBase(parent)
62{
63}
64
66{
67 // Skip disconnect — objects may already be destroyed during application shutdown.
68 // Just delete the tree nodes.
69 _deleteSubtree(&_rootNode, false);
70}
71
72//-----------------------------------------------------------------------------
73// QAbstractItemModel overrides
74//-----------------------------------------------------------------------------
75
76QModelIndex QmlObjectTreeModel::index(int row, int column, const QModelIndex& parent) const
77{
78 if (column != 0) {
79 return {};
80 }
81
82 const TreeNode* parentNode = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode;
83 if (!parentNode || row < 0 || row >= parentNode->children.count()) {
84 return {};
85 }
86
87 return createIndex(row, 0, parentNode->children.at(row));
88}
89
90QModelIndex QmlObjectTreeModel::parent(const QModelIndex& child) const
91{
92 if (!child.isValid()) {
93 return {};
94 }
95
96 const TreeNode* node = _nodeFromIndex(child);
97 if (!node || !node->parentNode || node->parentNode == &_rootNode) {
98 return {};
99 }
100
101 return _indexForNode(node->parentNode);
102}
103
104int QmlObjectTreeModel::rowCount(const QModelIndex& parent) const
105{
106 const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode;
107 return node ? node->children.count() : 0;
108}
109
110int QmlObjectTreeModel::columnCount(const QModelIndex& parent) const
111{
113 return 1;
114}
115
116bool QmlObjectTreeModel::hasChildren(const QModelIndex& parent) const
117{
118 const TreeNode* node = parent.isValid() ? _nodeFromIndex(parent) : &_rootNode;
119 return node && !node->children.isEmpty();
120}
121
122QVariant QmlObjectTreeModel::data(const QModelIndex& index, int role) const
123{
124 if (!index.isValid()) {
125 return {};
126 }
127
128 const TreeNode* node = _nodeFromIndex(index);
129 if (!node) {
130 return {};
131 }
132
133 switch (role) {
134 case ObjectRole:
135 return node->object ? QVariant::fromValue(node->object) : QVariant{};
136 case TextRole:
137 return node->object ? QVariant::fromValue(node->object->objectName()) : QVariant{};
138 case NodeTypeRole:
139 return QVariant::fromValue(node->nodeType);
140 case SeparatorRole: {
141 if (!node->children.isEmpty()) {
142 return false;
143 }
144 const TreeNode* parentNode = node->parentNode;
145 if (!parentNode) {
146 return false;
147 }
148 return node->row() < parentNode->children.size() - 1;
149 }
150 default:
151 return {};
152 }
153}
154
155bool QmlObjectTreeModel::setData(const QModelIndex& index, const QVariant& value, int role)
156{
157 if (!index.isValid() || role != ObjectRole) {
158 return false;
159 }
160
161 TreeNode* node = _nodeFromIndex(index);
162 if (!node) {
163 return false;
164 }
165
166 if (node->object) {
167 _disconnectDirtyChanged(node->object);
168 }
169
170 node->object = value.value<QObject*>();
171
172 if (node->object) {
173 QQmlEngine::setObjectOwnership(node->object, QQmlEngine::CppOwnership);
174 _connectDirtyChanged(node->object);
175 }
176
178 return true;
179}
180
181bool QmlObjectTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/)
182{
183 qCWarning(QmlObjectTreeModelLog) << "insertRows() not supported — use insertItem()";
184 return false;
185}
186
187bool QmlObjectTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/)
188{
189 qCWarning(QmlObjectTreeModelLog) << "removeRows() not supported — use removeItem()";
190 return false;
191}
192
193QHash<int, QByteArray> QmlObjectTreeModel::roleNames() const
194{
196 roles[NodeTypeRole] = "nodeType";
197 roles[SeparatorRole] = "separator";
198 return roles;
199}
200
201//-----------------------------------------------------------------------------
202// Properties
203//-----------------------------------------------------------------------------
204
206{
207 return _totalCount;
208}
209
211{
212 if (_dirty != dirty) {
213 _dirty = dirty;
215 }
216}
217
218//-----------------------------------------------------------------------------
219// QML-accessible tree operations
220//-----------------------------------------------------------------------------
221
222QObject* QmlObjectTreeModel::getObject(const QModelIndex& index) const
223{
224 if (!index.isValid()) {
225 return nullptr;
226 }
227
228 const TreeNode* node = _nodeFromIndex(index);
229 return node ? node->object : nullptr;
230}
231
232QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex)
233{
234 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
235 if (!parentNode) {
236 qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index";
237 return {};
238 }
239
240 return insertItem(parentNode->children.count(), object, parentIndex);
241}
242
243QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex)
244{
245 return insertItem(row, object, parentIndex, QString());
246}
247
248QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType)
249{
250 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
251 if (!parentNode) {
252 qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid parent index";
253 return {};
254 }
255
256 if (row < 0 || row > parentNode->children.count()) {
257 qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid row" << row << "count:" << parentNode->children.count();
258 return {};
259 }
260
261 auto* node = new TreeNode;
262 node->object = object;
263 node->parentNode = parentNode;
264 node->nodeType = nodeType;
265
266 if (object) {
267 QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
268 _connectDirtyChanged(object);
269 }
270
271 if (_resetModelNestingCount > 0) {
272 // During a batch reset we just accumulate nodes silently
273 parentNode->children.insert(row, node);
274 } else {
275 beginInsertRows(parentIndex, row, row);
276 parentNode->children.insert(row, node);
278 _emitSeparatorChanged(parentIndex, row > 0 ? row - 1 : 0);
280 }
281
282 _totalCount++;
283 setDirty(true);
284
285 return createIndex(row, 0, node);
286}
287
288QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType)
289{
290 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
291 if (!parentNode) {
292 qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index";
293 return {};
294 }
295 return insertItem(parentNode->children.count(), object, parentIndex, nodeType);
296}
297
298QObject* QmlObjectTreeModel::removeItem(const QModelIndex& index)
299{
300 if (!index.isValid()) {
301 qCWarning(QmlObjectTreeModelLog) << "removeItem: invalid index";
302 return nullptr;
303 }
304
305 TreeNode* node = _nodeFromIndex(index);
306 if (!node || !node->parentNode) {
307 qCWarning(QmlObjectTreeModelLog) << "removeItem: node not found or is the root";
308 return nullptr;
309 }
310
311 QObject* object = node->object;
312 TreeNode* parentNode = node->parentNode;
313 const int row = node->row();
314 const QModelIndex parentIdx = _indexForNode(parentNode);
315
316 _disconnectSubtree(node);
317
318 // Count the subtree nodes being removed (the node itself + all descendants)
319 const int removedCount = 1 + _subtreeCount(node);
320
321 if (_resetModelNestingCount == 0) {
322 beginRemoveRows(parentIdx, row, row);
323 }
324 parentNode->children.removeAt(row);
325 if (_resetModelNestingCount == 0) {
327 }
328
329 // Free the subtree's TreeNode objects but NOT the QObjects
330 _deleteSubtree(node, false);
331 delete node;
332
333 _totalCount -= removedCount;
334 if (_resetModelNestingCount == 0 && !parentNode->children.isEmpty()) {
335 _emitSeparatorChanged(parentIdx, parentNode->children.count() - 1);
336 }
338 setDirty(true);
339
340 return object;
341}
342
343QModelIndex QmlObjectTreeModel::indexForObject(QObject* object) const
344{
345 if (!object) {
346 return {};
347 }
348
349 const TreeNode* node = _findNode(&_rootNode, object);
350 return node ? _indexForNode(node) : QModelIndex();
351}
352
353int QmlObjectTreeModel::depth(const QModelIndex& index) const
354{
355 if (!index.isValid()) {
356 return -1;
357 }
358
359 int d = 0;
360 const TreeNode* node = _nodeFromIndex(index);
361 while (node && node->parentNode && node->parentNode != &_rootNode) {
362 d++;
363 node = node->parentNode;
364 }
365 return d;
366}
367
368//-----------------------------------------------------------------------------
369// C++ convenience API
370//-----------------------------------------------------------------------------
371
373{
374 appendItem(object);
375}
376
377void QmlObjectTreeModel::appendChild(const QModelIndex& parentIndex, QObject* object)
378{
379 appendItem(object, parentIndex);
380}
381
382QObject* QmlObjectTreeModel::removeAt(const QModelIndex& parentIndex, int row)
383{
384 return removeItem(index(row, 0, parentIndex));
385}
386
387void QmlObjectTreeModel::removeChildren(const QModelIndex& parentIndex)
388{
389 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
390 if (!parentNode || parentNode->children.isEmpty()) {
391 return;
392 }
393
394 const int childCount = parentNode->children.count();
395
396 // Count all nodes being removed (direct children + their subtrees)
398 for (const TreeNode* child : parentNode->children) {
399 removedCount += _subtreeCount(child);
400 }
401
402 if (_resetModelNestingCount == 0) {
404 }
405
406 // Disconnect signals but don't free nodes yet — views may still access them
407 for (TreeNode* child : parentNode->children) {
408 _disconnectSubtree(child);
409 }
410
411 // Detach from parent
412 QList<TreeNode*> orphans = parentNode->children;
413 parentNode->children.clear();
414 _totalCount -= removedCount;
415
416 if (_resetModelNestingCount == 0) {
419 }
420
421 // Now safe to free the TreeNode structs
422 for (TreeNode* child : orphans) {
423 _deleteSubtree(child, false);
424 delete child;
425 }
426}
427
429{
430 if (_rootNode.children.isEmpty()) {
431 return;
432 }
433
435 _disconnectSubtree(&_rootNode);
436 _deleteSubtree(&_rootNode, false);
437 _totalCount = 0;
439}
440
442{
443 if (_rootNode.children.isEmpty()) {
444 return;
445 }
446
448 _disconnectSubtree(&_rootNode);
449 _deleteSubtree(&_rootNode, true);
450 _totalCount = 0;
452}
453
454bool QmlObjectTreeModel::contains(QObject* object) const
455{
456 return _findNode(&_rootNode, object) != nullptr;
457}
458
459//-----------------------------------------------------------------------------
460// Private helpers
461//-----------------------------------------------------------------------------
462
463QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_nodeFromIndex(const QModelIndex& index) const
464{
465 if (!index.isValid()) {
466 return nullptr;
467 }
468 return static_cast<TreeNode*>(index.internalPointer());
469}
470
471QModelIndex QmlObjectTreeModel::_indexForNode(const TreeNode* node) const
472{
473 if (!node || node == &_rootNode) {
474 return {};
475 }
476 return createIndex(node->row(), 0, const_cast<TreeNode*>(node));
477}
478
479QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_findNode(const TreeNode* root, const QObject* object) const
480{
481 for (TreeNode* child : root->children) {
482 if (child->object == object) {
483 return child;
484 }
485 TreeNode* found = _findNode(child, object);
486 if (found) {
487 return found;
488 }
489 }
490 return nullptr;
491}
492
493int QmlObjectTreeModel::_subtreeCount(const TreeNode* node)
494{
495 int result = node->children.count();
496 for (const TreeNode* child : node->children) {
497 result += _subtreeCount(child);
498 }
499 return result;
500}
501
502void QmlObjectTreeModel::_disconnectSubtree(TreeNode* node)
503{
504 if (node != &_rootNode && node->object) {
505 _disconnectDirtyChanged(node->object);
506 }
507 for (TreeNode* child : node->children) {
508 _disconnectSubtree(child);
509 }
510}
511
512void QmlObjectTreeModel::_deleteSubtree(TreeNode* node, bool deleteObjects)
513{
514 for (TreeNode* child : node->children) {
515 _deleteSubtree(child, deleteObjects);
516 if (deleteObjects && child->object) {
517 child->object->deleteLater();
518 }
519 delete child;
520 }
521 node->children.clear();
522}
523
524void QmlObjectTreeModel::_connectDirtyChanged(QObject* object)
525{
526 const QMetaMethod signal = dirtyChangedSignal(object);
528 if (signal.isValid() && slot.isValid()) {
529 connect(object, signal, this, slot);
530 }
531}
532
533void QmlObjectTreeModel::_disconnectDirtyChanged(QObject* object)
534{
535 const QMetaMethod signal = dirtyChangedSignal(object);
537 if (signal.isValid() && slot.isValid()) {
538 disconnect(object, signal, this, slot);
539 }
540}
541
542void QmlObjectTreeModel::_emitSeparatorChanged(const QModelIndex& parentIdx, int fromRow)
543{
544 const TreeNode* parentNode = parentIdx.isValid() ? _nodeFromIndex(parentIdx) : &_rootNode;
545 if (!parentNode) {
546 return;
547 }
548 const int last = parentNode->children.count() - 1;
549 if (fromRow > last) {
550 fromRow = last;
551 }
552 if (fromRow < 0) {
553 return;
554 }
556 const QModelIndex end = index(last, 0, parentIdx);
558}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
Common base for QObject*-based item models (flat lists and trees).
void endResetModel()
Depth-counted endResetModel — only the outermost call has effect.
QHash< int, QByteArray > roleNames() const override
void beginResetModel()
Depth-counted beginResetModel — only the outermost call has effect.
static constexpr int ObjectRole
static constexpr int TextRole
void dirtyChanged(bool dirty)
void appendChild(const QModelIndex &parentIndex, QObject *object)
void setDirty(bool dirty) override
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Q_INVOKABLE int childCount(const QModelIndex &parentIndex=QModelIndex()) const
Number of direct children under parentIndex.
static constexpr int NodeTypeRole
QVariant data(const QModelIndex &index, int role=Qt::DisplayRole) const override
int count() const override
void removeChildren(const QModelIndex &parentIndex)
Removes all children of parentIndex without removing the parent itself.
int columnCount(const QModelIndex &parent=QModelIndex()) const override
Q_INVOKABLE QModelIndex parentIndex(const QModelIndex &index) const
Convenience wrapper around parent()
Q_INVOKABLE QObject * removeItem(const QModelIndex &index)
QObject * removeAt(const QModelIndex &parentIndex, int row)
QModelIndex index(int row, int column, const QModelIndex &parent=QModelIndex()) const override
void appendRootItem(QObject *object)
bool insertRows(int row, int count, const QModelIndex &parent=QModelIndex()) override
Q_INVOKABLE QModelIndex indexForObject(QObject *object) const
Searches the entire tree for object and returns its QModelIndex (invalid if not found)
void clearAndDeleteContents()
Clears the tree and calls deleteLater on every QObject.
Q_INVOKABLE QModelIndex insertItem(int row, QObject *object, const QModelIndex &parentIndex=QModelIndex())
Inserts object at row under parentIndex. Returns the new item's index.
Q_INVOKABLE int depth(const QModelIndex &index) const
Returns the depth of index (0 = root-level item, -1 = invalid index)
Q_INVOKABLE QModelIndex appendItem(QObject *object, const QModelIndex &parentIndex=QModelIndex())
Appends object as the last child of parentIndex (root if invalid). Returns the new item's index.
bool setData(const QModelIndex &index, const QVariant &value, int role=Qt::EditRole) override
bool contains(QObject *object) const
QHash< int, QByteArray > roleNames() const override
bool hasChildren(const QModelIndex &parent=QModelIndex()) const override
Q_INVOKABLE QObject * getObject(const QModelIndex &index) const
Returns the QObject* stored at index, or nullptr if invalid.
QmlObjectTreeModel(QObject *parent=nullptr)
static constexpr int SeparatorRole
bool removeRows(int row, int count, const QModelIndex &parent=QModelIndex()) override
QModelIndex parent(const QModelIndex &child) const override