#include "QQuickTreeViewEx.h" #include "QQuickTreeViewExPrivate.h" #include #include #include #include // Hard-code the tree column to be 0 for now static const int kTreeColumn = 0; QQuickTreeViewExPrivate::QQuickTreeViewExPrivate() : QQuickTableViewPrivate() { } QQuickTreeViewExPrivate::~QQuickTreeViewExPrivate() { } QVariant QQuickTreeViewExPrivate::modelImpl() const { return m_assignedModel; } void QQuickTreeViewExPrivate::setModelImpl(const QVariant& newModel) { Q_Q(QQuickTreeViewEx); if (newModel == m_assignedModel) return; m_assignedModel = newModel; QVariant effectiveModel = m_assignedModel; if (effectiveModel.userType() == qMetaTypeId()) effectiveModel = effectiveModel.value().toVariant(); if (effectiveModel.isNull()) m_treeModelToTableModel.setModel(nullptr); else if (const auto qaim = qvariant_cast(effectiveModel)) m_treeModelToTableModel.setModel(qaim); else qmlWarning(q) << "TreeView only accepts a model of type QAbstractItemModel"; scheduleRebuildTable(QQuickTableViewPrivate::RebuildOption::All); emit q->modelChanged(); } void QQuickTreeViewExPrivate::initItemCallback(int serializedModelIndex, QObject* object) { Q_Q(QQuickTreeViewEx); updateRequiredProperties(serializedModelIndex, object, true); QQuickTableViewPrivate::initItemCallback(serializedModelIndex, object); } void QQuickTreeViewExPrivate::itemReusedCallback(int serializedModelIndex, QObject* object) { updateRequiredProperties(serializedModelIndex, object, false); QQuickTableViewPrivate::itemReusedCallback(serializedModelIndex, object); } void QQuickTreeViewExPrivate::dataChangedCallback( const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector& roles) { Q_Q(QQuickTreeViewEx); 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 QQuickTreeViewExPrivate::updateRequiredProperties(int serializedModelIndex, QObject* object, bool init) { Q_Q(QQuickTreeViewEx); 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 QQuickTreeViewExPrivate::updateSelection(const QRect& oldSelection, const QRect& newSelection) { Q_Q(QQuickTreeViewEx); const QRect oldRect = oldSelection.normalized(); const QRect newRect = newSelection.normalized(); if (oldSelection == newSelection) return; // Select the rows inside newRect that doesn't overlap with oldRect for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) { if (oldRect.y() != -1 && oldRect.y() <= row && row <= oldRect.y() + oldRect.height()) continue; const QModelIndex startIndex = q->index(row, newRect.x()); const QModelIndex endIndex = q->index(row, newRect.x() + newRect.width()); selectionModel->select(QItemSelection(startIndex, endIndex), QItemSelectionModel::Select); } if (oldRect.x() != -1) { // Since oldRect is valid, this update is a continuation of an already existing selection! // Select the columns inside newRect that don't overlap with oldRect for (int column = newRect.x(); column <= newRect.x() + newRect.width(); ++column) { if (oldRect.x() <= column && column <= oldRect.x() + oldRect.width()) continue; for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) selectionModel->select(q->index(row, column), QItemSelectionModel::Select); } // Unselect the rows inside oldRect that don't overlap with newRect for (int row = oldRect.y(); row <= oldRect.y() + oldRect.height(); ++row) { if (newRect.y() <= row && row <= newRect.y() + newRect.height()) continue; const QModelIndex startIndex = q->index(row, oldRect.x()); const QModelIndex endIndex = q->index(row, oldRect.x() + oldRect.width()); selectionModel->select(QItemSelection(startIndex, endIndex), QItemSelectionModel::Deselect); } // Unselect the columns inside oldRect that don't overlap with newRect for (int column = oldRect.x(); column <= oldRect.x() + oldRect.width(); ++column) { if (newRect.x() <= column && column <= newRect.x() + newRect.width()) continue; // Since we're not allowed to call select/unselect on the selectionModel with // indices from different parents, and since indicies from different parents are // expected when working with trees, we need to unselect the indices in the column // one by one, rather than the whole column in one go. This, however, can cause a // lot of selection fragments in the selectionModel, which eventually can hurt // performance. But large selections containing a lot of columns is not normally // the case for a treeview, so accept this potential corner case for now. for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) selectionModel->select(q->index(row, column), QItemSelectionModel::Deselect); } } } QQuickTreeViewEx::QQuickTreeViewEx(QQuickItem* parent) : QQuickTableView(*(new QQuickTreeViewExPrivate), parent) { Q_D(QQuickTreeViewEx); 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, &QQuickTreeViewExPrivate::dataChangedCallback); QObject::connect(&d->m_treeModelToTableModel, &QQmlTreeModelToTableModel::rootIndexChanged, this, &QQuickTreeViewEx::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); }); } QQuickTreeViewEx::QQuickTreeViewEx(QQuickTreeViewExPrivate& dd, QQuickItem* parent) : QQuickTableView(dd, parent) { Q_D(QQuickTreeViewEx); 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, &QQuickTreeViewExPrivate::dataChangedCallback); QObject::connect(&d->m_treeModelToTableModel, &QQmlTreeModelToTableModel::rootIndexChanged, this, &QQuickTreeViewEx::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); }); d_func()->init(); } QQuickTreeViewEx::~QQuickTreeViewEx() { } QModelIndex QQuickTreeViewEx::rootIndex() const { return d_func()->m_treeModelToTableModel.rootIndex(); } void QQuickTreeViewEx::setRootIndex(const QModelIndex& index) { Q_D(QQuickTreeViewEx); d->m_treeModelToTableModel.setRootIndex(index); positionViewAtCell({ 0, 0 }, QQuickTableView::AlignTop | QQuickTableView::AlignLeft); } void QQuickTreeViewEx::resetRootIndex() { Q_D(QQuickTreeViewEx); d->m_treeModelToTableModel.resetRootIndex(); positionViewAtCell({ 0, 0 }, QQuickTableView::AlignTop | QQuickTableView::AlignLeft); } int QQuickTreeViewEx::depth(int row) const { Q_D(const QQuickTreeViewEx); if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) return -1; return d->m_treeModelToTableModel.depthAtRow(row); } bool QQuickTreeViewEx::isExpanded(int row) const { Q_D(const QQuickTreeViewEx); if (row < 0 || row >= d->m_treeModelToTableModel.rowCount()) return false; return d->m_treeModelToTableModel.isExpanded(row); } void QQuickTreeViewEx::expand(int row) { if (row >= 0) expandRecursively(row, 1); } void QQuickTreeViewEx::expandRecursively(int row, int depth) { Q_D(QQuickTreeViewEx); 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 QQuickTreeViewEx::expandToIndex(const QModelIndex& index) { Q_D(QQuickTreeViewEx); 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 QQuickTreeViewEx::collapse(int row) { Q_D(QQuickTreeViewEx); 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 QQuickTreeViewEx::collapseRecursively(int row) { Q_D(QQuickTreeViewEx); 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 QQuickTreeViewEx::toggleExpanded(int row) { if (isExpanded(row)) collapse(row); else expand(row); } void QQuickTreeViewEx::invalidateLayout() { Q_D(QQuickTreeViewEx); d->forceLayout(false); } QModelIndex QQuickTreeViewEx::modelIndex(const QPoint& cell) const { Q_D(const QQuickTreeViewEx); const QModelIndex tableIndex = d->m_treeModelToTableModel.index(cell.y(), cell.x()); return d->m_treeModelToTableModel.mapToModel(tableIndex); } QPoint QQuickTreeViewEx::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 QQuickTreeViewEx::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 QQuickTreeViewEx::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); }