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->index);
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->index));
169 Q_ASSERT(m_modelItems[modelItem->index]->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->index);
186
187 if (reusable == Reusable) {
188 m_reusableItemsPool.insertItem(modelItem);
189 emit itemPooled(index: modelItem->index, object: 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, mode: Deferred);
195 return QQmlInstanceModel::Destroyed;
196}
197
198void QQmlTableInstanceModel::destroyModelItem(QQmlDelegateModelItem *modelItem, DestructionMode mode)
199{
200 emit destroyingItem(object: 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 *>(v: object->property(name: 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->index));
221 Q_ASSERT(m_modelItems[modelItem->index]->object == object);
222
223 m_modelItems.remove(key: modelItem->index);
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(key: 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(key: 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, releaseItem: [this](QQmlDelegateModelItem *modelItem) {
253 destroyModelItem(modelItem, mode: 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(index: newModelIndex);
266 const int newColumn = m_adaptorModel.columnAt(index: newModelIndex);
267 item->setModelIndex(idx: 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(items: itemAsList, index: newModelIndex, count: 1, roles: 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(index: newModelIndex, object: 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
295 QQmlContext *creationContext = modelItem->delegate->creationContext();
296 const QQmlRefPointer<QQmlContextData> componentContext
297 = QQmlContextData::get(context: creationContext ? creationContext : m_qmlContext.data());
298
299 QQmlComponentPrivate *cp = QQmlComponentPrivate::get(c: modelItem->delegate);
300 if (cp->isBound()) {
301 modelItem->contextData = componentContext;
302 cp->incubateObject(
303 incubationTask: modelItem->incubationTask,
304 component: modelItem->delegate,
305 engine: m_qmlContext->engine(),
306 context: componentContext,
307 forContext: QQmlContextData::get(context: m_qmlContext));
308 } else {
309 QQmlRefPointer<QQmlContextData> ctxt = QQmlContextData::createRefCounted(
310 parent: QQmlContextData::get(context: creationContext ? creationContext : m_qmlContext.data()));
311 ctxt->setContextObject(modelItem);
312 modelItem->contextData = ctxt;
313
314 cp->incubateObject(
315 incubationTask: modelItem->incubationTask,
316 component: modelItem->delegate,
317 engine: m_qmlContext->engine(),
318 context: ctxt,
319 forContext: QQmlContextData::get(context: m_qmlContext));
320 }
321 }
322
323 // Remove the temporary guard
324 modelItem->scriptRef--;
325}
326
327void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status)
328{
329 QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate;
330 Q_ASSERT(modelItem->incubationTask);
331
332 modelItem->incubationTask = nullptr;
333 incubationTask->modelItemToIncubate = nullptr;
334
335 if (status == QQmlIncubator::Ready) {
336 // Tag the incubated object with the model item for easy retrieval upon release etc.
337 modelItem->object->setProperty(name: kModelItemTag, value: QVariant::fromValue(value: modelItem));
338
339 // Emit that the item has been created. What normally happens next is that the view
340 // upon receiving the signal asks for the model item once more. And since the item is
341 // now in the map, it will be returned directly.
342 Q_ASSERT(modelItem->object);
343 modelItem->scriptRef++;
344 emit createdItem(index: modelItem->index, object: modelItem->object);
345 modelItem->scriptRef--;
346 } else if (status == QQmlIncubator::Error) {
347 qWarning() << "Error incubating delegate:" << incubationTask->errors();
348 }
349
350 if (!modelItem->isReferenced() && !modelItem->isObjectReferenced()) {
351 // We have no internal reference to the model item, and the view has no
352 // reference to the incubated object. So just delete the model item.
353 // Note that being here means that the object was incubated _async_
354 // (otherwise modelItem->isReferenced() would be true).
355 m_modelItems.remove(key: modelItem->index);
356
357 if (modelItem->object) {
358 modelItem->scriptRef++;
359 emit destroyingItem(object: modelItem->object);
360 modelItem->scriptRef--;
361 Q_ASSERT(!modelItem->isReferenced());
362 }
363
364 deleteModelItemLater(modelItem);
365 }
366
367 deleteIncubationTaskLater(incubationTask);
368}
369
370QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) {
371 const auto modelItem = m_modelItems.value(key: index, defaultValue: nullptr);
372 if (!modelItem)
373 return QQmlIncubator::Null;
374
375 if (modelItem->incubationTask)
376 return modelItem->incubationTask->status();
377
378 // Since we clear the incubation task when we're done
379 // incubating, it means that the status is Ready.
380 return QQmlIncubator::Ready;
381}
382
383bool QQmlTableInstanceModel::setRequiredProperty(int index, const QString &name, const QVariant &value)
384{
385 // This function can be called from the view upon
386 // receiving the initItem signal. It can be used to
387 // give all required delegate properties used by the
388 // view an initial value.
389 const auto modelItem = m_modelItems.value(key: index, defaultValue: nullptr);
390 if (!modelItem)
391 return false;
392 if (!modelItem->object)
393 return false;
394 if (!modelItem->incubationTask)
395 return false;
396
397 bool wasInRequired = false;
398 const auto task = QQmlIncubatorPrivate::get(incubator: modelItem->incubationTask);
399 RequiredProperties *props = task->requiredProperties();
400 if (props->empty())
401 return false;
402
403 QQmlProperty componentProp = QQmlComponentPrivate::removePropertyFromRequired(
404 createdComponent: modelItem->object, name, requiredProperties: props, engine: QQmlEnginePrivate::get(p: task->enginePriv),
405 wasInRequiredProperties: &wasInRequired);
406 if (wasInRequired)
407 componentProp.write(value);
408 return wasInRequired;
409}
410
411void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask)
412{
413 // We often need to post-delete incubation tasks, since we cannot
414 // delete them while we're in the middle of an incubation change callback.
415 Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask));
416 m_finishedIncubationTasks.append(t: incubationTask);
417 if (m_finishedIncubationTasks.size() == 1)
418 QTimer::singleShot(interval: 1, receiver: this, slot: &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks);
419}
420
421void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks()
422{
423 qDeleteAll(c: m_finishedIncubationTasks);
424 m_finishedIncubationTasks.clear();
425}
426
427QVariant QQmlTableInstanceModel::model() const
428{
429 return m_adaptorModel.model();
430}
431
432void QQmlTableInstanceModel::setModel(const QVariant &model)
433{
434 // Pooled items are still accessible/alive for the application, and
435 // needs to stay in sync with the model. So we need to drain the pool
436 // completely when the model changes.
437 drainReusableItemsPool(maxPoolTime: 0);
438 if (auto const aim = abstractItemModel()) {
439 disconnect(sender: aim, signal: &QAbstractItemModel::dataChanged, receiver: this, slot: &QQmlTableInstanceModel::dataChangedCallback);
440 disconnect(sender: aim, signal: &QAbstractItemModel::modelAboutToBeReset, receiver: this, slot: &QQmlTableInstanceModel::modelAboutToBeResetCallback);
441 }
442 m_adaptorModel.setModel(model);
443 if (auto const aim = abstractItemModel()) {
444 connect(sender: aim, signal: &QAbstractItemModel::dataChanged, context: this, slot: &QQmlTableInstanceModel::dataChangedCallback);
445 connect(sender: aim, signal: &QAbstractItemModel::modelAboutToBeReset, context: this, slot: &QQmlTableInstanceModel::modelAboutToBeResetCallback);
446 }
447}
448
449void QQmlTableInstanceModel::dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QVector<int> &roles)
450{
451 // This function is called when model data has changed. In that case, we tell the adaptor model
452 // to go through all the items we have created, find the ones that are affected, and notify that
453 // their model data has changed. This will in turn update QML bindings inside the delegate items.
454 int numberOfRowsChanged = end.row() - begin.row() + 1;
455 int numberOfColumnsChanged = end.column() - begin.column() + 1;
456
457 for (int column = 0; column < numberOfColumnsChanged; ++column) {
458 const int columnIndex = begin.column() + column;
459 const int rowIndex = begin.row() + (columnIndex * rows());
460 m_adaptorModel.notify(items: m_modelItems.values(), index: rowIndex, count: numberOfRowsChanged, roles);
461 }
462}
463
464void QQmlTableInstanceModel::modelAboutToBeResetCallback()
465{
466 // When the model is reset, we can no longer rely on any of the data it has
467 // provided us so far. Normally it's enough for the view to recreate all the
468 // delegate items in that case, except if the model roles has changed as well
469 // (since those are cached by QQmlAdaptorModel / Accessors). For the latter case, we
470 // simply set the model once more in the delegate model to rebuild everything.
471 auto const aim = abstractItemModel();
472 auto oldRoleNames = aim->roleNames();
473 QObject::connect(sender: aim, signal: &QAbstractItemModel::modelReset, context: this, slot: [this, aim, oldRoleNames](){
474 if (oldRoleNames != aim->roleNames())
475 setModel(model());
476 }, type: Qt::SingleShotConnection);
477}
478
479QQmlComponent *QQmlTableInstanceModel::delegate() const
480{
481 return m_delegate;
482}
483
484void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate)
485{
486 if (m_delegate == delegate)
487 return;
488
489 m_delegateChooser = nullptr;
490 if (delegate) {
491 QQmlAbstractDelegateComponent *adc =
492 qobject_cast<QQmlAbstractDelegateComponent *>(object: delegate);
493 if (adc)
494 m_delegateChooser = adc;
495 }
496
497 m_delegate = delegate;
498}
499
500const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const
501{
502 return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr;
503}
504
505// --------------------------------------------------------
506
507void QQmlTableInstanceModelIncubationTask::setInitialState(QObject *object)
508{
509 initializeRequiredProperties(modelItemToIncubate, object);
510 modelItemToIncubate->object = object;
511 emit tableInstanceModel->initItem(index: modelItemToIncubate->index, object);
512
513 if (!QQmlIncubatorPrivate::get(incubator: this)->requiredProperties()->empty()) {
514 modelItemToIncubate->object = nullptr;
515 object->deleteLater();
516 }
517}
518
519void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status)
520{
521 if (!QQmlTableInstanceModel::isDoneIncubating(modelItem: modelItemToIncubate))
522 return;
523
524 // We require the view to cancel any ongoing load
525 // requests before the tableInstanceModel is destructed.
526 Q_ASSERT(tableInstanceModel);
527
528 tableInstanceModel->incubatorStatusChanged(incubationTask: this, status);
529}
530
531QT_END_NAMESPACE
532
533#include "moc_qqmltableinstancemodel_p.cpp"
534
535

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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