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 Q_ASSERT(m_qmlContext && m_qmlContext->isValid());
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(key: modelItem->index);
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 *>(v: object->property(name: kModelItemTag));
167 Q_ASSERT(modelItem);
168 // Ensure that the object was incubated by this QQmlTableInstanceModel
169 Q_ASSERT(m_modelItems.contains(modelItem->index));
170 Q_ASSERT(m_modelItems[modelItem->index]->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(key: modelItem->index);
187
188 if (reusable == Reusable) {
189 m_reusableItemsPool.insertItem(modelItem);
190 emit itemPooled(index: modelItem->index, object: modelItem->object);
191 return QQmlInstanceModel::Pooled;
192 }
193
194 // The item is not reused or referenced by anyone, so just delete it
195 destroyModelItem(modelItem, mode: Deferred);
196 return QQmlInstanceModel::Destroyed;
197}
198
199void QQmlTableInstanceModel::destroyModelItem(QQmlDelegateModelItem *modelItem, DestructionMode mode)
200{
201 emit destroyingItem(object: modelItem->object);
202 if (mode == Deferred)
203 modelItem->destroyObject();
204 else
205 delete modelItem->object;
206 delete modelItem;
207}
208
209void QQmlTableInstanceModel::dispose(QObject *object)
210{
211 Q_ASSERT(object);
212 auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(v: object->property(name: kModelItemTag));
213 Q_ASSERT(modelItem);
214
215 modelItem->releaseObject();
216
217 // The item is not referenced by anyone
218 Q_ASSERT(!modelItem->isObjectReferenced());
219 Q_ASSERT(!modelItem->isReferenced());
220 // Ensure that the object was incubated by this QQmlTableInstanceModel
221 Q_ASSERT(m_modelItems.contains(modelItem->index));
222 Q_ASSERT(m_modelItems[modelItem->index]->object == object);
223
224 m_modelItems.remove(key: modelItem->index);
225
226 emit destroyingItem(object);
227 delete object;
228 delete modelItem;
229}
230
231void QQmlTableInstanceModel::cancel(int index)
232{
233 auto modelItem = m_modelItems.value(key: index);
234 Q_ASSERT(modelItem);
235
236 // Since the view expects the item to be incubating, there should be
237 // an incubation task. And since the incubation is not done, no-one
238 // should yet have received, and therfore hold a reference to, the object.
239 Q_ASSERT(modelItem->incubationTask);
240 Q_ASSERT(!modelItem->isObjectReferenced());
241
242 m_modelItems.remove(key: index);
243
244 if (modelItem->object)
245 delete modelItem->object;
246
247 // modelItem->incubationTask will be deleted from the modelItems destructor
248 delete modelItem;
249}
250
251void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime)
252{
253 m_reusableItemsPool.drain(maxPoolTime, releaseItem: [this](QQmlDelegateModelItem *modelItem) {
254 destroyModelItem(modelItem, mode: Immediate);
255 });
256}
257
258void QQmlTableInstanceModel::reuseItem(QQmlDelegateModelItem *item, int newModelIndex)
259{
260 // Update the context properties index, row and column on
261 // the delegate item, and inform the application about it.
262 // Note that we set alwaysEmit to true, to force all bindings
263 // to be reevaluated, even if the index didn't change (since
264 // the model can have changed size since last usage).
265 const bool alwaysEmit = true;
266 const int newRow = m_adaptorModel.rowAt(index: newModelIndex);
267 const int newColumn = m_adaptorModel.columnAt(index: newModelIndex);
268 item->setModelIndex(idx: newModelIndex, newRow, newColumn, alwaysEmit);
269
270 // Notify the application that all 'dynamic'/role-based context data has
271 // changed as well (their getter function will use the updated index).
272 auto const itemAsList = QList<QQmlDelegateModelItem *>() << item;
273 auto const updateAllRoles = QVector<int>();
274 m_adaptorModel.notify(items: itemAsList, index: newModelIndex, count: 1, roles: updateAllRoles);
275
276 // Inform the view that the item is recycled. This will typically result
277 // in the view updating its own attached delegate item properties.
278 emit itemReused(index: newModelIndex, object: item->object);
279}
280
281void QQmlTableInstanceModel::incubateModelItem(QQmlDelegateModelItem *modelItem, QQmlIncubator::IncubationMode incubationMode)
282{
283 // Guard the model item temporarily so that it's not deleted from
284 // incubatorStatusChanged(), in case the incubation is done synchronously.
285 modelItem->scriptRef++;
286
287 if (modelItem->incubationTask) {
288 // We're already incubating the model item from a previous request. If the previous call requested
289 // the item async, but the current request needs it sync, we need to force-complete the incubation.
290 const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested);
291 if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous)
292 modelItem->incubationTask->forceCompletion();
293 } else {
294 modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode);
295
296 QQmlContext *creationContext = modelItem->delegate->creationContext();
297 const QQmlRefPointer<QQmlContextData> componentContext
298 = QQmlContextData::get(context: creationContext ? creationContext : m_qmlContext.data());
299
300 QQmlComponentPrivate *cp = QQmlComponentPrivate::get(c: modelItem->delegate);
301 if (cp->isBound()) {
302 modelItem->contextData = componentContext;
303 cp->incubateObject(
304 incubationTask: modelItem->incubationTask,
305 component: modelItem->delegate,
306 engine: m_qmlContext->engine(),
307 context: componentContext,
308 forContext: QQmlContextData::get(context: m_qmlContext));
309 } else {
310 QQmlRefPointer<QQmlContextData> ctxt = QQmlContextData::createRefCounted(
311 parent: QQmlContextData::get(context: creationContext ? creationContext : m_qmlContext.data()));
312 ctxt->setContextObject(modelItem);
313 modelItem->contextData = ctxt;
314
315 cp->incubateObject(
316 incubationTask: modelItem->incubationTask,
317 component: modelItem->delegate,
318 engine: m_qmlContext->engine(),
319 context: ctxt,
320 forContext: QQmlContextData::get(context: m_qmlContext));
321 }
322 }
323
324 // Remove the temporary guard
325 modelItem->scriptRef--;
326}
327
328void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status)
329{
330 QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate;
331 Q_ASSERT(modelItem->incubationTask);
332
333 modelItem->incubationTask = nullptr;
334 incubationTask->modelItemToIncubate = nullptr;
335
336 if (status == QQmlIncubator::Ready) {
337 // Tag the incubated object with the model item for easy retrieval upon release etc.
338 modelItem->object->setProperty(name: kModelItemTag, value: QVariant::fromValue(value: modelItem));
339
340 // Emit that the item has been created. What normally happens next is that the view
341 // upon receiving the signal asks for the model item once more. And since the item is
342 // now in the map, it will be returned directly.
343 Q_ASSERT(modelItem->object);
344 modelItem->scriptRef++;
345 emit createdItem(index: modelItem->index, object: modelItem->object);
346 modelItem->scriptRef--;
347 } else if (status == QQmlIncubator::Error) {
348 qWarning() << "Error incubating delegate:" << incubationTask->errors();
349 }
350
351 if (!modelItem->isReferenced() && !modelItem->isObjectReferenced()) {
352 // We have no internal reference to the model item, and the view has no
353 // reference to the incubated object. So just delete the model item.
354 // Note that being here means that the object was incubated _async_
355 // (otherwise modelItem->isReferenced() would be true).
356 m_modelItems.remove(key: modelItem->index);
357
358 if (modelItem->object) {
359 modelItem->scriptRef++;
360 emit destroyingItem(object: modelItem->object);
361 modelItem->scriptRef--;
362 Q_ASSERT(!modelItem->isReferenced());
363 }
364
365 deleteModelItemLater(modelItem);
366 }
367
368 deleteIncubationTaskLater(incubationTask);
369}
370
371QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) {
372 const auto modelItem = m_modelItems.value(key: index, defaultValue: nullptr);
373 if (!modelItem)
374 return QQmlIncubator::Null;
375
376 if (modelItem->incubationTask)
377 return modelItem->incubationTask->status();
378
379 // Since we clear the incubation task when we're done
380 // incubating, it means that the status is Ready.
381 return QQmlIncubator::Ready;
382}
383
384bool QQmlTableInstanceModel::setRequiredProperty(int index, const QString &name, const QVariant &value)
385{
386 // This function can be called from the view upon
387 // receiving the initItem signal. It can be used to
388 // give all required delegate properties used by the
389 // view an initial value.
390 const auto modelItem = m_modelItems.value(key: index, defaultValue: nullptr);
391 if (!modelItem)
392 return false;
393 if (!modelItem->object)
394 return false;
395 if (!modelItem->incubationTask)
396 return false;
397
398 bool wasInRequired = false;
399 const auto task = QQmlIncubatorPrivate::get(incubator: modelItem->incubationTask);
400 RequiredProperties *props = task->requiredProperties();
401 if (props->empty())
402 return false;
403
404 QQmlProperty componentProp = QQmlComponentPrivate::removePropertyFromRequired(
405 createdComponent: modelItem->object, name, requiredProperties: props, engine: QQmlEnginePrivate::get(p: task->enginePriv),
406 wasInRequiredProperties: &wasInRequired);
407 if (wasInRequired)
408 componentProp.write(value);
409 return wasInRequired;
410}
411
412void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask)
413{
414 // We often need to post-delete incubation tasks, since we cannot
415 // delete them while we're in the middle of an incubation change callback.
416 Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask));
417 m_finishedIncubationTasks.append(t: incubationTask);
418 if (m_finishedIncubationTasks.size() == 1)
419 QTimer::singleShot(interval: 1, receiver: this, slot: &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks);
420}
421
422void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks()
423{
424 qDeleteAll(c: m_finishedIncubationTasks);
425 m_finishedIncubationTasks.clear();
426}
427
428QVariant QQmlTableInstanceModel::model() const
429{
430 return m_adaptorModel.model();
431}
432
433void QQmlTableInstanceModel::setModel(const QVariant &model)
434{
435 // Pooled items are still accessible/alive for the application, and
436 // needs to stay in sync with the model. So we need to drain the pool
437 // completely when the model changes.
438 drainReusableItemsPool(maxPoolTime: 0);
439 if (auto const aim = abstractItemModel()) {
440 disconnect(sender: aim, signal: &QAbstractItemModel::dataChanged, receiver: this, slot: &QQmlTableInstanceModel::dataChangedCallback);
441 disconnect(sender: aim, signal: &QAbstractItemModel::modelAboutToBeReset, receiver: this, slot: &QQmlTableInstanceModel::modelAboutToBeResetCallback);
442 }
443 m_adaptorModel.setModel(model);
444 if (auto const aim = abstractItemModel()) {
445 connect(sender: aim, signal: &QAbstractItemModel::dataChanged, context: this, slot: &QQmlTableInstanceModel::dataChangedCallback);
446 connect(sender: aim, signal: &QAbstractItemModel::modelAboutToBeReset, context: this, slot: &QQmlTableInstanceModel::modelAboutToBeResetCallback);
447 }
448}
449
450void QQmlTableInstanceModel::dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles)
451{
452 // This function is called when model data has changed. In that case, we tell the adaptor model
453 // to go through all the items we have created, find the ones that are affected, and notify that
454 // their model data has changed. This will in turn update QML bindings inside the delegate items.
455 int numberOfRowsChanged = end.row() - begin.row() + 1;
456 int numberOfColumnsChanged = end.column() - begin.column() + 1;
457
458 for (int column = 0; column < numberOfColumnsChanged; ++column) {
459 const int columnIndex = begin.column() + column;
460 const int rowIndex = begin.row() + (columnIndex * rows());
461 m_adaptorModel.notify(items: m_modelItems.values(), index: rowIndex, count: numberOfRowsChanged, roles);
462 }
463}
464
465void QQmlTableInstanceModel::modelAboutToBeResetCallback()
466{
467 // When the model is reset, we can no longer rely on any of the data it has
468 // provided us so far. Normally it's enough for the view to recreate all the
469 // delegate items in that case, except if the model roles has changed as well
470 // (since those are cached by QQmlAdaptorModel / Accessors). For the latter case, we
471 // simply set the model once more in the delegate model to rebuild everything.
472 auto const aim = abstractItemModel();
473 auto oldRoleNames = aim->roleNames();
474 QObject::connect(sender: aim, signal: &QAbstractItemModel::modelReset, context: this, slot: [this, aim, oldRoleNames](){
475 if (oldRoleNames != aim->roleNames())
476 setModel(model());
477 }, type: Qt::SingleShotConnection);
478}
479
480QQmlComponent *QQmlTableInstanceModel::delegate() const
481{
482 return m_delegate;
483}
484
485void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate)
486{
487 if (m_delegate == delegate)
488 return;
489
490 m_delegateChooser = nullptr;
491 if (delegate) {
492 QQmlAbstractDelegateComponent *adc =
493 qobject_cast<QQmlAbstractDelegateComponent *>(object: delegate);
494 if (adc)
495 m_delegateChooser = adc;
496 }
497
498 m_delegate = delegate;
499}
500
501const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const
502{
503 return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr;
504}
505
506// --------------------------------------------------------
507
508void QQmlTableInstanceModelIncubationTask::setInitialState(QObject *object)
509{
510 initializeRequiredProperties(modelItemToIncubate, object);
511 modelItemToIncubate->object = object;
512 emit tableInstanceModel->initItem(index: modelItemToIncubate->index, object);
513
514 if (!QQmlIncubatorPrivate::get(incubator: this)->requiredProperties()->empty()) {
515 modelItemToIncubate->object = nullptr;
516 object->deleteLater();
517 }
518}
519
520void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status)
521{
522 if (!QQmlTableInstanceModel::isDoneIncubating(modelItem: modelItemToIncubate))
523 return;
524
525 // We require the view to cancel any ongoing load
526 // requests before the tableInstanceModel is destructed.
527 Q_ASSERT(tableInstanceModel);
528
529 tableInstanceModel->incubatorStatusChanged(incubationTask: this, status);
530}
531
532QT_END_NAMESPACE
533
534#include "moc_qqmltableinstancemodel_p.cpp"
535
536

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