From 362789fc465a06207fd8b9ab222d418f81c57544 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 1 Feb 2024 01:29:45 -0800 Subject: [PATCH] feat: implement soft reloading --- CMakeLists.txt | 3 ++ src/cpp/proxywindow.cpp | 114 ++++++++++++++++++++++++++++++++++++++++ src/cpp/proxywindow.hpp | 82 +++++++++++++++++++++++++++++ src/cpp/rootwrapper.cpp | 17 ++++-- src/cpp/rootwrapper.hpp | 7 ++- src/cpp/scavenge.cpp | 40 ++++++++++++++ src/cpp/scavenge.hpp | 49 +++++++++++++++++ src/cpp/shell.cpp | 22 +++++++- src/cpp/shell.hpp | 17 ++++-- src/cpp/variants.cpp | 39 +++++++++++--- src/cpp/variants.hpp | 16 ++++-- 11 files changed, 385 insertions(+), 21 deletions(-) create mode 100644 src/cpp/proxywindow.cpp create mode 100644 src/cpp/proxywindow.hpp create mode 100644 src/cpp/scavenge.cpp create mode 100644 src/cpp/scavenge.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d3f82076..3c115eb6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,9 @@ qt_add_executable(qtshell src/cpp/shell.cpp src/cpp/variants.cpp src/cpp/rootwrapper.cpp + src/cpp/proxywindow.cpp + src/cpp/scavenge.cpp + src/cpp/rootwrapper.cpp ) qt_add_qml_module(qtshell URI QtShell) diff --git a/src/cpp/proxywindow.cpp b/src/cpp/proxywindow.cpp new file mode 100644 index 00000000..b86c0ad5 --- /dev/null +++ b/src/cpp/proxywindow.cpp @@ -0,0 +1,114 @@ +#include "proxywindow.hpp" + +#include +#include +#include +#include +#include + +ProxyWindowBase::~ProxyWindowBase() { + if (this->window != nullptr) { + this->window->deleteLater(); + } +} + +void ProxyWindowBase::earlyInit(QObject* old) { + auto* oldpw = qobject_cast(old); + + if (oldpw == nullptr || oldpw->window == nullptr) { + this->window = new QQuickWindow(); + } else { + this->window = oldpw->disownWindow(); + } +} + +QQuickWindow* ProxyWindowBase::disownWindow() { + auto data = this->data(); + ProxyWindowBase::dataClear(&data); + data.clear(&data); + + auto* window = this->window; + this->window = nullptr; + return window; +} + +// NOLINTNEXTLINE +#define PROXYPROP(type, get, set) \ + type ProxyWindowBase::get() { return this->window->get(); } \ + void ProxyWindowBase::set(type value) { this->window->set(value); } + +PROXYPROP(bool, isVisible, setVisible); +PROXYPROP(qint32, width, setWidth); +PROXYPROP(qint32, height, setHeight); +PROXYPROP(QColor, color, setColor); + +// see: +// https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquickwindow.cpp +// https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquickitem.cpp +// +// relevant functions are private so we call them via the property + +QQmlListProperty ProxyWindowBase::data() { + return QQmlListProperty( + this, + nullptr, + ProxyWindowBase::dataAppend, + ProxyWindowBase::dataCount, + ProxyWindowBase::dataAt, + ProxyWindowBase::dataClear, + ProxyWindowBase::dataReplace, + ProxyWindowBase::dataRemoveLast + ); +} + +QQmlListProperty ProxyWindowBase::dataBacker(QQmlListProperty* prop) { + auto* that = static_cast(prop->object); // NOLINT + return that->window->property("data").value>(); +} + +void ProxyWindowBase::dataAppend(QQmlListProperty* prop, QObject* obj) { + auto backer = dataBacker(prop); + backer.append(&backer, obj); +} + +qsizetype ProxyWindowBase::dataCount(QQmlListProperty* prop) { + auto backer = dataBacker(prop); + return backer.count(&backer); +} + +QObject* ProxyWindowBase::dataAt(QQmlListProperty* prop, qsizetype i) { + auto backer = dataBacker(prop); + return backer.at(&backer, i); +} + +void ProxyWindowBase::dataClear(QQmlListProperty* prop) { + auto backer = dataBacker(prop); + backer.clear(&backer); +} + +void ProxyWindowBase::dataReplace(QQmlListProperty* prop, qsizetype i, QObject* obj) { + auto backer = dataBacker(prop); + backer.replace(&backer, i, obj); +} + +void ProxyWindowBase::dataRemoveLast(QQmlListProperty* prop) { + auto backer = dataBacker(prop); + backer.removeLast(&backer); +} + +void ProxyFloatingWindow::setVisible(bool value) { + this->geometryLocked |= value; + ProxyWindowBase::setVisible(value); +} + +void ProxyFloatingWindow::setWidth(qint32 value) { + if (!this->geometryLocked) { + ProxyWindowBase::setWidth(value); + } +} + +void ProxyFloatingWindow::setHeight(qint32 value) { + if (!this->geometryLocked) { + ProxyWindowBase::setHeight(value); + } +} diff --git a/src/cpp/proxywindow.hpp b/src/cpp/proxywindow.hpp new file mode 100644 index 00000000..ee7f35fa --- /dev/null +++ b/src/cpp/proxywindow.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "scavenge.hpp" + +// Proxy to an actual window exposing a limited property set with the ability to +// transfer it to a new window. +// Detaching a window and touching any property is a use after free. +// +// NOTE: setting an `id` in qml will point to the proxy window and not the real window so things +// like anchors dont work +class ProxyWindowBase: public Scavenger { + Q_OBJECT; + Q_PROPERTY(bool visible READ isVisible WRITE setVisible); + Q_PROPERTY(qint32 width READ width WRITE setWidth); + Q_PROPERTY(qint32 height READ height WRITE setHeight); + Q_PROPERTY(QColor color READ color WRITE setColor); + Q_PROPERTY(QQmlListProperty data READ data); + Q_CLASSINFO("DefaultProperty", "data"); + +protected: + void earlyInit(QObject* old) override; + +public: + explicit ProxyWindowBase(QObject* parent = nullptr): Scavenger(parent) {} + ~ProxyWindowBase() override; + + ProxyWindowBase(ProxyWindowBase&) = delete; + ProxyWindowBase(ProxyWindowBase&&) = delete; + void operator=(ProxyWindowBase&) = delete; + void operator=(ProxyWindowBase&&) = delete; + + // Disown the backing window and delete all its children. + QQuickWindow* disownWindow(); + + bool isVisible(); + virtual void setVisible(bool value); + + qint32 width(); + virtual void setWidth(qint32 value); + + qint32 height(); + virtual void setHeight(qint32 value); + + QColor color(); + void setColor(QColor value); + + QQmlListProperty data(); + +private: + static QQmlListProperty dataBacker(QQmlListProperty* prop); + static void dataAppend(QQmlListProperty* prop, QObject* obj); + static qsizetype dataCount(QQmlListProperty* prop); + static QObject* dataAt(QQmlListProperty* prop, qsizetype i); + static void dataClear(QQmlListProperty* prop); + static void dataReplace(QQmlListProperty* prop, qsizetype i, QObject* obj); + static void dataRemoveLast(QQmlListProperty* prop); + + QQuickWindow* window = nullptr; +}; + +// qt attempts to resize the window but fails because wayland +// and only resizes the graphics context which looks terrible. +class ProxyFloatingWindow: public ProxyWindowBase { + Q_OBJECT; + QML_ELEMENT; + +public: + void setVisible(bool value) override; + void setWidth(qint32 value) override; + void setHeight(qint32 value) override; + +private: + bool geometryLocked = false; +}; diff --git a/src/cpp/rootwrapper.cpp b/src/cpp/rootwrapper.cpp index 3902ce0b..b840957e 100644 --- a/src/cpp/rootwrapper.cpp +++ b/src/cpp/rootwrapper.cpp @@ -8,11 +8,12 @@ #include #include +#include "scavenge.hpp" #include "shell.hpp" RootWrapper::RootWrapper(QUrl rootUrl): QObject(nullptr), rootUrl(std::move(rootUrl)), engine(this) { - this->reloadGraph(); + this->reloadGraph(true); if (this->activeRoot == nullptr) { qCritical() << "could not create scene graph, exiting"; @@ -20,20 +21,24 @@ RootWrapper::RootWrapper(QUrl rootUrl): } } -void RootWrapper::reloadGraph() { +void RootWrapper::reloadGraph(bool hard) { if (this->activeRoot != nullptr) { this->engine.clearComponentCache(); } auto component = QQmlComponent(&this->engine, this->rootUrl); + + SCAVENGE_PARENT = hard ? nullptr : this; auto* obj = component.beginCreate(this->engine.rootContext()); + SCAVENGE_PARENT = nullptr; + if (obj == nullptr) { qWarning() << "failed to create root component"; return; } - auto* qtsobj = qobject_cast(obj); - if (qtsobj == nullptr) { + auto* newRoot = qobject_cast(obj); + if (newRoot == nullptr) { qWarning() << "root component was not a QtShell"; delete obj; return; @@ -46,7 +51,7 @@ void RootWrapper::reloadGraph() { this->activeRoot = nullptr; } - this->activeRoot = qtsobj; + this->activeRoot = newRoot; } void RootWrapper::changeRoot(QtShell* newRoot) { @@ -61,4 +66,6 @@ void RootWrapper::changeRoot(QtShell* newRoot) { } } +QObject* RootWrapper::scavengeTargetFor(QObject* /* child */) { return this->activeRoot; } + void RootWrapper::destroy() { this->deleteLater(); } diff --git a/src/cpp/rootwrapper.hpp b/src/cpp/rootwrapper.hpp index 4efdadfb..73b6bed2 100644 --- a/src/cpp/rootwrapper.hpp +++ b/src/cpp/rootwrapper.hpp @@ -6,17 +6,20 @@ #include #include +#include "scavenge.hpp" #include "shell.hpp" -class RootWrapper: public QObject { +class RootWrapper: public QObject, virtual public Scavengeable { Q_OBJECT; public: explicit RootWrapper(QUrl rootUrl); - void reloadGraph(); + void reloadGraph(bool hard); void changeRoot(QtShell* newRoot); + QObject* scavengeTargetFor(QObject* child) override; + private slots: void destroy(); diff --git a/src/cpp/scavenge.cpp b/src/cpp/scavenge.cpp new file mode 100644 index 00000000..b61f658f --- /dev/null +++ b/src/cpp/scavenge.cpp @@ -0,0 +1,40 @@ +#include "scavenge.hpp" + +#include +#include +#include +#include + +QObject* SCAVENGE_PARENT = nullptr; // NOLINT + +void Scavenger::classBegin() { + // prayers + if (this->parent() == nullptr) { + this->setParent(SCAVENGE_PARENT); + SCAVENGE_PARENT = nullptr; + } + + auto* parent = dynamic_cast(this->parent()); + + QObject* old = nullptr; + if (parent != nullptr) { + old = parent->scavengeTargetFor(this); + } + + this->earlyInit(old); +} + +QObject* createComponentScavengeable( + QObject& parent, + QQmlComponent& component, + QVariantMap& initialProperties +) { + SCAVENGE_PARENT = &parent; + auto* instance = component.beginCreate(QQmlEngine::contextForObject(&parent)); + SCAVENGE_PARENT = nullptr; + if (instance == nullptr) return nullptr; + if (instance->parent() != nullptr) instance->setParent(&parent); + component.setInitialProperties(instance, initialProperties); + component.completeCreate(); + return instance; +} diff --git a/src/cpp/scavenge.hpp b/src/cpp/scavenge.hpp new file mode 100644 index 00000000..44c3f9c1 --- /dev/null +++ b/src/cpp/scavenge.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +extern QObject* SCAVENGE_PARENT; // NOLINT + +class Scavenger: public QObject, public QQmlParserStatus { + Q_OBJECT; + Q_INTERFACES(QQmlParserStatus); + +public: + explicit Scavenger(QObject* parent = nullptr): QObject(parent) {} + ~Scavenger() override = default; + + Scavenger(Scavenger&) = delete; + Scavenger(Scavenger&&) = delete; + void operator=(Scavenger&) = delete; + void operator=(Scavenger&&) = delete; + + void classBegin() override; + void componentComplete() override {} + +protected: + // do early init, sometimes with a scavengeable target + virtual void earlyInit(QObject* old) = 0; +}; + +class Scavengeable { +public: + Scavengeable() = default; + virtual ~Scavengeable() = default; + + Scavengeable(Scavengeable&) = delete; + Scavengeable(Scavengeable&&) = delete; + void operator=(Scavengeable&) = delete; + void operator=(Scavengeable&&) = delete; + + // return an old object that might have salvageable resources + virtual QObject* scavengeTargetFor(QObject* child) = 0; +}; + +QObject* createComponentScavengeable( + QObject& parent, + QQmlComponent& component, + QVariantMap& initialProperties +); diff --git a/src/cpp/shell.cpp b/src/cpp/shell.cpp index f74e43c5..2543d36e 100644 --- a/src/cpp/shell.cpp +++ b/src/cpp/shell.cpp @@ -1,4 +1,5 @@ #include "shell.hpp" +#include #include #include @@ -8,7 +9,7 @@ #include "rootwrapper.hpp" -void QtShell::reload() { +void QtShell::reload(bool hard) { auto* rootobj = QQmlEngine::contextForObject(this)->engine()->parent(); auto* root = qobject_cast(rootobj); @@ -17,7 +18,23 @@ void QtShell::reload() { return; } - root->reloadGraph(); + root->reloadGraph(hard); +} + +void QtShell::earlyInit(QObject* old) { + auto* oldshell = qobject_cast(old); + + if (oldshell != nullptr) { + this->scavengeableChildren = std::move(oldshell->children); + } +} + +QObject* QtShell::scavengeTargetFor(QObject* /* child */) { + if (this->scavengeableChildren.length() > this->children.length()) { + return this->scavengeableChildren[this->children.length()]; + } + + return nullptr; } QQmlListProperty QtShell::components() { @@ -36,4 +53,5 @@ QQmlListProperty QtShell::components() { void QtShell::appendComponent(QQmlListProperty* list, QObject* component) { auto* shell = static_cast(list->object); // NOLINT component->setParent(shell); + shell->children.append(component); } diff --git a/src/cpp/shell.hpp b/src/cpp/shell.hpp index 299a3695..f42d8dc9 100644 --- a/src/cpp/shell.hpp +++ b/src/cpp/shell.hpp @@ -1,26 +1,37 @@ #pragma once #include +#include #include #include #include #include #include -class QtShell: public QObject { +#include "scavenge.hpp" + +class QtShell: public Scavenger, virtual public Scavengeable { Q_OBJECT; Q_PROPERTY(QQmlListProperty components READ components FINAL); Q_CLASSINFO("DefaultProperty", "components"); QML_ELEMENT; public: - explicit QtShell(QObject* parent = nullptr): QObject(parent) {} + explicit QtShell(QObject* parent = nullptr): Scavenger(parent) {} + + void earlyInit(QObject* old) override; + QObject* scavengeTargetFor(QObject* child) override; QQmlListProperty components(); public slots: - void reload(); + void reload(bool hard = true); private: static void appendComponent(QQmlListProperty* list, QObject* component); + +public: + // track only the children assigned to `components` in order + QList children; + QList scavengeableChildren; }; diff --git a/src/cpp/variants.cpp b/src/cpp/variants.cpp index 2b333c56..1521dcb5 100644 --- a/src/cpp/variants.cpp +++ b/src/cpp/variants.cpp @@ -4,17 +4,33 @@ #include #include +#include + +#include "scavenge.hpp" + +void Variants::earlyInit(QObject* old) { + auto* oldv = qobject_cast(old); + if (oldv != nullptr) { + this->scavengeableInstances = std::move(oldv->instances); + } +} + +QObject* Variants::scavengeTargetFor(QObject* /* child */) { + if (this->activeScavengeVariant != nullptr) { + auto* r = this->scavengeableInstances.get(*this->activeScavengeVariant); + if (r != nullptr) return *r; + } + + return nullptr; +} void Variants::setVariants(QVariantList variants) { this->mVariants = std::move(variants); - qDebug() << "configurations updated:" << this->mVariants; - this->updateVariants(); } void Variants::componentComplete() { - qDebug() << "configure ready"; - + Scavenger::componentComplete(); this->updateVariants(); } @@ -53,14 +69,14 @@ void Variants::updateVariants() { continue; // we dont need to recreate this one } - auto* instance = this->mComponent->createWithInitialProperties(variant, nullptr); + this->activeScavengeVariant = &variant; + auto* instance = createComponentScavengeable(*this, *this->mComponent, variant); if (instance == nullptr) { qWarning() << "failed to create variant with object" << variant; continue; } - instance->setParent(this); this->instances.insert(variant, instance); } @@ -75,6 +91,17 @@ bool AwfulMap::contains(const K& key) const { }); } +template +V* AwfulMap::get(const K& key) { + for (auto& [k, v]: this->values) { + if (key == k) { + return &v; + } + } + + return nullptr; +} + template void AwfulMap::insert(K key, V value) { this->values.push_back(QPair(key, value)); diff --git a/src/cpp/variants.hpp b/src/cpp/variants.hpp index a906c7fc..6f87f0bd 100644 --- a/src/cpp/variants.hpp +++ b/src/cpp/variants.hpp @@ -2,22 +2,26 @@ #include #include +#include #include #include #include #include +#include "scavenge.hpp" + // extremely inefficient map template class AwfulMap { public: [[nodiscard]] bool contains(const K& key) const; + [[nodiscard]] V* get(const K& key); void insert(K key, V value); // assumes no duplicates bool remove(const K& key); // returns true if anything was removed QList> values; }; -class Variants: public QObject, public QQmlParserStatus { +class Variants: public Scavenger, virtual public Scavengeable { Q_OBJECT; Q_PROPERTY(QQmlComponent* component MEMBER mComponent); Q_PROPERTY(QVariantList variants MEMBER mVariants WRITE setVariants); @@ -25,9 +29,11 @@ class Variants: public QObject, public QQmlParserStatus { QML_ELEMENT; public: - explicit Variants(QObject* parent = nullptr): QObject(parent) {} + explicit Variants(QObject* parent = nullptr): Scavenger(parent) {} + + void earlyInit(QObject* old) override; + QObject* scavengeTargetFor(QObject* child) override; - void classBegin() override {}; void componentComplete() override; private: @@ -37,4 +43,8 @@ private: QQmlComponent* mComponent = nullptr; QVariantList mVariants; AwfulMap instances; + + // pointers may die post componentComplete. + AwfulMap scavengeableInstances; + QVariantMap* activeScavengeVariant = nullptr; };