This post participates in the competition
"Smart phones for smart posts".
In this article I would like to share my experience in software development using QtComponents with the example of Meego Harmattan. We will write the editor of notes with synchronization by means of Ubuntu One.
')
All development will be carried out using scratchbox, it has some advantages in comparison with madde, but it works exclusively on linux systems.
Among the key advantages, I would like to note that the build is done in chroot, and in the case of armel, qemu is used for emulation. Conditions as close as possible to the fighting. This allows you to avoid additional fuss with the cross-configuration setting. An additional advantage is the presence of apt-get, which is able to install all the dependencies necessary for the assembly, which undoubtedly will be needed when writing an application is more complicated than helloworld.
Install and configure scratchbox'a
In order to install scratchbox you need to download and run this
script from root and then follow its instructions.
# ./harmattan-sdk-setup.py
After installation, you need to re-login so that the user is successfully added to the sbox group.
We will run the scratchbox with the command:
$ /scratchbox/login
If the installer worked correctly, you should see an invitation similar to the following:
[sbox-HARMATTAN_ARMEL: ~] >
If login swears, then try running the run_me_first.sh script, which lies at the root of the scratchbox. The desired target can be selected using sb_menu. The rest of the scratchbox manual can be found
here.Creating a cmake project
As a collector, I use not the usual qmake, but the more powerful cmake, which can search for dependencies, has a lot of configuration options and is much better suited for cross-platform development. In this article I will not go deep into the analysis of the assembly system, so for a better understanding I recommend reading this
article .
The only disadvantage is that cmake does not know how to Symbian, so you can forget about this platform for now or manually write a special project to build it for this platform. With all other cmake copes with ease, so in the future I plan to port this application to the desktop systems and, possibly, to Android or even on iOS.
The project consists of a number of dependent libraries, which are connected via git submodule to the main repository, each of them has its own cmake project. All of them are in the 3rdparty directory and are connected to the main project, so the build goes right away with the main dependencies that are not in the harmattan repositories.
List of 3rdparty libraries:
- QOauth - Qt Oauth protocol implementation
- k8json is a very fast JSON parser
- QmlObjectModel - The class that implements the model - a list of objects
In addition, there are still external libraries needed for building, but present in the main Harmattan turnips, qca applies to them, let's install it right away, and install cmake:
[sbox-HARMATTAN_ARMEL: ~] > apt-get install libqca2-dev cmake
In order to be able to use it, you need to write a special cmake file that would be able to find the directory with the library header files and the library file itself in order to link with it.
include(FindLibraryWithDebug) if(QCA2_INCLUDE_DIR AND QCA2_LIBRARIES) # in cache already set(QCA2_FOUND TRUE) else(QCA2_INCLUDE_DIR AND QCA2_LIBRARIES) if(NOT WIN32) find_package(PkgConfig) pkg_check_modules(PC_QCA2 QUIET qca2) set(QCA2_DEFINITIONS ${PC_QCA2_CFLAGS_OTHER}) endif(NOT WIN32) find_library_with_debug(QCA2_LIBRARIES WIN32_DEBUG_POSTFIX d NAMES qca HINTS ${PC_QCA2_LIBDIR} ${PC_QCA2_LIBRARY_DIRS} ${QT_LIBRARY_DIR}) find_path(QCA2_INCLUDE_DIR QtCrypto HINTS ${PC_QCA2_INCLUDEDIR} ${PC_QCA2_INCLUDE_DIRS} ${QT_INCLUDE_DIR}} PATH_SUFFIXES QtCrypto) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(QCA2 DEFAULT_MSG QCA2_LIBRARIES QCA2_INCLUDE_DIR) mark_as_advanced(QCA2_INCLUDE_DIR QCA2_LIBRARIES) endif(QCA2_INCLUDE_DIR AND QCA2_LIBRARIES)
The search for most dependencies is written in the same style. For systems with pgkconfig, to which Harmattan belongs everything is simple and clear, for systems where there is none, we will search in the $ QTDIR directory. In case cmake did not automatically find the library, it will offer to manually set the variables QCA2_INCLUDE_DIR QCA2_LIBRARIES. This approach makes life easier on systems that do not have a package manager.
In cmake, there are variables that allow you to determine the platform on which this or that program is built, for example:
if(WIN32) .... elseif(APPLE) ... elseif(LINUX) ... endif()
Unfortunately, cmake knows nothing about Harmattan, the easiest solution is to run cmake with the -DHARMATTAN = ON switch. Now we have the HARMATTAN variable defined, and you can write these things:
if(HARMATTAN) add_definitions(-DMEEGO_EDITION_HARMATTAN) # , . endif()
Using the same variables, you can determine which GUI implementation will be installed.
if(HARMATTAN) set(CLIENT_TYPE meego) message(STATUS "Using meego harmattan client") else() set(CLIENT_TYPE desktop) list(APPEND QML_MODULES QtDesktop) message(STATUS "Using desktop client") endif() set(QML_DIR "${CMAKE_CURRENT_SOURCE_DIR}/qml/${CLIENT_TYPE}") ... install(DIRECTORY ${QML_DIR} DESTINATION ${SHAREDIR}/qml)
For development, QtSDK with harmattan quick components and the -DHARMATTAN switch will be sufficient for development. In scratchbox, it makes sense to build more or less final versions.
C ++ plugin that implements the Tomboy notes API
I myself decided to bring the API into a separate qml module, which will be accessible through the import directive. This is done for the convenience of creating many different implementations of the GUI interface.
The most difficult thing in the development process was to implement authorization using OAuth, during which several different library implementations were enumerated and at the moment I settled on QOauth, which is certainly not perfect, but is quite working. On
Habré there is an article with a description of this library, so we will immediately move on to solving urgent problems. First of all, we need to get the most desired token.
The point is not tricky, just send a request to the address and wait for us to receive a request for basic authorization via https:
UbuntuOneApi::UbuntuOneApi(QObject *parent) : QObject(parent), m_manager(new QNetworkAccessManager(this)), m_oauth(new QOAuth::Interface(this)) { ... connect(m_manager, SIGNAL(authenticationRequired(QNetworkReply*,QAuthenticator*)), SLOT(onAuthenticationRequired(QNetworkReply*,QAuthenticator*))); } ... void UbuntuOneApi::requestToken(const QString &email, const QString &password) { m_email = email; m_password = password; QUrl url("https://login.ubuntu.com/api/1.0/authentications"); url.addQueryItem(QLatin1String("ws.op"), QLatin1String("authenticate")); url.addQueryItem(QLatin1String("token_name"), QLatin1Literal("Ubuntu One @ ") % m_machineName); qDebug() << url.toEncoded(); QNetworkRequest request(url); QNetworkReply *reply = m_manager->get(request); reply->setProperty("email", email); reply->setProperty("password", password); connect(reply, SIGNAL(finished()), SLOT(onAuthReplyFinished())); } ... void UbuntuOneApi::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *auth) { auth->setUser(reply->property("email").toString()); auth->setPassword(reply->property("password").toString()); }
As you can see, for authentication, the QNetworkAccessManager’s standard authenticationRequired signal is used, and I just remember the login and password as usual. Convenient and does not clog the interface with unnecessary details.
Upon completion, the reply should receive a reply in json format that contains the desired token and other important information. This is where we need the k8json library.
QNetworkReply *reply = static_cast<QNetworkReply*>(sender()); QVariantMap response = Json::parse(reply->readAll()).toMap(); if (response.isEmpty()) { emit authorizationFailed(tr("Unable to recieve token")); } m_token = response.value("token").toByteArray(); m_tokenSecret = response.value("token_secret").toByteArray(); m_oauth->setConsumerKey(response.value("consumer_key").toByteArray()); m_oauth->setConsumerSecret(response.value("consumer_secret").toByteArray()); QUrl url("https://one.ubuntu.com/oauth/sso-finished-so-get-tokens/" + reply->property("email").toString()); connect(get(url), SIGNAL(finished()), SLOT(onConfirmReplyFinished()));
The next step is to send confirmation of the fact that we received the token (note the last line). As a result, we should receive an answer ok.
void UbuntuOneApi::onConfirmReplyFinished() { QNetworkReply *reply = static_cast<QNetworkReply*>(sender()); QByteArray data = reply->readAll(); if (data.contains("ok")) { emit hasTokenChanged();
If such a word is in the answer, then everything, you can happily jump and send a signal that the token has finally been received and you can start working with notes, but it was not there! For compatibility with tomboy api, the notes server requires authorization via a web browser. Until I managed to get around this problem and, gritting my teeth, I had to add a window to the webkit application, which contains a button “allow this user access to notes”. We give a pointer to our QNetworkAccessManager to this webkit window and upon successful completion of authorization he will become the owner of the cherished cookies with the data necessary for authorization.
And so that the user doesn’t have to enter a username and password with a new one, we will fill in these fields through the DOM tree.
QWebFrame *frame = page()->mainFrame(); QWebElement email = frame->findFirstElement("#id_email"); email.setAttribute("value", m_email); QWebElement pass = frame->findFirstElement("#id_password"); pass.setAttribute("value", m_password); QWebElement submit = frame->findFirstElement("#continue"); submit.setFocus();
Do not forget to save the received cookies, we want to be too intrusive.
void Notes::onWebAuthFinished(bool success) { if (success) { QNetworkCookieJar *jar = m_api->manager()->cookieJar(); QList<QNetworkCookie> cookies = jar->cookiesForUrl(m_apiRef); QSettings settings; settings.beginWriteArray("cookies", cookies.count()); for (int i = 0; i != cookies.count(); i++) { settings.setArrayIndex(i); settings.setValue("cookie", cookies.at(i).toRawForm()); } settings.endArray(); sync(); } }
In order for the server to successfully process our requests it is necessary that they contain the token received by us earlier in the header. This is where QOauth comes in handy.
QNetworkReply *UbuntuOneApi::get(const QUrl &url) { QByteArray header = m_oauth->createParametersString(url.toEncoded(), QOAuth::GET, m_token, m_tokenSecret, QOAuth::HMAC_SHA1, QOAuth::ParamMap(), QOAuth::ParseForHeaderArguments); QNetworkRequest request(url); request.setRawHeader("Authorization", header); return m_manager->get(request); }
Now with a light heart you can begin to implement
tomboy api.For ease of operation from qml, I decided to submit each note with a separate QObject, and implemented the list of notes through QObjectListModel, the implementation of which I found in qt labs open spaces. Each note has its own guid, knowing that you can work with it. Guid is generated on the client side, for this Qt has corresponding methods that are in the QUuid class, so when constructing a new note, you need to generate for it a unique identifier, by which we will refer to it later.
Note::Note(Notes *notes) : QObject(notes), m_notes(notes), m_status(StatusNew), m_isMarkedForRemoral(false) { QUuid uid = QUuid::createUuid(); m_guid = uid.toString(); m_createDate = QDateTime::currentDateTime(); }
Main actions with notes:
- Sync notes with server
- Add new note
- Update note
- Delete note
Based on these actions, we will design the API, in the note model we will make the sync method, and in the note itself, save, remove. And of course we implement the properties title and content:
class Note : public QObject { Q_OBJECT Q_PROPERTY(QString title READ title WRITE setTitle NOTIFY titleChanged) Q_PROPERTY(QString content READ content WRITE setContent NOTIFY textChanged) Q_PROPERTY(int revision READ revision NOTIFY revisionChanged) Q_PROPERTY(Status status READ status NOTIFY statusChanged) Q_PROPERTY(QDateTime createDate READ createDate NOTIFY createDateChanged) ...
A good idea would also be to add the status property of a note that could be used in states in qml.
Q_ENUMS(Status) public: enum Status { StatusNew, StatusActual, StatusOutdated, StatusSyncing, StatusRemoral };
To do this, we use the magic Q_ENUMS macro, which generates meta information for enums. Now in the qml code, you can get their numerical value and compare with each other.
State { name: "syncing" when: note.status === Note.StatusSyncing
Convenient, readable and fast. Still, numbers are compared, not strings!
By default, in the QObjectListModel, the model element from the delegate can be accessed by the object name, but this does not suit me very much, so I just inherited from the model and changed the name for the ObjectRole role to note.
NotesModel::NotesModel(QObject *parent) : QObjectListModel(parent) { QHash<int, QByteArray> roles; roles[ObjectRole] = "note"; setRoleNames(roles); }
And now I will consider the creation of the qml module itself. In order for our api implementation to be able to be used through import in qml, we need to create in our module a class inherited from QDeclarativeExtensionPlugin and implement the registerTypes method in it, which would register all of our methods and classes.
void QmlBinding::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("com.ubuntu.one")); qmlRegisterType<UbuntuOneApi>(uri, 1, 0, "Api"); qmlRegisterType<ProgressIndicatorBase>(uri, 1, 0, "ProgressIndicatorBase"); qmlRegisterUncreatableType<Notes>(uri, 1, 0, "Notes", tr("Use Api.notes property")); qmlRegisterUncreatableType<Account>(uri, 1, 0, "Account", tr("Use Api.account property")); qmlRegisterUncreatableType<Note>(uri, 1, 0, "Note", tr("")); qmlRegisterUncreatableType<NotesModel>(uri, 1, 0, "NotesModel", tr("")); } Q_EXPORT_PLUGIN2(qmlbinding, QmlBinding)
You probably noticed assert and want to ask. And where does this uri come from? And it is taken from the name of the directory in which our module lies. That is, Qt will look for our module in:
$QML_IMPORTS_DIR/com/ubuntu/one/
But that's not all. In order for Qt to find and import our module, it is necessary for the directory to contain a correctly composed qmldir file, which lists binary plugins, qml and js files.
plugin qmlbinding
Developing a qml interface for Meego Harmattan
The core of most applications on Meego is the PageStackWindow element, which, oddly enough, is a stack of pages. Pages are added to the stack using the push method, and retrieved using pop. One of the pages should be designated as the original. Each page can have its own toolbar. You can assign several pages to the same one.
import QtQuick 1.1 import com.nokia.meego 1.0 import com.ubuntu.one 1.0 // notes API PageStackWindow { id: appWindow initialPage: noteListPage Api { //, API id: api Component.onCompleted: checkToken() onHasTokenChanged: checkToken() function checkToken() { if (!hasToken) loginPage.open(); else api.notes.sync(); } } ...
Now let's create all the pages we need as well as a standard toolbar, in which there will be a button to add a note and a menu with actions:
... LoginPage { id: loginPage onAccepted: api.requestToken(email, password); } NoteListPage { id: noteListPage notes: api.notes } NoteEditPage { id: noteEditPage } AboutPage { id: aboutPage } ToolBarLayout { id: commonTools visible: true ToolIcon { iconId: "toolbar-add" onClicked: { noteEditPage.note = api.notes.create(); pageStack.push(noteEditPage); } } ToolIcon { platformIconId: "toolbar-view-menu" anchors.right: (parent === undefined)? undefined: parent.right onClicked: (menu.status === DialogStatus.Closed)? menu.open(): menu.close() } } Menu { id: menu visualParent: pageStack MenuLayout { MenuItem { text: qsTr("About") onClicked: {menu.close(); pageStack.push(aboutPage)} } MenuItem { text: qsTr("Sync") onClicked: api.notes.sync(); } MenuItem { text: api.hasToken ? qsTr("Logout") : qsTr("Login") onClicked: { if (api.hasToken) api.purge(); else loginPage.open(); } } } }
Now let's look at what a separate page is like on the example of NoteListPage, the implementation of which lies in NoteListPage.qml:
import QtQuick 1.1 import com.nokia.meego 1.0 import com.ubuntu.one 1.0 Page { id: noteListPage property QtObject notes: null tools: commonTools // , main.qml PageHeader { // . id: header text: qsTr("Notes:") } ListView { id: listView anchors.top: header.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: 11 clip: true focus: true model: notes.model delegate: ItemDelegate { title: note.title // subtitle: truncate(note.content, 32) onClicked: { noteEditPage.note = note; pageStack.push(noteEditPage); // } function truncate(str, n, suffix) { str = str.replace(/\r\n/g, ""); if (suffix === undefined) suffix = "..."; if (str.length > n) str = str.substring(0, n) + suffix; return str; } } } ScrollDecorator { flickableItem: listView } }
The result is such a cute page:

For the login window, I used the Sheet object, which is a page that leaves at the top. Usually, using it, the user is asked for some information.
import QtQuick 1.0 import com.nokia.meego 1.0 import "constants.js" as UI // - js . namespace' qml'. Sheet { id: loginPage property alias email: loginInput.text //. , loginInput.text property alias password: passwordInput.text content: Column { // sheet' anchors.topMargin: UI.MARGIN_DEFAULT anchors.horizontalCenter: parent.horizontalCenter Image { id: logo source: "images/UbuntuOneLogo.svg" } Text { id: loginTitle width: parent.width text: qsTr("Email:") font.pixelSize: UI.FONT_DEFAULT_SIZE color: UI.LIST_TITLE_COLOR } TextField { id: loginInput width: parent.width } Text { id: passwordTitle width: parent.width text: qsTr("Password:") font.pixelSize: UI.FONT_DEFAULT_SIZE color: UI.LIST_TITLE_COLOR } TextField { id: passwordInput width: parent.width echoMode: TextInput.Password } } acceptButtonText: qsTr("Login") rejectButtonText: qsTr("Cancel") }
All this magnificence will look like this:

On the edit and about pages you need to implement a back button that would return us to the list of notes. For this, commonTools is no longer very suitable, we need our own toolbars:
ToolBarLayout { id: aboutTools visible: true ToolIcon { iconId: "toolbar-back" onClicked: { pageStack.pop() } } }
Launch icon
In order for the application to have a launch icon, create a .desktop file, which is familiar to all Linux users:
[Desktop Entry] Name=ubuntuNotes Name[ru]=ubuntuNotes GenericName=ubuntuNotes GenericName[ru]=ubuntuNotes Comment=Notes editor with sync Comment[ru]= Exec=/usr/bin/single-instance /opt/ubuntunotes/bin/ubuntuNotes %U Icon=/usr/share/icons/hicolor/80x80/apps/ubuntuNotes.png StartupNotify=true Terminal=false Type=Application Categories=Network;Qt;
Notice the Exec section: this way we say that the application cannot be started several times. If we want the application to have a beautiful splash, then we can use the invoker utility.
Exec=/usr/bin/invoker --splash=/usr/share/apps/qutim/declarative/meego/qutim-portrait.png --splash-landscape=/usr/share/apps/qutim/declarative/meego/qutim-landscape.png --type=e /usr/bin/qutim
Of course, all the pictures also need not forget to install otherwise, instead of a beautiful icon, we get a red square.
Build deb package
The standard dpkg-buildpackage and the usual debian, which is called debian_harmattan for convenience, are used for the build, and the simlink debian_harmattan> debian is set before the build itself. The control section is standard for debian packages and its creation has already been described in detail in many articles on Habré. I recommend reading
this series of articles.
Contents of the control file:
Source: ubuntunotes Section: user/network Priority: extra Maintainer: Aleksey Sidorov <gorthauer87@ya.ru> Build-Depends: debhelper (>= 5),locales,cmake, libgconf2-6,libssl-dev,libxext-dev,libqt4-dev,libqca2-dev,libqca2-plugin-ossl, libqtm-dev Standards-Version: 3.7.2 Package: ubuntunotes Section: user/network Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends},libqca2-plugin-ossl Description: TODO XSBC-Maemo-Display-Name: ubuntuNotes XSBC-Bugtracker: https://github.com/gorthauer/ubuntu-one-qml Package: ubuntunotes-dbg Section: debug Priority: extra Architecture: any Depends: ${misc:Depends}, qutim (= ${binary:Version}) Description: Debug symbols for ubuntuNotes Debug symbols to provide extra debug info in case of crash.
To keep a changelog, it would not be superfluous to install the dch program from the devscripts package. Using it is very simple:
$ dch - i
The rules file, due to the use of debhelper, turned out to be very simple:
#!/usr/bin/make -f %: dh $@ override_dh_auto_configure: dh_auto_configure -- -DCMAKE_INSTALL_PREFIX=/opt/ubuntunotes -DHARMATTAN=ON override_dh_auto_install: dh_auto_install --destdir=$(CURDIR)/debian/ubuntunotes
The same file is suitable for almost any project with minimal changes.
Build a package is also trivial:
$ ln -s ./debian_harmattan ./debian $ dpkg-buildpackage -b
Conclusion
Now you can safely install and run the resulting package and enjoy a fast and responsive interface. Source codes for self-assembly can be downloaded on
github . In the same place the collected
deb packet lies. I hope this article will help novice developers under Harmattan and not only start writing their first applications faster. In the future, I may try to better clarify to the Habrasoobschestvu the subtleties of working with cmake, many have already complained about the lack of articles about him.