From 518977932d2d774b26ee339c13c8dc60ac7c0b66 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Mar 2024 05:35:44 -0700 Subject: [PATCH] core/lazyloader: add LazyLoader Also fixes qml incubation in general, which was completely broken, meaning the native qml Loader type should also work now. --- src/core/CMakeLists.txt | 2 + src/core/floatingwindow.cpp | 3 + src/core/generation.cpp | 58 ++++++++++ src/core/generation.hpp | 13 +++ src/core/incubator.cpp | 16 +++ src/core/incubator.hpp | 30 +++++ src/core/lazyloader.cpp | 202 +++++++++++++++++++++++++++++++++ src/core/lazyloader.hpp | 163 ++++++++++++++++++++++++++ src/core/module.md | 1 + src/core/proxywindow.cpp | 9 ++ src/wayland/wlr_layershell.cpp | 2 + 11 files changed, 499 insertions(+) create mode 100644 src/core/incubator.cpp create mode 100644 src/core/incubator.hpp create mode 100644 src/core/lazyloader.cpp create mode 100644 src/core/lazyloader.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c1535ad..dd47085 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -19,6 +19,8 @@ qt_add_library(quickshell-core STATIC generation.cpp scan.cpp qsintercept.cpp + incubator.cpp + lazyloader.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/floatingwindow.cpp b/src/core/floatingwindow.cpp index 73f2b1b..4503440 100644 --- a/src/core/floatingwindow.cpp +++ b/src/core/floatingwindow.cpp @@ -1,6 +1,7 @@ #include "floatingwindow.hpp" #include +#include #include #include #include @@ -37,6 +38,8 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) } void FloatingWindowInterface::onReload(QObject* oldInstance) { + QQmlEngine::setContextForObject(this->window, QQmlEngine::contextForObject(this)); + auto* old = qobject_cast(oldInstance); this->window->onReload(old != nullptr ? old->window : nullptr); } diff --git a/src/core/generation.cpp b/src/core/generation.cpp index d48ffb2..5d24c5a 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -3,13 +3,18 @@ #include #include +#include #include #include +#include +#include #include #include #include +#include #include +#include "incubator.hpp" #include "plugin.hpp" #include "qsintercept.hpp" #include "reload.hpp" @@ -23,6 +28,7 @@ EngineGeneration::EngineGeneration(QmlScanner scanner) g_generations.insert(&this->engine, this); this->engine.setNetworkAccessManagerFactory(&this->interceptNetFactory); + this->engine.setIncubationController(&this->delayedIncubationController); } EngineGeneration::~EngineGeneration() { @@ -31,6 +37,13 @@ EngineGeneration::~EngineGeneration() { } 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->engine.setIncubationController(&old->delayedIncubationController); + } + auto* app = QCoreApplication::instance(); QObject::connect(&this->engine, &QQmlEngine::quit, app, &QCoreApplication::quit); QObject::connect(&this->engine, &QQmlEngine::exit, app, &QCoreApplication::exit); @@ -75,6 +88,51 @@ void EngineGeneration::setWatchingFiles(bool watching) { } } +void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { + auto* obj = dynamic_cast(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); + + QObject::connect( + obj, + &QObject::destroyed, + this, + &EngineGeneration::incubationControllerDestroyed + ); + + qCDebug(logIncubator) << "Registered incubation controller" << controller; + + if (this->engine.incubationController() == &this->delayedIncubationController) { + this->assignIncubationController(); + } +} + +void EngineGeneration::incubationControllerDestroyed() { + qCDebug(logIncubator) << "Active incubation controller destroyed, deregistering"; + + this->incubationControllers.removeAll(dynamic_cast(this->sender())); + this->assignIncubationController(); +} + +void EngineGeneration::assignIncubationController() { + auto* controller = this->incubationControllers.first(); + if (controller == nullptr) controller = &this->delayedIncubationController; + + qCDebug(logIncubator) << "Assigning incubation controller to engine:" << controller + << "fallback:" << (controller == &this->delayedIncubationController); + + this->engine.setIncubationController(controller); +} + 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 a2fef8a..5c21a95 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,9 +1,12 @@ #pragma once +#include #include #include +#include #include +#include "incubator.hpp" #include "qsintercept.hpp" #include "scan.hpp" #include "shell.hpp" @@ -21,6 +24,8 @@ public: void onReload(EngineGeneration* old); void setWatchingFiles(bool watching); + void registerIncubationController(QQmlIncubationController* controller); + static EngineGeneration* findObjectGeneration(QObject* object); QmlScanner scanner; @@ -29,7 +34,15 @@ public: ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; QFileSystemWatcher* watcher = nullptr; + DelayedQmlIncubationController delayedIncubationController; signals: void filesChanged(); + +private slots: + void incubationControllerDestroyed(); + +private: + void assignIncubationController(); + QVector incubationControllers; }; diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp new file mode 100644 index 0000000..c43703a --- /dev/null +++ b/src/core/incubator.cpp @@ -0,0 +1,16 @@ +#include "incubator.hpp" + +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logIncubator, "quickshell.incubator", QtWarningMsg); + +void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { + switch (status) { + case QQmlIncubator::Ready: emit this->completed(); break; + case QQmlIncubator::Error: emit this->failed(); break; + default: break; + } +} diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp new file mode 100644 index 0000000..5928ffe --- /dev/null +++ b/src/core/incubator.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logIncubator); + +class QsQmlIncubator + : public QObject + , public QQmlIncubator { + Q_OBJECT; + +public: + explicit QsQmlIncubator(QsQmlIncubator::IncubationMode mode, QObject* parent = nullptr) + : QObject(parent) + , QQmlIncubator(mode) {} + + void statusChanged(QQmlIncubator::Status status) override; + +signals: + void completed(); + void failed(); +}; + +class DelayedQmlIncubationController: public QQmlIncubationController { + // Do nothing. + // This ensures lazy loaders don't start blocking before onReload creates windows. +}; diff --git a/src/core/lazyloader.cpp b/src/core/lazyloader.cpp new file mode 100644 index 0000000..0779c0d --- /dev/null +++ b/src/core/lazyloader.cpp @@ -0,0 +1,202 @@ +#include "lazyloader.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "incubator.hpp" +#include "reload.hpp" + +void LazyLoader::onReload(QObject* oldInstance) { + auto* old = qobject_cast(oldInstance); + + this->incubateIfReady(true); + + if (old != nullptr && old->mItem != nullptr && this->incubator != nullptr) { + this->incubator->forceCompletion(); + } + + if (this->mItem != nullptr) { + if (auto* reloadable = qobject_cast(this->mItem)) { + reloadable->onReload(old == nullptr ? nullptr : old->mItem); + } else { + Reloadable::reloadRecursive(this->mItem, old); + } + } + + this->postReload = true; +} + +QObject* LazyLoader::item() { + if (this->isLoading()) this->setActive(true); + return this->mItem; +} + +void LazyLoader::setItem(QObject* item) { + if (item == this->mItem) return; + + if (this->mItem != nullptr) { + this->mItem->deleteLater(); + } + + this->mItem = item; + + if (item != nullptr) { + item->setParent(this); + + if (this->postReload) { + if (auto* reloadable = qobject_cast(this->mItem)) { + reloadable->onReload(nullptr); + } else { + Reloadable::reloadRecursive(this->mItem, nullptr); + } + } + } + + this->targetActive = this->isActive(); + + emit this->itemChanged(); + emit this->activeChanged(); +} + +bool LazyLoader::isLoading() const { return this->incubator != nullptr; } + +void LazyLoader::setLoading(bool loading) { + if (loading == this->targetLoading || this->isActive()) return; + this->targetLoading = loading; + + if (loading) { + this->incubateIfReady(); + } else if (this->mItem != nullptr) { + this->mItem->deleteLater(); + this->mItem = nullptr; + } else if (this->incubator != nullptr) { + delete this->incubator; + this->incubator = nullptr; + } +} + +bool LazyLoader::isActive() const { return this->mItem != nullptr; } + +void LazyLoader::setActive(bool active) { + if (active == this->targetActive) return; + this->targetActive = active; + + if (active) { + if (this->isLoading()) { + this->incubator->forceCompletion(); + } else if (!this->isActive()) { + this->incubateIfReady(); + } + } else if (this->isActive()) { + this->setItem(nullptr); + } +} + +QQmlComponent* LazyLoader::component() const { + return this->cleanupComponent ? nullptr : this->mComponent; +} + +void LazyLoader::setComponent(QQmlComponent* component) { + if (this->cleanupComponent) this->setSource(nullptr); + if (component == this->mComponent) return; + this->cleanupComponent = false; + + if (this->mComponent != nullptr) { + QObject::disconnect(this->mComponent, nullptr, this, nullptr); + } + + this->mComponent = component; + + if (component != nullptr) { + QObject::connect( + this->mComponent, + &QObject::destroyed, + this, + &LazyLoader::onComponentDestroyed + ); + } + + emit this->componentChanged(); +} + +void LazyLoader::onComponentDestroyed() { + this->mComponent = nullptr; + // todo: figure out what happens to the incubator +} + +QString LazyLoader::source() const { return this->mSource; } + +void LazyLoader::setSource(QString source) { + if (!this->cleanupComponent) this->setComponent(nullptr); + if (source == this->mSource) return; + this->cleanupComponent = true; + + this->mSource = std::move(source); + delete this->mComponent; + + if (!this->mSource.isEmpty()) { + auto* context = QQmlEngine::contextForObject(this); + this->mComponent = new QQmlComponent( + context == nullptr ? nullptr : context->engine(), + context == nullptr ? this->mSource : context->resolvedUrl(this->mSource) + ); + + if (this->mComponent->isError()) { + qWarning() << this->mComponent->errorString().toStdString().c_str(); + delete this->mComponent; + this->mComponent = nullptr; + } + } else { + this->mComponent = nullptr; + } + + emit this->sourceChanged(); +} + +void LazyLoader::incubateIfReady(bool overrideReloadCheck) { + if (!(this->postReload || overrideReloadCheck) || !(this->targetLoading || this->targetActive) + || this->mComponent == nullptr || this->incubator != nullptr) + { + return; + } + + this->incubator = new QsQmlIncubator( + this->targetActive ? QQmlIncubator::Synchronous : QQmlIncubator::Asynchronous, + this + ); + + // clang-format off + QObject::connect(this->incubator, &QsQmlIncubator::completed, this, &LazyLoader::onIncubationCompleted); + QObject::connect(this->incubator, &QsQmlIncubator::failed, this, &LazyLoader::onIncubationFailed); + // clang-format on + + emit this->loadingChanged(); + + this->mComponent->create(*this->incubator, QQmlEngine::contextForObject(this->mComponent)); +} + +void LazyLoader::onIncubationCompleted() { + this->setItem(this->incubator->object()); + delete this->incubator; + this->incubator = nullptr; + this->targetLoading = false; + emit this->loadingChanged(); +} + +void LazyLoader::onIncubationFailed() { + qWarning() << "Failed to create LazyLoader component"; + + for (auto& error: this->incubator->errors()) { + qWarning() << error; + } + + delete this->incubator; + this->targetLoading = false; + emit this->loadingChanged(); +} diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp new file mode 100644 index 0000000..4364783 --- /dev/null +++ b/src/core/lazyloader.hpp @@ -0,0 +1,163 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "incubator.hpp" +#include "reload.hpp" + +///! Asynchronous component loader. +/// The LazyLoader can be used to prepare components that don't need to be +/// created immediately, such as windows that aren't visible until triggered +/// by another action. It works on creating the component in the gaps between +/// frame rendering to prevent blocking the interface thread. +/// It can also be used to preserve memory by loading components only +/// when you need them and unloading them afterward. +/// +/// Note that when reloading the UI due to changes, lazy loaders will always +/// load synchronously so windows can be reused. +/// +/// #### Example +/// The following example creates a PopupWindow asynchronously as the bar loads. +/// This means the bar can be shown onscreen before the popup is ready, however +/// trying to show the popup before it has finished loading in the background +/// will cause the UI thread to block. +/// +/// ```qml +/// import QtQuick +/// import QtQuick.Controls +/// import Quickshell +/// +/// ShellRoot { +/// PanelWindow { +/// id: window +/// height: 50 +/// +/// anchors { +/// bottom: true +/// left: true +/// right: true +/// } +/// +/// LazyLoader { +/// id: popupLoader +/// +/// // start loading immediately +/// loading: true +/// +/// // this window will be loaded in the background during spare +/// // frame time unless active is set to true, where it will be +/// // loaded in the foreground +/// PopupWindow { +/// // position the popup above the button +/// parentWindow: window +/// relativeX: window.width / 2 - width / 2 +/// relativeY: -height +/// +/// // some heavy component here +/// +/// width: 200 +/// height: 200 +/// } +/// } +/// +/// Button { +/// anchors.centerIn: parent +/// text: "show popup" +/// +/// // accessing popupLoader.item will force the loader to +/// // finish loading on the UI thread if it isn't finished yet. +/// onClicked: popupLoader.item.visible = !popupLoader.item.visible +/// } +/// } +/// } +/// ``` +/// +/// > [!WARNING] Components that internally load other components must explicitly +/// > support asynchronous loading to avoid blocking. +/// > +/// > Notably, [Variants](../variants) does not corrently support asynchronous +/// > loading, meaning using it inside a LazyLoader will block similarly to not +/// > having a loader to start with. +/// +/// > [!WARNING] LazyLoaders do not start loading before the first window is created, +/// > meaning if you create all windows inside of lazy loaders, none of them will ever load. +class LazyLoader: public Reloadable { + Q_OBJECT; + /// The fully loaded item if the loader is `loading` or `active`, or `null` + /// if neither `loading` or `active`. + /// + /// Note that the item is owned by the LazyLoader, and destroying the LazyLoader + /// will destroy the item. + /// + /// > [!WARNING] If you access the `item` of a loader that is currently loading, + /// > it will block as if you had set `active` to true immediately beforehand. + /// > + /// > You can instead set `loading` and listen to the `activeChanged` signal to + /// > ensure loading happens asynchronously. + Q_PROPERTY(QObject* item READ item NOTIFY itemChanged); + /// If the loader is actively loading. + /// + /// If the component is not loaded, setting this property to true will start + /// loading it asynchronously. If the component is already loaded, setting + /// this property has no effect. + Q_PROPERTY(bool loading READ isLoading WRITE setLoading NOTIFY loadingChanged); + /// If the component is fully loaded. + /// + /// Setting this property to `true` will force the component to load to completion, + /// blocking the UI, and setting it to `false` will destroy the component, requiring + /// it to be loaded again. + Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged); + /// The component to load. Mutually exclusive to `source`. + Q_PROPERTY(QQmlComponent* component READ component WRITE setComponent NOTIFY componentChanged); + /// The URI to load the component from. Mutually exclusive to `component`. + Q_PROPERTY(QString source READ source WRITE setSource NOTIFY sourceChanged); + Q_CLASSINFO("DefaultProperty", "component"); + QML_ELEMENT; + +public: + void onReload(QObject* oldInstance) override; + + [[nodiscard]] bool isActive() const; + void setActive(bool active); + + [[nodiscard]] bool isLoading() const; + void setLoading(bool loading); + + [[nodiscard]] QObject* item(); + void setItem(QObject* item); + + [[nodiscard]] QQmlComponent* component() const; + void setComponent(QQmlComponent* component); + + [[nodiscard]] QString source() const; + void setSource(QString source); + +signals: + void activeChanged(); + void loadingChanged(); + void itemChanged(); + void sourceChanged(); + void componentChanged(); + +private slots: + void onIncubationCompleted(); + void onIncubationFailed(); + void onComponentDestroyed(); + +private: + void incubateIfReady(bool overrideReloadCheck = false); + void waitForObjectCreation(); + + bool postReload = false; + bool targetLoading = false; + bool targetActive = false; + QObject* mItem = nullptr; + QString mSource; + QQmlComponent* mComponent = nullptr; + QsQmlIncubator* incubator = nullptr; + bool cleanupComponent = false; +}; diff --git a/src/core/module.md b/src/core/module.md index a5aea16..b8b6e45 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -14,5 +14,6 @@ headers = [ "floatingwindow.hpp", "popupwindow.hpp", "singleton.hpp", + "lazyloader.hpp", ] ----- diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index d4f528c..6be8f5a 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include +#include "generation.hpp" #include "qmlscreen.hpp" #include "region.hpp" #include "reload.hpp" @@ -33,6 +35,13 @@ ProxyWindowBase::~ProxyWindowBase() { void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->createWindow(oldInstance); + + if (auto* generation = EngineGeneration::findObjectGeneration(this)) { + // All windows have effectively the same incubation controller so it dosen't matter + // which window it belongs to. We do want to replace the delay one though. + generation->registerIncubationController(this->window->incubationController()); + } + this->setupWindow(); Reloadable::reloadRecursive(this->mContentItem, oldInstance); diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index b4e50d9..7492ac7 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -182,6 +182,8 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent) } void WaylandPanelInterface::onReload(QObject* oldInstance) { + QQmlEngine::setContextForObject(this->layer, QQmlEngine::contextForObject(this)); + auto* old = qobject_cast(oldInstance); this->layer->onReload(old != nullptr ? old->layer : nullptr); }