1 | // Copyright (C) 2021 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qquicktreeview_p_p.h" |
5 | |
6 | #include <QtCore/qobject.h> |
7 | #include <QtQml/qqmlcontext.h> |
8 | #include <QtQuick/private/qquicktaphandler_p.h> |
9 | |
10 | #include <QtQmlModels/private/qqmltreemodeltotablemodel_p_p.h> |
11 | |
12 | /*! |
13 | \qmltype TreeView |
14 | \inqmlmodule QtQuick |
15 | \ingroup qtquick-views |
16 | \since 6.3 |
17 | \inherits TableView |
18 | \brief Provides a tree view to display data from a QAbstractItemModel. |
19 | |
20 | A TreeView has a \l model that defines the data to be displayed, and a |
21 | \l delegate that defines how the data should be displayed. |
22 | |
23 | TreeView inherits \l TableView. This means that even if the model |
24 | has a parent-child tree structure, TreeView is internally using a |
25 | proxy model that converts that structure into a flat table |
26 | model that can be rendered by TableView. Each node in the tree ends up |
27 | occupying one row in the table, where the first column renders the tree |
28 | itself. By indenting each delegate item in that column according to its |
29 | parent-child depth in the model, it will end up looking like a tree, even |
30 | if it's technically still just a flat list of items. |
31 | |
32 | To allow for maximum flexibility, TreeView itself will not position the delegate |
33 | items into a tree structure. This burden is placed on the delegate. |
34 | \l {Qt Quick Controls} offers a ready-made TreeViewDelegate that can be |
35 | used for this, which has the advantage that it works out-of-the-box and |
36 | renders a tree which follows the style of the platform where the application runs. |
37 | |
38 | Even if TreeViewDelegate is customizable, there might be situations |
39 | where you want to render the tree in a different way, or ensure that |
40 | the delegate ends up as minimal as possible, perhaps for performance reasons. |
41 | Creating your own delegate from scratch is easy, since TreeView offers |
42 | a set of properties that can be used to position and render each node |
43 | in the tree correctly. |
44 | |
45 | An example of a custom delegate is shown below: |
46 | |
47 | \snippet qml/treeview/qml-customdelegate.qml 0 |
48 | |
49 | The properties that are marked as \c required will be filled in by |
50 | TreeView, and are similar to attached properties. By marking them as |
51 | required, the delegate indirectly informs TreeView that it should take |
52 | responsibility for assigning them values. The following required properties |
53 | can be added to a delegate: |
54 | |
55 | \list |
56 | \li \c {required property TreeView treeView} |
57 | - Points to the TreeView that contains the delegate item. |
58 | \li \c {required property bool isTreeNode} |
59 | - Is \c true if the delegate item represents a node in |
60 | the tree. Only one column in the view will be used to draw the tree, and |
61 | therefore, only delegate items in that column will have this |
62 | property set to \c true. |
63 | A node in the tree should typically be indented according to its |
64 | \c depth, and show an indicator if \c hasChildren is \c true. |
65 | Delegate items in other columns will have this property set to |
66 | \c false, and will show data from the remaining columns |
67 | in the model (and typically not be indented). |
68 | \li \c {required property bool expanded} |
69 | - Is \c true if the model item drawn by the delegate is expanded |
70 | in the view. |
71 | \li \c {required property bool hasChildren} |
72 | - Is \c true if the model item drawn by the delegate has children |
73 | in the model. |
74 | \li \c {required property int depth} |
75 | - Contains the depth of the model item drawn by the delegate. |
76 | The depth of a model item is the same as the number of ancestors |
77 | it has in the model. |
78 | \endlist |
79 | |
80 | See also \l {Required Properties}. |
81 | |
82 | By default, TreeView \l {toggleExpanded()}{toggles} the expanded state |
83 | of a row when you double tap on it. Since this is in conflict with |
84 | double tapping to edit a cell, TreeView sets \l {TableView::}{editTriggers} to |
85 | \c TableView.EditKeyPressed by default (which is different from TableView, |
86 | which uses \c {TableView.EditKeyPressed | TableView.DoubleTapped}. |
87 | If you change \l {TableView::}{editTriggers} to also contain \c TableView.DoubleTapped, |
88 | toggling the expanded state with a double tap will be disabled. |
89 | |
90 | \note A TreeView only accepts a model that inherits \l QAbstractItemModel. |
91 | */ |
92 | |
93 | /*! |
94 | \qmlproperty QModelIndex QtQuick::TreeView::rootIndex |
95 | \since 6.6 |
96 | |
97 | This property holds the model index of the root item in the tree. |
98 | By default, this is the same as the root index in the model, but you can |
99 | set it to be a child index instead, to show only a branch of the tree. |
100 | Set it to \c undefined to show the whole model. |
101 | */ |
102 | |
103 | /*! |
104 | \qmlmethod int QtQuick::TreeView::depth(row) |
105 | |
106 | Returns the depth (the number of parents up to the root) of the given \a row. |
107 | |
108 | \a row should be the row in the view (table row), and not a row in the model. |
109 | If \a row is not between \c 0 and \l {TableView::}{rows}, the return value will |
110 | be \c -1. |
111 | |
112 | \sa {TableView::}{modelIndex()} |
113 | */ |
114 | |
115 | /*! |
116 | \qmlmethod bool QtQuick::TreeView::isExpanded(row) |
117 | |
118 | Returns if the given \a row in the view is shown as expanded. |
119 | |
120 | \a row should be the row in the view (table row), and not a row in the model. |
121 | If \a row is not between \c 0 and \l {TableView::}{rows}, the return value will |
122 | be \c false. |
123 | */ |
124 | |
125 | /*! |
126 | \qmlmethod QtQuick::TreeView::expand(row) |
127 | |
128 | Expands the tree node at the given \a row in the view. |
129 | |
130 | \a row should be the row in the view (table row), and not a row in the model. |
131 | |
132 | \note this function will not affect the model, only |
133 | the visual representation in the view. |
134 | |
135 | \sa collapse(), isExpanded(), expandRecursively() |
136 | */ |
137 | |
138 | /*! |
139 | \qmlmethod QtQuick::TreeView::expandRecursively(row = -1, depth = -1) |
140 | \since 6.4 |
141 | |
142 | Expands the tree node at the given \a row in the view recursively down to |
143 | \a depth. \a depth should be relative to the depth of \a row. If |
144 | \a depth is \c -1, the tree will be expanded all the way down to all leaves. |
145 | |
146 | For a model that has more than one root, you can also call this function |
147 | with \a row equal to \c -1. This will expand all roots. Hence, calling |
148 | expandRecursively(-1, -1), or simply expandRecursively(), will expand |
149 | all nodes in the model. |
150 | |
151 | \a row should be the row in the view (table row), and not a row in the model. |
152 | |
153 | \note This function will not try to \l{QAbstractItemModel::fetchMore}{fetch more} data. |
154 | \note This function will not affect the model, only the visual representation in the view. |
155 | \warning If the model contains a large number of items, this function will |
156 | take some time to execute. |
157 | |
158 | \sa collapseRecursively(), expand(), collapse(), isExpanded(), depth() |
159 | */ |
160 | |
161 | /*! |
162 | \qmlmethod QtQuick::TreeView::expandToIndex(QModelIndex index) |
163 | \since 6.4 |
164 | |
165 | Expands the tree from the given model \a index, and recursively all the way up |
166 | to the root. The result will be that the delegate item that represents \a index |
167 | becomes visible in the view (unless it ends up outside the viewport). To |
168 | ensure that the row ends up visible in the viewport, you can do: |
169 | |
170 | \code |
171 | expandToIndex(index) |
172 | forceLayout() |
173 | positionViewAtRow(rowAtIndex(index), Qt.AlignVCenter) |
174 | \endcode |
175 | |
176 | \sa expand(), expandRecursively() |
177 | */ |
178 | |
179 | /*! |
180 | \qmlmethod QtQuick::TreeView::collapse(row) |
181 | |
182 | Collapses the tree node at the given \a row in the view. |
183 | |
184 | \a row should be the row in the view (table row), and not a row in the model. |
185 | |
186 | \note this function will not affect the model, only |
187 | the visual representation in the view. |
188 | |
189 | \sa expand(), isExpanded() |
190 | */ |
191 | |
192 | /*! |
193 | \qmlmethod QtQuick::TreeView::collapseRecursively(row = -1) |
194 | \since 6.4 |
195 | |
196 | Collapses the tree node at the given \a row in the view recursively down to |
197 | all leaves. |
198 | |
199 | For a model has more than one root, you can also call this function |
200 | with \a row equal to \c -1. This will collapse all roots. Hence, calling |
201 | collapseRecursively(-1), or simply collapseRecursively(), will collapse |
202 | all nodes in the model. |
203 | |
204 | \a row should be the row in the view (table row), and not a row in the model. |
205 | |
206 | \note this function will not affect the model, only |
207 | the visual representation in the view. |
208 | |
209 | \sa expandRecursively(), expand(), collapse(), isExpanded(), depth() |
210 | */ |
211 | |
212 | /*! |
213 | \qmlmethod QtQuick::TreeView::toggleExpanded(row) |
214 | |
215 | Toggles if the tree node at the given \a row should be expanded. |
216 | This is a convenience for doing: |
217 | |
218 | \code |
219 | if (isExpanded(row)) |
220 | collapse(row) |
221 | else |
222 | expand(row) |
223 | \endcode |
224 | |
225 | \a row should be the row in the view (table row), and not a row in the model. |
226 | */ |
227 | |
228 | /*! |
229 | \qmlsignal QtQuick::TreeView::expanded(row, depth) |
230 | |
231 | This signal is emitted when a \a row is expanded in the view. |
232 | \a row and \a depth will be equal to the arguments given to the call |
233 | that caused the expansion to happen (\l expand() or \l expandRecursively()). |
234 | In case of \l expand(), \a depth will always be \c 1. |
235 | In case of \l expandToIndex(), \a depth will be the depth of the |
236 | target index. |
237 | |
238 | \note when a row is expanded recursively, the expanded signal will |
239 | only be emitted for that one row, and not for its descendants. |
240 | |
241 | \sa collapsed(), expand(), collapse(), toggleExpanded() |
242 | */ |
243 | |
244 | /*! |
245 | \qmlsignal QtQuick::TreeView::collapsed(row, recursively) |
246 | |
247 | This signal is emitted when a \a row is collapsed in the view. |
248 | \a row will be equal to the argument given to the call that caused |
249 | the collapse to happen (\l collapse() or \l collapseRecursively()). |
250 | If the row was collapsed recursively, \a recursively will be \c true. |
251 | |
252 | \note when a row is collapsed recursively, the collapsed signal will |
253 | only be emitted for that one row, and not for its descendants. |
254 | |
255 | \sa expanded(), expand(), collapse(), toggleExpanded() |
256 | */ |
257 | |
258 | // Hard-code the tree column to be 0 for now |
259 | static const int kTreeColumn = 0; |
260 | |
261 | QT_BEGIN_NAMESPACE |
262 | |
263 | QQuickTreeViewPrivate::QQuickTreeViewPrivate() |
264 | : QQuickTableViewPrivate() |
265 | { |
266 | } |
267 | |
268 | QQuickTreeViewPrivate::~QQuickTreeViewPrivate() |
269 | { |
270 | } |
271 | |
272 | QVariant QQuickTreeViewPrivate::modelImpl() const |
273 | { |
274 | return m_assignedModel; |
275 | } |
276 | |
277 | void QQuickTreeViewPrivate::setModelImpl(const QVariant &newModel) |
278 | { |
279 | Q_Q(QQuickTreeView); |
280 | |
281 | if (newModel == m_assignedModel) |
282 | return; |
283 | |
284 | m_assignedModel = newModel; |
285 | QVariant effectiveModel = m_assignedModel; |
286 | if (effectiveModel.userType() == qMetaTypeId<QJSValue>()) |
287 | effectiveModel = effectiveModel.value<QJSValue>().toVariant(); |
288 | |
289 | if (effectiveModel.isNull()) |
290 | m_treeModelToTableModel.setModel(nullptr); |
291 | else if (const auto qaim = qvariant_cast<QAbstractItemModel*>(v: effectiveModel)) |
292 | m_treeModelToTableModel.setModel(qaim); |
293 | else |
294 | qmlWarning(me: q) << "TreeView only accepts a model of type QAbstractItemModel" ; |
295 | |
296 | |
297 | scheduleRebuildTable(options: QQuickTableViewPrivate::RebuildOption::All); |
298 | emit q->modelChanged(); |
299 | } |
300 | |
301 | void QQuickTreeViewPrivate::initItemCallback(int serializedModelIndex, QObject *object) |
302 | { |
303 | updateRequiredProperties(serializedModelIndex, object, init: true); |
304 | QQuickTableViewPrivate::initItemCallback(modelIndex: serializedModelIndex, item: object); |
305 | } |
306 | |
307 | void QQuickTreeViewPrivate::itemReusedCallback(int serializedModelIndex, QObject *object) |
308 | { |
309 | updateRequiredProperties(serializedModelIndex, object, init: false); |
310 | QQuickTableViewPrivate::itemReusedCallback(modelIndex: serializedModelIndex, object); |
311 | } |
312 | |
313 | void QQuickTreeViewPrivate::dataChangedCallback( |
314 | const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) |
315 | { |
316 | Q_Q(QQuickTreeView); |
317 | Q_UNUSED(roles); |
318 | |
319 | for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { |
320 | for (int column = topLeft.column(); column <= bottomRight.column(); ++column) { |
321 | const QPoint cell(column, row); |
322 | auto item = q->itemAtCell(cell); |
323 | if (!item) |
324 | continue; |
325 | |
326 | const int serializedModelIndex = modelIndexAtCell(cell: QPoint(column, row)); |
327 | updateRequiredProperties(serializedModelIndex, object: item, init: false); |
328 | } |
329 | } |
330 | } |
331 | |
332 | void QQuickTreeViewPrivate::updateRequiredProperties(int serializedModelIndex, QObject *object, bool init) |
333 | { |
334 | Q_Q(QQuickTreeView); |
335 | const QPoint cell = cellAtModelIndex(modelIndex: serializedModelIndex); |
336 | const int row = cell.y(); |
337 | const int column = cell.x(); |
338 | |
339 | setRequiredProperty(property: "treeView" , value: QVariant::fromValue(value: q), serializedModelIndex, object, init); |
340 | setRequiredProperty(property: "isTreeNode" , value: column == kTreeColumn, serializedModelIndex, object, init); |
341 | setRequiredProperty(property: "hasChildren" , value: m_treeModelToTableModel.hasChildren(row), serializedModelIndex, object, init); |
342 | setRequiredProperty(property: "expanded" , value: q->isExpanded(row), serializedModelIndex, object, init); |
343 | setRequiredProperty(property: "depth" , value: m_treeModelToTableModel.depthAtRow(row), serializedModelIndex, object, init); |
344 | } |
345 | |
346 | void QQuickTreeViewPrivate::updateSelection(const QRect &oldSelection, const QRect &newSelection) |
347 | { |
348 | Q_Q(QQuickTreeView); |
349 | |
350 | const QRect oldRect = oldSelection.normalized(); |
351 | const QRect newRect = newSelection.normalized(); |
352 | |
353 | if (oldSelection == newSelection) |
354 | return; |
355 | |
356 | // Select the rows inside newRect that doesn't overlap with oldRect |
357 | for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) { |
358 | if (oldRect.y() != -1 && oldRect.y() <= row && row <= oldRect.y() + oldRect.height()) |
359 | continue; |
360 | const QModelIndex startIndex = q->index(row, column: newRect.x()); |
361 | const QModelIndex endIndex = q->index(row, column: newRect.x() + newRect.width()); |
362 | selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Select); |
363 | } |
364 | |
365 | if (oldRect.x() != -1) { |
366 | // Since oldRect is valid, this update is a continuation of an already existing selection! |
367 | |
368 | // Select the columns inside newRect that don't overlap with oldRect |
369 | for (int column = newRect.x(); column <= newRect.x() + newRect.width(); ++column) { |
370 | if (oldRect.x() <= column && column <= oldRect.x() + oldRect.width()) |
371 | continue; |
372 | for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) |
373 | selectionModel->select(index: q->index(row, column), command: QItemSelectionModel::Select); |
374 | } |
375 | |
376 | // Unselect the rows inside oldRect that don't overlap with newRect |
377 | for (int row = oldRect.y(); row <= oldRect.y() + oldRect.height(); ++row) { |
378 | if (newRect.y() <= row && row <= newRect.y() + newRect.height()) |
379 | continue; |
380 | const QModelIndex startIndex = q->index(row, column: oldRect.x()); |
381 | const QModelIndex endIndex = q->index(row, column: oldRect.x() + oldRect.width()); |
382 | selectionModel->select(selection: QItemSelection(startIndex, endIndex), command: QItemSelectionModel::Deselect); |
383 | } |
384 | |
385 | // Unselect the columns inside oldRect that don't overlap with newRect |
386 | for (int column = oldRect.x(); column <= oldRect.x() + oldRect.width(); ++column) { |
387 | if (newRect.x() <= column && column <= newRect.x() + newRect.width()) |
388 | continue; |
389 | // Since we're not allowed to call select/unselect on the selectionModel with |
390 | // indices from different parents, and since indicies from different parents are |
391 | // expected when working with trees, we need to unselect the indices in the column |
392 | // one by one, rather than the whole column in one go. This, however, can cause a |
393 | // lot of selection fragments in the selectionModel, which eventually can hurt |
394 | // performance. But large selections containing a lot of columns is not normally |
395 | // the case for a treeview, so accept this potential corner case for now. |
396 | for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) |
397 | selectionModel->select(index: q->index(row, column), command: QItemSelectionModel::Deselect); |
398 | } |
399 | } |
400 | } |
401 | |
402 | QQuickTreeView::QQuickTreeView(QQuickItem *parent) |
403 | : QQuickTableView(*(new QQuickTreeViewPrivate), parent) |
404 | { |
405 | Q_D(QQuickTreeView); |
406 | |
407 | setSelectionBehavior(SelectRows); |
408 | setEditTriggers(EditKeyPressed); |
409 | |
410 | // Note: QQuickTableView will only ever see the table model m_treeModelToTableModel, and |
411 | // never the actual tree model that is assigned to us by the application. |
412 | const auto modelAsVariant = QVariant::fromValue(value: std::addressof(r&: d->m_treeModelToTableModel)); |
413 | d->QQuickTableViewPrivate::setModelImpl(modelAsVariant); |
414 | QObjectPrivate::connect(sender: &d->m_treeModelToTableModel, signal: &QAbstractItemModel::dataChanged, |
415 | receiverPrivate: d, slot: &QQuickTreeViewPrivate::dataChangedCallback); |
416 | QObject::connect(sender: &d->m_treeModelToTableModel, signal: &QQmlTreeModelToTableModel::rootIndexChanged, |
417 | context: this, slot: &QQuickTreeView::rootIndexChanged); |
418 | |
419 | auto tapHandler = new QQuickTapHandler(this); |
420 | tapHandler->setAcceptedModifiers(Qt::NoModifier); |
421 | connect(sender: tapHandler, signal: &QQuickTapHandler::doubleTapped, slot: [this, tapHandler]{ |
422 | if (!pointerNavigationEnabled()) |
423 | return; |
424 | if (editTriggers() & DoubleTapped) |
425 | return; |
426 | |
427 | const int row = cellAtPosition(position: tapHandler->point().pressPosition()).y(); |
428 | toggleExpanded(row); |
429 | }); |
430 | } |
431 | |
432 | QQuickTreeView::~QQuickTreeView() |
433 | { |
434 | } |
435 | |
436 | QModelIndex QQuickTreeView::rootIndex() const |
437 | { |
438 | return d_func()->m_treeModelToTableModel.rootIndex(); |
439 | } |
440 | |
441 | void QQuickTreeView::setRootIndex(const QModelIndex &index) |
442 | { |
443 | Q_D(QQuickTreeView); |
444 | d->m_treeModelToTableModel.setRootIndex(index); |
445 | positionViewAtCell(cell: {0, 0}, mode: QQuickTableView::AlignTop | QQuickTableView::AlignLeft); |
446 | } |
447 | |
448 | void QQuickTreeView::resetRootIndex() |
449 | { |
450 | Q_D(QQuickTreeView); |
451 | d->m_treeModelToTableModel.resetRootIndex(); |
452 | positionViewAtCell(cell: {0, 0}, mode: QQuickTableView::AlignTop | QQuickTableView::AlignLeft); |
453 | } |
454 | |
455 | int QQuickTreeView::depth(int row) const |
456 | { |
457 | Q_D(const QQuickTreeView); |
458 | if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) |
459 | return -1; |
460 | |
461 | return d->m_treeModelToTableModel.depthAtRow(row); |
462 | } |
463 | |
464 | bool QQuickTreeView::isExpanded(int row) const |
465 | { |
466 | Q_D(const QQuickTreeView); |
467 | if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) |
468 | return false; |
469 | |
470 | return d->m_treeModelToTableModel.isExpanded(row); |
471 | } |
472 | |
473 | void QQuickTreeView::expand(int row) |
474 | { |
475 | if (row >= 0) |
476 | expandRecursively(row, depth: 1); |
477 | } |
478 | |
479 | void QQuickTreeView::expandRecursively(int row, int depth) |
480 | { |
481 | Q_D(QQuickTreeView); |
482 | if (row >= d->m_treeModelToTableModel.rowCount()) |
483 | return; |
484 | if (row < 0 && row != -1) |
485 | return; |
486 | if (depth == 0 || depth < -1) |
487 | return; |
488 | |
489 | auto expandRowRecursively = [this, d, depth](int startRow) { |
490 | d->m_treeModelToTableModel.expandRecursively(row: startRow, depth); |
491 | // Update the expanded state of the startRow. The descendant rows that gets |
492 | // expanded will get the correct state set from initItem/itemReused instead. |
493 | for (int c = leftColumn(); c <= rightColumn(); ++c) { |
494 | const QPoint treeNodeCell(c, startRow); |
495 | if (const auto item = itemAtCell(cell: treeNodeCell)) |
496 | d->setRequiredProperty(property: "expanded" , value: true, serializedModelIndex: d->modelIndexAtCell(cell: treeNodeCell), object: item, init: false); |
497 | } |
498 | }; |
499 | |
500 | if (row >= 0) { |
501 | // Expand only one row recursively |
502 | const bool isExpanded = d->m_treeModelToTableModel.isExpanded(row); |
503 | if (isExpanded && depth == 1) |
504 | return; |
505 | expandRowRecursively(row); |
506 | } else { |
507 | // Expand all root nodes recursively |
508 | const auto model = d->m_treeModelToTableModel.model(); |
509 | for (int r = 0; r < model->rowCount(); ++r) { |
510 | const int rootRow = d->m_treeModelToTableModel.itemIndex(index: model->index(row: r, column: 0)); |
511 | if (rootRow != -1) |
512 | expandRowRecursively(rootRow); |
513 | } |
514 | } |
515 | |
516 | emit expanded(row, depth); |
517 | } |
518 | |
519 | void QQuickTreeView::expandToIndex(const QModelIndex &index) |
520 | { |
521 | Q_D(QQuickTreeView); |
522 | |
523 | if (!index.isValid()) { |
524 | qmlWarning(me: this) << "index is not valid: " << index; |
525 | return; |
526 | } |
527 | |
528 | if (index.model() != d->m_treeModelToTableModel.model()) { |
529 | qmlWarning(me: this) << "index doesn't belong to correct model: " << index; |
530 | return; |
531 | } |
532 | |
533 | if (rowAtIndex(index) != -1) { |
534 | // index is already visible |
535 | return; |
536 | } |
537 | |
538 | int depth = 1; |
539 | QModelIndex parent = index.parent(); |
540 | int row = rowAtIndex(index: parent); |
541 | |
542 | while (parent.isValid()) { |
543 | if (row != -1) { |
544 | // The node is already visible, since it maps to a row in the table! |
545 | d->m_treeModelToTableModel.expandRow(n: row); |
546 | |
547 | // Update the state of the already existing delegate item |
548 | for (int c = leftColumn(); c <= rightColumn(); ++c) { |
549 | const QPoint treeNodeCell(c, row); |
550 | if (const auto item = itemAtCell(cell: treeNodeCell)) |
551 | d->setRequiredProperty(property: "expanded" , value: true, serializedModelIndex: d->modelIndexAtCell(cell: treeNodeCell), object: item, init: false); |
552 | } |
553 | |
554 | // When we hit a node that is visible, we know that all other nodes |
555 | // up to the parent have to be visible as well, so we can stop. |
556 | break; |
557 | } else { |
558 | d->m_treeModelToTableModel.expand(parent); |
559 | parent = parent.parent(); |
560 | row = rowAtIndex(index: parent); |
561 | depth++; |
562 | } |
563 | } |
564 | |
565 | emit expanded(row, depth); |
566 | } |
567 | |
568 | void QQuickTreeView::collapse(int row) |
569 | { |
570 | Q_D(QQuickTreeView); |
571 | if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) |
572 | return; |
573 | |
574 | if (!d->m_treeModelToTableModel.isExpanded(row)) |
575 | return; |
576 | |
577 | d_func()->m_treeModelToTableModel.collapseRow(n: row); |
578 | |
579 | for (int c = leftColumn(); c <= rightColumn(); ++c) { |
580 | const QPoint treeNodeCell(c, row); |
581 | if (const auto item = itemAtCell(cell: treeNodeCell)) |
582 | d->setRequiredProperty(property: "expanded" , value: false, serializedModelIndex: d->modelIndexAtCell(cell: treeNodeCell), object: item, init: false); |
583 | } |
584 | |
585 | emit collapsed(row, recursively: false); |
586 | } |
587 | |
588 | void QQuickTreeView::collapseRecursively(int row) |
589 | { |
590 | Q_D(QQuickTreeView); |
591 | if (row >= d->m_treeModelToTableModel.rowCount()) |
592 | return; |
593 | if (row < 0 && row != -1) |
594 | return; |
595 | |
596 | auto collapseRowRecursive = [this, d](int startRow) { |
597 | // Always collapse descendants recursively, |
598 | // even if the top row itself is already collapsed. |
599 | d->m_treeModelToTableModel.collapseRecursively(row: startRow); |
600 | // Update the expanded state of the (still visible) startRow |
601 | for (int c = leftColumn(); c <= rightColumn(); ++c) { |
602 | const QPoint treeNodeCell(c, startRow); |
603 | if (const auto item = itemAtCell(cell: treeNodeCell)) |
604 | d->setRequiredProperty(property: "expanded" , value: false, serializedModelIndex: d->modelIndexAtCell(cell: treeNodeCell), object: item, init: false); |
605 | } |
606 | }; |
607 | |
608 | if (row >= 0) { |
609 | collapseRowRecursive(row); |
610 | } else { |
611 | // Collapse all root nodes recursively |
612 | const auto model = d->m_treeModelToTableModel.model(); |
613 | for (int r = 0; r < model->rowCount(); ++r) { |
614 | const int rootRow = d->m_treeModelToTableModel.itemIndex(index: model->index(row: r, column: 0)); |
615 | if (rootRow != -1) |
616 | collapseRowRecursive(rootRow); |
617 | } |
618 | } |
619 | |
620 | emit collapsed(row, recursively: true); |
621 | } |
622 | |
623 | void QQuickTreeView::toggleExpanded(int row) |
624 | { |
625 | if (isExpanded(row)) |
626 | collapse(row); |
627 | else |
628 | expand(row); |
629 | } |
630 | |
631 | QModelIndex QQuickTreeView::modelIndex(const QPoint &cell) const |
632 | { |
633 | Q_D(const QQuickTreeView); |
634 | const QModelIndex tableIndex = d->m_treeModelToTableModel.index(row: cell.y(), column: cell.x()); |
635 | return d->m_treeModelToTableModel.mapToModel(index: tableIndex); |
636 | } |
637 | |
638 | QPoint QQuickTreeView::cellAtIndex(const QModelIndex &index) const |
639 | { |
640 | const QModelIndex tableIndex = d_func()->m_treeModelToTableModel.mapFromModel(index); |
641 | return QPoint(tableIndex.column(), tableIndex.row()); |
642 | } |
643 | |
644 | #if QT_DEPRECATED_SINCE(6, 4) |
645 | QModelIndex QQuickTreeView::modelIndex(int row, int column) const |
646 | { |
647 | static const bool compat6_4 = qEnvironmentVariable(varName: "QT_QUICK_TABLEVIEW_COMPAT_VERSION" ) == QStringLiteral("6.4" ); |
648 | if (compat6_4) { |
649 | // XXX Qt 7: Remove this compatibility path here and in QQuickTableView. |
650 | // In Qt 6.4.0 and 6.4.1, a source incompatible change led to row and column |
651 | // being documented to be specified in the opposite order. |
652 | // QT_QUICK_TABLEVIEW_COMPAT_VERSION can therefore be set to force tableview |
653 | // to continue accepting calls to modelIndex(column, row). |
654 | return modelIndex(cell: {row, column}); |
655 | } else { |
656 | qmlWarning(me: this) << "modelIndex(row, column) is deprecated. " |
657 | "Use index(row, column) instead. For more information, see " |
658 | "https://doc.qt.io/qt-6/qml-qtquick-tableview-obsolete.html" ; |
659 | return modelIndex(cell: {column, row}); |
660 | } |
661 | } |
662 | #endif |
663 | |
664 | void QQuickTreeView::keyPressEvent(QKeyEvent *event) |
665 | { |
666 | event->ignore(); |
667 | |
668 | if (!keyNavigationEnabled()) |
669 | return; |
670 | if (!selectionModel()) |
671 | return; |
672 | |
673 | const int row = cellAtIndex(index: selectionModel()->currentIndex()).y(); |
674 | switch (event->key()) { |
675 | case Qt::Key_Left: |
676 | collapse(row); |
677 | event->accept(); |
678 | break; |
679 | case Qt::Key_Right: |
680 | expand(row); |
681 | event->accept(); |
682 | break; |
683 | default: |
684 | break; |
685 | } |
686 | |
687 | if (!event->isAccepted()) |
688 | QQuickTableView::keyPressEvent(e: event); |
689 | } |
690 | |
691 | QT_END_NAMESPACE |
692 | |
693 | #include "moc_qquicktreeview_p.cpp" |
694 | |