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

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