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