Tuesday, July 19, 2011

Populating SelectionDialog from C++ backend

I have what I would imagine a pretty common need to create a data-set in C++ backend, and use that data to populate the values that can be selected in QML's SelectionDialog.

Since SelectionDialog seems to follow the QML Data model pattern, and it has its own delegate defined that handles the data for the platform, I thought it would be as easy as this:

PageStackWindow {
    id: appWindow

    initialPage: mainPage

    MainPage{id: mainPage}

    ToolBarLayout {
        id: commonTools
        visible: true
        ToolIcon { platformIconId: "toolbar-view-menu";
             anchors.right: parent===undefined ? undefined : parent.right
             onClicked: (myMenu.status == DialogStatus.Closed) ? myMenu.open() : myMenu.close()
        }
    }

    Menu {
        id: myMenu
        visualParent: pageStack
        MenuLayout {
            MenuItem { text: "Sample menu item" }
        }
    }
}

MainPage.qml:

Page {
    id: mainPage
    tools: commonTools

    SelectionDialog {
        id: selectionDialog
        titleText: "Select item"

        model: selectionModel
    }

    Button {
        text: "Select"

        onClicked: selectionDialog.open()
    }
}

main.cpp:

#include <QtGui/QApplication>
#include <QtDeclarative>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList dataList;
    dataList.append("Item 1");
    dataList.append("Item 2");
    dataList.append("Item 3");
    dataList.append("Item 4");
    dataList.append("Item 5");

    QStringListModel dataListModel(dataList);

    QDeclarativeView view;
    QDeclarativeContext *ctxt = view.rootContext();
    ctxt->setContextProperty("selectionModel", &dataListModel);
    view.setSource(QUrl("qrc:/qml/main.qml"));
    view.showFullScreen();

    return app.exec();
}

In these code samples, only MainPage.qml and main.cpp are interesting. I create a list of items I want to show in C++, assign them to a model so that it can be shown in QML, register the model as id so that it's available in QML and then load and show the QML. When pressing the "Select" button, the items should be shown.

But they don't show, instead I got an empty list. And there is at least two separate reasons why the items are not shown:

  • Qt's models have "roles" which are used to access data in the model. SelectionDialog tries to display data from roles that are not set by QStringListModel. SelectionDialog uses data from role "name", but QStringListModel gives access to the data in the role "display".
  • SelectionDialog's model property does not accept just any model, it expects QML ListModel type. QStringListModel can not be assigned to the QML property, trying to assign it will give an error.

If you want more information and discussion on those issues, see a posting on MeeGo Forum.

These issues, especially the second one, may be temporary problems that will be fixed in future. But for now, I'll show the cleanest way that I could figure out to handle this.

For mixed C++/QML programs I think the best practice is to have the C++ part of the code to be common as much as possible across platforms, and the QML part will be what is customized for each platform as necessary. So the C++ code I represent here hopefully works in MeeGo/Harmattan as well as in Symbian. For the QML part I will only represent the Harmattan part, as that's what I'm interested in and that's what I can test.

In a nut shell, I create in QML an empty ListModel, and signal from C++ to QML side to add elements to the ListModel using JavaScript. In the C++ code I create a wrapper for QStringListModel that sets the displayed role to "name" so that Meego Harmattan and Symbian Qt Component delegates can use it. In Symbian platforms, there should be no need for the "Connections" block in the QML, as you should be able to just assign the "selectionModel" to SelectionDialog's model directly.

There is one more caveat on Meego 1.2 Harmattan API. You need to set SelectionDialog's selectedIndex property to some value. If you do not set the selected index, initially the dialog does not show the first item. So for now displaying a dialog with this code is not possible without having a default selection.

I'll represent the code here, included with comments on the parts that need explaining. If you've figured a cleaner way to do this please post in the comments.

main.cpp:

#include <QtGui/QApplication>
#include <QtDeclarative>

#include "qstringlistmodelforqtcomponent.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QStringList dataList;
    QStringListModelForQtComponent dataListModel;

    dataList.append("Item 1");
    dataList.append("Item 2");
    dataList.append("Item 3");
    dataList.append("Item 4");
    dataList.append("Item 5");

    QDeclarativeView view;
    QDeclarativeContext *ctxt = view.rootContext();
    ctxt->setContextProperty("selectionModel", &dataListModel);
    view.setSource(QUrl("qrc:/qml/main.qml"));

    // The displayed elements must be set after the QML has been
    // loaded, as on MeeGo/Harmattan adding the elements to 
    // lists is done by emitting signals. The signal handlers 
    // are not instantiated before the QML has been loaded.
    // Doing it this way should also work on Symbian  
    // Qt Components and the stock ListView because QML models 
    // are updated on the fly if they are changed.
    dataListModel.setStringList(dataList);

    view.showFullScreen();

    dataListModel.setStringList(dataList);

    return app.exec();
}
qstringlistmodelforqtcomponent.h:
#ifndef Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H
#define Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H

#include <QObject>
#include <QStringListModel>

class QStringListModelForQtComponent : public QStringListModel
{
    Q_OBJECT

public:
    // Contrary to normal QStringListModel, this class does not
    // allow giving the strings in the model in constructor. 
    // The reason is that for this model to work also on 
    // MeeGo/Harmattan, the strings must be set after the
    // QML template has been loaded. Otherwise the signal 
    // handlers in QML have not been registered. The default 
    // role name used to display the roles is "name", which is 
    // used both by Symbian and Harmattan Qt Compontents.
    QStringListModelForQtComponent(const QByteArray &displayRoleName = "name", QObject *parent = 0);

    // Overrides QStringListModel's function. Setting this will
    // emit stringAdded(QString) for each string in the list. If
    // there was non empty string list set to this model before,
    // stringsReset() signal is emitted before 
    // stringAdded(QString) signals.
    void setStringList(const QStringList &strings);

signals:
    void stringAdded(const QString &newString);
    // Emitted when setStringList is called if already contained strings
    void stringsReset();
};

#endif // Q_STRING_LIST_MODEL_FOR_QT_COMPONENT_H

qstringlistmodelforqtcomponent.cpp:
#include "qstringlistmodelforqtcomponent.h"

QStringListModelForQtComponent::QStringListModelForQtComponent(const QByteArray &displayRoleName, QObject *parent)
    : QStringListModel(parent)
{
    QHash<int, QByteArray> roleNames;
    roleNames.insert(Qt::DisplayRole, displayRoleName);
    setRoleNames(roleNames);
}

void
QStringListModelForQtComponent::setStringList(const QStringList &strings)
{
    if (!stringList().isEmpty())
    {
        emit stringsReset();
    }
    QStringListModel::setStringList(strings);
    QStringList::const_iterator i = strings.constBegin();
    for (; i != strings.constEnd(); ++i) {
        QString s = *i;
        emit stringAdded(s);
    }
}
MainPage.qml:
import QtQuick 1.1
import com.meego 1.0

Page {
    id: mainPage
    tools: commonTools

    SelectionDialog {
        id: selectionDialog
        titleText: "Select item"
        selectedIndex: 0

        model: ListModel {
            // Populated from C++ backend
        }
    }

    Connections {
        target: selectionModel
        // Only onStringAdded signal is needed, since the C++ backend
        // needs only to initialize the strings once.
        onStringAdded: {
            selectionDialog.model.append({name: newString})
        }
    }
}

Tuesday, July 12, 2011

Setting up Qt SDK for Meego Harmattan development on Ubuntu 10.04

Meego Harmattan support in the current Qt SDK release 1.1.2 is experimental. That means there can be some quirks have to worked around. I expect these quirks to be fixed in up-coming SDK releases, but I'll list here for now the steps I had to do to make the development environment suite my preliminary needs:
  1. Running the application in Harmattan emulator (Qemu)
  2. Running the application in desktop environment (for faster testing)
First thing that had to be done when installing Qt SDK is selecting custom installation and tick the Experimental / Harmattan module. That can be done later also from Help / Start Updater.

When creating a new project I chose Harmattan target. It creates a "Hello world" stub that can be run on Harmattan emulator. Compiling that worked well, but when trying to run it from Qt I got an error:

Deployment failed: Qemu was not running. It has now been started up for you, but it will take a bit of time until it is ready
But nothing happens, Qemu is not started. Turns out that by default Qt Creator uses wrong build configuration for the Harmattan target. After creating Harmattan project, it is "Harmattan Platform API". When I changed it to "Meego 1.2 Harmattan API" the Harmattan emulator started up when running the application.

Qemu is very slow however. It's probably ok for doing final testing, but during development I want faster way to test the software and its UI. So it is helpful to be able to compile and run the application as native desktop application. Unfortunately, Harmattan uses QtQuick 1.1 which comes as part of Qt 4.7.4, which hasn't been released yet. And as it's not released, the SDK does not contain it for desktop builds. Instead it contains QtQuick 1.0 and Qt 4.7.3.

The situation should be much easier when Qt SDK with Qt 4.7.4 is released. For now though, I thought it is best to compile Qt 4.7.4 to use in the SDK for development. Note: you might be better off just downloading and installing QtCreator snapshot version, if it contains Qt 4.7.4 you'll save lots of time and trial-and-error. I didn't think this thoroughly and went the way of compiling Qt myself, so I don't know if the snapshot version of QtCreator includes Qt 4.7.4. If it does, you could also just copy Qt 4.7.4 from there to your stable Qt SDK installation, thus avoiding possible instabilities in the QtCreator snapshot.

Compiling Qt snapshot

First the source code is needed. The project is hosted in gitorious, these instructions are directly from the official documentation. First create a directory where the sources should be fetched, go there, and in the terminal do:
git config --global core.autocrlf input
git clone git://gitorious.org/qt/qt.git
Qt needs several libraries' development versions installed, or some modules are not compiled and the result won't work for applications using Harmattan UI. In my system the only thing I needed was dbus development libraries:
sudo apt-get install libdbus-1-dev
I also had a problem with the GL libraries, which is due to some package management problem if NVidia drivers are installed. In my system /usr/lib/libGL.so pointed to non-existing mesa version of the library, which will cause Qt's OpenGL library not to be built. It could be corrected by simply linking it to point to NVidia's GL implementation.

Then I configured the compilation so that it will install into QtSDK:

./configure -v -prefix [path to QtSDK installation directory]/Desktop/Qt/474/gcc -opengl desktop
Compile and install:
make install
The compilation takes quite some time.

Installing Qt Components

The following is based on Kate Alhola's helpful blog post.

Before Harmattan applications can be run on desktop, the Qt components Harmattan uses need to be installed. Forum Nokia PPA has the needed package qt-components. Since I want to keep my OS environment "pure" and only need these under QtCreator, I didn't do the usual apt-get install qt-components. Instead, I downloaded the package, used "dpkg -x" to extract it and then copied the relevant files to SDK (run this in the directory where you extracted the package):

cp -r usr/lib/qt4/imports [path to QtSDK installation directory]/Desktop/Qt/474/gcc/imports

Installing Harmattan theme

To use the Qt Compontents on desktop, you will also need the theme installed. To do this, I copied the theme straight from the Harmattan emulator. Start Harmattan emulator, for example by running the "Hello world" application created by new project wizard. When the emulator is up and running, run the following command in your host terminal (not in Harmattan emulator):
sudo scp -r -P 6666 'developer@localhost:/usr/share/themes/*' /usr/share/themes/

Running Harmattan application from QtCreator in desktop

First I had to tell QtCreator about the installed Qt 4.7.4 version. This can be done from Tools / Options / Qt4. In the Qt4 Versions tab, click Add. In the version name I put "Desktop Qt 4.7.4 for GCC (Qt SDK)" and pointed qmake location to the new installation at [path to QtSDK installation directory]/Desktop/Qt/474/gcc/bin/qmake

Now opening the Harmattan "Hello World" project that was created using new project wizard, I added a Desktop build and selected it to use Qt version 4.7.4.

After these steps I could run the "Hello World" in desktop environment and the application looked just like in Harmattan emulator.