diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 4eab3fad..c1535ad7 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -9,7 +9,6 @@ qt_add_library(quickshell-core STATIC rootwrapper.cpp qmlglobal.cpp qmlscreen.cpp - watcher.cpp region.cpp persistentprops.cpp windowinterface.cpp @@ -18,6 +17,8 @@ qt_add_library(quickshell-core STATIC popupwindow.cpp singleton.cpp generation.cpp + scan.cpp + qsintercept.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/enginecontext.hpp b/src/core/enginecontext.hpp new file mode 100644 index 00000000..6675fc30 --- /dev/null +++ b/src/core/enginecontext.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "qsintercept.hpp" +#include "scan.hpp" +#include "singleton.hpp" + +class EngineContext { +public: + explicit EngineContext(const QmlScanner& scanner); + +private: + const QmlScanner& scanner; + QQmlEngine engine; + QsInterceptNetworkAccessManagerFactory interceptFactory; + SingletonRegistry singletonRegistry; +}; diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 6454a002..d48ffb21 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -1,7 +1,9 @@ #include "generation.hpp" +#include #include #include +#include #include #include #include @@ -9,11 +11,19 @@ #include #include "plugin.hpp" +#include "qsintercept.hpp" #include "reload.hpp" +#include "scan.hpp" static QHash g_generations; // NOLINT -EngineGeneration::EngineGeneration() { g_generations.insert(&this->engine, this); } +EngineGeneration::EngineGeneration(QmlScanner scanner) + : scanner(std::move(scanner)) + , interceptNetFactory(this->scanner.qmldirIntercepts) { + g_generations.insert(&this->engine, this); + + this->engine.setNetworkAccessManagerFactory(&this->interceptNetFactory); +} EngineGeneration::~EngineGeneration() { g_generations.remove(&this->engine); @@ -41,6 +51,30 @@ void EngineGeneration::onReload(EngineGeneration* old) { } } +void EngineGeneration::setWatchingFiles(bool watching) { + if (watching) { + if (this->watcher == nullptr) { + this->watcher = new QFileSystemWatcher(); + + for (auto& file: this->scanner.scannedFiles) { + this->watcher->addPath(file); + } + + QObject::connect( + this->watcher, + &QFileSystemWatcher::fileChanged, + this, + &EngineGeneration::filesChanged + ); + } + } else { + if (this->watcher != nullptr) { + delete this->watcher; + this->watcher = nullptr; + } + } +} + EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index f62149f1..a2fef8ad 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,23 +1,35 @@ #pragma once +#include #include #include +#include "qsintercept.hpp" +#include "scan.hpp" #include "shell.hpp" #include "singleton.hpp" -class EngineGeneration { +class EngineGeneration: public QObject { + Q_OBJECT; + public: - explicit EngineGeneration(); - ~EngineGeneration(); + explicit EngineGeneration(QmlScanner scanner); + ~EngineGeneration() override; Q_DISABLE_COPY_MOVE(EngineGeneration); // assumes root has been initialized, consumes old generation void onReload(EngineGeneration* old); + void setWatchingFiles(bool watching); static EngineGeneration* findObjectGeneration(QObject* object); + QmlScanner scanner; + QsInterceptNetworkAccessManagerFactory interceptNetFactory; QQmlEngine engine; ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; + QFileSystemWatcher* watcher = nullptr; + +signals: + void filesChanged(); }; diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp new file mode 100644 index 00000000..35b92347 --- /dev/null +++ b/src/core/qsintercept.cpp @@ -0,0 +1,67 @@ +#include "qsintercept.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); + +QsInterceptDataReply::QsInterceptDataReply(const QString& qmldir, QObject* parent) + : QNetworkReply(parent) + , content(qmldir.toUtf8()) { + this->setOpenMode(QIODevice::ReadOnly); + this->setFinished(true); +} + +qint64 QsInterceptDataReply::readData(char* data, qint64 maxSize) { + auto size = qMin(maxSize, this->content.length() - this->offset); + if (size == 0) return -1; + memcpy(data, this->content.constData() + this->offset, size); // NOLINT + this->offset += size; + return size; +} + +QsInterceptNetworkAccessManager::QsInterceptNetworkAccessManager( + const QHash& qmldirIntercepts, + QObject* parent +) + : QNetworkAccessManager(parent) + , qmldirIntercepts(qmldirIntercepts) {} + +QNetworkReply* QsInterceptNetworkAccessManager::createRequest( + QNetworkAccessManager::Operation op, + const QNetworkRequest& req, + QIODevice* outgoingData +) { + auto url = req.url(); + 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); + } + + auto fileReq = req; + auto fileUrl = req.url(); + fileUrl.setScheme("file"); + qCDebug(logQsIntercept) << "Passing through intercept" << url << "to" << fileUrl; + fileReq.setUrl(fileUrl); + return this->QNetworkAccessManager::createRequest(op, fileReq, outgoingData); + } + + return this->QNetworkAccessManager::createRequest(op, req, outgoingData); +} + +QNetworkAccessManager* QsInterceptNetworkAccessManagerFactory::create(QObject* parent) { + return new QsInterceptNetworkAccessManager(this->qmldirIntercepts, parent); +} diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp new file mode 100644 index 00000000..7e84da9f --- /dev/null +++ b/src/core/qsintercept.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); + +class QsInterceptDataReply: public QNetworkReply { + Q_OBJECT; + +public: + QsInterceptDataReply(const QString& qmldir, QObject* parent = nullptr); + + qint64 readData(char* data, qint64 maxSize) override; + +private slots: + void abort() override {} + +private: + qint64 offset = 0; + QByteArray content; +}; + +class QsInterceptNetworkAccessManager: public QNetworkAccessManager { + Q_OBJECT; + +public: + QsInterceptNetworkAccessManager( + const QHash& qmldirIntercepts, + QObject* parent = nullptr + ); + +protected: + QNetworkReply* createRequest( + QNetworkAccessManager::Operation op, + const QNetworkRequest& req, + QIODevice* outgoingData = nullptr + ) override; + +private: + const QHash& qmldirIntercepts; +}; + +class QsInterceptNetworkAccessManagerFactory: public QQmlNetworkAccessManagerFactory { +public: + QsInterceptNetworkAccessManagerFactory(const QHash& qmldirIntercepts) + : qmldirIntercepts(qmldirIntercepts) {} + QNetworkAccessManager* create(QObject* parent) override; + +private: + const QHash& qmldirIntercepts; +}; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index bcbc56be..fbb624ee 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -12,8 +12,8 @@ #include "generation.hpp" #include "qmlglobal.hpp" +#include "scan.hpp" #include "shell.hpp" -#include "watcher.hpp" RootWrapper::RootWrapper(QString rootPath) : QObject(nullptr) @@ -42,7 +42,10 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto* generation = new EngineGeneration(); + auto scanner = QmlScanner(); + scanner.scanQmlFile(this->rootPath); + + auto* generation = new EngineGeneration(std::move(scanner)); // todo: move into EngineGeneration if (this->generation != nullptr) { @@ -51,7 +54,10 @@ void RootWrapper::reloadGraph(bool hard) { QDir::setCurrent(this->originalWorkingDirectory); - auto component = QQmlComponent(&generation->engine, QUrl::fromLocalFile(this->rootPath)); + auto url = QUrl::fromLocalFile(this->rootPath); + // unless the original file comes from the qsintercept scheme + url.setScheme("qsintercept"); + auto component = QQmlComponent(&generation->engine, url); auto* obj = component.beginCreate(generation->engine.rootContext()); @@ -80,25 +86,20 @@ void RootWrapper::reloadGraph(bool hard) { qInfo() << "Configuration Loaded"; + QObject::connect( + this->generation, + &EngineGeneration::filesChanged, + this, + &RootWrapper::onWatchedFilesChanged + ); + this->onWatchFilesChanged(); } void RootWrapper::onWatchFilesChanged() { auto watchFiles = QuickshellSettings::instance()->watchFiles(); - - if (watchFiles && this->configWatcher == nullptr) { - this->configWatcher = new FiletreeWatcher(); - this->configWatcher->addPath(QFileInfo(this->rootPath).dir().path()); - - QObject::connect( - this->configWatcher, - &FiletreeWatcher::fileChanged, - this, - &RootWrapper::onWatchedFilesChanged - ); - } else if (!watchFiles && this->configWatcher != nullptr) { - this->configWatcher->deleteLater(); - this->configWatcher = nullptr; + if (this->generation != nullptr) { + this->generation->setWatchingFiles(watchFiles); } } diff --git a/src/core/rootwrapper.hpp b/src/core/rootwrapper.hpp index 7b39d4af..7958ee5c 100644 --- a/src/core/rootwrapper.hpp +++ b/src/core/rootwrapper.hpp @@ -7,7 +7,6 @@ #include #include "generation.hpp" -#include "watcher.hpp" class RootWrapper: public QObject { Q_OBJECT; @@ -26,6 +25,5 @@ private slots: private: QString rootPath; EngineGeneration* generation = nullptr; - FiletreeWatcher* configWatcher = nullptr; QString originalWorkingDirectory; }; diff --git a/src/core/scan.cpp b/src/core/scan.cpp new file mode 100644 index 00000000..f5f078aa --- /dev/null +++ b/src/core/scan.cpp @@ -0,0 +1,119 @@ +#include "scan.hpp" + +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logQmlScanner, "quickshell.qmlscanner", QtWarningMsg); + +void QmlScanner::scanDir(const QString& path) { + if (this->scannedDirs.contains(path)) return; + this->scannedDirs.push_back(path); + + qCDebug(logQmlScanner) << "Scanning directory" << path; + auto dir = QDir(path); + + bool seenQmldir = false; + auto singletons = QVector(); + auto entries = QVector(); + for (auto& entry: dir.entryList(QDir::Files | QDir::NoDotAndDotDot)) { + if (entry == "qmldir") { + qCDebug(logQmlScanner + ) << "Found qmldir file, qmldir synthesization will be disabled for directory" + << path; + seenQmldir = true; + } else if (entry.at(0).isUpper() && entry.endsWith(".qml")) { + if (this->scanQmlFile(dir.filePath(entry))) { + singletons.push_back(entry); + } else { + entries.push_back(entry); + } + } + } + + // Due to the qsintercept:// protocol a qmldir is always required, even without singletons. + if (!seenQmldir) { + qCDebug(logQmlScanner) << "Synthesizing qmldir for directory" << path << "singletons" + << singletons; + + QString qmldir; + auto stream = QTextStream(&qmldir); + + for (auto& singleton: singletons) { + stream << "singleton " << singleton.sliced(0, singleton.length() - 4) << " 1.0 " << singleton + << "\n"; + } + + for (auto& entry: entries) { + stream << entry.sliced(0, entry.length() - 4) << " 1.0 " << entry << "\n"; + } + + qCDebug(logQmlScanner) << "Synthesized qmldir for" << path << qPrintable("\n" + qmldir); + this->qmldirIntercepts.insert(QDir(path).filePath("qmldir"), qmldir); + } +} + +bool QmlScanner::scanQmlFile(const QString& path) { + if (this->scannedFiles.contains(path)) return false; + this->scannedFiles.push_back(path); + + qCDebug(logQmlScanner) << "Scanning qml file" << path; + + auto file = QFile(path); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + qCWarning(logQmlScanner) << "Failed to open file" << path; + return false; + } + + auto stream = QTextStream(&file); + auto imports = QVector(); + + bool singleton = false; + + while (!stream.atEnd()) { + auto line = stream.readLine().trimmed(); + if (!singleton && line == "pragma Singleton") { + qCDebug(logQmlScanner) << "Discovered singleton" << path; + singleton = true; + } else if (line.startsWith("import")) { + + auto startQuot = line.indexOf('"'); + if (startQuot == -1 || line.length() < startQuot + 3) continue; + 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; + } + + file.close(); + + if (logQmlScanner().isDebugEnabled() && !imports.isEmpty()) { + qCDebug(logQmlScanner) << "Found imports" << imports; + } + + auto currentdir = QDir(QFileInfo(path).canonicalPath()); + + // the root can never be a singleton so it dosent matter if we skip it + this->scanDir(currentdir.path()); + + for (auto& import: imports) { + auto ipath = currentdir.filePath(import); + auto cpath = QFileInfo(ipath).canonicalFilePath(); + + if (cpath.isEmpty()) { + qCWarning(logQmlScanner) << "Ignoring unresolvable import" << ipath << "from" << path; + continue; + } + + if (import.endsWith(".js")) this->scannedFiles.push_back(cpath); + else this->scanDir(cpath); + } + + return singleton; +} diff --git a/src/core/scan.hpp b/src/core/scan.hpp new file mode 100644 index 00000000..32a6166d --- /dev/null +++ b/src/core/scan.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); + +// expects canonical paths +class QmlScanner { +public: + void scanDir(const QString& path); + // returns if the file has a singleton + bool scanQmlFile(const QString& path); + + QVector scannedDirs; + QVector scannedFiles; + QHash qmldirIntercepts; +}; diff --git a/src/core/singleton.cpp b/src/core/singleton.cpp index ef03a741..e3f0b333 100644 --- a/src/core/singleton.cpp +++ b/src/core/singleton.cpp @@ -1,11 +1,10 @@ #include "singleton.hpp" +#include #include -#include #include #include #include -#include #include "generation.hpp" #include "reload.hpp" diff --git a/src/core/singleton.hpp b/src/core/singleton.hpp index cbbce41a..200c97f1 100644 --- a/src/core/singleton.hpp +++ b/src/core/singleton.hpp @@ -1,9 +1,11 @@ #pragma once +#include #include +#include #include -#include #include +#include #include #include "reload.hpp" @@ -26,5 +28,5 @@ public: void onReload(SingletonRegistry* old); private: - QMap registry; + QHash registry; }; diff --git a/src/core/watcher.cpp b/src/core/watcher.cpp deleted file mode 100644 index 6b06d584..00000000 --- a/src/core/watcher.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "watcher.hpp" - -#include -#include -#include -#include -#include - -FiletreeWatcher::FiletreeWatcher(QObject* parent): QObject(parent) { - QObject::connect( - &this->watcher, - &QFileSystemWatcher::fileChanged, - this, - &FiletreeWatcher::onFileChanged - ); - - QObject::connect( - &this->watcher, - &QFileSystemWatcher::directoryChanged, - this, - &FiletreeWatcher::onDirectoryChanged - ); -} -void FiletreeWatcher::addPath(const QString& path) { - this->watcher.addPath(path); - - if (QFileInfo(path).isDir()) { - auto dir = QDir(path); - - for (auto& entry: dir.entryList(QDir::AllEntries | QDir::NoDotAndDotDot)) { - this->addPath(dir.filePath(entry)); - } - } -} - -void FiletreeWatcher::onDirectoryChanged(const QString& path) { this->addPath(path); } - -void FiletreeWatcher::onFileChanged(const QString& path) { emit this->fileChanged(path); } diff --git a/src/core/watcher.hpp b/src/core/watcher.hpp deleted file mode 100644 index a729f03c..00000000 --- a/src/core/watcher.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include -#include -#include - -class FiletreeWatcher: public QObject { - Q_OBJECT; - -public: - explicit FiletreeWatcher(QObject* parent = nullptr); - - void addPath(const QString& path); - -signals: - void fileChanged(const QString& path); - -private slots: - void onDirectoryChanged(const QString& path); - void onFileChanged(const QString& path); - -private: - QFileSystemWatcher watcher; -};