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 | |
49 | QT_BEGIN_NAMESPACE |
50 | |
51 | const char* kModelItemTag = "_tableinstancemodel_modelItem" ; |
52 | |
53 | bool 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 | |
62 | void 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 | |
78 | QQmlTableInstanceModel::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 | |
86 | void QQmlTableInstanceModel::useImportVersion(int minorVersion) |
87 | { |
88 | m_adaptorModel.useImportVersion(minorVersion); |
89 | } |
90 | |
91 | QQmlTableInstanceModel::~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 | |
116 | QQmlComponent *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 | |
133 | QQmlDelegateModelItem *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 | |
164 | QObject *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 | |
206 | QQmlInstanceModel::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 | |
239 | void 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 | |
249 | void 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 | |
268 | void 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 | |
288 | void QQmlTableInstanceModel::drainReusableItemsPool(int maxPoolTime) |
289 | { |
290 | m_reusableItemsPool.drain(maxPoolTime, releaseItem: [this](QQmlDelegateModelItem *modelItem) { |
291 | destroyModelItem(modelItem, mode: Immediate); |
292 | }); |
293 | } |
294 | |
295 | void 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 | |
318 | void 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 | |
351 | void 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 | |
394 | QQmlIncubator::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 | |
407 | void 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 | |
417 | void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks() |
418 | { |
419 | qDeleteAll(c: m_finishedIncubationTasks); |
420 | m_finishedIncubationTasks.clear(); |
421 | } |
422 | |
423 | QVariant QQmlTableInstanceModel::model() const |
424 | { |
425 | return m_adaptorModel.model(); |
426 | } |
427 | |
428 | void 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 | |
441 | void 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 | |
456 | QQmlComponent *QQmlTableInstanceModel::delegate() const |
457 | { |
458 | return m_delegate; |
459 | } |
460 | |
461 | void 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 | |
477 | const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const |
478 | { |
479 | return m_adaptorModel.adaptsAim() ? m_adaptorModel.aim() : nullptr; |
480 | } |
481 | |
482 | // -------------------------------------------------------- |
483 | |
484 | void 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 | |
495 | void 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 | |
509 | QT_END_NAMESPACE |
510 | |
511 | |