From 0cc6d2a8ac895c592ccdc430ad08bd399e19ba19 Mon Sep 17 00:00:00 2001 From: Fabian Kosmale Date: Thu, 15 Aug 2019 10:03:57 +0200 Subject: Support required properties in model delegates If a delegates declares a required property of a given name, and that name exists as a role in the model, we set the property accordingly. The same holds true for the special properties that come from the QQmlDelegateModel like "index" and "model". All roles are still injected into scope and thus accessible; changing this in Qt5 would be tedious or even impossible while still maintaining backwardscompatibility with delegates that do not use required properties. Change-Id: I4f388ba549c42f1ff9822bdb3b8357c4d45e4b66 Reviewed-by: Ulf Hermann --- .../data/delegatesWithRequiredProperties.qml | 47 ++++++++++++ .../quick/qquicklistview/tst_qquicklistview.cpp | 86 ++++++++++++++++++++++ .../data/delegateWithRequiredProperties.2.qml | 59 +++++++++++++++ .../data/delegateWithRequiredProperties.qml | 53 +++++++++++++ ...ewithUnrelatedRequiredPreventsAccessToModel.qml | 52 +++++++++++++ .../quick/qquickpathview/tst_qquickpathview.cpp | 30 ++++++++ .../quick/qquickrepeater/data/objlist_required.qml | 23 ++++++ .../quick/qquickrepeater/data/requiredProperty.qml | 16 ++++ .../quick/qquickrepeater/tst_qquickrepeater.cpp | 25 ++++++- .../qquicktableview/data/delegateWithRequired.qml | 70 ++++++++++++++++++ .../data/delegatewithRequiredUnset.qml | 71 ++++++++++++++++++ .../quick/qquicktableview/tst_qquicktableview.cpp | 45 +++++++++++ 12 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 tests/auto/quick/qquicklistview/data/delegatesWithRequiredProperties.qml create mode 100644 tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.2.qml create mode 100644 tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.qml create mode 100644 tests/auto/quick/qquickpathview/data/delegatewithUnrelatedRequiredPreventsAccessToModel.qml create mode 100644 tests/auto/quick/qquickrepeater/data/objlist_required.qml create mode 100644 tests/auto/quick/qquickrepeater/data/requiredProperty.qml create mode 100644 tests/auto/quick/qquicktableview/data/delegateWithRequired.qml create mode 100644 tests/auto/quick/qquicktableview/data/delegatewithRequiredUnset.qml (limited to 'tests/auto') diff --git a/tests/auto/quick/qquicklistview/data/delegatesWithRequiredProperties.qml b/tests/auto/quick/qquicklistview/data/delegatesWithRequiredProperties.qml new file mode 100644 index 0000000000..f354517678 --- /dev/null +++ b/tests/auto/quick/qquicklistview/data/delegatesWithRequiredProperties.qml @@ -0,0 +1,47 @@ +import QtQuick 2.12 +import Qt.fruit 1.0 + +Rectangle { + id: root + required property bool useCpp + width: 200; height: 200 + + + ListModel { + id: fruitModel + + ListElement { + name: "Apple" + cost: 2 + } + ListElement { + name: "Orange" + cost: 3 + } + ListElement { + name: "Banana" + cost: 1 + } + } + + + Component { + id: fruitDelegate + Row { + id: row + spacing: 10 + required property string name + required property int cost + Text { text: row.name } + Text { text: '$' + row.cost } + Component.onCompleted: () => { console.debug(row.name+row.cost) }; + } + } + + ListView { + anchors.fill: parent + model: root.useCpp ? FruitModelCpp : fruitModel + delegate: fruitDelegate + } + +} diff --git a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp index 7c26c22217..bf593cfd2e 100644 --- a/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp +++ b/tests/auto/quick/qquicklistview/tst_qquicklistview.cpp @@ -280,6 +280,8 @@ private slots: void touchCancel(); void resizeAfterComponentComplete(); + void delegateWithRequiredProperties(); + private: template void items(const QUrl &source); template void changed(const QUrl &source); @@ -9037,6 +9039,90 @@ void tst_QQuickListView::resizeAfterComponentComplete() // QTBUG-76487 QTRY_COMPARE(lastItem->property("y").toInt(), 9 * lastItem->property("height").toInt()); } +class Animal +{ +public: + Animal(const int cost, const QString &name) {m_name = name; m_cost = cost;} + + int cost() const {return m_cost;} + QString name() const {return m_name;} + + QString m_name; + int m_cost; +}; + +class FruitModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum AnimalRoles { + NameRole = Qt::UserRole + 1, + CostRole + }; + + FruitModel(QObject* = nullptr) { + m_animals.push_back(Animal {4, QLatin1String("Melon")}); + m_animals.push_back(Animal {5, QLatin1String("Cherry")}); + } + + int rowCount(const QModelIndex & = QModelIndex()) const override {return m_animals.count();} + + QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const override { + if (!checkIndex(index)) + return {}; + const Animal &animal = m_animals[index.row()]; + if (role == CostRole) + return animal.cost(); + else if (role == NameRole) + return animal.name(); + return QVariant(); + } + +protected: + QHash roleNames() const override { + QHash roles; + roles[CostRole] = "cost"; + roles[NameRole] = "name"; + return roles; + } +private: + QList m_animals; +}; + +void tst_QQuickListView::delegateWithRequiredProperties() +{ + FruitModel myModel; + qmlRegisterSingletonInstance("Qt.fruit", 1, 0, "FruitModelCpp", &myModel); + { + // ListModel + QTest::ignoreMessage(QtMsgType::QtDebugMsg, "Apple2"); + QTest::ignoreMessage(QtMsgType::QtDebugMsg, "Orange3"); + QTest::ignoreMessage(QtMsgType::QtDebugMsg, "Banana1"); + QScopedPointer window(createView()); + window->setInitialProperties({{QLatin1String("useCpp"), false}}); + window->setSource(testFileUrl("delegatesWithRequiredProperties.qml")); + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QObject *listView = window->rootObject(); + QVERIFY(listView); + } + { + // C++ model + QTest::ignoreMessage(QtMsgType::QtDebugMsg, "Melon4"); + QTest::ignoreMessage(QtMsgType::QtDebugMsg, "Cherry5"); + QScopedPointer window(createView()); + window->setInitialProperties({{QLatin1String("useCpp"), true}}); + window->setSource(testFileUrl("delegatesWithRequiredProperties.qml")); + + window->show(); + QVERIFY(QTest::qWaitForWindowExposed(window.data())); + + QObject *listView = window->rootObject(); + QVERIFY(listView); + } +} + QTEST_MAIN(tst_QQuickListView) #include "tst_qquicklistview.moc" diff --git a/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.2.qml b/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.2.qml new file mode 100644 index 0000000000..ae8ca784bc --- /dev/null +++ b/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.2.qml @@ -0,0 +1,59 @@ +import QtQuick 2.14 + +Item { + id: root + width: 800 + height: 600 + property bool working: false + + ListModel { + id: myModel + ListElement { + name: "Bill Jones" + place: "Berlin" + } + ListElement { + name: "Jane Doe" + place: "Oslo" + } + ListElement { + name: "John Smith" + place: "Oulo" + } + } + + Component { + id: delegateComponent + Rectangle { + id: myDelegate + height: 50 + width: 50 + required property string name + required property int index + onNameChanged: () => {if (myDelegate.name === "You-know-who") root.working = true} + Text { + text: myDelegate.name + font.pointSize: 10 + anchors.fill: myDelegate + } + } + } + + PathView { + anchors.fill: parent + model: myModel + delegate: delegateComponent + path: Path { + startX: 80; startY: 100 + PathQuad { x: 120; y: 25; controlX: 260; controlY: 75 } + PathQuad { x: 140; y: 100; controlX: -20; controlY: 75 } + } + } + Timer { + interval: 1 + running: true + repeat: false + onTriggered: () => { myModel.setProperty(1, "name", "You-know-who"); } + } + +} diff --git a/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.qml b/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.qml new file mode 100644 index 0000000000..5d721fd0c4 --- /dev/null +++ b/tests/auto/quick/qquickpathview/data/delegateWithRequiredProperties.qml @@ -0,0 +1,53 @@ +import QtQuick 2.14 + +Item { + width: 400 + height: 200 + + ListModel { + id: myModel + ListElement { + name: "Bill Jones" + place: "Berlin" + } + ListElement { + name: "Jane Doe" + place: "Oslo" + } + ListElement { + name: "John Smith" + place: "Oulo" + } + } + + Component { + id: delegateComponent + Rectangle { + id: myDelegate + required property int index + required property string name + required property string place + height: 100 + width: 100 + Text { + text: myDelegate.name + " lives in " + myDelegate.place + myDelegate.index + font.pointSize: 16 + anchors.fill: myDelegate + + Component.onCompleted: () => {console.info(myDelegate.name+myDelegate.place+myDelegate.index)} + } + } + } + + PathView { + anchors.fill: parent + model: myModel + delegate: delegateComponent + path: Path { + startX: 120; startY: 100 + PathQuad { x: 120; y: 25; controlX: 260; controlY: 75 } + PathQuad { x: 120; y: 100; controlX: -20; controlY: 75 } + } + } + +} diff --git a/tests/auto/quick/qquickpathview/data/delegatewithUnrelatedRequiredPreventsAccessToModel.qml b/tests/auto/quick/qquickpathview/data/delegatewithUnrelatedRequiredPreventsAccessToModel.qml new file mode 100644 index 0000000000..bf130a2d73 --- /dev/null +++ b/tests/auto/quick/qquickpathview/data/delegatewithUnrelatedRequiredPreventsAccessToModel.qml @@ -0,0 +1,52 @@ +import QtQuick 2.14 + +Item { + width: 400 + height: 200 + + ListModel { + id: myModel + ListElement { + name: "Bill Jones" + place: "Berlin" + } + ListElement { + name: "Jane Doe" + place: "Oslo" + } + ListElement { + name: "John Smith" + place: "Oulo" + } + } + + Component { + id: delegateComponent + Rectangle { + id: myDelegate + required property int set + set: 42 + height: 100 + width: 100 + Text { + text: "Test" + font.pointSize: 16 + anchors.fill: myDelegate + + Component.onCompleted: () => { try {index; console.log(index); console.log(name)} catch(ex) {console.info(ex.name)} } + } + } + } + + PathView { + anchors.fill: parent + model: myModel + delegate: delegateComponent + path: Path { + startX: 120; startY: 100 + PathQuad { x: 120; y: 25; controlX: 260; controlY: 75 } + PathQuad { x: 120; y: 100; controlX: -20; controlY: 75 } + } + } + +} diff --git a/tests/auto/quick/qquickpathview/tst_qquickpathview.cpp b/tests/auto/quick/qquickpathview/tst_qquickpathview.cpp index 8b963117ed..36a242bf3b 100644 --- a/tests/auto/quick/qquickpathview/tst_qquickpathview.cpp +++ b/tests/auto/quick/qquickpathview/tst_qquickpathview.cpp @@ -149,6 +149,8 @@ private slots: void movementDirection(); void removePath(); void objectModelMove(); + void requiredPropertiesInDelegate(); + void requiredPropertiesInDelegatePreventUnrelated(); }; class TestObject : public QObject @@ -2658,6 +2660,34 @@ void tst_QQuickPathView::objectModelMove() } } +void tst_QQuickPathView::requiredPropertiesInDelegate() +{ + { + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "Bill JonesBerlin0"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "Jane DoeOslo1"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "John SmithOulo2"); + QScopedPointer window(createView()); + window->setSource(testFileUrl("delegateWithRequiredProperties.qml")); + window->show(); + } + { + QScopedPointer window(createView()); + window->setSource(testFileUrl("delegateWithRequiredProperties.2.qml")); + window->show(); + QTRY_VERIFY(window->rootObject()->property("working").toBool()); + } +} + +void tst_QQuickPathView::requiredPropertiesInDelegatePreventUnrelated() +{ + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "ReferenceError"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "ReferenceError"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "ReferenceError"); + QScopedPointer window(createView()); + window->setSource(testFileUrl("delegatewithUnrelatedRequiredPreventsAccessToModel.qml")); + window->show(); +} + QTEST_MAIN(tst_QQuickPathView) #include "tst_qquickpathview.moc" diff --git a/tests/auto/quick/qquickrepeater/data/objlist_required.qml b/tests/auto/quick/qquickrepeater/data/objlist_required.qml new file mode 100644 index 0000000000..cc9dd9566c --- /dev/null +++ b/tests/auto/quick/qquickrepeater/data/objlist_required.qml @@ -0,0 +1,23 @@ +import QtQuick 2.0 + +Rectangle { + id: container + objectName: "container" + width: 240 + height: 320 + color: "white" + Repeater { + id: repeater + objectName: "repeater" + model: testData + property int errors: 0 + property int instantiated: 0 + Component { + Item{ + required property int index + required property int idx + Component.onCompleted: {if (index != idx) repeater.errors += 1; repeater.instantiated++} + } + } + } +} diff --git a/tests/auto/quick/qquickrepeater/data/requiredProperty.qml b/tests/auto/quick/qquickrepeater/data/requiredProperty.qml new file mode 100644 index 0000000000..80eb3c28ee --- /dev/null +++ b/tests/auto/quick/qquickrepeater/data/requiredProperty.qml @@ -0,0 +1,16 @@ +import QtQuick 2.14 + +Item { + Column { + Repeater { + model: ["apples", "oranges", "pears"] + Text { + id: txt + required property string modelData + required property int index + text: modelData + index + Component.onCompleted: () => {console.info(txt.text)} + } + } + } +} diff --git a/tests/auto/quick/qquickrepeater/tst_qquickrepeater.cpp b/tests/auto/quick/qquickrepeater/tst_qquickrepeater.cpp index 65e7d29595..ccfef63902 100644 --- a/tests/auto/quick/qquickrepeater/tst_qquickrepeater.cpp +++ b/tests/auto/quick/qquickrepeater/tst_qquickrepeater.cpp @@ -55,6 +55,7 @@ public: private slots: void numberModel(); + void objectList_data(); void objectList(); void stringList(); void dataModel_adding(); @@ -79,6 +80,7 @@ private slots: void QTBUG54859_asynchronousMove(); void package(); void ownership(); + void requiredProperties(); }; class TestObject : public QObject @@ -143,6 +145,14 @@ void tst_QQuickRepeater::numberModel() delete window; } +void tst_QQuickRepeater::objectList_data() +{ + QTest::addColumn("filename"); + + QTest::newRow("normal") << testFileUrl("objlist.qml"); + QTest::newRow("required") << testFileUrl("objlist_required.qml"); +} + class MyObject : public QObject { Q_OBJECT @@ -157,6 +167,7 @@ public: void tst_QQuickRepeater::objectList() { + QFETCH(QUrl, filename); QQuickView *window = createView(); QObjectList data; for (int i=0; i<100; i++) @@ -165,7 +176,7 @@ void tst_QQuickRepeater::objectList() QQmlContext *ctxt = window->rootContext(); ctxt->setContextProperty("testData", QVariant::fromValue(data)); - window->setSource(testFileUrl("objlist.qml")); + window->setSource(filename); qApp->processEvents(); QQuickRepeater *repeater = findItem(window->rootObject(), "repeater"); @@ -1108,6 +1119,18 @@ void tst_QQuickRepeater::ownership() QVERIFY(!modelGuard); } +void tst_QQuickRepeater::requiredProperties() +{ + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "apples0"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "oranges1"); + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "pears2"); + QQmlEngine engine; + + QQmlComponent component(&engine, testFileUrl("requiredProperty.qml")); + QScopedPointer o {component.create()}; + QVERIFY(o); +} + QTEST_MAIN(tst_QQuickRepeater) #include "tst_qquickrepeater.moc" diff --git a/tests/auto/quick/qquicktableview/data/delegateWithRequired.qml b/tests/auto/quick/qquicktableview/data/delegateWithRequired.qml new file mode 100644 index 0000000000..bebfd86931 --- /dev/null +++ b/tests/auto/quick/qquicktableview/data/delegateWithRequired.qml @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Window 2.3 + +Item { + width: 640 + height: 450 + + property alias tableView: tableView + + TableView { + id: tableView + width: 600 + height: 400 + delegate: tableViewDelegate + } + + Component { + id: tableViewDelegate + Rectangle { + id: rect + required property string position + required property bool hasModelChildren + required property QtObject model + Text {text: rect.position} + implicitWidth: 100 + implicitHeight: 100 + Component.onCompleted: () => {if (rect.position === "R1:C1" && rect.model.hasModelChildren == rect.hasModelChildren) console.info("success")} + } + } + +} diff --git a/tests/auto/quick/qquicktableview/data/delegatewithRequiredUnset.qml b/tests/auto/quick/qquicktableview/data/delegatewithRequiredUnset.qml new file mode 100644 index 0000000000..0c685cd49e --- /dev/null +++ b/tests/auto/quick/qquicktableview/data/delegatewithRequiredUnset.qml @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.12 +import QtQuick.Window 2.3 + +Item { + width: 640 + height: 450 + + property alias tableView: tableView + + TableView { + id: tableView + width: 600 + height: 400 + delegate: tableViewDelegate + } + + Component { + id: tableViewDelegate + Rectangle { + id: rect + required property string position + required property bool unset + required property bool hasModelChildren + required property QtObject model + Text {text: rect.position} + implicitWidth: 100 + implicitHeight: 100 + Component.onCompleted: () => {if (rect.position === "R1:C1" && rect.model.hasModelChildren == rect.hasModelChildren) console.info("success")} + } + } + +} diff --git a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp index 19967efcd9..e3f79995d2 100644 --- a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp +++ b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp @@ -172,6 +172,7 @@ private slots: void checkSyncView_differentSizedModels(); void checkSyncView_connect_late_data(); void checkSyncView_connect_late(); + void delegateWithRequiredProperties(); }; tst_QQuickTableView::tst_QQuickTableView() @@ -2592,6 +2593,50 @@ void tst_QQuickTableView::checkSyncView_connect_late() } +void tst_QQuickTableView::delegateWithRequiredProperties() +{ + constexpr static int PositionRole = Qt::UserRole+1; + struct MyTable : QAbstractTableModel { + + + using QAbstractTableModel::QAbstractTableModel; + + int rowCount(const QModelIndex& = QModelIndex()) const override { + return 3; + } + + int columnCount(const QModelIndex& = QModelIndex()) const override { + return 3; + } + + QVariant data(const QModelIndex &index, int = Qt::DisplayRole) const override { + return QVariant::fromValue(QString::asprintf("R%d:C%d", index.row(), index.column())); + } + + QHash roleNames() const override { + return QHash { {PositionRole, "position"} }; + } + }; + + auto model = QVariant::fromValue(QSharedPointer(new MyTable)); + { + QTest::ignoreMessage(QtMsgType::QtInfoMsg, "success"); + LOAD_TABLEVIEW("delegateWithRequired.qml") + QVERIFY(tableView); + tableView->setModel(model); + WAIT_UNTIL_POLISHED; + QVERIFY(view->errors().empty()); + } + { + QTest::ignoreMessage(QtMsgType::QtWarningMsg, QRegularExpression(R"|(TableView: failed loading index: \d)|")); + LOAD_TABLEVIEW("delegatewithRequiredUnset.qml") + QVERIFY(tableView); + tableView->setModel(model); + WAIT_UNTIL_POLISHED; + QTRY_VERIFY(view->errors().empty()); + } +} + QTEST_MAIN(tst_QQuickTableView) #include "tst_qquicktableview.moc" -- cgit v1.2.3