Tuesday, July 26, 2011

Changing and accessing profiles programmatically in Harmattan

Qt's mobility APIs have currently quite restricted support for any profile based operations. For example, you can not change the current active profile. Luckily you can do so by using D-Bus, as Harmattan uses as a backend MeeGo's profile daemon.

In this posting I'll show the code to do the following:

  • Iterating all available profiles
  • Setting the used active profile
  • Catching a notification when the active profile changes

A little background information first. Harmattan's profiles that you can see (Silent, Beep, Ringing), are not really profiles. Instead they match existing profiles (silent, meeting, general) like this:

Harmattan profile type Real profile name
Silent silent
Beep meeting
Ringing general

The profile backend specifies all profiles, and each profile contains properties as key/value pairs. The Harmattan profile types are defined in each profile type under a string key of "ringing.alert.type". So when selecting "Beep" profile from N9/N950, in the background really the "Silent" profile is selected because that profile has as a property "ringing.alert.type=Beep". What this means in practice is best shown with code.

For easier comprehension, these code samples are minimal and lack any error checking (ie. they assume the D-Bus queries always succeed).

Common defines

Here's some common defines used in the code samples. They are for using the D-Bus invocations
#define PROFILED_SERVICE "com.nokia.profiled"
#define PROFILED_PATH "/com/nokia/profiled"
#define PROFILED_INTERFACE "com.nokia.profiled"
#define PROFILED_GET_PROFILES "get_profiles"
#define PROFILED_SET_PROFILE "set_profile"
#define PROFILED_GET_VALUE "get_value"
#define PROFILED_PROFILE_CHANGED "profile_changed"
// The key for accessing Harmattan's profile type under profile
#define PROFILED_TYPE_VALUE "ringing.alert.type"

Iterating all known profiles

This piece of code can be used to iterate all known profiles. These are not the Harmattan profile types that you want to show if targetting N9/N950, but it is something that is anyway needed when constructing the Harmattan profile types list:
QStringList
ProfileClient::profiles() const
{
    QDBusInterface dbus_iface(PROFILED_SERVICE, PROFILED_PATH,
                              PROFILED_INTERFACE);

    QDBusReply<QStringList> reply = 
        dbus_iface.call(PROFILED_GET_PROFILES);
    return reply.value();
}
Here's the function that should be used with Harmattan if you want to show the same values as in Harmattan UI. This function basically iterates through the profiles and takes from each profile the property that defines the Harmattan profile type (Silent, Beep or Ringing):
QStringList
ProfileClient::profileTypes() const
{
    QDBusInterface dbus_iface(PROFILED_SERVICE, PROFILED_PATH,
                              PROFILED_INTERFACE);

    QStringList profileTypes;
    QString profile;
    foreach (profile, profiles())
    {
        QDBusReply<QString> reply = 
            dbus_iface.call(PROFILED_GET_VALUE, profile, 
                            PROFILED_TYPE_VALUE);
        profileTypes.append(reply.value());
    }

    // In Harmattan at least, profile type Ringing is 
    // attached to  "general" and "outdoors" profiles. 
    // The "general" profile is the one that's used for
    // "ringing", profile "outdoors" should not be used
    // when setting a profile.
    profileTypes.removeDuplicates();
    return profileTypes;
}

Setting the used active profile

Here's the code to set the active profile to the named one. Note that this function requires the real profile (silent, meeting, general). In Harmattan you probably want to set the profile by Harmattan's profile type. Since it's an easy mapping from Harmattan profile type to the real profile name, I'll leave that detail as a fun little exercise for anyone interested.
bool
ProfileClient::setProfile(QString profileName)
{
    QDBusInterface dbus_iface(PROFILED_SERVICE, PROFILED_PATH,
                              PROFILED_INTERFACE);

    // Returns true if success
    QDBusReply<bool> reply = 
        dbus_iface.call(PROFILED_SET_PROFILE, profileName);
    return reply.value();
}

Catching a notification when the active profile changes

If there's a need to do something when active profile changes, you can attach a signal handler to using the following code sample. This catches changes done either by your program or by anything else that uses the profile backend (like changing it from N9/N950 menu)
ProfileClient::ProfileClient(QObject *parent) : QObject(parent)
{
   QDBusConnection::sessionBus().connect("", "",
       PROFILED_INTERFACE, 
       PROFILED_PROFILE_CHANGED,
       this, 
       SLOT(profileChanged(bool, bool, QString))))
}

void
ProfileClient::profileChanged(bool changed, bool active, QString profile)
{
    if (changed && active) {
        qDebug("Profile changed to %s", qPrintable(profile));
    }
}

Further reading

That's the basics for getting started, but if you want more look for hints here:

Profile backend's configuration files

See in the device (or in QEMU emulator) the files stored under /etc/profiled. These are the configuration files, and there you can see under each profile the properties that they define and that you can access like I did above with the Harmattan profile type.

Profile backend's D-Bus API

MeeGo's profile backend that Harmattan uses is open source (luckily, or I would not have been able to figure out these things), and you can see other D-Bus functions that are available for use here: MeeGo profile_dbus.h

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.