forked from quickshell/quickshell
Reworks dbusmenu menus to be displayable with a system context menu. Breaks the entire DBusMenu api.
418 lines
12 KiB
C++
418 lines
12 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 <qicon.h>
|
|
#include <qiconengine.h>
|
|
#include <qlogging.h>
|
|
#include <qloggingcategory.h>
|
|
#include <qobject.h>
|
|
#include <qpixmap.h>
|
|
#include <qqmlcontext.h>
|
|
#include <qqmlengine.h>
|
|
#include <qqmlincubator.h>
|
|
#include <qquickimageprovider.h>
|
|
#include <qsize.h>
|
|
#include <qtmetamacros.h>
|
|
|
|
#include "iconimageprovider.hpp"
|
|
#include "imageprovider.hpp"
|
|
#include "incubator.hpp"
|
|
#include "plugin.hpp"
|
|
#include "qsintercept.hpp"
|
|
#include "reload.hpp"
|
|
#include "scan.hpp"
|
|
|
|
static QHash<QQmlEngine*, EngineGeneration*> g_generations; // NOLINT
|
|
|
|
EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner)
|
|
: rootPath(rootPath)
|
|
, scanner(std::move(scanner))
|
|
, urlInterceptor(this->rootPath)
|
|
, interceptNetFactory(this->scanner.qmldirIntercepts)
|
|
, engine(new QQmlEngine()) {
|
|
g_generations.insert(this->engine, this);
|
|
|
|
this->engine->addUrlInterceptor(&this->urlInterceptor);
|
|
this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory);
|
|
this->engine->setIncubationController(&this->delayedIncubationController);
|
|
|
|
this->engine->addImageProvider("icon", new IconImageProvider());
|
|
this->engine->addImageProvider("qsimage", new QsImageProvider());
|
|
this->engine->addImageProvider("qspixmap", new QsPixmapProvider());
|
|
|
|
QuickshellPlugin::runConstructGeneration(*this);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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 {
|
|
// 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
|
|
old->incubationControllers.clear();
|
|
old->assignIncubationController();
|
|
}
|
|
|
|
QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit);
|
|
QObject::connect(this->engine, &QQmlEngine::exit, this, &EngineGeneration::exit);
|
|
|
|
this->root->reload(old == nullptr ? nullptr : old->root);
|
|
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;
|
|
|
|
QuickshellPlugin::runOnReload();
|
|
PostReloadHook::postReloadTree(this->root);
|
|
this->singletonRegistry.onPostReload();
|
|
}
|
|
|
|
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());
|
|
}
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::fileChanged,
|
|
this,
|
|
&EngineGeneration::onFileChanged
|
|
);
|
|
|
|
QObject::connect(
|
|
this->watcher,
|
|
&QFileSystemWatcher::directoryChanged,
|
|
this,
|
|
&EngineGeneration::onDirectoryChanged
|
|
);
|
|
}
|
|
} else {
|
|
if (this->watcher != nullptr) {
|
|
delete this->watcher;
|
|
this->watcher = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::onFileChanged(const QString& name) {
|
|
if (!this->watcher->files().contains(name)) {
|
|
this->deletedWatchedFiles.push_back(name);
|
|
} else {
|
|
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()) {
|
|
emit this->filesChanged();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) {
|
|
auto* obj = dynamic_cast<QObject*>(controller);
|
|
|
|
// We only want controllers that we can swap out if destroyed.
|
|
// This happens if the window owning the active controller dies.
|
|
if (obj == nullptr) {
|
|
qCDebug(logIncubator) << "Could not register incubation controller as it is not a QObject"
|
|
<< controller;
|
|
|
|
return;
|
|
}
|
|
|
|
this->incubationControllers.push_back({controller, obj});
|
|
|
|
QObject::connect(
|
|
obj,
|
|
&QObject::destroyed,
|
|
this,
|
|
&EngineGeneration::incubationControllerDestroyed
|
|
);
|
|
|
|
qCDebug(logIncubator) << "Registered incubation controller" << controller;
|
|
|
|
// This function can run during destruction.
|
|
if (this->engine == nullptr) return;
|
|
|
|
if (this->engine->incubationController() == &this->delayedIncubationController) {
|
|
this->assignIncubationController();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::deregisterIncubationController(QQmlIncubationController* controller) {
|
|
QObject* obj = nullptr;
|
|
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
|
if (controller == other.first) {
|
|
obj = other.second;
|
|
return true;
|
|
} else return false;
|
|
});
|
|
|
|
if (obj == nullptr) {
|
|
qCWarning(logIncubator) << "Failed to deregister incubation controller" << controller
|
|
<< "as it was not registered to begin with";
|
|
qCWarning(logIncubator) << "Current registered incuabation controllers"
|
|
<< this->incubationControllers;
|
|
} else {
|
|
QObject::disconnect(obj, nullptr, this, nullptr);
|
|
qCDebug(logIncubator) << "Deregistered incubation controller" << controller;
|
|
}
|
|
|
|
// This function can run during destruction.
|
|
if (this->engine == nullptr) return;
|
|
|
|
if (this->engine->incubationController() == controller) {
|
|
qCDebug(logIncubator
|
|
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
|
this->assignIncubationController();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::incubationControllerDestroyed() {
|
|
auto* sender = this->sender();
|
|
QQmlIncubationController* controller = nullptr;
|
|
|
|
this->incubationControllers.removeIf([&](QPair<QQmlIncubationController*, QObject*> other) {
|
|
if (sender == other.second) {
|
|
controller = other.first;
|
|
return true;
|
|
} else return false;
|
|
});
|
|
|
|
if (controller == nullptr) {
|
|
qCCritical(logIncubator) << "Destroyed incubation controller" << this->sender()
|
|
<< "could not be identified, this may cause memory corruption";
|
|
qCCritical(logIncubator) << "Current registered incuabation controllers"
|
|
<< this->incubationControllers;
|
|
} else {
|
|
qCDebug(logIncubator) << "Destroyed incubation controller" << controller << "deregistered";
|
|
}
|
|
|
|
// This function can run during destruction.
|
|
if (this->engine == nullptr) return;
|
|
|
|
if (this->engine->incubationController() == controller) {
|
|
qCDebug(logIncubator
|
|
) << "Destroyed incubation controller was currently active, reassigning from pool";
|
|
this->assignIncubationController();
|
|
}
|
|
}
|
|
|
|
void EngineGeneration::quit() {
|
|
this->shouldTerminate = true;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::exit(int code) {
|
|
this->shouldTerminate = true;
|
|
this->exitCode = code;
|
|
this->destroy();
|
|
}
|
|
|
|
void EngineGeneration::assignIncubationController() {
|
|
QQmlIncubationController* controller = nullptr;
|
|
if (this->incubationControllers.isEmpty()) controller = &this->delayedIncubationController;
|
|
else controller = this->incubationControllers.first().first;
|
|
|
|
qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller
|
|
<< "fallback:" << (controller == &this->delayedIncubationController);
|
|
|
|
this->engine->setIncubationController(controller);
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::currentGeneration() {
|
|
if (g_generations.size() == 1) {
|
|
return *g_generations.begin();
|
|
} else return nullptr;
|
|
}
|
|
|
|
// QMenu re-calls pixmap() every time the mouse moves so its important to cache it.
|
|
class PixmapCacheIconEngine: public QIconEngine {
|
|
void paint(
|
|
QPainter* /*unused*/,
|
|
const QRect& /*unused*/,
|
|
QIcon::Mode /*unused*/,
|
|
QIcon::State /*unused*/
|
|
) override {
|
|
qFatal(
|
|
) << "Unexpected icon paint request bypassed pixmap method. Please report this as a bug.";
|
|
}
|
|
|
|
QPixmap pixmap(const QSize& size, QIcon::Mode /*unused*/, QIcon::State /*unused*/) override {
|
|
if (this->lastPixmap.isNull() || size != this->lastSize) {
|
|
this->lastPixmap = this->createPixmap(size);
|
|
this->lastSize = size;
|
|
}
|
|
|
|
return this->lastPixmap;
|
|
}
|
|
|
|
virtual QPixmap createPixmap(const QSize& size) = 0;
|
|
|
|
private:
|
|
QSize lastSize;
|
|
QPixmap lastPixmap;
|
|
};
|
|
|
|
class ImageProviderIconEngine: public PixmapCacheIconEngine {
|
|
public:
|
|
explicit ImageProviderIconEngine(QQuickImageProvider* provider, QString id)
|
|
: provider(provider)
|
|
, id(std::move(id)) {}
|
|
|
|
QPixmap createPixmap(const QSize& size) override {
|
|
if (this->provider->imageType() == QQmlImageProviderBase::Pixmap) {
|
|
return this->provider->requestPixmap(this->id, nullptr, size);
|
|
} else if (this->provider->imageType() == QQmlImageProviderBase::Image) {
|
|
auto image = this->provider->requestImage(this->id, nullptr, size);
|
|
return QPixmap::fromImage(image);
|
|
} else {
|
|
qFatal() << "Unexpected ImageProviderIconEngine image type" << this->provider->imageType();
|
|
return QPixmap(); // never reached, satisfies lint
|
|
}
|
|
}
|
|
|
|
[[nodiscard]] QIconEngine* clone() const override {
|
|
return new ImageProviderIconEngine(this->provider, this->id);
|
|
}
|
|
|
|
private:
|
|
QQuickImageProvider* provider;
|
|
QString id;
|
|
};
|
|
|
|
QIcon EngineGeneration::iconByUrl(const QUrl& url) const {
|
|
if (url.isEmpty()) return QIcon();
|
|
|
|
auto scheme = url.scheme();
|
|
if (scheme == "image") {
|
|
auto providerName = url.authority();
|
|
auto path = url.path();
|
|
if (!path.isEmpty()) path = path.sliced(1);
|
|
|
|
auto* provider = qobject_cast<QQuickImageProvider*>(this->engine->imageProvider(providerName));
|
|
|
|
if (provider == nullptr) {
|
|
qWarning() << "iconByUrl failed: no provider found for" << url;
|
|
return QIcon();
|
|
}
|
|
|
|
if (provider->imageType() == QQmlImageProviderBase::Pixmap
|
|
|| provider->imageType() == QQmlImageProviderBase::Image)
|
|
{
|
|
return QIcon(new ImageProviderIconEngine(provider, path));
|
|
}
|
|
|
|
} else {
|
|
qWarning() << "iconByUrl failed: unsupported scheme" << scheme << "in path" << url;
|
|
}
|
|
|
|
return QIcon();
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) {
|
|
return g_generations.value(engine);
|
|
}
|
|
|
|
EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) {
|
|
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;
|
|
}
|