Qt-based BB10 API Examples Documentation

Contents

Filtered DataModel Example

Files:

Description

The Filtered DataModel example allows the user to expand and collaps the header items in a ListView.

Overview

In this example we'll learn how to modify the structural appearance of a DataModel in a ListView by placing a filter model inbetween them. We will use the VegetablesDataModel from the vegetablesdatamodel example as source model and put a custom model FilteredDataModel on top of it to show all the top-level items but only the child items of the currently selected top-level item.

The UI

The UI of this sample application consists of a ListView that simply shows the content of our custom model.

We create the filter model in C++ and keep an object of it inside the App class.

    ListView {
        horizontalAlignment: HorizontalAlignment.Center

        dataModel: _model

The ListView uses the exported FilteredDataModel object as its dataModel.

    onTriggered: {
        clearSelection()
        select(indexPath)
    }

    onSelectionChanged: {
        _app.selectionChanged(indexPath, selected)
    }

Whenever the user clicks on an item, we update the selection in the onTriggered signal handler and invoke the selectionChanged() slot of the App object inside the onSelectionChanged signal handler.

    ListItemComponent {
        type: "header"
        Label {
            text: (ListItemData.expanded ? "\u25BC " : "\u25B6 ") + ListItemData.data
            textStyle {
                base: SystemDefaults.TextStyles.BigText
                color: Color.Black
                fontWeight: FontWeight.Bold
            }
        }
    }

Since the standard Header control, that is used by the ListView for header items, is too small, we declare our own ListItemComponent for it. The text inside the header is prefixed with a right or down arrow (used the Unicode symbols here), depending on the 'expanded' property of this header item. The 'expanded' property does not come from the source model but is injected by the FilteredDataModel.

The App class

App is the central class of the application that creates the UI and provides a public slot to mark one of the top-level items as selected.

    class App : public QObject
    {
        Q_OBJECT

    public:
        App(QObject *parent = 0);

    public Q_SLOTS:
        void selectionChanged(const QVariantList &indexPath, bool selected);

    private:
        FilteredDataModel *m_model;
    };

Inside the constructor of App we create an instance of VegetablesDataModel, which acts as the source model, and an instance of FilteredDataModel, which takes the source model as parameter in its constructor.

Afterwards we load the UI from the main.qml file and export the FilteredDataModel object under the name '_model' to QML and the App object as '_app'. The latter is needed to be able to invoke the selectionChanged() slot from within the QML document.

    App::App(QObject *parent)
        : QObject(parent)
        , m_model(0)
    {
        // Create the source data model
        VegetablesDataModel *vegetablesModel = new VegetablesDataModel(this);

        // Create the filtered data model and pass the source model
        m_model = new FilteredDataModel(vegetablesModel, this);

        QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
        qml->setContextProperty("_model", m_model);
        qml->setContextProperty("_app", this);

        AbstractPane* root = qml->createRootObject<AbstractPane>();
        Application::instance()->setScene(root);
    }

Whenever the user selects a top-level item in the ListView, the selectionChanged() slot of the App object is invoked. Inside this slot we first check that the input parameters are valid. In the following steps we extract the index of the selected item and call the expandHeader() method on the FilteredDataModel object to toggle the expansion state of the item.

    void App::selectionChanged(const QVariantList &indexPath, bool selected)
    {
        if (indexPath.size() != 1 || !selected)
            return; // Not interested

        // Selected a header item!
        const int selection = indexPath[0].toInt();

        // Toggle the expanded state of the selected header
        m_model->expandHeader(selection, !m_model->isHeaderExpanded(selection));
    }

The FilteredDataModel class

Like any other data model in Cascades, the FilteredDataModel also must inherit from the abstract class bb::cascades::DataModel.

    class FilteredDataModel : public bb::cascades::DataModel
    {
    public:
        FilteredDataModel(bb::cascades::DataModel *sourceModel, QObject *parent = 0);

        // Required interface implementation
        virtual int childCount(const QVariantList& indexPath);
        virtual bool hasChildren(const QVariantList& indexPath);
        virtual QVariant data(const QVariantList& indexPath);
        virtual QString itemType(const QVariantList& indexPath);

        void expandHeader(int headerIndex, bool selected);
        bool isHeaderExpanded(int headerIndex) const;

    private:
        bool isFiltered(const QVariantList& indexPath) const;
        void setExpandedHeader(int headerIndex);

    private:
        bb::cascades::DataModel* m_sourceDataModel;
        int m_expandedIndex;  // currently expanded header by index, or -1 if none expanded
    };

It reimplements all the abstract methods and additionally provides methods to toggle the expansion state of header items. It contains two member variables, one is the pointer to its source model and the other the index of the currently expanded header item.

    FilteredDataModel::FilteredDataModel(bb::cascades::DataModel *sourceModel, QObject *parent)
        : bb::cascades::DataModel(parent)
        , m_sourceDataModel(sourceModel)
        , m_expandedIndex(-1) // no header expanded
    {
    }

Inside the constructor the member variables are initialized.

    int FilteredDataModel::childCount(const QVariantList& indexPath)
    {
        if (isFiltered(indexPath)) {
            // Unexpanded header
            return 0;
        }

        return m_sourceDataModel->childCount(indexPath); // pointer always initialized
    }

Inside the childCount() method we test whether the passed 'indexPath' belongs to a header item that is not expanded. If that's the case we return '0', so the ListView won't show any child items underneath this header item. In all other cases, we simply forward the call to the source model.

    bool FilteredDataModel::hasChildren(const QVariantList& indexPath)
    {
        if (isFiltered(indexPath)) {
            // Unexpanded header
            return false;
        }

        return m_sourceDataModel->hasChildren(indexPath); // pointer always initialized
    }

Inside the hasChildren() method we test again whether the passed 'indexPath' belongs to a header item that is not expanded. If that's the case we return 'false', so the ListView won't show any child items underneath this header item. In all other cases, we simply forward the call to the source model.

    QVariant FilteredDataModel::data(const QVariantList& indexPath)
    {
        if (indexPath.size() == 1) { // header item
            // Enrich the original data of the source model with additional data about expanded state
            QVariantMap data;
            data["data"] = m_sourceDataModel->data(indexPath);
            data["expanded"] = (indexPath[0] == m_expandedIndex);

            return data;
        } else {
            // Pass through the data from the source model
            return m_sourceDataModel->data(indexPath);
        }
    }

Inside the data() method we test again whether the data for a header item or a normal item are requested. For a header item we want to enrich the original data (the color of the vegetables as string) with the current expansion state. We use a QVariantMap for this purpose, add an "data" entry with the original data from the source model and an "expanded" entry with a boolean value.

If the data of a normal item have been requested, we simply forward the call to the source model.

    QString FilteredDataModel::itemType(const QVariantList& indexPath)
    {
        return m_sourceDataModel->itemType(indexPath); // pointer always initialized
    }

Since the do not reorder any indexes in our filter model, we can forward all calls of itemType() to the source model.

    void FilteredDataModel::expandHeader(int headerIndex, bool expand)
    {
        if (!expand) {
            if (headerIndex == m_expandedIndex) {
                // Collapse the header
                setExpandedHeader(-1);
            }
        } else {
            setExpandedHeader(headerIndex);
        }
    }

The expandHeader() method is called whenever the user selects a top-level item in the ListView. We first check whether the selected item should be expanded or collapsed. If it should be collapsed and the currently expanded item is the current one, we change set the index to '-1', which means none of the top-level items is expanded. In all other cases we set the passed index as the new expanded header.

    bool FilteredDataModel::isHeaderExpanded(int headerIndex) const
    {
        return (headerIndex == m_expandedIndex);
    }

The isHeaderExpanded() method is just a helper method to check whether the passed index is the one that is marked as expanded.

    void FilteredDataModel::setExpandedHeader(int index)
    {
        if (m_expandedIndex != index) {
            // Only emit if we actually make a change
            m_expandedIndex = index;
            emit itemsChanged(bb::cascades::DataModelChangeType::AddRemove);
        }
    }

The setExpandedHeader() method changes the member variable to new expanded index and emits the itemsChanged() signal that is defined in the bb::cascades::DataModel class. Whenever this signal is emitted, the ListView will reread all the structural information from the model and adapt its UI.

    bool FilteredDataModel::isFiltered(const QVariantList& indexPath) const
    {
        return indexPath.size() == 1 &&
               indexPath[0].toInt() != m_expandedIndex;
    }