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 
49 QT_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 
QDeclarativeSupportedCategoriesModel(QObject * parent)138 QDeclarativeSupportedCategoriesModel::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 
~QDeclarativeSupportedCategoriesModel()144 QDeclarativeSupportedCategoriesModel::~QDeclarativeSupportedCategoriesModel()
145 {
146     qDeleteAll(m_categoriesTree);
147 }
148 
149 /*!
150     \internal
151 */
152 // From QQmlParserStatus
componentComplete()153 void 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 */
rowCount(const QModelIndex & parent) const163 int 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(QString());
171     else if (m_categoriesTree.keys(node).isEmpty())
172         return 0;
173 
174     return node->childIds.count();
175 }
176 
177 /*!
178     \internal
179 */
columnCount(const QModelIndex & parent) const180 int QDeclarativeSupportedCategoriesModel::columnCount(const QModelIndex &parent) const
181 {
182     Q_UNUSED(parent);
183 
184     return 1;
185 }
186 
187 /*!
188     \internal
189 */
index(int row,int column,const QModelIndex & parent) const190 QModelIndex 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(QString());
199     else if (m_categoriesTree.keys(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(row);
206     Q_ASSERT(m_categoriesTree.contains(id));
207 
208     return createIndex(row, 0, m_categoriesTree.value(id));
209 }
210 
211 /*!
212     \internal
213 */
parent(const QModelIndex & child) const214 QModelIndex QDeclarativeSupportedCategoriesModel::parent(const QModelIndex &child) const
215 {
216     PlaceCategoryNode *childNode = static_cast<PlaceCategoryNode *>(child.internalPointer());
217     if (m_categoriesTree.keys(childNode).isEmpty())
218         return QModelIndex();
219 
220     return index(childNode->parentId);
221 }
222 
223 /*!
224     \internal
225 */
data(const QModelIndex & index,int role) const226 QVariant 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(QString());
231     else if (m_categoriesTree.keys(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(category);
241     case ParentCategoryRole: {
242         if (!m_categoriesTree.keys().contains(node->parentId))
243             return QVariant();
244         else
245             return QVariant::fromValue(m_categoriesTree.value(node->parentId)->declCategory.data());
246     }
247     default:
248         return QVariant();
249     }
250 }
251 
roleNames() const252 QHash<int, QByteArray> QDeclarativeSupportedCategoriesModel::roleNames() const
253 {
254     QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
255     roles.insert(CategoryRole, "category");
256     roles.insert(ParentCategoryRole, "parentCategory");
257     return roles;
258 }
259 
260 /*!
261     \internal
262 */
setPlugin(QDeclarativeGeoServiceProvider * plugin)263 void 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(m_plugin, nullptr, this, nullptr);
271         QGeoServiceProvider *serviceProvider = m_plugin->sharedGeoServiceProvider();
272         if (serviceProvider) {
273             QPlaceManager *placeManager = serviceProvider->placeManager();
274             if (placeManager) {
275                 disconnect(placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)),
276                            this, SLOT(addedCategory(QPlaceCategory,QString)));
277                 disconnect(placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)),
278                            this, SLOT(updatedCategory(QPlaceCategory,QString)));
279                 disconnect(placeManager, SIGNAL(categoryRemoved(QString,QString)),
280                            this, SLOT(removedCategory(QString,QString)));
281                 disconnect(placeManager, SIGNAL(dataChanged()),
282                            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(m_plugin, &QDeclarativeGeoServiceProvider::attached, this, &QDeclarativeSupportedCategoriesModel::update);
296             connect(m_plugin, &QDeclarativeGeoServiceProvider::attached, this, &QDeclarativeSupportedCategoriesModel::connectNotificationSignals);
297         }
298     }
299 
300     if (m_complete)
301         emit pluginChanged();
302 }
303 
304 /*!
305     \internal
306 */
plugin() const307 QDeclarativeGeoServiceProvider *QDeclarativeSupportedCategoriesModel::plugin() const
308 {
309     return m_plugin;
310 }
311 
312 /*!
313     \internal
314 */
setHierarchical(bool hierarchical)315 void 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 */
hierarchical() const329 bool QDeclarativeSupportedCategoriesModel::hierarchical() const
330 {
331     return m_hierarchical;
332 }
333 
334 /*!
335     \internal
336 */
replyFinished()337 void 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(QDeclarativeSupportedCategoriesModel::Ready);
351     } else {
352         const QString errorString = m_response->errorString();
353 
354         m_response = 0;
355 
356         setStatus(Error, errorString);
357     }
358 }
359 
360 /*!
361     \internal
362 */
addedCategory(const QPlaceCategory & category,const QString & parentId)363 void QDeclarativeSupportedCategoriesModel::addedCategory(const QPlaceCategory &category,
364                                                          const QString &parentId)
365 {
366     if (m_response)
367         return;
368 
369     if (!m_categoriesTree.contains(parentId))
370         return;
371 
372     if (category.categoryId().isEmpty())
373         return;
374 
375     PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId);
376     if (!parentNode)
377         return;
378 
379     int rowToBeAdded = rowToAddChild(parentNode, category);
380     QModelIndex parentIndex = index(parentId);
381     beginInsertRows(parentIndex, rowToBeAdded, 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(category.categoryId(), categoryNode);
387     parentNode->childIds.insert(rowToBeAdded,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 */
updatedCategory(const QPlaceCategory & category,const QString & parentId)399 void 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(parentId))
408         return;
409 
410     if (category.categoryId().isEmpty() || !m_categoriesTree.contains(categoryId))
411         return;
412 
413     PlaceCategoryNode *newParentNode = m_categoriesTree.value(parentId);
414     if (!newParentNode)
415         return;
416 
417     PlaceCategoryNode *categoryNode = m_categoriesTree.value(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(parentId);
425         int rowToBeAdded = rowToAddChild(newParentNode, category);
426         int oldRow = newParentNode->childIds.indexOf(categoryId);
427 
428         //check if we are changing the position of the category
429         if (qAbs(rowToBeAdded - newParentNode->childIds.indexOf(categoryId)) > 1) {
430             //if the position has changed we are moving rows
431             beginMoveRows(parentIndex, oldRow, oldRow,
432                           parentIndex, rowToBeAdded);
433 
434             newParentNode->childIds.removeAll(categoryId);
435             newParentNode->childIds.insert(rowToBeAdded, categoryId);
436             endMoveRows();
437         } else {// if the position has not changed we modifying an existing row
438             QModelIndex categoryIndex = index(categoryId);
439             emit dataChanged(categoryIndex, categoryIndex);
440         }
441     } else { //reparenting to different parents
442         QPlaceCategory oldCategory = categoryNode->declCategory->category();
443         PlaceCategoryNode *oldParentNode = m_categoriesTree.value(categoryNode->parentId);
444         if (!oldParentNode)
445             return;
446         QModelIndex oldParentIndex = index(categoryNode->parentId);
447         QModelIndex newParentIndex = index(parentId);
448 
449         int rowToBeAdded = rowToAddChild(newParentNode, category);
450         beginMoveRows(oldParentIndex, oldParentNode->childIds.indexOf(categoryId),
451                       oldParentNode->childIds.indexOf(categoryId), newParentIndex, rowToBeAdded);
452         oldParentNode->childIds.removeAll(oldCategory.categoryId());
453         newParentNode->childIds.insert(rowToBeAdded, 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 */
removedCategory(const QString & categoryId,const QString & parentId)467 void QDeclarativeSupportedCategoriesModel::removedCategory(const QString &categoryId, const QString &parentId)
468 {
469     if (m_response)
470         return;
471 
472     if (!m_categoriesTree.contains(categoryId) || !m_categoriesTree.contains(parentId))
473         return;
474 
475     QModelIndex parentIndex = index(parentId);
476     QModelIndex categoryIndex = index(categoryId);
477 
478     beginRemoveRows(parentIndex, categoryIndex.row(), categoryIndex.row());
479     PlaceCategoryNode *parentNode = m_categoriesTree.value(parentId);
480     parentNode->childIds.removeAll(categoryId);
481     delete m_categoriesTree.take(categoryId);
482     endRemoveRows();
483 }
484 
485 /*!
486     \internal
487 */
connectNotificationSignals()488 void 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(placeManager, SIGNAL(categoryAdded(QPlaceCategory,QString)),
504             this, SLOT(addedCategory(QPlaceCategory,QString)));
505     connect(placeManager, SIGNAL(categoryUpdated(QPlaceCategory,QString)),
506             this, SLOT(updatedCategory(QPlaceCategory,QString)));
507     connect(placeManager, SIGNAL(categoryRemoved(QString,QString)),
508             this, SLOT(removedCategory(QString,QString)));
509     connect(placeManager, SIGNAL(dataChanged()),
510             this, SIGNAL(dataChanged()));
511 }
512 
513 /*!
514     \internal
515 */
update()516 void QDeclarativeSupportedCategoriesModel::update()
517 {
518     if (!m_complete)
519         return;
520 
521     if (m_response)
522         return;
523 
524     setStatus(Loading);
525 
526     if (!m_plugin) {
527         updateLayout();
528         setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, 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(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_PROVIDER_ERROR)
536                          .arg(m_plugin->name()));
537         return;
538     }
539 
540     QPlaceManager *placeManager = serviceProvider->placeManager();
541     if (!placeManager) {
542         updateLayout();
543         setStatus(Error, QCoreApplication::translate(CONTEXT_NAME, PLUGIN_ERROR)
544                          .arg(m_plugin->name()).arg(serviceProvider->errorString()));
545         return;
546     }
547 
548     m_response = placeManager->initializeCategories();
549     if (m_response) {
550         connect(m_response, SIGNAL(finished()), this, SLOT(replyFinished()));
551     } else {
552         updateLayout();
553         setStatus(Error, QCoreApplication::translate(CONTEXT_NAME,
554                     CATEGORIES_NOT_INITIALIZED));
555     }
556 }
557 
558 /*!
559     \internal
560 */
updateLayout()561 void QDeclarativeSupportedCategoriesModel::updateLayout()
562 {
563     beginResetModel();
564     qDeleteAll(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, QPlaceCategory());
574                 m_categoriesTree.insert(QString(), node);
575                 node->declCategory = QSharedPointer<QDeclarativeCategory>
576                     (new QDeclarativeCategory(QPlaceCategory(), m_plugin, this));
577             }
578         }
579     }
580 
581     endResetModel();
582 }
583 
errorString() const584 QString 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 */
setStatus(Status status,const QString & errorString)611 void 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 
status() const621 QDeclarativeSupportedCategoriesModel::Status QDeclarativeSupportedCategoriesModel::status() const
622 {
623     return m_status;
624 }
625 
626 /*!
627     \internal
628 */
populateCategories(QPlaceManager * manager,const QPlaceCategory & parent)629 QStringList 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(parent.categoryId());
640     std::sort(categories.begin(), categories.end(), byName);
641 
642     for (const auto &category : qAsConst(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, category);
649 
650         m_categoriesTree.insert(node->declCategory->categoryId(), node);
651         childIds.append(category.categoryId());
652 
653         if (!m_hierarchical) {
654             childIds.append(populateCategories(manager,node->declCategory->category()));
655         }
656     }
657     return childIds;
658 }
659 
660 /*!
661     \internal
662 */
index(const QString & categoryId) const663 QModelIndex QDeclarativeSupportedCategoriesModel::index(const QString &categoryId) const
664 {
665     if (categoryId.isEmpty())
666         return QModelIndex();
667 
668     if (!m_categoriesTree.contains(categoryId))
669         return QModelIndex();
670 
671     PlaceCategoryNode *categoryNode = m_categoriesTree.value(categoryId);
672     if (!categoryNode)
673         return QModelIndex();
674 
675     QString parentCategoryId = categoryNode->parentId;
676 
677     PlaceCategoryNode *parentNode = m_categoriesTree.value(parentCategoryId);
678 
679     return createIndex(parentNode->childIds.indexOf(categoryId), 0, categoryNode);
680 }
681 
682 /*!
683     \internal
684 */
rowToAddChild(PlaceCategoryNode * node,const QPlaceCategory & category)685 int 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(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 
708 QT_END_NAMESPACE
709