core/qmljson: add support for synthesized .qml.json files

This commit is contained in:
outfoxxed 2025-05-18 20:39:48 -07:00
parent 6026c4ce27
commit 5193426cd7
Signed by untrusted user: outfoxxed
GPG key ID: 4C88A185FB89301E
5 changed files with 112 additions and 18 deletions

View file

@ -30,7 +30,7 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
: rootPath(rootPath) : rootPath(rootPath)
, scanner(std::move(scanner)) , scanner(std::move(scanner))
, urlInterceptor(this->rootPath) , urlInterceptor(this->rootPath)
, interceptNetFactory(this->scanner.qmldirIntercepts) , interceptNetFactory(this->scanner.fileIntercepts)
, engine(new QQmlEngine()) { , engine(new QQmlEngine()) {
g_generations.insert(this->engine, this); g_generations.insert(this->engine, this);

View file

@ -49,9 +49,9 @@ QUrl QsUrlInterceptor::intercept(
return url; return url;
} }
QsInterceptDataReply::QsInterceptDataReply(const QString& qmldir, QObject* parent) QsInterceptDataReply::QsInterceptDataReply(const QString& data, QObject* parent)
: QNetworkReply(parent) : QNetworkReply(parent)
, content(qmldir.toUtf8()) { , content(data.toUtf8()) {
this->setOpenMode(QIODevice::ReadOnly); this->setOpenMode(QIODevice::ReadOnly);
this->setFinished(true); this->setFinished(true);
} }
@ -65,11 +65,11 @@ qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) {
} }
QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager(
const QHash<QString, QString>& qmldirIntercepts, const QHash<QString, QString>& fileIntercepts,
QObject* parent QObject* parent
) )
: QNetworkAccessManager(parent) : QNetworkAccessManager(parent)
, qmldirIntercepts(qmldirIntercepts) {} , fileIntercepts(fileIntercepts) {}
QNetworkReply* QsInterceptNetworkAccessManager::createRequest( QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
QNetworkAccessManager::Operation op, QNetworkAccessManager::Operation op,
@ -80,10 +80,10 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
if (url.scheme() == "qsintercept") { if (url.scheme() == "qsintercept") {
auto path = url.path(); auto path = url.path();
qCDebug(logQsIntercept) << "Got intercept for" << path << "contains" qCDebug(logQsIntercept) << "Got intercept for" << path << "contains"
<< this->qmldirIntercepts.value(path); << this->fileIntercepts.value(path);
auto qmldir = this->qmldirIntercepts.value(path); auto data = this->fileIntercepts.value(path);
if (qmldir != nullptr) { if (data != nullptr) {
return new QsInterceptDataReply(qmldir, this); return new QsInterceptDataReply(data, this);
} }
auto fileReq = req; auto fileReq = req;
@ -98,5 +98,5 @@ QNetworkReply* QsInterceptNetworkAccessManager::createRequest(
} }
QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) {
return new QsInterceptNetworkAccessManager(this->qmldirIntercepts, parent); return new QsInterceptNetworkAccessManager(this->fileIntercepts, parent);
} }

View file

@ -26,7 +26,7 @@ class QsInterceptDataReply: public QNetworkReply {
Q_OBJECT; Q_OBJECT;
public: public:
QsInterceptDataReply(const QString& qmldir, QObject* parent = nullptr); QsInterceptDataReply(const QString& data, QObject* parent = nullptr);
qint64 readData(char* data, qint64 maxSize) override; qint64 readData(char* data, qint64 maxSize) override;
@ -43,7 +43,7 @@ class QsInterceptNetworkAccessManager: public QNetworkAccessManager {
public: public:
QsInterceptNetworkAccessManager( QsInterceptNetworkAccessManager(
const QHash<QString, QString>& qmldirIntercepts, const QHash<QString, QString>& fileIntercepts,
QObject* parent = nullptr QObject* parent = nullptr
); );
@ -55,15 +55,15 @@ protected:
) override; ) override;
private: private:
const QHash<QString, QString>& qmldirIntercepts; const QHash<QString, QString>& fileIntercepts;
}; };
class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory {
public: public:
QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& qmldirIntercepts) QsInterceptNetworkAccessManagerFactory(const QHash<QString, QString>& fileIntercepts)
: qmldirIntercepts(qmldirIntercepts) {} : fileIntercepts(fileIntercepts) {}
QNetworkAccessManager* create(QObject* parent) override; QNetworkAccessManager* create(QObject* parent) override;
private: private:
const QHash<QString, QString>& qmldirIntercepts; const QHash<QString, QString>& fileIntercepts;
}; };

View file

@ -1,11 +1,18 @@
#include "scan.hpp" #include "scan.hpp"
#include <cmath>
#include <qcontainerfwd.h> #include <qcontainerfwd.h>
#include <qdir.h> #include <qdir.h>
#include <qfileinfo.h> #include <qfileinfo.h>
#include <qjsonarray.h>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonvalue.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qpair.h>
#include <qstring.h> #include <qstring.h>
#include <qstringliteral.h>
#include <qtextstream.h> #include <qtextstream.h>
Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg);
@ -32,6 +39,9 @@ void QmlScanner::scanDir(const QString& path) {
} else { } else {
entries.push_back(entry); 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); 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; 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<QString, QString> 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<int>(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");
}
}

View file

@ -20,8 +20,11 @@ public:
QVector<QString> scannedDirs; QVector<QString> scannedDirs;
QVector<QString> scannedFiles; QVector<QString> scannedFiles;
QHash<QString, QString> qmldirIntercepts; QHash<QString, QString> fileIntercepts;
private: private:
QDir rootPath; QDir rootPath;
void scanQmlJson(const QString& path);
[[nodiscard]] static QPair<QString, QString> jsonToQml(const QJsonValue& value, int indent = 0);
}; };