1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28#include <qtest.h>
29#include <QtQuick/private/qquickitem_p.h>
30#include <QtQuick/private/qquicktext_p.h>
31#include <QtQml/private/qqmlengine_p.h>
32#include <QtQmlModels/private/qqmllistmodel_p.h>
33#include <QtQml/private/qqmlexpression_p.h>
34#include <QQmlComponent>
35
36#include <QtCore/qtimer.h>
37#include <QtCore/qdebug.h>
38#include <QtCore/qtranslator.h>
39#include <QSignalSpy>
40
41#include "../../shared/util.h"
42
43Q_DECLARE_METATYPE(QList<int>)
44Q_DECLARE_METATYPE(QList<QVariantHash>)
45
46#define RUNEVAL(object, string) \
47 QVERIFY(QMetaObject::invokeMethod(object, "runEval", Q_ARG(QVariant, QString(string))));
48
49inline QVariant runexpr(QQmlEngine *engine, const QString &str)
50{
51 QQmlExpression expr(engine->rootContext(), nullptr, str);
52 return expr.evaluate();
53}
54
55#define RUNEXPR(string) runexpr(&engine, QString(string))
56
57static bool isValidErrorMessage(const QString &msg, bool dynamicRoleTest)
58{
59 bool valid = true;
60
61 if (msg.isEmpty()) {
62 valid = false;
63 } else if (dynamicRoleTest) {
64 if (msg.contains(s: "Can't assign to existing role") || msg.contains(s: "Can't create role for unsupported data type"))
65 valid = false;
66 }
67
68 return valid;
69}
70
71class tst_qqmllistmodelworkerscript : public QQmlDataTest
72{
73 Q_OBJECT
74public:
75 tst_qqmllistmodelworkerscript()
76 {
77 qRegisterMetaType<QVector<int> >();
78 }
79
80private:
81 int roleFromName(const QQmlListModel *model, const QString &roleName);
82 QQuickItem *createWorkerTest(QQmlEngine *eng, QQmlComponent *component, QQmlListModel *model);
83 void waitForWorker(QQuickItem *item);
84
85 static bool compareVariantList(const QVariantList &testList, QVariant object);
86
87private slots:
88 void dynamic_data();
89 void dynamic_worker_data();
90 void dynamic_worker();
91 void dynamic_worker_sync_data();
92 void dynamic_worker_sync();
93 void get_data();
94 void get_worker();
95 void get_worker_data();
96 void property_changes_data();
97 void property_changes_worker();
98 void property_changes_worker_data();
99 void worker_sync_data();
100 void worker_sync();
101 void worker_remove_element_data();
102 void worker_remove_element();
103 void worker_remove_list_data();
104 void worker_remove_list();
105 void dynamic_role_data();
106 void dynamic_role();
107 void correctMoves();
108};
109
110bool tst_qqmllistmodelworkerscript::compareVariantList(const QVariantList &testList, QVariant object)
111{
112 bool allOk = true;
113
114 QQmlListModel *model = qobject_cast<QQmlListModel *>(object: object.value<QObject *>());
115 if (model == nullptr)
116 return false;
117
118 if (model->count() != testList.count())
119 return false;
120
121 for (int i=0 ; i < testList.count() ; ++i) {
122 const QVariant &testVariant = testList.at(i);
123 if (testVariant.type() != QVariant::Map)
124 return false;
125 const QVariantMap &map = testVariant.toMap();
126
127 const QHash<int, QByteArray> roleNames = model->roleNames();
128
129 QVariantMap::const_iterator it = map.begin();
130 QVariantMap::const_iterator end = map.end();
131
132 while (it != end) {
133 const QString &testKey = it.key();
134 const QVariant &testData = it.value();
135
136 int roleIndex = roleNames.key(value: testKey.toUtf8(), defaultKey: -1);
137 if (roleIndex == -1)
138 return false;
139
140 const QVariant &modelData = model->data(index: model->index(row: i, column: 0, parent: QModelIndex()), role: roleIndex);
141
142 if (testData.type() == QVariant::List) {
143 const QVariantList &subList = testData.toList();
144 allOk = allOk && compareVariantList(testList: subList, object: modelData);
145 } else {
146 allOk = allOk && (testData == modelData);
147 }
148
149 ++it;
150 }
151 }
152
153 return allOk;
154}
155
156int tst_qqmllistmodelworkerscript::roleFromName(const QQmlListModel *model, const QString &roleName)
157{
158 return model->roleNames().key(value: roleName.toUtf8(), defaultKey: -1);
159}
160
161QQuickItem *tst_qqmllistmodelworkerscript::createWorkerTest(QQmlEngine *eng, QQmlComponent *component, QQmlListModel *model)
162{
163 QQuickItem *item = qobject_cast<QQuickItem*>(object: component->create());
164 QQmlEngine::setContextForObject(model, eng->rootContext());
165 if (item)
166 item->setProperty(name: "model", value: QVariant::fromValue(value: model));
167 return item;
168}
169
170void tst_qqmllistmodelworkerscript::waitForWorker(QQuickItem *item)
171{
172 QQmlProperty prop(item, "done");
173 QVERIFY(prop.isValid());
174 if (prop.read().toBool())
175 return; // already finished
176
177 QEventLoop loop;
178 QTimer timer;
179 timer.setSingleShot(true);
180 connect(sender: &timer, SIGNAL(timeout()), receiver: &loop, SLOT(quit()));
181
182 QVERIFY(prop.connectNotifySignal(&loop, SLOT(quit())));
183 timer.start(msec: 10000);
184 loop.exec();
185 QVERIFY(timer.isActive());
186 QVERIFY(prop.read().toBool());
187}
188
189void tst_qqmllistmodelworkerscript::dynamic_data()
190{
191 QTest::addColumn<QString>(name: "script");
192 QTest::addColumn<int>(name: "result");
193 QTest::addColumn<QString>(name: "warning");
194 QTest::addColumn<bool>(name: "dynamicRoles");
195
196 for (int i = 0; i < 2; ++i) {
197 bool dr = (i != 0);
198
199 // Simple flat model
200 QTest::newRow(dataTag: "count") << "count" << 0 << "" << dr;
201
202 QTest::newRow(dataTag: "get1") << "{get(0) === undefined}" << 1 << "" << dr;
203 QTest::newRow(dataTag: "get2") << "{get(-1) === undefined}" << 1 << "" << dr;
204 QTest::newRow(dataTag: "get3") << "{append({'foo':123});get(0) != undefined}" << 1 << "" << dr;
205 QTest::newRow(dataTag: "get4") << "{append({'foo':123});get(0).foo}" << 123 << "" << dr;
206 QTest::newRow(dataTag: "get-modify1") << "{append({'foo':123,'bar':456});get(0).foo = 333;get(0).foo}" << 333 << "" << dr;
207 QTest::newRow(dataTag: "get-modify2") << "{append({'z':1});append({'foo':123,'bar':456});get(1).bar = 999;get(1).bar}" << 999 << "" << dr;
208 QTest::newRow(dataTag: "get-set") << "{append({'foo':123});get(0).foo;setProperty(0, 'foo', 999);get(0).foo}" << 999 << "" << dr;
209
210 QTest::newRow(dataTag: "append1") << "{append({'foo':123});count}" << 1 << "" << dr;
211 QTest::newRow(dataTag: "append2") << "{append({'foo':123,'bar':456});count}" << 1 << "" << dr;
212 QTest::newRow(dataTag: "append3a") << "{append({'foo':123});append({'foo':456});get(0).foo}" << 123 << "" << dr;
213 QTest::newRow(dataTag: "append3b") << "{append({'foo':123});append({'foo':456});get(1).foo}" << 456 << "" << dr;
214 QTest::newRow(dataTag: "append4a") << "{append(123)}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
215 QTest::newRow(dataTag: "append4b") << "{append([{'foo':123},{'foo':456},{'foo':789}]);count}" << 3 << "" << dr;
216 QTest::newRow(dataTag: "append4c") << "{append([{'foo':123},{'foo':456},{'foo':789}]);get(1).foo}" << 456 << "" << dr;
217
218 QTest::newRow(dataTag: "clear1") << "{append({'foo':456});clear();count}" << 0 << "" << dr;
219 QTest::newRow(dataTag: "clear2") << "{append({'foo':123});append({'foo':456});clear();count}" << 0 << "" << dr;
220 QTest::newRow(dataTag: "clear3") << "{append({'foo':123});clear()}" << 0 << "" << dr;
221
222 QTest::newRow(dataTag: "remove1") << "{append({'foo':123});remove(0);count}" << 0 << "" << dr;
223 QTest::newRow(dataTag: "remove2a") << "{append({'foo':123});append({'foo':456});remove(0);count}" << 1 << "" << dr;
224 QTest::newRow(dataTag: "remove2b") << "{append({'foo':123});append({'foo':456});remove(0);get(0).foo}" << 456 << "" << dr;
225 QTest::newRow(dataTag: "remove2c") << "{append({'foo':123});append({'foo':456});remove(1);get(0).foo}" << 123 << "" << dr;
226 QTest::newRow(dataTag: "remove3") << "{append({'foo':123});remove(0)}" << 0 << "" << dr;
227 QTest::newRow(dataTag: "remove3a") << "{append({'foo':123});remove(-1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [-1 - 0] out of range [0 - 1]" << dr;
228 QTest::newRow(dataTag: "remove4a") << "{remove(0)}" << 0 << "<Unknown File>: QML ListModel: remove: indices [0 - 1] out of range [0 - 0]" << dr;
229 QTest::newRow(dataTag: "remove4b") << "{append({'foo':123});remove(0);remove(0);count}" << 0 << "<Unknown File>: QML ListModel: remove: indices [0 - 1] out of range [0 - 0]" << dr;
230 QTest::newRow(dataTag: "remove4c") << "{append({'foo':123});remove(1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [1 - 2] out of range [0 - 1]" << dr;
231 QTest::newRow(dataTag: "remove5a") << "{append({'foo':123});append({'foo':456});remove(0,2);count}" << 0 << "" << dr;
232 QTest::newRow(dataTag: "remove5b") << "{append({'foo':123});append({'foo':456});remove(0,1);count}" << 1 << "" << dr;
233 QTest::newRow(dataTag: "remove5c") << "{append({'foo':123});append({'foo':456});remove(1,1);count}" << 1 << "" << dr;
234 QTest::newRow(dataTag: "remove5d") << "{append({'foo':123});append({'foo':456});remove(0,1);get(0).foo}" << 456 << "" << dr;
235 QTest::newRow(dataTag: "remove5e") << "{append({'foo':123});append({'foo':456});remove(1,1);get(0).foo}" << 123 << "" << dr;
236 QTest::newRow(dataTag: "remove5f") << "{append({'foo':123});append({'foo':456});append({'foo':789});remove(0,1);remove(1,1);get(0).foo}" << 456 << "" << dr;
237 QTest::newRow(dataTag: "remove6a") << "{remove();count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr;
238 QTest::newRow(dataTag: "remove6b") << "{remove(1,2,3);count}" << 0 << "<Unknown File>: QML ListModel: remove: incorrect number of arguments" << dr;
239 QTest::newRow(dataTag: "remove7a") << "{append({'foo':123});remove(0,0);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [0 - 0] out of range [0 - 1]" << dr;
240 QTest::newRow(dataTag: "remove7b") << "{append({'foo':123});remove(0,-1);count}" << 1 << "<Unknown File>: QML ListModel: remove: indices [0 - -1] out of range [0 - 1]" << dr;
241
242 QTest::newRow(dataTag: "insert1") << "{insert(0,{'foo':123});count}" << 1 << "" << dr;
243 QTest::newRow(dataTag: "insert2") << "{insert(1,{'foo':123});count}" << 0 << "<Unknown File>: QML ListModel: insert: index 1 out of range" << dr;
244 QTest::newRow(dataTag: "insert3a") << "{append({'foo':123});insert(1,{'foo':456});count}" << 2 << "" << dr;
245 QTest::newRow(dataTag: "insert3b") << "{append({'foo':123});insert(1,{'foo':456});get(0).foo}" << 123 << "" << dr;
246 QTest::newRow(dataTag: "insert3c") << "{append({'foo':123});insert(1,{'foo':456});get(1).foo}" << 456 << "" << dr;
247 QTest::newRow(dataTag: "insert3d") << "{append({'foo':123});insert(0,{'foo':456});get(0).foo}" << 456 << "" << dr;
248 QTest::newRow(dataTag: "insert3e") << "{append({'foo':123});insert(0,{'foo':456});get(1).foo}" << 123 << "" << dr;
249 QTest::newRow(dataTag: "insert4") << "{append({'foo':123});insert(-1,{'foo':456});count}" << 1 << "<Unknown File>: QML ListModel: insert: index -1 out of range" << dr;
250 QTest::newRow(dataTag: "insert5a") << "{insert(0,123)}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
251 QTest::newRow(dataTag: "insert5b") << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);count}" << 3 << "" << dr;
252 QTest::newRow(dataTag: "insert5c") << "{insert(0,[{'foo':11},{'foo':22},{'foo':33}]);get(2).foo}" << 33 << "" << dr;
253
254 QTest::newRow(dataTag: "set1") << "{append({'foo':123});set(0,{'foo':456});count}" << 1 << "" << dr;
255 QTest::newRow(dataTag: "set2") << "{append({'foo':123});set(0,{'foo':456});get(0).foo}" << 456 << "" << dr;
256 QTest::newRow(dataTag: "set3a") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).foo}" << 999 << "" << dr;
257 QTest::newRow(dataTag: "set3b") << "{append({'foo':123,'bar':456});set(0,{'foo':999});get(0).bar}" << 456 << "" << dr;
258 QTest::newRow(dataTag: "set4a") << "{set(0,{'foo':456});count}" << 1 << "" << dr;
259 QTest::newRow(dataTag: "set4c") << "{set(-1,{'foo':456})}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr;
260 QTest::newRow(dataTag: "set5a") << "{append({'foo':123,'bar':456});set(0,123);count}" << 1 << "<Unknown File>: QML ListModel: set: value is not an object" << dr;
261 QTest::newRow(dataTag: "set5b") << "{append({'foo':123,'bar':456});set(0,[1,2,3]);count}" << 1 << "" << dr;
262 QTest::newRow(dataTag: "set6") << "{append({'foo':123});set(1,{'foo':456});count}" << 2 << "" << dr;
263
264 QTest::newRow(dataTag: "setprop1") << "{append({'foo':123});setProperty(0,'foo',456);count}" << 1 << "" << dr;
265 QTest::newRow(dataTag: "setprop2") << "{append({'foo':123});setProperty(0,'foo',456);get(0).foo}" << 456 << "" << dr;
266 QTest::newRow(dataTag: "setprop3a") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).foo}" << 999 << "" << dr;
267 QTest::newRow(dataTag: "setprop3b") << "{append({'foo':123,'bar':456});setProperty(0,'foo',999);get(0).bar}" << 456 << "" << dr;
268 QTest::newRow(dataTag: "setprop4a") << "{setProperty(0,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index 0 out of range" << dr;
269 QTest::newRow(dataTag: "setprop4b") << "{setProperty(-1,'foo',456)}" << 0 << "<Unknown File>: QML ListModel: set: index -1 out of range" << dr;
270 QTest::newRow(dataTag: "setprop4c") << "{append({'foo':123,'bar':456});setProperty(1,'foo',456);count}" << 1 << "<Unknown File>: QML ListModel: set: index 1 out of range" << dr;
271 QTest::newRow(dataTag: "setprop5") << "{append({'foo':123,'bar':456});append({'foo':111});setProperty(1,'bar',222);get(1).bar}" << 222 << "" << dr;
272
273 QTest::newRow(dataTag: "move1a") << "{append({'foo':123});append({'foo':456});move(0,1,1);count}" << 2 << "" << dr;
274 QTest::newRow(dataTag: "move1b") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(0).foo}" << 456 << "" << dr;
275 QTest::newRow(dataTag: "move1c") << "{append({'foo':123});append({'foo':456});move(0,1,1);get(1).foo}" << 123 << "" << dr;
276 QTest::newRow(dataTag: "move1d") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(0).foo}" << 456 << "" << dr;
277 QTest::newRow(dataTag: "move1e") << "{append({'foo':123});append({'foo':456});move(1,0,1);get(1).foo}" << 123 << "" << dr;
278 QTest::newRow(dataTag: "move2a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);count}" << 3 << "" << dr;
279 QTest::newRow(dataTag: "move2b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(0).foo}" << 789 << "" << dr;
280 QTest::newRow(dataTag: "move2c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(1).foo}" << 123 << "" << dr;
281 QTest::newRow(dataTag: "move2d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,1,2);get(2).foo}" << 456 << "" << dr;
282 QTest::newRow(dataTag: "move3a") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,3);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
283 QTest::newRow(dataTag: "move3b") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,-1,1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
284 QTest::newRow(dataTag: "move3c") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(1,0,-1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
285 QTest::newRow(dataTag: "move3d") << "{append({'foo':123});append({'foo':456});append({'foo':789});move(0,3,1);count}" << 3 << "<Unknown File>: QML ListModel: move: out of range" << dr;
286
287 QTest::newRow(dataTag: "large1") << "{append({'a':1,'b':2,'c':3,'d':4,'e':5,'f':6,'g':7,'h':8});get(0).h}" << 8 << "" << dr;
288
289 QTest::newRow(dataTag: "datatypes1") << "{append({'a':1});append({'a':'string'});}" << 0 << "<Unknown File>: Can't assign to existing role 'a' of different type [String -> Number]" << dr;
290
291 QTest::newRow(dataTag: "null") << "{append({'a':null});}" << 0 << "" << dr;
292 QTest::newRow(dataTag: "setNull") << "{append({'a':1});set(0, {'a':null});}" << 0 << "" << dr;
293 QTest::newRow(dataTag: "setString") << "{append({'a':'hello'});set(0, {'a':'world'});get(0).a == 'world'}" << 1 << "" << dr;
294 QTest::newRow(dataTag: "setInt") << "{append({'a':5});set(0, {'a':10});get(0).a}" << 10 << "" << dr;
295 QTest::newRow(dataTag: "setNumber") << "{append({'a':6});set(0, {'a':5.5});get(0).a < 5.6}" << 1 << "" << dr;
296 QTest::newRow(dataTag: "badType0") << "{append({'a':'hello'});set(0, {'a':1});}" << 0 << "<Unknown File>: Can't assign to existing role 'a' of different type [Number -> String]" << dr;
297 QTest::newRow(dataTag: "invalidInsert0") << "{insert(0);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
298 QTest::newRow(dataTag: "invalidAppend0") << "{append();}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
299 QTest::newRow(dataTag: "invalidInsert1") << "{insert(0, 34);}" << 0 << "<Unknown File>: QML ListModel: insert: value is not an object" << dr;
300 QTest::newRow(dataTag: "invalidAppend1") << "{append(37);}" << 0 << "<Unknown File>: QML ListModel: append: value is not an object" << dr;
301
302 // QObjects
303 QTest::newRow(dataTag: "qobject0") << "{append({'a':dummyItem0});}" << 0 << "" << dr;
304 QTest::newRow(dataTag: "qobject1") << "{append({'a':dummyItem0});set(0,{'a':dummyItem1});get(0).a == dummyItem1;}" << 1 << "" << dr;
305 QTest::newRow(dataTag: "qobject2") << "{append({'a':dummyItem0});get(0).a == dummyItem0;}" << 1 << "" << dr;
306 QTest::newRow(dataTag: "qobject3") << "{append({'a':dummyItem0});append({'b':1});}" << 0 << "" << dr;
307
308 // JS objects
309 QTest::newRow(dataTag: "js1") << "{append({'foo':{'prop':1}});count}" << 1 << "" << dr;
310 QTest::newRow(dataTag: "js2") << "{append({'foo':{'prop':27}});get(0).foo.prop}" << 27 << "" << dr;
311 QTest::newRow(dataTag: "js3") << "{append({'foo':{'prop':27}});append({'bar':1});count}" << 2 << "" << dr;
312 QTest::newRow(dataTag: "js4") << "{append({'foo':{'prop':27}});append({'bar':1});set(0, {'foo':{'prop':28}});get(0).foo.prop}" << 28 << "" << dr;
313 QTest::newRow(dataTag: "js5") << "{append({'foo':{'prop':27}});append({'bar':1});set(1, {'foo':{'prop':33}});get(1).foo.prop}" << 33 << "" << dr;
314 QTest::newRow(dataTag: "js6") << "{append({'foo':{'prop':27}});clear();count}" << 0 << "" << dr;
315 QTest::newRow(dataTag: "js7") << "{append({'foo':{'prop':27}});set(0, {'foo':null});count}" << 1 << "" << dr;
316 QTest::newRow(dataTag: "js8") << "{append({'foo':{'prop':27}});set(0, {'foo':{'prop2':31}});get(0).foo.prop2}" << 31 << "" << dr;
317
318 // Nested models
319 QTest::newRow(dataTag: "nested-append1") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});count}" << 1 << "" << dr;
320 QTest::newRow(dataTag: "nested-append2") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.get(1).a}" << 2 << "" << dr;
321 QTest::newRow(dataTag: "nested-append3") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]});get(0).bars.append({'a':4});get(0).bars.get(3).a}" << 4 << "" << dr;
322
323 QTest::newRow(dataTag: "nested-insert") << "{append({'foo':123});insert(0,{'bars':[{'a':1},{'b':2},{'c':3}]});get(0).bars.get(0).a}" << 1 << "" << dr;
324 QTest::newRow(dataTag: "nested-set") << "{append({'foo':[{'x':1}]});set(0,{'foo':[{'x':123}]});get(0).foo.get(0).x}" << 123 << "" << dr;
325
326 QTest::newRow(dataTag: "nested-count") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.count}" << 3 << "" << dr;
327 QTest::newRow(dataTag: "nested-clear") << "{append({'foo':123,'bars':[{'a':1},{'a':2},{'a':3}]}); get(0).bars.clear(); get(0).bars.count}" << 0 << "" << dr;
328 }
329}
330
331void tst_qqmllistmodelworkerscript::dynamic_worker_data()
332{
333 dynamic_data();
334}
335
336void tst_qqmllistmodelworkerscript::dynamic_worker()
337{
338 QFETCH(QString, script);
339 QFETCH(int, result);
340 QFETCH(QString, warning);
341 QFETCH(bool, dynamicRoles);
342
343 if (QByteArray(QTest::currentDataTag()).startsWith(c: "qobject"))
344 return;
345
346 // This is same as dynamic() except it applies the test to a ListModel called
347 // from a WorkerScript.
348
349 QQmlListModel model;
350 model.setDynamicRoles(dynamicRoles);
351 QQmlEngine eng;
352 QQmlComponent component(&eng, testFileUrl(fileName: "model.qml"));
353 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
354 QVERIFY(item != nullptr);
355
356 QSignalSpy spyCount(&model, SIGNAL(countChanged()));
357
358 if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}'))
359 script = script.mid(position: 1, n: script.length() - 2);
360 QVariantList operations;
361 foreach (const QString &s, script.split(';')) {
362 if (!s.isEmpty())
363 operations << s;
364 }
365
366 if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles))
367 QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1());
368
369 QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker",
370 Q_ARG(QVariant, operations)));
371 waitForWorker(item);
372 QCOMPARE(QQmlProperty(item, "result").read().toInt(), result);
373
374 if (model.count() > 0)
375 QVERIFY(spyCount.count() > 0);
376
377 delete item;
378 qApp->processEvents();
379}
380
381void tst_qqmllistmodelworkerscript::dynamic_worker_sync_data()
382{
383 dynamic_data();
384}
385
386void tst_qqmllistmodelworkerscript::dynamic_worker_sync()
387{
388 QFETCH(QString, script);
389 QFETCH(int, result);
390 QFETCH(QString, warning);
391 QFETCH(bool, dynamicRoles);
392
393 if (QByteArray(QTest::currentDataTag()).startsWith(c: "qobject"))
394 return;
395
396 // This is the same as dynamic_worker() except that it executes a set of list operations
397 // from the worker script, calls sync(), and tests the changes are reflected in the
398 // list in the main thread
399
400 QQmlListModel model;
401 model.setDynamicRoles(dynamicRoles);
402 QQmlEngine eng;
403 QQmlComponent component(&eng, testFileUrl(fileName: "model.qml"));
404 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
405 QVERIFY(item != nullptr);
406
407 if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}'))
408 script = script.mid(position: 1, n: script.length() - 2);
409 QVariantList operations;
410 foreach (const QString &s, script.split(';')) {
411 if (!s.isEmpty())
412 operations << s;
413 }
414
415 if (isValidErrorMessage(msg: warning, dynamicRoleTest: dynamicRoles))
416 QTest::ignoreMessage(type: QtWarningMsg, message: warning.toLatin1());
417
418 // execute a set of commands on the worker list model, then check the
419 // changes are reflected in the list model in the main thread
420 QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker",
421 Q_ARG(QVariant, operations.mid(0, operations.length()-1))));
422 waitForWorker(item);
423
424 QQmlExpression e(eng.rootContext(), &model, operations.last().toString());
425 QCOMPARE(e.evaluate().toInt(), result);
426
427 delete item;
428 qApp->processEvents();
429}
430
431void tst_qqmllistmodelworkerscript::get_data()
432{
433 QTest::addColumn<QString>(name: "expression");
434 QTest::addColumn<int>(name: "index");
435 QTest::addColumn<QString>(name: "roleName");
436 QTest::addColumn<QVariant>(name: "roleValue");
437 QTest::addColumn<bool>(name: "dynamicRoles");
438
439 for (int i =0; i < 2; ++i) {
440 bool dr = (i != 0);
441
442 QTest::newRow(dataTag: "simple value") << "get(0).roleA = 500" << 0 << "roleA" << QVariant(500) << dr;
443 QTest::newRow(dataTag: "simple value 2") << "get(1).roleB = 500" << 1 << "roleB" << QVariant(500) << dr;
444
445 QVariantMap map;
446 QVariantList list;
447 map.clear(); map["a"] = 50; map["b"] = 500;
448 list << map;
449 map.clear(); map["c"] = 1000;
450 list << map;
451 QTest::newRow(dataTag: "list of objects") << "get(2).roleD = [{'a': 50, 'b': 500}, {'c': 1000}]" << 2 << "roleD" << QVariant::fromValue(value: list) << dr;
452 }
453}
454
455void tst_qqmllistmodelworkerscript::get_worker()
456{
457 QFETCH(QString, expression);
458 QFETCH(int, index);
459 QFETCH(QString, roleName);
460 QFETCH(QVariant, roleValue);
461 QFETCH(bool, dynamicRoles);
462
463 QQmlListModel model;
464 model.setDynamicRoles(dynamicRoles);
465 QQmlEngine eng;
466 QQmlComponent component(&eng, testFileUrl(fileName: "model.qml"));
467 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
468 QVERIFY(item != nullptr);
469
470 // Add some values like get() test
471 RUNEVAL(item, "model.append({roleA: 100})");
472 RUNEVAL(item, "model.append({roleA: 200, roleB: 400})");
473 RUNEVAL(item, "model.append({roleA: 200, roleB: 400})");
474 RUNEVAL(item, "model.append({roleC: {} })");
475 RUNEVAL(item, "model.append({roleD: [ { a:1, b:2 }, { c: 3 } ] })");
476
477 int role = roleFromName(model: &model, roleName);
478 QVERIFY(role >= 0);
479
480 QSignalSpy spy(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
481
482 // in the worker thread, change the model data and call sync()
483 QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker",
484 Q_ARG(QVariant, QStringList(expression))));
485 waitForWorker(item);
486
487 // see if we receive the model changes in the main thread's model
488 if (roleValue.type() == QVariant::List) {
489 const QVariantList &list = roleValue.toList();
490 QVERIFY(compareVariantList(list, model.data(index, role)));
491 } else {
492 QCOMPARE(model.data(index, role), roleValue);
493 }
494
495 QCOMPARE(spy.count(), 1);
496
497 QList<QVariant> spyResult = spy.takeFirst();
498 QCOMPARE(spyResult.at(0).value<QModelIndex>(), model.index(index, 0, QModelIndex()));
499 QCOMPARE(spyResult.at(1).value<QModelIndex>(), model.index(index, 0, QModelIndex())); // only 1 item is modified at a time
500 QVERIFY(spyResult.at(2).value<QVector<int> >().contains(role));
501
502 delete item;
503}
504
505void tst_qqmllistmodelworkerscript::get_worker_data()
506{
507 get_data();
508}
509
510void tst_qqmllistmodelworkerscript::property_changes_data()
511{
512 QTest::addColumn<QString>(name: "script_setup");
513 QTest::addColumn<QString>(name: "script_change");
514 QTest::addColumn<QString>(name: "roleName");
515 QTest::addColumn<int>(name: "listIndex");
516 QTest::addColumn<bool>(name: "itemsChanged");
517 QTest::addColumn<QString>(name: "testExpression");
518 QTest::addColumn<bool>(name: "dynamicRoles");
519
520 for (int i=1 ; i < 2 ; ++i) {
521 bool dr = (i != 0);
522
523 QTest::newRow(dataTag: "set: plain") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':123});"
524 << "b" << 0 << true << "get(0).b == 123" << dr;
525 QTest::newRow(dataTag: "setProperty: plain") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 123);"
526 << "b" << 0 << true << "get(0).b == 123" << dr;
527
528 QTest::newRow(dataTag: "set: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "set(0,{'b':456});"
529 << "b" << 0 << false << "get(0).b == 456" << dr;
530 QTest::newRow(dataTag: "setProperty: plain, no changes") << "append({'a':123, 'b':456, 'c':789});" << "setProperty(0, 'b', 456);"
531 << "b" << 0 << false << "get(0).b == 456" << dr;
532
533 QTest::newRow(dataTag: "set: inserted item")
534 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
535 << "set(1, {'a':456});"
536 << "a" << 1 << true << "get(1).a == 456" << dr;
537 QTest::newRow(dataTag: "setProperty: inserted item")
538 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
539 << "setProperty(1, 'a', 456);"
540 << "a" << 1 << true << "get(1).a == 456" << dr;
541 QTest::newRow(dataTag: "get: inserted item")
542 << "{append({'a':123, 'b':456, 'c':789}); get(0); insert(0, {'a':0, 'b':0, 'c':0});}"
543 << "get(1).a = 456;"
544 << "a" << 1 << true << "get(1).a == 456" << dr;
545 QTest::newRow(dataTag: "set: removed item")
546 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
547 << "set(0, {'a':456});"
548 << "a" << 0 << true << "get(0).a == 456" << dr;
549 QTest::newRow(dataTag: "setProperty: removed item")
550 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
551 << "setProperty(0, 'a', 456);"
552 << "a" << 0 << true << "get(0).a == 456" << dr;
553 QTest::newRow(dataTag: "get: removed item")
554 << "{append({'a':0, 'b':0, 'c':0}); append({'a':123, 'b':456, 'c':789}); get(1); remove(0);}"
555 << "get(0).a = 456;"
556 << "a" << 0 << true << "get(0).a == 456" << dr;
557
558 // Following tests only call set() since setProperty() only allows plain
559 // values, not lists, as the argument.
560 // Note that when a list is changed, itemsChanged() is currently always
561 // emitted regardless of whether it actually changed or not.
562
563 QTest::newRow(dataTag: "nested-set: list, new size") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2}]});"
564 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2" << dr;
565
566 QTest::newRow(dataTag: "nested-set: list, empty -> non-empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});"
567 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3" << dr;
568
569 QTest::newRow(dataTag: "nested-set: list, non-empty -> empty") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[]});"
570 << "b" << 0 << true << "get(0).b.count == 0" << dr;
571
572 QTest::newRow(dataTag: "nested-set: list, same size, different values") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':222},{'a':3}]});"
573 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 222 && get(0).b.get(2).a == 3" << dr;
574
575 QTest::newRow(dataTag: "nested-set: list, no changes") << "append({'a':123, 'b':[{'a':1},{'a':2},{'a':3}], 'c':789});" << "set(0,{'b':[{'a':1},{'a':2},{'a':3}]});"
576 << "b" << 0 << true << "get(0).b.get(0).a == 1 && get(0).b.get(1).a == 2 && get(0).b.get(2).a == 3" << dr;
577
578 QTest::newRow(dataTag: "nested-set: list, no changes, empty") << "append({'a':123, 'b':[], 'c':789});" << "set(0,{'b':[]});"
579 << "b" << 0 << false << "get(0).b.count == 0" << dr;
580 }
581}
582
583void tst_qqmllistmodelworkerscript::property_changes_worker()
584{
585 QFETCH(QString, script_setup);
586 QFETCH(QString, script_change);
587 QFETCH(QString, roleName);
588 QFETCH(int, listIndex);
589 QFETCH(bool, itemsChanged);
590 QFETCH(bool, dynamicRoles);
591
592 QQmlListModel model;
593 model.setDynamicRoles(dynamicRoles);
594 QQmlEngine engine;
595 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
596 QVERIFY2(component.errorString().isEmpty(), component.errorString().toUtf8());
597 QQuickItem *item = createWorkerTest(eng: &engine, component: &component, model: &model);
598 QVERIFY(item != nullptr);
599
600 QQmlExpression expr(engine.rootContext(), &model, script_setup);
601 expr.evaluate();
602 QVERIFY2(!expr.hasError(), QTest::toString(expr.error().toString()));
603
604 QSignalSpy spyItemsChanged(&model, SIGNAL(dataChanged(QModelIndex,QModelIndex,QVector<int>)));
605
606 QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker",
607 Q_ARG(QVariant, QStringList(script_change))));
608 waitForWorker(item);
609
610 // test itemsChanged() is emitted correctly
611 if (itemsChanged) {
612 QCOMPARE(spyItemsChanged.count(), 1);
613 QCOMPARE(spyItemsChanged.at(0).at(0).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex()));
614 QCOMPARE(spyItemsChanged.at(0).at(1).value<QModelIndex>(), model.index(listIndex, 0, QModelIndex()));
615 } else {
616 QCOMPARE(spyItemsChanged.count(), 0);
617 }
618
619 delete item;
620 qApp->processEvents();
621}
622
623void tst_qqmllistmodelworkerscript::property_changes_worker_data()
624{
625 property_changes_data();
626}
627
628void tst_qqmllistmodelworkerscript::worker_sync_data()
629{
630 QTest::addColumn<bool>(name: "dynamicRoles");
631
632 QTest::newRow(dataTag: "staticRoles") << false;
633 QTest::newRow(dataTag: "dynamicRoles") << true;
634}
635
636void tst_qqmllistmodelworkerscript::worker_sync()
637{
638 QFETCH(bool, dynamicRoles);
639
640 QQmlListModel model;
641 model.setDynamicRoles(dynamicRoles);
642 QQmlEngine eng;
643 QQmlComponent component(&eng, testFileUrl(fileName: "workersync.qml"));
644 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
645 QVERIFY(item != nullptr);
646
647 QCOMPARE(model.count(), 0);
648
649 QVERIFY(QMetaObject::invokeMethod(item, "addItem0"));
650
651 QCOMPARE(model.count(), 2);
652 QVariant childData = model.data(index: 0, role: 0);
653 QQmlListModel *childModel = qobject_cast<QQmlListModel *>(object: childData.value<QObject *>());
654 QVERIFY(childModel);
655 QCOMPARE(childModel->count(), 1);
656
657 QSignalSpy spyModelInserted(&model, SIGNAL(rowsInserted(QModelIndex,int,int)));
658 QSignalSpy spyChildInserted(childModel, SIGNAL(rowsInserted(QModelIndex,int,int)));
659
660 QVERIFY(QMetaObject::invokeMethod(item, "addItemViaWorker"));
661 waitForWorker(item);
662
663 QCOMPARE(model.count(), 2);
664 QCOMPARE(childModel->count(), 1);
665 QCOMPARE(spyModelInserted.count(), 0);
666 QCOMPARE(spyChildInserted.count(), 0);
667
668 QVERIFY(QMetaObject::invokeMethod(item, "doSync"));
669 waitForWorker(item);
670
671 QCOMPARE(model.count(), 2);
672 QCOMPARE(childModel->count(), 2);
673 QCOMPARE(spyModelInserted.count(), 0);
674 QCOMPARE(spyChildInserted.count(), 1);
675
676 QVERIFY(QMetaObject::invokeMethod(item, "addItemViaWorker"));
677 waitForWorker(item);
678
679 QCOMPARE(model.count(), 2);
680 QCOMPARE(childModel->count(), 2);
681 QCOMPARE(spyModelInserted.count(), 0);
682 QCOMPARE(spyChildInserted.count(), 1);
683
684 QVERIFY(QMetaObject::invokeMethod(item, "doSync"));
685 waitForWorker(item);
686
687 QCOMPARE(model.count(), 2);
688 QCOMPARE(childModel->count(), 3);
689 QCOMPARE(spyModelInserted.count(), 0);
690 QCOMPARE(spyChildInserted.count(), 2);
691
692 delete item;
693 qApp->processEvents();
694}
695
696void tst_qqmllistmodelworkerscript::worker_remove_element_data()
697{
698 worker_sync_data();
699}
700
701void tst_qqmllistmodelworkerscript::worker_remove_element()
702{
703 QFETCH(bool, dynamicRoles);
704
705 QQmlListModel model;
706 model.setDynamicRoles(dynamicRoles);
707 QQmlEngine eng;
708 QQmlComponent component(&eng, testFileUrl(fileName: "workerremoveelement.qml"));
709 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
710 QVERIFY(item != nullptr);
711
712 QSignalSpy spyModelRemoved(&model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
713
714 QCOMPARE(model.count(), 0);
715 QCOMPARE(spyModelRemoved.count(), 0);
716
717 QVERIFY(QMetaObject::invokeMethod(item, "addItem"));
718
719 QCOMPARE(model.count(), 1);
720
721 QVERIFY(QMetaObject::invokeMethod(item, "removeItemViaWorker"));
722 waitForWorker(item);
723
724 QCOMPARE(model.count(), 1);
725 QCOMPARE(spyModelRemoved.count(), 0);
726
727 QVERIFY(QMetaObject::invokeMethod(item, "doSync"));
728 waitForWorker(item);
729
730 QCOMPARE(model.count(), 0);
731 QCOMPARE(spyModelRemoved.count(), 1);
732
733 delete item;
734 qApp->processEvents();
735
736 {
737 //don't crash if model was deleted earlier
738 QQmlListModel* model = new QQmlListModel;
739 model->setDynamicRoles(dynamicRoles);
740 QQmlEngine eng;
741 QQmlComponent component(&eng, testFileUrl(fileName: "workerremoveelement.qml"));
742 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model);
743 QVERIFY(item != nullptr);
744
745 QVERIFY(QMetaObject::invokeMethod(item, "addItem"));
746
747 QCOMPARE(model->count(), 1);
748
749 QVERIFY(QMetaObject::invokeMethod(item, "removeItemViaWorker"));
750 QVERIFY(QMetaObject::invokeMethod(item, "doSync"));
751 delete model;
752 qApp->processEvents(); //must not crash here
753 waitForWorker(item);
754
755 delete item;
756 }
757}
758
759void tst_qqmllistmodelworkerscript::worker_remove_list_data()
760{
761 worker_sync_data();
762}
763
764void tst_qqmllistmodelworkerscript::worker_remove_list()
765{
766 QFETCH(bool, dynamicRoles);
767
768 QQmlListModel model;
769 model.setDynamicRoles(dynamicRoles);
770 QQmlEngine eng;
771 QQmlComponent component(&eng, testFileUrl(fileName: "workerremovelist.qml"));
772 QQuickItem *item = createWorkerTest(eng: &eng, component: &component, model: &model);
773 QVERIFY(item != nullptr);
774
775 QSignalSpy spyModelRemoved(&model, SIGNAL(rowsRemoved(QModelIndex,int,int)));
776
777 QCOMPARE(model.count(), 0);
778 QCOMPARE(spyModelRemoved.count(), 0);
779
780 QVERIFY(QMetaObject::invokeMethod(item, "addList"));
781
782 QCOMPARE(model.count(), 1);
783
784 QVERIFY(QMetaObject::invokeMethod(item, "removeListViaWorker"));
785 waitForWorker(item);
786
787 QCOMPARE(model.count(), 1);
788 QCOMPARE(spyModelRemoved.count(), 0);
789
790 QVERIFY(QMetaObject::invokeMethod(item, "doSync"));
791 waitForWorker(item);
792
793 QCOMPARE(model.count(), 0);
794 QCOMPARE(spyModelRemoved.count(), 1);
795
796 delete item;
797 qApp->processEvents();
798}
799
800void tst_qqmllistmodelworkerscript::dynamic_role_data()
801{
802 QTest::addColumn<QString>(name: "preamble");
803 QTest::addColumn<QString>(name: "script");
804 QTest::addColumn<int>(name: "result");
805
806 QTest::newRow(dataTag: "sync1") << "{append({'a':[{'b':1},{'b':2}]})}" << "{get(0).a = 'string';count}" << 1;
807}
808
809void tst_qqmllistmodelworkerscript::dynamic_role()
810{
811 QFETCH(QString, preamble);
812 QFETCH(QString, script);
813 QFETCH(int, result);
814
815 QQmlListModel model;
816 model.setDynamicRoles(true);
817 QQmlEngine engine;
818 QQmlComponent component(&engine, testFileUrl(fileName: "model.qml"));
819 QQuickItem *item = createWorkerTest(eng: &engine, component: &component, model: &model);
820 QVERIFY(item != nullptr);
821
822 QQmlExpression preExp(engine.rootContext(), &model, preamble);
823 QCOMPARE(preExp.evaluate().toInt(), 0);
824
825 if (script[0] == QLatin1Char('{') && script[script.length()-1] == QLatin1Char('}'))
826 script = script.mid(position: 1, n: script.length() - 2);
827 QVariantList operations;
828 foreach (const QString &s, script.split(';')) {
829 if (!s.isEmpty())
830 operations << s;
831 }
832
833 // execute a set of commands on the worker list model, then check the
834 // changes are reflected in the list model in the main thread
835 QVERIFY(QMetaObject::invokeMethod(item, "evalExpressionViaWorker",
836 Q_ARG(QVariant, operations.mid(0, operations.length()-1))));
837 waitForWorker(item);
838
839 QQmlExpression e(engine.rootContext(), &model, operations.last().toString());
840 QCOMPARE(e.evaluate().toInt(), result);
841
842 delete item;
843 qApp->processEvents();
844}
845
846void tst_qqmllistmodelworkerscript::correctMoves()
847{
848 QQmlEngine engine;
849 QQmlComponent component(&engine, testFileUrl(fileName: "listmodel_async_sort/main.qml"));
850 QScopedPointer<QObject> root {component.create()};
851 QVERIFY2(root, qPrintable(component.errorString()));
852 bool ok =QMetaObject::invokeMethod(obj: root.get(), member: "doSort");
853 QVERIFY(ok);
854 auto check = [&](){
855 bool success = false;
856 QMetaObject::invokeMethod(obj: root.get(), member: "verify", Q_RETURN_ARG(bool, success));
857 return success;
858 };
859 QTRY_VERIFY(check());
860}
861
862QTEST_MAIN(tst_qqmllistmodelworkerscript)
863
864#include "tst_qqmllistmodelworkerscript.moc"
865

source code of qtdeclarative/tests/auto/qml/qqmllistmodelworkerscript/tst_qqmllistmodelworkerscript.cpp