diff options
Diffstat (limited to 'src/qmlmodels/qqmltreemodeltotablemodel.cpp')
-rw-r--r-- | src/qmlmodels/qqmltreemodeltotablemodel.cpp | 1170 |
1 files changed, 1170 insertions, 0 deletions
diff --git a/src/qmlmodels/qqmltreemodeltotablemodel.cpp b/src/qmlmodels/qqmltreemodeltotablemodel.cpp new file mode 100644 index 0000000000..db128761cd --- /dev/null +++ b/src/qmlmodels/qqmltreemodeltotablemodel.cpp @@ -0,0 +1,1170 @@ +// 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 <math.h> +#include <QtCore/qstack.h> +#include <QtCore/qdebug.h> + +#include "qqmltreemodeltotablemodel_p_p.h" + +QT_BEGIN_NAMESPACE + +//#define QQMLTREEMODELADAPTOR_DEBUG +#if defined(QQMLTREEMODELADAPTOR_DEBUG) && !defined(QT_TESTLIB_LIB) +# define ASSERT_CONSISTENCY() Q_ASSERT_X(testConsistency(true /* dumpOnFail */), Q_FUNC_INFO, "Consistency test failed") +#else +# define ASSERT_CONSISTENCY qt_noop +#endif + +QQmlTreeModelToTableModel::QQmlTreeModelToTableModel(QObject *parent) + : QAbstractItemModel(parent) +{ +} + +QAbstractItemModel *QQmlTreeModelToTableModel::model() const +{ + return m_model; +} + +void QQmlTreeModelToTableModel::connectToModel() +{ + m_connections = { + QObject::connect(m_model, &QAbstractItemModel::destroyed, + this, &QQmlTreeModelToTableModel::modelHasBeenDestroyed), + QObject::connect(m_model, &QAbstractItemModel::modelReset, + this, &QQmlTreeModelToTableModel::modelHasBeenReset), + QObject::connect(m_model, &QAbstractItemModel::dataChanged, + this, &QQmlTreeModelToTableModel::modelDataChanged), + + QObject::connect(m_model, &QAbstractItemModel::layoutAboutToBeChanged, + this, &QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged), + QObject::connect(m_model, &QAbstractItemModel::layoutChanged, + this, &QQmlTreeModelToTableModel::modelLayoutChanged), + + QObject::connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, + this, &QQmlTreeModelToTableModel::modelRowsAboutToBeInserted), + QObject::connect(m_model, &QAbstractItemModel::rowsInserted, + this, &QQmlTreeModelToTableModel::modelRowsInserted), + QObject::connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, + this, &QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved), + QObject::connect(m_model, &QAbstractItemModel::rowsRemoved, + this, &QQmlTreeModelToTableModel::modelRowsRemoved), + QObject::connect(m_model, &QAbstractItemModel::rowsAboutToBeMoved, + this, &QQmlTreeModelToTableModel::modelRowsAboutToBeMoved), + QObject::connect(m_model, &QAbstractItemModel::rowsMoved, + this, &QQmlTreeModelToTableModel::modelRowsMoved), + + QObject::connect(m_model, &QAbstractItemModel::columnsAboutToBeInserted, + this, &QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted), + QObject::connect(m_model, &QAbstractItemModel::columnsAboutToBeRemoved, + this, &QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved), + QObject::connect(m_model, &QAbstractItemModel::columnsInserted, + this, &QQmlTreeModelToTableModel::modelColumnsInserted), + QObject::connect(m_model, &QAbstractItemModel::columnsRemoved, + this, &QQmlTreeModelToTableModel::modelColumnsRemoved) + }; +} + +void QQmlTreeModelToTableModel::setModel(QAbstractItemModel *arg) +{ + if (m_model != arg) { + if (m_model) { + for (const auto &c : m_connections) + QObject::disconnect(c); + m_connections.fill({}); + } + + clearModelData(); + m_model = arg; + + if (m_rootIndex.isValid() && m_rootIndex.model() != m_model) + m_rootIndex = QModelIndex(); + + if (m_model) { + connectToModel(); + showModelTopLevelItems(); + } + + emit modelChanged(arg); + } +} + +void QQmlTreeModelToTableModel::clearModelData() +{ + beginResetModel(); + m_items.clear(); + m_expandedItems.clear(); + endResetModel(); +} + +QModelIndex QQmlTreeModelToTableModel::rootIndex() const +{ + return m_rootIndex; +} + +void QQmlTreeModelToTableModel::setRootIndex(const QModelIndex &idx) +{ + if (m_rootIndex == idx) + return; + + if (m_model) + clearModelData(); + m_rootIndex = idx; + if (m_model) + showModelTopLevelItems(); + emit rootIndexChanged(); +} + +void QQmlTreeModelToTableModel::resetRootIndex() +{ + setRootIndex(QModelIndex()); +} + +QModelIndex QQmlTreeModelToTableModel::index(int row, int column, const QModelIndex &parent) const +{ + return hasIndex(row, column, parent) ? createIndex(row, column) : QModelIndex(); +} + +QModelIndex QQmlTreeModelToTableModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + return QModelIndex(); +} + +QHash<int, QByteArray> QQmlTreeModelToTableModel::roleNames() const +{ + if (!m_model) + return QHash<int, QByteArray>(); + return m_model->roleNames(); +} + +int QQmlTreeModelToTableModel::rowCount(const QModelIndex &) const +{ + if (!m_model) + return 0; + return m_items.size(); +} + +int QQmlTreeModelToTableModel::columnCount(const QModelIndex &parent) const +{ + if (!m_model) + return 0; + return m_model->columnCount(parent); +} + +QVariant QQmlTreeModelToTableModel::data(const QModelIndex &index, int role) const +{ + if (!m_model) + return QVariant(); + + return m_model->data(mapToModel(index), role); +} + +bool QQmlTreeModelToTableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!m_model) + return false; + + return m_model->setData(mapToModel(index), value, role); +} + +QVariant QQmlTreeModelToTableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return m_model->headerData(section, orientation, role); +} + +Qt::ItemFlags QQmlTreeModelToTableModel::flags(const QModelIndex &index) const +{ + return m_model->flags(mapToModel(index)); +} + +int QQmlTreeModelToTableModel::depthAtRow(int row) const +{ + if (row < 0 || row >= m_items.size()) + return 0; + return m_items.at(row).depth; +} + +int QQmlTreeModelToTableModel::itemIndex(const QModelIndex &index) const +{ + // This is basically a plagiarism of QTreeViewPrivate::viewIndex() + if (!index.isValid() || index == m_rootIndex || m_items.isEmpty()) + return -1; + + const int totalCount = m_items.size(); + + // We start nearest to the lastViewedItem + int localCount = qMin(m_lastItemIndex - 1, totalCount - m_lastItemIndex); + + for (int i = 0; i < localCount; ++i) { + const TreeItem &item1 = m_items.at(m_lastItemIndex + i); + if (item1.index == index) { + m_lastItemIndex = m_lastItemIndex + i; + return m_lastItemIndex; + } + const TreeItem &item2 = m_items.at(m_lastItemIndex - i - 1); + if (item2.index == index) { + m_lastItemIndex = m_lastItemIndex - i - 1; + return m_lastItemIndex; + } + } + + for (int j = qMax(0, m_lastItemIndex + localCount); j < totalCount; ++j) { + const TreeItem &item = m_items.at(j); + if (item.index == index) { + m_lastItemIndex = j; + return j; + } + } + + for (int j = qMin(totalCount, m_lastItemIndex - localCount) - 1; j >= 0; --j) { + const TreeItem &item = m_items.at(j); + if (item.index == index) { + m_lastItemIndex = j; + return j; + } + } + + // nothing found + return -1; +} + +bool QQmlTreeModelToTableModel::isVisible(const QModelIndex &index) +{ + return itemIndex(index) != -1; +} + +bool QQmlTreeModelToTableModel::childrenVisible(const QModelIndex &index) +{ + return (index == m_rootIndex && !m_items.isEmpty()) + || (m_expandedItems.contains(index) && isVisible(index)); +} + +QModelIndex QQmlTreeModelToTableModel::mapToModel(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + const int row = index.row(); + if (row < 0 || row > m_items.size() - 1) + return QModelIndex(); + + const QModelIndex sourceIndex = m_items.at(row).index; + return m_model->index(sourceIndex.row(), index.column(), sourceIndex.parent()); +} + +QModelIndex QQmlTreeModelToTableModel::mapFromModel(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + + int row = -1; + for (int i = 0; i < m_items.size(); ++i) { + const QModelIndex proxyIndex = m_items[i].index; + if (proxyIndex.row() == index.row() && proxyIndex.parent() == index.parent()) { + row = i; + break; + } + } + + if (row == -1) + return QModelIndex(); + + return this->index(row, index.column()); +} + +QModelIndex QQmlTreeModelToTableModel::mapToModel(int row) const +{ + if (row < 0 || row >= m_items.size()) + return QModelIndex(); + return m_items.at(row).index; +} + +QItemSelection QQmlTreeModelToTableModel::selectionForRowRange(const QModelIndex &fromIndex, const QModelIndex &toIndex) const +{ + int from = itemIndex(fromIndex); + int to = itemIndex(toIndex); + if (from == -1) { + if (to == -1) + return QItemSelection(); + return QItemSelection(toIndex, toIndex); + } + + to = qMax(to, 0); + if (from > to) + qSwap(from, to); + + typedef QPair<QModelIndex, QModelIndex> MIPair; + typedef QHash<QModelIndex, MIPair> MI2MIPairHash; + MI2MIPairHash ranges; + QModelIndex firstIndex = m_items.at(from).index; + QModelIndex lastIndex = firstIndex; + QModelIndex previousParent = firstIndex.parent(); + bool selectLastRow = false; + for (int i = from + 1; i <= to || (selectLastRow = true); i++) { + // We run an extra iteration to make sure the last row is + // added to the selection. (And also to avoid duplicating + // the insertion code.) + QModelIndex index; + QModelIndex parent; + if (!selectLastRow) { + index = m_items.at(i).index; + parent = index.parent(); + } + if (selectLastRow || previousParent != parent) { + const MI2MIPairHash::iterator &it = ranges.find(previousParent); + if (it == ranges.end()) + ranges.insert(previousParent, MIPair(firstIndex, lastIndex)); + else + it->second = lastIndex; + + if (selectLastRow) + break; + + firstIndex = index; + previousParent = parent; + } + lastIndex = index; + } + + QItemSelection sel; + sel.reserve(ranges.size()); + for (const MIPair &pair : std::as_const(ranges)) + sel.append(QItemSelectionRange(pair.first, pair.second)); + + return sel; +} + +void QQmlTreeModelToTableModel::showModelTopLevelItems(bool doInsertRows) +{ + if (!m_model) + return; + + if (m_model->hasChildren(m_rootIndex) && m_model->canFetchMore(m_rootIndex)) + m_model->fetchMore(m_rootIndex); + const long topLevelRowCount = m_model->rowCount(m_rootIndex); + if (topLevelRowCount == 0) + return; + + showModelChildItems(TreeItem(m_rootIndex), 0, topLevelRowCount - 1, doInsertRows); +} + +void QQmlTreeModelToTableModel::showModelChildItems(const TreeItem &parentItem, int start, int end, bool doInsertRows, bool doExpandPendingRows) +{ + const QModelIndex &parentIndex = parentItem.index; + int rowIdx = parentIndex.isValid() && parentIndex != m_rootIndex ? itemIndex(parentIndex) + 1 : 0; + Q_ASSERT(rowIdx == 0 || parentItem.expanded); + if (parentIndex.isValid() && parentIndex != m_rootIndex && (rowIdx == 0 || !parentItem.expanded)) + return; + + if (m_model->rowCount(parentIndex) == 0) { + if (m_model->hasChildren(parentIndex) && m_model->canFetchMore(parentIndex)) + m_model->fetchMore(parentIndex); + return; + } + + int insertCount = end - start + 1; + int startIdx; + if (start == 0) { + startIdx = rowIdx; + } else { + // Prefer to insert before next sibling instead of after last child of previous, as + // the latter is potentially buggy, see QTBUG-66062 + const QModelIndex &nextSiblingIdx = m_model->index(end + 1, 0, parentIndex); + if (nextSiblingIdx.isValid()) { + startIdx = itemIndex(nextSiblingIdx); + } else { + const QModelIndex &prevSiblingIdx = m_model->index(start - 1, 0, parentIndex); + startIdx = lastChildIndex(prevSiblingIdx) + 1; + } + } + + int rowDepth = rowIdx == 0 ? 0 : parentItem.depth + 1; + if (doInsertRows) + beginInsertRows(QModelIndex(), startIdx, startIdx + insertCount - 1); + m_items.reserve(m_items.size() + insertCount); + + for (int i = 0; i < insertCount; i++) { + const QModelIndex &cmi = m_model->index(start + i, 0, parentIndex); + const bool expanded = m_expandedItems.contains(cmi); + const TreeItem treeItem(cmi, rowDepth, expanded); + m_items.insert(startIdx + i, treeItem); + + if (expanded) + m_itemsToExpand.append(treeItem); + } + + if (doInsertRows) + endInsertRows(); + + if (doExpandPendingRows) + expandPendingRows(doInsertRows); +} + + +void QQmlTreeModelToTableModel::expand(const QModelIndex &idx) +{ + ASSERT_CONSISTENCY(); + if (!m_model) + return; + + Q_ASSERT(!idx.isValid() || idx.model() == m_model); + + if (!idx.isValid() || !m_model->hasChildren(idx)) + return; + if (m_expandedItems.contains(idx)) + return; + + int row = itemIndex(idx); + if (row != -1) + expandRow(row); + else + m_expandedItems.insert(idx); + ASSERT_CONSISTENCY(); + + emit expanded(idx); +} + +void QQmlTreeModelToTableModel::collapse(const QModelIndex &idx) +{ + ASSERT_CONSISTENCY(); + if (!m_model) + return; + + Q_ASSERT(!idx.isValid() || idx.model() == m_model); + + if (!idx.isValid() || !m_model->hasChildren(idx)) + return; + if (!m_expandedItems.contains(idx)) + return; + + int row = itemIndex(idx); + if (row != -1) + collapseRow(row); + else + m_expandedItems.remove(idx); + ASSERT_CONSISTENCY(); + + emit collapsed(idx); +} + +bool QQmlTreeModelToTableModel::isExpanded(const QModelIndex &index) const +{ + ASSERT_CONSISTENCY(); + if (!m_model) + return false; + + Q_ASSERT(!index.isValid() || index.model() == m_model); + return !index.isValid() || m_expandedItems.contains(index); +} + +bool QQmlTreeModelToTableModel::isExpanded(int row) const +{ + if (row < 0 || row >= m_items.size()) + return false; + return m_items.at(row).expanded; +} + +bool QQmlTreeModelToTableModel::hasChildren(int row) const +{ + if (row < 0 || row >= m_items.size()) + return false; + return m_model->hasChildren(m_items[row].index); +} + +bool QQmlTreeModelToTableModel::hasSiblings(int row) const +{ + const QModelIndex &index = mapToModel(row); + return index.row() != m_model->rowCount(index.parent()) - 1; +} + +void QQmlTreeModelToTableModel::expandRow(int n) +{ + if (!m_model || isExpanded(n)) + return; + + TreeItem &item = m_items[n]; + if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index)) + return; + item.expanded = true; + m_expandedItems.insert(item.index); + QVector<int> changedRole(1, ExpandedRole); + emit dataChanged(index(n, m_column), index(n, m_column), changedRole); + + m_itemsToExpand.append(item); + expandPendingRows(); +} + +void QQmlTreeModelToTableModel::expandRecursively(int row, int depth) +{ + Q_ASSERT(depth == -1 || depth > 0); + const int startDepth = depthAtRow(row); + + auto expandHelp = [this, depth, startDepth] (const auto expandHelp, const QModelIndex &index) -> void { + const int rowToExpand = itemIndex(index); + if (!m_expandedItems.contains(index)) + expandRow(rowToExpand); + + if (depth != -1 && depthAtRow(rowToExpand) == startDepth + depth - 1) + return; + + const int childCount = m_model->rowCount(index); + for (int childRow = 0; childRow < childCount; ++childRow) { + const QModelIndex childIndex = m_model->index(childRow, 0, index); + if (m_model->hasChildren(childIndex)) + expandHelp(expandHelp, childIndex); + } + }; + + const QModelIndex index = m_items[row].index; + if (index.isValid()) + expandHelp(expandHelp, index); +} + +void QQmlTreeModelToTableModel::expandPendingRows(bool doInsertRows) +{ + while (!m_itemsToExpand.isEmpty()) { + const TreeItem item = m_itemsToExpand.takeFirst(); + Q_ASSERT(item.expanded); + const QModelIndex &index = item.index; + int childrenCount = m_model->rowCount(index); + if (childrenCount == 0) { + if (m_model->hasChildren(index) && m_model->canFetchMore(index)) + m_model->fetchMore(index); + continue; + } + + // TODO Pre-compute the total number of items made visible + // so that we only call a single beginInsertRows()/endInsertRows() + // pair per expansion (same as we do for collapsing). + showModelChildItems(item, 0, childrenCount - 1, doInsertRows, false); + } +} + +void QQmlTreeModelToTableModel::collapseRecursively(int row) +{ + auto collapseHelp = [this] (const auto collapseHelp, const QModelIndex &index) -> void { + if (m_expandedItems.contains(index)) { + const int rowToCollapse = itemIndex(index); + if (rowToCollapse != -1) + collapseRow(rowToCollapse); + else + m_expandedItems.remove(index); + } + + const int childCount = m_model->rowCount(index); + for (int childRow = 0; childRow < childCount; ++childRow) { + const QModelIndex childIndex = m_model->index(childRow, 0, index); + if (m_model->hasChildren(childIndex)) + collapseHelp(collapseHelp, childIndex); + } + }; + + const QModelIndex index = m_items[row].index; + if (index.isValid()) + collapseHelp(collapseHelp, index); +} + +void QQmlTreeModelToTableModel::collapseRow(int n) +{ + if (!m_model || !isExpanded(n)) + return; + + SignalFreezer aggregator(this); + + TreeItem &item = m_items[n]; + item.expanded = false; + m_expandedItems.remove(item.index); + QVector<int> changedRole(1, ExpandedRole); + queueDataChanged(index(n, m_column), index(n, m_column), changedRole); + int childrenCount = m_model->rowCount(item.index); + if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index) || childrenCount == 0) + return; + + const QModelIndex &emi = m_model->index(childrenCount - 1, 0, item.index); + int lastIndex = lastChildIndex(emi); + removeVisibleRows(n + 1, lastIndex); +} + +int QQmlTreeModelToTableModel::lastChildIndex(const QModelIndex &index) const +{ + // The purpose of this function is to return the row of the last decendant of a node N. + // But note: index should point to the last child of N, and not N itself! + // This means that if index is not expanded, the last child will simply be index itself. + // Otherwise, since the tree underneath index can be of any depth, it will instead find + // the first sibling of N, get its table row, and simply return the row above. + if (!m_expandedItems.contains(index)) + return itemIndex(index); + + QModelIndex parent = index.parent(); + QModelIndex nextSiblingIndex; + while (parent.isValid()) { + nextSiblingIndex = parent.sibling(parent.row() + 1, 0); + if (nextSiblingIndex.isValid()) + break; + parent = parent.parent(); + } + + int firstIndex = nextSiblingIndex.isValid() ? itemIndex(nextSiblingIndex) : m_items.size(); + return firstIndex - 1; +} + +void QQmlTreeModelToTableModel::removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows) +{ + if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) + return; + + if (doRemoveRows) + beginRemoveRows(QModelIndex(), startIndex, endIndex); + m_items.erase(m_items.begin() + startIndex, m_items.begin() + endIndex + 1); + if (doRemoveRows) { + endRemoveRows(); + + /* We need to update the model index for all the items below the removed ones */ + int lastIndex = m_items.size() - 1; + if (startIndex <= lastIndex) { + const QModelIndex &topLeft = index(startIndex, 0, QModelIndex()); + const QModelIndex &bottomRight = index(lastIndex, 0, QModelIndex()); + const QVector<int> changedRole(1, ModelIndexRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } +} + +void QQmlTreeModelToTableModel::modelHasBeenDestroyed() +{ + // The model has been deleted. This should behave as if no model was set + clearModelData(); + emit modelChanged(nullptr); +} + +void QQmlTreeModelToTableModel::modelHasBeenReset() +{ + clearModelData(); + + showModelTopLevelItems(); + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) +{ + Q_ASSERT(topLeft.parent() == bottomRight.parent()); + const QModelIndex &parent = topLeft.parent(); + if (parent.isValid() && !childrenVisible(parent)) { + ASSERT_CONSISTENCY(); + return; + } + + int topIndex = itemIndex(topLeft.siblingAtColumn(0)); + if (topIndex == -1) // 'parent' is not visible anymore, though it's been expanded previously + return; + for (int i = topLeft.row(); i <= bottomRight.row(); i++) { + // Group items with same parent to minize the number of 'dataChanged()' emits + int bottomIndex = topIndex; + while (bottomIndex < m_items.size()) { + const QModelIndex &idx = m_items.at(bottomIndex).index; + if (idx.parent() != parent) { + --bottomIndex; + break; + } + if (idx.row() == bottomRight.row()) + break; + ++bottomIndex; + } + emit dataChanged(index(topIndex, topLeft.column()), index(bottomIndex, bottomRight.column()), roles); + + i += bottomIndex - topIndex; + if (i == bottomRight.row()) + break; + topIndex = bottomIndex + 1; + while (topIndex < m_items.size() + && m_items.at(topIndex).index.parent() != parent) + topIndex++; + } + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(hint) + + // Since the m_items is a list of TreeItems that contains QPersistentModelIndexes, we + // cannot wait until we get a modelLayoutChanged() before we remove the affected rows + // from that list. After the layout has changed, the list (or, the persistent indexes + // that it contains) is no longer in sync with the model (after all, that is what we're + // supposed to correct in modelLayoutChanged()). + // This means that vital functions, like itemIndex(index), cannot be trusted at that point. + // Therefore we need to do the update in two steps; First remove all the affected rows + // from here (while we're still in sync with the model), and then add back the + // affected rows, and notify about it, from modelLayoutChanged(). + m_modelLayoutChanged = false; + + if (parents.isEmpty() || !parents[0].isValid()) { + // Update entire model + emit layoutAboutToBeChanged(); + m_modelLayoutChanged = true; + m_items.clear(); + return; + } + + for (const QPersistentModelIndex &pmi : parents) { + if (!m_expandedItems.contains(pmi)) + continue; + const int row = itemIndex(pmi); + if (row == -1) + continue; + const int rowCount = m_model->rowCount(pmi); + if (rowCount == 0) + continue; + + if (!m_modelLayoutChanged) { + emit layoutAboutToBeChanged(); + m_modelLayoutChanged = true; + } + + const QModelIndex &lmi = m_model->index(rowCount - 1, 0, pmi); + const int lastRow = lastChildIndex(lmi); + removeVisibleRows(row + 1, lastRow, false /*doRemoveRows*/); + } + + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(hint) + + if (!m_modelLayoutChanged) { + // No relevant changes done from modelLayoutAboutToBeChanged() + return; + } + + if (m_items.isEmpty()) { + // Entire model has changed. Add back all rows. + showModelTopLevelItems(false /*doInsertRows*/); + const QModelIndex &mi = m_model->index(0, 0); + const int columnCount = m_model->columnCount(mi); + emit dataChanged(index(0, 0), index(m_items.size() - 1, columnCount - 1)); + emit layoutChanged(); + return; + } + + for (const QPersistentModelIndex &pmi : parents) { + if (!m_expandedItems.contains(pmi)) + continue; + const int row = itemIndex(pmi); + if (row == -1) + continue; + const int rowCount = m_model->rowCount(pmi); + if (rowCount == 0) + continue; + + const QModelIndex &lmi = m_model->index(rowCount - 1, 0, pmi); + const int columnCount = m_model->columnCount(lmi); + showModelChildItems(m_items.at(row), 0, rowCount - 1, false /*doInsertRows*/); + const int lastRow = lastChildIndex(lmi); + emit dataChanged(index(row + 1, 0), index(lastRow, columnCount - 1)); + } + + emit layoutChanged(); + + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(parent) + Q_UNUSED(start) + Q_UNUSED(end) + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelRowsInserted(const QModelIndex & parent, int start, int end) +{ + TreeItem item; + int parentRow = itemIndex(parent); + if (parentRow >= 0) { + const QModelIndex& parentIndex = index(parentRow, m_column); + QVector<int> changedRole(1, HasChildrenRole); + queueDataChanged(parentIndex, parentIndex, changedRole); + item = m_items.at(parentRow); + if (!item.expanded) { + ASSERT_CONSISTENCY(); + return; + } + } else if (parent == m_rootIndex) { + item = TreeItem(parent); + } else { + ASSERT_CONSISTENCY(); + return; + } + showModelChildItems(item, start, end); + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) +{ + ASSERT_CONSISTENCY(); + enableSignalAggregation(); + if (parent == m_rootIndex || childrenVisible(parent)) { + const QModelIndex &smi = m_model->index(start, 0, parent); + int startIndex = itemIndex(smi); + const QModelIndex &emi = m_model->index(end, 0, parent); + int endIndex = -1; + if (isExpanded(emi)) { + int rowCount = m_model->rowCount(emi); + if (rowCount > 0) { + const QModelIndex &idx = m_model->index(rowCount - 1, 0, emi); + endIndex = lastChildIndex(idx); + } + } + if (endIndex == -1) + endIndex = itemIndex(emi); + + removeVisibleRows(startIndex, endIndex); + } + + for (int r = start; r <= end; r++) { + const QModelIndex &cmi = m_model->index(r, 0, parent); + m_expandedItems.remove(cmi); + } +} + +void QQmlTreeModelToTableModel::modelRowsRemoved(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(start) + Q_UNUSED(end) + int parentRow = itemIndex(parent); + if (parentRow >= 0) { + const QModelIndex& parentIndex = index(parentRow, m_column); + QVector<int> changedRole(1, HasChildrenRole); + queueDataChanged(parentIndex, parentIndex, changedRole); + } + disableSignalAggregation(); + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) +{ + ASSERT_CONSISTENCY(); + enableSignalAggregation(); + m_visibleRowsMoved = false; + if (!childrenVisible(sourceParent)) + return; // Do nothing now. See modelRowsMoved() below. + + if (!childrenVisible(destinationParent)) { + modelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + /* If the destination parent has no children, we'll need to + * report a change on the HasChildrenRole */ + if (isVisible(destinationParent) && m_model->rowCount(destinationParent) == 0) { + const QModelIndex &topLeft = index(itemIndex(destinationParent), 0, QModelIndex()); + const QModelIndex &bottomRight = topLeft; + const QVector<int> changedRole(1, HasChildrenRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } else { + int depthDifference = -1; + if (destinationParent.isValid()) { + int destParentIndex = itemIndex(destinationParent); + depthDifference = m_items.at(destParentIndex).depth; + } + if (sourceParent.isValid()) { + int sourceParentIndex = itemIndex(sourceParent); + depthDifference -= m_items.at(sourceParentIndex).depth; + } else { + depthDifference++; + } + + int startIndex = itemIndex(m_model->index(sourceStart, 0, sourceParent)); + const QModelIndex &emi = m_model->index(sourceEnd, 0, sourceParent); + int endIndex = -1; + if (isExpanded(emi)) { + int rowCount = m_model->rowCount(emi); + if (rowCount > 0) + endIndex = lastChildIndex(m_model->index(rowCount - 1, 0, emi)); + } + if (endIndex == -1) + endIndex = itemIndex(emi); + + int destIndex = -1; + if (destinationRow == m_model->rowCount(destinationParent)) { + const QModelIndex &emi = m_model->index(destinationRow - 1, 0, destinationParent); + destIndex = lastChildIndex(emi) + 1; + } else { + destIndex = itemIndex(m_model->index(destinationRow, 0, destinationParent)); + } + + int totalMovedCount = endIndex - startIndex + 1; + + /* This beginMoveRows() is matched by a endMoveRows() in the + * modelRowsMoved() method below. */ + m_visibleRowsMoved = startIndex != destIndex && + beginMoveRows(QModelIndex(), startIndex, endIndex, QModelIndex(), destIndex); + + const QList<TreeItem> &buffer = m_items.mid(startIndex, totalMovedCount); + int bufferCopyOffset; + if (destIndex > endIndex) { + for (int i = endIndex + 1; i < destIndex; i++) { + m_items.swapItemsAt(i, i - totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex - totalMovedCount; + } else { + // NOTE: we will not enter this loop if startIndex == destIndex + for (int i = startIndex - 1; i >= destIndex; i--) { + m_items.swapItemsAt(i, i + totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex; + } + for (int i = 0; i < buffer.size(); i++) { + TreeItem item = buffer.at(i); + item.depth += depthDifference; + m_items.replace(bufferCopyOffset + i, item); + } + + /* If both source and destination items are visible, the indexes of + * all the items in between will change. If they share the same + * parent, then this is all; however, if they belong to different + * parents, their bottom siblings will also get displaced, so their + * index also needs to be updated. + * Given that the bottom siblings of the top moved elements are + * already included in the update (since they lie between the + * source and the dest elements), we only need to worry about the + * siblings of the bottom moved element. + */ + const int top = qMin(startIndex, bufferCopyOffset); + int bottom = qMax(endIndex, bufferCopyOffset + totalMovedCount - 1); + if (sourceParent != destinationParent) { + const QModelIndex &bottomParent = + bottom == endIndex ? sourceParent : destinationParent; + + const int rowCount = m_model->rowCount(bottomParent); + if (rowCount > 0) + bottom = qMax(bottom, lastChildIndex(m_model->index(rowCount - 1, 0, bottomParent))); + } + const QModelIndex &topLeft = index(top, 0, QModelIndex()); + const QModelIndex &bottomRight = index(bottom, 0, QModelIndex()); + const QVector<int> changedRole(1, ModelIndexRole); + queueDataChanged(topLeft, bottomRight, changedRole); + + if (depthDifference != 0) { + const QModelIndex &topLeft = index(bufferCopyOffset, 0, QModelIndex()); + const QModelIndex &bottomRight = index(bufferCopyOffset + totalMovedCount - 1, 0, QModelIndex()); + const QVector<int> changedRole(1, DepthRole); + queueDataChanged(topLeft, bottomRight, changedRole); + } + } +} + +void QQmlTreeModelToTableModel::modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) +{ + if (!childrenVisible(sourceParent)) { + modelRowsInserted(destinationParent, destinationRow, destinationRow + sourceEnd - sourceStart); + } else if (!childrenVisible(destinationParent)) { + modelRowsRemoved(sourceParent, sourceStart, sourceEnd); + } + + if (m_visibleRowsMoved) + endMoveRows(); + + if (isVisible(sourceParent) && m_model->rowCount(sourceParent) == 0) { + int parentRow = itemIndex(sourceParent); + collapseRow(parentRow); + const QModelIndex &topLeft = index(parentRow, 0, QModelIndex()); + const QModelIndex &bottomRight = topLeft; + const QVector<int> changedRole { ExpandedRole, HasChildrenRole }; + queueDataChanged(topLeft, bottomRight, changedRole); + } + + disableSignalAggregation(); + + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelColumnsAboutToBeInserted(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(parent); + beginInsertColumns({}, start, end); +} + +void QQmlTreeModelToTableModel::modelColumnsAboutToBeRemoved(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(parent); + beginRemoveColumns({}, start, end); +} + +void QQmlTreeModelToTableModel::modelColumnsInserted(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(parent); + Q_UNUSED(start); + Q_UNUSED(end); + endInsertColumns(); + m_items.clear(); + showModelTopLevelItems(); + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelColumnsRemoved(const QModelIndex & parent, int start, int end) +{ + Q_UNUSED(parent); + Q_UNUSED(start); + Q_UNUSED(end); + endRemoveColumns(); + m_items.clear(); + showModelTopLevelItems(); + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::dump() const +{ + if (!m_model) + return; + int count = m_items.size(); + if (count == 0) + return; + int countWidth = floor(log10(double(count))) + 1; + qInfo() << "Dumping" << this; + for (int i = 0; i < count; i++) { + const TreeItem &item = m_items.at(i); + bool hasChildren = m_model->hasChildren(item.index); + int children = m_model->rowCount(item.index); + qInfo().noquote().nospace() + << QStringLiteral("%1 ").arg(i, countWidth) << QString(4 * item.depth, QChar::fromLatin1('.')) + << QLatin1String(!hasChildren ? ".. " : item.expanded ? " v " : " > ") + << item.index << children; + } +} + +bool QQmlTreeModelToTableModel::testConsistency(bool dumpOnFail) const +{ + if (!m_model) { + if (!m_items.isEmpty()) { + qWarning() << "Model inconsistency: No model but stored visible items"; + return false; + } + if (!m_expandedItems.isEmpty()) { + qWarning() << "Model inconsistency: No model but stored expanded items"; + return false; + } + return true; + } + QModelIndex parent = m_rootIndex; + QStack<QModelIndex> ancestors; + QModelIndex idx = m_model->index(0, 0, parent); + for (int i = 0; i < m_items.size(); i++) { + bool isConsistent = true; + const TreeItem &item = m_items.at(i); + if (item.index != idx) { + qWarning() << "QModelIndex inconsistency" << i << item.index; + qWarning() << " expected" << idx; + isConsistent = false; + } + if (item.index.parent() != parent) { + qWarning() << "Parent inconsistency" << i << item.index; + qWarning() << " stored index parent" << item.index.parent() << "model parent" << parent; + isConsistent = false; + } + if (item.depth != ancestors.size()) { + qWarning() << "Depth inconsistency" << i << item.index; + qWarning() << " item depth" << item.depth << "ancestors stack" << ancestors.size(); + isConsistent = false; + } + if (item.expanded && !m_expandedItems.contains(item.index)) { + qWarning() << "Expanded inconsistency" << i << item.index; + qWarning() << " set" << m_expandedItems.contains(item.index) << "item" << item.expanded; + isConsistent = false; + } + if (!isConsistent) { + if (dumpOnFail) + dump(); + return false; + } + QModelIndex firstChildIndex; + if (item.expanded) + firstChildIndex = m_model->index(0, 0, idx); + if (firstChildIndex.isValid()) { + ancestors.push(parent); + parent = idx; + idx = m_model->index(0, 0, parent); + } else { + while (idx.row() == m_model->rowCount(parent) - 1) { + if (ancestors.isEmpty()) + break; + idx = parent; + parent = ancestors.pop(); + } + idx = m_model->index(idx.row() + 1, 0, parent); + } + } + + return true; +} + +void QQmlTreeModelToTableModel::enableSignalAggregation() { + m_signalAggregatorStack++; +} + +void QQmlTreeModelToTableModel::disableSignalAggregation() { + m_signalAggregatorStack--; + Q_ASSERT(m_signalAggregatorStack >= 0); + if (m_signalAggregatorStack == 0) { + emitQueuedSignals(); + } +} + +void QQmlTreeModelToTableModel::queueDataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector<int> &roles) +{ + if (isAggregatingSignals()) { + m_queuedDataChanged.append(DataChangedParams { topLeft, bottomRight, roles }); + } else { + emit dataChanged(topLeft, bottomRight, roles); + } +} + +void QQmlTreeModelToTableModel::emitQueuedSignals() +{ + QVector<DataChangedParams> combinedUpdates; + /* First, iterate through the queued updates and merge the overlapping ones + * to reduce the number of updates. + * We don't merge adjacent updates, because they are typically filed with a + * different role (a parent row is next to its children). + */ + for (const DataChangedParams &dataChange : std::as_const(m_queuedDataChanged)) { + int startRow = dataChange.topLeft.row(); + int endRow = dataChange.bottomRight.row(); + bool merged = false; + for (DataChangedParams &combined : combinedUpdates) { + int combinedStartRow = combined.topLeft.row(); + int combinedEndRow = combined.bottomRight.row(); + if ((startRow <= combinedStartRow && endRow >= combinedStartRow) || + (startRow <= combinedEndRow && endRow >= combinedEndRow)) { + if (startRow < combinedStartRow) { + combined.topLeft = dataChange.topLeft; + } + if (endRow > combinedEndRow) { + combined.bottomRight = dataChange.bottomRight; + } + for (int role : dataChange.roles) { + if (!combined.roles.contains(role)) + combined.roles.append(role); + } + merged = true; + break; + } + } + if (!merged) { + combinedUpdates.append(dataChange); + } + } + + /* Finally, emit the dataChanged signals */ + for (const DataChangedParams &dataChange : combinedUpdates) { + emit dataChanged(dataChange.topLeft, dataChange.bottomRight, dataChange.roles); + } + m_queuedDataChanged.clear(); +} + +QT_END_NAMESPACE + +#include "moc_qqmltreemodeltotablemodel_p_p.cpp" |