diff options
30 files changed, 3578 insertions, 125 deletions
diff --git a/examples/quick/controls/filesystembrowser/deployment.pri b/examples/quick/controls/filesystembrowser/deployment.pri new file mode 100644 index 000000000..5441b63dc --- /dev/null +++ b/examples/quick/controls/filesystembrowser/deployment.pri @@ -0,0 +1,27 @@ +android-no-sdk { + target.path = /data/user/qt + export(target.path) + INSTALLS += target +} else:android { + x86 { + target.path = /libs/x86 + } else: armeabi-v7a { + target.path = /libs/armeabi-v7a + } else { + target.path = /libs/armeabi + } + export(target.path) + INSTALLS += target +} else:unix { + isEmpty(target.path) { + qnx { + target.path = /tmp/$${TARGET}/bin + } else { + target.path = /opt/$${TARGET}/bin + } + export(target.path) + } + INSTALLS += target +} + +export(INSTALLS) diff --git a/examples/quick/controls/filesystembrowser/filesystembrowser.pro b/examples/quick/controls/filesystembrowser/filesystembrowser.pro new file mode 100644 index 000000000..1cdc565af --- /dev/null +++ b/examples/quick/controls/filesystembrowser/filesystembrowser.pro @@ -0,0 +1,13 @@ +TEMPLATE = app + +QT += qml quick widgets + +SOURCES += main.cpp + +RESOURCES += qml.qrc + +# Additional import path used to resolve QML modules in Qt Creator's code model +QML_IMPORT_PATH = + +# Default rules for deployment. +include(deployment.pri) diff --git a/examples/quick/controls/filesystembrowser/main.cpp b/examples/quick/controls/filesystembrowser/main.cpp new file mode 100644 index 000000000..1941d129e --- /dev/null +++ b/examples/quick/controls/filesystembrowser/main.cpp @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd. nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QApplication> +#include <QQmlApplicationEngine> +#include <QtQml> +#include <QFileSystemModel> + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QQmlApplicationEngine engine; + QFileSystemModel *fsm = new QFileSystemModel(&engine); + fsm->setRootPath(QDir::homePath()); + fsm->setResolveSymlinks(true); + engine.rootContext()->setContextProperty("fileSystemModel", fsm); + engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); + + return app.exec(); +} diff --git a/examples/quick/controls/filesystembrowser/main.qml b/examples/quick/controls/filesystembrowser/main.qml new file mode 100644 index 000000000..4874f7d3e --- /dev/null +++ b/examples/quick/controls/filesystembrowser/main.qml @@ -0,0 +1,115 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd. nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQml.Models 2.2 + +ApplicationWindow { + visible: true + width: 640 + height: 480 + title: qsTr("File System") + + menuBar: MenuBar { + Menu { + title: qsTr("File") + MenuItem { + text: qsTr("Exit") + onTriggered: Qt.quit(); + } + } + } + + Row { + id: row + anchors.top: parent.top + anchors.topMargin: 12 + anchors.horizontalCenter: parent.horizontalCenter + + ExclusiveGroup { + id: eg + } + + Repeater { + model: [ "None", "Single", "Extended", "Multi", "Contig."] + Button { + text: modelData + exclusiveGroup: eg + checkable: true + checked: index === 1 + onClicked: view.selectionMode = index + } + } + } + + TreeView { + id: view + anchors.fill: parent + anchors.margins: 2 * 12 + row.height + model: fileSystemModel + + selection: ItemSelectionModel { + id: s + model: fileSystemModel + onSelectionChanged: { + console.log("selected", selected) + console.log("deselected", deselected) + console.log("selection", s.selection()) + } + onCurrentChanged: console.log("current", current) + } + + onCurrentIndexChanged: console.log("current index", currentIndex) + + TableViewColumn { + title: "Name" + role: "fileName" + } + +// TableViewColumn { +// title: "Permissions" +// role: "filePermissions" +// } + + onClicked: console.log("clicked", index) + onDoubleClicked: isExpanded(index) ? collapse(index) : expand(index) + } +} diff --git a/examples/quick/controls/filesystembrowser/qml.qrc b/examples/quick/controls/filesystembrowser/qml.qrc new file mode 100644 index 000000000..5f6483ac3 --- /dev/null +++ b/examples/quick/controls/filesystembrowser/qml.qrc @@ -0,0 +1,5 @@ +<RCC> + <qresource prefix="/"> + <file>main.qml</file> + </qresource> +</RCC> diff --git a/src/controls/Private/BasicTableView.qml b/src/controls/Private/BasicTableView.qml index cac673eb9..f5fa3238b 100644 --- a/src/controls/Private/BasicTableView.qml +++ b/src/controls/Private/BasicTableView.qml @@ -543,6 +543,7 @@ ScrollView { property bool itemSelected: __mouseArea.selected(rowIndex) property bool alternate: alternatingRowColors && rowIndex % 2 === 1 readonly property color itemTextColor: itemSelected ? __style.highlightedTextColor : __style.textColor + property Item branchDecoration: null width: itemrow.width height: rowstyle.height @@ -608,6 +609,37 @@ ScrollView { : modelData && modelData.hasOwnProperty(role) ? modelData[role] // QObjectList / QObject : modelData != undefined ? modelData : "" // Models without role + readonly property int depth: itemModel && column === 0 && itemModel["_q_TreeView_ItemDepth"] || 0 + readonly property bool hasChildren: itemModel && itemModel["_q_TreeView_HasChildren"] || false + readonly property bool hasSibling: itemModel && itemModel["_q_TreeView_HasSibling"] || false + readonly property bool isExpanded: itemModel && itemModel["_q_TreeView_ItemExpanded"] || false + } + + readonly property int __itemIndentation: __style.__indentation * (styleData.depth + 1) + + Binding { + target: item + property: "x" + value: __itemIndentation + } + + Binding { + target: item + property: "width" + value: itemDelegateLoader.width - __itemIndentation + } + + Loader { + id: branchDelegateLoader + active: rowitem.itemModel !== undefined + && index === 0 + && itemDelegateLoader.width > __itemIndentation + && styleData.hasChildren + sourceComponent: __style ? __style.__branchDelegate : null + anchors.right: parent.item.left + anchors.verticalCenter: parent.verticalCenter + property QtObject styleData: itemDelegateLoader.styleData + onLoaded: rowitem.branchDecoration = item } } } diff --git a/src/controls/Private/private.pri b/src/controls/Private/private.pri index 3d6a71624..7d23fc479 100644 --- a/src/controls/Private/private.pri +++ b/src/controls/Private/private.pri @@ -9,7 +9,8 @@ HEADERS += \ $$PWD/qquickwheelarea_p.h \ $$PWD/qquickabstractstyle_p.h \ $$PWD/qquickpadding_p.h \ - $$PWD/qquickcontrolsprivate_p.h + $$PWD/qquickcontrolsprivate_p.h \ + $$PWD/qquicktreemodeladaptor_p.h SOURCES += \ $$PWD/qquickcalendarmodel.cpp \ @@ -19,7 +20,8 @@ SOURCES += \ $$PWD/qquickrangeddate.cpp \ $$PWD/qquickcontrolsettings.cpp \ $$PWD/qquickwheelarea.cpp \ - $$PWD/qquickabstractstyle.cpp + $$PWD/qquickabstractstyle.cpp \ + $$PWD/qquicktreemodeladaptor.cpp !no_desktop { diff --git a/src/controls/Private/qquickstyleitem.cpp b/src/controls/Private/qquickstyleitem.cpp index fb49e6e3f..1e10176b2 100644 --- a/src/controls/Private/qquickstyleitem.cpp +++ b/src/controls/Private/qquickstyleitem.cpp @@ -367,6 +367,19 @@ void QQuickStyleItem::initStyleOption() } } break; + case ItemBranchIndicator: { + if (!m_styleoption) + m_styleoption = new QStyleOption; + + m_styleoption->state = QStyle::State_Item; // We don't want to fully support Win 95 + if (m_properties.value("hasChildren").toBool()) + m_styleoption->state |= QStyle::State_Children; + if (m_properties.value("hasSibling").toBool()) // Even this one could go away + m_styleoption->state |= QStyle::State_Sibling; + if (m_on) + m_styleoption->state |= QStyle::State_Open; + } + break; case Header: { if (!m_styleoption) m_styleoption = new QStyleOptionHeader(); @@ -1220,6 +1233,8 @@ int QQuickStyleItem::pixelMetric(const QString &metric) return qApp->style()->pixelMetric(QStyle::PM_SplitterWidth, 0 ); else if (metric == "scrollbarspacing") return abs(qApp->style()->pixelMetric(QStyle::PM_ScrollView_ScrollBarSpacing, 0 )); + else if (metric == "treeviewindentation") + return qApp->style()->pixelMetric(QStyle::PM_TreeViewIndentation, 0 ); return 0; } @@ -1306,6 +1321,8 @@ void QQuickStyleItem::setElementType(const QString &str) } else { m_itemType = (str == "item") ? Item : ItemRow; } + } else if (str == "itembranchindicator") { + m_itemType = ItemBranchIndicator; } else if (str == "groupbox") { m_itemType = GroupBox; } else if (str == "tab") { @@ -1419,6 +1436,11 @@ QRectF QQuickStyleItem::subControlRect(const QString &subcontrolString) subcontrol); } break; + case ItemBranchIndicator: { + QStyleOption opt; + opt.rect = QRect(0, 0, implicitWidth(), implicitHeight()); + return qApp->style()->subElementRect(QStyle::SE_TreeViewDisclosureItem, &opt, 0); + } default: break; } @@ -1499,6 +1521,9 @@ void QQuickStyleItem::paint(QPainter *painter) case Item: qApp->style()->drawControl(QStyle::CE_ItemViewItem, m_styleoption, painter); break; + case ItemBranchIndicator: + qApp->style()->drawPrimitive(QStyle::PE_IndicatorBranch, m_styleoption, painter); + break; case Header: qApp->style()->drawControl(QStyle::CE_Header, m_styleoption, painter); break; diff --git a/src/controls/Private/qquickstyleitem_p.h b/src/controls/Private/qquickstyleitem_p.h index 10a4e5702..c52157fd5 100644 --- a/src/controls/Private/qquickstyleitem_p.h +++ b/src/controls/Private/qquickstyleitem_p.h @@ -119,6 +119,7 @@ public: Header, Item, ItemRow, + ItemBranchIndicator, Splitter, Menu, MenuItem, diff --git a/src/controls/Private/qquicktreemodeladaptor.cpp b/src/controls/Private/qquicktreemodeladaptor.cpp new file mode 100644 index 000000000..6ea2e4591 --- /dev/null +++ b/src/controls/Private/qquicktreemodeladaptor.cpp @@ -0,0 +1,799 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <math.h> +#include "qquicktreemodeladaptor_p.h" +#include <QtCore/qstack.h> +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +//#define QQUICKTREEMODELADAPTOR_DEBUG +#ifndef QQUICKTREEMODELADAPTOR_DEBUG +# undef qDebug +# define qDebug QT_NO_QDEBUG_MACRO +#elif !defined(QT_TESTLIB_LIB) +# define ASSERT_CONSISTENCY() Q_ASSERT_X(testConsistency(true /* dumpOnFail */), Q_FUNC_INFO, "Consistency test failed") +#endif +#ifndef ASSERT_CONSISTENCY +# define ASSERT_CONSISTENCY qt_noop +#endif + +QQuickTreeModelAdaptor::QQuickTreeModelAdaptor(QObject *parent) : + QAbstractListModel(parent), m_model(0), m_lastItemIndex(0) +{ +} + +QAbstractItemModel *QQuickTreeModelAdaptor::model() const +{ + return m_model; +} + +void QQuickTreeModelAdaptor::setModel(QAbstractItemModel *arg) +{ + struct Cx { + const char *signal; + const char *slot; + }; + static const Cx connections[] = { + { SIGNAL(modelReset()), + SLOT(modelHasBeenReset()) }, + { SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)), + SLOT(modelDataChanged(const QModelIndex&, const QModelIndex&, const QVector<int>&)) }, + + { SIGNAL(layoutAboutToBeChanged(const QList<QPersistentModelIndex>&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex>&, QAbstractItemModel::LayoutChangeHint)) }, + { SIGNAL(layoutChanged(const QList<QPersistentModelIndex>&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutChanged(const QList<QPersistentModelIndex>&, 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)) }, + { 0, 0 } + }; + + 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 QQuickTreeModelAdaptor::clearModelData() +{ + beginResetModel(); + m_items.clear(); + m_expandedItems.clear(); + endResetModel(); +} + +QHash<int, QByteArray> QQuickTreeModelAdaptor::roleNames() const +{ + if (!m_model) + return QHash<int, QByteArray>(); + + QHash<int, QByteArray> modelRoleNames = m_model->roleNames(); + modelRoleNames.insert(DepthRole, "_q_TreeView_ItemDepth"); + modelRoleNames.insert(ExpandedRole, "_q_TreeView_ItemExpanded"); + modelRoleNames.insert(HasChildrenRole, "_q_TreeView_HasChildren"); + modelRoleNames.insert(HasSiblingRole, "_q_TreeView_HasSibling"); + return modelRoleNames; +} + +int QQuickTreeModelAdaptor::rowCount(const QModelIndex &) const +{ + return m_items.count(); +} + +QVariant QQuickTreeModelAdaptor::data(const QModelIndex &index, int role) const +{ + if (!m_model) + return QVariant(); + + const QModelIndex &modelIndex = mapToModel(index); + + switch (role) { + case DepthRole: + return m_items.at(index.row()).depth; + case ExpandedRole: + return isExpanded(index.row()); + case HasChildrenRole: + return !(modelIndex.flags() & Qt::ItemNeverHasChildren) && m_model->hasChildren(modelIndex); + case HasSiblingRole: + return modelIndex.row() != m_model->rowCount(modelIndex.parent()) - 1; + default: + return m_model->data(modelIndex, role); + } +} + +bool QQuickTreeModelAdaptor::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!m_model) + return false; + + switch (role) { + case DepthRole: + case ExpandedRole: + case HasChildrenRole: + case HasSiblingRole: + return false; + default: { + const QModelIndex &pmi = mapToModel(index); + qDebug() << "setData" << pmi << role; + return m_model->setData(pmi, value, role); + } + } +} + +int QQuickTreeModelAdaptor::itemIndex(const QModelIndex &index) +{ + // This is basically a plagiarism of QTreeViewPrivate::viewIndex() + if (!index.isValid() || 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 QQuickTreeModelAdaptor::isVisible(const QModelIndex &index) +{ + return itemIndex(index) != -1; +} + +bool QQuickTreeModelAdaptor::childrenVisible(const QModelIndex &index) +{ + return (!index.isValid() && !m_items.isEmpty()) + || (m_expandedItems.contains(index) && isVisible(index)); +} + +const QModelIndex &QQuickTreeModelAdaptor::mapToModel(const QModelIndex &index) const +{ + return m_items.at(index.row()).index; +} + +QModelIndex QQuickTreeModelAdaptor::mapRowToModelIndex(int row) const +{ + if (row < 0 || row >= m_items.count()) + return QModelIndex(); + return m_items.at(row).index; +} + +QItemSelection QQuickTreeModelAdaptor::selectionForRowRange(int from, int to) const +{ + Q_ASSERT(0 <= from && from < m_items.count()); + Q_ASSERT(0 <= to && to < m_items.count()); + + 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.count()); + foreach (const MIPair &pair, ranges) + sel.append(QItemSelectionRange(pair.first, pair.second)); + + return sel; +} + +void QQuickTreeModelAdaptor::showModelTopLevelItems(bool doInsertRows) +{ + if (!m_model) + return; + + if (m_model->hasChildren(QModelIndex()) && m_model->canFetchMore(QModelIndex())) + m_model->fetchMore(QModelIndex()); + const long topLevelRowCount = m_model->rowCount(); + if (topLevelRowCount == 0) { + qDebug() << "no toplevel items"; + return; + } + + showModelChildItems(TreeItem(), 0, topLevelRowCount - 1, doInsertRows); +} + +void QQuickTreeModelAdaptor::showModelChildItems(const TreeItem &parentItem, int start, int end, bool doInsertRows, bool doExpandPendingRows) +{ + const QModelIndex &parentIndex = parentItem.index; + int rowIdx = parentIndex.isValid() ? itemIndex(parentIndex) + 1 : 0; + Q_ASSERT(rowIdx == 0 || parentItem.expanded); + if (parentIndex.isValid() && (rowIdx == 0 || !parentItem.expanded)) { + if (rowIdx == 0) + qDebug() << "not found" << parentIndex; + else + qDebug() << "not expanded" << rowIdx - 1; + return; + } + + if (m_model->rowCount(parentIndex) == 0) { + if (m_model->hasChildren(parentIndex) && m_model->canFetchMore(parentIndex)) + m_model->fetchMore(parentIndex); + qDebug() << "no children" << parentIndex; + return; + } + + int insertCount = end - start + 1; + int startIdx; + if (start == 0) { + startIdx = rowIdx; + } else { + const QModelIndex &prevSiblingIdx = m_model->index(start - 1, 0, parentIndex); + startIdx = lastChildIndex(prevSiblingIdx) + 1; + } + + int rowDepth = rowIdx == 0 ? 0 : parentItem.depth + 1; + qDebug() << "inserting from" << startIdx << "to" << startIdx + insertCount - 1 << "depth" << rowDepth; + 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); + bool expanded = m_expandedItems.contains(cmi); + m_items.insert(startIdx + i, TreeItem(cmi, rowDepth, expanded)); + if (expanded) { + qDebug() << "will expand" << startIdx + i; + m_itemsToExpand.append(&m_items[startIdx + i]); + } + } + if (doInsertRows) + endInsertRows(); + qDebug() << "insertion done"; + + if (doExpandPendingRows) + expandPendingRows(doInsertRows); +} + + +void QQuickTreeModelAdaptor::expand(QModelIndex idx) +{ + ASSERT_CONSISTENCY(); + 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 QQuickTreeModelAdaptor::collapse(QModelIndex idx) +{ + ASSERT_CONSISTENCY(); + 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 QQuickTreeModelAdaptor::isExpanded(QModelIndex index) const +{ + ASSERT_CONSISTENCY(); + return !index.isValid() || m_expandedItems.contains(index); +} + +bool QQuickTreeModelAdaptor::isExpanded(int row) const +{ + return m_items.at(row).expanded; +} + +void QQuickTreeModelAdaptor::expandRow(int n) +{ + if (!m_model || isExpanded(n)) { + qDebug() << "already expanded" << n; + return; + } + + TreeItem &item = m_items[n]; + if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index)) { + qDebug() << "no children" << n; + return; + } + item.expanded = true; + m_expandedItems.insert(item.index); + QVector<int> changedRole(1, ExpandedRole); + emit dataChanged(index(n), index(n), changedRole); + + qDebug() << "expanding" << n << m_model->rowCount(item.index) << m_items[n].expanded; + m_itemsToExpand.append(&item); + expandPendingRows(); +} + +void QQuickTreeModelAdaptor::expandPendingRows(bool doInsertRows) +{ + while (!m_itemsToExpand.isEmpty()) { + 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); + qDebug() << "no children for row" << itemIndex(index); + continue; + } + + qDebug() << "expanding pending row" << itemIndex(index) << "children"<< childrenCount; + + // 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 QQuickTreeModelAdaptor::collapseRow(int n) +{ + if (!m_model || !isExpanded(n)) { + qDebug() << "not expanded" << n; + return; + } + + TreeItem &item = m_items[n]; + item.expanded = false; + m_expandedItems.remove(item.index); + QVector<int> changedRole(1, ExpandedRole); + emit dataChanged(index(n), index(n), changedRole); + int childrenCount = m_model->rowCount(item.index); + if ((item.index.flags() & Qt::ItemNeverHasChildren) || !m_model->hasChildren(item.index) || childrenCount == 0) { + qDebug() << "no children" << n; + return; + } + + qDebug() << "collapsing" << n << childrenCount; + const QModelIndex &emi = m_model->index(m_model->rowCount(item.index) - 1, 0, item.index); + int lastIndex = lastChildIndex(emi); + removeVisibleRows(n + 1, lastIndex); +} + +int QQuickTreeModelAdaptor::lastChildIndex(const QModelIndex &index) +{ +// qDebug() << "last child of" << itemIndex(index.parent()); + + if (!m_expandedItems.contains(index)) { +// qDebug() << "not expanded" << itemIndex(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(); + qDebug() << "first index" << firstIndex - 1; + return firstIndex - 1; +} + +void QQuickTreeModelAdaptor::removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows) +{ + if (startIndex < 0 || endIndex < 0 || startIndex > endIndex) + return; + + qDebug() << "removing" << startIndex << endIndex; + if (doRemoveRows) + beginRemoveRows(QModelIndex(), startIndex, endIndex); + m_items.erase(m_items.begin() + startIndex, m_items.begin() + endIndex + 1); + if (doRemoveRows) + endRemoveRows(); +} + +void QQuickTreeModelAdaptor::modelHasBeenReset() +{ + qDebug() << "modelHasBeenReset"; + clearModelData(); + + showModelTopLevelItems(); + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRigth, const QVector<int> &roles) +{ + qDebug() << "modelDataChanged" << topLeft << bottomRigth; + Q_ASSERT(topLeft.parent() == bottomRigth.parent()); + const QModelIndex &parent = topLeft.parent(); + if (parent.isValid() && !childrenVisible(parent)) { + qDebug() << "not visible" << parent; + ASSERT_CONSISTENCY(); + return; + } + + int topIndex = itemIndex(topLeft); + if (topIndex == -1) // 'parent' is not visible anymore, though it's been expanded previously + return; + for (int i = topLeft.row(); i <= bottomRigth.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() == bottomRigth.row()) + break; + ++bottomIndex; + } + emit dataChanged(index(topIndex), index(bottomIndex), roles); + + i += bottomIndex - topIndex; + if (i == bottomRigth.row()) + break; + topIndex = bottomIndex + 1; + while (topIndex < m_items.count() + && m_items.at(topIndex).index.parent() != parent) + topIndex++; + } + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + qDebug() << "modelLayoutAboutToBeChanged" << parents << hint << m_items.count(); + ASSERT_CONSISTENCY(); + Q_UNUSED(parents); + Q_UNUSED(hint); +} + +void QQuickTreeModelAdaptor::modelLayoutChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + Q_UNUSED(hint); + qDebug() << "modelLayoutChanged" << parents << hint << m_items.count(); + if (parents.isEmpty()) { + m_items.clear(); + showModelTopLevelItems(false /*doInsertRows*/); + emit dataChanged(index(0), index(m_items.count() - 1)); + } + + Q_FOREACH (const QPersistentModelIndex &pmi, parents) { + if (m_expandedItems.contains(pmi) && m_model->hasChildren(pmi)) { + int row = itemIndex(pmi); + if (row != -1) { + const QModelIndex &lmi = m_model->index(m_model->rowCount(pmi) - 1, 0, pmi); + int lastRow = lastChildIndex(lmi); + removeVisibleRows(row + 1, lastRow, false /*doRemoveRows*/); + showModelChildItems(m_items.at(row), 0, m_model->rowCount(pmi) - 1, false /*doInsertRows*/); + emit dataChanged(index(row + 1), index(lastRow)); + } + } + } + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end) +{ + qDebug() << "modelRowsAboutToBeInserted" << parent << "start" << start << "end" << end; + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelRowsInserted(const QModelIndex & parent, int start, int end) +{ + qDebug() << "modelRowsInserted" << parent << "start" << start << "end" << end; + TreeItem item; + int parentRow = itemIndex(parent); + if (parentRow >= 0) { + item = m_items.at(parentRow); + if (!item.expanded) { + ASSERT_CONSISTENCY(); + return; + } + } else if (parent.isValid()) { + item = TreeItem(parent); + } + showModelChildItems(item, start, end); + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) +{ + qDebug() << "modelRowsAboutToBeRemoved" << parent << "start" << start << "end" << end; + ASSERT_CONSISTENCY(); + if (!parent.isValid() || 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 = itemIndex(emi); + if (isExpanded(emi)) { + const QModelIndex &idx = m_model->index(m_model->rowCount(emi) - 1, 0, emi); + endIndex = lastChildIndex(idx); + } + removeVisibleRows(startIndex, endIndex); + } + + for (int r = start; r <= end; r++) { + const QModelIndex &cmi = m_model->index(r, 0, parent); + m_expandedItems.remove(cmi); + } +} + +void QQuickTreeModelAdaptor::modelRowsRemoved(const QModelIndex & parent, int start, int end) +{ + qDebug() << "modelRowsRemoved" << parent << "start" << start << "end" << end; + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) +{ + qDebug() << "modelRowsAboutToBeMoved" << sourceParent << "source start" << sourceStart << "end" << sourceEnd; + qDebug() << " destination" << destinationParent << "row" << destinationRow; + ASSERT_CONSISTENCY(); + if (!childrenVisible(sourceParent)) + return; // Do nothing now. See modelRowsMoved() below. + + if (!childrenVisible(destinationParent)) { + modelRowsAboutToBeRemoved(sourceParent, sourceStart, sourceEnd); + } 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++; + } + qDebug() << "depth difference" << depthDifference; + + int startIndex = itemIndex(m_model->index(sourceStart, 0, sourceParent)); + const QModelIndex &emi = m_model->index(sourceEnd, 0, sourceParent); + int endIndex; + if (isExpanded(emi)) + endIndex = lastChildIndex(m_model->index(m_model->rowCount(emi) - 1, 0, emi)); + else + 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)); + } + + qDebug() << "moving" << (destIndex > endIndex ? "forward" : "backward") << startIndex << endIndex << destIndex << m_items.count(); + beginMoveRows(QModelIndex(), startIndex, endIndex, QModelIndex(), destIndex); + int totalMovedCount = endIndex - startIndex + 1; + const QList<TreeItem> &buffer = m_items.mid(startIndex, totalMovedCount); + qDebug() << "copied" << startIndex << totalMovedCount; + int bufferCopyOffset; + if (destIndex > endIndex) { + for (int i = endIndex + 1; i < destIndex; i++) { + m_items.swap(i, i - totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex - totalMovedCount; + } else { + for (int i = startIndex - 1; i >= destIndex; i--) { + m_items.swap(i, i + totalMovedCount); // Fast move from 1st to 2nd position + } + bufferCopyOffset = destIndex; + } + qDebug() << "copying back" << bufferCopyOffset << buffer.length(); + for (int i = 0; i < buffer.length(); i++) { + TreeItem item = buffer.at(i); + item.depth += depthDifference; + m_items.replace(bufferCopyOffset + i, item); + } + endMoveRows(); + } +} + +void QQuickTreeModelAdaptor::modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow) +{ + qDebug() << "modelRowsMoved" << sourceParent << "source start" << sourceStart << "end" << sourceEnd; + qDebug() << " destination" << destinationParent << "row" << destinationRow; + if (!childrenVisible(sourceParent) && childrenVisible(destinationParent)) + modelRowsInserted(destinationParent, destinationRow, destinationRow + sourceEnd - sourceStart); + ASSERT_CONSISTENCY(); +} + +void QQuickTreeModelAdaptor::dump() const +{ + int count = m_items.count(); + if (count == 0) + return; + int countWidth = floor(log10(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() + << QString("%1 ").arg(i, countWidth) << QString(4 * item.depth, QChar::fromLatin1('.')) + << QLatin1String(!hasChildren ? ".. " : item.expanded ? " v " : " > ") + << item.index << children; + } +} + +bool QQuickTreeModelAdaptor::testConsistency(bool dumpOnFail) const +{ + QModelIndex parent; + QStack<QModelIndex> ancestors; + QModelIndex idx = m_model->index(0, 0); + 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; +} + +QT_END_NAMESPACE diff --git a/src/controls/Private/qquicktreemodeladaptor_p.h b/src/controls/Private/qquicktreemodeladaptor_p.h new file mode 100644 index 000000000..838ab8055 --- /dev/null +++ b/src/controls/Private/qquicktreemodeladaptor_p.h @@ -0,0 +1,158 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKTREEMODELADAPTOR_H +#define QQUICKTREEMODELADAPTOR_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 <QtCore/qset.h> +#include <QtCore/qabstractitemmodel.h> +#include <QtCore/qitemselectionmodel.h> + +QT_BEGIN_NAMESPACE + +class QAbstractItemModel; + +class QQuickTreeModelAdaptor : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged) + + struct TreeItem; + +public: + explicit QQuickTreeModelAdaptor(QObject *parent = 0); + + QAbstractItemModel *model() const; + + enum { + DepthRole = Qt::UserRole - 4, + ExpandedRole, + HasChildrenRole, + HasSiblingRole + }; + + QHash<int, QByteArray> roleNames() const; + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &, int role) const; + bool setData(const QModelIndex &index, const QVariant &value, int role); + + void clearModelData(); + + bool isVisible(const QModelIndex &index); + bool childrenVisible(const QModelIndex &index); + + const QModelIndex &mapToModel(const QModelIndex &index) const; + Q_INVOKABLE QModelIndex mapRowToModelIndex(int row) const; + + Q_INVOKABLE QItemSelection selectionForRowRange(int form, int to) 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); + void expandPendingRows(bool doInsertRows = true); + int lastChildIndex(const QModelIndex &index); + void removeVisibleRows(int startIndex, int endIndex, bool doRemoveRows = true); + + void expandRow(int n); + void collapseRow(int n); + bool isExpanded(int row) const; + + Q_INVOKABLE bool isExpanded(QModelIndex) const; + + void dump() const; + bool testConsistency(bool dumpOnFail = false) const; + +signals: + void modelChanged(QAbstractItemModel *model); + void expanded(QModelIndex index); + void collapsed(QModelIndex index); + +public slots: + void expand(QModelIndex); + void collapse(QModelIndex); + + void setModel(QAbstractItemModel *model); + +private slots: + void modelHasBeenReset(); + void modelDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRigth, const QVector<int> &roles); + void modelLayoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelLayoutChanged(const QList<QPersistentModelIndex> &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; + } + }; + + QAbstractItemModel *m_model; + QList<TreeItem> m_items; + QSet<QPersistentModelIndex> m_expandedItems; + QList<TreeItem *> m_itemsToExpand; + int m_lastItemIndex; +}; + +QT_END_NAMESPACE + +#endif // QQUICKTREEMODELADAPTOR_H diff --git a/src/controls/Styles/Base/BasicTableViewStyle.qml b/src/controls/Styles/Base/BasicTableViewStyle.qml new file mode 100644 index 000000000..24a528f52 --- /dev/null +++ b/src/controls/Styles/Base/BasicTableViewStyle.qml @@ -0,0 +1,150 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Private 1.0 + +/*! + \qmltype BasicTableViewStyle + \inqmlmodule QtQuick.Controls.Styles + \since 5.1 + \internal + \qmlabstract + \ingroup viewsstyling + \brief Provides custom styling for TableView + + \note This class derives from \l {QtQuick.Controls.Styles::}{ScrollViewStyle} + and supports all of the properties defined there. +*/ +ScrollViewStyle { + id: root + + /*! The \l TableView this style is attached to. */ + readonly property BasicTableView control: __control + + /*! The text color. */ + property color textColor: SystemPaletteSingleton.text(control.enabled) + + /*! The background color. */ + property color backgroundColor: control.backgroundVisible ? SystemPaletteSingleton.base(control.enabled) : "transparent" + + /*! The alternate background color. */ + property color alternateBackgroundColor: "#f5f5f5" + + /*! The text highlight color, used within selections. */ + property color highlightedTextColor: "white" + + /*! Activates items on single click. */ + property bool activateItemOnSingleClick: false + + padding.top: control.headerVisible ? 0 : 1 + + /*! \qmlproperty Component BasicTableViewStyle::headerDelegate + Delegate for header. This delegate is described in \l {TableView::headerDelegate} + */ + property Component headerDelegate: BorderImage { + height: textItem.implicitHeight * 1.2 + source: "images/header.png" + border.left: 4 + border.bottom: 2 + border.top: 2 + Text { + id: textItem + anchors.fill: parent + verticalAlignment: Text.AlignVCenter + horizontalAlignment: styleData.textAlignment + anchors.leftMargin: 12 + text: styleData.value + elide: Text.ElideRight + color: textColor + renderType: Settings.isMobile ? Text.QtRendering : Text.NativeRendering + } + Rectangle { + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + anchors.topMargin: 1 + width: 1 + color: "#ccc" + } + } + + /*! \qmlproperty Component BasicTableViewStyle::rowDelegate + Delegate for row. This delegate is described in \l {TableView::rowDelegate} + */ + property Component rowDelegate: Rectangle { + height: Math.round(TextSingleton.implicitHeight * 1.2) + property color selectedColor: control.activeFocus ? "#07c" : "#999" + color: styleData.selected ? selectedColor : + !styleData.alternate ? alternateBackgroundColor : backgroundColor + } + + /*! \qmlproperty Component BasicTableViewStyle::itemDelegate + Delegate for item. This delegate is described in \l {TableView::itemDelegate} + */ + property Component itemDelegate: Item { + height: Math.max(16, label.implicitHeight) + property int implicitWidth: label.implicitWidth + 20 + + Text { + id: label + objectName: "label" + width: parent.width +// anchors.leftMargin: 12 + anchors.left: parent.left + anchors.right: parent.right + horizontalAlignment: styleData.textAlignment + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: 1 + elide: styleData.elideMode + text: styleData.value !== undefined ? styleData.value : "" + color: styleData.textColor + renderType: Settings.isMobile ? Text.QtRendering : Text.NativeRendering + } + } + + /*! \internal + Part of TreeViewStyle + */ + property Component __branchDelegate: null + + /*! \internal + Part of TreeViewStyle + */ + property int __indentation: 12 +} diff --git a/src/controls/Styles/Base/TableViewStyle.qml b/src/controls/Styles/Base/TableViewStyle.qml index e5e6db2fd..3715eb266 100644 --- a/src/controls/Styles/Base/TableViewStyle.qml +++ b/src/controls/Styles/Base/TableViewStyle.qml @@ -37,8 +37,8 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -import QtQuick 2.2 -import QtQuick.Controls 1.2 +import QtQuick 2.5 +import QtQuick.Controls 1.4 import QtQuick.Controls.Private 1.0 /*! @@ -51,92 +51,9 @@ import QtQuick.Controls.Private 1.0 \note This class derives from \l {QtQuick.Controls.Styles::}{ScrollViewStyle} and supports all of the properties defined there. */ -ScrollViewStyle { +BasicTableViewStyle { id: root /*! The \l TableView this style is attached to. */ readonly property TableView control: __control - - /*! The text color. */ - property color textColor: SystemPaletteSingleton.text(control.enabled) - - /*! The background color. */ - property color backgroundColor: control.backgroundVisible ? SystemPaletteSingleton.base(control.enabled) : "transparent" - - /*! The alternate background color. */ - property color alternateBackgroundColor: "#f5f5f5" - - /*! The text highlight color, used within selections. */ - property color highlightedTextColor: "white" - - /*! Activates items on single click. */ - property bool activateItemOnSingleClick: false - - padding.top: control.headerVisible ? 0 : 1 - - /*! \qmlproperty Component TableViewStyle::headerDelegate - Delegate for header. This delegate is described in \l {TableView::headerDelegate} - */ - property Component headerDelegate: BorderImage { - height: textItem.implicitHeight * 1.2 - source: "images/header.png" - border.left: 4 - border.bottom: 2 - border.top: 2 - Text { - id: textItem - anchors.fill: parent - verticalAlignment: Text.AlignVCenter - horizontalAlignment: styleData.textAlignment - anchors.leftMargin: 12 - text: styleData.value - elide: Text.ElideRight - color: textColor - renderType: Settings.isMobile ? Text.QtRendering : Text.NativeRendering - } - Rectangle { - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.bottomMargin: 1 - anchors.topMargin: 1 - width: 1 - color: "#ccc" - } - } - - /*! \qmlproperty Component TableViewStyle::rowDelegate - Delegate for row. This delegate is described in \l {TableView::rowDelegate} - */ - property Component rowDelegate: Rectangle { - height: Math.round(TextSingleton.implicitHeight * 1.2) - property color selectedColor: control.activeFocus ? "#07c" : "#999" - color: styleData.selected ? selectedColor : - !styleData.alternate ? alternateBackgroundColor : backgroundColor - } - - /*! \qmlproperty Component TableViewStyle::itemDelegate - Delegate for item. This delegate is described in \l {TableView::itemDelegate} - */ - property Component itemDelegate: Item { - height: Math.max(16, label.implicitHeight) - property int implicitWidth: label.implicitWidth + 20 - - Text { - id: label - objectName: "label" - width: parent.width - anchors.leftMargin: 12 - anchors.left: parent.left - anchors.right: parent.right - horizontalAlignment: styleData.textAlignment - anchors.verticalCenter: parent.verticalCenter - anchors.verticalCenterOffset: 1 - elide: styleData.elideMode - text: styleData.value !== undefined ? styleData.value : "" - color: styleData.textColor - renderType: Settings.isMobile ? Text.QtRendering : Text.NativeRendering - } - } } - diff --git a/src/controls/Styles/Base/TreeViewStyle.qml b/src/controls/Styles/Base/TreeViewStyle.qml new file mode 100644 index 000000000..2e36c3166 --- /dev/null +++ b/src/controls/Styles/Base/TreeViewStyle.qml @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Private 1.0 + +/*! + \qmltype TreeViewStyle + \inqmlmodule QtQuick.Controls.Styles + \since 5.5 + \ingroup viewsstyling + \inherits TableViewStyle + \brief Provides custom styling for TreeView + + \note This class derives from \l {QtQuick.Controls.Styles::}{TableViewStyle} + and supports all of the properties defined there. +*/ +BasicTableViewStyle { + id: root + + /*! The \l TreeView this style is attached to. */ + readonly property TreeView control: __control + + /*! + The amount each level is indented relatively to its parent level. + */ + property int indentation: 12 + + // TODO - to update + /*! \qmlproperty Component TreeViewStyle::branchDelegate + + This property defines a delegate to draw the branch indicator. + + In the branch delegate you have access to the following special properties: + \list + \li styleData.column - the index of the column + \li styleData.selected - if the item is currently selected + \li styleData.textColor - the default text color for an item + \li styleData.isExpanded - true when the item is expanded + \li styleData.hasChildren - true if the model index of the current item has children + \li styleData.hasSibling - true if the model index of the current item has sibling + \endlist + */ + + property Component branchDelegate: Item { + width: 16 + height: 16 + Text { + visible: styleData.column === 0 && styleData.hasChildren + text: styleData.isExpanded ? "\u25bc" : "\u25b6" + color: !control.activeFocus || styleData.selected ? styleData.textColor : "#666" + font.pointSize: 10 + renderType: Text.NativeRendering + anchors.centerIn: parent + anchors.verticalCenterOffset: styleData.isExpanded ? 2 : 0 + } + } + + __branchDelegate: branchDelegate + __indentation: indentation +} diff --git a/src/controls/Styles/Desktop/TableViewStyle.qml b/src/controls/Styles/Desktop/TableViewStyle.qml index 11f8ed54c..377b3a750 100644 --- a/src/controls/Styles/Desktop/TableViewStyle.qml +++ b/src/controls/Styles/Desktop/TableViewStyle.qml @@ -45,7 +45,8 @@ import "." ScrollViewStyle { id: root - readonly property TableView control: __control + readonly property BasicTableView control: __control + property int __indentation: 8 property bool activateItemOnSingleClick: __styleitem.styleHint("activateItemOnSingleClick") property color textColor: __styleitem.textColor property color backgroundColor: SystemPaletteSingleton.base(control.enabled) @@ -95,7 +96,6 @@ ScrollViewStyle { id: label objectName: "label" width: parent.width - anchors.leftMargin: 8 font: __styleitem.font anchors.left: parent.left anchors.right: parent.right @@ -107,5 +107,6 @@ ScrollViewStyle { renderType: Text.NativeRendering } } -} + property Component __branchDelegate: null +} diff --git a/src/controls/Styles/Desktop/TreeViewStyle.qml b/src/controls/Styles/Desktop/TreeViewStyle.qml new file mode 100644 index 000000000..1901c40cc --- /dev/null +++ b/src/controls/Styles/Desktop/TreeViewStyle.qml @@ -0,0 +1,67 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.2 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Private 1.0 +import "." as Desktop + +Desktop.TableViewStyle { + id: root + + __indentation: 12 + + __branchDelegate: StyleItem { + id: si + elementType: "itembranchindicator" + properties: { + "hasChildren": styleData.hasChildren, + "hasSibling": styleData.hasSibling && !styleData.isExpanded + } + on: styleData.isExpanded + selected: styleData.selected + hasFocus: __styleitem.active + + Component.onCompleted: { + implicitWidth = si.pixelMetric("treeviewindentation") + implicitHeight = implicitWidth + var rect = si.subControlRect("dummy"); + width = rect.width + height = rect.height + root.__indentation = width + } + } +} diff --git a/src/controls/Styles/qmldir b/src/controls/Styles/qmldir index 7e21858e1..0be8756d5 100644 --- a/src/controls/Styles/qmldir +++ b/src/controls/Styles/qmldir @@ -15,6 +15,7 @@ SpinBoxStyle 1.1 Base/SpinBoxStyle.qml SwitchStyle 1.1 Base/SwitchStyle.qml TabViewStyle 1.0 Base/TabViewStyle.qml TableViewStyle 1.0 Base/TableViewStyle.qml +TreeViewStyle 1.0 Base/TreeViewStyle.qml TextAreaStyle 1.1 Base/TextAreaStyle.qml TextFieldStyle 1.0 Base/TextFieldStyle.qml ToolBarStyle 1.0 Base/ToolBarStyle.qml diff --git a/src/controls/Styles/styles.pri b/src/controls/Styles/styles.pri index 1fcf41476..f91a9d1a2 100644 --- a/src/controls/Styles/styles.pri +++ b/src/controls/Styles/styles.pri @@ -18,7 +18,9 @@ STYLES_QML_FILES = \ $$PWD/Base/SpinBoxStyle.qml \ $$PWD/Base/SwitchStyle.qml \ $$PWD/Base/StatusBarStyle.qml \ + $$PWD/Base/BasicTableViewStyle.qml \ $$PWD/Base/TableViewStyle.qml \ + $$PWD/Base/TreeViewStyle.qml \ $$PWD/Base/TabViewStyle.qml \ $$PWD/Base/TextAreaStyle.qml \ $$PWD/Base/TextFieldStyle.qml \ @@ -49,6 +51,7 @@ STYLES_QML_FILES = \ $$PWD/Desktop/StatusBarStyle.qml\ $$PWD/Desktop/TabViewStyle.qml \ $$PWD/Desktop/TableViewStyle.qml \ + $$PWD/Desktop/TreeViewStyle.qml \ $$PWD/Desktop/TextAreaStyle.qml \ $$PWD/Desktop/TextFieldStyle.qml \ $$PWD/Desktop/ToolBarStyle.qml \ diff --git a/src/controls/TableView.qml b/src/controls/TableView.qml index 0bec3de6d..629c95dd0 100644 --- a/src/controls/TableView.qml +++ b/src/controls/TableView.qml @@ -1,38 +1,34 @@ /**************************************************************************** ** -** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). -** Contact: http://www.qt-project.org/legal +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ ** ** This file is part of the Qt Quick Controls module of the Qt Toolkit. ** -** $QT_BEGIN_LICENSE:BSD$ -** You may use this file under the terms of the BSD license as follows: +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. ** -** "Redistribution and use in source and binary forms, with or without -** modification, are permitted provided that the following conditions are -** met: -** * Redistributions of source code must retain the above copyright -** notice, this list of conditions and the following disclaimer. -** * Redistributions in binary form must reproduce the above copyright -** notice, this list of conditions and the following disclaimer in -** the documentation and/or other materials provided with the -** distribution. -** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names -** of its contributors may be used to endorse or promote products derived -** from this software without specific prior written permission. +** 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.LGPLv3 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.html. ** -** -** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $QT_END_LICENSE$ ** diff --git a/src/controls/TableViewColumn.qml b/src/controls/TableViewColumn.qml index 2d911a1ec..3cea1a2bc 100644 --- a/src/controls/TableViewColumn.qml +++ b/src/controls/TableViewColumn.qml @@ -45,11 +45,11 @@ import QtQuick 2.2 \inqmlmodule QtQuick.Controls \since 5.1 \ingroup viewitems - \brief Used to define columns in a \l TableView. + \brief Used to define columns in a \l TableView or in a \l TreeView. \image tableview.png - TableViewColumn represents a column within a TableView. It provides + TableViewColumn represents a column within a TableView or a TreeView. It provides properties to decide how the data in that column is presented. \qml @@ -60,7 +60,7 @@ import QtQuick 2.2 } \endqml - \sa TableView + \sa TableView, TreeView */ QtObject { @@ -121,7 +121,7 @@ QtObject { property int horizontalAlignment: Text.AlignLeft /*! The delegate of the column. This can be used to set the - \l TableView::itemDelegate for a specific column. + \l TableView::itemDelegate or TreeView::itemDelegate for a specific column. In the delegate you have access to the following special properties: \list diff --git a/src/controls/TreeView.qml b/src/controls/TreeView.qml new file mode 100644 index 000000000..7e1520da9 --- /dev/null +++ b/src/controls/TreeView.qml @@ -0,0 +1,500 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Quick Controls module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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.LGPLv3 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.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Private 1.0 +import QtQuick.Controls.Styles 1.2 +import QtQml.Models 2.2 + +/*! + \qmltype TreeView + \inqmlmodule QtQuick.Controls + \since 5.5 + \ingroup views + \brief Provides a tree view with scroll bars, styling and header sections. + + \image treeview.png + + A TreeView implements a tree representation of items from a model. + + Data for each row in the TreeView + is provided by the model. TreeView accepts models derived from the QAbstractItemModel class. + + You provide title and size of a column header + by adding a \l TableViewColumn as demonstrated below. + + \code + TreeView { + TableViewColumn { + title: "Name" + role: "fileName" + width: 300 + } + TableViewColumn { + title: "Permissions" + role: "filePermissions" + width: 100 + } + model: fileSystemModel + } + \endcode + + The header sections are attached to values in the \l model by defining + the model role they attach to. Each property in the model will + then be shown in their corresponding column. + + You can customize the look by overriding the \l itemDelegate, + \l rowDelegate, or \l headerDelegate properties. + + The view itself does not provide sorting. This has to + be done on the model itself. However you can provide sorting + on the model, and enable sort indicators on headers. + +\list + \li int sortIndicatorColumn - The index of the current sort column + \li bool sortIndicatorVisible - Whether the sort indicator should be enabled + \li enum sortIndicatorOrder - Qt.AscendingOrder or Qt.DescendingOrder depending on state +\endlist + + You can create a custom appearance for a TreeView by + assigning a \l {QtQuick.Controls.Styles::TreeViewStyle}{TreeViewStyle}. +*/ + +BasicTableView { + id: root + + /*! + \qmlproperty QAbstractItemModel TreeView::model + This property holds the model providing data for the tree view. + + The model provides the set of data that is displayed by the view. + The TreeView accept models derived from the QAbstractItemModel class. + */ + property var model: null + + /*! + \qmlproperty QModelIndex TreeView::currentIndex + The model index of the current row in the tree view. + */ + readonly property var currentIndex: modelAdaptor.mapRowToModelIndex(__currentRow) + + /*! + \qmlproperty QItemSelectionModel TreeView::selection + */ + property ItemSelectionModel selection: null + + /*! + \qmlsignal TreeView::activated(QModelIndex index) + + Emitted when the user activates a row in the tree by mouse or keyboard interaction. + Mouse activation is triggered by single- or double-clicking, depending on the platform. + + \a index is the model index of the activated row in the tree. + + \note This signal is only emitted for mouse interaction that is not blocked in the row or item delegate. + + The corresponding handler is \c onActivated. + */ + signal activated(var index) + + /*! + \qmlsignal TreeView::clicked(QModelIndex index) + + Emitted when the user clicks a valid row in the tree by single clicking + + \a index is the model index of the clicked row in the tree. + + \note This signal is only emitted if the row or item delegate does not accept mouse events. + + The corresponding handler is \c onClicked. + */ + signal clicked(var index) + + /*! + \qmlsignal TreeView::doubleClicked(QModelIndex index) + + Emitted when the user presses and holds a valid row in the tree. + + \a index is the model index of the double clicked row in the tree. + + \note This signal is only emitted if the row or item delegate does not accept mouse events. + + The corresponding handler is \c onPressAndHold. + */ + signal doubleClicked(var index) + + /*! + \qmlsignal TreeView::pressAndHold(QModelIndex index) + + Emitted when the user presses and holds a valid row in the tree. + + \a index is the model index of the pressed row in the tree. + + \note This signal is only emitted if the row or item delegate does not accept mouse events. + + The corresponding handler is \c onPressAndHold. + */ + signal pressAndHold(var index) + + /*! + \qmlsignal TreeView::expanded(QModelIndex index) + + Emitted when a valid row in the tree is expanded, displaying its children. + + \a index is the model index of the expanded row in the tree. + + \note This signal is only emitted if the row or item delegate does not accept mouse events. + + The corresponding handler is \c onExpanded. + */ + signal expanded(var index) + + /*! + \qmlsignal TreeView::collapsed(QModelIndex index) + + Emitted when a valid row in the tree is collapsed, hiding its children. + + \a index is the model index of the collapsed row in the tree. + + \note This signal is only emitted if the row or item delegate does not accept mouse events. + + The corresponding handler is \c onCollapsed. + */ + signal collapsed(var index) + + /*! + \qmlmethod bool TreeView::isExpanded(QModelIndex index) + + Returns true if the model item index is expanded; otherwise returns false. + + \sa {expanded}, {expand} + */ + function isExpanded(index) { + return modelAdaptor.isExpanded(index) + } + + /*! + \qmlmethod void TreeView::collapse(QModelIndex index) + + Collapses the model item specified by the index. + + \sa {collapsed}, {isExpanded} + */ + function collapse(index) { + modelAdaptor.collapse(index) + } + + /*! + \qmlmethod void TreeView::expand(QModelIndex index) + + Expands the model item specified by the index. + + \sa {expanded}, {isExpanded} + */ + function expand(index) { + modelAdaptor.expand(index) + } + + style: Qt.createComponent(Settings.style + "/TreeViewStyle.qml", root) + + // Internal stuff. Do not look + + __viewTypeName: "TreeView" + + __model: TreeModelAdaptor { + id: modelAdaptor + model: root.model + + onExpanded: root.expanded(index) + onCollapsed: root.collapsed(index) + } + + onSelectionModeChanged: if (!!selection) selection.clear() + + __mouseArea: MouseArea { + id: mouseArea + + parent: __listView + width: __listView.width + height: __listView.height + z: -1 + propagateComposedEvents: true + focus: true + // Note: with boolean preventStealing we are keeping + // the flickable from eating our mouse press events + preventStealing: !Settings.hasTouchScreen + + property int clickedRow: -1 + property int pressedRow: -1 + property int pressedColumn: -1 + readonly property alias currentRow: root.__currentRow + + // Handle vertical scrolling whem dragging mouse outside boundaries + property int autoScroll: 0 // 0 -> do nothing; 1 -> increment; 2 -> decrement + property bool shiftPressed: false // forward shift key state to the autoscroll timer + + Timer { + running: mouseArea.autoScroll !== 0 && __verticalScrollBar.visible + interval: 20 + repeat: true + onTriggered: { + var oldPressedRow = mouseArea.pressedRow + var row + if (mouseArea.autoScroll === 1) { + __listView.incrementCurrentIndexBlocking(); + row = __listView.indexAt(0, __listView.height + __listView.contentY) + if (row === -1) + row = __listView.count - 1 + } else { + __listView.decrementCurrentIndexBlocking(); + row = __listView.indexAt(0, __listView.contentY) + } + + if (row !== oldPressedRow) { + mouseArea.pressedRow = row + var modifiers = mouseArea.shiftPressed ? Qt.ShiftModifier : Qt.NoModifier + mouseArea.mouseSelect(row, modifiers, true /* drag */) + } + } + } + + function mouseSelect(row, modifiers, drag) { + if (!selection) { + maybeWarnAboutSelectionMode() + return + } + + if (selectionMode) { + var modelIndex = modelAdaptor.mapRowToModelIndex(row) + if (selectionMode === SelectionMode.SingleSelection) { + selection.setCurrentIndex(modelIndex, ItemSelectionModel.NoUpdate) + } else { + var itemSelection = clickedRow === row ? modelIndex + : modelAdaptor.selectionForRowRange(clickedRow, row) + if (selectionMode === SelectionMode.MultiSelection + || modifiers & Qt.ControlModifier) { + if (drag) + selection.select(itemSelection, ItemSelectionModel.ToggleCurrent) + else + selection.select(modelIndex, ItemSelectionModel.Toggle) + } else if (modifiers & Qt.ShiftModifier) { + selection.select(itemSelection, ItemSelectionModel.SelectCurrent) + } else { + clickedRow = row // Needed only when drag is true + selection.select(modelIndex, ItemSelectionModel.ClearAndSelect) + } + } + } + } + + function keySelect(keyModifiers) { + if (selectionMode) { + if (!keyModifiers) + clickedRow = currentRow + if (!(keyModifiers & Qt.ControlModifier)) + mouseSelect(currentRow, keyModifiers, keyModifiers & Qt.ShiftModifier) + } + } + + function selected(row) { + if (selectionMode === SelectionMode.NoSelection) + return false + + var modelIndex = null + if (!!selection) { + modelIndex = modelAdaptor.mapRowToModelIndex(row) + if (modelIndex.valid) { + if (selectionMode === SelectionMode.SingleSelection) + return selection.currentIndex === modelIndex + return selection.hasSelection && selection.isSelected(modelIndex) + } + } + + return row === currentRow + && (selectionMode === SelectionMode.SingleSelection + || (selectionMode > SelectionMode.SingleSelection && !selection)) + } + + function branchDecorationContains(x, y) { + var clickedItem = __listView.itemAt(0, y + __listView.contentY) + if (!(clickedItem && clickedItem.rowItem)) + return false + var branchDecoration = clickedItem.rowItem.branchDecoration + if (!branchDecoration) + return false + var pos = mapToItem(branchDecoration, x, y) + return branchDecoration.contains(Qt.point(pos.x, pos.y)) + } + + function maybeWarnAboutSelectionMode() { + if (selectionMode > SelectionMode.SingleSelection) + console.warn("TreeView: Non-single selection is not supported without an ItemSelectionModel.") + } + + onPressed: { + pressedRow = __listView.indexAt(0, mouseY + __listView.contentY) + pressedColumn = __listView.columnAt(mouseX) + __listView.forceActiveFocus() + if (pressedRow > -1 && !Settings.hasTouchScreen + && !branchDecorationContains(mouse.x, mouse.y)) { + __listView.currentIndex = pressedRow + if (clickedRow === -1) + clickedRow = pressedRow + mouseSelect(pressedRow, mouse.modifiers, false) + if (!mouse.modifiers) + clickedRow = pressedRow + } + } + + onReleased: { + pressedRow = -1 + pressedColumn = -1 + autoScroll = 0 + } + + onPositionChanged: { + // NOTE: Testing for pressed is not technically needed, at least + // until we decide to support tooltips or some other hover feature + if (mouseY > __listView.height && pressed) { + if (autoScroll === 1) return; + autoScroll = 1 + } else if (mouseY < 0 && pressed) { + if (autoScroll === 2) return; + autoScroll = 2 + } else { + autoScroll = 0 + } + + if (pressed && containsMouse) { + var oldPressedRow = pressedRow + pressedRow = __listView.indexAt(0, mouseY + __listView.contentY) + pressedColumn = __listView.columnAt(mouseX) + if (pressedRow > -1 && oldPressedRow !== pressedRow) { + __listView.currentIndex = pressedRow + mouseSelect(pressedRow, mouse.modifiers, true /* drag */) + } + } + } + + onExited: { + pressedRow = -1 + pressedColumn = -1 + } + + onCanceled: { + pressedRow = -1 + pressedColumn = -1 + autoScroll = 0 + } + + onClicked: { + var clickIndex = __listView.indexAt(0, mouseY + __listView.contentY) + if (clickIndex > -1) { + var modelIndex = modelAdaptor.mapRowToModelIndex(clickIndex) + if (branchDecorationContains(mouse.x, mouse.y)) { + if (modelAdaptor.isExpanded(modelIndex)) + modelAdaptor.collapse(modelIndex) + else + modelAdaptor.expand(modelIndex) + } else if (root.__activateItemOnSingleClick) { + root.activated(modelIndex) + } + root.clicked(modelIndex) + } + } + + onDoubleClicked: { + var clickIndex = __listView.indexAt(0, mouseY + __listView.contentY) + if (clickIndex > -1) { + var modelIndex = modelAdaptor.mapRowToModelIndex(clickIndex) + if (!root.__activateItemOnSingleClick) + root.activated(modelIndex) + root.doubleClicked(modelIndex) + } + } + + onPressAndHold: { + var pressIndex = __listView.indexAt(0, mouseY + __listView.contentY) + if (pressIndex > -1) { + var modelIndex = modelAdaptor.mapRowToModelIndex(pressIndex) + root.pressAndHold(modelIndex) + } + } + + Keys.forwardTo: [root] + + Keys.onUpPressed: { + event.accepted = __listView.decrementCurrentIndexBlocking() + keySelect(event.modifiers) + } + + Keys.onDownPressed: { + event.accepted = __listView.incrementCurrentIndexBlocking() + keySelect(event.modifiers) + } + + Keys.onRightPressed: { + if (root.currentIndex.valid) + root.expand(currentIndex) + else + event.accepted = false + } + + Keys.onLeftPressed: { + if (root.currentIndex.valid) + root.collapse(currentIndex) + else + event.accepted = false + } + + Keys.onReturnPressed: { + if (root.currentIndex.valid) + root.activated(currentIndex) + else + event.accepted = false + } + + Keys.onPressed: { + __listView.scrollIfNeeded(event.key) + + if (event.key === Qt.Key_A && event.modifiers & Qt.ControlModifier + && !!selection && selectionMode > SelectionMode.SingleSelection) { + var sel = modelAdaptor.selectionForRowRange(0, __listView.count - 1) + selection.select(sel, ItemSelectionModel.SelectCurrent) + } else if (event.key === Qt.Key_Shift) { + shiftPressed = true + } + } + + Keys.onReleased: { + if (event.key === Qt.Key_Shift) + shiftPressed = false + } + } +} diff --git a/src/controls/controls.pro b/src/controls/controls.pro index 78edc913c..6b54028fb 100644 --- a/src/controls/controls.pro +++ b/src/controls/controls.pro @@ -32,6 +32,7 @@ CONTROLS_QML_FILES = \ TabView.qml \ TableView.qml \ TableViewColumn.qml \ + TreeView.qml \ TextArea.qml \ TextField.qml \ ToolBar.qml \ diff --git a/src/controls/doc/images/qtquickcontrols-example-filesystembrowser.png b/src/controls/doc/images/qtquickcontrols-example-filesystembrowser.png Binary files differnew file mode 100644 index 000000000..b28431017 --- /dev/null +++ b/src/controls/doc/images/qtquickcontrols-example-filesystembrowser.png diff --git a/src/controls/doc/images/treeview.png b/src/controls/doc/images/treeview.png Binary files differnew file mode 100644 index 000000000..aac520a3e --- /dev/null +++ b/src/controls/doc/images/treeview.png diff --git a/src/controls/doc/src/qtquickcontrols-examples.qdoc b/src/controls/doc/src/qtquickcontrols-examples.qdoc index 29c8d2b55..36b59bbe7 100644 --- a/src/controls/doc/src/qtquickcontrols-examples.qdoc +++ b/src/controls/doc/src/qtquickcontrols-examples.qdoc @@ -502,3 +502,17 @@ \include examples-run.qdocinc */ + +/*! + \example filesystembrowser + \title Qt Quick Controls - File System Browser Example + \ingroup qtquickcontrols_examples + \brief An example for the TreeView control. + \image qtquickcontrols-example-filesystembrowser.png + + This example project demonstrates the usage of \l {TreeView} from + \l{Qt Quick Controls} - a control to display a tree representation of items + from a model derived from the QAbstractItemModel class. + + The example displays the home path data given by the QFileSystemModel model. +*/ diff --git a/src/controls/plugin.cpp b/src/controls/plugin.cpp index 0a652d7f0..27cdebd65 100644 --- a/src/controls/plugin.cpp +++ b/src/controls/plugin.cpp @@ -51,6 +51,7 @@ #include "Private/qquickspinboxvalidator_p.h" #include "Private/qquickabstractstyle_p.h" #include "Private/qquickcontrolsprivate_p.h" +#include "Private/qquicktreemodeladaptor_p.h" #ifdef QT_WIDGETS_LIB #include <QtQuick/qquickimageprovider.h> @@ -104,7 +105,9 @@ static const struct { { "BusyIndicator", 1, 1 }, - { "TextArea", 1, 3 } + { "TextArea", 1, 3 }, + + { "TreeView", 1, 4 } }; void QtQuickControlsPlugin::registerTypes(const char *uri) @@ -143,11 +146,15 @@ void QtQuickControlsPlugin::initializeEngine(QQmlEngine *engine, const char *uri qmlRegisterType<QQuickSpinBoxValidator>(private_uri, 1, 0, "SpinBoxValidator"); qmlRegisterSingletonType<QQuickTooltip>(private_uri, 1, 0, "Tooltip", QQuickControlsPrivate::registerTooltipModule); qmlRegisterSingletonType<QQuickControlSettings>(private_uri, 1, 0, "Settings", QQuickControlsPrivate::registerSettingsModule); + qmlRegisterType<QQuickTreeModelAdaptor>(private_uri, 1, 0, "TreeModelAdaptor"); qmlRegisterType<QQuickMenu>(private_uri, 1, 0, "MenuPrivate"); qmlRegisterType<QQuickMenuBar>(private_uri, 1, 0, "MenuBarPrivate"); qmlRegisterType<QQuickPopupWindow>(private_uri, 1, 0, "PopupWindow"); + qmlRegisterUncreatableType<QAbstractItemModel>(private_uri, 1, 0, "AbstractItemModel", + QLatin1String("AbstractItemModel is an abstract type.")); + #ifdef QT_WIDGETS_LIB qmlRegisterType<QQuickStyleItem>(private_uri, 1, 0, "StyleItem"); engine->addImageProvider("__tablerow", new QQuickTableRowImageProvider); diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index 810b46787..97cfd32f4 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,3 +1,3 @@ TEMPLATE = subdirs -SUBDIRS += testplugin controls activeFocusOnTab applicationwindow dialogs +SUBDIRS += testplugin controls activeFocusOnTab applicationwindow dialogs qquicktreemodeladaptor controls.depends = testplugin diff --git a/tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro b/tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro new file mode 100644 index 000000000..61b27acb2 --- /dev/null +++ b/tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro @@ -0,0 +1,12 @@ +TEMPLATE = app +TARGET = tst_qquicktreemodeladaptor + +CONFIG += testcase +CONFIG -= app_bundle +QT = core testlib + +INCLUDEPATH += $$PWD/../../../src/controls/Private +HEADERS += $$PWD/../../../src/controls/Private/qquicktreemodeladaptor_p.h \ + $$PWD/../shared/qtestmodel.h +SOURCES += $$PWD/tst_qquicktreemodeladaptor.cpp \ + $$PWD/../../../src/controls/Private/qquicktreemodeladaptor.cpp diff --git a/tests/auto/qquicktreemodeladaptor/tst_qquicktreemodeladaptor.cpp b/tests/auto/qquicktreemodeladaptor/tst_qquicktreemodeladaptor.cpp new file mode 100644 index 000000000..aae0e5eac --- /dev/null +++ b/tests/auto/qquicktreemodeladaptor/tst_qquicktreemodeladaptor.cpp @@ -0,0 +1,1135 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtTest/QtTest> +#include <QtCore> +#include <qquicktreemodeladaptor_p.h> +#include "../shared/qtestmodel.h" + +class tst_QQuickTreeModelAdaptor : public QObject +{ + Q_OBJECT + +public: + void compareData(int row, QQuickTreeModelAdaptor &tma, const QModelIndex &idx, QtTestModel &model, bool expanded = false); + void compareModels(QQuickTreeModelAdaptor &tma, QtTestModel &model); + void expandAndTest(const QModelIndex &idx, QQuickTreeModelAdaptor &tma, bool expandable, int expectedRowCountDifference); + void collapseAndTest(const QModelIndex &idx, QQuickTreeModelAdaptor &tma, bool expandable, int expectedRowCountDifference); + +private slots: + void initTestCase(); + void cleanup(); + + void setModel(); + void modelReset(); + + void dataAccess(); + void dataChange(); + void groupedDataChange(); + + void expandAndCollapse_data(); + void expandAndCollapse(); + void expandAndCollapse2ndLevel(); + + void layoutChange(); + + void removeRows_data(); + void removeRows(); + + void insertRows_data(); + void insertRows(); + + void moveRows_data(); + void moveRows(); + + void selectionForRowRange(); +}; + +void tst_QQuickTreeModelAdaptor::initTestCase() +{ +} + +void tst_QQuickTreeModelAdaptor::cleanup() +{ +} + +void tst_QQuickTreeModelAdaptor::compareData(int row, QQuickTreeModelAdaptor &tma, const QModelIndex &modelIdx, QtTestModel &model, bool expanded) +{ + const QModelIndex &tmaIdx = tma.index(row); + QCOMPARE(tma.mapToModel(tmaIdx), modelIdx); + QCOMPARE(tma.data(tmaIdx, Qt::DisplayRole).toString(), model.displayData(modelIdx)); + QCOMPARE(tma.data(tmaIdx, QQuickTreeModelAdaptor::DepthRole).toInt(), model.level(modelIdx)); + QCOMPARE(tma.data(tmaIdx, QQuickTreeModelAdaptor::ExpandedRole).toBool(), expanded); + QCOMPARE(tma.data(tmaIdx, QQuickTreeModelAdaptor::HasChildrenRole).toBool(), model.hasChildren(modelIdx)); +} + +void tst_QQuickTreeModelAdaptor::expandAndTest(const QModelIndex &idx, QQuickTreeModelAdaptor &tma, bool expandable, + int expectedRowCountDifference) +{ + QSignalSpy rowsAboutToBeInsertedSpy(&tma, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int))); + QSignalSpy rowsInsertedSpy(&tma, SIGNAL(rowsInserted(QModelIndex,int,int))); + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); + + int oldRowCount = tma.rowCount(); + tma.expand(idx); + QCOMPARE(tma.isExpanded(idx), expandable); + + const QModelIndex &tmaIdx = tma.index(tma.itemIndex(idx)); + QCOMPARE(tma.data(tmaIdx, QQuickTreeModelAdaptor::ExpandedRole).toBool(), expandable); + + if (expandable) { + // Rows were added below the parent + QCOMPARE(tma.rowCount(), oldRowCount + expectedRowCountDifference); + QCOMPARE(rowsAboutToBeInsertedSpy.count(), rowsInsertedSpy.count()); + QVERIFY(rowsInsertedSpy.count() > 0); + if (rowsInsertedSpy.count() == 1) { + const QVariantList &rowsAboutToBeInsertedArgs = rowsAboutToBeInsertedSpy.takeFirst(); + const QVariantList &rowsInsertedArgs = rowsInsertedSpy.takeFirst(); + for (int i = 0; i < rowsInsertedArgs.count(); i++) + QCOMPARE(rowsAboutToBeInsertedArgs.at(i), rowsInsertedArgs.at(i)); + QCOMPARE(rowsInsertedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsInsertedArgs.at(1).toInt(), tma.itemIndex(idx) + 1); + QCOMPARE(rowsInsertedArgs.at(2).toInt(), tma.itemIndex(idx) + expectedRowCountDifference); + } + + // Data changed for the parent's ExpandedRole (value checked above) + QCOMPARE(dataChangedSpy.count(), 1); + const QVariantList &dataChangedArgs = dataChangedSpy.first(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), QVector<int>(1, QQuickTreeModelAdaptor::ExpandedRole)); + } else { + QCOMPARE(tma.rowCount(), oldRowCount); + QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); + QCOMPARE(rowsInsertedSpy.count(), 0); + } +} + +void tst_QQuickTreeModelAdaptor::collapseAndTest(const QModelIndex &idx, QQuickTreeModelAdaptor &tma, + bool expandable, int expectedRowCountDifference) +{ + QSignalSpy rowsAboutToBeRemovedSpy(&tma, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int))); + QSignalSpy rowsRemovedSpy(&tma, SIGNAL(rowsRemoved(QModelIndex,int,int))); + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); + + int oldRowCount = tma.rowCount(); + tma.collapse(idx); + QVERIFY(!tma.isExpanded(idx)); + + const QModelIndex &tmaIdx = tma.index(tma.itemIndex(idx)); + if (tmaIdx.isValid()) + QCOMPARE(tma.data(tmaIdx, QQuickTreeModelAdaptor::ExpandedRole).toBool(), false); + + if (expandable) { + // Rows were removed below the parent + QCOMPARE(tma.rowCount(), oldRowCount - expectedRowCountDifference); + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.count(), 1); + const QVariantList &rowsAboutToBeRemovedArgs = rowsAboutToBeRemovedSpy.takeFirst(); + const QVariantList &rowsRemovedArgs = rowsRemovedSpy.takeFirst(); + for (int i = 0; i < rowsRemovedArgs.count(); i++) + QCOMPARE(rowsAboutToBeRemovedArgs.at(i), rowsRemovedArgs.at(i)); + QCOMPARE(rowsRemovedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsRemovedArgs.at(1).toInt(), tma.itemIndex(idx) + 1); + QCOMPARE(rowsRemovedArgs.at(2).toInt(), tma.itemIndex(idx) + expectedRowCountDifference); + + // Data changed for the parent's ExpandedRole (value checked above) + QCOMPARE(dataChangedSpy.count(), 1); + const QVariantList &dataChangedArgs = dataChangedSpy.first(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), QVector<int>(1, QQuickTreeModelAdaptor::ExpandedRole)); + } else { + QCOMPARE(tma.rowCount(), oldRowCount); + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); + QCOMPARE(rowsRemovedSpy.count(), 0); + } +} + +void tst_QQuickTreeModelAdaptor::compareModels(QQuickTreeModelAdaptor &tma, QtTestModel &model) +{ + QModelIndex parent; + QStack<QModelIndex> parents; + QModelIndex idx = model.index(0, 0); + int modelVisibleRows = model.rowCount(parent); + for (int i = 0; i < tma.rowCount(); i++) { + bool expanded = tma.isExpanded(i); + compareData(i, tma, idx, model, expanded); + if (expanded) { + parents.push(parent); + parent = idx; + modelVisibleRows += model.rowCount(parent); + idx = model.index(0, 0, parent); + } else { + while (idx.row() == model.rowCount(parent) - 1) { + if (parents.isEmpty()) + break; + idx = parent; + parent = parents.pop(); + } + idx = model.index(idx.row() + 1, 0, parent); + } + } + QCOMPARE(tma.rowCount(), modelVisibleRows); + + // Duplicates the model inspection above, but provides extra tests + QVERIFY(tma.testConsistency()); +} + +void tst_QQuickTreeModelAdaptor::setModel() +{ + QtTestModel model(5, 1); + QQuickTreeModelAdaptor tma; + + QSignalSpy modelChangedSpy(&tma, SIGNAL(modelChanged(QAbstractItemModel*))); + tma.setModel(&model); + QCOMPARE(modelChangedSpy.count(), 1); + QCOMPARE(tma.model(), &model); + + // Set same model twice + tma.setModel(&model); + QCOMPARE(modelChangedSpy.count(), 1); + + modelChangedSpy.clear(); + tma.setModel(0); + QCOMPARE(modelChangedSpy.count(), 1); + QCOMPARE(tma.model(), static_cast<QAbstractItemModel *>(0)); +} + +void tst_QQuickTreeModelAdaptor::modelReset() +{ + QtTestModel model(5, 1); + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + QSignalSpy modelAboutToBeResetSpy(&tma, SIGNAL(modelAboutToBeReset())); + QSignalSpy modelResetSpy(&tma, SIGNAL(modelReset())); + + // Nothing expanded + model.resetModel(); + QCOMPARE(modelAboutToBeResetSpy.count(), 1); + QCOMPARE(modelResetSpy.count(), 1); + QCOMPARE(tma.rowCount(), model.rowCount()); + compareModels(tma, model); + + // Expanded items should not be anymore + tma.expand(model.index(0, 0)); + tma.expand(model.index(2, 0)); + tma.expand(model.index(2, 0, model.index(2, 0))); + modelAboutToBeResetSpy.clear(); + modelResetSpy.clear(); + model.resetModel(); + QCOMPARE(modelAboutToBeResetSpy.count(), 1); + QCOMPARE(modelResetSpy.count(), 1); + QCOMPARE(tma.rowCount(), model.rowCount()); + compareModels(tma, model); +} + +void tst_QQuickTreeModelAdaptor::dataAccess() +{ + QtTestModel model(5, 1); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + QCOMPARE(tma.rowCount(), model.rowCount()); + compareModels(tma, model); + + QModelIndex parentIdx = model.index(2, 0); + QVERIFY(model.hasChildren(parentIdx)); + tma.expand(parentIdx); + QVERIFY(tma.isExpanded(parentIdx)); + QCOMPARE(tma.rowCount(), model.rowCount() + model.rowCount(parentIdx)); + compareModels(tma, model); + + tma.collapse(parentIdx); + QCOMPARE(tma.rowCount(), model.rowCount()); + compareModels(tma, model); +} + +void tst_QQuickTreeModelAdaptor::dataChange() +{ + QtTestModel model(5, 1); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); + const QModelIndex &idx = model.index(2, 0); + model.setData(idx, QVariant(), Qt::DisplayRole); + QCOMPARE(dataChangedSpy.count(), 1); + const QVariantList &dataChangedArgs = dataChangedSpy.first(); + const QModelIndex &tmaIdx = tma.index(tma.itemIndex(idx)); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), QVector<int>(1, Qt::DisplayRole)); + compareModels(tma, model); + + { + // Non expanded children shouldn't emit any signal + dataChangedSpy.clear(); + const QModelIndex &childIdx = model.index(4, 0, idx); + model.setData(childIdx, QVariant(), Qt::DisplayRole); + QCOMPARE(dataChangedSpy.count(), 0); + compareModels(tma, model); + + // But expanded children should + tma.expand(idx); + QVERIFY(tma.isExpanded(idx)); + dataChangedSpy.clear(); // expand() emits dataChanged() with ExpandedRole + model.setData(childIdx, QVariant(), Qt::DisplayRole); + QCOMPARE(dataChangedSpy.count(), 1); + const QVariantList &dataChangedArgs = dataChangedSpy.first(); + const QModelIndex &tmaIdx = tma.index(tma.itemIndex(childIdx)); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), QVector<int>(1, Qt::DisplayRole)); + compareModels(tma, model); + } +} + +void tst_QQuickTreeModelAdaptor::groupedDataChange() +{ + QtTestModel model(10, 1); + const QModelIndex &topLeftIdx = model.index(1, 0); + const QModelIndex &bottomRightIdx = model.index(7, 0); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); + const QVector<int> roles(1, Qt::DisplayRole); + + { + // No expanded items + model.groupedSetData(topLeftIdx, bottomRightIdx, roles); + QCOMPARE(dataChangedSpy.count(), 1); + compareModels(tma, model); + + const QModelIndex &tmaTLIdx = tma.index(tma.itemIndex(topLeftIdx)); + const QModelIndex &tmaBRIdx = tma.index(tma.itemIndex(bottomRightIdx)); + const QVariantList &dataChangedArgs = dataChangedSpy.first(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaTLIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaBRIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + } + + // One item expanded in the group range + const QModelIndex &expandedIdx = model.index(4, 0); + tma.expand(expandedIdx); + QVERIFY(tma.isExpanded(expandedIdx)); + + for (int i = 0; i < 2; i++) { + const QModelIndex &tmaTLIdx = tma.index(tma.itemIndex(topLeftIdx)); + const QModelIndex &tmaExpandedIdx = tma.index(tma.itemIndex(expandedIdx)); + const QModelIndex &tmaExpandedSiblingIdx = tma.index(tma.itemIndex(expandedIdx.sibling(expandedIdx.row() + 1, 0))); + const QModelIndex &tmaBRIdx = tma.index(tma.itemIndex(bottomRightIdx)); + + dataChangedSpy.clear(); // expand() sends a dataChaned() signal + model.groupedSetData(topLeftIdx, bottomRightIdx, roles); + QCOMPARE(dataChangedSpy.count(), 2); + compareModels(tma, model); + + QVariantList dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaTLIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaExpandedIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaBRIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + + // Further expanded descendants should not change grouping + tma.expand(model.index(0, 0, expandedIdx)); + QVERIFY(tma.isExpanded(expandedIdx)); + } + tma.collapse(model.index(0, 0, expandedIdx)); + + // Let's expand one more and see what happens... + const QModelIndex &otherExpandedIdx = model.index(6, 0); + tma.expand(otherExpandedIdx); + QVERIFY(tma.isExpanded(otherExpandedIdx)); + + for (int i = 0; i < 3; i++) { + const QModelIndex &tmaTLIdx = tma.index(tma.itemIndex(topLeftIdx)); + const QModelIndex &tmaExpandedIdx = tma.index(tma.itemIndex(expandedIdx)); + const QModelIndex &tmaExpandedSiblingIdx = tma.index(tma.itemIndex(expandedIdx.sibling(expandedIdx.row() + 1, 0))); + const QModelIndex &tmaOtherExpandedIdx = tma.index(tma.itemIndex(otherExpandedIdx)); + const QModelIndex &tmaOtherExpandedSiblingIdx = tma.index(tma.itemIndex(otherExpandedIdx.sibling(otherExpandedIdx.row() + 1, 0))); + const QModelIndex &tmaBRIdx = tma.index(tma.itemIndex(bottomRightIdx)); + + dataChangedSpy.clear(); // expand() sends a dataChaned() signal + model.groupedSetData(topLeftIdx, bottomRightIdx, roles); + QCOMPARE(dataChangedSpy.count(), 3); + compareModels(tma, model); + + QVariantList dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaTLIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaExpandedIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaOtherExpandedIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaOtherExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaBRIdx); + QCOMPARE(dataChangedArgs.at(2).value<QVector<int> >(), roles); + + // Further expanded descendants should not change grouping + if (i == 0) { + tma.expand(model.index(0, 0, expandedIdx)); + QVERIFY(tma.isExpanded(expandedIdx)); + } else { + tma.expand(model.index(0, 0, otherExpandedIdx)); + QVERIFY(tma.isExpanded(expandedIdx)); + } + } +} + +void tst_QQuickTreeModelAdaptor::expandAndCollapse_data() +{ + QTest::addColumn<int>("parentRow"); + QTest::newRow("First") << 0; + QTest::newRow("Middle") << 2; + QTest::newRow("Last") << 4; + QTest::newRow("Non expandable") << 3; +} + +void tst_QQuickTreeModelAdaptor::expandAndCollapse() +{ + QFETCH(int, parentRow); + QtTestModel model(5, 1); + const QModelIndex &parentIdx = model.index(parentRow, 0); + bool expandable = model.hasChildren(parentIdx); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + expandAndTest(parentIdx, tma, expandable, model.rowCount(parentIdx)); + compareModels(tma, model); + + collapseAndTest(parentIdx, tma, expandable, model.rowCount(parentIdx)); + compareModels(tma, model); +} + +void tst_QQuickTreeModelAdaptor::expandAndCollapse2ndLevel() +{ + const int expandRows[] = { 0, 2, 4, 3 }; + const int expandRowsCount = sizeof(expandRows) / sizeof(expandRows[0]); + for (int i = 0; i < expandRowsCount - 1; i++) { // Skip last non-expandable row + QtTestModel model(5, 1); + const QModelIndex &parentIdx = model.index(expandRows[i], 0); + QVERIFY(model.hasChildren(parentIdx)); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + tma.expand(parentIdx); + QVERIFY(tma.isExpanded(parentIdx)); + QCOMPARE(tma.rowCount(), model.rowCount() + model.rowCount(parentIdx)); + + for (int j = 0; j < expandRowsCount; j++) { + const QModelIndex &childIdx = model.index(expandRows[j], 0, parentIdx); + bool expandable = model.hasChildren(childIdx); + + // Expand child + expandAndTest(childIdx, tma, expandable, model.rowCount(childIdx)); + compareModels(tma, model); + // Collapse child + collapseAndTest(childIdx, tma, expandable, model.rowCount(childIdx)); + compareModels(tma, model); + + // Expand child again + expandAndTest(childIdx, tma, expandable, model.rowCount(childIdx)); + compareModels(tma, model); + // Collapse parent -> child node invisible, but expanded + collapseAndTest(parentIdx, tma, true, model.rowCount(parentIdx) + model.rowCount(childIdx)); + compareModels(tma, model); + QCOMPARE(tma.isExpanded(childIdx), expandable); + // Expand parent again + expandAndTest(parentIdx, tma, true, model.rowCount(parentIdx) + model.rowCount(childIdx)); + compareModels(tma, model); + + // Collapse parent -> child node invisible, but expanded + collapseAndTest(parentIdx, tma, true, model.rowCount(parentIdx) + model.rowCount(childIdx)); + compareModels(tma, model); + QCOMPARE(tma.isExpanded(childIdx), expandable); + // Collapse child -> nothing should change + collapseAndTest(childIdx, tma, false, 0); + compareModels(tma, model); + // Expand parent again + expandAndTest(parentIdx, tma, true, model.rowCount(parentIdx)); + compareModels(tma, model); + + // Expand child, one last time + expandAndTest(childIdx, tma, expandable, model.rowCount(childIdx)); + compareModels(tma, model); + // Collapse child, and done + collapseAndTest(childIdx, tma, expandable, model.rowCount(childIdx)); + compareModels(tma, model); + } + } +} + +void tst_QQuickTreeModelAdaptor::layoutChange() +{ + QtTestModel model(5, 1); + const QModelIndex &idx = model.index(0, 0); + const QModelIndex &idx2 = model.index(2, 0); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + // Nothing expanded + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>))); + model.changeLayout(); + QCOMPARE(dataChangedSpy.count(), 1); + QVariantList dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(0)); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.rowCount() - 1)); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + compareModels(tma, model); + + // One item expanded + tma.expand(idx); + QVERIFY(tma.isExpanded(idx)); + dataChangedSpy.clear(); + model.changeLayout(); + QCOMPARE(dataChangedSpy.count(), 1); + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(0)); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.rowCount() - 1)); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + compareModels(tma, model); + + // One parent layout change, expanded + dataChangedSpy.clear(); + QList<QPersistentModelIndex> parents; + parents << idx; + model.changeLayout(parents); + QCOMPARE(dataChangedSpy.count(), 1); + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(tma.itemIndex(model.index(0, 0, idx)))); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.itemIndex(model.index(model.rowCount(idx) - 1, 0, idx)))); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + compareModels(tma, model); + + // One parent layout change, collapsed + tma.collapse(idx); + dataChangedSpy.clear(); + model.changeLayout(parents); + QCOMPARE(dataChangedSpy.count(), 0); + compareModels(tma, model); + + // Two-parent layout change, both collapsed + parents << idx2; + dataChangedSpy.clear(); + model.changeLayout(parents); + QCOMPARE(dataChangedSpy.count(), 0); + compareModels(tma, model); + + // Two-parent layout change, only one expanded + tma.expand(idx2); + QVERIFY(tma.isExpanded(idx2)); + dataChangedSpy.clear(); + model.changeLayout(parents); + QCOMPARE(dataChangedSpy.count(), 1); + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(tma.itemIndex(model.index(0, 0, idx2)))); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.itemIndex(model.index(model.rowCount(idx2) - 1, 0, idx2)))); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + compareModels(tma, model); + + // Two-parent layout change, both expanded + tma.expand(idx); + QVERIFY(tma.isExpanded(idx)); + dataChangedSpy.clear(); + model.changeLayout(parents); + QCOMPARE(dataChangedSpy.count(), 2); + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(tma.itemIndex(model.index(0, 0, idx)))); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.itemIndex(model.index(model.rowCount(idx) - 1, 0, idx)))); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tma.index(tma.itemIndex(model.index(0, 0, idx2)))); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tma.index(tma.itemIndex(model.index(model.rowCount(idx2) - 1, 0, idx2)))); + QVERIFY(dataChangedArgs.at(2).value<QVector<int> >().isEmpty()); + compareModels(tma, model); +} + +static const int ModelRowCount = 9; + +void tst_QQuickTreeModelAdaptor::removeRows_data() +{ + QTest::addColumn<int>("removeFromRow"); + QTest::addColumn<int>("removeCount"); + QTest::addColumn<int>("removeParentRow"); + QTest::addColumn<int>("expandRow"); + QTest::addColumn<int>("expandParentRow"); + QTest::addColumn<int>("expectedRemovedCount"); + + QTest::newRow("Nothing expanded, remove 1st row") << 0 << 1 << -1 << -1 << -1 << 1; + QTest::newRow("Expand 1st row, remove 1st row") << 0 << 1 << -1 << 0 << -1 << 1 + ModelRowCount; + QTest::newRow("Expand last row, remove 1st row") << 0 << 1 << -1 << ModelRowCount - 1 << -1 << 1; + QTest::newRow("Nothing expanded, remove last row") << ModelRowCount - 1 << 1 << -1 << -1 << -1 << 1; + QTest::newRow("Expand 1st row, remove last row") << ModelRowCount - 1 << 1 << -1 << 0 << -1 << 1; + QTest::newRow("Expand last row, remove last row") << ModelRowCount - 1 << 1 << -1 << ModelRowCount - 1 << -1 << 1 + ModelRowCount; + QTest::newRow("Remove child row, parent collapsed") << 2 << 1 << 0 << -1 << -1 << 0; + QTest::newRow("Remove child row, parent expanded") << 2 << 1 << 0 << 0 << -1 << 1; + QTest::newRow("Remove several rows, nothing expanded") << 2 << 5 << -1 << -1 << -1 << 5; + QTest::newRow("Remove several rows, 1st row expanded") << 2 << 5 << -1 << 0 << -1 << 5; + QTest::newRow("Remove several rows, last row expanded") << 2 << 5 << -1 << ModelRowCount - 1 << -1 << 5; + QTest::newRow("Remove several rows, one of them expanded") << 2 << 5 << -1 << 4 << -1 << 5 + ModelRowCount; + QTest::newRow("Remove all rows, nothing expanded") << 0 << ModelRowCount << -1 << -1 << -1 << ModelRowCount; + QTest::newRow("Remove all rows, 1st row expanded") << 0 << ModelRowCount << -1 << 0 << -1 << ModelRowCount * 2; + QTest::newRow("Remove all rows, last row expanded") << 0 << ModelRowCount << -1 << ModelRowCount - 1 << -1 << ModelRowCount * 2; + QTest::newRow("Remove all rows, random one expanded") << 0 << ModelRowCount << -1 << 4 << -1 << ModelRowCount * 2; +} + +void tst_QQuickTreeModelAdaptor::removeRows() +{ + QFETCH(int, removeFromRow); + QFETCH(int, removeCount); + QFETCH(int, removeParentRow); + QFETCH(int, expandRow); + QFETCH(int, expandParentRow); + QFETCH(int, expectedRemovedCount); + + QtTestModel model(ModelRowCount, 1); + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + const QModelIndex &expandParentIdx = expandParentRow == -1 ? QModelIndex() : model.index(expandParentRow, 0); + if (expandParentIdx.isValid()) { + tma.expand(expandParentIdx); + QVERIFY(tma.isExpanded(expandParentIdx)); + } + const QModelIndex &expandIdx = model.index(expandRow, 0, expandParentIdx); + if (expandIdx.isValid()) { + tma.expand(expandIdx); + QVERIFY(tma.isExpanded(expandIdx)); + } + + const QModelIndex &removeParentIdx = removeParentRow == -1 ? QModelIndex() : model.index(removeParentRow, 0); + const QModelIndex &removeIdx = model.index(removeFromRow, 0, removeParentIdx); + int tmaItemIdx = tma.itemIndex(removeIdx); + + QSignalSpy rowsAboutToBeRemovedSpy(&tma, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int))); + QSignalSpy rowsRemovedSpy(&tma, SIGNAL(rowsRemoved(const QModelIndex&, int, int))); + model.removeRows(removeFromRow, removeCount, removeParentIdx); + if (expectedRemovedCount == 0) { + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); + QCOMPARE(rowsRemovedSpy.count(), 0); + } else { + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.count(), 1); + QVariantList rowsAboutToBeRemovedArgs = rowsAboutToBeRemovedSpy.first(); + QVariantList rowsRemovedArgs = rowsRemovedSpy.first(); + QCOMPARE(rowsAboutToBeRemovedArgs, rowsRemovedArgs); + QCOMPARE(rowsAboutToBeRemovedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeRemovedArgs.at(1).toInt(), tmaItemIdx); + QCOMPARE(rowsAboutToBeRemovedArgs.at(2).toInt(), tmaItemIdx + expectedRemovedCount - 1); + } +} + +void tst_QQuickTreeModelAdaptor::insertRows_data() +{ + QTest::addColumn<int>("insertFromRow"); + QTest::addColumn<int>("insertCount"); + QTest::addColumn<int>("insertParentRow"); + QTest::addColumn<int>("expandRow"); + QTest::addColumn<int>("expandParentRow"); + QTest::addColumn<int>("expectedInsertedCount"); + + QTest::newRow("Nothing expanded, insert 1st row") << 0 << 1 << -1 << -1 << -1 << 1; + QTest::newRow("Expand 1st row, insert 1st row") << 0 << 1 << -1 << 0 << -1 << 1; + QTest::newRow("Expand last row, insert 1st row") << 0 << 1 << -1 << ModelRowCount - 1 << -1 << 1; + QTest::newRow("Nothing expanded, insert before the last row") << ModelRowCount - 1 << 1 << -1 << -1 << -1 << 1; + QTest::newRow("Nothing expanded, insert after the last row") << ModelRowCount << 1 << -1 << -1 << -1 << 1; + QTest::newRow("Expand 1st row, insert before the last row") << ModelRowCount - 1 << 1 << -1 << 0 << -1 << 1; + QTest::newRow("Expand 1st row, insert after the last row") << ModelRowCount << 1 << -1 << 0 << -1 << 1; + QTest::newRow("Expand last row, insert before the last row") << ModelRowCount - 1 << 1 << -1 << ModelRowCount - 1 << -1 << 1; + QTest::newRow("Expand last row, insert after the last row") << ModelRowCount << 1 << -1 << ModelRowCount - 1 << -1 << 1; + QTest::newRow("Insert child row, parent collapsed") << 2 << 1 << 0 << -1 << -1 << 0; + QTest::newRow("Insert child row, parent expanded") << 2 << 1 << 0 << 0 << -1 << 1; + QTest::newRow("Insert several rows, nothing expanded") << 2 << 5 << -1 << -1 << -1 << 5; + QTest::newRow("Insert several rows, 1st row expanded") << 2 << 5 << -1 << 0 << -1 << 5; + QTest::newRow("Insert several rows, last row expanded") << 2 << 5 << -1 << ModelRowCount - 1 << -1 << 5; +} + +void tst_QQuickTreeModelAdaptor::insertRows() +{ + QFETCH(int, insertFromRow); + QFETCH(int, insertCount); + QFETCH(int, insertParentRow); + QFETCH(int, expandRow); + QFETCH(int, expandParentRow); + QFETCH(int, expectedInsertedCount); + + QtTestModel model(ModelRowCount, 1); + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + const QModelIndex &expandParentIdx = expandParentRow == -1 ? QModelIndex() : model.index(expandParentRow, 0); + if (expandParentIdx.isValid()) { + tma.expand(expandParentIdx); + QVERIFY(tma.isExpanded(expandParentIdx)); + } + const QModelIndex &expandIdx = model.index(expandRow, 0, expandParentIdx); + if (expandIdx.isValid()) { + tma.expand(expandIdx); + QVERIFY(tma.isExpanded(expandIdx)); + } + + const QModelIndex &insertParentIdx = insertParentRow == -1 ? QModelIndex() : model.index(insertParentRow, 0); + const QModelIndex &insertIdx = model.index(insertFromRow, 0, insertParentIdx); + int tmaItemIdx = insertFromRow == model.rowCount(insertParentIdx) ? tma.rowCount() : tma.itemIndex(insertIdx); + + QSignalSpy rowsAboutToBeInsertedSpy(&tma, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int))); + QSignalSpy rowsInsertedSpy(&tma, SIGNAL(rowsInserted(const QModelIndex&, int, int))); + model.insertRows(insertFromRow, insertCount, insertParentIdx); + if (expectedInsertedCount == 0) { + QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); + QCOMPARE(rowsInsertedSpy.count(), 0); + } else { + QCOMPARE(rowsAboutToBeInsertedSpy.count(), 1); + QCOMPARE(rowsInsertedSpy.count(), 1); + QVariantList rowsAboutToBeInsertedArgs = rowsAboutToBeInsertedSpy.first(); + QVariantList rowsInsertedArgs = rowsInsertedSpy.first(); + QCOMPARE(rowsAboutToBeInsertedArgs, rowsInsertedArgs); + QCOMPARE(rowsAboutToBeInsertedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeInsertedArgs.at(1).toInt(), tmaItemIdx); + QCOMPARE(rowsAboutToBeInsertedArgs.at(2).toInt(), tmaItemIdx + expectedInsertedCount - 1); + QCOMPARE(tma.itemIndex(model.index(insertFromRow, 0, insertParentIdx)), tmaItemIdx); + } +} + +enum MoveSignalType { + RowsMoved = 0, RowsInserted, RowsRemoved +}; + +void tst_QQuickTreeModelAdaptor::moveRows_data() +{ + QTest::addColumn<int>("sourceRow"); + QTest::addColumn<bool>("expandSource"); + QTest::addColumn<int>("moveCount"); + QTest::addColumn<int>("sourceParentRow"); + QTest::addColumn<bool>("expandSourceParent"); + QTest::addColumn<int>("destRow"); + QTest::addColumn<bool>("expandDest"); + QTest::addColumn<int>("destParentRow"); + QTest::addColumn<bool>("expandDestParent"); + QTest::addColumn<int>("expandRow"); + QTest::addColumn<int>("expandParentRow"); + QTest::addColumn<int>("signalType"); + QTest::addColumn<int>("expectedMovedCount"); + + QTest::newRow("From and to top-level parent") + << 0 << false << 1 << -1 << false + << 3 << false << -1 << false + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to top-level parent, expanded") + << 0 << true << 1 << -1 << false + << 3 << false << -1 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 1; + QTest::newRow("From and to top-level parent, backwards") + << 4 << false << 1 << -1 << false + << 0 << false << -1 << false + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to top-level parent, expanded, backwards") + << 4 << true << 1 << -1 << false + << 0 << false << -1 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 1; + QTest::newRow("Moving between collapsed parents") + << 0 << false << 1 << 0 << false + << 0 << false << 2 << false + << -1 << -1 << (int)RowsMoved << 0; + QTest::newRow("From expanded parent to collapsed parent") + << 0 << false << 1 << 0 << true + << 0 << false << 2 << false + << -1 << -1 << (int)RowsRemoved << 1; + QTest::newRow("From collapsed parent to expanded parent") + << 0 << false << 1 << 0 << false + << 0 << false << 2 << true + << -1 << -1 << (int)RowsInserted << 1; + QTest::newRow("From and to same expanded parent") + << 0 << false << 1 << 0 << true + << 2 << false << 0 << false + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From expanded parent to collapsed parent, expanded row") + << 0 << true << 1 << 0 << true + << 0 << false << 2 << false + << -1 << -1 << (int)RowsRemoved << ModelRowCount + 1; + QTest::newRow("From collapsed parent to expanded parent, expanded row") + << 0 << true << 1 << 0 << false + << 0 << false << 2 << true + << -1 << -1 << (int)RowsInserted << ModelRowCount + 1; + QTest::newRow("From and to same expanded parent, expanded row, forward") + << 0 << true << 1 << 0 << true + << 5 << false << 0 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 1; + QTest::newRow("From and to same expanded parent, expanded row, last row") + << 0 << true << 1 << 0 << true + << ModelRowCount << false << 0 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 1; + QTest::newRow("From and to same expanded parent, expanded row, several") + << 0 << true << 3 << 0 << true + << 5 << false << 0 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 3; + QTest::newRow("From and to same expanded parent, expanded row, backward") + << 6 << true << 1 << 0 << true + << 0 << false << 0 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 1; + QTest::newRow("From and to same expanded parent, expanded row, several, backward") + << 6 << true << 2 << 0 << true + << 0 << false << 0 << false + << -1 << -1 << (int)RowsMoved << ModelRowCount + 2; + QTest::newRow("From and to different expanded parents") + << 0 << false << 1 << 0 << true + << 1 << false << 4 << true + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to different expanded parents, backward") + << 0 << false << 1 << 4 << true + << 2 << false << 0 << true + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to different expanded parents, up in level") + << 0 << false << 1 << 0 << true + << 5 << true << -1 << true + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to different expanded parents, up in level, backwards") + << 0 << false << 1 << 4 << true + << 1 << false << -1 << true + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to different expanded parents, up in level, as 1st item") + << 0 << false << 1 << 0 << true + << 0 << false << -1 << true + << -1 << -1 << (int)RowsMoved << 1; + QTest::newRow("From and to different expanded parents, backward, up in level") + << 0 << false << 1 << 4 << true + << 2 << false << 0 << true + << -1 << -1 << (int)RowsMoved << 1; +} + +void tst_QQuickTreeModelAdaptor::moveRows() +{ + QFETCH(int, sourceRow); + QFETCH(bool, expandSource); + QFETCH(int, moveCount); + QFETCH(int, sourceParentRow); + QFETCH(bool, expandSourceParent); + QFETCH(int, destRow); + QFETCH(bool, expandDest); + QFETCH(int, destParentRow); + QFETCH(bool, expandDestParent); + QFETCH(int, expandRow); + QFETCH(int, expandParentRow); + QFETCH(int, signalType); + QFETCH(int, expectedMovedCount); + + QtTestModel model(ModelRowCount, 1); + model.alternateChildlessRows = false; + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + const QModelIndex &expandParentIdx = expandParentRow == -1 ? QModelIndex() : model.index(expandParentRow, 0); + if (expandParentIdx.isValid()) { + tma.expand(expandParentIdx); + QVERIFY(tma.isExpanded(expandParentIdx)); + } + const QModelIndex &expandIdx = model.index(expandRow, 0, expandParentIdx); + if (expandIdx.isValid()) { + tma.expand(expandIdx); + QVERIFY(tma.isExpanded(expandIdx)); + } + + const QModelIndex &sourceParentIdx = sourceParentRow == -1 ? QModelIndex() : model.index(sourceParentRow, 0); + if (expandSourceParent && sourceParentIdx.isValid()) { + tma.expand(sourceParentIdx); + QVERIFY(tma.isExpanded(sourceParentIdx)); + } + const QModelIndex &sourceIdx = model.index(sourceRow, 0, sourceParentIdx); + if (expandSource) { + tma.expand(sourceIdx); + QVERIFY(tma.isExpanded(sourceIdx)); + } + + const QModelIndex &destParentIdx = destParentRow == -1 ? QModelIndex() : model.index(destParentRow, 0); + if (expandDestParent && destParentIdx.isValid()) { + tma.expand(destParentIdx); + QVERIFY(tma.isExpanded(destParentIdx)); + } + const QModelIndex &destIdx = model.index(destRow, 0, destParentIdx); + if (expandDest) { + tma.expand(destIdx); + QVERIFY(tma.isExpanded(destIdx)); + } + + int tmaSourceItemIdx = signalType == RowsInserted ? -1 // Not tested if RowsInserted + : tma.itemIndex(sourceIdx); + int tmaDestItemIdx = signalType == RowsRemoved ? -1 : // Not tested if RowsRemoved + destRow == model.rowCount(destParentIdx) ? -1 /* FIXME */ : tma.itemIndex(destIdx); + + QSignalSpy rowsAboutToBeMovedSpy(&tma, SIGNAL(rowsAboutToBeMoved(QModelIndex,int,int,QModelIndex,int))); + QSignalSpy rowsMovedSpy(&tma, SIGNAL(rowsMoved(QModelIndex,int,int,QModelIndex,int))); + QSignalSpy rowsAboutToBeInsertedSpy(&tma, SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int))); + QSignalSpy rowsInsertedSpy(&tma, SIGNAL(rowsInserted(const QModelIndex&, int, int))); + QSignalSpy rowsAboutToBeRemovedSpy(&tma, SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int))); + QSignalSpy rowsRemovedSpy(&tma, SIGNAL(rowsRemoved(const QModelIndex&, int, int))); + + QVERIFY(model.moveRows(sourceParentIdx, sourceRow, moveCount, destParentIdx, destRow)); + + if (signalType != RowsMoved || expectedMovedCount == 0) { + QCOMPARE(rowsAboutToBeMovedSpy.count(), 0); + QCOMPARE(rowsMovedSpy.count(), 0); + } + if (signalType != RowsInserted || expectedMovedCount == 0) { + QCOMPARE(rowsAboutToBeInsertedSpy.count(), 0); + QCOMPARE(rowsInsertedSpy.count(), 0); + } + if (signalType != RowsRemoved || expectedMovedCount == 0) { + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); + QCOMPARE(rowsRemovedSpy.count(), 0); + } + + if (expectedMovedCount != 0) { + if (signalType == RowsMoved) { + QCOMPARE(rowsAboutToBeMovedSpy.count(), 1); + QCOMPARE(rowsMovedSpy.count(), 1); + QVariantList rowsAboutToBeMovedArgs = rowsAboutToBeMovedSpy.first(); + QVariantList rowsMovedArgs = rowsMovedSpy.first(); + QCOMPARE(rowsAboutToBeMovedArgs, rowsMovedArgs); + QCOMPARE(rowsAboutToBeMovedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeMovedArgs.at(1).toInt(), tmaSourceItemIdx); + QCOMPARE(rowsAboutToBeMovedArgs.at(2).toInt(), tmaSourceItemIdx + expectedMovedCount - 1); + QCOMPARE(rowsAboutToBeMovedArgs.at(3).toModelIndex(), QModelIndex()); + if (tmaDestItemIdx != -1) + QCOMPARE(rowsAboutToBeMovedArgs.at(4).toInt(), tmaDestItemIdx); + } else if (signalType == RowsInserted) { + // We only test with one level of expanded children here, so we can do + // exhaustive testing depending on whether the moved row is expanded. + int signalCount = expandSource ? 2 : 1; + QCOMPARE(rowsAboutToBeInsertedSpy.count(), signalCount); + QCOMPARE(rowsInsertedSpy.count(), signalCount); + QVariantList rowsAboutToBeInsertedArgs = rowsAboutToBeInsertedSpy.takeFirst(); + QVariantList rowsInsertedArgs = rowsInsertedSpy.takeFirst(); + QCOMPARE(rowsAboutToBeInsertedArgs, rowsInsertedArgs); + QCOMPARE(rowsAboutToBeInsertedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeInsertedArgs.at(1).toInt(), tmaDestItemIdx); + if (expandSource) { + QCOMPARE(rowsAboutToBeInsertedArgs.at(2).toInt(), tmaDestItemIdx); + rowsAboutToBeInsertedArgs = rowsAboutToBeInsertedSpy.first(); + rowsInsertedArgs = rowsInsertedSpy.first(); + QCOMPARE(rowsAboutToBeInsertedArgs, rowsInsertedArgs); + QCOMPARE(rowsAboutToBeInsertedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeInsertedArgs.at(1).toInt(), tmaDestItemIdx + 1); + } + QCOMPARE(rowsAboutToBeInsertedArgs.at(2).toInt(), tmaDestItemIdx + expectedMovedCount - 1); + QCOMPARE(tma.itemIndex(model.index(destRow, 0, destParentIdx)), tmaDestItemIdx); + } else if (signalType == RowsRemoved) { + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 1); + QCOMPARE(rowsRemovedSpy.count(), 1); + QVariantList rowsAboutToBeRemovedArgs = rowsAboutToBeRemovedSpy.first(); + QVariantList rowsRemovedArgs = rowsRemovedSpy.first(); + QCOMPARE(rowsAboutToBeRemovedArgs, rowsRemovedArgs); + QCOMPARE(rowsAboutToBeRemovedArgs.at(0).toModelIndex(), QModelIndex()); + QCOMPARE(rowsAboutToBeRemovedArgs.at(1).toInt(), tmaSourceItemIdx); + QCOMPARE(rowsAboutToBeRemovedArgs.at(2).toInt(), tmaSourceItemIdx + expectedMovedCount - 1); + } + } + QVERIFY(tma.testConsistency()); + compareModels(tma, model); +} + +void tst_QQuickTreeModelAdaptor::selectionForRowRange() +{ + const int ModelRowCount = 9; + const int ModelRowCountLoopStep = 4; + + QtTestModel model(ModelRowCount, 1); + model.alternateChildlessRows = false; + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + // NOTE: Some selections may look a bit cryptic. Insert a call to + // tma.dump() before each block if you need to see what's going on. + + for (int i = 0; i < ModelRowCount; i += ModelRowCountLoopStep) { + // Single row selection + const QItemSelection &sel = tma.selectionForRowRange(i, i); + QCOMPARE(sel.count(), 1); + const QItemSelectionRange &range = sel.first(); + QCOMPARE(QModelIndex(range.topLeft()), model.index(i, 0)); + QCOMPARE(QModelIndex(range.bottomRight()), model.index(i, 0)); + } + + for (int i = 0; i < ModelRowCount - ModelRowCountLoopStep; i += ModelRowCountLoopStep) { + // Single range selection + const QItemSelection &sel = tma.selectionForRowRange(i, i + ModelRowCountLoopStep); + QCOMPARE(sel.count(), 1); + const QItemSelectionRange &range = sel.first(); + QCOMPARE(QModelIndex(range.topLeft()), model.index(i, 0)); + QCOMPARE(QModelIndex(range.bottomRight()), model.index(i + ModelRowCountLoopStep, 0)); + } + + { // Select all, no branch expanded + const QItemSelection &sel = tma.selectionForRowRange(0, ModelRowCount - 1); + QCOMPARE(sel.count(), 1); + const QItemSelectionRange &range = sel.first(); + QCOMPARE(QModelIndex(range.topLeft()), model.index(0, 0)); + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0)); + } + + // Expand 1st top-level item + const QModelIndex &parent = model.index(0, 0); + tma.expand(parent); + + { // 1st item expanded, select first 5 rows + const QItemSelection &sel = tma.selectionForRowRange(0, 4); + QCOMPARE(sel.count(), 2); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(0, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(0, 0)); + else if (range.topLeft() == model.index(0, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(3, 0, parent)); + else + QFAIL("Unexpected selection range"); + } + } + + { // 1st item expanded, select first 5 top-level items + const QItemSelection &sel = tma.selectionForRowRange(0, 4 + ModelRowCount); + QCOMPARE(sel.count(), 2); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(0, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(4, 0)); + else if (range.topLeft() == model.index(0, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent)); + else + QFAIL("Unexpected selection range"); + } + } + + // Expand 2nd top-level item + const QModelIndex &parent2 = model.index(1, 0); + tma.expand(parent2); + + { // 1st two items expanded, select first 5 top-level items + const QItemSelection &sel = tma.selectionForRowRange(0, 4 + 2 * ModelRowCount); + QCOMPARE(sel.count(), 3); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(0, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(4, 0)); + else if (range.topLeft() == model.index(0, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent)); + else if (range.topLeft() == model.index(0, 0, parent2)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent2)); + else + QFAIL("Unexpected selection range"); + } + } + + // Expand 1st child of 1st top-level item + const QModelIndex &parent3 = model.index(0, 0, parent); + tma.expand(parent3); + + { // 1st two items, and 1st child of 1st item expanded, select first 5 rows + const QItemSelection &sel = tma.selectionForRowRange(0, 4); + QCOMPARE(sel.count(), 3); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(0, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(0, 0)); + else if (range.topLeft() == model.index(0, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(0, 0, parent)); + else if (range.topLeft() == model.index(0, 0, parent3)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(2, 0, parent3)); + else + QFAIL("Unexpected selection range"); + } + } + + { // 1st two items, and 1st child of 1st item expanded, select all + const QItemSelection &sel = tma.selectionForRowRange(0, 4 * ModelRowCount - 1); + QCOMPARE(sel.count(), 4); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(0, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0)); + else if (range.topLeft() == model.index(0, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent)); + else if (range.topLeft() == model.index(0, 0, parent2)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent2)); + else if (range.topLeft() == model.index(0, 0, parent3)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent3)); + else + QFAIL("Unexpected selection range"); + } + } + + { // 1st two items, and 1st child of 1st item expanded, select rows across branches + const QItemSelection &sel = tma.selectionForRowRange(8, 23); + QCOMPARE(sel.count(), 4); + // We don't know in which order the selection ranges are + // being added, so we iterate and try to find what we expect. + foreach (const QItemSelectionRange &range, sel) { + if (range.topLeft() == model.index(1, 0)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(1, 0)); + else if (range.topLeft() == model.index(1, 0, parent)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent)); + else if (range.topLeft() == model.index(0, 0, parent2)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(3, 0, parent2)); + else if (range.topLeft() == model.index(6, 0, parent3)) + QCOMPARE(QModelIndex(range.bottomRight()), model.index(ModelRowCount - 1, 0, parent3)); + else + QFAIL("Unexpected selection range"); + } + } +} + +QTEST_MAIN(tst_QQuickTreeModelAdaptor) +#include "tst_qquicktreemodeladaptor.moc" diff --git a/tests/auto/shared/qtestmodel.h b/tests/auto/shared/qtestmodel.h new file mode 100644 index 000000000..e9472f84e --- /dev/null +++ b/tests/auto/shared/qtestmodel.h @@ -0,0 +1,319 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** 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 http://www.qt.io/terms-conditions. For further +** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtCore> +#include <QAbstractItemModel> + +class QtTestModel: public QAbstractItemModel +{ +public: + QtTestModel(QObject *parent = 0): QAbstractItemModel(parent), + fetched(false), rows(10), cols(1), levels(INT_MAX), wrongIndex(false) { init(); } + + QtTestModel(int _rows, int _cols, QObject *parent = 0): QAbstractItemModel(parent), + fetched(false), rows(_rows), cols(_cols), levels(INT_MAX), wrongIndex(false) { init(); } + + void init() { + decorationsEnabled = false; + alternateChildlessRows = true; + tree = new Node(rows); + } + + inline qint32 level(const QModelIndex &index) const { + Node *n = (Node *)index.internalPointer(); + if (!n) + return -1; + int l = -1; + while (n != tree) { + n = n->parent; + ++l; + } + return l; + } + + void resetModel() + { + beginResetModel(); + fetched = false; + delete tree; + tree = new Node(rows); + endResetModel(); + } + + QString displayData(const QModelIndex &idx) const + { + return QString("[%1,%2,%3,%4]").arg(idx.row()).arg(idx.column()).arg(idx.internalId()).arg(hasChildren(idx)); + } + + bool canFetchMore(const QModelIndex &) const { + return !fetched; + } + + void fetchMore(const QModelIndex &) { + fetched = true; + } + + bool hasChildren(const QModelIndex &parent = QModelIndex()) const { + bool hasFetched = fetched; + fetched = true; + bool r = QAbstractItemModel::hasChildren(parent); + fetched = hasFetched; + return r; + } + + int rowCount(const QModelIndex& parent = QModelIndex()) const { + if (!fetched) + qFatal("%s: rowCount should not be called before fetching", Q_FUNC_INFO); + if ((parent.column() > 0) || (level(parent) > levels) + || (alternateChildlessRows && parent.row() > 0 && (parent.row() & 1))) + return 0; + Node *n = (Node*)parent.internalPointer(); + if (!n) + n = tree; + return n->children.count(); + } + + int columnCount(const QModelIndex& parent = QModelIndex()) const { + if ((parent.column() > 0) || (level(parent) > levels) + || (alternateChildlessRows && parent.row() > 0 && (parent.row() & 1))) + return 0; + return cols; + } + + bool isEditable(const QModelIndex &index) const { + if (index.isValid()) + return true; + return false; + } + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const + { + if (row < 0 || column < 0 || (level(parent) > levels) || column >= cols) + return QModelIndex(); + Node *pn = (Node*)parent.internalPointer(); + if (!pn) + pn = tree; + if (row >= pn->children.count()) + return QModelIndex(); + + Node *n = pn->children.at(row); + if (!n) { + n = new Node(rows, pn); + pn->children[row] = n; + } + return createIndex(row, column, n); + } + + QModelIndex parent(const QModelIndex &index) const + { + Node *n = (Node *)index.internalPointer(); + if (!n || n->parent == tree) + return QModelIndex(); + Q_ASSERT(n->parent->parent); + int parentRow = n->parent->parent->children.indexOf(n->parent); + Q_ASSERT(parentRow != -1); + return createIndex(parentRow, 0, n->parent); + } + + QVariant data(const QModelIndex &idx, int role) const + { + if (!idx.isValid()) + return QVariant(); + + Node *pn = (Node *)idx.internalPointer(); + if (!pn) + pn = tree; + if (pn != tree) + pn = pn->parent; + if (idx.row() < 0 || idx.column() < 0 || idx.column() >= cols + || idx.row() >= pn->children.count()) { + wrongIndex = true; + qWarning("Invalid modelIndex [%d,%d,%p]", idx.row(), idx.column(), + idx.internalPointer()); + return QVariant(); + } + + if (role == Qt::DisplayRole) { + return displayData(idx); + } + + return QVariant(); + } + + bool setData(const QModelIndex &index, const QVariant &value, int role) + { + Q_UNUSED(value); + QVector<int> changedRole(1, role); + emit dataChanged(index, index, changedRole); + return true; + } + + void groupedSetData(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) + { + emit dataChanged(topLeft, bottomRight, roles); + } + + void changeLayout(const QList<QPersistentModelIndex> &parents = QList<QPersistentModelIndex>()) + { + emit layoutAboutToBeChanged(parents); + emit layoutChanged(parents); + } + + bool removeRows(int row, int count, const QModelIndex &parent) + { + beginRemoveRows(parent, row, row + count - 1); + Node *n = (Node *)parent.internalPointer(); + if (!n) + n = tree; + n->removeRows(row, count); + endRemoveRows(); + return true; + } + + void removeLastColumn() + { + beginRemoveColumns(QModelIndex(), cols - 1, cols - 1); + --cols; + endRemoveColumns(); + } + + void removeAllColumns() + { + beginRemoveColumns(QModelIndex(), 0, cols - 1); + cols = 0; + endRemoveColumns(); + } + + bool insertRows(int row, int count, const QModelIndex &parent) + { + beginInsertRows(parent, row, row + count - 1); + Node *n = (Node *)parent.internalPointer(); + if (!n) + n = tree; + n->addRows(row, count); + endInsertRows(); + return true; + } + + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int count, const QModelIndex &destinationParent, int destinationChild) + { + Q_ASSERT_X(sourceRow >= 0 && sourceRow < rowCount(sourceParent) + && count > 0 && sourceRow + count < rowCount(sourceParent) + && destinationChild >= 0 && destinationChild <= rowCount(destinationParent), + Q_FUNC_INFO, "Rows out of range."); + Q_ASSERT_X(!(sourceParent == destinationParent && destinationChild >= sourceRow && destinationChild < sourceRow + count), + Q_FUNC_INFO, "Moving rows onto themselves."); + if (!beginMoveRows(sourceParent, sourceRow, sourceRow + count - 1, destinationParent, destinationChild)) + return false; + Node *src = (Node *)sourceParent.internalPointer(); + if (!src) + src = tree; + Node *dest = (Node *)destinationParent.internalPointer(); + if (!dest) + dest = tree; + QVector<Node *> buffer = src->children.mid(sourceRow, count); + if (src != dest) { + src->removeRows(sourceRow, count, true /* keep alive */); + dest->addRows(destinationChild, count); + } else { + QVector<Node *> &c = dest->children; + if (sourceRow < destinationChild) { + memmove(&c[sourceRow], &c[sourceRow + count], sizeof(Node *) * (destinationChild - sourceRow - count)); + destinationChild -= count; + } else { + memmove(&c[destinationChild + count], &c[destinationChild], sizeof(Node *) * (sourceRow - destinationChild)); + } + } + for (int i = 0; i < count; i++) { + Node *n = buffer[i]; + n->parent = dest; + dest->children[i + destinationChild] = n; + } + + endMoveRows(); + return true; + } + + void setDecorationsEnabled(bool enable) + { + decorationsEnabled = enable; + } + + mutable bool fetched; + bool decorationsEnabled; + bool alternateChildlessRows; + int rows, cols; + int levels; + mutable bool wrongIndex; + + struct Node { + Node *parent; + QVector<Node *> children; + + Node(int rows, Node *p = 0) : parent(p) + { + addRows(0, rows); + } + + ~Node() + { + foreach (Node *n, children) + delete n; + } + + void addRows(int row, int count) + { + if (count > 0) { + children.reserve(children.count() + count); + children.insert(row, count, (Node *)0); + } + } + + void removeRows(int row, int count, bool keepAlive = false) + { + int newCount = qMax(children.count() - count, 0); + int effectiveCountDiff = children.count() - newCount; + if (effectiveCountDiff > 0) { + if (!keepAlive) + for (int i = 0; i < effectiveCountDiff; i++) + delete children[i + row]; + children.remove(row, effectiveCountDiff); + } + } + }; + + Node *tree; +}; |