From 14278ee1172b9ccec95a45a3403a02408525a839 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 16 May 2025 00:11:09 -0700 Subject: [PATCH] core/qmljson: add support for synthesized .qml.json files --- src/core/generation.cpp | 2 +- src/core/qsintercept.cpp | 18 ++++---- src/core/qsintercept.hpp | 12 +++--- src/core/scan.cpp | 93 +++++++++++++++++++++++++++++++++++++++- src/core/scan.hpp | 5 ++- 5 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 32f7586..f91bc18 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -30,7 +30,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) : rootPath(rootPath) , scanner(std::move(scanner)) , urlInterceptor(this->rootPath) - , interceptNetFactory(this->scanner.qmldirIntercepts) + , interceptNetFactory(this->scanner.fileIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index ba46ab7..12ca118 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -49,9 +49,9 @@ QUrl QsUrlInterceptor::intercept( return url; } -QsInterceptDataReply::QsInterceptDataReply(const QString& qmldir, QObject* parent) +QsInterceptDataReply::QsInterceptDataReply(const QString& data, QObject* parent) : QNetworkReply(parent) - , content(qmldir.toUtf8()) { + , content(data.toUtf8()) { this->setOpenMode(QIODevice::ReadOnly); this->setFinished(true); } @@ -65,11 +65,11 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) { } QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( - const QHash& qmldirIntercepts, + const QHash& fileIntercepts, QObject* parent ) : QNetworkAccessManager(parent) - , qmldirIntercepts(qmldirIntercepts) {} + , fileIntercepts(fileIntercepts) {} QNetworkReply* QsInterceptNetworkAccessManager::createRequest( QNetworkAccessManager::Operation op, @@ -80,10 +80,10 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( if (url.scheme() == "qsintercept") { auto path = url.path(); qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" - << this->qmldirIntercepts.value(path); - auto qmldir = this->qmldirIntercepts.value(path); - if (qmldir != nullptr) { - return new QsInterceptDataReply(qmldir, this); + << this->fileIntercepts.value(path); + auto data = this->fileIntercepts.value(path); + if (data != nullptr) { + return new QsInterceptDataReply(data, this); } auto fileReq = req; @@ -98,5 +98,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest( } QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { - return new QsInterceptNetworkAccessManager(this->qmldirIntercepts, parent); + return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent); } diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index 5792356..8113749 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -26,7 +26,7 @@ class QsInterceptDataReply: public QNetworkReply { Q_OBJECT; public: - QsInterceptDataReply(const QString& qmldir, QObject* parent = nullptr); + QsInterceptDataReply(const QString& data, QObject* parent = nullptr); qint64 readData(char* data, qint64 maxSize) override; @@ -43,7 +43,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager { public: QsInterceptNetworkAccessManager( - const QHash& qmldirIntercepts, + const QHash& fileIntercepts, QObject* parent = nullptr ); @@ -55,15 +55,15 @@ protected: ) override; private: - const QHash& qmldirIntercepts; + const QHash& fileIntercepts; }; class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { public: - QsInterceptNetworkAccessManagerFactory(const QHash& qmldirIntercepts) - : qmldirIntercepts(qmldirIntercepts) {} + QsInterceptNetworkAccessManagerFactory(const QHash& fileIntercepts) + : fileIntercepts(fileIntercepts) {} QNetworkAccessManager* create(QObject* parent) override; private: - const QHash& qmldirIntercepts; + const QHash& fileIntercepts; }; diff --git a/src/core/scan.cpp b/src/core/scan.cpp index 59ec05b..8d6362e 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -1,11 +1,18 @@ #include "scan.hpp" +#include #include #include #include +#include +#include +#include +#include #include #include +#include #include +#include #include Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); @@ -32,6 +39,9 @@ void QmlScanner::scanDir(const QString& path) { } else { entries.push_back(entry); } + } else if (entry.at(0).isUpper() && entry.endsWith(".qml.json")) { + this->scanQmlJson(dir.filePath(entry)); + singletons.push_back(entry.first(entry.length() - 5)); } } @@ -53,7 +63,7 @@ void QmlScanner::scanDir(const QString& path) { } qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); - this->qmldirIntercepts.insert(QDir(path).filePath("qmldir"), qmldir); + this->fileIntercepts.insert(QDir(path).filePath("qmldir"), qmldir); } } @@ -125,3 +135,84 @@ bool QmlScanner::scanQmlFile(const QString& path) { return singleton; } + +void QmlScanner::scanQmlJson(const QString& path) { + qCDebug(logQmlScanner) << "Scanning qml.json file" << path; + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCWarning(logQmlScanner) << "Failed to open file" << path; + return; + } + + auto data = file.readAll(); + + // Importing this makes CI builds fail for some reason. + QJsonParseError error; // NOLINT (misc-include-cleaner) + auto json = QJsonDocument::fromJson(data, &error); + + if (error.error != QJsonParseError::NoError) { + qCCritical(logQmlScanner).nospace() + << "Failed to parse qml.json file at " << path << ": " << error.errorString(); + return; + } + + const QString body = + "pragma Singleton\nimport QtQuick as Q\n\n" % QmlScanner::jsonToQml(json.object()).second; + + qCDebug(logQmlScanner) << "Synthesized qml file for" << path << qPrintable("\n" + body); + + this->fileIntercepts.insert(path.first(path.length() - 5), body); + this->scannedFiles.push_back(path); +} + +QPair QmlScanner::jsonToQml(const QJsonValue& value, int indent) { + if (value.isObject()) { + const auto& object = value.toObject(); + + auto valIter = object.constBegin(); + + QString accum = "Q.QtObject {\n"; + for (const auto& key: object.keys()) { + const auto& val = *valIter++; + auto [type, repr] = QmlScanner::jsonToQml(val, indent + 2); + accum += QString(' ').repeated(indent + 2) % "readonly property " % type % ' ' % key % ": " + % repr % ";\n"; + } + + accum += QString(' ').repeated(indent) % '}'; + return qMakePair(QStringLiteral("Q.QtObject"), accum); + } else if (value.isArray()) { + return qMakePair( + QStringLiteral("var"), + QJsonDocument(value.toArray()).toJson(QJsonDocument::Compact) + ); + } else if (value.isString()) { + const auto& str = value.toString(); + + if (str.startsWith('#') && (str.length() == 4 || str.length() == 7 || str.length() == 9)) { + for (auto c: str.sliced(1)) { + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) { + goto noncolor; + } + } + + return qMakePair(QStringLiteral("Q.color"), '"' % str % '"'); + } + + noncolor: + return qMakePair(QStringLiteral("string"), '"' % QString(str).replace("\"", "\\\"") % '"'); + } else if (value.isDouble()) { + auto num = value.toDouble(); + double whole = 0; + if (std::modf(num, &whole) == 0.0) { + return qMakePair(QStringLiteral("int"), QString::number(static_cast(whole))); + } else { + return qMakePair(QStringLiteral("real"), QString::number(num)); + } + } else if (value.isBool()) { + return qMakePair(QStringLiteral("bool"), value.toBool() ? "true" : "false"); + } else { + return qMakePair(QStringLiteral("var"), "null"); + } +} diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 0b4f160..d8fb500 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -20,8 +20,11 @@ public: QVector scannedDirs; QVector scannedFiles; - QHash qmldirIntercepts; + QHash fileIntercepts; private: QDir rootPath; + + void scanQmlJson(const QString& path); + [[nodiscard]] static QPair jsonToQml(const QJsonValue& value, int indent = 0); };