/**************************************************************************** ** ** Copyright (C) 2014 Aaron McCarthy ** Copyright (C) 2015 The Qt Company Ltd. ** Contact: http://www.qt.io/licensing/ ** ** This file is part of the QtLocation module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL3$ ** 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 http://www.qt.io/terms-conditions. For further ** information use the contact form at http://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.LGPLv3 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.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 later as published by the Free ** Software Foundation and appearing in the file LICENSE.GPL included in ** the packaging of this file. Please review the following information to ** ensure the GNU General Public License version 2.0 requirements will be ** met: http://www.gnu.org/licenses/gpl-2.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qdeclarativesupportedcategoriesmodel_p.h" #include "qdeclarativeplaceicon_p.h" #include "qgeoserviceprovider.h" #include "error_messages_p.h" #include #include #include #include #include QT_BEGIN_NAMESPACE /*! \qmltype CategoryModel \instantiates QDeclarativeSupportedCategoriesModel \inqmlmodule QtLocation \ingroup qml-QtLocation5-places \ingroup qml-QtLocation5-places-models \since QtLocation 5.5 \brief The CategoryModel type provides a model of the categories supported by a \l Plugin. The CategoryModel type provides a model of the categories that are available from the current \l Plugin. The model supports both a flat list of categories and a hierarchical tree representing category groupings. This can be controlled by the \l hierarchical property. The model supports the following roles: \table \header \li Role \li Type \li Description \row \li category \li \l Category \li Category object for the current item. \row \li parentCategory \li \l Category \li Parent category object for the current item. If there is no parent, null is returned. \endtable The following example displays a flat list of all available categories: \snippet declarative/places.qml QtQuick import \snippet declarative/maps.qml QtLocation import \codeline \snippet declarative/places.qml CategoryView To access the hierarchical category model it is necessary to use a \l DelegateModel to access the child items. */ /*! \qmlproperty Plugin CategoryModel::plugin This property holds the provider \l Plugin used by this model. */ /*! \qmlproperty bool CategoryModel::hierarchical This property holds whether the model provides a hierarchical tree of categories or a flat list. The default is true. */ /*! \qmlmethod string QtLocation::CategoryModel::data(ModelIndex index, int role) \internal This method retrieves the model's data per \a index and \a role. */ /*! \qmlmethod string QtLocation::CategoryModel::errorString() const This read-only property holds the textual presentation of the latest category model error. If no error has occurred, an empty string is returned. An empty string may also be returned if an error occurred which has no associated textual representation. */ /*! \qmlmethod void QtLocation::CategoryModel::update() \internal Updates the model. \note The CategoryModel auto updates automatically when needed. Calling this method explicitly is normally not necessary. */ /*! \internal \enum QDeclarativeSupportedCategoriesModel::Roles */ QDeclarativeSupportedCategoriesModel::QDeclarativeSupportedCategoriesModel(QObject *parent) : QAbstractItemModel(parent), m_response(0), m_plugin(0), m_hierarchical(true), m_complete(false), m_status(Null) { } QDeclarativeSupportedCategoriesModel::~QDeclarativeSupportedCategoriesModel() { qDeleteAll(m_categoriesTree); } /*! \internal */ // From QQmlParserStatus void QDeclarativeSupportedCategoriesModel::componentComplete() { m_complete = true; if (m_plugin) // do not try to load or change status when trying to update in componentComplete() if the plugin hasn't been set yet even once. update(); } /*! \internal */ int QDeclarativeSupportedCategoriesModel::rowCount(const QModelIndex &parent) const { if (m_categoriesTree.keys().isEmpty()) return 0; PlaceCategoryNode *node = static_cast(parent.internalPointer()); if (!node) node = m_categoriesTree.value(QString()); else if (m_categoriesTree.keys(node).isEmpty()) return 0; return node->childIds.count(); } /*! \internal */ int QDeclarativeSupportedCategoriesModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent); return 1; } /*! \internal */ QModelIndex QDeclarativeSupportedCategoriesModel::index(int row, int column, const QModelIndex &parent) const { if (column != 0 || row < 0) return QModelIndex(); PlaceCategoryNode *node = static_cast(parent.internalPointer()); if (!node) node = m_categoriesTree.value(QString()); else if (m_categoriesTree.keys(node).isEmpty()) //return root index if parent is non-existent return QModelIndex(); if (row > node->childIds.count()) return QModelIndex(); QString id = node->childIds.at(row); Q_ASSERT(m_categoriesTree.contains(id)); return createIndex(row, 0, m_categoriesTree.value(id)); } /*! \internal */ QModelIndex QDeclarativeSupportedCategoriesModel::parent(const QModelIndex &child) const { PlaceCategoryNode *childNode = static_cast(child.internalPointer()); if (m_categoriesTree.keys(childNode).isEmpty()) return QModelIndex(); return index(childNode->parentId); } /*! \internal */ QVariant QDeclarativeSupportedCategoriesModel::data(const QModelIndex &index, int role) const { PlaceCategoryNode *node = static_cast(index.internalPointer()); if (!node) node = m_categoriesTree.value(QString()); else if (m_categoriesTree.keys(node).isEmpty()) return QVariant(); QDeclarativeCategory *category = node->declCategory.data(); switch (role) { case Qt::DisplayRole: return category->name(); case CategoryRole: return QVariant::fromValue(category); case ParentCategoryRole: { if (!m_categoriesTree.keys().contains(node->parentId)) return QVariant(); else return QVariant::fromValue(m_categoriesTree.value(node->parentId)->declCategory.data()); } default: return QVariant(); } } QHash QDeclarativeSupportedCategoriesModel::roleNames() const { QHash roles = QAbstractItemModel::roleNames(); roles.insert(CategoryRole, "category"); roles.insert(ParentCategoryRole, "parentCategory"); return roles; } /*! \internal */ void QDeclarativeSupportedCategoriesModel::setPlugin(QDeclarativeGeoServiceProvider *plugin) { if (m_plugin == plugin) return; //disconnect the manager of the old plugin if we have one if (m_plugin) { disconnect(m_plugin, nullptr, this, nullptr); QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider(); if (serviceProvider) { QPlaceManager *placeManager = serviceProvider->placeManager(); if (placeManager) { disconnect(placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)), this, SLOT(addedCategory(QPlaceCategory,QString))); disconnect(placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)), this, SLOT(updatedCategory(QPlaceCategory,QString))); disconnect(placeManager, SIGNAL(categoryRemoved(QString,QString)), this, SLOT(removedCategory(QString,QString))); disconnect(placeManager, SIGNAL(dataChanged()), this, SIGNAL(dataChanged())); } } } m_plugin = plugin; // handle plugin attached changes -> update categories if (m_plugin) { if (m_plugin->isAttached()) { connectNotificationSignals(); update(); } else { connect(m_plugin, &QDeclarativeGeoServiceProvider::attached, this, &QDeclarativeSupportedCategoriesModel::update); connect(m_plugin, &QDeclarativeGeoServiceProvider::attached, this, &QDeclarativeSupportedCategoriesModel::connectNotificationSignals); } } if (m_complete) emit pluginChanged(); } /*! \internal */ QDeclarativeGeoServiceProvider *QDeclarativeSupportedCategoriesModel::plugin() const { return m_plugin; } /*! \internal */ void QDeclarativeSupportedCategoriesModel::setHierarchical(bool hierarchical) { if (m_hierarchical == hierarchical) return; m_hierarchical = hierarchical; emit hierarchicalChanged(); updateLayout(); } /*! \internal */ bool QDeclarativeSupportedCategoriesModel::hierarchical() const { return m_hierarchical; } /*! \internal */ void QDeclarativeSupportedCategoriesModel::replyFinished() { if (!m_response) return; m_response->deleteLater(); if (m_response->error() == QPlaceReply::NoError) { m_errorString.clear(); m_response = 0; updateLayout(); setStatus(QDeclarativeSupportedCategoriesModel::Ready); } else { const QString errorString = m_response->errorString(); m_response = 0; setStatus(Error, errorString); } } /*! \internal */ void QDeclarativeSupportedCategoriesModel::addedCategory(const QPlaceCategory &category, const QString &parentId) { if (m_response) return; if (!m_categoriesTree.contains(parentId)) return; if (category.categoryId().isEmpty()) return; PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId); if (!parentNode) return; int rowToBeAdded = rowToAddChild(parentNode, category); QModelIndex parentIndex = index(parentId); beginInsertRows(parentIndex, rowToBeAdded, rowToBeAdded); PlaceCategoryNode *categoryNode = new PlaceCategoryNode; categoryNode->parentId = parentId; categoryNode->declCategory = QSharedPointer(new QDeclarativeCategory(category, m_plugin, this)); m_categoriesTree.insert(category.categoryId(), categoryNode); parentNode->childIds.insert(rowToBeAdded,category.categoryId()); endInsertRows(); //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel //does not get updated when a child is added to a model beginResetModel(); endResetModel(); } /*! \internal */ void QDeclarativeSupportedCategoriesModel::updatedCategory(const QPlaceCategory &category, const QString &parentId) { if (m_response) return; QString categoryId = category.categoryId(); if (!m_categoriesTree.contains(parentId)) return; if (category.categoryId().isEmpty() || !m_categoriesTree.contains(categoryId)) return; PlaceCategoryNode *newParentNode = m_categoriesTree.value(parentId); if (!newParentNode) return; PlaceCategoryNode *categoryNode = m_categoriesTree.value(categoryId); if (!categoryNode) return; categoryNode->declCategory->setCategory(category); if (categoryNode->parentId == parentId) { //reparenting to same parent QModelIndex parentIndex = index(parentId); int rowToBeAdded = rowToAddChild(newParentNode, category); int oldRow = newParentNode->childIds.indexOf(categoryId); //check if we are changing the position of the category if (qAbs(rowToBeAdded - newParentNode->childIds.indexOf(categoryId)) > 1) { //if the position has changed we are moving rows beginMoveRows(parentIndex, oldRow, oldRow, parentIndex, rowToBeAdded); newParentNode->childIds.removeAll(categoryId); newParentNode->childIds.insert(rowToBeAdded, categoryId); endMoveRows(); } else {// if the position has not changed we modifying an existing row QModelIndex categoryIndex = index(categoryId); emit dataChanged(categoryIndex, categoryIndex); } } else { //reparenting to different parents QPlaceCategory oldCategory = categoryNode->declCategory->category(); PlaceCategoryNode *oldParentNode = m_categoriesTree.value(categoryNode->parentId); if (!oldParentNode) return; QModelIndex oldParentIndex = index(categoryNode->parentId); QModelIndex newParentIndex = index(parentId); int rowToBeAdded = rowToAddChild(newParentNode, category); beginMoveRows(oldParentIndex, oldParentNode->childIds.indexOf(categoryId), oldParentNode->childIds.indexOf(categoryId), newParentIndex, rowToBeAdded); oldParentNode->childIds.removeAll(oldCategory.categoryId()); newParentNode->childIds.insert(rowToBeAdded, categoryId); categoryNode->parentId = parentId; endMoveRows(); //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel //does not get updated when an index is updated to contain children beginResetModel(); endResetModel(); } } /*! \internal */ void QDeclarativeSupportedCategoriesModel::removedCategory(const QString &categoryId, const QString &parentId) { if (m_response) return; if (!m_categoriesTree.contains(categoryId) || !m_categoriesTree.contains(parentId)) return; QModelIndex parentIndex = index(parentId); QModelIndex categoryIndex = index(categoryId); beginRemoveRows(parentIndex, categoryIndex.row(), categoryIndex.row()); PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId); parentNode->childIds.removeAll(categoryId); delete m_categoriesTree.take(categoryId); endRemoveRows(); } /*! \internal */ void QDeclarativeSupportedCategoriesModel::connectNotificationSignals() { if (!m_plugin) return; QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider(); if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError) return; QPlaceManager *placeManager = serviceProvider->placeManager(); if (!placeManager) return; // listen for any category notifications so that we can reupdate the categories // model. connect(placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)), this, SLOT(addedCategory(QPlaceCategory,QString))); connect(placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)), this, SLOT(updatedCategory(QPlaceCategory,QString))); connect(placeManager, SIGNAL(categoryRemoved(QString,QString)), this, SLOT(removedCategory(QString,QString))); connect(placeManager, SIGNAL(dataChanged()), this, SIGNAL(dataChanged())); } /*! \internal */ void QDeclarativeSupportedCategoriesModel::update() { if (!m_complete) return; if (m_response) return; setStatus(Loading); if (!m_plugin) { updateLayout(); setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_PROPERTY_NOT_SET)); return; } QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider(); if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError) { updateLayout(); setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_PROVIDER_ERROR) .arg(m_plugin->name())); return; } QPlaceManager *placeManager = serviceProvider->placeManager(); if (!placeManager) { updateLayout(); setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_ERROR) .arg(m_plugin->name()).arg(serviceProvider->errorString())); return; } m_response = placeManager->initializeCategories(); if (m_response) { connect(m_response, SIGNAL(finished()), this, SLOT(replyFinished())); } else { updateLayout(); setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, CATEGORIES_NOT_INITIALIZED)); } } /*! \internal */ void QDeclarativeSupportedCategoriesModel::updateLayout() { beginResetModel(); qDeleteAll(m_categoriesTree); m_categoriesTree.clear(); if (m_plugin) { QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider(); if (serviceProvider && serviceProvider->error() == QGeoServiceProvider::NoError) { QPlaceManager *placeManager = serviceProvider->placeManager(); if (placeManager) { PlaceCategoryNode *node = new PlaceCategoryNode; node->childIds = populateCategories(placeManager, QPlaceCategory()); m_categoriesTree.insert(QString(), node); node->declCategory = QSharedPointer (new QDeclarativeCategory(QPlaceCategory(), m_plugin, this)); } } } endResetModel(); } QString QDeclarativeSupportedCategoriesModel::errorString() const { return m_errorString; } /*! \qmlproperty enumeration CategoryModel::status This property holds the status of the model. It can be one of: \table \row \li CategoryModel.Null \li No category fetch query has been executed. The model is empty. \row \li CategoryModel.Ready \li No error occurred during the last operation, further operations may be performed on the model. \row \li CategoryModel.Loading \li The model is being updated, no other operations may be performed until complete. \row \li CategoryModel.Error \li An error occurred during the last operation, further operations can still be performed on the model. \endtable */ void QDeclarativeSupportedCategoriesModel::setStatus(Status status, const QString &errorString) { Status originalStatus = m_status; m_status = status; m_errorString = errorString; if (originalStatus != m_status) emit statusChanged(); } QDeclarativeSupportedCategoriesModel::Status QDeclarativeSupportedCategoriesModel::status() const { return m_status; } /*! \internal */ QStringList QDeclarativeSupportedCategoriesModel::populateCategories(QPlaceManager *manager, const QPlaceCategory &parent) { Q_ASSERT(manager); QStringList childIds; const auto byName = [](const QPlaceCategory &lhs, const QPlaceCategory &rhs) { return lhs.name() < rhs.name(); }; auto categories = manager->childCategories(parent.categoryId()); std::sort(categories.begin(), categories.end(), byName); for (const auto &category : qAsConst(categories)) { auto node = new PlaceCategoryNode; node->parentId = parent.categoryId(); node->declCategory = QSharedPointer(new QDeclarativeCategory(category, m_plugin ,this)); if (m_hierarchical) node->childIds = populateCategories(manager, category); m_categoriesTree.insert(node->declCategory->categoryId(), node); childIds.append(category.categoryId()); if (!m_hierarchical) { childIds.append(populateCategories(manager,node->declCategory->category())); } } return childIds; } /*! \internal */ QModelIndex QDeclarativeSupportedCategoriesModel::index(const QString &categoryId) const { if (categoryId.isEmpty()) return QModelIndex(); if (!m_categoriesTree.contains(categoryId)) return QModelIndex(); PlaceCategoryNode *categoryNode = m_categoriesTree.value(categoryId); if (!categoryNode) return QModelIndex(); QString parentCategoryId = categoryNode->parentId; PlaceCategoryNode *parentNode = m_categoriesTree.value(parentCategoryId); return createIndex(parentNode->childIds.indexOf(categoryId), 0, categoryNode); } /*! \internal */ int QDeclarativeSupportedCategoriesModel::rowToAddChild(PlaceCategoryNode *node, const QPlaceCategory &category) { Q_ASSERT(node); for (int i = 0; i < node->childIds.count(); ++i) { if (category.name() < m_categoriesTree.value(node->childIds.at(i))->declCategory->name()) return i; } return node->childIds.count(); } /*! \qmlsignal CategoryModel::dataChanged() This signal is emitted when significant changes have been made to the underlying datastore. Applications should act on this signal at their own discretion. The data provided by the model could be out of date and so the model should be reupdated sometime, however an immediate reupdate may be disconcerting to users if the categories change without any action on their part. The corresponding handler is \c onDataChanged. */ QT_END_NAMESPACE