Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmltableinstancemodel.cpp
Go to the documentation of this file.
1// Copyright (C) 2018 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:significant
4
7
8#include <QtCore/QTimer>
9
10#include <QtQml/private/qqmlincubator_p.h>
11#include <QtQmlModels/private/qqmlchangeset_p.h>
12#include <QtQml/private/qqmlcomponent_p.h>
13
15
16const char* kModelItemTag = "_tableinstancemodel_modelItem";
17
18bool QQmlTableInstanceModel::isDoneIncubating(QQmlDelegateModelItem *modelItem)
19{
20 if (!modelItem->incubationTask)
21 return true;
22
23 const auto status = modelItem->incubationTask->status();
24 return (status == QQmlIncubator::Ready) || (status == QQmlIncubator::Error);
25}
26
27void QQmlTableInstanceModel::deleteModelItemLater(QQmlDelegateModelItem *modelItem)
28{
29 Q_ASSERT(modelItem);
30
31 delete modelItem->object;
32 modelItem->object = nullptr;
33 modelItem->contextData.reset();
34 modelItem->deleteLater();
35}
36
37QQmlTableInstanceModel::QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent)
38 : QQmlInstanceModel(*(new QObjectPrivate()), parent)
39 , m_qmlContext(qmlContext)
40 , m_metaType(QQml::makeRefPointer<QQmlDelegateModelItemMetaType>(
41 m_qmlContext->engine()->handle(), this))
42{
43}
44
45void QQmlTableInstanceModel::useImportVersion(QTypeRevision version)
46{
47 m_adaptorModel.useImportVersion(version);
48}
49
50QQmlTableInstanceModel::~QQmlTableInstanceModel()
51{
52 for (const auto modelItem : m_modelItems) {
53 // No item in m_modelItems should be referenced at this point. The view
54 // should release all its items before it deletes this model. Only model items
55 // that are still being incubated should be left for us to delete.
56 Q_ASSERT(modelItem->objectRef == 0);
57 Q_ASSERT(modelItem->incubationTask);
58 // Check that we are not being deleted while we're
59 // in the process of e.g emitting a created signal.
60 Q_ASSERT(modelItem->scriptRef == 0);
61
62 if (modelItem->object) {
63 delete modelItem->object;
64 modelItem->object = nullptr;
65 modelItem->contextData.reset();
66 }
67 }
68
69 deleteAllFinishedIncubationTasks();
70 qDeleteAll(m_modelItems);
71 drainReusableItemsPool(0);
72}
73
74QQmlComponent *QQmlTableInstanceModel::resolveDelegate(int index)
75{
76 if (m_delegateChooser) {
77 const int row = m_adaptorModel.rowAt(index);
78 const int column = m_adaptorModel.columnAt(index);
79 QQmlComponent *delegate = nullptr;
80 QQmlAbstractDelegateComponent *chooser = m_delegateChooser;
81 do {
82 delegate = chooser->delegate(&m_adaptorModel, row, column);
83 chooser = qobject_cast<QQmlAbstractDelegateComponent *>(delegate);
84 } while (chooser);
85 return delegate;
86 }
87
88 return m_delegate;
89}
90
91QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int index)
92{
93 // Check if an item for the given index is already loaded and ready
94 QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr);
95 if (modelItem)
96 return modelItem;
97
98 QQmlComponent *delegate = resolveDelegate(index);
99 if (!delegate)
100 return nullptr;
101
102 // Check if the pool contains an item that can be reused
103 modelItem = m_reusableItemsPool.takeItem(delegate, index);
104 if (modelItem) {
105 reuseItem(modelItem, index);
106 m_modelItems.insert(index, modelItem);
107 return modelItem;
108 }
109
110 // Create a new item from scratch
111 modelItem = m_adaptorModel.createItem(m_metaType.data(), index);
112 if (modelItem) {
113 modelItem->delegate = delegate;
114 m_modelItems.insert(index, modelItem);
115 return modelItem;
116 }
117
118 qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << index;
119 return nullptr;
120}
121
122QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode)
123{
124 Q_ASSERT(m_delegate);
125 Q_ASSERT(index >= 0 && index < m_adaptorModel.count());
126
127 QQmlDelegateModelItem *modelItem = resolveModelItem(index);
128 if (!modelItem)
129 return nullptr;
130
131 if (modelItem->object) {
132 // The model item has already been incubated. So
133 // just bump the ref-count and return it.
134 modelItem->referenceObject();
135 return modelItem->object;
136 }
137
138 // The object is not ready, and needs to be incubated
139 incubateModelItem(modelItem, incubationMode);
140 if (!isDoneIncubating(modelItem))
141 return nullptr;
142
143 // Incubation is done, so the task should be removed
144 Q_ASSERT(!modelItem->incubationTask);
145
146 if (!modelItem->object) {
147 // The object was incubated synchronously (otherwise we would return above). But since
148 // we have no object, the incubation must have failed. And when we have no object, there
149 // should be no object references either. And there should also not be any internal script
150 // refs at this point. So we delete the model item.
151 Q_ASSERT(!modelItem->isObjectReferenced());
152 Q_ASSERT(!modelItem->isReferenced());
153 m_modelItems.remove(modelItem->modelIndex());
154 delete modelItem;
155 return nullptr;
156 }
157
158 // Incubation was completed sync and successful
159 modelItem->referenceObject();
160 return modelItem->object;
161}
162
163QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object, ReusableFlag reusable)
164{
165 Q_ASSERT(object);
166 auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(kModelItemTag));
167 Q_ASSERT(modelItem);
168 // Ensure that the object was incubated by this QQmlTableInstanceModel
169 Q_ASSERT(m_modelItems.contains(modelItem->modelIndex()));
170 Q_ASSERT(m_modelItems[modelItem->modelIndex()]->object == object);
171
172 if (!modelItem->releaseObject())
173 return QQmlDelegateModel::Referenced;
174
175 if (modelItem->isReferenced()) {
176 // We still have an internal reference to this object, which means that we are told to release an
177 // object while the createdItem signal for it is still on the stack. This can happen when objects
178 // are e.g delivered async, and the user flicks back and forth quicker than the loading can catch
179 // up with. The view might then find that the object is no longer visible and should be released.
180 // We detect this case in incubatorStatusChanged(), and delete it there instead. But from the callers
181 // point of view, it should consider it destroyed.
182 return QQmlDelegateModel::Destroyed;
183 }
184
185 // The item is not referenced by anyone
186 m_modelItems.remove(modelItem->modelIndex());
187
188 if (reusable == Reusable && m_reusableItemsPool.insertItem(modelItem)) {
189 emit itemPooled(modelItem->modelIndex(), modelItem->object);
190 return QQmlInstanceModel::Pooled;
191 }
192
193 // The item is not reused or referenced by anyone, so just delete it
194 destroyModelItem(modelItem, Deferred);
195 return QQmlInstanceModel::Destroyed;
196}
197
198void QQmlTableInstanceModel::destroyModelItem(QQmlDelegateModelItem *modelItem, DestructionMode mode)
199{
200 emit destroyingItem(modelItem->object);
201 if (mode == Deferred)
202 modelItem->destroyObject();
203 else
204 delete modelItem->object;
205 delete modelItem;
206}
207
208void QQmlTableInstanceModel::dispose(QObject *object)
209{
210 Q_ASSERT(object);
211 auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(kModelItemTag));
212 Q_ASSERT(modelItem);
213
214 modelItem->releaseObject();
215
216 // The item is not referenced by anyone
217 Q_ASSERT(!modelItem->isObjectReferenced());
218 Q_ASSERT(!modelItem->isReferenced());
219 // Ensure that the object was incubated by this QQmlTableInstanceModel
220 Q_ASSERT(m_modelItems.contains(modelItem->modelIndex()));
221 Q_ASSERT(m_modelItems[modelItem->modelIndex()]->object == object);
222
223 m_modelItems.remove(modelItem->modelIndex());
224
225 emit destroyingItem(object);
226 delete object;
227 delete modelItem;
228}
229
230void QQmlTableInstanceModel::cancel(int index)
231{
232 auto modelItem = m_modelItems.value(index);
233 Q_ASSERT(modelItem);
234
235 // Since the view expects the item to be incubating, there should be
236 // an incubation task. And since the incubation is not done, no-one
237 // should yet have received, and therfore hold a reference to, the object.
238 Q_ASSERT(modelItem->incubationTask);
239 Q_ASSERT(!modelItem->isObjectReferenced());
240
241 m_modelItems.remove(index);
242
243 if (modelItem->object)
244 delete modelItem->object;
245
246 // modelItem->incubationTask will be deleted from the modelItems destructor
247 delete modelItem;
248}
249
250void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime)
251{
252 m_reusableItemsPool.drain(maxPoolTime, [this](QQmlDelegateModelItem *modelItem) {
253 destroyModelItem(modelItem, Immediate);
254 });
255}
256
257void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModelIndex)
258{
259 // Update the context properties index, row and column on
260 // the delegate item, and inform the application about it.
261 // Note that we set alwaysEmit to true, to force all bindings
262 // to be reevaluated, even if the index didn't change (since
263 // the model can have changed size since last usage).
264 const bool alwaysEmit = true;
265 const int newRow = m_adaptorModel.rowAt(newModelIndex);
266 const int newColumn = m_adaptorModel.columnAt(newModelIndex);
267 item->setModelIndex(newModelIndex, newRow, newColumn, alwaysEmit);
268
269 // Notify the application that all 'dynamic'/role-based context data has
270 // changed as well (their getter function will use the updated index).
271 auto const itemAsList = QList<QQmlDelegateModelItem *>() << item;
272 auto const updateAllRoles = QVector<int>();
273 m_adaptorModel.notify(itemAsList, newModelIndex, 1, updateAllRoles);
274
275 // Inform the view that the item is recycled. This will typically result
276 // in the view updating its own attached delegate item properties.
277 emit itemReused(newModelIndex, item->object);
278}
279
280void QQmlTableInstanceModel::incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode)
281{
282 // Guard the model item temporarily so that it's not deleted from
283 // incubatorStatusChanged(), in case the incubation is done synchronously.
284 modelItem->scriptRef++;
285
286 if (modelItem->incubationTask) {
287 // We're already incubating the model item from a previous request. If the previous call requested
288 // the item async, but the current request needs it sync, we need to force-complete the incubation.
289 const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested);
290 if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous)
291 modelItem->incubationTask->forceCompletion();
292 } else if (m_qmlContext && m_qmlContext->isValid()) {
293 modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode);
294 // TODO: In order to retain compatibility, we cannot allow the incubation task to clear the
295 // context object in the presence of required properties. This results in the context
296 // properties still being available in the delegate even though they shouldn't.
297 // modelItem->incubationTask->incubating = modelItem;
298
299 QQmlContext *creationContext = modelItem->delegate->creationContext();
300 const QQmlRefPointer<QQmlContextData> componentContext
301 = QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data());
302
303 QQmlComponentPrivate *cp = QQmlComponentPrivate::get(modelItem->delegate);
304 if (cp->isBound()) {
305 modelItem->contextData = componentContext;
306
307 // Ignore return value of initProxy. We want to know the proxy when assigning required
308 // properties, but we don't want it to pollute our context. The context is bound.
309 if (m_adaptorModel.hasProxyObject())
310 modelItem->initProxy();
311
312 cp->incubateObject(
313 modelItem->incubationTask,
314 modelItem->delegate,
315 m_qmlContext->engine(),
316 componentContext,
317 QQmlContextData::get(m_qmlContext));
318 } else {
319 QQmlRefPointer<QQmlContextData> ctxt = QQmlContextData::createRefCounted(
320 QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data()));
321 ctxt->setContextObject(modelItem);
322 modelItem->contextData = ctxt;
323
324 // If the model is read-only we cannot just expose the object as context
325 // We actually need a separate model object to moderate access.
326 if (m_adaptorModel.hasProxyObject()) {
327 if (m_adaptorModel.delegateModelAccess == QQmlDelegateModel::ReadOnly)
328 modelItem->initProxy();
329 else
330 ctxt = modelItem->initProxy();
331 }
332
333 cp->incubateObject(
334 modelItem->incubationTask,
335 modelItem->delegate,
336 m_qmlContext->engine(),
337 ctxt,
338 QQmlContextData::get(m_qmlContext));
339 }
340 }
341
342 // Remove the temporary guard
343 modelItem->scriptRef--;
344}
345
346void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status)
347{
348 QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate;
349 Q_ASSERT(modelItem->incubationTask);
350
351 modelItem->incubationTask = nullptr;
352 incubationTask->modelItemToIncubate = nullptr;
353
354 if (status == QQmlIncubator::Ready) {
355 // Tag the incubated object with the model item for easy retrieval upon release etc.
356 modelItem->object->setProperty(kModelItemTag, QVariant::fromValue(modelItem));
357
358 // Emit that the item has been created. What normally happens next is that the view
359 // upon receiving the signal asks for the model item once more. And since the item is
360 // now in the map, it will be returned directly.
361 Q_ASSERT(modelItem->object);
362 modelItem->scriptRef++;
363 emit createdItem(modelItem->modelIndex(), modelItem->object);
364 modelItem->scriptRef--;
365 } else if (status == QQmlIncubator::Error) {
366 qWarning() << "Error incubating delegate:" << incubationTask->errors();
367 }
368
369 if (!modelItem->isReferenced() && !modelItem->isObjectReferenced()) {
370 // We have no internal reference to the model item, and the view has no
371 // reference to the incubated object. So just delete the model item.
372 // Note that being here means that the object was incubated _async_
373 // (otherwise modelItem->isReferenced() would be true).
374 m_modelItems.remove(modelItem->modelIndex());
375
376 if (modelItem->object) {
377 modelItem->scriptRef++;
378 emit destroyingItem(modelItem->object);
379 modelItem->scriptRef--;
380 Q_ASSERT(!modelItem->isReferenced());
381 }
382
383 deleteModelItemLater(modelItem);
384 }
385
386 deleteIncubationTaskLater(incubationTask);
387}
388
389QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) {
390 const auto modelItem = m_modelItems.value(index, nullptr);
391 if (!modelItem)
392 return QQmlIncubator::Null;
393
394 if (modelItem->incubationTask)
395 return modelItem->incubationTask->status();
396
397 // Since we clear the incubation task when we're done
398 // incubating, it means that the status is Ready.
399 return QQmlIncubator::Ready;
400}
401
402bool QQmlTableInstanceModel::setRequiredProperty(int index, const QString &name, const QVariant &value)
403{
404 // This function can be called from the view upon
405 // receiving the initItem signal. It can be used to
406 // give all required delegate properties used by the
407 // view an initial value.
408 const auto modelItem = m_modelItems.value(index, nullptr);
409 if (!modelItem)
410 return false;
411 if (!modelItem->object)
412 return false;
413 if (!modelItem->incubationTask)
414 return false;
415
416 bool wasInRequired = false;
417 const auto task = QQmlIncubatorPrivate::get(modelItem->incubationTask);
418 RequiredProperties *props = task->requiredProperties();
419 if (props->empty())
420 return false;
421
422 QQmlProperty componentProp = QQmlComponentPrivate::removePropertyFromRequired(
423 modelItem->object, name, props, QQmlEnginePrivate::get(task->enginePriv),
424 &wasInRequired);
425 if (wasInRequired)
426 componentProp.write(value);
427 return wasInRequired;
428}
429
430QQmlDelegateModelItem *QQmlTableInstanceModel::getModelItem(int index)
431{
432 return m_modelItems.value(index, nullptr);
433}
434
435void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask)
436{
437 // We often need to post-delete incubation tasks, since we cannot
438 // delete them while we're in the middle of an incubation change callback.
439 Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask));
440 m_finishedIncubationTasks.append(incubationTask);
441 if (m_finishedIncubationTasks.size() == 1)
442 QTimer::singleShot(1, this, &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks);
443}
444
445void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks()
446{
447 qDeleteAll(m_finishedIncubationTasks);
448 m_finishedIncubationTasks.clear();
449}
450
451QVariant QQmlTableInstanceModel::model() const
452{
453 return m_adaptorModel.model();
454}
455
456void QQmlTableInstanceModel::forceSetModel(const QVariant &model)
457{
458 // Pooled items are still accessible/alive for the application, and
459 // needs to stay in sync with the model. So we need to drain the pool
460 // completely when the model changes.
461 drainReusableItemsPool(0);
462 if (auto const aim = abstractItemModel()) {
463 disconnect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback);
464 disconnect(aim, &QAbstractItemModel::modelAboutToBeReset, this, &QQmlTableInstanceModel::modelAboutToBeResetCallback);
465 }
466 m_adaptorModel.setModel(model);
467 if (auto const aim = abstractItemModel()) {
468 connect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback);
469 connect(aim, &QAbstractItemModel::modelAboutToBeReset, this, &QQmlTableInstanceModel::modelAboutToBeResetCallback);
470 }
471}
472
473void QQmlTableInstanceModel::setModel(const QVariant &model)
474{
475 if (m_adaptorModel.model() == model)
476 return;
477
478 forceSetModel(model);
479
480 emit modelChanged();
481}
482
483void QQmlTableInstanceModel::dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles)
484{
485 // This function is called when model data has changed. In that case, we tell the adaptor model
486 // to go through all the items we have created, find the ones that are affected, and notify that
487 // their model data has changed. This will in turn update QML bindings inside the delegate items.
488 int numberOfRowsChanged = end.row() - begin.row() + 1;
489 int numberOfColumnsChanged = end.column() - begin.column() + 1;
490
491 for (int column = 0; column < numberOfColumnsChanged; ++column) {
492 const int columnIndex = begin.column() + column;
493 const int rowIndex = begin.row() + (columnIndex * rows());
494 m_adaptorModel.notify(m_modelItems.values(), rowIndex, numberOfRowsChanged, roles);
495 }
496}
497
498void QQmlTableInstanceModel::modelAboutToBeResetCallback()
499{
500 // When the model is reset, we can no longer rely on any of the data it has
501 // provided us so far. Normally it's enough for the view to recreate all the
502 // delegate items in that case, except if the model roles has changed as well
503 // (since those are cached by QQmlAdaptorModel / Accessors). For the latter case, we
504 // simply set the model once more in the delegate model to rebuild everything.
505 auto const aim = abstractItemModel();
506 auto oldRoleNames = aim->roleNames();
507 QObject::connect(aim, &QAbstractItemModel::modelReset, this, [this, aim, oldRoleNames](){
508 if (oldRoleNames != aim->roleNames()) {
509 // We refresh the model, but without sending any signals. The actual model object
510 // stays the same after all.
511 forceSetModel(model());
512 }
513 }, Qt::SingleShotConnection);
514}
515
516QQmlComponent *QQmlTableInstanceModel::delegate() const
517{
518 return m_delegate;
519}
520
521void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate)
522{
523 if (m_delegate == delegate)
524 return;
525
526 m_delegateChooser = nullptr;
527 if (delegate) {
528 QQmlAbstractDelegateComponent *adc =
529 qobject_cast<QQmlAbstractDelegateComponent *>(delegate);
530 if (adc)
531 m_delegateChooser = adc;
532 }
533
534 m_delegate = delegate;
535}
536
537const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const
538{
539 return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr;
540}
541
542// --------------------------------------------------------
543
545{
546 initializeRequiredProperties(
547 modelItemToIncubate, object, tableInstanceModel->delegateModelAccess());
548 modelItemToIncubate->object = object;
549 emit tableInstanceModel->initItem(modelItemToIncubate->modelIndex(), object);
550
551 if (!QQmlIncubatorPrivate::get(this)->requiredProperties()->empty()) {
552 modelItemToIncubate->object = nullptr;
553 object->deleteLater();
554 }
555}
556
557void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status)
558{
559 if (!QQmlTableInstanceModel::isDoneIncubating(modelItemToIncubate))
560 return;
561
562 // We require the view to cancel any ongoing load
563 // requests before the tableInstanceModel is destructed.
564 Q_ASSERT(tableInstanceModel);
565
566 tableInstanceModel->incubatorStatusChanged(this, status);
567}
568
569QT_END_NAMESPACE
570
571#include "moc_qqmltableinstancemodel_p.cpp"
void statusChanged(Status status) override
Called when the status of the incubator changes.
void setInitialState(QObject *object) override
Called after the object is first created, but before complex property bindings are evaluated and,...
QT_BEGIN_NAMESPACE const char * kModelItemTag