| 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 | |