Этот пост участвует в конкурсе „Умные телефоны за умные посты“.
![image](http://www.gaadget.ru/f/items/2/2586.jpg)
В данной статье хотелось бы поделится с Хабрасообществом своим опытом по разработке софта с использованием QtComponents'ов на примере Meego Harmattan'а. Писать мы будем редактор заметок с синхронизацией средствами Ubuntu One.
Вся разработка будет вестись при помощи scratchbox'а, он имеет некоторые преимущества в сравнении с madde, но работает исключительно в linux системах.
Среди ключевых преимуществ хочется отметить то, что сборка производится в chroot'е и в случае armel для эмуляции используется qemu. Условия максимально приближены к боевым. Это позволяет избежать дополнительной возни с настройкой кросскомпиляции. Дополнительным плюсом является наличие apt-get'а, способного установить все зависимости, необходимые для сборки, что несомненно понадобится при написании приложения сложнее, чем helloworld.
Для того, чтобы установить scratchbox нужно скачать и запустить от рута этот скрипт и в дальнейшем следовать его указаниям.
После установки необходимо перелогинится, чтобы пользователь был успешно добавлен в группу sbox.
Запускать scratchbox мы будем с помощью команды:
Если установщик правильно отработал, то должно появится приглашение примерно следующего содержания:
Если login ругается, то попробуйте выполнить скрипт run_me_first.sh, лежащий в корне scratchbox'а. Нужный таргет можно выбрать с помощью sb_menu. Остальное руководство по использованию scratchbox'а можно найти здесь.
В качестве сборщика я использую не привычный qmake, а более мощный cmake, который умеет искать зависимости, имеет кучу опций настройки и гораздо лучше подходит для кроссплатформенной разработки. В данной статье я не буду сильно углубляться в разбор системы сборки, поэтому для лучшего понимания рекомендую прочесть эту статью.
Единственный минус в том, что cmake не умеет Symbian, поэтому об этой платформе пока можно забыть или же написать вручную специальный проект для сборки именно под эту платформу. Со всеми остальными cmake справляется с легкостью, поэтому в дальнейшем я планирую портировать это приложение на настольные системы и, возможно, на Андроид или даже на iOS.
Проект состоит из некоторого количества зависимых библиотек, которые подключены при помощи git submodule к основному репозиторию, для каждой из них написан свой cmake проект. Все они лежат в каталоге 3rdparty и подключены к основному проекту, поэтому сборка идёт сразу с основными зависимостями, которых нет в репозиториях harmattan'а.
Список 3rdparty библиотек:
Помимо этого есть ещё внешние библиотеки, необходимые для сборки, но присутствующие в основных репах Harmattan'а, к ним относится qca, давайте сразу её установим, а также установим cmake:
Для того, чтобы её можно было использовать необходимо написать специальный cmake файл, который бы смог найти каталог с заголовочными файлами библиотеки и сам файл библитеки для того, чтобы с ним слинковаться.
В таком же стиле написан поиск большинства зависимостей. Для систем с pgkconfig'ом, к которым относится и Harmattan всё просто и ясно, для систем, где его нет, будем искать в каталоге $QTDIR. В случае, если cmake автоматически не нашел библиотеку, он предложит вручную задать переменные QCA2_INCLUDE_DIR QCA2_LIBRARIES. Такой подход здорово облегчает жизнь на системах, в которых отсутствует менеджер пакетов.
В cmake'е есть переменные, которые позволяют определить платформу, на которой собирается та или иная программа, например:
К сожалению, cmake ничего не знает про Harmattan, самым простым решением является запуск cmake'а с ключем -DHARMATTAN=ON. Теперь у нас определена переменная HARMATTAN, и можно писать подобные вещи:
С помощью этих же переменных можно определять, какая именно реализация GUI будет устанавливаться.
Для разработки большую часть времени будет достаточно QtSDK с harmattan quick components и ключа -DHARMATTAN при сборке. В scratchbox'е имеет смысл собирать уже более-менее конечные версии.
Сам API я решил вынести в отдельный qml модуль, который будет доступен через директиву import. Сделано это для удобства создания множества различных реализаций GUI интерфейса.
Самым сложным в процессе разработки оказалось реализовать авторизацию средствами OAuth, в процессе которой было перебрано несколько различных реализаций библиотек и на данный момент я остановился на QOauth, которая конечно не идеальна, но является вполне рабочей. На хабре есть статья с описанием этой библиотеки, поэтому сразу перейдем к решению насущных проблем. Перво-наперво нам нужно получить тот самый вожделенный token.
Дело это не хитрое, просто посылаем запрос на адрес и ждём, когда же нам прилетит запрос на basic авторизацию по https:
Как вы могли заметить, для авторизации используется стандартный для QNetworkAccessManager'а сигнал authenticationRequired, а логин и пароль я просто запоминаю обычными пропертями. Удобно и не засоряет интерфейс лишними деталями.
По завершению в reply должен прийти ответ в json формате, который содержит искомый токен и прочую важную информацию. Тут-то нам и понадобится библиотека k8json.
Следующим шагом будет отправка подтверждения того факта, что мы получили токен (обратите внимание на последнюю строчку). В результате нам должен прийти ответ ok.
Если такое слово есть в ответе, то всё, можно радостно прыгать и посылать сигнал о том, что токен наконец получен и можно начинать работать с заметками, но не тут-то было! Для совместимости с tomboy api сервер заметок требует авторизацию посредством веб браузера. Пока мне не удалось обойти эту проблему и, скрипя зубами, мне пришлось добавить в приложение webkit окошко, которое содержит кнопку «разрешить данному пользователю доступ к заметкам». Этому webkit окошку мы даем указатель на наш QNetworkAccessManager и по успешному завершению авторизации он станет обладателем заветных cookies с данными, необходимыми для авторизации.
А чтобы пользователю по новой не пришлось вбивать логин и пароль, мы заполним эти поля через DOM дерево.
Не забудем сохранить полученные кукисы, мы же хотим быть слишком навязчивыми.
Для того, чтобы сервер успешно обрабатывал наши запросы нужно, чтобы они в заголовке содержали полученный нами ранее токен. Тут нам и пригодится QOauth.
Теперь с легким сердем можно приступать к реализации tomboy api.
Для простоты работы из qml'я, я каждую заметку решил представить отдельным QObject'ом, а список заметок реализовал через QObjectListModel, реализацию которой нашел на просторах qt labs'ов. У каждой заметки свой guid, зная который можно с ней работать. Guid генерируетя на клиентской стороне, для этого в Qt есть соответствующие методы, находящиеся в классе QUuid, поэтому при конструировании новой заметки нужно сгенерировать для неё уникальный идентификатор, по которому мы будем обращаться к ней в дальнейшем.
Основные действия с заметками:
Исходя из этих действий и будем проектировать API, в модели заметок сделаем метод sync, а в самой заметке методы save, remove. Ну и конечно реализуем свойства title и content:
Неплохой идеей также было бы добавить свойство статуса заметки, который можно было бы использовать в states'ах в qml'е.
Для этого мы используем волшебный макрос Q_ENUMS, который генерирует метаинформацию для перечислений. Теперь в qml коде можно получать их численное значение и сравнивать между собой.
Удобно, читабельно и быстро. Всё-таки сравниваются числа, а не строки!
По умолчанию в QObjectListModel к элементу модели из делегата можно обращаться по имени object, но меня это не очень устраивает, поэтому я просто унаследовался от модели и поменял имя для роли ObjectRole на note.
А теперь я рассмотрю создание самого qml модуля. Для того, чтобы нашу реализацию api можно было использовать через import в qml мы должны в нашем модуле создать класс, унаследованный от QDeclarativeExtensionPlugin и реализовать в нем метод registerTypes, который бы зарегистрировал все наши методы и классы.
Вы наверное обратили внимание на assert и хотите спросить. А откуда же берётся это самое uri? А берётся оно из названия каталога, в котором лежит наш модуль. То есть Qt будет искать наш модуль в:
Но и это ещё не всё. Чтобы Qt нашла и заимпортила наш модуль нужно, чтобы в директории лежал правильно составленный файл qmldir, в котором перечислены бинарные плагины, qml и js файлы.
Основной большинства приложений на Meego является элемент PageStackWindow, который, как это не странно, являет собой стек страниц. Страницы добавляются в стек при помощи метода push, а извлекаются при помощи pop'а. Одна из страниц должна быть назначена как исходная. У каждой страницы может быть свой собственный тулбар. Можно же нескольким страницам назначать один и тот же.
Теперь давайте создадим все нужные нам страницы а также стандартный toolbar, в котором будет кнопка добавить заметку и меню с действиями:
Теперь рассмотрим что же из себя представляет отдельная страница на примере NoteListPage, реализация которой лежит в NoteListPage.qml:
В результате получится такая милая страничка:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/cc4/f5a/65b/cc4f5a65bbc83d14096c6f3048719494.png)
Для окошка логина я использовал объект Sheet, который представляет из себя страничку, выезжающую сверху. Обычно с помощью него у пользователя запрашивают какую либо информацию.
Выглядеть всё это великолепие будет вот так:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/19e/b1a/f01/19eb1af018b8a049062faddad7908fc1.png)
На страничках редактирования и about нужно реализовать кнопку назад, которая бы возращала нас к списку заметок. Для этого commonTools уже не очень подходит, нужны свои тулбары:
Чтобы у приложения появилась иконка запуска создадим знакомый всем линуксоидам .desktop файл:
Обратите внимание на секцию Exec: таким образом мы говорим, что приложение не может быть запущено несколько раз. Если мы хотим чтобы у приложения был красивый сплеш, то можно использовать утилиту invoker.
Разумеется, все картинки также необходимо не забыть установить иначе вместо красивого значка мы получим красный квадрат.
Для сборки используется стандартный dpkg-buildpackage и обычный debian, который для удобства называется debian_harmattan, а перед непосредственно сборкой выставляется симлинк debian_harmattan > debian. Секция control стандартная для debian пакетов и ее создание уже подробно было описано во многих статьях на Хабре. Рекомендую к прочтению эту серию статей.
Содержимое control файла:
Для ведения changelog'а не лишним было бы установить прогу dch из пакета devscripts. Использовать же ее очень просто:
rules файл, благодаря использованию debhelper'ов, оказался уж очень простым:
Этот же файл подойдет почти для любого проекта с минимальными изменениями.
Сборка пакета тоже тривиальна:
Теперь можно спокойно устанавливать и запускать получившийся пакет и наслаждаться быстрым и отзывчивым интерфейсом. Исходные коды для самостоятельной сборки можно скачать на гитхабе. Там же лежит собранный deb пакет. Я надеюсь эта статья поможет начинающим разработчикам под Harmattan и не только быстрее начать писать свои первые приложения. В будущем я, возможно, постараюсь получше осветить Хабрасообществу тонкости работы с cmake'ом, многие уже жаловались на недостаток статей про него.
![image](http://www.gaadget.ru/f/items/2/2586.jpg)
В данной статье хотелось бы поделится с Хабрасообществом своим опытом по разработке софта с использованием QtComponents'ов на примере Meego Harmattan'а. Писать мы будем редактор заметок с синхронизацией средствами Ubuntu One.
Вся разработка будет вестись при помощи scratchbox'а, он имеет некоторые преимущества в сравнении с madde, но работает исключительно в linux системах.
Среди ключевых преимуществ хочется отметить то, что сборка производится в chroot'е и в случае armel для эмуляции используется qemu. Условия максимально приближены к боевым. Это позволяет избежать дополнительной возни с настройкой кросскомпиляции. Дополнительным плюсом является наличие apt-get'а, способного установить все зависимости, необходимые для сборки, что несомненно понадобится при написании приложения сложнее, чем helloworld.
Установка и настройка scratchbox'а
Для того, чтобы установить scratchbox нужно скачать и запустить от рута этот скрипт и в дальнейшем следовать его указаниям.
# ./harmattan-sdk-setup.py
После установки необходимо перелогинится, чтобы пользователь был успешно добавлен в группу sbox.
Запускать scratchbox мы будем с помощью команды:
$ /scratchbox/login
Если установщик правильно отработал, то должно появится приглашение примерно следующего содержания:
[sbox-HARMATTAN_ARMEL: ~] >
Если login ругается, то попробуйте выполнить скрипт run_me_first.sh, лежащий в корне scratchbox'а. Нужный таргет можно выбрать с помощью sb_menu. Остальное руководство по использованию scratchbox'а можно найти здесь.
Создание cmake проекта
В качестве сборщика я использую не привычный qmake, а более мощный cmake, который умеет искать зависимости, имеет кучу опций настройки и гораздо лучше подходит для кроссплатформенной разработки. В данной статье я не буду сильно углубляться в разбор системы сборки, поэтому для лучшего понимания рекомендую прочесть эту статью.
Единственный минус в том, что cmake не умеет Symbian, поэтому об этой платформе пока можно забыть или же написать вручную специальный проект для сборки именно под эту платформу. Со всеми остальными cmake справляется с легкостью, поэтому в дальнейшем я планирую портировать это приложение на настольные системы и, возможно, на Андроид или даже на iOS.
Проект состоит из некоторого количества зависимых библиотек, которые подключены при помощи git submodule к основному репозиторию, для каждой из них написан свой cmake проект. Все они лежат в каталоге 3rdparty и подключены к основному проекту, поэтому сборка идёт сразу с основными зависимостями, которых нет в репозиториях harmattan'а.
Список 3rdparty библиотек:
- QOauth — реализация протокола Oauth на Qt
- k8json — очень быстрый парсер JSON
- QmlObjectModel — Класс, реализующий модель — список обьектов
Помимо этого есть ещё внешние библиотеки, необходимые для сборки, но присутствующие в основных репах Harmattan'а, к ним относится qca, давайте сразу её установим, а также установим cmake:
[sbox-HARMATTAN_ARMEL: ~] > apt-get install libqca2-dev cmake
Для того, чтобы её можно было использовать необходимо написать специальный cmake файл, который бы смог найти каталог с заголовочными файлами библиотеки и сам файл библитеки для того, чтобы с ним слинковаться.
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)
В таком же стиле написан поиск большинства зависимостей. Для систем с pgkconfig'ом, к которым относится и Harmattan всё просто и ясно, для систем, где его нет, будем искать в каталоге $QTDIR. В случае, если cmake автоматически не нашел библиотеку, он предложит вручную задать переменные QCA2_INCLUDE_DIR QCA2_LIBRARIES. Такой подход здорово облегчает жизнь на системах, в которых отсутствует менеджер пакетов.
В cmake'е есть переменные, которые позволяют определить платформу, на которой собирается та или иная программа, например:
if(WIN32)
....
elseif(APPLE)
...
elseif(LINUX)
...
endif()
К сожалению, cmake ничего не знает про Harmattan, самым простым решением является запуск cmake'а с ключем -DHARMATTAN=ON. Теперь у нас определена переменная HARMATTAN, и можно писать подобные вещи:
if(HARMATTAN)
add_definitions(-DMEEGO_EDITION_HARMATTAN) #дефайн для компилятора, без него приложение не будет разворачиваться на весь экран.
endif()
С помощью этих же переменных можно определять, какая именно реализация GUI будет устанавливаться.
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)
Для разработки большую часть времени будет достаточно QtSDK с harmattan quick components и ключа -DHARMATTAN при сборке. В scratchbox'е имеет смысл собирать уже более-менее конечные версии.
С++ плагин, реализующий Tomboy notes API
Сам API я решил вынести в отдельный qml модуль, который будет доступен через директиву import. Сделано это для удобства создания множества различных реализаций GUI интерфейса.
Самым сложным в процессе разработки оказалось реализовать авторизацию средствами OAuth, в процессе которой было перебрано несколько различных реализаций библиотек и на данный момент я остановился на QOauth, которая конечно не идеальна, но является вполне рабочей. На хабре есть статья с описанием этой библиотеки, поэтому сразу перейдем к решению насущных проблем. Перво-наперво нам нужно получить тот самый вожделенный token.
Дело это не хитрое, просто посылаем запрос на адрес и ждём, когда же нам прилетит запрос на basic авторизацию по 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());
}
Как вы могли заметить, для авторизации используется стандартный для QNetworkAccessManager'а сигнал authenticationRequired, а логин и пароль я просто запоминаю обычными пропертями. Удобно и не засоряет интерфейс лишними деталями.
По завершению в reply должен прийти ответ в json формате, который содержит искомый токен и прочую важную информацию. Тут-то нам и понадобится библиотека k8json.
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()));
Следующим шагом будет отправка подтверждения того факта, что мы получили токен (обратите внимание на последнюю строчку). В результате нам должен прийти ответ ok.
void UbuntuOneApi::onConfirmReplyFinished()
{
QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
QByteArray data = reply->readAll();
if (data.contains("ok")) {
emit hasTokenChanged();
Если такое слово есть в ответе, то всё, можно радостно прыгать и посылать сигнал о том, что токен наконец получен и можно начинать работать с заметками, но не тут-то было! Для совместимости с tomboy api сервер заметок требует авторизацию посредством веб браузера. Пока мне не удалось обойти эту проблему и, скрипя зубами, мне пришлось добавить в приложение webkit окошко, которое содержит кнопку «разрешить данному пользователю доступ к заметкам». Этому webkit окошку мы даем указатель на наш QNetworkAccessManager и по успешному завершению авторизации он станет обладателем заветных cookies с данными, необходимыми для авторизации.
А чтобы пользователю по новой не пришлось вбивать логин и пароль, мы заполним эти поля через DOM дерево.
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();
Не забудем сохранить полученные кукисы, мы же хотим быть слишком навязчивыми.
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();
}
}
Для того, чтобы сервер успешно обрабатывал наши запросы нужно, чтобы они в заголовке содержали полученный нами ранее токен. Тут нам и пригодится QOauth.
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);
}
Теперь с легким сердем можно приступать к реализации tomboy api.
Для простоты работы из qml'я, я каждую заметку решил представить отдельным QObject'ом, а список заметок реализовал через QObjectListModel, реализацию которой нашел на просторах qt labs'ов. У каждой заметки свой guid, зная который можно с ней работать. Guid генерируетя на клиентской стороне, для этого в Qt есть соответствующие методы, находящиеся в классе QUuid, поэтому при конструировании новой заметки нужно сгенерировать для неё уникальный идентификатор, по которому мы будем обращаться к ней в дальнейшем.
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();
}
Основные действия с заметками:
- Синхронизировать заметки с сервером
- Добавить новую заметку
- Обновить заметку
- Удалить заметку
Исходя из этих действий и будем проектировать API, в модели заметок сделаем метод sync, а в самой заметке методы save, remove. Ну и конечно реализуем свойства title и 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)
...
Неплохой идеей также было бы добавить свойство статуса заметки, который можно было бы использовать в states'ах в qml'е.
Q_ENUMS(Status)
public:
enum Status
{
StatusNew,
StatusActual,
StatusOutdated,
StatusSyncing,
StatusRemoral
};
Для этого мы используем волшебный макрос Q_ENUMS, который генерирует метаинформацию для перечислений. Теперь в qml коде можно получать их численное значение и сравнивать между собой.
State {
name: "syncing"
when: note.status === Note.StatusSyncing
Удобно, читабельно и быстро. Всё-таки сравниваются числа, а не строки!
По умолчанию в QObjectListModel к элементу модели из делегата можно обращаться по имени object, но меня это не очень устраивает, поэтому я просто унаследовался от модели и поменял имя для роли ObjectRole на note.
NotesModel::NotesModel(QObject *parent) :
QObjectListModel(parent)
{
QHash<int, QByteArray> roles;
roles[ObjectRole] = "note";
setRoleNames(roles);
}
А теперь я рассмотрю создание самого qml модуля. Для того, чтобы нашу реализацию api можно было использовать через import в qml мы должны в нашем модуле создать класс, унаследованный от QDeclarativeExtensionPlugin и реализовать в нем метод registerTypes, который бы зарегистрировал все наши методы и классы.
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)
Вы наверное обратили внимание на assert и хотите спросить. А откуда же берётся это самое uri? А берётся оно из названия каталога, в котором лежит наш модуль. То есть Qt будет искать наш модуль в:
$QML_IMPORTS_DIR/com/ubuntu/one/
Но и это ещё не всё. Чтобы Qt нашла и заимпортила наш модуль нужно, чтобы в директории лежал правильно составленный файл qmldir, в котором перечислены бинарные плагины, qml и js файлы.
plugin qmlbinding
Разработка qml интерфейса для Meego Harmattan
Основной большинства приложений на Meego является элемент PageStackWindow, который, как это не странно, являет собой стек страниц. Страницы добавляются в стек при помощи метода push, а извлекаются при помощи pop'а. Одна из страниц должна быть назначена как исходная. У каждой страницы может быть свой собственный тулбар. Можно же нескольким страницам назначать один и тот же.
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();
}
}
...
Теперь давайте создадим все нужные нам страницы а также стандартный toolbar, в котором будет кнопка добавить заметку и меню с действиями:
...
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();
}
}
}
}
Теперь рассмотрим что же из себя представляет отдельная страница на примере NoteListPage, реализация которой лежит в 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
}
}
В результате получится такая милая страничка:
![](https://habrastorage.org/getpro/habr/post_images/cc4/f5a/65b/cc4f5a65bbc83d14096c6f3048719494.png)
Для окошка логина я использовал объект Sheet, который представляет из себя страничку, выезжающую сверху. Обычно с помощью него у пользователя запрашивают какую либо информацию.
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")
}
Выглядеть всё это великолепие будет вот так:
![](https://habrastorage.org/getpro/habr/post_images/19e/b1a/f01/19eb1af018b8a049062faddad7908fc1.png)
На страничках редактирования и about нужно реализовать кнопку назад, которая бы возращала нас к списку заметок. Для этого commonTools уже не очень подходит, нужны свои тулбары:
ToolBarLayout {
id: aboutTools
visible: true
ToolIcon {
iconId: "toolbar-back"
onClicked: {
pageStack.pop()
}
}
}
Иконка запуска
Чтобы у приложения появилась иконка запуска создадим знакомый всем линуксоидам .desktop файл:
[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;
Обратите внимание на секцию Exec: таким образом мы говорим, что приложение не может быть запущено несколько раз. Если мы хотим чтобы у приложения был красивый сплеш, то можно использовать утилиту invoker.
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
Разумеется, все картинки также необходимо не забыть установить иначе вместо красивого значка мы получим красный квадрат.
Сборка deb пакета
Для сборки используется стандартный dpkg-buildpackage и обычный debian, который для удобства называется debian_harmattan, а перед непосредственно сборкой выставляется симлинк debian_harmattan > debian. Секция control стандартная для debian пакетов и ее создание уже подробно было описано во многих статьях на Хабре. Рекомендую к прочтению эту серию статей.
Содержимое control файла:
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.
Для ведения changelog'а не лишним было бы установить прогу dch из пакета devscripts. Использовать же ее очень просто:
$ dch - i
rules файл, благодаря использованию debhelper'ов, оказался уж очень простым:
#!/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
Этот же файл подойдет почти для любого проекта с минимальными изменениями.
Сборка пакета тоже тривиальна:
$ ln -s ./debian_harmattan ./debian
$ dpkg-buildpackage -b
Заключение
Теперь можно спокойно устанавливать и запускать получившийся пакет и наслаждаться быстрым и отзывчивым интерфейсом. Исходные коды для самостоятельной сборки можно скачать на гитхабе. Там же лежит собранный deb пакет. Я надеюсь эта статья поможет начинающим разработчикам под Harmattan и не только быстрее начать писать свои первые приложения. В будущем я, возможно, постараюсь получше осветить Хабрасообществу тонкости работы с cmake'ом, многие уже жаловались на недостаток статей про него.