mirror of
https://git.outfoxxed.me/quickshell/quickshell.git
synced 2026-04-10 06:11:54 +10:00
345 lines
9.7 KiB
C++
345 lines
9.7 KiB
C++
#include "generation.hpp"
|
|
#include <utility>
|
|
|
|
#include <qcontainerfwd.h>
|
|
#include <qcoreapplication.h>
|
|
#include <qdebug.h>
|
|
#include <qdir.h>
|
|
#include <qfileinfo.h>
|
|
#include <qfilesystemwatcher.h>
|
|
#include <qhash.h>
|
|
#include <qlist.h>
|
|
#include <qlogging.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qobject.h>
|
|
#include <qqmlcontext.h>
|
|
#include <qqmlengine.h>
|
|
#include <qqmlerror.h>
|
|
#include <qqmlincubator.h>
|
|
#include <qquickwindow.h>
|
|
#include <qtmetamacros.h>
|
|
|
|
#include "iconimageprovider.hpp"
|
|
#include "imageprovider.hpp"
|
|
#include "incubator.hpp"
|
|
#include "logcat.hpp"
|
|
#include "plugin.hpp"
|
|
#include "qsintercept.hpp"
|
|
#include "reload.hpp"
|
|
#include "scan.hpp"
|
|
|
|
namespace {
|
|
QS_LOGGING_CATEGORY(logScene, "scene");
|
|
}
|
|
|
|
static QHash<const QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
|
|
|
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|
: rootPath(rootPath)
|
|
, scanner(std::move(scanner))
|
|
, urlInterceptor(this->rootPath)
|
|
, interceptNetFactory(this->rootPath, this->scanner.fileIntercepts)
|
|
, engine(new QQmlEngine()) {
|
|
g_generations.insert(this->engine, this);
|
|
|
|
this->engine->setOutputWarningsToStandardError(false);
|
|
QObject::connect(this->engine, &QQmlEngine::warnings, this, &EngineGeneration::onEngineWarnings);
|
|
|
|
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
|
this->engine->addImportPath("qs:@/");
|
|
|
|
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
|
this->incubationController.initLoop();
|
|
this->engine->setIncubationController(&this->incubationController);
|
|
|
|
this->engine->addImageProvider("icon", new IconImageProvider());
|
|
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
|
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
|
|
|
QsEnginePlugin::runConstructGeneration(*this);
|
|
}
|
|
|
|
EngineGeneration::EngineGeneration(): EngineGeneration(QDir(), QmlScanner()) {}
|
|
|
|
EngineGeneration::~EngineGeneration() {
|
|
if (this->engine != nullptr) {
|
|
qFatal() << this << "destroyed without calling destroy()";
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::destroy() {
|
|
if (this->destroying) return;
|
|
this->destroying = true;
|
|
|
|
if (this->watcher != nullptr) {
|
|
// Multiple generations can detect a reload at the same time.
|
|
QObject::disconnect(this->watcher, nullptr, this, nullptr);
|
|
this->watcher->deleteLater();
|
|
this->watcher = nullptr;
|
|
}
|
|
|
|
for (auto* extension: this->extensions.values()) {
|
|
delete extension;
|
|
}
|
|
|
|
if (this->root != nullptr) {
|
|
QObject::connect(this->root, &QObject::destroyed, this, [this]() {
|
|
// prevent further js execution between garbage collection and engine destruction.
|
|
this->engine->setInterrupted(true);
|
|
|
|
g_generations.remove(this->engine);
|
|
|
|
// Garbage is not collected during engine destruction.
|
|
this->engine->collectGarbage();
|
|
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
|
|
auto terminate = this->shouldTerminate;
|
|
auto code = this->exitCode;
|
|
delete this;
|
|
|
|
if (terminate) QCoreApplication::exit(code);
|
|
});
|
|
|
|
this->root->deleteLater();
|
|
this->root = nullptr;
|
|
} else {
|
|
g_generations.remove(this->engine);
|
|
|
|
// the engine has never been used, no need to clean up
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
|
|
auto terminate = this->shouldTerminate;
|
|
auto code = this->exitCode;
|
|
delete this;
|
|
|
|
if (terminate) QCoreApplication::exit(code);
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::shutdown() {
|
|
if (this->destroying) return;
|
|
|
|
delete this->root;
|
|
this->root = nullptr;
|
|
delete this->engine;
|
|
this->engine = nullptr;
|
|
delete this;
|
|
}
|
|
|
|
void EngineGeneration::onReload(EngineGeneration* old) {
|
|
if (old != nullptr) {
|
|
// if the old generation holds the window incubation controller as the
|
|
// new generation acquires it then incubators will hang intermittently
|
|
qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old;
|
|
old->incubationControllersLocked = true;
|
|
old->updateIncubationMode();
|
|
}
|
|
|
|
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
|
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
|
|
|
|
if (auto* reloadable = qobject_cast<Reloadable*>(this->root)) {
|
|
reloadable->reload(old ? old->root : nullptr);
|
|
}
|
|
|
|
this->singletonRegistry.onReload(old == nullptr ? nullptr : &old->singletonRegistry);
|
|
this->reloadComplete = true;
|
|
emit this->reloadFinished();
|
|
|
|
if (old != nullptr) {
|
|
QObject::connect(old, &QObject::destroyed, this, [this]() { this->postReload(); });
|
|
old->destroy();
|
|
} else {
|
|
this->postReload();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::postReload() {
|
|
// This can be called on a generation during its destruction.
|
|
if (this->engine == nullptr || this->root == nullptr) return;
|
|
|
|
QsEnginePlugin::runOnReload();
|
|
|
|
emit this->firePostReload();
|
|
QObject::disconnect(this, &EngineGeneration::firePostReload, nullptr, nullptr);
|
|
}
|
|
|
|
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);
|
|
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
|
}
|
|
|
|
for (auto& file: this->extraWatchedFiles) {
|
|
this->watcher->addPath(file);
|
|
this->watcher->addPath(QFileInfo(file).dir().absolutePath());
|
|
}
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::fileChanged,
|
|
this,
|
|
&EngineGeneration::onFileChanged
|
|
);
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::directoryChanged,
|
|
this,
|
|
&EngineGeneration::onDirectoryChanged
|
|
);
|
|
}
|
|
} else {
|
|
if (this->watcher != nullptr) {
|
|
this->watcher->deleteLater();
|
|
this->watcher = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool EngineGeneration::setExtraWatchedFiles(const QVector<QString>& files) {
|
|
this->extraWatchedFiles.clear();
|
|
for (const auto& file: files) {
|
|
if (!this->scanner.scannedFiles.contains(file)) {
|
|
this->extraWatchedFiles.append(file);
|
|
QByteArray data;
|
|
this->scanner.readAndHashFile(file, data);
|
|
}
|
|
}
|
|
|
|
if (this->watcher) {
|
|
this->setWatchingFiles(false);
|
|
this->setWatchingFiles(true);
|
|
}
|
|
|
|
return !this->extraWatchedFiles.isEmpty();
|
|
}
|
|
|
|
void EngineGeneration::onFileChanged(const QString& name) {
|
|
if (!this->watcher->files().contains(name)) {
|
|
this->deletedWatchedFiles.push_back(name);
|
|
} else {
|
|
// some editors (e.g vscode) perform file saving in two steps: truncate + write
|
|
// ignore the first event (truncate) with size 0 to prevent incorrect live reloading
|
|
auto fileInfo = QFileInfo(name);
|
|
if (fileInfo.isFile() && fileInfo.size() == 0) return;
|
|
|
|
if (!this->scanner.hasFileContentChanged(name)) {
|
|
qCDebug(logQmlScanner) << "Ignoring file change with unchanged content:" << name;
|
|
return;
|
|
}
|
|
|
|
emit this->filesChanged();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::onDirectoryChanged() {
|
|
// try to find any files that were just deleted from a replace operation
|
|
for (auto& file: this->deletedWatchedFiles) {
|
|
if (QFileInfo(file).exists()) {
|
|
if (!this->scanner.hasFileContentChanged(file)) {
|
|
qCDebug(logQmlScanner) << "Ignoring restored file with unchanged content:" << file;
|
|
continue;
|
|
}
|
|
|
|
emit this->filesChanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::onEngineWarnings(const QList<QQmlError>& warnings) {
|
|
for (const auto& error: warnings) {
|
|
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();
|
|
if (auto i = desc.indexOf(": "); i != -1 && desc.startsWith("QML ")) {
|
|
objectName = desc.first(i) + " at ";
|
|
desc = desc.sliced(i + 2);
|
|
}
|
|
|
|
qCWarning(logScene).noquote().nospace()
|
|
<< objectName << rel << '[' << error.line() << ':' << error.column() << "]: " << desc;
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::registerExtension(const void* key, EngineGenerationExt* extension) {
|
|
if (this->extensions.contains(key)) {
|
|
delete this->extensions.value(key);
|
|
}
|
|
|
|
this->extensions.insert(key, extension);
|
|
}
|
|
|
|
EngineGenerationExt* EngineGeneration::findExtension(const void* key) {
|
|
return this->extensions.value(key);
|
|
}
|
|
|
|
void EngineGeneration::quit() {
|
|
this->shouldTerminate = true;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::exit(int code) {
|
|
this->shouldTerminate = true;
|
|
this->exitCode = code;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) {
|
|
if (this->trackedWindows.contains(window)) return;
|
|
|
|
QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed);
|
|
this->trackedWindows.append(window);
|
|
this->updateIncubationMode();
|
|
}
|
|
|
|
void EngineGeneration::onTrackedWindowDestroyed(QObject* object) {
|
|
this->trackedWindows.removeAll(static_cast<QQuickWindow*>(object)); // NOLINT
|
|
this->updateIncubationMode();
|
|
}
|
|
|
|
void EngineGeneration::updateIncubationMode() {
|
|
// If we're in a situation with only hidden but tracked windows this might be wrong,
|
|
// but it seems to at least work.
|
|
this->incubationController.setIncubationMode(!this->trackedWindows.empty());
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::currentGeneration() {
|
|
if (g_generations.size() == 1) {
|
|
return *g_generations.begin();
|
|
} else return nullptr;
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) {
|
|
return g_generations.value(engine);
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) {
|
|
// Objects can still attempt to find their generation after it has been destroyed.
|
|
// if (g_generations.size() == 1) return EngineGeneration::currentGeneration();
|
|
|
|
while (object != nullptr) {
|
|
auto* context = QQmlEngine::contextForObject(object);
|
|
|
|
if (context != nullptr) {
|
|
if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) {
|
|
return generation;
|
|
}
|
|
}
|
|
|
|
object = object->parent();
|
|
}
|
|
|
|
return nullptr;
|
|
}
|