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

source code of qtdeclarative/src/qmlmodels/qqmltableinstancemodel.cpp