1/****************************************************************************
2**
3** Copyright (C) 2014 Aaron McCarthy <mccarthy.aaron@gmail.com>
4** Copyright (C) 2015 The Qt Company Ltd.
5** Contact: http://www.qt.io/licensing/
6**
7** This file is part of the QtLocation module of the Qt Toolkit.
8**
9** $QT_BEGIN_LICENSE:LGPL3$
10** Commercial License Usage
11** Licensees holding valid commercial Qt licenses may use this file in
12** accordance with the commercial license agreement provided with the
13** Software or, alternatively, in accordance with the terms contained in
14** a written agreement between you and The Qt Company. For licensing terms
15** and conditions see http://www.qt.io/terms-conditions. For further
16** information use the contact form at http://www.qt.io/contact-us.
17**
18** GNU Lesser General Public License Usage
19** Alternatively, this file may be used under the terms of the GNU Lesser
20** General Public License version 3 as published by the Free Software
21** Foundation and appearing in the file LICENSE.LGPLv3 included in the
22** packaging of this file. Please review the following information to
23** ensure the GNU Lesser General Public License version 3 requirements
24** will be met: https://www.gnu.org/licenses/lgpl.html.
25**
26** GNU General Public License Usage
27** Alternatively, this file may be used under the terms of the GNU
28** General Public License version 2.0 or later as published by the Free
29** Software Foundation and appearing in the file LICENSE.GPL included in
30** the packaging of this file. Please review the following information to
31** ensure the GNU General Public License version 2.0 requirements will be
32** met: http://www.gnu.org/licenses/gpl-2.0.html.
33**
34** $QT_END_LICENSE$
35**
36****************************************************************************/
37
38#include "qdeclarativesupportedcategoriesmodel_p.h"
39#include "qdeclarativeplaceicon_p.h"
40#include "qgeoserviceprovider.h"
41#include "error_messages_p.h"
42#include <QtCore/private/qobject_p.h>
43
44#include <QCoreApplication>
45#include <QtQml/QQmlInfo>
46#include <QtLocation/QPlaceManager>
47#include <QtLocation/QPlaceIcon>
48
49QT_BEGIN_NAMESPACE
50
51/*!
52 \qmltype CategoryModel
53 \instantiates QDeclarativeSupportedCategoriesModel
54 \inqmlmodule QtLocation
55 \ingroup qml-QtLocation5-places
56 \ingroup qml-QtLocation5-places-models
57 \since QtLocation 5.5
58
59 \brief The CategoryModel type provides a model of the categories supported by a \l Plugin.
60
61 The CategoryModel type provides a model of the categories that are available from the
62 current \l Plugin. The model supports both a flat list of categories and a hierarchical tree
63 representing category groupings. This can be controlled by the \l hierarchical property.
64
65 The model supports the following roles:
66
67 \table
68 \header
69 \li Role
70 \li Type
71 \li Description
72 \row
73 \li category
74 \li \l Category
75 \li Category object for the current item.
76 \row
77 \li parentCategory
78 \li \l Category
79 \li Parent category object for the current item.
80 If there is no parent, null is returned.
81 \endtable
82
83 The following example displays a flat list of all available categories:
84
85 \snippet declarative/places.qml QtQuick import
86 \snippet declarative/maps.qml QtLocation import
87 \codeline
88 \snippet declarative/places.qml CategoryView
89
90 To access the hierarchical category model it is necessary to use a \l DelegateModel to access
91 the child items.
92*/
93
94/*!
95 \qmlproperty Plugin CategoryModel::plugin
96
97 This property holds the provider \l Plugin used by this model.
98*/
99
100/*!
101 \qmlproperty bool CategoryModel::hierarchical
102
103 This property holds whether the model provides a hierarchical tree of categories or a flat
104 list. The default is true.
105*/
106
107/*!
108 \qmlmethod string QtLocation::CategoryModel::data(ModelIndex index, int role)
109 \internal
110
111 This method retrieves the model's data per \a index and \a role.
112*/
113
114/*!
115 \qmlmethod string QtLocation::CategoryModel::errorString() const
116
117 This read-only property holds the textual presentation of the latest category model error.
118 If no error has occurred, an empty string is returned.
119
120 An empty string may also be returned if an error occurred which has no associated
121 textual representation.
122*/
123
124/*!
125 \qmlmethod void QtLocation::CategoryModel::update()
126 \internal
127
128 Updates the model.
129
130 \note The CategoryModel auto updates automatically when needed. Calling this method explicitly is normally not necessary.
131*/
132
133/*!
134 \internal
135 \enum QDeclarativeSupportedCategoriesModel::Roles
136*/
137
138QDeclarativeSupportedCategoriesModel::QDeclarativeSupportedCategoriesModel(QObject *parent)
139: QAbstractItemModel(parent), m_response(0), m_plugin(0), m_hierarchical(true),
140 m_complete(false), m_status(Null)
141{
142}
143
144QDeclarativeSupportedCategoriesModel::~QDeclarativeSupportedCategoriesModel()
145{
146 qDeleteAll(c: m_categoriesTree);
147}
148
149/*!
150 \internal
151*/
152// From QQmlParserStatus
153void QDeclarativeSupportedCategoriesModel::componentComplete()
154{
155 m_complete = true;
156 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.
157 update();
158}
159
160/*!
161 \internal
162*/
163int QDeclarativeSupportedCategoriesModel::rowCount(const QModelIndex &parent) const
164{
165 if (m_categoriesTree.keys().isEmpty())
166 return 0;
167
168 PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(parent.internalPointer());
169 if (!node)
170 node = m_categoriesTree.value(akey: QString());
171 else if (m_categoriesTree.keys(avalue: node).isEmpty())
172 return 0;
173
174 return node->childIds.count();
175}
176
177/*!
178 \internal
179*/
180int QDeclarativeSupportedCategoriesModel::columnCount(const QModelIndex &parent) const
181{
182 Q_UNUSED(parent);
183
184 return 1;
185}
186
187/*!
188 \internal
189*/
190QModelIndex QDeclarativeSupportedCategoriesModel::index(int row, int column, const QModelIndex &parent) const
191{
192 if (column != 0 || row < 0)
193 return QModelIndex();
194
195 PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(parent.internalPointer());
196
197 if (!node)
198 node = m_categoriesTree.value(akey: QString());
199 else if (m_categoriesTree.keys(avalue: node).isEmpty()) //return root index if parent is non-existent
200 return QModelIndex();
201
202 if (row > node->childIds.count())
203 return QModelIndex();
204
205 QString id = node->childIds.at(i: row);
206 Q_ASSERT(m_categoriesTree.contains(id));
207
208 return createIndex(arow: row, acolumn: 0, adata: m_categoriesTree.value(akey: id));
209}
210
211/*!
212 \internal
213*/
214QModelIndex QDeclarativeSupportedCategoriesModel::parent(const QModelIndex &child) const
215{
216 PlaceCategoryNode *childNode = static_cast<PlaceCategoryNode *>(child.internalPointer());
217 if (m_categoriesTree.keys(avalue: childNode).isEmpty())
218 return QModelIndex();
219
220 return index(categoryId: childNode->parentId);
221}
222
223/*!
224 \internal
225*/
226QVariant QDeclarativeSupportedCategoriesModel::data(const QModelIndex &index, int role) const
227{
228 PlaceCategoryNode *node = static_cast<PlaceCategoryNode *>(index.internalPointer());
229 if (!node)
230 node = m_categoriesTree.value(akey: QString());
231 else if (m_categoriesTree.keys(avalue: node).isEmpty())
232 return QVariant();
233
234 QDeclarativeCategory *category = node->declCategory.data();
235
236 switch (role) {
237 case Qt::DisplayRole:
238 return category->name();
239 case CategoryRole:
240 return QVariant::fromValue(value: category);
241 case ParentCategoryRole: {
242 if (!m_categoriesTree.keys().contains(t: node->parentId))
243 return QVariant();
244 else
245 return QVariant::fromValue(value: m_categoriesTree.value(akey: node->parentId)->declCategory.data());
246 }
247 default:
248 return QVariant();
249 }
250}
251
252QHash<int, QByteArray> QDeclarativeSupportedCategoriesModel::roleNames() const
253{
254 QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
255 roles.insert(akey: CategoryRole, avalue: "category");
256 roles.insert(akey: ParentCategoryRole, avalue: "parentCategory");
257 return roles;
258}
259
260/*!
261 \internal
262*/
263void QDeclarativeSupportedCategoriesModel::setPlugin(QDeclarativeGeoServiceProvider *plugin)
264{
265 if (m_plugin == plugin)
266 return;
267
268 //disconnect the manager of the old plugin if we have one
269 if (m_plugin) {
270 disconnect(sender: m_plugin, signal: nullptr, receiver: this, member: nullptr);
271 QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
272 if (serviceProvider) {
273 QPlaceManager *placeManager = serviceProvider->placeManager();
274 if (placeManager) {
275 disconnect(sender: placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)),
276 receiver: this, SLOT(addedCategory(QPlaceCategory,QString)));
277 disconnect(sender: placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)),
278 receiver: this, SLOT(updatedCategory(QPlaceCategory,QString)));
279 disconnect(sender: placeManager, SIGNAL(categoryRemoved(QString,QString)),
280 receiver: this, SLOT(removedCategory(QString,QString)));
281 disconnect(sender: placeManager, SIGNAL(dataChanged()),
282 receiver: this, SIGNAL(dataChanged()));
283 }
284 }
285 }
286
287 m_plugin = plugin;
288
289 // handle plugin attached changes -> update categories
290 if (m_plugin) {
291 if (m_plugin->isAttached()) {
292 connectNotificationSignals();
293 update();
294 } else {
295 connect(sender: m_plugin, signal: &QDeclarativeGeoServiceProvider::attached, receiver: this, slot: &QDeclarativeSupportedCategoriesModel::update);
296 connect(sender: m_plugin, signal: &QDeclarativeGeoServiceProvider::attached, receiver: this, slot: &QDeclarativeSupportedCategoriesModel::connectNotificationSignals);
297 }
298 }
299
300 if (m_complete)
301 emit pluginChanged();
302}
303
304/*!
305 \internal
306*/
307QDeclarativeGeoServiceProvider *QDeclarativeSupportedCategoriesModel::plugin() const
308{
309 return m_plugin;
310}
311
312/*!
313 \internal
314*/
315void QDeclarativeSupportedCategoriesModel::setHierarchical(bool hierarchical)
316{
317 if (m_hierarchical == hierarchical)
318 return;
319
320 m_hierarchical = hierarchical;
321 emit hierarchicalChanged();
322
323 updateLayout();
324}
325
326/*!
327 \internal
328*/
329bool QDeclarativeSupportedCategoriesModel::hierarchical() const
330{
331 return m_hierarchical;
332}
333
334/*!
335 \internal
336*/
337void QDeclarativeSupportedCategoriesModel::replyFinished()
338{
339 if (!m_response)
340 return;
341
342 m_response->deleteLater();
343
344 if (m_response->error() == QPlaceReply::NoError) {
345 m_errorString.clear();
346
347 m_response = 0;
348
349 updateLayout();
350 setStatus(status: QDeclarativeSupportedCategoriesModel::Ready);
351 } else {
352 const QString errorString = m_response->errorString();
353
354 m_response = 0;
355
356 setStatus(status: Error, errorString);
357 }
358}
359
360/*!
361 \internal
362*/
363void QDeclarativeSupportedCategoriesModel::addedCategory(const QPlaceCategory &category,
364 const QString &parentId)
365{
366 if (m_response)
367 return;
368
369 if (!m_categoriesTree.contains(akey: parentId))
370 return;
371
372 if (category.categoryId().isEmpty())
373 return;
374
375 PlaceCategoryNode *parentNode = m_categoriesTree.value(akey: parentId);
376 if (!parentNode)
377 return;
378
379 int rowToBeAdded = rowToAddChild(parentNode, category);
380 QModelIndex parentIndex = index(categoryId: parentId);
381 beginInsertRows(parent: parentIndex, first: rowToBeAdded, last: rowToBeAdded);
382 PlaceCategoryNode *categoryNode = new PlaceCategoryNode;
383 categoryNode->parentId = parentId;
384 categoryNode->declCategory = QSharedPointer<QDeclarativeCategory>(new QDeclarativeCategory(category, m_plugin, this));
385
386 m_categoriesTree.insert(akey: category.categoryId(), avalue: categoryNode);
387 parentNode->childIds.insert(i: rowToBeAdded,t: category.categoryId());
388 endInsertRows();
389
390 //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel
391 //does not get updated when a child is added to a model
392 beginResetModel();
393 endResetModel();
394}
395
396/*!
397 \internal
398*/
399void QDeclarativeSupportedCategoriesModel::updatedCategory(const QPlaceCategory &category,
400 const QString &parentId)
401{
402 if (m_response)
403 return;
404
405 QString categoryId = category.categoryId();
406
407 if (!m_categoriesTree.contains(akey: parentId))
408 return;
409
410 if (category.categoryId().isEmpty() || !m_categoriesTree.contains(akey: categoryId))
411 return;
412
413 PlaceCategoryNode *newParentNode = m_categoriesTree.value(akey: parentId);
414 if (!newParentNode)
415 return;
416
417 PlaceCategoryNode *categoryNode = m_categoriesTree.value(akey: categoryId);
418 if (!categoryNode)
419 return;
420
421 categoryNode->declCategory->setCategory(category);
422
423 if (categoryNode->parentId == parentId) { //reparenting to same parent
424 QModelIndex parentIndex = index(categoryId: parentId);
425 int rowToBeAdded = rowToAddChild(newParentNode, category);
426 int oldRow = newParentNode->childIds.indexOf(t: categoryId);
427
428 //check if we are changing the position of the category
429 if (qAbs(t: rowToBeAdded - newParentNode->childIds.indexOf(t: categoryId)) > 1) {
430 //if the position has changed we are moving rows
431 beginMoveRows(sourceParent: parentIndex, sourceFirst: oldRow, sourceLast: oldRow,
432 destinationParent: parentIndex, destinationRow: rowToBeAdded);
433
434 newParentNode->childIds.removeAll(t: categoryId);
435 newParentNode->childIds.insert(i: rowToBeAdded, t: categoryId);
436 endMoveRows();
437 } else {// if the position has not changed we modifying an existing row
438 QModelIndex categoryIndex = index(categoryId);
439 emit dataChanged(topLeft: categoryIndex, bottomRight: categoryIndex);
440 }
441 } else { //reparenting to different parents
442 QPlaceCategory oldCategory = categoryNode->declCategory->category();
443 PlaceCategoryNode *oldParentNode = m_categoriesTree.value(akey: categoryNode->parentId);
444 if (!oldParentNode)
445 return;
446 QModelIndex oldParentIndex = index(categoryId: categoryNode->parentId);
447 QModelIndex newParentIndex = index(categoryId: parentId);
448
449 int rowToBeAdded = rowToAddChild(newParentNode, category);
450 beginMoveRows(sourceParent: oldParentIndex, sourceFirst: oldParentNode->childIds.indexOf(t: categoryId),
451 sourceLast: oldParentNode->childIds.indexOf(t: categoryId), destinationParent: newParentIndex, destinationRow: rowToBeAdded);
452 oldParentNode->childIds.removeAll(t: oldCategory.categoryId());
453 newParentNode->childIds.insert(i: rowToBeAdded, t: categoryId);
454 categoryNode->parentId = parentId;
455 endMoveRows();
456
457 //this is a workaround to deal with the fact that the hasModelChildren field of DelegateModel
458 //does not get updated when an index is updated to contain children
459 beginResetModel();
460 endResetModel();
461 }
462}
463
464/*!
465 \internal
466*/
467void QDeclarativeSupportedCategoriesModel::removedCategory(const QString &categoryId, const QString &parentId)
468{
469 if (m_response)
470 return;
471
472 if (!m_categoriesTree.contains(akey: categoryId) || !m_categoriesTree.contains(akey: parentId))
473 return;
474
475 QModelIndex parentIndex = index(categoryId: parentId);
476 QModelIndex categoryIndex = index(categoryId);
477
478 beginRemoveRows(parent: parentIndex, first: categoryIndex.row(), last: categoryIndex.row());
479 PlaceCategoryNode *parentNode = m_categoriesTree.value(akey: parentId);
480 parentNode->childIds.removeAll(t: categoryId);
481 delete m_categoriesTree.take(akey: categoryId);
482 endRemoveRows();
483}
484
485/*!
486 \internal
487*/
488void QDeclarativeSupportedCategoriesModel::connectNotificationSignals()
489{
490 if (!m_plugin)
491 return;
492
493 QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
494 if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError)
495 return;
496
497 QPlaceManager *placeManager = serviceProvider->placeManager();
498 if (!placeManager)
499 return;
500
501 // listen for any category notifications so that we can reupdate the categories
502 // model.
503 connect(sender: placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)),
504 receiver: this, SLOT(addedCategory(QPlaceCategory,QString)));
505 connect(sender: placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)),
506 receiver: this, SLOT(updatedCategory(QPlaceCategory,QString)));
507 connect(sender: placeManager, SIGNAL(categoryRemoved(QString,QString)),
508 receiver: this, SLOT(removedCategory(QString,QString)));
509 connect(sender: placeManager, SIGNAL(dataChanged()),
510 receiver: this, SIGNAL(dataChanged()));
511}
512
513/*!
514 \internal
515*/
516void QDeclarativeSupportedCategoriesModel::update()
517{
518 if (!m_complete)
519 return;
520
521 if (m_response)
522 return;
523
524 setStatus(status: Loading);
525
526 if (!m_plugin) {
527 updateLayout();
528 setStatus(status: Error, errorString: QCoreApplication::translate(context: CONTEXT_NAME, key: PLUGIN_PROPERTY_NOT_SET));
529 return;
530 }
531
532 QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
533 if (!serviceProvider || serviceProvider->error() != QGeoServiceProvider::NoError) {
534 updateLayout();
535 setStatus(status: Error, errorString: QCoreApplication::translate(context: CONTEXT_NAME, key: PLUGIN_PROVIDER_ERROR)
536 .arg(a: m_plugin->name()));
537 return;
538 }
539
540 QPlaceManager *placeManager = serviceProvider->placeManager();
541 if (!placeManager) {
542 updateLayout();
543 setStatus(status: Error, errorString: QCoreApplication::translate(context: CONTEXT_NAME, key: PLUGIN_ERROR)
544 .arg(a: m_plugin->name()).arg(a: serviceProvider->errorString()));
545 return;
546 }
547
548 m_response = placeManager->initializeCategories();
549 if (m_response) {
550 connect(sender: m_response, SIGNAL(finished()), receiver: this, SLOT(replyFinished()));
551 } else {
552 updateLayout();
553 setStatus(status: Error, errorString: QCoreApplication::translate(context: CONTEXT_NAME,
554 key: CATEGORIES_NOT_INITIALIZED));
555 }
556}
557
558/*!
559 \internal
560*/
561void QDeclarativeSupportedCategoriesModel::updateLayout()
562{
563 beginResetModel();
564 qDeleteAll(c: m_categoriesTree);
565 m_categoriesTree.clear();
566
567 if (m_plugin) {
568 QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
569 if (serviceProvider && serviceProvider->error() == QGeoServiceProvider::NoError) {
570 QPlaceManager *placeManager = serviceProvider->placeManager();
571 if (placeManager) {
572 PlaceCategoryNode *node = new PlaceCategoryNode;
573 node->childIds = populateCategories(placeManager, parent: QPlaceCategory());
574 m_categoriesTree.insert(akey: QString(), avalue: node);
575 node->declCategory = QSharedPointer<QDeclarativeCategory>
576 (new QDeclarativeCategory(QPlaceCategory(), m_plugin, this));
577 }
578 }
579 }
580
581 endResetModel();
582}
583
584QString QDeclarativeSupportedCategoriesModel::errorString() const
585{
586 return m_errorString;
587}
588
589/*!
590 \qmlproperty enumeration CategoryModel::status
591
592 This property holds the status of the model. It can be one of:
593
594 \table
595 \row
596 \li CategoryModel.Null
597 \li No category fetch query has been executed. The model is empty.
598 \row
599 \li CategoryModel.Ready
600 \li No error occurred during the last operation, further operations may be performed on
601 the model.
602 \row
603 \li CategoryModel.Loading
604 \li The model is being updated, no other operations may be performed until complete.
605 \row
606 \li CategoryModel.Error
607 \li An error occurred during the last operation, further operations can still be
608 performed on the model.
609 \endtable
610*/
611void QDeclarativeSupportedCategoriesModel::setStatus(Status status, const QString &errorString)
612{
613 Status originalStatus = m_status;
614 m_status = status;
615 m_errorString = errorString;
616
617 if (originalStatus != m_status)
618 emit statusChanged();
619}
620
621QDeclarativeSupportedCategoriesModel::Status QDeclarativeSupportedCategoriesModel::status() const
622{
623 return m_status;
624}
625
626/*!
627 \internal
628*/
629QStringList QDeclarativeSupportedCategoriesModel::populateCategories(QPlaceManager *manager, const QPlaceCategory &parent)
630{
631 Q_ASSERT(manager);
632
633 QStringList childIds;
634
635 const auto byName = [](const QPlaceCategory &lhs, const QPlaceCategory &rhs) {
636 return lhs.name() < rhs.name();
637 };
638
639 auto categories = manager->childCategories(parentId: parent.categoryId());
640 std::sort(first: categories.begin(), last: categories.end(), comp: byName);
641
642 for (const auto &category : qAsConst(t&: categories)) {
643 auto node = new PlaceCategoryNode;
644 node->parentId = parent.categoryId();
645 node->declCategory = QSharedPointer<QDeclarativeCategory>(new QDeclarativeCategory(category, m_plugin ,this));
646
647 if (m_hierarchical)
648 node->childIds = populateCategories(manager, parent: category);
649
650 m_categoriesTree.insert(akey: node->declCategory->categoryId(), avalue: node);
651 childIds.append(t: category.categoryId());
652
653 if (!m_hierarchical) {
654 childIds.append(t: populateCategories(manager,parent: node->declCategory->category()));
655 }
656 }
657 return childIds;
658}
659
660/*!
661 \internal
662*/
663QModelIndex QDeclarativeSupportedCategoriesModel::index(const QString &categoryId) const
664{
665 if (categoryId.isEmpty())
666 return QModelIndex();
667
668 if (!m_categoriesTree.contains(akey: categoryId))
669 return QModelIndex();
670
671 PlaceCategoryNode *categoryNode = m_categoriesTree.value(akey: categoryId);
672 if (!categoryNode)
673 return QModelIndex();
674
675 QString parentCategoryId = categoryNode->parentId;
676
677 PlaceCategoryNode *parentNode = m_categoriesTree.value(akey: parentCategoryId);
678
679 return createIndex(arow: parentNode->childIds.indexOf(t: categoryId), acolumn: 0, adata: categoryNode);
680}
681
682/*!
683 \internal
684*/
685int QDeclarativeSupportedCategoriesModel::rowToAddChild(PlaceCategoryNode *node, const QPlaceCategory &category)
686{
687 Q_ASSERT(node);
688 for (int i = 0; i < node->childIds.count(); ++i) {
689 if (category.name() < m_categoriesTree.value(akey: node->childIds.at(i))->declCategory->name())
690 return i;
691 }
692 return node->childIds.count();
693}
694
695/*!
696 \qmlsignal CategoryModel::dataChanged()
697
698 This signal is emitted when significant changes have been made to the underlying datastore.
699
700 Applications should act on this signal at their own discretion. The data
701 provided by the model could be out of date and so the model should be reupdated
702 sometime, however an immediate reupdate may be disconcerting to users if the categories
703 change without any action on their part.
704
705 The corresponding handler is \c onDataChanged.
706*/
707
708QT_END_NAMESPACE
709

source code of qtlocation/src/location/declarativeplaces/qdeclarativesupportedcategoriesmodel.cpp