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