diff --git a/src/qmlmodels/CMakeLists.txt b/src/qmlmodels/CMakeLists.txt index b29dd86d8a..ccee022672 100644 --- a/src/qmlmodels/CMakeLists.txt +++ b/src/qmlmodels/CMakeLists.txt @@ -39,6 +39,7 @@ qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_object_model qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_table_model SOURCES qqmltableinstancemodel.cpp qqmltableinstancemodel_p.h + qqmltreemodeltotablemodel.cpp qqmltreemodeltotablemodel_p_p.h ) qt_internal_extend_target(QmlModels CONDITION QT_FEATURE_qml_list_model diff --git a/src/qmlmodels/qqmltreemodeltotablemodel.cpp b/src/qmlmodels/qqmltreemodeltotablemodel.cpp new file mode 100644 index 0000000000..a5fdfa0a7d --- /dev/null +++ b/src/qmlmodels/qqmltreemodeltotablemodel.cpp @@ -0,0 +1,1051 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include + +#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::setModel(QAbstractItemModel *arg) +{ + struct Cx { + const char *signal; + const char *slot; + }; + static const Cx connections[] = { + { SIGNAL(destroyed(QObject*)), + SLOT(modelHasBeenDestroyed()) }, + { SIGNAL(modelReset()), + SLOT(modelHasBeenReset()) }, + { SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector&)), + SLOT(modelDataChanged(const QModelIndex&, const QModelIndex&, const QVector&)) }, + + { SIGNAL(layoutAboutToBeChanged(const QList&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutAboutToBeChanged(const QList&, QAbstractItemModel::LayoutChangeHint)) }, + { SIGNAL(layoutChanged(const QList&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutChanged(const QList&, QAbstractItemModel::LayoutChangeHint)) }, + + { SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)), + SLOT(modelRowsAboutToBeInserted(const QModelIndex &, int, int)) }, + { SIGNAL(rowsInserted(const QModelIndex&, int, int)), + SLOT(modelRowsInserted(const QModelIndex&, int, int)) }, + { SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), + SLOT(modelRowsAboutToBeRemoved(const QModelIndex&, int, int)) }, + { SIGNAL(rowsRemoved(const QModelIndex&, int, int)), + SLOT(modelRowsRemoved(const QModelIndex&, int, int)) }, + { SIGNAL(rowsAboutToBeMoved(const QModelIndex&, int, int, const QModelIndex&, int)), + SLOT(modelRowsAboutToBeMoved(const QModelIndex&, int, int, const QModelIndex&, int)) }, + { SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), + SLOT(modelRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)) }, + { nullptr, nullptr } + }; + + if (m_model != arg) { + if (m_model) { + for (const Cx *c = &connections[0]; c->signal; c++) + disconnect(m_model, c->signal, this, c->slot); + } + + clearModelData(); + m_model = arg; + + if (m_model) { + for (const Cx *c = &connections[0]; c->signal; c++) + connect(m_model, c->signal, this, c->slot); + + 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 QQmlTreeModelToTableModel::roleNames() const +{ + if (!m_model) + return QHash(); + return m_model->roleNames(); +} + +int QQmlTreeModelToTableModel::rowCount(const QModelIndex &) const +{ + if (!m_model) + return 0; + return m_items.count(); +} + +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); +} + +int QQmlTreeModelToTableModel::depthAtRow(int row) const +{ + if (row < 0 || row >= m_items.count()) + 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.count(); + + // 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.count() - 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.count(); ++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.count()) + 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 MIPair; + typedef QHash 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.count()); + for (const MIPair &pair : qAsConst(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.count() + 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.count()) + return false; + return m_items.at(row).expanded; +} + +bool QQmlTreeModelToTableModel::hasChildren(int row) const +{ + if (row < 0 || row >= m_items.count()) + 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 changedRole(1, ExpandedRole); + emit dataChanged(index(n, m_column), index(n, m_column), changedRole); + + m_itemsToExpand.append(item); + expandPendingRows(); +} + +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::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 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) +{ + 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.count(); + 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.count() - 1; + if (startIndex <= lastIndex) { + const QModelIndex &topLeft = index(startIndex, 0, QModelIndex()); + const QModelIndex &bottomRight = index(lastIndex, 0, QModelIndex()); + const QVector 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 &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.count()) { + 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.count() + && m_items.at(topIndex).index.parent() != parent) + topIndex++; + } + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::modelLayoutAboutToBeChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + ASSERT_CONSISTENCY(); + Q_UNUSED(parents) + Q_UNUSED(hint) +} + +void QQmlTreeModelToTableModel::modelLayoutChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(hint) + if (parents.isEmpty()) { + m_items.clear(); + 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.count() - 1, columnCount - 1)); + } + + for (const QPersistentModelIndex &pmi : parents) { + if (m_expandedItems.contains(pmi)) { + int row = itemIndex(pmi); + if (row != -1) { + int rowCount = m_model->rowCount(pmi); + if (rowCount > 0) { + const QModelIndex &lmi = m_model->index(rowCount - 1, 0, pmi); + const int lastRow = lastChildIndex(lmi); + const int columnCount = m_model->columnCount(lmi); + removeVisibleRows(row + 1, lastRow, false /*doRemoveRows*/); + showModelChildItems(m_items.at(row), 0, rowCount - 1, false /*doInsertRows*/); + emit dataChanged(index(row + 1, 0), index(lastRow, columnCount - 1)); + } + } + } + } + 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 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 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 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 &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.length(); 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 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 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 changedRole { ExpandedRole, HasChildrenRole }; + queueDataChanged(topLeft, bottomRight, changedRole); + } + + disableSignalAggregation(); + + ASSERT_CONSISTENCY(); +} + +void QQmlTreeModelToTableModel::dump() const +{ + if (!m_model) + return; + int count = m_items.count(); + 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 ancestors; + QModelIndex idx = m_model->index(0, 0, parent); + for (int i = 0; i < m_items.count(); 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.count()) { + qWarning() << "Depth inconsistency" << i << item.index; + qWarning() << " item depth" << item.depth << "ancestors stack" << ancestors.count(); + 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 &roles) +{ + if (isAggregatingSignals()) { + m_queuedDataChanged.append(DataChangedParams { topLeft, bottomRight, roles }); + } else { + emit dataChanged(topLeft, bottomRight, roles); + } +} + +void QQmlTreeModelToTableModel::emitQueuedSignals() +{ + QVector 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 : qAsConst(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 diff --git a/src/qmlmodels/qqmltreemodeltotablemodel_p_p.h b/src/qmlmodels/qqmltreemodeltotablemodel_p_p.h new file mode 100644 index 0000000000..a772b980e3 --- /dev/null +++ b/src/qmlmodels/qqmltreemodeltotablemodel_p_p.h @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQmlTreeModelToTableModel_H +#define QQmlTreeModelToTableModel_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qtqmlmodelsglobal_p.h" + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QAbstractItemModel; + +class Q_QMLMODELS_PRIVATE_EXPORT QQmlTreeModelToTableModel : public QAbstractItemModel +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QModelIndex rootIndex READ rootIndex WRITE setRootIndex RESET resetRootIndex NOTIFY rootIndexChanged) + + struct TreeItem; + +public: + explicit QQmlTreeModelToTableModel(QObject *parent = nullptr); + + QAbstractItemModel *model() const; + QModelIndex rootIndex() const; + void setRootIndex(const QModelIndex &idx); + void resetRootIndex(); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + + enum { + DepthRole = Qt::UserRole - 5, + ExpandedRole, + HasChildrenRole, + HasSiblingRole, + ModelIndexRole + }; + + QHash roleNames() const override; + QVariant data(const QModelIndex &, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + void clearModelData(); + + bool isVisible(const QModelIndex &index); + bool childrenVisible(const QModelIndex &index); + + QModelIndex mapToModel(const QModelIndex &index) const; + QModelIndex mapFromModel(const QModelIndex &index) const; + QModelIndex mapToModel(int row) const; + + Q_INVOKABLE QItemSelection selectionForRowRange(const QModelIndex &fromIndex, const QModelIndex &toIndex) const; + + void showModelTopLevelItems(bool doInsertRows = true); + void showModelChildItems(const TreeItem &parent, int start, int end, bool doInsertRows = true, bool doExpandPendingRows = true); + + int itemIndex(const QModelIndex &index) const; + void expandPendingRows(bool doInsertRows = true); + int lastChildIndex(const QModelIndex &index); + void removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows = true); + + void dump() const; + bool testConsistency(bool dumpOnFail = false) const; + + using QAbstractItemModel::hasChildren; + +signals: + void modelChanged(QAbstractItemModel *model); + void rootIndexChanged(); + void expanded(const QModelIndex &index); + void collapsed(const QModelIndex &index); + +public slots: + void expand(const QModelIndex &); + void collapse(const QModelIndex &); + void setModel(QAbstractItemModel *model); + bool isExpanded(const QModelIndex &) const; + bool isExpanded(int row) const; + bool hasChildren(int row) const; + bool hasSiblings(int row) const; + int depthAtRow(int row) const; + void expandRow(int n); + void collapseRow(int n); + +private slots: + void modelHasBeenDestroyed(); + void modelHasBeenReset(); + void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void modelLayoutAboutToBeChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelLayoutChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end); + void modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow); + void modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end); + void modelRowsInserted(const QModelIndex & parent, int start, int end); + void modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow); + void modelRowsRemoved(const QModelIndex & parent, int start, int end); + +private: + struct TreeItem { + QPersistentModelIndex index; + int depth; + bool expanded; + + explicit TreeItem(const QModelIndex &idx = QModelIndex(), int d = 0, int e = false) + : index(idx), depth(d), expanded(e) + { } + + inline bool operator== (const TreeItem &other) const + { + return this->index == other.index; + } + }; + + struct DataChangedParams { + QModelIndex topLeft; + QModelIndex bottomRight; + QVector roles; + }; + + struct SignalFreezer { + SignalFreezer(QQmlTreeModelToTableModel *parent) : m_parent(parent) { + m_parent->enableSignalAggregation(); + } + ~SignalFreezer() { m_parent->disableSignalAggregation(); } + + private: + QQmlTreeModelToTableModel *m_parent; + }; + + void enableSignalAggregation(); + void disableSignalAggregation(); + bool isAggregatingSignals() const { return m_signalAggregatorStack > 0; } + void queueDataChanged(const QModelIndex &topLeft, + const QModelIndex &bottomRight, + const QVector &roles); + void emitQueuedSignals(); + + QPointer m_model = nullptr; + QPersistentModelIndex m_rootIndex; + QList m_items; + QSet m_expandedItems; + QList m_itemsToExpand; + mutable int m_lastItemIndex = 0; + bool m_visibleRowsMoved = false; + int m_signalAggregatorStack = 0; + QVector m_queuedDataChanged; + int m_column = 0; +}; + +QT_END_NAMESPACE + +#endif // QQmlTreeModelToTableModel_H diff --git a/tests/auto/qml/CMakeLists.txt b/tests/auto/qml/CMakeLists.txt index 51f824c49d..ad2551756c 100644 --- a/tests/auto/qml/CMakeLists.txt +++ b/tests/auto/qml/CMakeLists.txt @@ -109,6 +109,7 @@ if(QT_FEATURE_private_tests) add_subdirectory(qqmlimport) add_subdirectory(qqmlobjectmodel) add_subdirectory(qqmltablemodel) + add_subdirectory(qqmltreemodeltotablemodel) add_subdirectory(qv4assembler) add_subdirectory(qv4mm) add_subdirectory(qv4identifiertable) diff --git a/tests/auto/qml/qqmltreemodeltotablemodel/CMakeLists.txt b/tests/auto/qml/qqmltreemodeltotablemodel/CMakeLists.txt new file mode 100644 index 0000000000..34ff903873 --- /dev/null +++ b/tests/auto/qml/qqmltreemodeltotablemodel/CMakeLists.txt @@ -0,0 +1,36 @@ +##################################################################### +## tst_qqmltreemodeltotablemodel Test: +##################################################################### + +# Collect test data +file(GLOB_RECURSE test_data_glob + RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} + data/*) +list(APPEND test_data ${test_data_glob}) + +qt_internal_add_test(tst_qqmltreemodeltotablemodel + SOURCES + tst_qqmltreemodeltotablemodel.cpp + testmodel.h testmodel.cpp + PUBLIC_LIBRARIES + Qt::Gui + Qt::Qml + Qt::QmlPrivate + Qt::Quick + Qt::QuickPrivate + Qt::QuickTestUtilsPrivate + TESTDATA ${test_data} +) + +## Scopes: +##################################################################### + +qt_internal_extend_target(tst_qqmltreemodeltotablemodel CONDITION ANDROID OR IOS + DEFINES + QT_QMLTEST_DATADIR=\\\":/data\\\" +) + +qt_internal_extend_target(tst_qqmltreemodeltotablemodel CONDITION NOT ANDROID AND NOT IOS + DEFINES + QT_QMLTEST_DATADIR=\\\"${CMAKE_CURRENT_SOURCE_DIR}/data\\\" +) diff --git a/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.cpp b/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.cpp new file mode 100644 index 0000000000..85d39298aa --- /dev/null +++ b/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.cpp @@ -0,0 +1,167 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "testmodel.h" + +TreeItem::TreeItem(TreeItem *parent) + : m_parentItem(parent) +{} + +TreeItem::~TreeItem() +{ + qDeleteAll(m_childItems); +} + +int TreeItem::row() const +{ + if (!m_parentItem) + return 0; + return m_parentItem->m_childItems.indexOf(const_cast(this)); +} + +TestModel::TestModel(QObject *parent) + : QAbstractItemModel(parent) +{ + m_rootItem.reset(new TreeItem()); + for (int col = 0; col < m_columnCount; ++col) + m_rootItem.data()->m_entries << QVariant(QString("0, %1").arg(col)); + createTreeRecursive(m_rootItem.data(), 4, 0, 4); +} + +void TestModel::createTreeRecursive(TreeItem *item, int childCount, int currentDepth, int maxDepth) +{ + for (int row = 0; row < childCount; ++row) { + auto childItem = new TreeItem(item); + for (int col = 0; col < m_columnCount; ++col) + childItem->m_entries << QVariant(QString("%1, %2").arg(row).arg(col)); + item->m_childItems.append(childItem); + if (currentDepth < maxDepth && row == childCount - 1) + createTreeRecursive(childItem, childCount, currentDepth + 1, maxDepth); + } +} + +TreeItem *TestModel::treeItem(const QModelIndex &index) const +{ + if (!index.isValid()) + return m_rootItem.data(); + return static_cast(index.internalPointer()); +} + +int TestModel::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) + return 1; // root of the tree + if (parent.column() == 0) + return treeItem(parent)->m_childItems.count(); + + return 0; +} + +int TestModel::columnCount(const QModelIndex &parent) const +{ + return parent.column() == 0 ? m_columnCount : 0; +} + +QVariant TestModel::data(const QModelIndex &index, int role) const +{ + Q_UNUSED(role) + if (!index.isValid()) + return QVariant(); + TreeItem *item = treeItem(TestModel::index(index.row(), 0, index.parent())); + return item->m_entries.at(index.column()); +} + +bool TestModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + Q_UNUSED(role) + if (!index.isValid()) + return false; + TreeItem *item = treeItem(TestModel::index(index.row(), 0, index.parent())); + item->m_entries[index.column()] = value; + emit dataChanged(index, index); + return true; +} + +QModelIndex TestModel::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(column) + if (!hasIndex(row, column, parent)) + return QModelIndex(); + if (column != 0) + return createIndex(row, column); + if (!parent.isValid()) + return createIndex(0, 0, m_rootItem.data()); + + return createIndex(row, 0, treeItem(parent)->m_childItems.at(row)); +} + +QModelIndex TestModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return QModelIndex(); + if (index.column() != 0) + return QModelIndex(); + TreeItem *parentItem = treeItem(index)->m_parentItem; + if (!parentItem) { + qWarning() << "failed to resolve parent item for:" << index; + return QModelIndex(); + } + return createIndex(parentItem->row(), 0, parentItem); +} + +bool TestModel::insertRows(int position, int rows, const QModelIndex &parent) +{ + if (!parent.isValid()) { + qWarning() << "Cannot insert rows on an invalid parent!"; + return false; + } + + beginInsertRows(parent, position, position + rows - 1); + TreeItem *parentItem = treeItem(parent); + + for (int row = 0; row < rows; ++row) { + auto newChildItem = new TreeItem(parentItem); + for (int col = 0; col < m_columnCount; ++col) + newChildItem->m_entries << QVariant(QString("%1, %2 (inserted)").arg(position + row).arg(col)); + parentItem->m_childItems.insert(position + row, newChildItem); + } + + endInsertRows(); + return true; +} diff --git a/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.h b/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.h new file mode 100644 index 0000000000..4d9006d112 --- /dev/null +++ b/tests/auto/qml/qqmltreemodeltotablemodel/testmodel.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef TESTMODEL_H +#define TESTMODEL_H + +#include +#include + +class TreeItem +{ +public: + explicit TreeItem(TreeItem *parent = nullptr); + ~TreeItem(); + + int row() const; + QVector m_childItems; + TreeItem *m_parentItem; + QVector m_entries; +}; + +// ######################################################## + +class TestModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit TestModel(QObject *parent = nullptr); + + void createTreeRecursive(TreeItem *item, int childCount, int currentDepth, int maxDepth); + TreeItem *treeItem(const QModelIndex &index) const; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + + bool insertRows(int position, int rows, const QModelIndex &parent) override; + +private: + QScopedPointer m_rootItem; + int m_columnCount = 2; +}; + +#endif // TESTMODEL_H diff --git a/tests/auto/qml/qqmltreemodeltotablemodel/tst_qqmltreemodeltotablemodel.cpp b/tests/auto/qml/qqmltreemodeltotablemodel/tst_qqmltreemodeltotablemodel.cpp new file mode 100644 index 0000000000..9b4b8733c9 --- /dev/null +++ b/tests/auto/qml/qqmltreemodeltotablemodel/tst_qqmltreemodeltotablemodel.cpp @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include + +#include + +#include "testmodel.h" + +class tst_QQmlTreeModelToTableModel : public QObject { + Q_OBJECT + +private slots: + void testTestModel(); + void testTreeModelToTableModel(); +}; + +void tst_QQmlTreeModelToTableModel::testTestModel() +{ + TestModel treeModel; + QAbstractItemModelTester tester(&treeModel, QAbstractItemModelTester::FailureReportingMode::QtTest); +} + +void tst_QQmlTreeModelToTableModel::testTreeModelToTableModel() +{ + QQmlTreeModelToTableModel model; + TestModel treeModel; + model.setModel(&treeModel); + QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest); +} + +QTEST_MAIN(tst_QQmlTreeModelToTableModel) + +#include "tst_qqmltreemodeltotablemodel.moc"