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 QQDMIncubationTask *incubationTask = modelItem->incubationTask();
21 if (!incubationTask)
22 return true;
23
24 switch (incubationTask->status()) {
25 case QQmlIncubator::Ready:
26 case QQmlIncubator::Error:
27 return true;
28 default:
29 break;
30 }
31
32 return false;
33}
34
35void QQmlTableInstanceModel::deleteModelItemLater(QQmlDelegateModelItem *modelItem)
36{
37 Q_ASSERT(modelItem);
38
39 modelItem->destroyObject();
40 modelItem->deleteLater();
41}
42
43QQmlTableInstanceModel::QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent)
44 : QQmlInstanceModel(*(new QObjectPrivate()), parent)
45 , m_qmlContext(qmlContext)
46 , m_metaType(QQml::makeRefPointer<QQmlDelegateModelItemMetaType>(
47 m_qmlContext->engine()->handle(), this))
48{
49}
50
51void QQmlTableInstanceModel::useImportVersion(QTypeRevision version)
52{
53 m_adaptorModel.useImportVersion(version);
54}
55
56QQmlTableInstanceModel::~QQmlTableInstanceModel()
57{
58 for (const auto modelItem : m_modelItems) {
59 // No item in m_modelItems should be referenced at this point. The view
60 // should release all its items before it deletes this model. Only model items
61 // that are still being incubated should be left for us to delete.
62 // We can't rely on that, though. So we only check the strong ref.
63 Q_ASSERT(modelItem->objectStrongRef() == 0);
64 Q_ASSERT(modelItem->incubationTask());
65 // Check that we are not being deleted while we're
66 // in the process of e.g emitting a created signal.
67 Q_ASSERT(modelItem->scriptRef() == 0);
68
69 modelItem->destroyObject();
70 }
71
72 deleteAllFinishedIncubationTasks();
73 qDeleteAll(m_modelItems);
74 drainReusableItemsPool(0);
75}
76
77QQmlComponent *QQmlTableInstanceModel::resolveDelegate(int index)
78{
79 if (m_delegateChooser) {
80 const int row = m_adaptorModel.rowAt(index);
81 const int column = m_adaptorModel.columnAt(index);
82 QQmlComponent *delegate = nullptr;
83 QQmlAbstractDelegateComponent *chooser = m_delegateChooser;
84 do {
85 delegate = chooser->delegate(&m_adaptorModel, row, column);
86 chooser = qobject_cast<QQmlAbstractDelegateComponent *>(delegate);
87 } while (chooser);
88 return delegate;
89 }
90
91 return m_delegate;
92}
93
94QQmlDelegateModelItem *QQmlTableInstanceModel::resolveModelItem(int index)
95{
96 // Check if an item for the given index is already loaded and ready
97 QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr);
98 if (modelItem)
99 return modelItem;
100
101 QQmlComponent *delegate = resolveDelegate(index);
102 if (!delegate)
103 return nullptr;
104
105 // Check if the pool contains an item that can be reused
106 modelItem = m_reusableItemsPool.takeItem(delegate, index);
107 if (modelItem) {
108 reuseItem(modelItem, index);
109 m_modelItems.insert(index, modelItem);
110 return modelItem;
111 }
112
113 // Create a new item from scratch
114 modelItem = m_adaptorModel.createItem(m_metaType.data(), index);
115 if (modelItem) {
116 modelItem->setDelegate(delegate);
117 m_modelItems.insert(index, modelItem);
118 return modelItem;
119 }
120
121 qWarning() << Q_FUNC_INFO << "failed creating a model item for index: " << index;
122 return nullptr;
123}
124
125QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode)
126{
127 Q_ASSERT(m_delegate);
128 Q_ASSERT(index >= 0 && index < m_adaptorModel.count());
129
130 QQmlDelegateModelItem *modelItem = resolveModelItem(index);
131 if (!modelItem)
132 return nullptr;
133
134 // The model item has already been incubated. So
135 // just bump the ref-count and return it.
136 if (modelItem->object())
137 return modelItem->referenceObjectWeak();
138
139 // The object is not ready, and needs to be incubated
140 incubateModelItem(modelItem, incubationMode);
141 if (!isDoneIncubating(modelItem))
142 return nullptr;
143
144 // Incubation is done, so the task should be removed
145 Q_ASSERT(!modelItem->incubationTask());
146
147 // Incubation was completed sync and successful
148 if (modelItem->object())
149 return modelItem->referenceObjectWeak();
150
151 // The object was incubated synchronously (otherwise we would return above). But since
152 // we have no object, the incubation must have failed. And when we have no object, there
153 // should be no object references either. And there should also not be any internal script
154 // refs at this point. So we delete the model item.
155 Q_ASSERT(!modelItem->isObjectReferenced());
156 Q_ASSERT(!modelItem->isScriptReferenced());
157 m_modelItems.remove(modelItem->modelIndex());
158 delete modelItem;
159 return nullptr;
160}
161
162QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object, ReusableFlag reusable)
163{
164 Q_ASSERT(object);
165 auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(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->releaseObjectWeak())
172 return QQmlDelegateModel::Referenced;
173
174 if (modelItem->isScriptReferenced()) {
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(modelItem->modelIndex());
186
187 if (reusable == Reusable && m_reusableItemsPool.insertItem(modelItem)) {
188 emit itemPooled(modelItem->modelIndex(), 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, Deferred);
194 return QQmlInstanceModel::Destroyed;
195}
196
197void QQmlTableInstanceModel::destroyModelItem(QQmlDelegateModelItem *modelItem, DestructionMode mode)
198{
199 emit destroyingItem(modelItem->object());
200 if (mode == Deferred)
201 modelItem->destroyObjectLater();
202 else
203 modelItem->destroyObject();
204 delete modelItem;
205}
206
207void QQmlTableInstanceModel::dispose(QObject *object)
208{
209 Q_ASSERT(object);
210 auto modelItem = qvariant_cast<QQmlDelegateModelItem *>(object->property(kModelItemTag));
211 Q_ASSERT(modelItem);
212
213 modelItem->releaseObjectWeak();
214
215 // The item is not referenced by anyone
216 Q_ASSERT(!modelItem->isObjectReferenced());
217 Q_ASSERT(!modelItem->isScriptReferenced());
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(modelItem->modelIndex());
223
224 emit destroyingItem(object);
225 modelItem->destroyObject();
226
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 modelItem->destroyObject();
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, [this](QQmlDelegateModelItem *modelItem) {
252 destroyModelItem(modelItem, 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(newModelIndex);
265 const int newColumn = m_adaptorModel.columnAt(newModelIndex);
266 item->setModelIndex(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 = QList<int>();
272 m_adaptorModel.notify(itemAsList, newModelIndex, 1, 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(newModelIndex, 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 QQmlDelegateModelItem::ScriptReference scriptRef(modelItem);
284
285 if (QQDMIncubationTask *incubationTask = 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 && incubationTask->incubationMode() == QQmlIncubator::Asynchronous)
290 incubationTask->forceCompletion();
291 } else if (m_qmlContext && m_qmlContext->isValid()) {
292 modelItem->setIncubationTask(
293 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 QQmlComponent *delegate = modelItem->delegate();
300 QQmlContext *creationContext = delegate->creationContext();
301 const QQmlRefPointer<QQmlContextData> componentContext
302 = QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data());
303
304 QQmlComponentPrivate *cp = QQmlComponentPrivate::get(delegate);
305 if (cp->isBound()) {
306 modelItem->setContextData(componentContext);
307
308 // Ignore return value of initProxy. We want to know the proxy when assigning required
309 // properties, but we don't want it to pollute our context. The context is bound.
310 if (m_adaptorModel.hasProxyObject())
311 modelItem->initProxy();
312
313 cp->incubateObject(
314 modelItem->incubationTask(), delegate, m_qmlContext->engine(), componentContext,
315 QQmlContextData::get(m_qmlContext));
316 } else {
317 QQmlRefPointer<QQmlContextData> ctxt = QQmlContextData::createRefCounted(
318 QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data()));
319 ctxt->setContextObject(modelItem);
320 modelItem->setContextData(ctxt);
321
322 // If the model is read-only we cannot just expose the object as context
323 // We actually need a separate model object to moderate access.
324 if (m_adaptorModel.hasProxyObject()) {
325 if (m_adaptorModel.delegateModelAccess == QQmlDelegateModel::ReadOnly)
326 modelItem->initProxy();
327 else
328 ctxt = modelItem->initProxy();
329 }
330
331 cp->incubateObject(
332 modelItem->incubationTask(), modelItem->delegate(), m_qmlContext->engine(),
333 ctxt, QQmlContextData::get(m_qmlContext));
334 }
335 }
336}
337
338void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status)
339{
340 QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate;
341 Q_ASSERT(modelItem->incubationTask());
342
343 modelItem->clearIncubationTask();
344 incubationTask->modelItemToIncubate = nullptr;
345
346 if (status == QQmlIncubator::Ready) {
347 QObject *object = modelItem->object();
348 Q_ASSERT(object);
349
350 // Tag the incubated object with the model item for easy retrieval upon release etc.
351 object->setProperty(kModelItemTag, QVariant::fromValue(modelItem));
352
353 // Emit that the item has been created. What normally happens next is that the view
354 // upon receiving the signal asks for the model item once more. And since the item is
355 // now in the map, it will be returned directly.
356 QQmlDelegateModelItem::ScriptReference scriptRef(modelItem);
357 emit createdItem(modelItem->modelIndex(), object);
358 } else if (status == QQmlIncubator::Error) {
359 qWarning() << "Error incubating delegate:" << incubationTask->errors();
360 }
361
362 if (!modelItem->isScriptReferenced() && !modelItem->isObjectReferenced()) {
363 // We have no internal reference to the model item, and the view has no
364 // reference to the incubated object. So just delete the model item.
365 // Note that being here means that the object was incubated _async_
366 // (otherwise modelItem->isReferenced() would be true).
367 m_modelItems.remove(modelItem->modelIndex());
368
369 if (QObject *object = modelItem->object()) {
370 QQmlDelegateModelItem::ScriptReference scriptRef(modelItem);
371 emit destroyingItem(object);
372 }
373
374 Q_ASSERT(!modelItem->isScriptReferenced());
375 deleteModelItemLater(modelItem);
376 }
377
378 deleteIncubationTaskLater(incubationTask);
379}
380
381QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) {
382 const auto modelItem = m_modelItems.value(index, nullptr);
383 if (!modelItem)
384 return QQmlIncubator::Null;
385
386 if (QQDMIncubationTask *incubationTask = modelItem->incubationTask())
387 return incubationTask->status();
388
389 // Since we clear the incubation task when we're done
390 // incubating, it means that the status is Ready.
391 return QQmlIncubator::Ready;
392}
393
394bool QQmlTableInstanceModel::setRequiredProperty(int index, const QString &name, const QVariant &value)
395{
396 // This function can be called from the view upon
397 // receiving the initItem signal. It can be used to
398 // give all required delegate properties used by the
399 // view an initial value.
400 const auto modelItem = m_modelItems.value(index, nullptr);
401 if (!modelItem)
402 return false;
403 if (!modelItem->object())
404 return false;
405 if (!modelItem->incubationTask())
406 return false;
407
408 bool wasInRequired = false;
409 const auto task = QQmlIncubatorPrivate::get(modelItem->incubationTask());
410 RequiredProperties *props = task->requiredProperties();
411 if (props->empty())
412 return false;
413
414 QQmlProperty componentProp = QQmlComponentPrivate::removePropertyFromRequired(
415 modelItem->object(), name, props, QQmlEnginePrivate::get(task->enginePriv),
416 &wasInRequired);
417 if (wasInRequired)
418 componentProp.write(value);
419 return wasInRequired;
420}
421
422QQmlDelegateModelItem *QQmlTableInstanceModel::getModelItem(int index)
423{
424 return m_modelItems.value(index, nullptr);
425}
426
427void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask)
428{
429 // We often need to post-delete incubation tasks, since we cannot
430 // delete them while we're in the middle of an incubation change callback.
431 Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask));
432 m_finishedIncubationTasks.append(incubationTask);
433 if (m_finishedIncubationTasks.size() == 1)
434 QTimer::singleShot(1, this, &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks);
435}
436
437void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks()
438{
439 qDeleteAll(m_finishedIncubationTasks);
440 m_finishedIncubationTasks.clear();
441}
442
443QVariant QQmlTableInstanceModel::model() const
444{
445 return m_adaptorModel.model();
446}
447
448void QQmlTableInstanceModel::forceSetModel(const QVariant &model)
449{
450 // Pooled items are still accessible/alive for the application, and
451 // needs to stay in sync with the model. So we need to drain the pool
452 // completely when the model changes.
453 drainReusableItemsPool(0);
454 if (auto const aim = abstractItemModel()) {
455 disconnect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback);
456 disconnect(aim, &QAbstractItemModel::modelAboutToBeReset, this, &QQmlTableInstanceModel::modelAboutToBeResetCallback);
457 }
458 m_adaptorModel.setModel(model);
459 if (auto const aim = abstractItemModel()) {
460 connect(aim, &QAbstractItemModel::dataChanged, this, &QQmlTableInstanceModel::dataChangedCallback);
461 connect(aim, &QAbstractItemModel::modelAboutToBeReset, this, &QQmlTableInstanceModel::modelAboutToBeResetCallback);
462 }
463}
464
465void QQmlTableInstanceModel::setModel(const QVariant &model)
466{
467 if (m_adaptorModel.model() == model)
468 return;
469
470 forceSetModel(model);
471
472 emit modelChanged();
473}
474
475void QQmlTableInstanceModel::dataChangedCallback(const QModelIndex &begin, const QModelIndex &end, const QList<int> &roles)
476{
477 // This function is called when model data has changed. In that case, we tell the adaptor model
478 // to go through all the items we have created, find the ones that are affected, and notify that
479 // their model data has changed. This will in turn update QML bindings inside the delegate items.
480 int numberOfRowsChanged = end.row() - begin.row() + 1;
481 int numberOfColumnsChanged = end.column() - begin.column() + 1;
482
483 for (int column = 0; column < numberOfColumnsChanged; ++column) {
484 const int columnIndex = begin.column() + column;
485 const int rowIndex = begin.row() + (columnIndex * rows());
486 m_adaptorModel.notify(m_modelItems.values(), rowIndex, numberOfRowsChanged, roles);
487 }
488}
489
490void QQmlTableInstanceModel::modelAboutToBeResetCallback()
491{
492 // When the model is reset, we can no longer rely on any of the data it has
493 // provided us so far. Normally it's enough for the view to recreate all the
494 // delegate items in that case, except if the model roles has changed as well
495 // (since those are cached by QQmlAdaptorModel / Accessors). For the latter case, we
496 // simply set the model once more in the delegate model to rebuild everything.
497 auto const aim = abstractItemModel();
498 auto oldRoleNames = aim->roleNames();
499 QObject::connect(aim, &QAbstractItemModel::modelReset, this, [this, aim, oldRoleNames](){
500 if (oldRoleNames != aim->roleNames()) {
501 // We refresh the model, but without sending any signals. The actual model object
502 // stays the same after all.
503 forceSetModel(model());
504 }
505 }, Qt::SingleShotConnection);
506}
507
508QQmlComponent *QQmlTableInstanceModel::delegate() const
509{
510 return m_delegate;
511}
512
513void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate)
514{
515 if (m_delegate == delegate)
516 return;
517
518 m_delegateChooser = nullptr;
519 if (delegate) {
520 QQmlAbstractDelegateComponent *adc =
521 qobject_cast<QQmlAbstractDelegateComponent *>(delegate);
522 if (adc)
523 m_delegateChooser = adc;
524 }
525
526 m_delegate = delegate;
527}
528
529const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const
530{
531 return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr;
532}
533
534// --------------------------------------------------------
535
537{
538 initializeRequiredProperties(
539 modelItemToIncubate, object, tableInstanceModel->delegateModelAccess());
540 modelItemToIncubate->setObject(object);
541 emit tableInstanceModel->initItem(modelItemToIncubate->modelIndex(), object);
542
543 if (!QQmlIncubatorPrivate::get(this)->requiredProperties()->empty())
544 modelItemToIncubate->destroyObjectLater();
545}
546
547void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status)
548{
549 if (!QQmlTableInstanceModel::isDoneIncubating(modelItemToIncubate))
550 return;
551
552 // We require the view to cancel any ongoing load
553 // requests before the tableInstanceModel is destructed.
554 Q_ASSERT(tableInstanceModel);
555
556 tableInstanceModel->incubatorStatusChanged(this, status);
557}
558
559QT_END_NAMESPACE
560
561#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,...
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE const char * kModelItemTag