diff options
Diffstat (limited to 'src/quick/items/qquicktreeview.cpp')
-rw-r--r-- | src/quick/items/qquicktreeview.cpp | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/src/quick/items/qquicktreeview.cpp b/src/quick/items/qquicktreeview.cpp new file mode 100644 index 0000000000..033884d58d --- /dev/null +++ b/src/quick/items/qquicktreeview.cpp @@ -0,0 +1,674 @@ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qquicktreeview_p_p.h" + +#include <QtCore/qobject.h> +#include <QtQml/qqmlcontext.h> +#include <QtQuick/private/qquicktaphandler_p.h> + +#include <QtQmlModels/private/qqmltreemodeltotablemodel_p_p.h> + +/*! + \qmltype TreeView + \inqmlmodule QtQuick + \ingroup qtquick-views + \since 6.3 + \inherits TableView + \brief Provides a tree view to display data from a QAbstractItemModel. + + A TreeView has a \l model that defines the data to be displayed, and a + \l delegate that defines how the data should be displayed. + + TreeView inherits \l TableView. This means that even if the model + has a parent-child tree structure, TreeView is internally using a + proxy model that converts that structure into a flat table + model that can be rendered by TableView. Each node in the tree ends up + occupying one row in the table, where the first column renders the tree + itself. By indenting each delegate item in that column according to its + parent-child depth in the model, it will end up looking like a tree, even + if it's technically still just a flat list of items. + + To allow for maximum flexibility, TreeView itself will not position the delegate + items into a tree structure. This burden is placed on the delegate. + \l {Qt Quick Controls} offers a ready-made TreeViewDelegate that can be + used for this, which has the advantage that it works out-of-the-box and + renders a tree which follows the style of the platform where the application runs. + + Even if TreeViewDelegate is customizable, there might be situations + where you want to render the tree in a different way, or ensure that + the delegate ends up as minimal as possible, perhaps for performance reasons. + Creating your own delegate from scratch is easy, since TreeView offers + a set of properties that can be used to position and render each node + in the tree correctly. + + An example of a custom delegate with an animating indicator is shown below: + + \snippet qml/treeview/qml-customdelegate.qml 0 + + The properties that are marked as \c required will be filled in by + TreeView, and are similar to attached properties. By marking them as + required, the delegate indirectly informs TreeView that it should take + responsibility for assigning them values. The following required properties + can be added to a delegate: + + \list + \li \c {required property TreeView treeView} + - Points to the TreeView that contains the delegate item. + \li \c {required property bool isTreeNode} + - Is \c true if the delegate item represents a node in + the tree. Only one column in the view will be used to draw the tree, and + therefore, only delegate items in that column will have this + property set to \c true. + A node in the tree should typically be indented according to its + \c depth, and show an indicator if \c hasChildren is \c true. + Delegate items in other columns will have this property set to + \c false, and will show data from the remaining columns + in the model (and typically not be indented). + \li \c {required property bool expanded} + - Is \c true if the model item drawn by the delegate is expanded + in the view. + \li \c {required property bool hasChildren} + - Is \c true if the model item drawn by the delegate has children + in the model. + \li \c {required property int depth} + - Contains the depth of the model item drawn by the delegate. + The depth of a model item is the same as the number of ancestors + it has in the model. + \endlist + + See also \l {Required Properties}. + + By default, TreeView \l {toggleExpanded()}{toggles} the expanded state + of a row when you double tap on it. Since this is in conflict with + double tapping to edit a cell, TreeView sets \l {TableView::}{editTriggers} to + \c TableView.EditKeyPressed by default (which is different from TableView, + which uses \c {TableView.EditKeyPressed | TableView.DoubleTapped}. + If you change \l {TableView::}{editTriggers} to also contain \c TableView.DoubleTapped, + toggling the expanded state with a double tap will be disabled. + + \note A TreeView only accepts a model that inherits \l QAbstractItemModel. +*/ + +/*! + \qmlproperty QModelIndex QtQuick::TreeView::rootIndex + \since 6.6 + + This property holds the model index of the root item in the tree. + By default, this is the same as the root index in the model, but you can + set it to be a child index instead, to show only a branch of the tree. + Set it to \c undefined to show the whole model. +*/ + +/*! + \qmlmethod int QtQuick::TreeView::depth(row) + + Returns the depth (the number of parents up to the root) of the given \a row. + + \a row should be the row in the view (table row), and not a row in the model. + If \a row is not between \c 0 and \l {TableView::}{rows}, the return value will + be \c -1. + + \sa {TableView::}{modelIndex()} +*/ + +/*! + \qmlmethod bool QtQuick::TreeView::isExpanded(row) + + Returns if the given \a row in the view is shown as expanded. + + \a row should be the row in the view (table row), and not a row in the model. + If \a row is not between \c 0 and \l {TableView::}{rows}, the return value will + be \c false. +*/ + +/*! + \qmlmethod QtQuick::TreeView::expand(row) + + Expands the tree node at the given \a row in the view. + + \a row should be the row in the view (table row), and not a row in the model. + + \note this function will not affect the model, only + the visual representation in the view. + + \sa collapse(), isExpanded(), expandRecursively() +*/ + +/*! + \qmlmethod QtQuick::TreeView::expandRecursively(row = -1, depth = -1) + \since 6.4 + + Expands the tree node at the given \a row in the view recursively down to + \a depth. \a depth should be relative to the depth of \a row. If + \a depth is \c -1, the tree will be expanded all the way down to all leaves. + + For a model that has more than one root, you can also call this function + with \a row equal to \c -1. This will expand all roots. Hence, calling + expandRecursively(-1, -1), or simply expandRecursively(), will expand + all nodes in the model. + + \a row should be the row in the view (table row), and not a row in the model. + + \note This function will not try to \l{QAbstractItemModel::fetchMore}{fetch more} data. + \note This function will not affect the model, only the visual representation in the view. + \warning If the model contains a large number of items, this function will + take some time to execute. + + \sa collapseRecursively(), expand(), collapse(), isExpanded(), depth() +*/ + +/*! + \qmlmethod QtQuick::TreeView::expandToIndex(QModelIndex index) + \since 6.4 + + Expands the tree from the given model \a index, and recursively all the way up + to the root. The result will be that the delegate item that represents \a index + becomes visible in the view (unless it ends up outside the viewport). To + ensure that the row ends up visible in the viewport, you can do: + + \code + expandToIndex(index) + forceLayout() + positionViewAtRow(rowAtIndex(index), Qt.AlignVCenter) + \endcode + + \sa expand(), expandRecursively() +*/ + +/*! + \qmlmethod QtQuick::TreeView::collapse(row) + + Collapses the tree node at the given \a row in the view. + + \a row should be the row in the view (table row), and not a row in the model. + + \note this function will not affect the model, only + the visual representation in the view. + + \sa expand(), isExpanded() +*/ + +/*! + \qmlmethod QtQuick::TreeView::collapseRecursively(row = -1) + \since 6.4 + + Collapses the tree node at the given \a row in the view recursively down to + all leaves. + + For a model has more than one root, you can also call this function + with \a row equal to \c -1. This will collapse all roots. Hence, calling + collapseRecursively(-1), or simply collapseRecursively(), will collapse + all nodes in the model. + + \a row should be the row in the view (table row), and not a row in the model. + + \note this function will not affect the model, only + the visual representation in the view. + + \sa expandRecursively(), expand(), collapse(), isExpanded(), depth() +*/ + +/*! + \qmlmethod QtQuick::TreeView::toggleExpanded(row) + + Toggles if the tree node at the given \a row should be expanded. + This is a convenience for doing: + + \code + if (isExpanded(row)) + collapse(row) + else + expand(row) + \endcode + + \a row should be the row in the view (table row), and not a row in the model. +*/ + +/*! + \qmlsignal QtQuick::TreeView::expanded(row, depth) + + This signal is emitted when a \a row is expanded in the view. + \a row and \a depth will be equal to the arguments given to the call + that caused the expansion to happen (\l expand() or \l expandRecursively()). + In case of \l expand(), \a depth will always be \c 1. + In case of \l expandToIndex(), \a depth will be the depth of the + target index. + + \note when a row is expanded recursively, the expanded signal will + only be emitted for that one row, and not for its descendants. + + \sa collapsed(), expand(), collapse(), toggleExpanded() +*/ + +/*! + \qmlsignal QtQuick::TreeView::collapsed(row, recursively) + + This signal is emitted when a \a row is collapsed in the view. + \a row will be equal to the argument given to the call that caused + the collapse to happen (\l collapse() or \l collapseRecursively()). + If the row was collapsed recursively, \a recursively will be \c true. + + \note when a row is collapsed recursively, the collapsed signal will + only be emitted for that one row, and not for its descendants. + + \sa expanded(), expand(), collapse(), toggleExpanded() +*/ + +// Hard-code the tree column to be 0 for now +static const int kTreeColumn = 0; + +QT_BEGIN_NAMESPACE + +QQuickTreeViewPrivate::QQuickTreeViewPrivate() + : QQuickTableViewPrivate() +{ +} + +QQuickTreeViewPrivate::~QQuickTreeViewPrivate() +{ +} + +QVariant QQuickTreeViewPrivate::modelImpl() const +{ + return m_assignedModel; +} + +void QQuickTreeViewPrivate::setModelImpl(const QVariant &newModel) +{ + Q_Q(QQuickTreeView); + + m_assignedModel = newModel; + QVariant effectiveModel = m_assignedModel; + if (effectiveModel.userType() == qMetaTypeId<QJSValue>()) + effectiveModel = effectiveModel.value<QJSValue>().toVariant(); + + if (effectiveModel.isNull()) + m_treeModelToTableModel.setModel(nullptr); + else if (const auto qaim = qvariant_cast<QAbstractItemModel*>(effectiveModel)) + m_treeModelToTableModel.setModel(qaim); + else + qmlWarning(q) << "TreeView only accepts a model of type QAbstractItemModel"; + + + scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::All); + emit q->modelChanged(); +} + +void QQuickTreeViewPrivate::initItemCallback(int serializedModelIndex, QObject *object) +{ + updateRequiredProperties(serializedModelIndex, object, true); + QQuickTableViewPrivate::initItemCallback(serializedModelIndex, object); +} + +void QQuickTreeViewPrivate::itemReusedCallback(int serializedModelIndex, QObject *object) +{ + updateRequiredProperties(serializedModelIndex, object, false); + QQuickTableViewPrivate::itemReusedCallback(serializedModelIndex, object); +} + +void QQuickTreeViewPrivate::dataChangedCallback( + const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) +{ + Q_Q(QQuickTreeView); + Q_UNUSED(roles); + + for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { + for (int column = topLeft.column(); column <= bottomRight.column(); ++column) { + const QPoint cell(column, row); + auto item = q->itemAtCell(cell); + if (!item) + continue; + + const int serializedModelIndex = modelIndexAtCell(QPoint(column, row)); + updateRequiredProperties(serializedModelIndex, item, false); + } + } +} + +void QQuickTreeViewPrivate::updateRequiredProperties(int serializedModelIndex, QObject *object, bool init) +{ + Q_Q(QQuickTreeView); + const QPoint cell = cellAtModelIndex(serializedModelIndex); + const int row = cell.y(); + const int column = cell.x(); + + setRequiredProperty("treeView", QVariant::fromValue(q), serializedModelIndex, object, init); + setRequiredProperty("isTreeNode", column == kTreeColumn, serializedModelIndex, object, init); + setRequiredProperty("hasChildren", m_treeModelToTableModel.hasChildren(row), serializedModelIndex, object, init); + setRequiredProperty("expanded", q->isExpanded(row), serializedModelIndex, object, init); + setRequiredProperty("depth", m_treeModelToTableModel.depthAtRow(row), serializedModelIndex, object, init); +} + +void QQuickTreeViewPrivate::updateSelection(const QRect &oldSelection, const QRect &newSelection) +{ + Q_Q(QQuickTreeView); + + if (oldSelection == newSelection) + return; + + QItemSelection select; + QItemSelection deselect; + + // Because each row can have a different parent, we need to create separate QItemSelections + // per row. But all the cells in a given row have the same parent, so they can be combined. + // As a result, the final QItemSelection can end up more fragmented compared to a selection + // in QQuickTableView, where all cells have the same parent. In the end, if TreeView has + // a lot of columns and the selection mode is "SelectCells", using the mouse to adjust + // a selection containing a _large_ number of columns can be slow. + const QRect cells = newSelection.normalized(); + for (int row = cells.y(); row <= cells.y() + cells.height(); ++row) { + const QModelIndex startIndex = q->index(row, cells.x()); + const QModelIndex endIndex = q->index(row, cells.x() + cells.width()); + select.merge(QItemSelection(startIndex, endIndex), QItemSelectionModel::Select); + } + + const QModelIndexList indexes = selectionModel->selection().indexes(); + for (const QModelIndex &index : indexes) { + if (!select.contains(index) && !existingSelection.contains(index)) + deselect.merge(QItemSelection(index, index), QItemSelectionModel::Select); + } + + if (selectionFlag == QItemSelectionModel::Select) { + selectionModel->select(deselect, QItemSelectionModel::Deselect); + selectionModel->select(select, QItemSelectionModel::Select); + } else { + QItemSelection oldSelection = existingSelection; + oldSelection.merge(select, QItemSelectionModel::Deselect); + selectionModel->select(oldSelection, QItemSelectionModel::Select); + selectionModel->select(select, QItemSelectionModel::Deselect); + } +} + +QQuickTreeView::QQuickTreeView(QQuickItem *parent) + : QQuickTableView(*(new QQuickTreeViewPrivate), parent) +{ + Q_D(QQuickTreeView); + + setSelectionBehavior(SelectRows); + setEditTriggers(EditKeyPressed); + + // Note: QQuickTableView will only ever see the table model m_treeModelToTableModel, and + // never the actual tree model that is assigned to us by the application. + const auto modelAsVariant = QVariant::fromValue(std::addressof(d->m_treeModelToTableModel)); + d->QQuickTableViewPrivate::setModelImpl(modelAsVariant); + QObjectPrivate::connect(&d->m_treeModelToTableModel, &QAbstractItemModel::dataChanged, + d, &QQuickTreeViewPrivate::dataChangedCallback); + QObject::connect(&d->m_treeModelToTableModel, &QQmlTreeModelToTableModel::rootIndexChanged, + this, &QQuickTreeView::rootIndexChanged); + + auto tapHandler = new QQuickTapHandler(this); + tapHandler->setAcceptedModifiers(Qt::NoModifier); + connect(tapHandler, &QQuickTapHandler::doubleTapped, [this, tapHandler]{ + if (!pointerNavigationEnabled()) + return; + if (editTriggers() & DoubleTapped) + return; + + const int row = cellAtPosition(tapHandler->point().pressPosition()).y(); + toggleExpanded(row); + }); +} + +QQuickTreeView::~QQuickTreeView() +{ +} + +QModelIndex QQuickTreeView::rootIndex() const +{ + return d_func()->m_treeModelToTableModel.rootIndex(); +} + +void QQuickTreeView::setRootIndex(const QModelIndex &index) +{ + Q_D(QQuickTreeView); + d->m_treeModelToTableModel.setRootIndex(index); + positionViewAtCell({0, 0}, QQuickTableView::AlignTop | QQuickTableView::AlignLeft); +} + +void QQuickTreeView::resetRootIndex() +{ + Q_D(QQuickTreeView); + d->m_treeModelToTableModel.resetRootIndex(); + positionViewAtCell({0, 0}, QQuickTableView::AlignTop | QQuickTableView::AlignLeft); +} + +int QQuickTreeView::depth(int row) const +{ + Q_D(const QQuickTreeView); + if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) + return -1; + + return d->m_treeModelToTableModel.depthAtRow(row); +} + +bool QQuickTreeView::isExpanded(int row) const +{ + Q_D(const QQuickTreeView); + if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) + return false; + + return d->m_treeModelToTableModel.isExpanded(row); +} + +void QQuickTreeView::expand(int row) +{ + if (row >= 0) + expandRecursively(row, 1); +} + +void QQuickTreeView::expandRecursively(int row, int depth) +{ + Q_D(QQuickTreeView); + if (row >= d->m_treeModelToTableModel.rowCount()) + return; + if (row < 0 && row != -1) + return; + if (depth == 0 || depth < -1) + return; + + auto expandRowRecursively = [this, d, depth](int startRow) { + d->m_treeModelToTableModel.expandRecursively(startRow, depth); + // Update the expanded state of the startRow. The descendant rows that gets + // expanded will get the correct state set from initItem/itemReused instead. + for (int c = leftColumn(); c <= rightColumn(); ++c) { + const QPoint treeNodeCell(c, startRow); + if (const auto item = itemAtCell(treeNodeCell)) + d->setRequiredProperty("expanded", true, d->modelIndexAtCell(treeNodeCell), item, false); + } + }; + + if (row >= 0) { + // Expand only one row recursively + const bool isExpanded = d->m_treeModelToTableModel.isExpanded(row); + if (isExpanded && depth == 1) + return; + expandRowRecursively(row); + } else { + // Expand all root nodes recursively + const auto model = d->m_treeModelToTableModel.model(); + for (int r = 0; r < model->rowCount(); ++r) { + const int rootRow = d->m_treeModelToTableModel.itemIndex(model->index(r, 0)); + if (rootRow != -1) + expandRowRecursively(rootRow); + } + } + + emit expanded(row, depth); +} + +void QQuickTreeView::expandToIndex(const QModelIndex &index) +{ + Q_D(QQuickTreeView); + + if (!index.isValid()) { + qmlWarning(this) << "index is not valid: " << index; + return; + } + + if (index.model() != d->m_treeModelToTableModel.model()) { + qmlWarning(this) << "index doesn't belong to correct model: " << index; + return; + } + + if (rowAtIndex(index) != -1) { + // index is already visible + return; + } + + int depth = 1; + QModelIndex parent = index.parent(); + int row = rowAtIndex(parent); + + while (parent.isValid()) { + if (row != -1) { + // The node is already visible, since it maps to a row in the table! + d->m_treeModelToTableModel.expandRow(row); + + // Update the state of the already existing delegate item + for (int c = leftColumn(); c <= rightColumn(); ++c) { + const QPoint treeNodeCell(c, row); + if (const auto item = itemAtCell(treeNodeCell)) + d->setRequiredProperty("expanded", true, d->modelIndexAtCell(treeNodeCell), item, false); + } + + // When we hit a node that is visible, we know that all other nodes + // up to the parent have to be visible as well, so we can stop. + break; + } else { + d->m_treeModelToTableModel.expand(parent); + parent = parent.parent(); + row = rowAtIndex(parent); + depth++; + } + } + + emit expanded(row, depth); +} + +void QQuickTreeView::collapse(int row) +{ + Q_D(QQuickTreeView); + if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) + return; + + if (!d->m_treeModelToTableModel.isExpanded(row)) + return; + + d_func()->m_treeModelToTableModel.collapseRow(row); + + for (int c = leftColumn(); c <= rightColumn(); ++c) { + const QPoint treeNodeCell(c, row); + if (const auto item = itemAtCell(treeNodeCell)) + d->setRequiredProperty("expanded", false, d->modelIndexAtCell(treeNodeCell), item, false); + } + + emit collapsed(row, false); +} + +void QQuickTreeView::collapseRecursively(int row) +{ + Q_D(QQuickTreeView); + if (row >= d->m_treeModelToTableModel.rowCount()) + return; + if (row < 0 && row != -1) + return; + + auto collapseRowRecursive = [this, d](int startRow) { + // Always collapse descendants recursively, + // even if the top row itself is already collapsed. + d->m_treeModelToTableModel.collapseRecursively(startRow); + // Update the expanded state of the (still visible) startRow + for (int c = leftColumn(); c <= rightColumn(); ++c) { + const QPoint treeNodeCell(c, startRow); + if (const auto item = itemAtCell(treeNodeCell)) + d->setRequiredProperty("expanded", false, d->modelIndexAtCell(treeNodeCell), item, false); + } + }; + + if (row >= 0) { + collapseRowRecursive(row); + } else { + // Collapse all root nodes recursively + const auto model = d->m_treeModelToTableModel.model(); + for (int r = 0; r < model->rowCount(); ++r) { + const int rootRow = d->m_treeModelToTableModel.itemIndex(model->index(r, 0)); + if (rootRow != -1) + collapseRowRecursive(rootRow); + } + } + + emit collapsed(row, true); +} + +void QQuickTreeView::toggleExpanded(int row) +{ + if (isExpanded(row)) + collapse(row); + else + expand(row); +} + +QModelIndex QQuickTreeView::modelIndex(const QPoint &cell) const +{ + Q_D(const QQuickTreeView); + const QModelIndex tableIndex = d->m_treeModelToTableModel.index(cell.y(), cell.x()); + return d->m_treeModelToTableModel.mapToModel(tableIndex); +} + +QPoint QQuickTreeView::cellAtIndex(const QModelIndex &index) const +{ + const QModelIndex tableIndex = d_func()->m_treeModelToTableModel.mapFromModel(index); + return QPoint(tableIndex.column(), tableIndex.row()); +} + +#if QT_DEPRECATED_SINCE(6, 4) +QModelIndex QQuickTreeView::modelIndex(int row, int column) const +{ + static const bool compat6_4 = qEnvironmentVariable("QT_QUICK_TABLEVIEW_COMPAT_VERSION") == QStringLiteral("6.4"); + if (compat6_4) { + // XXX Qt 7: Remove this compatibility path here and in QQuickTableView. + // In Qt 6.4.0 and 6.4.1, a source incompatible change led to row and column + // being documented to be specified in the opposite order. + // QT_QUICK_TABLEVIEW_COMPAT_VERSION can therefore be set to force tableview + // to continue accepting calls to modelIndex(column, row). + return modelIndex({row, column}); + } else { + qmlWarning(this) << "modelIndex(row, column) is deprecated. " + "Use index(row, column) instead. For more information, see " + "https://doc.qt.io/qt-6/qml-qtquick-tableview-obsolete.html"; + return modelIndex({column, row}); + } +} +#endif + +void QQuickTreeView::keyPressEvent(QKeyEvent *event) +{ + event->ignore(); + + if (!keyNavigationEnabled()) + return; + if (!selectionModel()) + return; + + const int row = cellAtIndex(selectionModel()->currentIndex()).y(); + switch (event->key()) { + case Qt::Key_Left: + collapse(row); + event->accept(); + break; + case Qt::Key_Right: + expand(row); + event->accept(); + break; + default: + break; + } + + if (!event->isAccepted()) + QQuickTableView::keyPressEvent(event); +} + +QT_END_NAMESPACE + +#include "moc_qquicktreeview_p.cpp" |