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 default:
141 return {};
142 }
143}
144
145bool QmlObjectTreeModel::setData(const QModelIndex& index, const QVariant& value, int role)
146{
147 if (!index.isValid() || role != ObjectRole) {
148 return false;
149 }
150
151 TreeNode* node = _nodeFromIndex(index);
152 if (!node) {
153 return false;
154 }
155
156 if (node->object) {
157 _disconnectDirtyChanged(node->object);
158 }
159
160 node->object = value.value<QObject*>();
161
162 if (node->object) {
163 QQmlEngine::setObjectOwnership(node->object, QQmlEngine::CppOwnership);
164 _connectDirtyChanged(node->object);
165 }
166
168 return true;
169}
170
171bool QmlObjectTreeModel::insertRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/)
172{
173 qCWarning(QmlObjectTreeModelLog) << "insertRows() not supported — use insertItem()";
174 return false;
175}
176
177bool QmlObjectTreeModel::removeRows(int /*row*/, int /*count*/, const QModelIndex& /*parent*/)
178{
179 qCWarning(QmlObjectTreeModelLog) << "removeRows() not supported — use removeItem()";
180 return false;
181}
182
183QHash<int, QByteArray> QmlObjectTreeModel::roleNames() const
184{
186 roles[NodeTypeRole] = "nodeType";
187 return roles;
188}
189
190//-----------------------------------------------------------------------------
191// Properties
192//-----------------------------------------------------------------------------
193
195{
196 return _totalCount;
197}
198
200{
201 if (_dirty != dirty) {
202 _dirty = dirty;
204 }
205}
206
207//-----------------------------------------------------------------------------
208// QML-accessible tree operations
209//-----------------------------------------------------------------------------
210
211QObject* QmlObjectTreeModel::getObject(const QModelIndex& index) const
212{
213 if (!index.isValid()) {
214 return nullptr;
215 }
216
217 const TreeNode* node = _nodeFromIndex(index);
218 return node ? node->object : nullptr;
219}
220
221QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex)
222{
223 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
224 if (!parentNode) {
225 qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index";
226 return {};
227 }
228
229 return insertItem(parentNode->children.count(), object, parentIndex);
230}
231
232QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex)
233{
234 return insertItem(row, object, parentIndex, QString());
235}
236
237QModelIndex QmlObjectTreeModel::insertItem(int row, QObject* object, const QModelIndex& parentIndex, const QString& nodeType)
238{
239 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
240 if (!parentNode) {
241 qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid parent index";
242 return {};
243 }
244
245 if (row < 0 || row > parentNode->children.count()) {
246 qCWarning(QmlObjectTreeModelLog) << "insertItem: invalid row" << row << "count:" << parentNode->children.count();
247 return {};
248 }
249
250 auto* node = new TreeNode;
251 node->object = object;
252 node->parentNode = parentNode;
253 node->nodeType = nodeType;
254
255 if (object) {
256 QQmlEngine::setObjectOwnership(object, QQmlEngine::CppOwnership);
257 _connectDirtyChanged(object);
258 }
259
260 if (_resetModelNestingCount > 0) {
261 // During a batch reset we just accumulate nodes silently
262 parentNode->children.insert(row, node);
263 } else {
264 beginInsertRows(parentIndex, row, row);
265 parentNode->children.insert(row, node);
268 }
269
270 _totalCount++;
271 setDirty(true);
272
273 return createIndex(row, 0, node);
274}
275
276QModelIndex QmlObjectTreeModel::appendItem(QObject* object, const QModelIndex& parentIndex, const QString& nodeType)
277{
278 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
279 if (!parentNode) {
280 qCWarning(QmlObjectTreeModelLog) << "appendItem: invalid parent index";
281 return {};
282 }
283 return insertItem(parentNode->children.count(), object, parentIndex, nodeType);
284}
285
286QObject* QmlObjectTreeModel::removeItem(const QModelIndex& index)
287{
288 if (!index.isValid()) {
289 qCWarning(QmlObjectTreeModelLog) << "removeItem: invalid index";
290 return nullptr;
291 }
292
293 TreeNode* node = _nodeFromIndex(index);
294 if (!node || !node->parentNode) {
295 qCWarning(QmlObjectTreeModelLog) << "removeItem: node not found or is the root";
296 return nullptr;
297 }
298
299 QObject* object = node->object;
300 TreeNode* parentNode = node->parentNode;
301 const int row = node->row();
302 const QModelIndex parentIdx = _indexForNode(parentNode);
303
304 _disconnectSubtree(node);
305
306 // Count the subtree nodes being removed (the node itself + all descendants)
307 const int removedCount = 1 + _subtreeCount(node);
308
309 beginRemoveRows(parentIdx, row, row);
310 parentNode->children.removeAt(row);
312
313 // Free the subtree's TreeNode objects but NOT the QObjects
314 _deleteSubtree(node, false);
315 delete node;
316
317 _totalCount -= removedCount;
319 setDirty(true);
320
321 return object;
322}
323
324QModelIndex QmlObjectTreeModel::indexForObject(QObject* object) const
325{
326 if (!object) {
327 return {};
328 }
329
330 const TreeNode* node = _findNode(&_rootNode, object);
331 return node ? _indexForNode(node) : QModelIndex();
332}
333
334int QmlObjectTreeModel::depth(const QModelIndex& index) const
335{
336 if (!index.isValid()) {
337 return -1;
338 }
339
340 int d = 0;
341 const TreeNode* node = _nodeFromIndex(index);
342 while (node && node->parentNode && node->parentNode != &_rootNode) {
343 d++;
344 node = node->parentNode;
345 }
346 return d;
347}
348
349//-----------------------------------------------------------------------------
350// C++ convenience API
351//-----------------------------------------------------------------------------
352
354{
355 appendItem(object);
356}
357
358void QmlObjectTreeModel::appendChild(const QModelIndex& parentIndex, QObject* object)
359{
360 appendItem(object, parentIndex);
361}
362
363QObject* QmlObjectTreeModel::removeAt(const QModelIndex& parentIndex, int row)
364{
365 return removeItem(index(row, 0, parentIndex));
366}
367
368void QmlObjectTreeModel::removeChildren(const QModelIndex& parentIndex)
369{
370 TreeNode* parentNode = parentIndex.isValid() ? _nodeFromIndex(parentIndex) : &_rootNode;
371 if (!parentNode || parentNode->children.isEmpty()) {
372 return;
373 }
374
375 const int childCount = parentNode->children.count();
376
377 // Count all nodes being removed (direct children + their subtrees)
379 for (const TreeNode* child : parentNode->children) {
380 removedCount += _subtreeCount(child);
381 }
382
383 if (_resetModelNestingCount == 0) {
385 }
386
387 for (TreeNode* child : parentNode->children) {
388 _disconnectSubtree(child);
389 _deleteSubtree(child, false);
390 delete child;
391 }
392 parentNode->children.clear();
393
394 _totalCount -= removedCount;
395
396 if (_resetModelNestingCount == 0) {
399 }
400}
401
403{
404 if (_rootNode.children.isEmpty()) {
405 return;
406 }
407
409 _disconnectSubtree(&_rootNode);
410 _deleteSubtree(&_rootNode, false);
411 _totalCount = 0;
413}
414
416{
417 if (_rootNode.children.isEmpty()) {
418 return;
419 }
420
422 _disconnectSubtree(&_rootNode);
423 _deleteSubtree(&_rootNode, true);
424 _totalCount = 0;
426}
427
428bool QmlObjectTreeModel::contains(QObject* object) const
429{
430 return _findNode(&_rootNode, object) != nullptr;
431}
432
433//-----------------------------------------------------------------------------
434// Private helpers
435//-----------------------------------------------------------------------------
436
437QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_nodeFromIndex(const QModelIndex& index) const
438{
439 if (!index.isValid()) {
440 return nullptr;
441 }
442 return static_cast<TreeNode*>(index.internalPointer());
443}
444
445QModelIndex QmlObjectTreeModel::_indexForNode(const TreeNode* node) const
446{
447 if (!node || node == &_rootNode) {
448 return {};
449 }
450 return createIndex(node->row(), 0, const_cast<TreeNode*>(node));
451}
452
453QmlObjectTreeModel::TreeNode* QmlObjectTreeModel::_findNode(const TreeNode* root, const QObject* object) const
454{
455 for (TreeNode* child : root->children) {
456 if (child->object == object) {
457 return child;
458 }
459 TreeNode* found = _findNode(child, object);
460 if (found) {
461 return found;
462 }
463 }
464 return nullptr;
465}
466
467int QmlObjectTreeModel::_subtreeCount(const TreeNode* node)
468{
469 int result = node->children.count();
470 for (const TreeNode* child : node->children) {
471 result += _subtreeCount(child);
472 }
473 return result;
474}
475
476void QmlObjectTreeModel::_disconnectSubtree(TreeNode* node)
477{
478 if (node != &_rootNode && node->object) {
479 _disconnectDirtyChanged(node->object);
480 }
481 for (TreeNode* child : node->children) {
482 _disconnectSubtree(child);
483 }
484}
485
486void QmlObjectTreeModel::_deleteSubtree(TreeNode* node, bool deleteObjects)
487{
488 for (TreeNode* child : node->children) {
489 _deleteSubtree(child, deleteObjects);
490 if (deleteObjects && child->object) {
491 child->object->deleteLater();
492 }
493 delete child;
494 }
495 node->children.clear();
496}
497
498void QmlObjectTreeModel::_connectDirtyChanged(QObject* object)
499{
500 const QMetaMethod signal = dirtyChangedSignal(object);
502 if (signal.isValid() && slot.isValid()) {
503 connect(object, signal, this, slot);
504 }
505}
506
507void QmlObjectTreeModel::_disconnectDirtyChanged(QObject* object)
508{
509 const QMetaMethod signal = dirtyChangedSignal(object);
511 if (signal.isValid() && slot.isValid()) {
512 disconnect(object, signal, this, slot);
513 }
514}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
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
QModelIndex parentIndex(const QModelIndex &index) const
Convenience wrapper around parent()
int rowCount(const QModelIndex &parent=QModelIndex()) const override
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
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
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.
QModelIndex insertItem(int row, QObject *object, const QModelIndex &parentIndex=QModelIndex())
Inserts object at row under parentIndex. Returns the new item's index.
int depth(const QModelIndex &index) const
Returns the depth of index (0 = root-level item, -1 = invalid index)
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
QObject * getObject(const QModelIndex &index) const
Returns the QObject* stored at index, or nullptr if invalid.
QmlObjectTreeModel(QObject *parent=nullptr)
bool removeRows(int row, int count, const QModelIndex &parent=QModelIndex()) override
QModelIndex parent(const QModelIndex &child) const override
int childCount(const QModelIndex &parentIndex=QModelIndex()) const
Number of direct children under parentIndex.