From 82c7760b819f73ebc7f4ba6203fa2fc92b383236 Mon Sep 17 00:00:00 2001 From: Gabriel de Dietrich Date: Wed, 7 Jan 2015 12:07:28 +0100 Subject: Introducing TreeView The TreeView, as currently implemented, extends the TableView by adding support for hierarchical models. In the broad sense, it remains a list view with columns, like TableView. The main architecture is based on TreeModelAdaptor, that wraps the hierarchical model. It keeps track of which items are expanded or collapsed, and also relays model changes to the view. (TreeModelAdaptor is a private type and should be considered as an implementation detail.) The TreeView only supports QAbstractItemModels for the time being, and, just like TableView, relies on roles to pass the data to the view. This also means that model columns are not supported. Selection is supported by ItemSelectionModel which exposes part of the API of QItemSelectionModel. For this, support has been added for QModelIndex and related classes. This requires importing QtQml.Models 2.2 should an actual usage of the TreeView use selection. In the same way, TreeViewStyle currently extends TableViewStyle with the relevant features, like branch indicator. [ChangeLog][QtQuick.Controls] Introducing TreeView With-Help-From: Caroline Chao Change-Id: Id3dba240a732744571e4a646b7b98678ab522da6 Reviewed-by: Caroline Chao --- examples/quick/controls/controls.pro | 3 +- .../controls/filesystembrowser/deployment.pri | 27 + .../filesystembrowser/filesystembrowser.pro | 13 + examples/quick/controls/filesystembrowser/main.cpp | 58 + examples/quick/controls/filesystembrowser/main.qml | 118 ++ examples/quick/controls/filesystembrowser/qml.qrc | 5 + src/controls/Private/BasicTableView.qml | 62 +- src/controls/Private/private.pri | 6 +- src/controls/Private/qquickstyleitem.cpp | 25 + src/controls/Private/qquickstyleitem_p.h | 1 + src/controls/Private/qquicktreemodeladaptor.cpp | 799 ++++++++++++++ src/controls/Private/qquicktreemodeladaptor_p.h | 158 +++ src/controls/Styles/Base/BasicTableViewStyle.qml | 148 +++ src/controls/Styles/Base/TableViewStyle.qml | 90 +- src/controls/Styles/Base/TreeViewStyle.qml | 91 ++ src/controls/Styles/Desktop/TableViewStyle.qml | 8 +- src/controls/Styles/Desktop/TreeViewStyle.qml | 67 ++ src/controls/Styles/qmldir | 1 + src/controls/Styles/styles.pri | 3 + src/controls/TableViewColumn.qml | 8 +- src/controls/TreeView.qml | 523 +++++++++ src/controls/controls.pro | 3 +- .../qtquickcontrols-example-filesystembrowser.png | Bin 0 -> 39132 bytes src/controls/doc/images/treeview.png | Bin 0 -> 14784 bytes src/controls/doc/src/qtquickcontrols-examples.qdoc | 14 + src/controls/plugin.cpp | 9 +- tests/auto/auto.pro | 3 +- tests/auto/controls/controls.pro | 3 +- tests/auto/controls/data/treeview/treeview_1.qml | 76 ++ tests/auto/controls/data/treeview/treeview_2.qml | 81 ++ tests/auto/controls/data/tst_treeview.qml | 761 +++++++++++++ .../qquicktreemodeladaptor.pro | 12 + .../tst_qquicktreemodeladaptor.cpp | 1135 ++++++++++++++++++++ tests/auto/shared/testmodel.h | 333 ++++++ tests/auto/testplugin/testplugin.cpp | 2 + tests/auto/testplugin/testplugin.pro | 3 +- 36 files changed, 4540 insertions(+), 109 deletions(-) create mode 100644 examples/quick/controls/filesystembrowser/deployment.pri create mode 100644 examples/quick/controls/filesystembrowser/filesystembrowser.pro create mode 100644 examples/quick/controls/filesystembrowser/main.cpp create mode 100644 examples/quick/controls/filesystembrowser/main.qml create mode 100644 examples/quick/controls/filesystembrowser/qml.qrc create mode 100644 src/controls/Private/qquicktreemodeladaptor.cpp create mode 100644 src/controls/Private/qquicktreemodeladaptor_p.h create mode 100644 src/controls/Styles/Base/BasicTableViewStyle.qml create mode 100644 src/controls/Styles/Base/TreeViewStyle.qml create mode 100644 src/controls/Styles/Desktop/TreeViewStyle.qml create mode 100644 src/controls/TreeView.qml create mode 100644 src/controls/doc/images/qtquickcontrols-example-filesystembrowser.png create mode 100644 src/controls/doc/images/treeview.png create mode 100644 tests/auto/controls/data/treeview/treeview_1.qml create mode 100644 tests/auto/controls/data/treeview/treeview_2.qml create mode 100644 tests/auto/controls/data/tst_treeview.qml create mode 100644 tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro create mode 100644 tests/auto/qquicktreemodeladaptor/tst_qquicktreemodeladaptor.cpp create mode 100644 tests/auto/shared/testmodel.h diff --git a/examples/quick/controls/controls.pro b/examples/quick/controls/controls.pro index f6696bc5..2b164dc2 100644 --- a/examples/quick/controls/controls.pro +++ b/examples/quick/controls/controls.pro @@ -5,7 +5,8 @@ SUBDIRS += \ tableview \ touch \ basiclayouts \ - styles + styles \ + filesystembrowser qtHaveModule(widgets) { SUBDIRS += texteditor diff --git a/examples/quick/controls/filesystembrowser/deployment.pri b/examples/quick/controls/filesystembrowser/deployment.pri new file mode 100644 index 00000000..5441b63d --- /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 00000000..1cdc565a --- /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 00000000..e64a9a22 --- /dev/null +++ b/examples/quick/controls/filesystembrowser/main.cpp @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** 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 +#include +#include +#include + +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 00000000..4b182b73 --- /dev/null +++ b/examples/quick/controls/filesystembrowser/main.qml @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** 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 + } + } + } + + ItemSelectionModel { + id: sel + model: fileSystemModel + onSelectionChanged: { + console.log("selected", selected) + console.log("deselected", deselected) + console.log("selection", sel.selection()) + } + onCurrentChanged: console.log("current", current) + } + + TreeView { + id: view + anchors.fill: parent + anchors.margins: 2 * 12 + row.height + model: fileSystemModel + selection: sel + + onCurrentIndexChanged: console.log("current index", currentIndex) + + TableViewColumn { + title: "Name" + role: "fileName" + resizable: true + } + + TableViewColumn { + title: "Permissions" + role: "filePermissions" + resizable: true + } + + 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 00000000..5f6483ac --- /dev/null +++ b/examples/quick/controls/filesystembrowser/qml.qrc @@ -0,0 +1,5 @@ + + + main.qml + + diff --git a/src/controls/Private/BasicTableView.qml b/src/controls/Private/BasicTableView.qml index bc8ebdb0..a42a406c 100644 --- a/src/controls/Private/BasicTableView.qml +++ b/src/controls/Private/BasicTableView.qml @@ -269,11 +269,14 @@ ScrollView { Otherwise \c null is returned. */ function insertColumn(index, column) { + if (__isTreeView && index === 0 && columnCount > 0) { + console.warn(__viewTypeName + "::insertColumn(): Can't replace column 0") + return null + } var object = column - if (typeof column['createObject'] === 'function') + if (typeof column['createObject'] === 'function') { object = column.createObject(root) - - else if (object.__view) { + } else if (object.__view) { console.warn(__viewTypeName + "::insertColumn(): you cannot add a column to multiple views") return null } @@ -299,6 +302,10 @@ ScrollView { console.warn(__viewTypeName + "::removeColumn(): invalid argument") return } + if (__isTreeView && index === 0) { + console.warn(__viewTypeName + "::removeColumn(): Can't remove column 0") + return + } var column = columnModel.get(index).columnItem columnModel.remove(index, 1) column.destroy() @@ -314,6 +321,10 @@ ScrollView { console.warn(__viewTypeName + "::moveColumn(): invalid argument") return } + if (__isTreeView && to === 0) { + console.warn(__viewTypeName + "::moveColumn(): Can't move column 0") + return + } columnModel.move(from, to, 1) } @@ -370,6 +381,9 @@ ScrollView { */ property string __viewTypeName + /*! \internal */ + readonly property bool __isTreeView: __viewTypeName === "TreeView" + /*! \internal */ default property alias __columns: root.data @@ -543,6 +557,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 +623,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 } } } @@ -654,6 +700,8 @@ ScrollView { visible: modelData.visible height: headerStyle.height + readonly property bool treeViewMovable: !__isTreeView || index > 0 + Loader { id: headerStyle sourceComponent: root.headerDelegate @@ -673,7 +721,7 @@ ScrollView { id: targetmark width: parent.width height:parent.height - opacity: (index === repeater.targetIndex && repeater.targetIndex !== repeater.dragIndex) ? 0.5 : 0 + opacity: (treeViewMovable && index === repeater.targetIndex && repeater.targetIndex !== repeater.dragIndex) ? 0.5 : 0 Behavior on opacity { NumberAnimation { duration: 160 } } color: palette.highlight visible: modelData.movable @@ -693,7 +741,7 @@ ScrollView { // NOTE: the direction is different from the master branch // so this indicates that I am using an invalid assumption on item ordering onPositionChanged: { - if (modelData.movable && pressed && columnCount > 1) { // only do this while dragging + if (drag.active && modelData.movable && pressed && columnCount > 1) { // only do this while dragging for (var h = columnCount-1 ; h >= 0 ; --h) { if (drag.target.x + listView.contentX + headerRowDelegate.width/2 > headerrow.children[h].x) { repeater.targetIndex = h @@ -710,7 +758,7 @@ ScrollView { onReleased: { if (repeater.targetIndex >= 0 && repeater.targetIndex !== index ) { var targetColumn = columnModel.get(repeater.targetIndex).columnItem - if (targetColumn.movable) { + if (targetColumn.movable && (!__isTreeView || repeater.targetIndex > 0)) { columnModel.move(index, repeater.targetIndex, 1) if (sortIndicatorColumn === index) sortIndicatorColumn = repeater.targetIndex @@ -719,7 +767,7 @@ ScrollView { repeater.targetIndex = -1 repeater.dragIndex = -1 } - drag.target: modelData.movable && columnCount > 1 ? draghandle : null + drag.target: treeViewMovable && modelData.movable && columnCount > 1 ? draghandle : null } Loader { diff --git a/src/controls/Private/private.pri b/src/controls/Private/private.pri index 3d6a7162..7d23fc47 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 4cd4f52b..2ac692af 100644 --- a/src/controls/Private/qquickstyleitem.cpp +++ b/src/controls/Private/qquickstyleitem.cpp @@ -370,6 +370,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(); @@ -1223,6 +1236,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; } @@ -1309,6 +1324,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") { @@ -1422,6 +1439,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; } @@ -1502,6 +1524,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 4ef86c73..74674454 100644 --- a/src/controls/Private/qquickstyleitem_p.h +++ b/src/controls/Private/qquickstyleitem_p.h @@ -122,6 +122,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 00000000..70e038fe --- /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 +#include "qquicktreemodeladaptor_p.h" +#include +#include + +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&)), + SLOT(modelDataChanged(const QModelIndex&, const QModelIndex&, const QVector&)) }, + + { SIGNAL(layoutAboutToBeChanged(const QList&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutAboutToBeChanged(const QList&, QAbstractItemModel::LayoutChangeHint)) }, + { SIGNAL(layoutChanged(const QList&, QAbstractItemModel::LayoutChangeHint)), + SLOT(modelLayoutChanged(const QList&, QAbstractItemModel::LayoutChangeHint)) }, + + { SIGNAL(rowsAboutToBeInserted(const QModelIndex&, int, int)), + SLOT(modelRowsAboutToBeInserted(const QModelIndex &, int, int)) }, + { SIGNAL(rowsInserted(const QModelIndex&, int, int)), + SLOT(modelRowsInserted(const QModelIndex&, int, int)) }, + { SIGNAL(rowsAboutToBeRemoved(const QModelIndex&, int, int)), + SLOT(modelRowsAboutToBeRemoved(const QModelIndex&, int, int)) }, + { SIGNAL(rowsRemoved(const QModelIndex&, int, int)), + SLOT(modelRowsRemoved(const QModelIndex&, int, int)) }, + { SIGNAL(rowsAboutToBeMoved(const QModelIndex&, int, int, const QModelIndex&, int)), + SLOT(modelRowsAboutToBeMoved(const QModelIndex&, int, int, const QModelIndex&, int)) }, + { SIGNAL(rowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)), + SLOT(modelRowsMoved(const QModelIndex&, int, int, const QModelIndex&, int)) }, + { 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 QQuickTreeModelAdaptor::roleNames() const +{ + if (!m_model) + return QHash(); + + QHash 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 MIPair; + typedef QHash MI2MIPairHash; + MI2MIPairHash ranges; + QModelIndex firstIndex = m_items.at(from).index; + QModelIndex lastIndex = firstIndex; + QModelIndex previousParent = firstIndex.parent(); + bool selectLastRow = false; + for (int i = from + 1; i <= to || (selectLastRow = true); i++) { + // We run an extra iteration to make sure the last row is + // added to the selection. (And also to avoid duplicating + // the insertion code.) + QModelIndex index; + QModelIndex parent; + if (!selectLastRow) { + index = m_items.at(i).index; + parent = index.parent(); + } + if (selectLastRow || previousParent != parent) { + const MI2MIPairHash::iterator &it = ranges.find(previousParent); + if (it == ranges.end()) + ranges.insert(previousParent, MIPair(firstIndex, lastIndex)); + else + it->second = lastIndex; + + if (selectLastRow) + break; + + firstIndex = index; + previousParent = parent; + } + lastIndex = index; + } + + QItemSelection sel; + sel.reserve(ranges.count()); + 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 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 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 &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 &parents, QAbstractItemModel::LayoutChangeHint hint) +{ + qDebug() << "modelLayoutAboutToBeChanged" << parents << hint << m_items.count(); + ASSERT_CONSISTENCY(); + Q_UNUSED(parents); + Q_UNUSED(hint); +} + +void QQuickTreeModelAdaptor::modelLayoutChanged(const QList &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 &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 = floorf(log10f(float(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 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 00000000..838ab805 --- /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 +#include +#include + +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 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 &roles); + void modelLayoutAboutToBeChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelLayoutChanged(const QList &parents, QAbstractItemModel::LayoutChangeHint hint); + void modelRowsAboutToBeInserted(const QModelIndex & parent, int start, int end); + void modelRowsAboutToBeMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow); + void modelRowsAboutToBeRemoved(const QModelIndex & parent, int start, int end); + void modelRowsInserted(const QModelIndex & parent, int start, int end); + void modelRowsMoved(const QModelIndex & sourceParent, int sourceStart, int sourceEnd, const QModelIndex & destinationParent, int destinationRow); + void modelRowsRemoved(const QModelIndex & parent, int start, int end); + +private: + struct TreeItem { + QPersistentModelIndex index; + int depth; + bool expanded; + + explicit TreeItem(const QModelIndex &idx = QModelIndex(), int d = 0, int e = false) + : index(idx), depth(d), expanded(e) + { } + + inline bool operator== (const TreeItem &other) const + { + return this->index == other.index; + } + }; + + QAbstractItemModel *m_model; + QList m_items; + QSet m_expandedItems; + QList 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 00000000..b395b6fd --- /dev/null +++ b/src/controls/Styles/Base/BasicTableViewStyle.qml @@ -0,0 +1,148 @@ +/**************************************************************************** +** +** 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 + + /*! \internal */ + 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 - x + x: styleData.depth && styleData.column === 0 ? 0 : 8 + 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 3c60a976..2e322465 100644 --- a/src/controls/Styles/Base/TableViewStyle.qml +++ b/src/controls/Styles/Base/TableViewStyle.qml @@ -34,9 +34,8 @@ ** ****************************************************************************/ -import QtQuick 2.2 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Private 1.0 +import QtQuick 2.5 +import QtQuick.Controls 1.4 /*! \qmltype TableViewStyle @@ -48,92 +47,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 00000000..774b1738 --- /dev/null +++ b/src/controls/Styles/Base/TreeViewStyle.qml @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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 + \brief Provides custom styling for TreeView +*/ +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 37d55b1a..f5199f3f 100644 --- a/src/controls/Styles/Desktop/TableViewStyle.qml +++ b/src/controls/Styles/Desktop/TableViewStyle.qml @@ -42,7 +42,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) @@ -92,10 +93,10 @@ ScrollViewStyle { id: label objectName: "label" width: parent.width - anchors.leftMargin: 8 font: __styleitem.font anchors.left: parent.left anchors.right: parent.right + anchors.leftMargin: styleData["depth"] && styleData.column === 0 ? 0 : 8 horizontalAlignment: styleData.textAlignment anchors.verticalCenter: parent.verticalCenter elide: styleData.elideMode @@ -104,5 +105,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 00000000..1901c40c --- /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 7e21858e..582a7898 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.4 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 1fcf4147..f91a9d1a 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/TableViewColumn.qml b/src/controls/TableViewColumn.qml index fc6d6a3d..3ee4bf31 100644 --- a/src/controls/TableViewColumn.qml +++ b/src/controls/TableViewColumn.qml @@ -41,11 +41,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 @@ -56,7 +56,7 @@ import QtQuick 2.2 } \endqml - \sa TableView + \sa TableView, TreeView */ QtObject { @@ -117,7 +117,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 00000000..2f1a4412 --- /dev/null +++ b/src/controls/TreeView.qml @@ -0,0 +1,523 @@ +/**************************************************************************** +** +** 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 ItemSelectionModel TreeView::selection + + By default the selection model is \c null and only single selection is supported. + + To use a different selection mode as described in \l {BasicTableView::selectionMode}{selectionMode}, + an ItemSelectionModel must by set to the selection. + + For example: + + \code + TreeView { + model: myModel + selection: ItemSelectionModel { + model: myModel + } + TableViewColumn { + role: "name" + title: "Name + } + } + \endcode + + \sa {BasicTableView::selectionMode}{selectionMode} + + */ + 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 78edc913..151ccb4b 100644 --- a/src/controls/controls.pro +++ b/src/controls/controls.pro @@ -1,6 +1,6 @@ TARGET = qtquickcontrolsplugin TARGETPATH = QtQuick/Controls -IMPORT_VERSION = 1.3 +IMPORT_VERSION = 1.4 QT += qml quick quick-private qml-private gui-private core-private @@ -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 new file mode 100644 index 00000000..b2843101 Binary files /dev/null and b/src/controls/doc/images/qtquickcontrols-example-filesystembrowser.png differ diff --git a/src/controls/doc/images/treeview.png b/src/controls/doc/images/treeview.png new file mode 100644 index 00000000..aac520a3 Binary files /dev/null and b/src/controls/doc/images/treeview.png differ diff --git a/src/controls/doc/src/qtquickcontrols-examples.qdoc b/src/controls/doc/src/qtquickcontrols-examples.qdoc index 785181c8..3418d6c2 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 f134b2e3..3fa827e7 100644 --- a/src/controls/plugin.cpp +++ b/src/controls/plugin.cpp @@ -54,6 +54,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 @@ -107,7 +108,9 @@ static const struct { { "BusyIndicator", 1, 1 }, - { "TextArea", 1, 3 } + { "TextArea", 1, 3 }, + + { "TreeView", 1, 4 } }; void QtQuickControlsPlugin::registerTypes(const char *uri) @@ -146,11 +149,15 @@ void QtQuickControlsPlugin::initializeEngine(QQmlEngine *engine, const char *uri qmlRegisterType(private_uri, 1, 0, "SpinBoxValidator"); qmlRegisterSingletonType(private_uri, 1, 0, "Tooltip", QQuickControlsPrivate::registerTooltipModule); qmlRegisterSingletonType(private_uri, 1, 0, "Settings", QQuickControlsPrivate::registerSettingsModule); + qmlRegisterType(private_uri, 1, 0, "TreeModelAdaptor"); qmlRegisterType(private_uri, 1, 0, "MenuPrivate"); qmlRegisterType(private_uri, 1, 0, "MenuBarPrivate"); qmlRegisterType(private_uri, 1, 0, "PopupWindow"); + qmlRegisterUncreatableType(private_uri, 1, 0, "AbstractItemModel", + QLatin1String("AbstractItemModel is an abstract type.")); + #ifdef QT_WIDGETS_LIB qmlRegisterType(private_uri, 1, 0, "StyleItem"); engine->addImageProvider("__tablerow", new QQuickTableRowImageProvider); diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro index de1b45b6..86de357d 100644 --- a/tests/auto/auto.pro +++ b/tests/auto/auto.pro @@ -1,3 +1,4 @@ TEMPLATE = subdirs -SUBDIRS += testplugin controls activeFocusOnTab applicationwindow dialogs extras paint +SUBDIRS += testplugin controls activeFocusOnTab applicationwindow dialogs \ + extras paint qquicktreemodeladaptor controls.depends = testplugin diff --git a/tests/auto/controls/controls.pro b/tests/auto/controls/controls.pro index 40fa77cd..8f8d1507 100644 --- a/tests/auto/controls/controls.pro +++ b/tests/auto/controls/controls.pro @@ -49,4 +49,5 @@ OTHER_FILES += \ $$PWD/data/tst_splitview.qml \ $$PWD/data/tst_styles.qml \ $$PWD/data/tst_layout.qml \ - $$PWD/data/tst_keys.qml + $$PWD/data/tst_keys.qml \ + $$PWD/data/tst_treeview.qml diff --git a/tests/auto/controls/data/treeview/treeview_1.qml b/tests/auto/controls/data/treeview/treeview_1.qml new file mode 100644 index 00000000..29c2efdc --- /dev/null +++ b/tests/auto/controls/data/treeview/treeview_1.qml @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuickControlsTests 1.0 + +TreeView { + width: 400 + height: 400 + model: TreeModel {} + + TableViewColumn { + role: "display" + title: "Default" + width: 200 + } + + style: TreeViewStyle { + indentation: 20 + rowDelegate: Rectangle { + color: styleData.selected? "blue" : styleData.alternate ? "red" : "yellow" + height: 50 + width: parent.width + } + headerDelegate: Text { + text: styleData.value + height: 50 + width: parent.width + } + itemDelegate: Text { + text: styleData.value + height: 50 + width: parent.width + } + branchDelegate: Rectangle { + color: styleData.isExpanded? "purple" : "green" + height: 50 + width: 20 + } + } +} diff --git a/tests/auto/controls/data/treeview/treeview_2.qml b/tests/auto/controls/data/treeview/treeview_2.qml new file mode 100644 index 00000000..f8c54028 --- /dev/null +++ b/tests/auto/controls/data/treeview/treeview_2.qml @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +import QtQuick 2.4 +import QtQuick.Controls 1.4 +import QtQuickControlsTests 1.0 + +TreeView { + width: 500 + height: 500 + property var treeModel: TreeModel {} + + model: treeModel + + property int collapsedCount: 0 + property int expandedCount: 0 + property var signalArgIndex + + TableViewColumn { + role: "display" + title: "Default" + width: 200 + } + rowDelegate: Rectangle { + color: styleData.selected? "blue" : styleData.alternate ? "red" : "yellow" + height: 50 + width: parent.width + } + headerDelegate: Text { + text: styleData.value + height: 50 + width: parent.width + } + itemDelegate: Text { + text: styleData.value + height: 50 + width: parent.width + } + + onCollapsed: { + signalArgIndex = index + collapsedCount++ + } + onExpanded: { + signalArgIndex = index + expandedCount++ + } +} diff --git a/tests/auto/controls/data/tst_treeview.qml b/tests/auto/controls/data/tst_treeview.qml new file mode 100644 index 00000000..7ce5581b --- /dev/null +++ b/tests/auto/controls/data/tst_treeview.qml @@ -0,0 +1,761 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +import QtQuick 2.4 +import QtTest 1.0 +import QtQuick.Controls 1.4 +import QtQuickControlsTests 1.0 + +Item { + id: container + width: 400 + height: 400 + + TestCase { + id: testCase + name: "Tests_TreeView" + when:windowShown + width:400 + height:400 + objectName: "testCase" + + SignalSpy { + id: spy + } + + Component { + id: newColumn + TableViewColumn { + role: "name" + title: "Name" + } + } + + property var instance_selectionModel: 'import QtQml.Models 2.2; ItemSelectionModel {}' + property var semiIndent: 20/2 // PM_TreeViewIndentation 20 in commonStyle + + function cleanup() + { + // Make sure to delete all children even when the test has failed. + for (var child in container.children) { + if (container.children[child].objectName !== "testCase") + container.children[child].destroy() + } + } + + function test_basic_setup() + { + var test_instanceStr = + 'import QtQuick 2.4; \ + import QtQuick.Controls 1.4; \ + import QtQuickControlsTests 1.0;\ + TreeView { \ + model: TreeModel {} \ + TableViewColumn { \ + role: "display"; \ + title: "Default"; \ + } \ + }' + + var tree = Qt.createQmlObject(test_instanceStr, container, '') + verify(!tree.currentIndex.valid) + compare(tree.columnCount, 1) + tree.addColumn(newColumn) + compare(tree.columnCount, 2) + tree.destroy() + } + + function test_clicked_signals() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + tree.forceActiveFocus() + + verify(!tree.currentIndex.valid) + spy.clear() + spy.target = tree + spy.signalName = "clicked" + compare(spy.count, 0) + mouseClick(tree, semiIndent + 50, 120, Qt.LeftButton) + compare(spy.count, 1) + var clickedItem = spy.signalArguments[0][0] + verify(clickedItem.valid) + compare(clickedItem.row, 1) + compare(tree.currentIndex.row, 1) + compare(clickedItem.internalId, tree.currentIndex.internalId) + + // TO FIX +// spy.clear() +// spy.target = tree +// spy.signalName = "doubleClicked" +// compare(spy.count, 0) +// mouseDoubleClick(tree, semiIndent + 50, 120, Qt.LeftButton) +// compare(spy.count, 1) +// verify(spy.signalArguments[1][0].valid) +// compare(spy.signalArguments[1][0].row, 2) +// compare(tree.currentIndex.row, 2) + +// spy.clear() +// spy.target = tree +// spy.signalName = "activated" +// compare(spy.count, 0) +// if (!tree.__activateItemOnSingleClick) +// mouseDoubleClick(tree, semiIndent + 50 , 120, Qt.LeftButton) +// else +// mouseClick(tree, semiIndent + 50, 120, Qt.LeftButton) +// compare(spy.count, 1) +// verify(spy.signalArguments[0][0].valid) +// compare(spy.signalArguments[0][0].row, 1) +// compare(tree.currentIndex.row, 1) +// tree.destroy() + } + + function test_headerHidden() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + tree.headerVisible = false + tree.forceActiveFocus() + + verify(!tree.currentIndex.valid) + spy.clear() + spy.target = tree + spy.signalName = "clicked" + compare(spy.count, 0) + mouseClick(tree, semiIndent + 50, 20, Qt.LeftButton) + compare(spy.count, 1) + verify(spy.signalArguments[0][0].valid) + compare(spy.signalArguments[0][0].row, 0) + compare(tree.currentIndex.row, 0) + tree.destroy() + } + + function test_expand_collapse() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + tree.forceActiveFocus() + + spy.clear() + spy.target = tree + spy.signalName = "expanded" + + // expanded on click + compare(spy.count, 0) + mouseClick(tree, semiIndent, 70, Qt.LeftButton) + compare(spy.count, 1) + var expandedIndex = spy.signalArguments[0][0] + verify(expandedIndex.valid) + compare(expandedIndex.row, 0) + compare(tree.isExpanded(expandedIndex), true) + + // expand first child on click + mouseClick(tree, semiIndent * 3, 120, Qt.LeftButton) + compare(spy.count, 2) + var childIndex = spy.signalArguments[1][0] + verify(childIndex.valid) + compare(childIndex.row, 0) + compare(tree.isExpanded(childIndex), true) + compare(childIndex.parent.internalId, expandedIndex.internalId) + + spy.clear() + spy.signalName = "collapsed" + + // collapsed on click top item + compare(spy.count, 0) + mouseClick(tree, semiIndent, 70, Qt.LeftButton) + compare(spy.count, 1) + var collapsedIndex = spy.signalArguments[0][0] + verify(collapsedIndex.valid) + compare(collapsedIndex.row, 0) + compare(tree.isExpanded(collapsedIndex), false) + compare(expandedIndex.internalId, collapsedIndex.internalId) + + // check hidden child is still expanded + compare(tree.isExpanded(childIndex), true) + + // collapse child with function + tree.collapse(childIndex) + compare(tree.isExpanded(childIndex), false) + compare(spy.count, 2) + compare(spy.signalArguments[1][0].row, 0) + + spy.clear() + spy.signalName = "expanded" + compare(spy.count, 0) + + // expand child with function + tree.expand(expandedIndex) + compare(tree.isExpanded(expandedIndex), true) + compare(spy.count, 1) + compare(spy.signalArguments[0][0].row, 0) + + tree.destroy() + } + + function test_pressAndHold() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + tree.forceActiveFocus() + + var styleIndent = !!tree.style.indentation ? tree.style.indentation/2 : 6 + verify(!tree.currentIndex.valid) + spy.clear() + spy.target = tree + spy.signalName = "pressAndHold" + compare(spy.count, 0) + mousePress(tree, styleIndent + 50, 70, Qt.LeftButton) + mouseRelease(tree, styleIndent + 50, 70, Qt.LeftButton, Qt.NoModifier, 1000) + compare(spy.count, 1) + verify(spy.signalArguments[0][0].valid) + compare(spy.signalArguments[0][0].row, 0) + compare(tree.currentIndex.row, 0) + tree.destroy() + } + + function test_keys_navigation() + { + var component = Qt.createComponent("treeview/treeview_2.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + tree.forceActiveFocus() + + // select second item with no children + verify(!tree.currentIndex.valid) + mouseClick(tree, semiIndent + 50, 120, Qt.LeftButton) + var secondTopItem = tree.currentIndex + verify(secondTopItem.valid) + verify(!secondTopItem.parent.valid) + compare(secondTopItem.row, 1) + + // Press right (selected item is non expandable) + compare(tree.collapsedCount, 0) + compare(tree.expandedCount, 0) + keyClick(Qt.Key_Right) + compare(tree.collapsedCount, 0) + compare(tree.expandedCount, 0) + compare(tree.currentIndex, secondTopItem) + + // Going down + keyClick(Qt.Key_Down) + var thirdTopItem = tree.currentIndex + compare(thirdTopItem.row, 2) + verify(!thirdTopItem.parent.valid) + + // Press right - expand - go down - go up - collapse + keyClick(Qt.Key_Right) + compare(tree.collapsedCount, 0) + compare(tree.expandedCount, 1) + compare(tree.isExpanded(thirdTopItem), true) + keyClick(Qt.Key_Down) + var firstChild_thirdTopItem = tree.currentIndex + compare(firstChild_thirdTopItem.row, 0) + verify(firstChild_thirdTopItem.parent.valid) + compare(firstChild_thirdTopItem.parent.row, 2) + compare(firstChild_thirdTopItem.parent.internalId, thirdTopItem.internalId) + keyClick(Qt.Key_Up) + verify(!tree.currentIndex.parent.valid) + compare(tree.currentIndex.internalId, thirdTopItem.internalId) + compare(tree.currentIndex.row, 2) + compare(tree.isExpanded(tree.currentIndex), true) + keyClick(Qt.Key_Left) + compare(tree.isExpanded(tree.currentIndex), false) + compare(tree.collapsedCount, 1) + compare(tree.expandedCount, 1) + tree.destroy() + } + + function test_selection_singleSelection() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + var selectionModel = Qt.createQmlObject(testCase.instance_selectionModel, container, '') + selectionModel.model = tree.model + + // Collect some model index + mouseClick(tree, semiIndent + 50, 20 + 50, Qt.LeftButton) + var firstItem = tree.currentIndex + verify(firstItem.valid) + compare(firstItem.row, 0) + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + var secondItem = tree.currentIndex + verify(secondItem.valid) + compare(secondItem.row, 1) + mouseClick(tree, semiIndent + 50, 20 + 3*50, Qt.LeftButton) + var thirdItem = tree.currentIndex + verify(thirdItem.valid) + compare(thirdItem.row, 2) + mouseClick(tree, semiIndent + 50, 20 + 4*50, Qt.LeftButton) + var fourthItem = tree.currentIndex + verify(fourthItem.valid) + compare(fourthItem.row, 3) + mouseClick(tree, semiIndent + 50, 20 + 5*50, Qt.LeftButton) + var fifthItem = tree.currentIndex + verify(fifthItem.valid) + compare(fifthItem.row, 4) + mouseClick(tree, semiIndent + 50, 20 + 6*50, Qt.LeftButton) + var sixthItem = tree.currentIndex + verify(sixthItem.valid) + compare(sixthItem.row, 5) + + compare(tree.selection, null) + tree.selection = selectionModel + compare(tree.selection, selectionModel) + tree.selection.clear() + compare(tree.selection.hasSelection, false) + + //// Single selectionModel + compare(tree.selectionMode, SelectionMode.SingleSelection) + verify(!tree.selection.currentIndex.valid) + + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + verify(tree.selection.currentIndex.valid) + + compare(secondItem.internalId, tree.currentIndex.internalId) + compare(secondItem.internalId, tree.selection.currentIndex.internalId) + expectFailContinue('', 'BUG isSelected is always false when SingleSelection') + compare(tree.selection.isSelected(secondItem), true) + expectFailContinue('', 'BUG hasSelection is always false when SingleSelection') + compare(tree.selection.hasSelection, true) + var list = tree.selection.selectedIndexes() + expectFailContinue('', 'BUG empty selectedIndex when SingleSelection') + compare(list.length, 1) + if (list.length === 1) { + compare(list.at(0).internalId, secondItem.internalId) + compare(tree.selection.isSelected(secondItem), true) + } + + keyClick(Qt.Key_Down, Qt.ShiftModifier) + compare(thirdItem.internalId, tree.currentIndex.internalId) + compare(thirdItem.internalId, tree.selection.currentIndex.internalId) + + keyClick(Qt.Key_Down, Qt.ControlModifier) + + compare(fourthItem.internalId, tree.currentIndex.internalId) + expectFailContinue('', 'BUG selected state not updated with Command/Control when SingleSelection') + compare(fourthItem.internalId, tree.selection.currentIndex.internalId) + expectFailContinue('', 'BUG selected state not updated with Command/Control when SingleSelection') + compare(tree.selection.isSelected(fourthItem), true) + + tree.destroy() + } + + function test_selection_noSelection() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + var selectionModel = Qt.createQmlObject(testCase.instance_selectionModel, container, '') + selectionModel.model = tree.model + + // Collect some model index + mouseClick(tree, semiIndent + 50, 20 + 50, Qt.LeftButton) + var firstItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + var secondItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 3*50, Qt.LeftButton) + var thirdItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 4*50, Qt.LeftButton) + var fourthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 5*50, Qt.LeftButton) + var fifthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 6*50, Qt.LeftButton) + var sixthItem = tree.currentIndex + + compare(tree.selection, null) + tree.selection = selectionModel + compare(tree.selection, selectionModel) + tree.selection.clear() + compare(tree.selection.hasSelection, false) + + //// No selection + tree.selectionMode = SelectionMode.NoSelection + compare(tree.selectionMode, SelectionMode.NoSelection) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + + compare(secondItem.internalId, tree.currentIndex.internalId) + verify(!tree.selection.currentIndex.valid) + compare(tree.selection.hasSelection, false) + compare(tree.selection.isSelected(secondItem), false) + + keyClick(Qt.Key_Down, Qt.ShiftModifier) + verify(!tree.selection.currentIndex.valid) + compare(tree.selection.hasSelection, false) + compare(tree.selection.isSelected(thirdItem), false) + + keyClick(Qt.Key_Down, Qt.ControlModifier) + verify(!tree.selection.currentIndex.valid) + compare(tree.selection.hasSelection, false) + expectFailContinue('', 'BUG selected state not updated with Command/Controls when SingleSelection') + compare(tree.selection.isSelected(fourthItem), true) + + tree.destroy() + } + + function test_selection_multiSelection() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + var selectionModel = Qt.createQmlObject(testCase.instance_selectionModel, container, '') + selectionModel.model = tree.model + + // Collect some model index + mouseClick(tree, semiIndent + 50, 20 + 50, Qt.LeftButton) + var firstItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + var secondItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 3*50, Qt.LeftButton) + var thirdItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 4*50, Qt.LeftButton) + var fourthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 5*50, Qt.LeftButton) + var fifthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 6*50, Qt.LeftButton) + var sixthItem = tree.currentIndex + + compare(tree.selection, null) + tree.selection = selectionModel + compare(tree.selection, selectionModel) + tree.selection.clear() + compare(tree.selection.hasSelection, false) + + ////// Multi selection + tree.selectionMode = SelectionMode.MultiSelection + compare(tree.selectionMode, SelectionMode.MultiSelection) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + + compare(secondItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + var listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 1) + compare(listIndexes.at(0).internalId, secondItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when MultiSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, secondItem.internalId) + + mouseClick(tree, semiIndent + 50, 70+150, Qt.LeftButton) + compare(fourthItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(fourthItem), true) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 2) + compare(listIndexes.at(0).internalId, secondItem.internalId) + compare(listIndexes.at(1).internalId, fourthItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when MultiSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, fourthItem.internalId) + + keyPress(Qt.Key_Shift) + mouseClick(tree, semiIndent + 50, 70+250, Qt.LeftButton) + keyRelease(Qt.Key_Shift) + compare(sixthItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(fifthItem), false) + compare(tree.selection.isSelected(sixthItem), true) + + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 3) + compare(listIndexes.at(0).internalId, secondItem.internalId) + compare(listIndexes.at(1).internalId, fourthItem.internalId) + compare(listIndexes.at(2).internalId, sixthItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when MultiSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, sixthItem.internalId) + + + mouseClick(tree, semiIndent + 50, 70+150, Qt.LeftButton) + compare(fourthItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(fourthItem), false) + compare(tree.selection.isSelected(sixthItem), true) + + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 2) + compare(listIndexes.at(0).internalId, secondItem.internalId) + compare(listIndexes.at(1).internalId, sixthItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when MultiSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) // TO VERIFY + verify(!tree.selection.currentIndex.valid) + + mouseClick(tree, semiIndent + 50, 70+150, Qt.LeftButton) + compare(fourthItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(sixthItem), true) + + keyPress(Qt.Key_Shift) + keyClick(Qt.Key_Down) + keyClick(Qt.Key_Down) + keyClick(Qt.Key_Down) + keyRelease(Qt.Key_Shift) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(fifthItem), true) + compare(tree.selection.isSelected(sixthItem), false) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 4) + + tree.destroy() + } + + function test_selection_extendedSelection() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + var selectionModel = Qt.createQmlObject(testCase.instance_selectionModel, container, '') + selectionModel.model = tree.model + + // Collect some model index + mouseClick(tree, semiIndent + 50, 20 + 50, Qt.LeftButton) + var firstItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + var secondItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 3*50, Qt.LeftButton) + var thirdItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 4*50, Qt.LeftButton) + var fourthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 5*50, Qt.LeftButton) + var fifthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 6*50, Qt.LeftButton) + var sixthItem = tree.currentIndex + + compare(tree.selection, null) + tree.selection = selectionModel + compare(tree.selection, selectionModel) + tree.selection.clear() + compare(tree.selection.hasSelection, false) + + ////// Extended selection + tree.selectionMode = SelectionMode.ExtendedSelection + compare(tree.selectionMode, SelectionMode.ExtendedSelection) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + + compare(secondItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + var listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 1) + compare(listIndexes.at(0).internalId, secondItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when ExtendedSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, secondItem.internalId) + + // Re-click does not deselect + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + // Ctrl/Cmd click deselect + mouseClick(tree, semiIndent + 50, 70+52, Qt.LeftButton, Qt.ControlModifier) + compare(tree.selection.hasSelection, false) + compare(tree.selection.isSelected(secondItem), false) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 0) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + keyPress(Qt.Key_Down, Qt.ShiftModifier) + keyPress(Qt.Key_Down, Qt.ShiftModifier) + keyClick(Qt.Key_Down, Qt.ShiftModifier) + + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 4) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(thirdItem), true) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(fifthItem), true) + + mouseClick(tree, semiIndent + 50, 70+300, Qt.LeftButton, Qt.ShiftModifier) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 6) + + mouseClick(tree, semiIndent + 50, 70+150, Qt.LeftButton, Qt.ControlModifier) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 5) + compare(tree.selection.isSelected(fourthItem), false) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(thirdItem), true) + compare(tree.selection.isSelected(sixthItem), true) + compare(tree.selection.isSelected(fifthItem), true) + + tree.destroy() + } + + function test_selection_contiguousSelection() + { + var component = Qt.createComponent("treeview/treeview_1.qml") + compare(component.status, Component.Ready) + var tree = component.createObject(container); + verify(tree !== null, "tree created is null") + waitForRendering(tree) + + var selectionModel = Qt.createQmlObject(testCase.instance_selectionModel, container, '') + selectionModel.model = tree.model + + // Collect some model index + mouseClick(tree, semiIndent + 50, 20 + 50, Qt.LeftButton) + var firstItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 2*50, Qt.LeftButton) + var secondItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 3*50, Qt.LeftButton) + var thirdItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 4*50, Qt.LeftButton) + var fourthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 5*50, Qt.LeftButton) + var fifthItem = tree.currentIndex + mouseClick(tree, semiIndent + 50, 20 + 6*50, Qt.LeftButton) + var sixthItem = tree.currentIndex + + compare(tree.selection, null) + tree.selection = selectionModel + compare(tree.selection, selectionModel) + tree.selection.clear() + compare(tree.selection.hasSelection, false) + + ////// Contiguous selection + tree.selectionMode = SelectionMode.ContiguousSelection + compare(tree.selectionMode, SelectionMode.ContiguousSelection) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + + compare(secondItem.internalId, tree.currentIndex.internalId) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + var listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 1) + compare(listIndexes.at(0).internalId, secondItem.internalId) + expectFailContinue('', 'BUG selection.currentIndex is invalid when ContiguousSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, secondItem.internalId) + + // Re-click does not deselect + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + // Ctrl/Cmd click deselect + mouseClick(tree, semiIndent + 50, 70+52, Qt.LeftButton, Qt.ControlModifier) + compare(tree.selection.hasSelection, false) + compare(tree.selection.isSelected(secondItem), false) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 0) + + mouseClick(tree, semiIndent + 50, 70+50, Qt.LeftButton) + keyPress(Qt.Key_Down, Qt.ShiftModifier) + keyPress(Qt.Key_Down, Qt.ShiftModifier) + keyClick(Qt.Key_Down, Qt.ShiftModifier) + + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 4) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(thirdItem), true) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(fifthItem), true) + expectFailContinue('', 'BUG selection.currentIndex is invalid when ContiguousSelection') + verify(tree.selection.currentIndex.valid) + if (tree.selection.currentIndex.valid) + compare(tree.selection.currentIndex.internalId, fifthItem.internalId) + + mouseClick(tree, semiIndent + 50, 70+300, Qt.LeftButton, Qt.ShiftModifier) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 6) + + mouseClick(tree, semiIndent + 50, 70+150, Qt.LeftButton, Qt.ShiftModifier) + + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 3) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(secondItem), true) + compare(tree.selection.isSelected(thirdItem), true) + compare(tree.selection.isSelected(fourthItem), true) + compare(tree.selection.isSelected(fifthItem), false) + compare(tree.selection.isSelected(sixthItem), false) + + mouseClick(tree, semiIndent + 50, 70+100, Qt.LeftButton) + listIndexes = tree.selection.selectedIndexes() + compare(listIndexes.length, 1) + compare(tree.selection.hasSelection, true) + compare(tree.selection.isSelected(thirdItem), true) + + tree.destroy() + } + } +} diff --git a/tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro b/tests/auto/qquicktreemodeladaptor/qquicktreemodeladaptor.pro new file mode 100644 index 00000000..cae5e4cd --- /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/testmodel.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 00000000..ea082bc0 --- /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 +#include +#include +#include "../shared/testmodel.h" + +class tst_QQuickTreeModelAdaptor : public QObject +{ + Q_OBJECT + +public: + void compareData(int row, QQuickTreeModelAdaptor &tma, const QModelIndex &idx, TestModel &model, bool expanded = false); + void compareModels(QQuickTreeModelAdaptor &tma, TestModel &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, TestModel &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 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(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 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(1, QQuickTreeModelAdaptor::ExpandedRole)); + } else { + QCOMPARE(tma.rowCount(), oldRowCount); + QCOMPARE(rowsAboutToBeRemovedSpy.count(), 0); + QCOMPARE(rowsRemovedSpy.count(), 0); + } +} + +void tst_QQuickTreeModelAdaptor::compareModels(QQuickTreeModelAdaptor &tma, TestModel &model) +{ + QModelIndex parent; + QStack 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() +{ + TestModel 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(0)); +} + +void tst_QQuickTreeModelAdaptor::modelReset() +{ + TestModel 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() +{ + TestModel 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() +{ + TestModel model(5, 1); + + QQuickTreeModelAdaptor tma; + tma.setModel(&model); + + QSignalSpy dataChangedSpy(&tma, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector))); + 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(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(1, Qt::DisplayRole)); + compareModels(tma, model); + } +} + +void tst_QQuickTreeModelAdaptor::groupedDataChange() +{ + TestModel 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))); + const QVector 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 >(), 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 >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaBRIdx); + QCOMPARE(dataChangedArgs.at(2).value >(), 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 >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaOtherExpandedIdx); + QCOMPARE(dataChangedArgs.at(2).value >(), roles); + + dataChangedArgs = dataChangedSpy.takeFirst(); + QCOMPARE(dataChangedArgs.at(0).toModelIndex(), tmaOtherExpandedSiblingIdx); + QCOMPARE(dataChangedArgs.at(1).toModelIndex(), tmaBRIdx); + QCOMPARE(dataChangedArgs.at(2).value >(), 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("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); + TestModel 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 + TestModel 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() +{ + TestModel 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))); + 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 >().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 >().isEmpty()); + compareModels(tma, model); + + // One parent layout change, expanded + dataChangedSpy.clear(); + QList 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 >().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 >().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 >().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 >().isEmpty()); + compareModels(tma, model); +} + +static const int ModelRowCount = 9; + +void tst_QQuickTreeModelAdaptor::removeRows_data() +{ + QTest::addColumn("removeFromRow"); + QTest::addColumn("removeCount"); + QTest::addColumn("removeParentRow"); + QTest::addColumn("expandRow"); + QTest::addColumn("expandParentRow"); + QTest::addColumn("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); + + TestModel 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("insertFromRow"); + QTest::addColumn("insertCount"); + QTest::addColumn("insertParentRow"); + QTest::addColumn("expandRow"); + QTest::addColumn("expandParentRow"); + QTest::addColumn("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); + + TestModel 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("sourceRow"); + QTest::addColumn("expandSource"); + QTest::addColumn("moveCount"); + QTest::addColumn("sourceParentRow"); + QTest::addColumn("expandSourceParent"); + QTest::addColumn("destRow"); + QTest::addColumn("expandDest"); + QTest::addColumn("destParentRow"); + QTest::addColumn("expandDestParent"); + QTest::addColumn("expandRow"); + QTest::addColumn("expandParentRow"); + QTest::addColumn("signalType"); + QTest::addColumn("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); + + TestModel 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; + + TestModel 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/testmodel.h b/tests/auto/shared/testmodel.h new file mode 100644 index 00000000..0bc06757 --- /dev/null +++ b/tests/auto/shared/testmodel.h @@ -0,0 +1,333 @@ +/**************************************************************************** +** +** 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 +#include + +class TestModel: public QAbstractItemModel +{ + Q_OBJECT +public: + TestModel(QObject *parent = 0): QAbstractItemModel(parent), + fetched(false), rows(10), cols(1), levels(INT_MAX), wrongIndex(false) { init(); } + + TestModel(int _rows, int _cols, QObject *parent = 0): QAbstractItemModel(parent), + fetched(false), rows(_rows), cols(_cols), levels(INT_MAX), wrongIndex(false) { init(); } + + enum { + NameRole = Qt::UserRole + 1 + }; + + 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; + } + + Q_INVOKABLE 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); + + if (role == NameRole) + return QString("Name-%1-%2-%3-%4").arg(idx.row()).arg(idx.column()).arg(idx.internalId()).arg(hasChildren(idx)); + + return QVariant(); + } + + bool setData(const QModelIndex &index, const QVariant &value, int role) + { + Q_UNUSED(value); + QVector changedRole(1, role); + emit dataChanged(index, index, changedRole); + return true; + } + + void groupedSetData(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) + { + emit dataChanged(topLeft, bottomRight, roles); + } + + void changeLayout(const QList &parents = QList()) + { + 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 buffer = src->children.mid(sourceRow, count); + if (src != dest) { + src->removeRows(sourceRow, count, true /* keep alive */); + dest->addRows(destinationChild, count); + } else { + QVector &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 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); + } + } + }; + + QHash roleNames() const + { + QHash rn = QAbstractItemModel::roleNames(); + rn[NameRole] = "name"; + return rn; + } + + Node *tree; +}; diff --git a/tests/auto/testplugin/testplugin.cpp b/tests/auto/testplugin/testplugin.cpp index cece3939..0f23f0b4 100644 --- a/tests/auto/testplugin/testplugin.cpp +++ b/tests/auto/testplugin/testplugin.cpp @@ -40,12 +40,14 @@ #include #include "testplugin.h" #include "testcppmodels.h" +#include "../shared/testmodel.h" void TestPlugin::registerTypes(const char *uri) { // cpp models qmlRegisterType(uri, 1, 0, "TestObject"); qmlRegisterType(uri, 1, 0, "TestItemModel"); + qmlRegisterType(uri, 1, 0, "TreeModel"); } void TestPlugin::initializeEngine(QQmlEngine *engine, const char * /*uri*/) diff --git a/tests/auto/testplugin/testplugin.pro b/tests/auto/testplugin/testplugin.pro index 3914c7ee..dfdd1a81 100644 --- a/tests/auto/testplugin/testplugin.pro +++ b/tests/auto/testplugin/testplugin.pro @@ -18,7 +18,8 @@ SOURCES += \ HEADERS += \ $$PWD/testplugin.h \ - $$PWD/testcppmodels.h + $$PWD/testcppmodels.h \ + $$PWD/../shared/testmodel.h mac { LIBS += -framework Carbon -- cgit v1.2.1