diff options
author | Martin Kampas <martin.kampas@jolla.com> | 2018-10-26 09:57:57 +0200 |
---|---|---|
committer | Svetlana Abramenkova <sabramenkova@luxoft.com> | 2020-03-04 13:30:08 +0000 |
commit | f49c059de342884dc5b93ced4d9c865cd2441309 (patch) | |
tree | b5d88368fd92a43dd8b22eb59ada84acacdd5305 | |
parent | b7311851cc2a5f57cc980f277ed8e94f9b576d22 (diff) |
Let it work with compiled-in resources
Change-Id: I41e3fc4e9a6916aeec3e61c37cc452bd7325a5d4
Task-number: AUTOSUITE-1098
Reviewed-by: Svetlana Abramenkova <sabramenkova@luxoft.com>
-rw-r--r-- | src/bench/directorypreviewadapter.cpp | 10 | ||||
-rw-r--r-- | src/bench/hostwidget.cpp | 10 | ||||
-rw-r--r-- | src/fontadapter.cpp | 7 | ||||
-rw-r--r-- | src/imageadapter.cpp | 3 | ||||
-rw-r--r-- | src/livedocument.cpp | 159 | ||||
-rw-r--r-- | src/livedocument.h | 11 | ||||
-rw-r--r-- | src/livenodeengine.cpp | 93 | ||||
-rw-r--r-- | src/livenodeengine.h | 7 | ||||
-rw-r--r-- | src/overlay.cpp | 13 | ||||
-rw-r--r-- | src/overlay.h | 7 | ||||
-rw-r--r-- | src/resourcemap.cpp | 292 | ||||
-rw-r--r-- | src/resourcemap.h | 66 | ||||
-rw-r--r-- | src/src.pri | 6 |
13 files changed, 642 insertions, 42 deletions
diff --git a/src/bench/directorypreviewadapter.cpp b/src/bench/directorypreviewadapter.cpp index e70dabe..856909d 100644 --- a/src/bench/directorypreviewadapter.cpp +++ b/src/bench/directorypreviewadapter.cpp @@ -31,6 +31,7 @@ ****************************************************************************/ #include "directorypreviewadapter.h" +#include "livedocument.h" #include <QFileInfo> #include <QDir> #include <QQmlContext> @@ -64,15 +65,16 @@ bool DirectoryPreviewAdapter::isFullScreen() const bool DirectoryPreviewAdapter::canAdapt(const QUrl &url) const { - QFileInfo info(url.toLocalFile()); - return info.isDir(); + QString path(LiveDocument::toFilePath(url)); + return !path.isEmpty() && QFileInfo(path).isDir(); } QUrl DirectoryPreviewAdapter::adapt(const QUrl &url, QQmlContext *context) { - QDir dir(url.toLocalFile()); + QString path(LiveDocument::toFilePath(url)); + QDir dir(path); context->setContextProperty("files", dir.entryList(QDir::Files | QDir::NoDotDot | QDir::NoDot | QDir::NoSymLinks)); - context->setContextProperty("path", url.toLocalFile()); + context->setContextProperty("path", path); context->setContextProperty("adapter", this); if (availableFeatures().testFlag(QtQuickControls)) diff --git a/src/bench/hostwidget.cpp b/src/bench/hostwidget.cpp index 7ea6781..9ee16f9 100644 --- a/src/bench/hostwidget.cpp +++ b/src/bench/hostwidget.cpp @@ -295,6 +295,16 @@ void HostWidget::onConnected() disconnect(m_connectDisconnectAction, &QAction::triggered, 0, 0); connect(m_connectDisconnectAction, &QAction::triggered, &m_publisher, &RemotePublisher::disconnectFromServer); + // Send .qrc files to let the node fill its resourceMap + // TODO not using bulkSend as this could be misinterpreted as a response to + // needsPublishWorkspace + QDirIterator it(m_engine->workspace(), {"*.qrc"}, QDir::AllEntries | QDir::NoDotAndDotDot, + QDirIterator::Subdirectories); + while (it.hasNext()) { + LiveDocument qrcFile = LiveDocument::resolve(m_engine->workspace(), it.next()); + sendDocument(qrcFile); + } + m_publisher.initComplete(); } diff --git a/src/fontadapter.cpp b/src/fontadapter.cpp index 7ff3ff0..2592588 100644 --- a/src/fontadapter.cpp +++ b/src/fontadapter.cpp @@ -31,6 +31,7 @@ ****************************************************************************/ #include "fontadapter.h" +#include "livedocument.h" #include <QDebug> #include <QQmlContext> @@ -64,7 +65,9 @@ QImage FontAdapter::preview(const QString &path, const QSize &requestedSize) bool FontAdapter::canAdapt(const QUrl &url) const { - QString path = url.toLocalFile(); + QString path = LiveDocument::toFilePath(url); + if (path.isEmpty()) + return false; foreach (const QString& extension, fontExtensions) { if (path.endsWith(extension)) @@ -81,7 +84,7 @@ bool FontAdapter::isFullScreen() const QUrl FontAdapter::adapt(const QUrl &url, QQmlContext *context) { - fontId = base.addApplicationFont(url.toLocalFile()); + fontId = base.addApplicationFont(LiveDocument::toFilePath(url)); QStringList families = base.applicationFontFamilies(fontId); diff --git a/src/imageadapter.cpp b/src/imageadapter.cpp index dca2da6..4844940 100644 --- a/src/imageadapter.cpp +++ b/src/imageadapter.cpp @@ -31,6 +31,7 @@ ****************************************************************************/ #include "imageadapter.h" +#include "livedocument.h" #include <QImageReader> #include <QDebug> #include <QFileInfo> @@ -67,7 +68,7 @@ QImage ImageAdapter::preview(const QString &path, const QSize &requestedSize) bool ImageAdapter::canAdapt(const QUrl &url) const { - return !QImageReader::imageFormat(url.toLocalFile()).isEmpty(); + return !QImageReader::imageFormat(LiveDocument::toFilePath(url)).isEmpty(); } QUrl ImageAdapter::adapt(const QUrl &url, QQmlContext *context) diff --git a/src/livedocument.cpp b/src/livedocument.cpp index 39a2f39..71730e5 100644 --- a/src/livedocument.cpp +++ b/src/livedocument.cpp @@ -31,13 +31,23 @@ ****************************************************************************/ #include "livedocument.h" +#include "resourcemap.h" #include <QDebug> /*! * \class LiveDocument - * \brief Encapsulates a relative path to a workspace document. + * \brief Identifies a workspace document * \inmodule qmllive + * + * On the LiveHubEngine side, i.e. where the QmlLive Bench is running, it always + * represents a document on file system, under the workspace directory. + * + * On the LiveNodeEngine side, i.e. on the QmlLive Runtime side, it represents a + * document that may exist either on file system or in a Qt resource. In either + * case the document is identified by the relative path of the original file + * under the workspace directory on the LiveHubEngine side. See + * runtimeLocation(). */ /*! @@ -81,9 +91,9 @@ LiveDocument::LiveDocument(const QString &relativeFilePath) /*! * \fn QString LiveDocument::errorString() const * - * When called just after resolve(), existsIn() or isFileIn() failed, returns a - * descriptive message suitable for displaying in user interface. When called in - * other context, the result is undefined. + * When called just after resolve(), existsIn(), isFileIn() or mapsToResource() + * failed, returns a descriptive message suitable for displaying in user + * interface. When called in other context, the result is undefined. */ /*! @@ -149,6 +159,54 @@ QString LiveDocument::absoluteFilePathIn(const QDir &workspace) const } /*! + * Returns \c true if the runtime location of the document is in a Qt resource. + * This is determined using the given \a resourceMap. + * + * \sa runtimeLocation(), LiveNodeEngine::resourceMap(), errorString() + */ +bool LiveDocument::mapsToResource(const ResourceMap &resourceMap) const +{ + LIVE_ASSERT(!isNull(), return false); + + QString resource = resourceMap.toResource(*this); + if (resource.isEmpty()) { + m_errorString = tr("Document '%1' does not exist as a Qt resource") + .arg(m_relativeFilePath); + return false; + } + + // TODO Necessary to check if it actually exists? ResourceMap may not be in sync. + + return true; +} + +/*! + * Determines the runtime location of the document. On LiveNodeEngine side a + * document may exist either on file system (under the given \a workspace) or in + * a Qt resource (determined using the given \a resourceMap). + * + * Example: + * + * \code + * LiveDocument document = ...; + * LiveNodeEngine *engine = ...; + * QFile file(document.runtimeLocation(engine->workspace(), *engine->resourceMap())); + * \endcode + * + * \sa mapsToResource(), LiveNodeEngine::resourceMap() + */ +QUrl LiveDocument::runtimeLocation(const QDir &workspace, const ResourceMap &resourceMap) const +{ + LIVE_ASSERT(!isNull(), return QString()); + + const QString resource = resourceMap.toResource(*this); + if (!resource.isEmpty()) + return toUrl(resource); + else + return QUrl::fromLocalFile(absoluteFilePathIn(workspace)); +} + +/*! * Constructs a non-null instance unless the \a filePath resolves outside of the * \a workspace directory. * @@ -182,6 +240,99 @@ LiveDocument LiveDocument::resolve(const QDir &workspace, const QString &filePat } /*! + * Constructs a non-null instance for \a filePath that either resolves to a path + * under the \a workspace directory or determines a Qt resource for which a + * mapping exists in the given \a resourceMap. + * + * \a filePath may be an absolute or relative file path to a file or a Qt + * resource path (starting with \c {":/"} ). In either case the file is NOT + * required to exist, + * + * \sa isNull(), errorString() + */ +LiveDocument LiveDocument::resolve(const QDir &workspace, const ResourceMap &resourceMap, const QString &filePath) +{ + if (filePath.isEmpty()) { + qWarning() << "filePath is an empty string"; + LiveDocument retv; + retv.m_errorString = tr("Not a valid file path: ''"); + return retv; + } + + if (filePath.startsWith(":/")) { + LiveDocument retv = resourceMap.toDocument(filePath); + if (retv.isNull()) { + retv.m_errorString = tr("No mapping exists for resource: '%1'").arg(filePath); + } + return retv; + } else { + return resolve(workspace, filePath); + } +} + +/*! + * Constructs a non-null instance for \a fileUrl that either resolves to a path + * under the \a workspace directory or determines a Qt resource for which a + * mapping exists in the given \a resourceMap. + * + * \a fileUrl may be an URL representing a local file or a Qt resource path + * (e.g. \c {"qrc:/example/icon.png"}). In either case the file is NOT required + * to exist, + * + * \sa isNull(), errorString() + */ +LiveDocument LiveDocument::resolve(const QDir &workspace, const ResourceMap &resourceMap, const QUrl &fileUrl) +{ + if (fileUrl.isEmpty()) { + qWarning() << "fileUrl is an empty URL:" << fileUrl; + LiveDocument retv; + retv.m_errorString = tr("Not a valid URL: '%1'").arg(fileUrl.toString()); + return retv; + } + + if (fileUrl.scheme() == QLatin1String("qrc")) { + LiveDocument retv = resourceMap.toDocument(QLatin1String(":") + fileUrl.path()); + if (retv.isNull()) { + retv.m_errorString = tr("No mapping exists for resource: '%1'").arg(fileUrl.toString()); + } + return retv; + } else if (fileUrl.scheme() == QLatin1String("file")) { + return resolve(workspace, fileUrl.toLocalFile()); + } else { + qWarning() << "fileUrl is not a supported URL:" << fileUrl; + LiveDocument retv; + retv.m_errorString = tr("Unsupported URL: '%1'").arg(fileUrl.toString()); + return retv; + } +} + +/*! + * Converts a local/Qt resource \a url to file path + */ +QString LiveDocument::toFilePath(const QUrl &url) +{ + if (url.scheme() == QLatin1String("qrc")) + return QLatin1String(":") + url.path(); + else + return url.toLocalFile(); +} + +/*! + * Converts a local/Qt resource \a filePath to URL + */ +QUrl LiveDocument::toUrl(const QString &filePath) +{ + if (filePath.startsWith(":/")) { + QUrl url; + url.setScheme("qrc"); + url.setPath(filePath.mid(1)); + return url; + } else { + return QUrl::fromLocalFile(filePath); + } +} + +/*! * Allows to print LiveDocument \a document via debug stream \a dbg. */ QDebug operator<<(QDebug dbg, const LiveDocument &document) diff --git a/src/livedocument.h b/src/livedocument.h index b43f49e..0087b0a 100644 --- a/src/livedocument.h +++ b/src/livedocument.h @@ -36,6 +36,8 @@ #include "qmllive_global.h" +class ResourceMap; + class QMLLIVESHARED_EXPORT LiveDocument { Q_DECLARE_TR_FUNCTIONS(LiveDocument) @@ -53,7 +55,16 @@ public: QString relativeFilePath() const; QString absoluteFilePathIn(const QDir &workspace) const; + bool mapsToResource(const ResourceMap &resourceMap) const; + QUrl runtimeLocation(const QDir &workspace, const ResourceMap &resourceMap) const; + static LiveDocument resolve(const QDir &workspace, const QString &filePath); + static LiveDocument resolve(const QDir &workspace, const ResourceMap &resourceMap, const QString + &filePath); + static LiveDocument resolve(const QDir &workspace, const ResourceMap &resourceMap, const QUrl &fileUrl); + + static QString toFilePath(const QUrl &url); + static QUrl toUrl(const QString &filePath); friend inline bool operator==(const LiveDocument &d1, const LiveDocument &d2) { return d1.m_relativeFilePath == d2.m_relativeFilePath; } diff --git a/src/livenodeengine.cpp b/src/livenodeengine.cpp index dbbea10..8f1ff0f 100644 --- a/src/livenodeengine.cpp +++ b/src/livenodeengine.cpp @@ -33,6 +33,7 @@ #include "livenodeengine.h" #include "liveruntime.h" #include "qmlhelper.h" +#include "resourcemap.h" #include "contentpluginfactory.h" #include "imageadapter.h" #include "fontadapter.h" @@ -48,6 +49,11 @@ #define DEBUG if (0) qDebug() #endif +namespace { +const char *const OVERLAY_PATH_PREFIX = "qml-live-overlay--"; +const char OVERLAY_PATH_SEPARATOR = '-'; +} + /*! * \class LiveNodeEngine * \brief The LiveNodeEngine class instantiates QML components in cooperation with LiveHubEngine. @@ -83,7 +89,10 @@ * With this option enabled, updates can be received even if workspace * is read only. Updates will be stored in a writable overlay stacked * over the original workspace with the help of - * QQmlAbstractUrlInterceptor. Requires \l AllowUpdates. + * QQmlAbstractUrlInterceptor. This option only influences the way how + * updates to file system files are handled - updates to Qt resource + * files are always stored in an overlay. + * Requires \l AllowUpdates. * \value AllowCreateMissing * Without this option enabled, updates are only accepted for existing * workspace documents. Requires \l AllowUpdates. @@ -91,17 +100,21 @@ * \sa {QML Live Runtime} */ -class OverlayUrlInterceptor : public QObject, public QQmlAbstractUrlInterceptor +class UrlInterceptor : public QObject, public QQmlAbstractUrlInterceptor { Q_OBJECT public: - OverlayUrlInterceptor(const Overlay *overlay, QQmlAbstractUrlInterceptor *otherInterceptor, QObject *parent) + UrlInterceptor(const QDir &workspace, const Overlay *overlay, const ResourceMap *resourceMap, + QQmlAbstractUrlInterceptor *otherInterceptor, QObject *parent) : QObject(parent) , m_otherInterceptor(otherInterceptor) + , m_workspace(workspace) , m_overlay(overlay) + , m_resourceMap(resourceMap) { Q_ASSERT(overlay); + Q_ASSERT(resourceMap); } // From QQmlAbstractUrlInterceptor @@ -109,7 +122,20 @@ public: { const QUrl url_ = m_otherInterceptor ? m_otherInterceptor->intercept(url, type) : url; if (url_.scheme() == QLatin1String("file")) { - return QUrl::fromLocalFile(m_overlay->map(url_.toLocalFile())); + bool existingOnly = true; + return QUrl::fromLocalFile(m_overlay->map(url_.toLocalFile(), existingOnly)); + } else if (url_.scheme() == QLatin1String("qrc")) { + const LiveDocument document = LiveDocument::resolve(m_workspace, *m_resourceMap, url_); + if (document.isNull()) + return url_; + + QString filePath = document.absoluteFilePathIn(m_workspace); + bool existingOnly = false; + filePath = m_overlay->map(filePath, existingOnly); + if (!QFileInfo(filePath).exists()) + return url_; + + return QUrl::fromLocalFile(filePath); } else { return url_; } @@ -117,7 +143,9 @@ public: private: QQmlAbstractUrlInterceptor *m_otherInterceptor; - QPointer<const Overlay> m_overlay; + const QDir m_workspace; + const QPointer<const Overlay> m_overlay; + const QPointer<const ResourceMap> m_resourceMap; }; /*! @@ -129,6 +157,7 @@ LiveNodeEngine::LiveNodeEngine(QObject *parent) , m_xOffset(0) , m_yOffset(0) , m_rotation(0) + , m_resourceMap(new ResourceMap(this)) , m_delayReload(new QTimer(this)) , m_pluginFactory(new ContentPluginFactory(this)) , m_activePlugin(0) @@ -276,10 +305,11 @@ void LiveNodeEngine::usePreloadedDocument(const LiveDocument &document, QObject m_activeFile = document; - if (!m_activeFile.existsIn(m_workspace)) { + if (!m_activeFile.existsIn(m_workspace) && !m_activeFile.mapsToResource(*m_resourceMap)) { QQmlError error; error.setUrl(QUrl::fromLocalFile(m_activeFile.absoluteFilePathIn(m_workspace))); - error.setDescription(tr("File not found")); + error.setDescription(tr("File not found under the workspace " + "and no mapping to a Qt resource exists for that file")); emit logErrors(QList<QQmlError>() << error); } @@ -312,7 +342,7 @@ void LiveNodeEngine::usePreloadedDocument(const QString &document, QQuickWindow { LIVE_ASSERT(m_activeFile.isNull(), return); - LiveDocument resolved = LiveDocument::resolve(workspace(), document); + LiveDocument resolved = LiveDocument::resolve(workspace(), *m_resourceMap, document); if (resolved.isNull()) { qWarning() << "Failed to resolve preloaded document path:" << document << "Workspace: " << workspace(); @@ -411,9 +441,13 @@ void LiveNodeEngine::reloadDocument() emit clearLog(); - const QUrl originalUrl = QUrl::fromLocalFile(m_activeFile.absoluteFilePathIn(m_workspace)); + const QUrl originalUrl = m_activeFile.runtimeLocation(m_workspace, *m_resourceMap); const QUrl url = queryDocumentViewer(originalUrl); + DEBUG << "Loading document" << m_activeFile << "runtime location:" << originalUrl; + if (url != originalUrl) + DEBUG << "Using viewer" << url; + auto showErrorScreen = [this] { Q_ASSERT(m_fallbackView); m_fallbackView->setResizeMode(QQuickView::SizeRootObjectToView); @@ -505,20 +539,28 @@ void LiveNodeEngine::reloadDocument() */ void LiveNodeEngine::updateDocument(const LiveDocument &document, const QByteArray &content) { + if (QFileInfo(document.relativeFilePath()).suffix() == QLatin1String("qrc")) { + QBuffer buffer; + buffer.setData(content); + buffer.open(QIODevice::ReadOnly); + if (!m_resourceMap->updateMapping(document, &buffer)) + qWarning() << "Unable to parse qrc file " << document.relativeFilePath() << ":" << m_resourceMap->errorString(); + } + if (!(m_workspaceOptions & AllowUpdates)) { return; } - QString documentPath = document.absoluteFilePathIn(m_workspace); + bool existsInWorkspace = document.existsIn(m_workspace); + bool mapsToResource = document.mapsToResource(*m_resourceMap); + if (!existsInWorkspace && !mapsToResource && !(m_workspaceOptions & AllowCreateMissing)) + return; - if (!(m_workspaceOptions & AllowCreateMissing)) { - if (!QFileInfo(documentPath).exists()) - return; - } + bool useOverlay = (m_workspaceOptions & UpdatesAsOverlay) || mapsToResource; - QString writablePath = (m_workspaceOptions & UpdatesAsOverlay) - ? m_overlay->reserve(document) - : documentPath; + QString writablePath = useOverlay + ? m_overlay->reserve(document, existsInWorkspace) + : document.absoluteFilePathIn(m_workspace); QString writableDirPath = QFileInfo(writablePath).absoluteDir().absolutePath(); QDir().mkpath(writableDirPath); @@ -592,16 +634,27 @@ void LiveNodeEngine::setWorkspace(const QString &path, WorkspaceOptions options) m_workspaceOptions |= AllowUpdates; } - if (m_workspaceOptions & UpdatesAsOverlay) { + if (m_workspaceOptions & AllowUpdates) { + // Even without UpdatesAsOverlay the overlay is used for Qt resources m_overlay = new Overlay(m_workspace.path(), this); - m_overlayUrlInterceptor = new OverlayUrlInterceptor(m_overlay, qmlEngine()->urlInterceptor(), this); - qmlEngine()->setUrlInterceptor(m_overlayUrlInterceptor); + m_urlInterceptor = new UrlInterceptor(m_workspace, m_overlay, m_resourceMap, qmlEngine()->urlInterceptor(), this); + qmlEngine()->setUrlInterceptor(m_urlInterceptor); } emit workspaceChanged(workspace()); } /*! + * Returns the ResourceMap managed by this instance. + * + * \sa LiveDocument + */ +ResourceMap *LiveNodeEngine::resourceMap() const +{ + return m_resourceMap; +} + +/*! * Sets the pluginPath to \a path. * * The pluginPath will be used to load QML Live plugins diff --git a/src/livenodeengine.h b/src/livenodeengine.h index 8594084..17c5cf9 100644 --- a/src/livenodeengine.h +++ b/src/livenodeengine.h @@ -42,7 +42,8 @@ class LiveRuntime; class ContentPluginFactory; -class OverlayUrlInterceptor; +class ResourceMap; +class UrlInterceptor; class QMLLIVESHARED_EXPORT LiveNodeEngine : public QObject { @@ -79,6 +80,7 @@ public: QString workspace() const; void setWorkspace(const QString &path, WorkspaceOptions options = NoWorkspaceOption); + ResourceMap *resourceMap() const; void setPluginPath(const QString& path); QString pluginPath() const; @@ -136,8 +138,9 @@ private: QList<QMetaObject::Connection> m_activeWindowConnections; QDir m_workspace; WorkspaceOptions m_workspaceOptions; - QPointer<OverlayUrlInterceptor> m_overlayUrlInterceptor; + QPointer<UrlInterceptor> m_urlInterceptor; QPointer<Overlay> m_overlay; + QPointer<ResourceMap> m_resourceMap; QTimer *m_delayReload; ContentPluginFactory* m_pluginFactory; diff --git a/src/overlay.cpp b/src/overlay.cpp index 92f890c..a97e1d7 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -60,18 +60,23 @@ QDir Overlay::overlay() const return m_overlay.path(); } -QString Overlay::reserve(const LiveDocument &document) +QString Overlay::reserve(const LiveDocument &document, bool existing) { QWriteLocker locker(&m_lock); QString overlayingPath = document.absoluteFilePathIn(m_overlay.path()); - m_mappings.insert(document.absoluteFilePathIn(m_basePath), overlayingPath); + m_mappings.insert(document.absoluteFilePathIn(m_basePath), qMakePair(overlayingPath, existing)); return overlayingPath; } -QString Overlay::map(const QString &file) const +QString Overlay::map(const QString &file, bool existingOnly) const { QReadLocker locker(&m_lock); - return m_mappings.value(file, file); + auto it = m_mappings.find(file); + if (it == m_mappings.end()) + return file; + if (existingOnly && !it->second) + return file; + return it->first; } QString Overlay::overlayTemplatePath() diff --git a/src/overlay.h b/src/overlay.h index 92f609a..48d80e0 100644 --- a/src/overlay.h +++ b/src/overlay.h @@ -45,16 +45,17 @@ public: Overlay(const QString &basePath, QObject *parent); ~Overlay(); - QString reserve(const LiveDocument &document); + QString reserve(const LiveDocument &document, bool existing); QDir overlay() const; - QString map(const QString &file) const; + QString map(const QString &file, bool existingOnly) const; private: static QString overlayTemplatePath(); private: mutable QReadWriteLock m_lock; - QHash<QString, QString> m_mappings; + // base path -> ( overlaying path, file exists at base path ) + QHash<QString, QPair<QString, bool>> m_mappings; QString m_basePath; QTemporaryDir m_overlay; }; diff --git a/src/resourcemap.cpp b/src/resourcemap.cpp new file mode 100644 index 0000000..3d8f80b --- /dev/null +++ b/src/resourcemap.cpp @@ -0,0 +1,292 @@ +/**************************************************************************** +** +** Copyright (C) 2018 Jolla Ltd +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QmlLive tool. +** +** $QT_BEGIN_LICENSE:GPL-QTAS$ +** Commercial License Usage +** Licensees holding valid commercial Qt Automotive Suite licenses may use +** this file in accordance with the commercial license agreement provided +** with the Software or, alternatively, in accordance with the terms +** contained in a written agreement between you and The Qt Company. For +** licensing terms and conditions see https://www.qt.io/terms-conditions. +** For further information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +** SPDX-License-Identifier: GPL-3.0 +** +****************************************************************************/ + +#include "resourcemap.h" + +#include "livedocument.h" + +Q_DECLARE_LOGGING_CATEGORY(rmLog) +Q_LOGGING_CATEGORY(rmLog, "QmlLive.ResourceMap", QtInfoMsg) + +/*! + * \class ResourceMap + * \brief Maps compiled-in Qt resources to workspace documents and vice versa + * \inmodule qmllive + * + * A ResourceMap is managed and owned by a LiveNodeEngine. LiveDocument uses + * ResourceMap to enable convenient handling of documents that may reside + * either under workspace on file system or in Qt resources at run time. + * + * \sa LiveNodeEngine::resourceMap(), LiveDocument + */ + +class QrcReader : public QObject +{ + Q_OBJECT + +public: + explicit QrcReader(QObject *parent = nullptr) + : QObject(parent) + { + } + + bool read(QIODevice *input, QString *errorString) + { + Q_ASSERT(errorString); + + m_xml.setDevice(input); + + if (m_xml.readNextStartElement()) { + if (m_xml.name() == QLatin1String("RCC") + && (!m_xml.attributes().hasAttribute(QLatin1String("version")) + || m_xml.attributes().value(QLatin1String("version")) == QLatin1String("1.0"))) { + readRCC(); + } else { + m_xml.raiseError(QObject::tr("The file is not an RCC (version 1.0) file.")); + } + } + + if (m_xml.error()) + *errorString = m_xml.errorString(); + + return !m_xml.error(); + } + +signals: + void fileRead(const QString &resource, const QString &file, + QLocale::Language language, QLocale::Country country); + +private: + void readRCC() + { + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == QLatin1String("RCC")); + + while (m_xml.readNextStartElement()) { + if (m_xml.name() == QLatin1String("qresource")) + readQresource(); + else + raiseUnexpectedTagError(); + } + } + + void readQresource() + { + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == QLatin1String("qresource")); + + m_prefix = m_xml.attributes().value(QLatin1String("prefix")).toString(); + + m_language = QLocale::c().language(); + m_country = QLocale::c().country(); + QString lang = m_xml.attributes().value(QLatin1String("lang")).toString(); + if (!lang.isEmpty()) { + QLocale locale = QLocale(lang); + m_language = locale.language(); + m_country = lang.length() > 2 ? locale.country() : QLocale::AnyCountry; + } + + while (m_xml.readNextStartElement()) { + if (m_xml.name() == QLatin1String("file")) + readFile(); + else + raiseUnexpectedTagError(); + } + } + + void readFile() + { + Q_ASSERT(m_xml.isStartElement() && m_xml.name() == QLatin1String("file")); + + QString resource = m_xml.attributes().value(QLatin1String("alias")).toString(); + QString file = m_xml.readElementText(); + + if (resource.isEmpty()) + resource = file; + if (!m_prefix.isEmpty()) + resource = m_prefix + QLatin1Char('/') + resource; + if (resource.at(0) != QLatin1Char('/')) + resource.prepend(QLatin1String(":/")); + else + resource.prepend(QLatin1Char(':')); + + emit fileRead(resource, file, m_language, m_country); + } + + void raiseUnexpectedTagError() + { + m_xml.raiseError(QStringLiteral("Unexpected tag %1").arg(m_xml.name().toString())); + } + +private: + QXmlStreamReader m_xml; + QString m_prefix; + QLocale::Language m_language{QLocale::AnyLanguage}; + QLocale::Country m_country{QLocale::AnyCountry}; +}; + +/*! + * Constructs with the given \a parent object. + */ +ResourceMap::ResourceMap(QObject *parent) + : QObject(parent) +{ +} + +/*! + * Returns the resource that maps to the given \a document or an empty QString + * if no corresponding mapping exists. + * + * This is a low level API. LiveDocument provides more convenient API. + */ +QString ResourceMap::toResource(const LiveDocument &document) const +{ + LIVE_ASSERT(!document.isNull(), return QString()); + + QReadLocker locker(&m_lock); + auto localizedIt = m_resourcesByDocument.find(systemLocalePrefix() + document.relativeFilePath()); + if (localizedIt != m_resourcesByDocument.end()) + return *localizedIt; + auto nonlocalizedIt = m_resourcesByDocument.find(cLocalePrefix() + document.relativeFilePath()); + if (nonlocalizedIt != m_resourcesByDocument.end()) + return *nonlocalizedIt; + return QString(); +} + +/*! + * Returns the document that maps to the given \a resource or a null + * LiveDocument if no corresponding mapping exists. + * + * This is a low level API. LiveDocument provides more convenient API. + */ +LiveDocument ResourceMap::toDocument(const QString &resource) const +{ + LIVE_ASSERT(!resource.isEmpty(), return LiveDocument()); + + QReadLocker locker(&m_lock); + auto localizedIt = m_documentByResource.find(systemLocalePrefix() + resource); + if (localizedIt != m_documentByResource.end()) + return LiveDocument(*localizedIt); + auto nonlocalizedIt = m_documentByResource.find(cLocalePrefix() + resource); + if (nonlocalizedIt != m_documentByResource.end()) + return LiveDocument(*nonlocalizedIt); + return LiveDocument(); +} + +/*! + * Returns an user readable description of the last error occurred. + * + * The error string may be set by the updateMapping() call. + */ +QString ResourceMap::errorString() const +{ + return m_errorString; +} + +/*! + * Updates mapping from the given \c .qrc document + * + * Old mappings for the given \a qrcDocument will be removed, then new mappings + * will be added based on the content of the \a qrcFile. + * + * Returns \c true on success. Otherwise errorString() is set and \c false + * returned. + */ +bool ResourceMap::updateMapping(const LiveDocument &qrcDocument, QIODevice *qrcFile) +{ + LIVE_ASSERT(!qrcDocument.isNull(), return false); + LIVE_ASSERT(qrcFile, return false); + LIVE_ASSERT(qrcFile->openMode() & QIODevice::ReadOnly, return false); + + qCDebug(rmLog) << "Updating resource map for" << qrcDocument; + + const QString qrcPath = QFileInfo(qrcDocument.relativeFilePath()).path(); + + QWriteLocker locker(&m_lock); + + removeMapping(qrcDocument); + + auto addMapping = [&](const QString &resource, const QString &file, + QLocale::Language language, QLocale::Country country) { + QString localePrefix = toLocalePrefix(language, country); + QString prefixedResourceName = localePrefix + resource; + QString filePath = QDir::cleanPath(QFileInfo(qrcPath, file).filePath()); + + // The bench sends all .qrc files found in the workspace, not just those actually built-in + if (!QFileInfo(resource).exists()) { + qCDebug(rmLog) << "Not mapping" << prefixedResourceName << "to" << filePath << "(resource does not exist)"; + return; + } + qCDebug(rmLog) << "Mapping" << prefixedResourceName << "to" << filePath; + + m_resourcesByDocument.insertMulti(localePrefix + filePath, resource); + m_documentByResource.insert(prefixedResourceName, filePath); + m_resourcesByQrc.insert(qrcDocument.relativeFilePath(), prefixedResourceName); + }; + + QrcReader reader; + connect(&reader, &QrcReader::fileRead, addMapping); + + bool ok = reader.read(qrcFile, &m_errorString); + if (!ok) + removeMapping(qrcDocument); + + return ok; +} + +void ResourceMap::removeMapping(const LiveDocument &qrcDocument) +{ + LIVE_ASSERT(!qrcDocument.isNull(), return); + + auto it = m_resourcesByQrc.find(qrcDocument.relativeFilePath()); + while (it != m_resourcesByQrc.end() && it.key() == qrcDocument.relativeFilePath()) { + QString localePrefix = it->left(it->indexOf(QLatin1Char('/'))); + QString resource = it->mid(localePrefix.length() + 1); + QString file = m_documentByResource.take(*it); + m_resourcesByDocument.remove(localePrefix + file, resource); + it = m_resourcesByQrc.erase(it); + } +} + +QString ResourceMap::toLocalePrefix(QLocale::Language language, QLocale::Country country) +{ + return QLocale(language, country).name() + QLatin1Char('/'); +} + +QString ResourceMap::cLocalePrefix() +{ + return QLocale::c().name() + QLatin1Char('/'); +} + +QString ResourceMap::systemLocalePrefix() +{ + return QLocale::system().name() + QLatin1Char('/'); +} + +#include "resourcemap.moc" diff --git a/src/resourcemap.h b/src/resourcemap.h new file mode 100644 index 0000000..11726f6 --- /dev/null +++ b/src/resourcemap.h @@ -0,0 +1,66 @@ +/**************************************************************************** +** +** Copyright (C) 2018 Jolla Ltd +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QmlLive tool. +** +** $QT_BEGIN_LICENSE:GPL-QTAS$ +** Commercial License Usage +** Licensees holding valid commercial Qt Automotive Suite licenses may use +** this file in accordance with the commercial license agreement provided +** with the Software or, alternatively, in accordance with the terms +** contained in a written agreement between you and The Qt Company. For +** licensing terms and conditions see https://www.qt.io/terms-conditions. +** For further information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 or (at your option) any later version +** approved by the KDE Free Qt Foundation. The licenses are as published by +** the Free Software Foundation and appearing in the file LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +** SPDX-License-Identifier: GPL-3.0 +** +****************************************************************************/ + +#pragma once + +#include <QtCore> + +#include "qmllive_global.h" + +class LiveDocument; + +class QMLLIVESHARED_EXPORT ResourceMap : public QObject +{ + Q_OBJECT + +public: + explicit ResourceMap(QObject *parent = nullptr); + + QString toResource(const LiveDocument &document) const; + LiveDocument toDocument(const QString &resourceName) const; + + QString errorString() const; + + bool updateMapping(const LiveDocument &qrcDocument, QIODevice *qrcFile); + +private: + void removeMapping(const LiveDocument &qrcDocument); + static QString toLocalePrefix(QLocale::Language language, QLocale::Country country); + static QString cLocalePrefix(); + static QString systemLocalePrefix(); + +private: + mutable QReadWriteLock m_lock; + QString m_errorString; + QMultiHash<QString, QString> m_resourcesByDocument; + QHash<QString, QString> m_documentByResource; + QMultiHash<QString, QString> m_resourcesByQrc; +}; diff --git a/src/src.pri b/src/src.pri index e730078..dc7c5ea 100644 --- a/src/src.pri +++ b/src/src.pri @@ -23,7 +23,8 @@ SOURCES += \ $$PWD/remotelogger.cpp \ $$PWD/logreceiver.cpp \ $$PWD/fontadapter.cpp \ - $$PWD/projectmanager.cpp + $$PWD/projectmanager.cpp \ + $$PWD/resourcemap.cpp public_headers += \ $$PWD/livedocument.h \ @@ -37,7 +38,8 @@ public_headers += \ $$PWD/remotereceiver.h \ $$PWD/contentadapterinterface.h \ $$PWD/remotelogger.h \ - $$PWD/projectmanager.h + $$PWD/projectmanager.h \ + $$PWD/resourcemap.h HEADERS += \ $$PWD/constants.h \ |