forked from quickshell/quickshell
core: support qs. imports
This commit is contained in:
parent
3d594e16dd
commit
4b35d7b51b
7 changed files with 123 additions and 35 deletions
|
@ -37,7 +37,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|||
: rootPath(rootPath)
|
||||
, scanner(std::move(scanner))
|
||||
, urlInterceptor(this->rootPath)
|
||||
, interceptNetFactory(this->scanner.fileIntercepts)
|
||||
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
||||
, engine(new QQmlEngine()) {
|
||||
g_generations.insert(this->engine, this);
|
||||
|
||||
|
@ -45,6 +45,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|||
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
||||
|
||||
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
||||
this->engine->addImportPath("qs:@/");
|
||||
|
||||
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
||||
this->engine->setIncubationController(&this->delayedIncubationController);
|
||||
|
||||
|
@ -322,9 +324,11 @@ void EngineGeneration::incubationControllerDestroyed() {
|
|||
}
|
||||
}
|
||||
|
||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) const {
|
||||
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
||||
for (const auto& error: warnings) {
|
||||
auto rel = "**/" % this->rootPath.relativeFilePath(error.url().path());
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
|
||||
QString objectName;
|
||||
auto desc = error.description();
|
||||
|
|
|
@ -84,7 +84,7 @@ private slots:
|
|||
void onFileChanged(const QString& name);
|
||||
void onDirectoryChanged();
|
||||
void incubationControllerDestroyed();
|
||||
void onEngineWarnings(const QList<QQmlError>& warnings) const;
|
||||
static void onEngineWarnings(const QList<QQmlError>& warnings);
|
||||
|
||||
private:
|
||||
void postReload();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "qsintercept.hpp"
|
||||
#include <cstring>
|
||||
|
||||
#include <qdir.h>
|
||||
#include <qhash.h>
|
||||
#include <qiodevice.h>
|
||||
#include <qlogging.h>
|
||||
|
@ -25,27 +26,44 @@ QUrl QsUrlInterceptor::intercept(
|
|||
auto url = originalUrl;
|
||||
|
||||
if (url.scheme() == "root") {
|
||||
url.setScheme("qsintercept");
|
||||
url.setScheme("qs");
|
||||
|
||||
auto path = url.path();
|
||||
if (path.startsWith('/')) path = path.sliced(1);
|
||||
url.setPath(this->configRoot.filePath(path));
|
||||
url.setPath("@/qs/" % path);
|
||||
|
||||
qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url;
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qsintercept is considered to be over a network.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (url.path().endsWith(".qml")) return url;
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
// Our import path is on "qs:@/".
|
||||
// We want to blackhole any import resolution outside of the config folder as it breaks Qt
|
||||
// but NOT file lookups that might be on "qs:/" due to a missing "file:/" prefix.
|
||||
if (path.startsWith("@/qs/")) {
|
||||
path = this->configRoot.filePath(path.sliced(5));
|
||||
} else if (!path.startsWith("/")) {
|
||||
qCDebug(logQsIntercept) << "Blackholed import URL" << url;
|
||||
return QUrl("qrc:/qs-blackhole");
|
||||
}
|
||||
|
||||
// Some types such as Image take into account where they are loading from, and force
|
||||
// asynchronous loading over a network. qs: is considered to be over a network.
|
||||
// In those cases we want to return a file:// url so asynchronous loading is not forced.
|
||||
if (type == QQmlAbstractUrlInterceptor::DataType::UrlString) {
|
||||
// Qt.resolvedUrl and context->resolvedUrl can use this on qml files, in which
|
||||
// case we want to keep the intercept, otherwise objects created from those paths
|
||||
// will not be able to use singletons.
|
||||
if (path.endsWith(".qml")) return url;
|
||||
|
||||
auto newUrl = url;
|
||||
newUrl.setScheme("file");
|
||||
// above check asserts path starts with /qs/
|
||||
newUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Rewrote intercept" << url << "to" << newUrl;
|
||||
return newUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
|
@ -67,10 +85,12 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
|
|||
}
|
||||
|
||||
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent
|
||||
)
|
||||
: QNetworkAccessManager(parent)
|
||||
, configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
|
||||
QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
||||
|
@ -79,19 +99,26 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
|||
QIODevice* outgoingData
|
||||
) {
|
||||
auto url = req.url();
|
||||
if (url.scheme() == "qsintercept") {
|
||||
|
||||
if (url.scheme() == "qs") {
|
||||
auto path = url.path();
|
||||
|
||||
if (path.startsWith("@/qs/")) path = this->configRoot.filePath(path.sliced(5));
|
||||
// otherwise pass through to fs
|
||||
|
||||
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
|
||||
<< this->fileIntercepts.value(path);
|
||||
auto data = this->fileIntercepts.value(path);
|
||||
if (data != nullptr) {
|
||||
|
||||
if (auto data = this->fileIntercepts.value(path); !data.isEmpty()) {
|
||||
return new QsInterceptDataReply(data, this);
|
||||
}
|
||||
|
||||
auto fileReq = req;
|
||||
auto fileUrl = req.url();
|
||||
fileUrl.setScheme("file");
|
||||
fileUrl.setPath(path);
|
||||
qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl;
|
||||
|
||||
fileReq.setUrl(fileUrl);
|
||||
return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData);
|
||||
}
|
||||
|
@ -100,5 +127,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
|
|||
}
|
||||
|
||||
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
|
||||
return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
|
||||
return new QsInterceptNetworkAccessManager(this->configRoot, this->fileIntercepts, parent);
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
|
|||
|
||||
public:
|
||||
QsInterceptNetworkAccessManager(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts,
|
||||
QObject* parent = nullptr
|
||||
);
|
||||
|
@ -57,15 +58,21 @@ protected:
|
|||
) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
|
||||
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
|
||||
public:
|
||||
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
|
||||
: fileIntercepts(fileIntercepts) {}
|
||||
QsInterceptNetworkAccessManagerFactory(
|
||||
const QDir& configRoot,
|
||||
const QHash<QString, QString>& fileIntercepts
|
||||
)
|
||||
: configRoot(configRoot)
|
||||
, fileIntercepts(fileIntercepts) {}
|
||||
QNetworkAccessManager* create(QObject* parent) override;
|
||||
|
||||
private:
|
||||
QDir configRoot;
|
||||
const QHash<QString, QString>& fileIntercepts;
|
||||
};
|
||||
|
|
|
@ -43,7 +43,8 @@ RootWrapper::~RootWrapper() {
|
|||
}
|
||||
|
||||
void RootWrapper::reloadGraph(bool hard) {
|
||||
auto rootPath = QFileInfo(this->rootPath).dir();
|
||||
auto rootFile = QFileInfo(this->rootPath);
|
||||
auto rootPath = rootFile.dir();
|
||||
auto scanner = QmlScanner(rootPath);
|
||||
scanner.scanQmlFile(this->rootPath);
|
||||
|
||||
|
@ -58,9 +59,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
|||
|
||||
QDir::setCurrent(this->originalWorkingDirectory);
|
||||
|
||||
auto url = QUrl::fromLocalFile(this->rootPath);
|
||||
// unless the original file comes from the qsintercept scheme
|
||||
url.setScheme("qsintercept");
|
||||
QUrl url;
|
||||
url.setScheme("qs");
|
||||
url.setPath("@/qs/" % rootFile.fileName());
|
||||
auto component = QQmlComponent(generation->engine, url);
|
||||
|
||||
if (!component.isReady()) {
|
||||
|
@ -69,7 +70,9 @@ void RootWrapper::reloadGraph(bool hard) {
|
|||
|
||||
auto errors = component.errors();
|
||||
for (auto& error: errors) {
|
||||
auto rel = "**/" % rootPath.relativeFilePath(error.url().path());
|
||||
const auto& url = error.url();
|
||||
auto rel = url.scheme() == "qs" && url.path().startsWith("@/qs/") ? "@" % url.path().sliced(5)
|
||||
: url.toString();
|
||||
auto msg = " caused by " % rel % '[' % QString::number(error.line()) % ':'
|
||||
% QString::number(error.column()) % "]: " % error.description();
|
||||
errorString += '\n' % msg;
|
||||
|
|
|
@ -47,7 +47,6 @@ void QmlScanner::scanDir(const QString& path) {
|
|||
}
|
||||
}
|
||||
|
||||
// Due to the qsintercept:// protocol a qmldir is always required, even without singletons.
|
||||
if (!seenQmldir) {
|
||||
qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons"
|
||||
<< singletons;
|
||||
|
@ -55,6 +54,29 @@ void QmlScanner::scanDir(const QString& path) {
|
|||
QString qmldir;
|
||||
auto stream = QTextStream(&qmldir);
|
||||
|
||||
// cant derive a module name if not in shell path
|
||||
if (path.startsWith(this->rootPath.path())) {
|
||||
auto end = path.sliced(this->rootPath.path().length());
|
||||
|
||||
// verify we have a valid module name.
|
||||
for (auto& c: end) {
|
||||
if (c == '/') c = '.';
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner)
|
||||
<< "Module path contains invalid characters for a module name: " << end;
|
||||
goto skipadd;
|
||||
}
|
||||
}
|
||||
|
||||
stream << "module qs" << end << '\n';
|
||||
skipadd:;
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Module path" << path << "is outside of the config folder.";
|
||||
}
|
||||
|
||||
for (auto& singleton: singletons) {
|
||||
stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton
|
||||
<< "\n";
|
||||
|
@ -92,15 +114,39 @@ bool QmlScanner::scanQmlFile(const QString& path) {
|
|||
qCDebug(logQmlScanner) << "Discovered singleton" << path;
|
||||
singleton = true;
|
||||
} else if (line.startsWith("import")) {
|
||||
// we dont care about "import qs" as we always load the root folder
|
||||
if (auto importCursor = line.indexOf(" qs."); importCursor != -1) {
|
||||
importCursor += 4;
|
||||
QString path;
|
||||
|
||||
auto startQuot = line.indexOf('"');
|
||||
if (startQuot == -1 || line.length() < startQuot + 3) continue;
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
while (importCursor != line.length()) {
|
||||
auto c = line.at(importCursor);
|
||||
if (c == '.') c = '/';
|
||||
else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
|
||||
|| c == '_')
|
||||
{
|
||||
} else {
|
||||
qCWarning(logQmlScanner) << "Import line contains invalid characters: " << line;
|
||||
goto next;
|
||||
}
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
path.append(c);
|
||||
importCursor += 1;
|
||||
}
|
||||
|
||||
imports.append(this->rootPath.filePath(path));
|
||||
} else if (auto startQuot = line.indexOf('"');
|
||||
startQuot != -1 && line.length() >= startQuot + 3)
|
||||
{
|
||||
auto endQuot = line.indexOf('"', startQuot + 1);
|
||||
if (endQuot == -1) continue;
|
||||
|
||||
auto name = line.sliced(startQuot + 1, endQuot - startQuot - 1);
|
||||
imports.push_back(name);
|
||||
}
|
||||
} else if (line.contains('{')) break;
|
||||
|
||||
next:;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
|
|
@ -16,6 +16,7 @@ public:
|
|||
QmlScanner() = default;
|
||||
QmlScanner(const QDir& rootPath): rootPath(rootPath) {}
|
||||
|
||||
// path must be canonical
|
||||
void scanDir(const QString& path);
|
||||
// returns if the file has a singleton
|
||||
bool scanQmlFile(const QString& path);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue