From f40514fe681f5163deb5f579140ef4f7ac77f5a8 Mon Sep 17 00:00:00 2001 From: Javier Date: Sun, 14 Dec 2014 03:26:46 +0100 Subject: add icons to the slots managament UI --- app/app.pro | 8 ++- app/pebble.cpp | 12 +++- app/pebbleappiconprovider.cpp | 28 ++++++++ app/pebbleappiconprovider.h | 18 +++++ app/pebbledinterface.cpp | 18 ++++- app/pebbledinterface.h | 6 +- app/qml/pages/AppConfigPage.qml | 14 +++- app/qml/pages/InstallAppDialog.qml | 17 +++-- app/qml/pages/WatchPage.qml | 49 ++++++++++--- app/qml/pebble.qml | 4 -- daemon/appinfo.cpp | 25 ++++++- daemon/appinfo.h | 9 ++- daemon/appmanager.cpp | 142 ++++++++++++++++++++++++++++++++++--- daemon/appmanager.h | 2 + daemon/manager.cpp | 5 ++ 15 files changed, 315 insertions(+), 42 deletions(-) create mode 100644 app/pebbleappiconprovider.cpp create mode 100644 app/pebbleappiconprovider.h diff --git a/app/app.pro b/app/app.pro index d375800..48fcf68 100644 --- a/app/app.pro +++ b/app/app.pro @@ -3,16 +3,18 @@ TARGET = pebble CONFIG += sailfishapp QT += dbus -QMAKE_CXXFLAGS += -std=c++0x +CONFIG += c++11 DEFINES += APP_VERSION=\\\"$$VERSION\\\" SOURCES += \ pebble.cpp \ - pebbledinterface.cpp + pebbledinterface.cpp \ + pebbleappiconprovider.cpp HEADERS += \ - pebbledinterface.h + pebbledinterface.h \ + pebbleappiconprovider.h DBUS_INTERFACES += ../org.pebbled.Watch.xml diff --git a/app/pebble.cpp b/app/pebble.cpp index 44f1aeb..41da080 100644 --- a/app/pebble.cpp +++ b/app/pebble.cpp @@ -33,16 +33,22 @@ #include #include "pebbledinterface.h" +#include "pebbleappiconprovider.h" int main(int argc, char *argv[]) { - // Register Pebble daemon interface object on QML side - qmlRegisterType("org.pebbled", 0, 1, "PebbledInterface"); - QScopedPointer app(SailfishApp::application(argc, argv)); + qmlRegisterUncreatableType("org.pebbled", 0, 1, "PebbledInterface", + "Please use pebbled context property"); + QScopedPointer view(SailfishApp::createView()); + QScopedPointer pebbled(new PebbledInterface); + QScopedPointer appicons(new PebbleAppIconProvider(pebbled.data())); + view->rootContext()->setContextProperty("APP_VERSION", APP_VERSION); + view->rootContext()->setContextProperty("pebbled", pebbled.data()); + view->engine()->addImageProvider("pebble-app-icon", appicons.data()); view->setSource(SailfishApp::pathTo("qml/pebble.qml")); view->show(); diff --git a/app/pebbleappiconprovider.cpp b/app/pebbleappiconprovider.cpp new file mode 100644 index 0000000..0e694ff --- /dev/null +++ b/app/pebbleappiconprovider.cpp @@ -0,0 +1,28 @@ +#include +#include +#include "pebbleappiconprovider.h" + +PebbleAppIconProvider::PebbleAppIconProvider(PebbledInterface *interface) + : QQuickImageProvider(QQmlImageProviderBase::Image), pebbled(interface) +{ +} + +QImage PebbleAppIconProvider::requestImage(const QString &id, QSize *size, const QSize &requestedSize) +{ + QUuid uuid(QUrl::fromPercentEncoding(id.toLatin1())); + QImage img = pebbled->menuIconForApp(uuid); + + if (requestedSize.width() > 0 && requestedSize.height() > 0) { + img = img.scaled(requestedSize, Qt::KeepAspectRatio); + } else if (requestedSize.width() > 0) { + img = img.scaledToWidth(requestedSize.width()); + } else if (requestedSize.height() > 0) { + img = img.scaledToHeight(requestedSize.height()); + } + + if (size) { + *size = img.size(); + } + + return img; +} diff --git a/app/pebbleappiconprovider.h b/app/pebbleappiconprovider.h new file mode 100644 index 0000000..c76641a --- /dev/null +++ b/app/pebbleappiconprovider.h @@ -0,0 +1,18 @@ +#ifndef PEBBLEAPPICONPROVIDER_H +#define PEBBLEAPPICONPROVIDER_H + +#include +#include "pebbledinterface.h" + +class PebbleAppIconProvider : public QQuickImageProvider +{ +public: + explicit PebbleAppIconProvider(PebbledInterface *interface); + + QImage requestImage(const QString &id, QSize *size, const QSize &requestedSize); + +private: + PebbledInterface *pebbled; +}; + +#endif // PEBBLEAPPICONPROVIDER_H diff --git a/app/pebbledinterface.cpp b/app/pebbledinterface.cpp index 588e24a..c978dd0 100644 --- a/app/pebbledinterface.cpp +++ b/app/pebbledinterface.cpp @@ -183,7 +183,7 @@ QUrl PebbledInterface::configureApp(const QString &uuid) } } -bool PebbledInterface::isAppInstalled(const QString &uuid) +bool PebbledInterface::isAppInstalled(const QString &uuid) const { QUuid u(uuid); @@ -196,6 +196,11 @@ bool PebbledInterface::isAppInstalled(const QString &uuid) return false; } +QImage PebbledInterface::menuIconForApp(const QUuid &uuid) const +{ + return _appMenuIcons.value(uuid); +} + void PebbledInterface::setAppConfiguration(const QString &uuid, const QString &data) { qDebug() << Q_FUNC_INFO << uuid << data; @@ -210,8 +215,11 @@ void PebbledInterface::launchApp(const QString &uuid) // TODO Terrible hack; need to give time for the watch to open the app // A better solution would be to wait until AppUuidChanged is generated. + QUuid u(uuid); + if (u.isNull()) return; int sleep_count = 0; - while (watch->appUuid() != uuid && sleep_count < 5) { + while (QUuid(watch->appUuid()) != u && sleep_count < 5) { + qDebug() << "Waiting for" << u.toString() << "to launch"; QThread::sleep(1); sleep_count++; } @@ -271,6 +279,7 @@ void PebbledInterface::refreshAllApps() { _apps.clear(); _appsByUuid.clear(); + _appMenuIcons.clear(); qDebug() << "refreshing all apps list"; @@ -288,6 +297,11 @@ void PebbledInterface::refreshAllApps() m.insert("shortName", orig.value("short-name")); m.insert("longName", orig.value("long-name")); + QByteArray pngIcon = orig.value("menu-icon").toByteArray(); + if (!pngIcon.isEmpty()) { + _appMenuIcons.insert(uuid, QImage::fromData(pngIcon, "PNG")); + } + _apps.append(QVariant::fromValue(m)); } diff --git a/app/pebbledinterface.h b/app/pebbledinterface.h index e468505..51efa12 100644 --- a/app/pebbledinterface.h +++ b/app/pebbledinterface.h @@ -5,6 +5,7 @@ #include #include #include +#include #include class OrgPebbledWatchInterface; @@ -39,7 +40,9 @@ public: Q_INVOKABLE QUrl configureApp(const QString &uuid); - Q_INVOKABLE bool isAppInstalled(const QString &uuid); + Q_INVOKABLE bool isAppInstalled(const QString &uuid) const; + + QImage menuIconForApp(const QUuid &uuid) const; signals: void enabledChanged(); @@ -82,6 +85,7 @@ private: QStringList _appSlots; QVariantList _apps; QHash _appsByUuid; + QHash _appMenuIcons; }; #endif // PEBBLEDINTERFACE_H diff --git a/app/qml/pages/AppConfigPage.qml b/app/qml/pages/AppConfigPage.qml index 10fbe05..00eb05c 100644 --- a/app/qml/pages/AppConfigPage.qml +++ b/app/qml/pages/AppConfigPage.qml @@ -11,6 +11,7 @@ Page { SilicaWebView { id: webview + visible: url != "" anchors.fill: parent header: PageHeader { @@ -32,8 +33,17 @@ Page { } } - ViewPlaceholder { - enabled: url == "" + Text { + anchors.centerIn: parent + visible: url == "" text: qsTr("No configuration settings available") + width: parent.width - 2*Theme.paddingLarge + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.Wrap + font { + pixelSize: Theme.fontSizeLarge + family: Theme.fontFamilyHeading + } + color: Theme.highlightColor } } diff --git a/app/qml/pages/InstallAppDialog.qml b/app/qml/pages/InstallAppDialog.qml index 3a3c0b1..79283a6 100644 --- a/app/qml/pages/InstallAppDialog.qml +++ b/app/qml/pages/InstallAppDialog.qml @@ -27,20 +27,29 @@ Dialog { property string uuid: modelData.uuid property bool alreadyInstalled: pebbled.isAppInstalled(uuid) - Image { - id: appImage + Item { + id: appIcon + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + anchors { top: parent.top left: parent.left leftMargin: Theme.paddingLarge } - width: Theme.itemSizeSmall + + Image { + id: appImage + anchors.centerIn: parent + source: "image://pebble-app-icon/" + uuid; + scale: 2 + } } Label { id: appName anchors { - left: appImage.right + left: appIcon.right leftMargin: Theme.paddingMedium right: parent.right rightMargin: Theme.paddiumLarge diff --git a/app/qml/pages/WatchPage.qml b/app/qml/pages/WatchPage.qml index ce9d636..3a712ab 100644 --- a/app/qml/pages/WatchPage.qml +++ b/app/qml/pages/WatchPage.qml @@ -77,7 +77,8 @@ Page { } Item { - height: Theme.paddingMedium + width: parent.width + height: Theme.paddingLarge } Label { @@ -139,26 +140,49 @@ Page { } - Image { - id: slotImage + Item { + id: slotIcon + width: Theme.itemSizeSmall + height: Theme.itemSizeSmall + anchors { top: parent.top left: parent.left leftMargin: Theme.paddingLarge } - width: Theme.itemSizeSmall - } - BusyIndicator { - id: slotBusy - anchors.centerIn: slotImage - running: slotDelegate.busy + Image { + id: slotImage + anchors.centerIn: parent + source: isKnownApp ? "image://pebble-app-icon/" + modelData : "" + scale: 2 + visible: !isEmptySlot && isKnownApp && !slotBusy.running + } + + Rectangle { + width: 30 + height: 30 + anchors.centerIn: parent + scale: 2 + border { + width: 2 + color: slotDelegate.highlighted ? Theme.highlightColor : Theme.primaryColor + } + color: "transparent" + visible: isEmptySlot && !slotBusy.running + } + + BusyIndicator { + id: slotBusy + anchors.centerIn: parent + running: slotDelegate.busy + } } Label { id: slotName anchors { - left: slotImage.right + left: slotIcon.right leftMargin: Theme.paddingMedium right: parent.right rightMargin: Theme.paddiumLarge @@ -172,6 +196,11 @@ Page { Component { id: slotMenu ContextMenu { + MenuItem { + text: qsTr("Install app...") + visible: isEmptySlot + onClicked: install(); + } MenuItem { text: qsTr("Configure...") visible: !isEmptySlot && isKnownApp diff --git a/app/qml/pebble.qml b/app/qml/pebble.qml index da3bfb5..2e26ebe 100644 --- a/app/qml/pebble.qml +++ b/app/qml/pebble.qml @@ -38,8 +38,4 @@ ApplicationWindow { initialPage: Component { ManagerPage { } } cover: Qt.resolvedUrl("cover/CoverPage.qml") - - PebbledInterface { - id: pebbled - } } diff --git a/daemon/appinfo.cpp b/daemon/appinfo.cpp index fd43248..4397abc 100644 --- a/daemon/appinfo.cpp +++ b/daemon/appinfo.cpp @@ -1,5 +1,6 @@ -#include "appinfo.h" #include +#include +#include "appinfo.h" struct AppInfoData : public QSharedData { QUuid uuid; @@ -13,6 +14,7 @@ struct AppInfoData : public QSharedData { AppInfo::Capabilities capabilities; QHash keyInts; QHash keyNames; + QImage menuIcon; QString path; }; @@ -21,6 +23,7 @@ AppInfo::AppInfo() : d(new AppInfoData) d->versionCode = 0; d->watchface = false; d->jskit = false; + d->capabilities = 0; } AppInfo::AppInfo(const AppInfo &rhs) : d(rhs.d) @@ -154,6 +157,26 @@ int AppInfo::valueForAppKey(const QString &key) const return d->keyInts.value(key, -1); } +QImage AppInfo::menuIcon() const +{ + return d->menuIcon; +} + +QByteArray AppInfo::menuIconAsPng() const +{ + QByteArray data; + QBuffer buf(&data); + buf.open(QIODevice::WriteOnly); + d->menuIcon.save(&buf, "PNG"); + buf.close(); + return data; +} + +void AppInfo::setMenuIcon(const QImage &img) +{ + d->menuIcon = img; +} + QString AppInfo::path() const { return d->path; diff --git a/daemon/appinfo.h b/daemon/appinfo.h index 6f97639..3d5c4b4 100644 --- a/daemon/appinfo.h +++ b/daemon/appinfo.h @@ -1,10 +1,10 @@ #ifndef APPINFO_H #define APPINFO_H -#include +#include #include #include -#include +#include class AppInfoData; @@ -28,6 +28,7 @@ public: Q_PROPERTY(bool watchface READ isWatchface WRITE setWatchface) Q_PROPERTY(bool jskit READ isJSKit WRITE setJSKit) Q_PROPERTY(Capabilities capabilities READ capabilities WRITE setCapabilities) + Q_PROPERTY(QImage menuIcon READ menuIcon WRITE setMenuIcon) Q_PROPERTY(QString path READ path WRITE setPath) public: @@ -71,6 +72,10 @@ public: bool hasAppKey(const QString &key) const; int valueForAppKey(const QString &key) const; + QImage menuIcon() const; + QByteArray menuIconAsPng() const; + void setMenuIcon(const QImage &img); + QString path() const; void setPath(const QString &string); diff --git a/daemon/appmanager.cpp b/daemon/appmanager.cpp index 10f2e3e..8745160 100644 --- a/daemon/appmanager.cpp +++ b/daemon/appmanager.cpp @@ -4,6 +4,17 @@ #include #include #include "appmanager.h" +#include "unpacker.h" +#include "stm32crc.h" + +namespace { +struct ResourceEntry { + int index; + quint32 offset; + quint32 length; + quint32 crc; +}; +} AppManager::AppManager(QObject *parent) : QObject(parent), @@ -116,18 +127,55 @@ void AppManager::scanApp(const QString &path) info.setWatchface(watchapp["watchface"].toBool()); info.setJSKit(appDir.exists("pebble-js-app.js")); - const QJsonArray capabilities = root["capabilities"].toArray(); - AppInfo::Capabilities caps = 0; - for (QJsonArray::const_iterator it = capabilities.constBegin(); it != capabilities.constEnd(); ++it) { - QString cap = (*it).toString(); - if (cap == "location") caps |= AppInfo::Location; - if (cap == "configurable") caps |= AppInfo::Configurable; + if (root.contains("capabilities")) { + const QJsonArray capabilities = root["capabilities"].toArray(); + AppInfo::Capabilities caps = 0; + for (auto it = capabilities.constBegin(); it != capabilities.constEnd(); ++it) { + QString cap = (*it).toString(); + if (cap == "location") caps |= AppInfo::Location; + if (cap == "configurable") caps |= AppInfo::Configurable; + } + info.setCapabilities(caps); } - info.setCapabilities(caps); - const QJsonObject appkeys = root["appKeys"].toObject(); - for (QJsonObject::const_iterator it = appkeys.constBegin(); it != appkeys.constEnd(); ++it) { - info.addAppKey(it.key(), it.value().toInt()); + if (root.contains("appKeys")) { + const QJsonObject appkeys = root["appKeys"].toObject(); + for (auto it = appkeys.constBegin(); it != appkeys.constEnd(); ++it) { + info.addAppKey(it.key(), it.value().toInt()); + } + } + + if (root.contains("resources")) { + const QJsonObject resources = root["resources"].toObject(); + const QJsonArray media = resources["media"].toArray(); + int index = 0; + + for (auto it = media.constBegin(); it != media.constEnd(); ++it) { + const QJsonObject res = (*it).toObject(); + const QJsonValue menuIcon = res["menuIcon"]; + + bool is_menu_icon = false; + switch (menuIcon.type()) { + case QJsonValue::Bool: + is_menu_icon = menuIcon.toBool(); + break; + case QJsonValue::String: + is_menu_icon = !menuIcon.toString().isEmpty(); + break; + default: + break; + } + + if (is_menu_icon) { + QByteArray data = extractFromResourcePack(appDir.filePath("app_resources.pbpack"), index); + if (!data.isEmpty()) { + QImage icon = decodeResourceImage(data); + info.setMenuIcon(icon); + } + } + + index++; + } } info.setPath(path); @@ -143,3 +191,77 @@ void AppManager::scanApp(const QString &path) const char *type = info.isWatchface() ? "watchface" : "app"; logger()->debug() << "found installed" << type << info.shortName() << info.versionLabel() << "with uuid" << info.uuid().toString(); } + +QByteArray AppManager::extractFromResourcePack(const QString &file, int wanted_id) const +{ + QFile f(file); + if (!f.open(QIODevice::ReadOnly)) { + logger()->warn() << "cannot open resource file" << f.fileName(); + return QByteArray(); + } + + QByteArray data = f.readAll(); + Unpacker u(data); + + int num_files = u.readLE(); + u.readLE(); // crc for entire file + u.readLE(); // timestamp + + logger()->debug() << "reading" << num_files << "resources from" << file; + + QList table; + + for (int i = 0; i < num_files; i++) { + ResourceEntry e; + e.index = u.readLE(); + e.offset = u.readLE(); + e.length = u.readLE(); + e.crc = u.readLE(); + + if (u.bad()) { + logger()->warn() << "short read on resource file"; + return QByteArray(); + } + + table.append(e); + } + + if (wanted_id >= table.size()) { + logger()->warn() << "specified resource does not exist"; + return QByteArray(); + } + + const ResourceEntry &e = table[wanted_id]; + + int offset = 12 + 256 * 16 + e.offset; + + QByteArray res = data.mid(offset, e.length); + + Stm32Crc crc; + crc.addData(res); + + if (crc.result() != e.crc) { + logger()->warn() << "CRC failure in resource" << e.index << "on file" << file; + return QByteArray(); + } + + return res; +} + +QImage AppManager::decodeResourceImage(const QByteArray &data) const +{ + Unpacker u(data); + int scanline = u.readLE(); + u.skip(sizeof(quint16) + sizeof(quint32)); + int width = u.readLE(); + int height = u.readLE(); + + QImage img(width, height, QImage::Format_MonoLSB); + const uchar *src = reinterpret_cast(&data.constData()[12]); + for (int line = 0; line < height; ++line) { + memcpy(img.scanLine(line), src, qMin(scanline, img.bytesPerLine())); + src += scanline; + } + + return img; +} diff --git a/daemon/appmanager.h b/daemon/appmanager.h index 1725c14..d5e5ba1 100644 --- a/daemon/appmanager.h +++ b/daemon/appmanager.h @@ -30,6 +30,8 @@ signals: private: void scanApp(const QString &path); + QByteArray extractFromResourcePack(const QString &file, int id) const; + QImage decodeResourceImage(const QByteArray &data) const; private: QFileSystemWatcher *_watcher; diff --git a/daemon/manager.cpp b/daemon/manager.cpp index 25908e4..212a1d7 100644 --- a/daemon/manager.cpp +++ b/daemon/manager.cpp @@ -380,6 +380,11 @@ QVariantList PebbledProxy::AllApps() const m.insert("company-name", QVariant::fromValue(info.companyName())); m.insert("version-label", QVariant::fromValue(info.versionLabel())); m.insert("is-watchface", QVariant::fromValue(info.isWatchface())); + + if (!info.menuIcon().isNull()) { + m.insert("menu-icon", QVariant::fromValue(info.menuIconAsPng())); + } + l.append(QVariant::fromValue(m)); } -- cgit v1.2.3