From e732e5c969b8ee3ddfb7ae33761ddc105e0af4cb Mon Sep 17 00:00:00 2001 From: Richard Moe Gustavsen Date: Sat, 23 Jun 2018 11:33:04 +0200 Subject: QQmlTableInstanceModel: add new TableView helper class This patch will add a new model class: QQmlTableInstanceModel. It will be used by QQuickTableView to communicate with the data model, instead of using QQmlDelegateModel. The reason we abandon QQmlDelegateModel for QQuickTableView, is because QQmlDelegateModel was written from the start to only support list models, not table models. And to work around this, we proxy table models as a list models, and combine all columns to form one big list. This as some disadvantages. First and foremost because QQmlDelegateModel is using QQmlListCompositor internally. QQmlListCompositor can combine many different list models into one big list model, and divide them into groups. Any change done to a list model will also be mirrored in the list compositor. The compositor will further create QQmlChangeSets describing such model changes, which will then be emitted by QQmlDelegateModel so that the view can transition in the same changes. This flow is especially bad when adding/removing a new row in a table. In that case, since the table looks like as a list, the list compositor will need to update its own internal structures for each item in the new row. So if you have 1000 columns, 1000 updates will be processed. Even worse, since the list compositor create QQmlChangeSets for each item in the row, the view will end up receiving 1000 signals about the change. Combine this with the fact that QQmlDelegateModel contains a lot of undocumented complex code for dealing with groups, which is not needed or used by TableView (or ListView for that sake *), adding a new QQmlTableInstanceModel that understands table models, is the right solution. Auto testing the new class will be done from QQuickTableView, once it takes it into use. * Note: The fact that TableView/ListView is using QQmlDelegateModel internally to communicate with the data model is a private implementation detail. The application will never have access to this model (and can therefore not create any groups on it etc). The application can, however, create its own DelegateModel and assigns it to TableView/ListView. But this is a different issue, and will continue to work as before. Change-Id: If1c35c27e75f236161df84ecb1d919aa3050f5a0 Reviewed-by: Mitch Curtis --- src/qml/types/qqmldelegatemodel_p_p.h | 6 +- src/qml/types/qqmltableinstancemodel.cpp | 341 +++++++++++++++++++++++++++++++ src/qml/types/qqmltableinstancemodel_p.h | 132 ++++++++++++ src/qml/types/types.pri | 6 +- 4 files changed, 480 insertions(+), 5 deletions(-) create mode 100644 src/qml/types/qqmltableinstancemodel.cpp create mode 100644 src/qml/types/qqmltableinstancemodel_p.h diff --git a/src/qml/types/qqmldelegatemodel_p_p.h b/src/qml/types/qqmldelegatemodel_p_p.h index 4c7069966b..c4e253800a 100644 --- a/src/qml/types/qqmldelegatemodel_p_p.h +++ b/src/qml/types/qqmldelegatemodel_p_p.h @@ -68,7 +68,7 @@ typedef QQmlListCompositor Compositor; class QQmlDelegateModelAttachedMetaObject; -class QQmlDelegateModelItemMetaType : public QQmlRefCount +class Q_QML_PRIVATE_EXPORT QQmlDelegateModelItemMetaType : public QQmlRefCount { public: QQmlDelegateModelItemMetaType(QV4::ExecutionEngine *engine, QQmlDelegateModel *model, const QStringList &groupNames); @@ -196,8 +196,8 @@ public: void statusChanged(Status) override; void setInitialState(QObject *) override; - QQmlDelegateModelItem *incubating; - QQmlDelegateModelPrivate *vdm; + QQmlDelegateModelItem *incubating = nullptr; + QQmlDelegateModelPrivate *vdm = nullptr; int index[QQmlListCompositor::MaximumGroupCount]; }; diff --git a/src/qml/types/qqmltableinstancemodel.cpp b/src/qml/types/qqmltableinstancemodel.cpp new file mode 100644 index 0000000000..dcad84cfb8 --- /dev/null +++ b/src/qml/types/qqmltableinstancemodel.cpp @@ -0,0 +1,341 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qqmltableinstancemodel_p.h" +#include "qqmltableinstancemodel_p.h" + +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +const char* kModelItemTag = "_tableinstancemodel_modelItem"; + +bool QQmlTableInstanceModel::isDoneIncubating(QQmlDelegateModelItem *modelItem) +{ + if (!modelItem->incubationTask) + return true; + + const auto status = modelItem->incubationTask->status(); + return (status == QQmlIncubator::Ready) || (status == QQmlIncubator::Error); +} + +void QQmlTableInstanceModel::deleteModelItemLater(QQmlDelegateModelItem *modelItem) +{ + Q_ASSERT(modelItem); + + delete modelItem->object; + modelItem->object = nullptr; + + if (modelItem->contextData) { + modelItem->contextData->invalidate(); + Q_ASSERT(modelItem->contextData->refCount == 1); + modelItem->contextData = nullptr; + } + + modelItem->deleteLater(); +} + +QQmlTableInstanceModel::QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent) + : QQmlInstanceModel(*(new QObjectPrivate()), parent) + , m_qmlContext(qmlContext) + , m_metaType(new QQmlDelegateModelItemMetaType(m_qmlContext->engine()->handle(), nullptr, QStringList())) +{ +} + +QQmlTableInstanceModel::~QQmlTableInstanceModel() +{ + deleteAllFinishedIncubationTasks(); + + // If we have model items loaded at this point, it means that + // the view is still holding references to them. So basically + // the view needs to be deleted first (and thereby release all + // its items), before this destructor is run. + Q_ASSERT(m_modelItems.isEmpty()); +} + +QObject *QQmlTableInstanceModel::object(int index, QQmlIncubator::IncubationMode incubationMode) +{ + Q_ASSERT(m_delegate); + Q_ASSERT(index >= 0 && index < m_adaptorModel.count()); + Q_ASSERT(m_qmlContext && m_qmlContext->isValid()); + + // Check if we've already created an item for the given index + QQmlDelegateModelItem *modelItem = m_modelItems.value(index, nullptr); + + if (!modelItem) { + // Create a new model item + modelItem = m_adaptorModel.createItem(m_metaType, index); + if (!modelItem) { + qWarning() << Q_FUNC_INFO << "Adaptor model failed creating a model item"; + return nullptr; + } + m_modelItems.insert(index, modelItem); + } + + if (modelItem->object) { + // The model item has already been incubated. So + // just bump the ref-count and return it. + modelItem->referenceObject(); + return modelItem->object; + } + + // Guard the model item temporarily so that it's not deleted from + // incubatorStatusChanged(), in case the incubation is done synchronously. + modelItem->scriptRef++; + + if (modelItem->incubationTask) { + // We're already incubating the model item from a previous request. If the previous call requested + // the item async, but the current request needs it sync, we need to force-complete the incubation. + const bool sync = (incubationMode == QQmlIncubator::Synchronous || incubationMode == QQmlIncubator::AsynchronousIfNested); + if (sync && modelItem->incubationTask->incubationMode() == QQmlIncubator::Asynchronous) + modelItem->incubationTask->forceCompletion(); + } else { + modelItem->incubationTask = new QQmlTableInstanceModelIncubationTask(this, modelItem, incubationMode); + + QQmlContextData *ctxt = new QQmlContextData; + QQmlContext *creationContext = m_delegate->creationContext(); + ctxt->setParent(QQmlContextData::get(creationContext ? creationContext : m_qmlContext.data())); + ctxt->contextObject = modelItem; + modelItem->contextData = ctxt; + + QQmlComponentPrivate::get(m_delegate)->incubateObject( + modelItem->incubationTask, + m_delegate, + m_qmlContext->engine(), + ctxt, + QQmlContextData::get(m_qmlContext)); + } + + // Remove the temporary guard + modelItem->scriptRef--; + Q_ASSERT(modelItem->scriptRef == 0); + + if (!isDoneIncubating(modelItem)) + return nullptr; + + // When incubation is done, the task should be removed + Q_ASSERT(!modelItem->incubationTask); + + if (!modelItem->object) { + // The object was incubated synchronously (otherwise we would return above). But since + // we have no object, the incubation must have failed. And when we have no object, there + // should be no object references either. And there should also not be any internal script + // refs at this point. So we delete the model item. + Q_ASSERT(!modelItem->isObjectReferenced()); + Q_ASSERT(!modelItem->isReferenced()); + m_modelItems.remove(modelItem->index); + delete modelItem; + return nullptr; + } + + // Incubation was completed sync and successful + modelItem->referenceObject(); + return modelItem->object; +} + +QQmlInstanceModel::ReleaseFlags QQmlTableInstanceModel::release(QObject *object) +{ + Q_ASSERT(object); + auto modelItem = qvariant_cast(object->property(kModelItemTag)); + Q_ASSERT(modelItem); + + if (!modelItem->releaseObject()) + return QQmlDelegateModel::Referenced; + + if (modelItem->isReferenced()) { + // We still have an internal reference to this object, which means that we are told to release an + // object while the createdItem signal for it is still on the stack. This can happen when objects + // are e.g delivered async, and the user flicks back and forth quicker than the loading can catch + // up with. The view might then find that the object is no longer visible and should be released. + // We detect this case in incubatorStatusChanged(), and delete it there instead. But from the callers + // points of view, it should consider it destroyed. + return QQmlDelegateModel::Destroyed; + } + + m_modelItems.remove(modelItem->index); + modelItem->destroyObject(); + emit destroyingItem(object); + + delete modelItem; + return QQmlInstanceModel::Destroyed; +} + +void QQmlTableInstanceModel::cancel(int index) +{ + auto modelItem = m_modelItems.value(index); + Q_ASSERT(modelItem); + + // Since the view expects the item to be incubating, there should be + // an incubation task. And since the incubation is not done, no-one + // should yet have received, and therfore hold a reference to, the object. + Q_ASSERT(modelItem->incubationTask); + Q_ASSERT(!modelItem->isObjectReferenced()); + + m_modelItems.remove(index); + + if (modelItem->object) + delete modelItem->object; + + // modelItem->incubationTask will be deleted from the modelItems destructor + delete modelItem; +} + +void QQmlTableInstanceModel::incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *incubationTask, QQmlIncubator::Status status) +{ + QQmlDelegateModelItem *modelItem = incubationTask->modelItemToIncubate; + Q_ASSERT(modelItem->incubationTask); + + modelItem->incubationTask = nullptr; + incubationTask->modelItemToIncubate = nullptr; + + if (status == QQmlIncubator::Ready) { + // Tag the incubated object with the model item for easy retrieval upon release etc. + modelItem->object->setProperty(kModelItemTag, QVariant::fromValue(modelItem)); + + // Emit that the item has been created. What normally happens next is that the view + // upon receiving the signal asks for the model item once more. And since the item is + // now in the map, it will be returned directly. + Q_ASSERT(modelItem->object); + modelItem->scriptRef++; + emit createdItem(modelItem->index, modelItem->object); + modelItem->scriptRef--; + } else if (status == QQmlIncubator::Error) { + qWarning() << "Error incubating delegate:" << incubationTask->errors(); + } + + if (!modelItem->isReferenced() && !modelItem->isObjectReferenced()) { + // We have no internal reference to the model item, and the view has no + // reference to the incubated object. So just delete the model item. + // Note that being here means that the object was incubated _async_ + // (otherwise modelItem->isReferenced() would be true). + m_modelItems.remove(modelItem->index); + + if (modelItem->object) { + modelItem->scriptRef++; + emit destroyingItem(modelItem->object); + modelItem->scriptRef--; + Q_ASSERT(!modelItem->isReferenced()); + } + + deleteModelItemLater(modelItem); + } + + deleteIncubationTaskLater(incubationTask); +} + +QQmlIncubator::Status QQmlTableInstanceModel::incubationStatus(int index) { + const auto modelItem = m_modelItems.value(index, nullptr); + if (!modelItem) + return QQmlIncubator::Null; + + if (modelItem->incubationTask) + return modelItem->incubationTask->status(); + + // Since we clear the incubation task when we're done + // incubating, it means that the status is Ready. + return QQmlIncubator::Ready; +} + +void QQmlTableInstanceModel::deleteIncubationTaskLater(QQmlIncubator *incubationTask) +{ + // We often need to post-delete incubation tasks, since we cannot + // delete them while we're in the middle of an incubation change callback. + Q_ASSERT(!m_finishedIncubationTasks.contains(incubationTask)); + m_finishedIncubationTasks.append(incubationTask); + if (m_finishedIncubationTasks.count() == 1) + QTimer::singleShot(1, this, &QQmlTableInstanceModel::deleteAllFinishedIncubationTasks); +} + +void QQmlTableInstanceModel::deleteAllFinishedIncubationTasks() +{ + qDeleteAll(m_finishedIncubationTasks); + m_finishedIncubationTasks.clear(); +} + +QVariant QQmlTableInstanceModel::model() const +{ + return m_adaptorModel.model(); +} + +void QQmlTableInstanceModel::setModel(const QVariant &model) +{ + m_adaptorModel.setModel(model, this, m_qmlContext->engine()); +} + +QQmlComponent *QQmlTableInstanceModel::delegate() const +{ + return m_delegate; +} + +void QQmlTableInstanceModel::setDelegate(QQmlComponent *delegate) +{ + m_delegate = delegate; +} + +const QAbstractItemModel *QQmlTableInstanceModel::abstractItemModel() const +{ + return m_adaptorModel.aim(); +} + +// -------------------------------------------------------- + +void QQmlTableInstanceModelIncubationTask::setInitialState(QObject *object) +{ + modelItemToIncubate->object = object; + emit tableInstanceModel->initItem(modelItemToIncubate->index, object); +} + +void QQmlTableInstanceModelIncubationTask::statusChanged(QQmlIncubator::Status status) +{ + if (!QQmlTableInstanceModel::isDoneIncubating(modelItemToIncubate)) + return; + + // We require the view to cancel any ongoing load + // requests before the tableInstanceModel is destructed. + Q_ASSERT(tableInstanceModel); + + tableInstanceModel->incubatorStatusChanged(this, status); +} + +QT_END_NAMESPACE + diff --git a/src/qml/types/qqmltableinstancemodel_p.h b/src/qml/types/qqmltableinstancemodel_p.h new file mode 100644 index 0000000000..fb222d8bc4 --- /dev/null +++ b/src/qml/types/qqmltableinstancemodel_p.h @@ -0,0 +1,132 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQml module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQMLTABLEINSTANCEMODEL_P_H +#define QQMLTABLEINSTANCEMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include + +QT_BEGIN_NAMESPACE + +class QQmlTableInstanceModel; + +class QQmlTableInstanceModelIncubationTask : public QQDMIncubationTask +{ +public: + QQmlTableInstanceModelIncubationTask( + QQmlTableInstanceModel *tableInstanceModel + , QQmlDelegateModelItem* modelItemToIncubate + , IncubationMode mode) + : QQDMIncubationTask(nullptr, mode) + , modelItemToIncubate(modelItemToIncubate) + , tableInstanceModel(tableInstanceModel) { + clear(); + } + + void statusChanged(Status status) override; + void setInitialState(QObject *object) override; + + QQmlDelegateModelItem *modelItemToIncubate = nullptr; + QQmlTableInstanceModel *tableInstanceModel = nullptr; +}; + +class Q_QML_PRIVATE_EXPORT QQmlTableInstanceModel : public QQmlInstanceModel +{ +public: + QQmlTableInstanceModel(QQmlContext *qmlContext, QObject *parent = nullptr); + ~QQmlTableInstanceModel() override; + + int count() const override { return m_adaptorModel.count(); } + int rows() const { return m_adaptorModel.rowCount(); } + int columns() const { return m_adaptorModel.columnCount(); } + + bool isValid() const override { return true; } + + QVariant model() const; + void setModel(const QVariant &model); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *); + + const QAbstractItemModel *abstractItemModel() const override; + + QObject *object(int index, QQmlIncubator::IncubationMode incubationMode = QQmlIncubator::AsynchronousIfNested) override; + ReleaseFlags release(QObject *) override; + void cancel(int) override; + + QQmlIncubator::Status incubationStatus(int index) override; + + QString stringValue(int, const QString &) override { Q_UNREACHABLE(); return QString(); } + void setWatchedRoles(const QList &) override { Q_UNREACHABLE(); } + int indexOf(QObject *, QObject *) const override { Q_UNREACHABLE(); return 0; } + +private: + QQmlComponent *m_delegate = nullptr; + QQmlAdaptorModel m_adaptorModel; + QPointer m_qmlContext; + QQmlDelegateModelItemMetaType *m_metaType; + + QHash m_modelItems; + QList m_finishedIncubationTasks; + + void incubatorStatusChanged(QQmlTableInstanceModelIncubationTask *dmIncubationTask, QQmlIncubator::Status status); + void deleteIncubationTaskLater(QQmlIncubator *incubationTask); + void deleteAllFinishedIncubationTasks(); + + static bool isDoneIncubating(QQmlDelegateModelItem *modelItem); + static void deleteModelItemLater(QQmlDelegateModelItem *modelItem); + + friend class QQmlTableInstanceModelIncubationTask; +}; + +QT_END_NAMESPACE + +#endif // QQMLTABLEINSTANCEMODEL_P_H diff --git a/src/qml/types/types.pri b/src/qml/types/types.pri index 5d75759281..6a74d70a0e 100644 --- a/src/qml/types/types.pri +++ b/src/qml/types/types.pri @@ -6,7 +6,8 @@ SOURCES += \ $$PWD/qqmlobjectmodel.cpp \ $$PWD/qquickpackage.cpp \ $$PWD/qquickworkerscript.cpp \ - $$PWD/qqmlinstantiator.cpp + $$PWD/qqmlinstantiator.cpp \ + $$PWD/qqmltableinstancemodel.cpp HEADERS += \ $$PWD/qqmlbind_p.h \ @@ -17,7 +18,8 @@ HEADERS += \ $$PWD/qquickpackage_p.h \ $$PWD/qquickworkerscript_p.h \ $$PWD/qqmlinstantiator_p.h \ - $$PWD/qqmlinstantiator_p_p.h + $$PWD/qqmlinstantiator_p_p.h \ + $$PWD/qqmltableinstancemodel_p.h qtConfig(qml-list-model) { SOURCES += \ -- cgit v1.2.3