From 609834d8f2902202afd5353e4fe80b928e473c11 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 12 Jul 2024 21:21:35 -0700 Subject: [PATCH] core/retainable: add Retainable and RetainableLock --- src/core/CMakeLists.txt | 1 + src/core/module.md | 3 +- src/core/retainable.cpp | 163 ++++++++++++++++++++++++++++++++++++++++ src/core/retainable.hpp | 162 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 src/core/retainable.cpp create mode 100644 src/core/retainable.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d730d1dd..6eace03b 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -32,6 +32,7 @@ qt_add_library(quickshell-core STATIC objectrepeater.cpp platformmenu.cpp qsmenu.cpp + retainable.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index c70b4876..f0d296a5 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -22,6 +22,7 @@ headers = [ "elapsedtimer.hpp", "desktopentry.hpp", "objectrepeater.hpp", - "qsmenu.hpp" + "qsmenu.hpp", + "retainable.hpp", ] ----- diff --git a/src/core/retainable.cpp b/src/core/retainable.cpp new file mode 100644 index 00000000..4e77e051 --- /dev/null +++ b/src/core/retainable.cpp @@ -0,0 +1,163 @@ +#include "retainable.hpp" + +#include +#include +#include +#include + +RetainableHook* RetainableHook::getHook(QObject* object, bool create) { + auto v = object->property("__qs_retainable"); + + if (v.canConvert()) { + return v.value(); + } else if (create) { + auto* retainable = dynamic_cast(object); + if (!retainable) return nullptr; + + auto* hook = new RetainableHook(object); + hook->retainableFacet = retainable; + retainable->hook = hook; + + object->setProperty("__qs_retainable", QVariant::fromValue(hook)); + + return hook; + } else return nullptr; +} + +RetainableHook* RetainableHook::qmlAttachedProperties(QObject* object) { + return RetainableHook::getHook(object, true); +} + +void RetainableHook::ref() { this->refcount++; } + +void RetainableHook::unref() { + this->refcount--; + if (this->refcount == 0) this->unlocked(); +} + +void RetainableHook::lock() { + this->explicitRefcount++; + this->ref(); +} + +void RetainableHook::unlock() { + if (this->explicitRefcount < 1) { + qWarning() << "Retainable object" << this->parent() + << "unlocked more times than it was locked!"; + } else { + this->explicitRefcount--; + this->unref(); + } +} + +void RetainableHook::forceUnlock() { this->unlocked(); } + +bool RetainableHook::isRetained() const { return !this->inactive; } + +void RetainableHook::unlocked() { + if (this->inactive) return; + + emit this->aboutToDestroy(); + this->retainableFacet->retainFinished(); +} + +void Retainable::retainedDestroy() { + this->retaining = true; + + auto* hook = RetainableHook::getHook(dynamic_cast(this), false); + + if (hook) { + // let all signal handlers run before acting on changes + emit hook->dropped(); + hook->inactive = false; + + if (hook->refcount == 0) hook->unlocked(); + else emit hook->retainedChanged(); + } else { + this->retainFinished(); + } +} + +bool Retainable::isRetained() const { return this->retaining; } + +void Retainable::retainFinished() { + // a normal delete tends to cause deref errors in a listview. + dynamic_cast(this)->deleteLater(); +} + +RetainableLock::~RetainableLock() { + if (this->mEnabled && this->mObject) { + this->hook->unref(); + } +} + +QObject* RetainableLock::object() const { return this->mObject; } + +void RetainableLock::setObject(QObject* object) { + if (object == this->mObject) return; + + if (this->mObject) { + QObject::disconnect(this->mObject, nullptr, this, nullptr); + if (this->hook->isRetained()) emit this->retainedChanged(); + this->hook->unref(); + } + + this->mObject = nullptr; + this->hook = nullptr; + + if (object) { + if (auto* hook = RetainableHook::getHook(object, true)) { + this->mObject = object; + this->hook = hook; + + QObject::connect(object, &QObject::destroyed, this, &RetainableLock::onObjectDestroyed); + QObject::connect(hook, &RetainableHook::dropped, this, &RetainableLock::dropped); + QObject::connect( + hook, + &RetainableHook::aboutToDestroy, + this, + &RetainableLock::aboutToDestroy + ); + QObject::connect( + hook, + &RetainableHook::retainedChanged, + this, + &RetainableLock::retainedChanged + ); + if (hook->isRetained()) emit this->retainedChanged(); + + hook->ref(); + } else { + qCritical() << "Tried to set non retainable object" << object << "as the target of" << this; + } + } + + emit this->objectChanged(); +} + +void RetainableLock::onObjectDestroyed() { + this->mObject = nullptr; + this->hook = nullptr; + + emit this->objectChanged(); +} + +bool RetainableLock::locked() const { return this->mEnabled; } + +void RetainableLock::setLocked(bool locked) { + if (locked == this->mEnabled) return; + + this->mEnabled = locked; + + if (this->mObject) { + if (locked) this->hook->ref(); + else { + if (this->hook->isRetained()) emit this->retainedChanged(); + this->hook->unref(); + } + } + + emit this->lockedChanged(); +} + +bool RetainableLock::isRetained() const { return this->mObject && this->hook->isRetained(); } diff --git a/src/core/retainable.hpp b/src/core/retainable.hpp new file mode 100644 index 00000000..598d6f95 --- /dev/null +++ b/src/core/retainable.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include +#include +#include +#include + +class Retainable; + +///! Attached object for types that can have delayed destruction. +/// Retainable works as an attached property that allows objects to be +/// kept around (retained) after they would normally be destroyed, which +/// is especially useful for things like exit transitions. +/// +/// An object that is retainable will have `Retainable` as an attached property. +/// All retainable objects will say that they are retainable on their respective +/// typeinfo pages. +/// +/// > [!INFO] Working directly with Retainable is often overly complicated and +/// > error prone. For this reason [RetainableLock](../retainablelock) should +/// > usually be used instead. +class RetainableHook: public QObject { + Q_OBJECT; + /// If the object is currently in a retained state. + Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); + QML_ATTACHED(RetainableHook); + QML_NAMED_ELEMENT(Retainable); + QML_UNCREATABLE("Retainable can only be used as an attached object."); + +public: + static RetainableHook* getHook(QObject* object, bool create = false); + + void destroyOnRelease(); + + void ref(); + void unref(); + + /// Hold a lock on the object so it cannot be destroyed. + /// + /// A counter is used to ensure you can lock the object from multiple places + /// and it will not be unlocked until the same number of unlocks as locks have occurred. + /// + /// > [!WARNING] It is easy to forget to unlock a locked object. + /// > Doing so will create what is effectively a memory leak. + /// > + /// > Using [RetainableLock](../retainablelock) is recommended as it will help + /// > avoid this scenario and make misuse more obvious. + Q_INVOKABLE void lock(); + /// Remove a lock on the object. See `lock()` for more information. + Q_INVOKABLE void unlock(); + /// Forcibly remove all locks, destroying the object. + /// + /// `unlock()` should usually be preferred. + Q_INVOKABLE void forceUnlock(); + + [[nodiscard]] bool isRetained() const; + + static RetainableHook* qmlAttachedProperties(QObject* object); + +signals: + /// This signal is sent when the object would normally be destroyed. + /// + /// If all signal handlers return and no locks are in place, the object will be destroyed. + /// If at least one lock is present the object will be retained until all are removed. + void dropped(); + /// This signal is sent immediately before the object is destroyed. + /// At this point destruction cannot be interrupted. + void aboutToDestroy(); + + void retainedChanged(); + +private: + explicit RetainableHook(QObject* parent): QObject(parent) {} + + void unlocked(); + + uint refcount = 0; + // tracked separately so a warning can be given when unlock is called too many times, + // without affecting other lock sources such as RetainableLock. + uint explicitRefcount = 0; + Retainable* retainableFacet = nullptr; + bool inactive = true; + + friend class Retainable; +}; + +class Retainable { +public: + Retainable() = default; + virtual ~Retainable() = default; + Q_DISABLE_COPY_MOVE(Retainable); + + void retainedDestroy(); + [[nodiscard]] bool isRetained() const; + +protected: + virtual void retainFinished(); + +private: + RetainableHook* hook = nullptr; + bool retaining = false; + + friend class RetainableHook; +}; + +///! A helper for easily using Retainable. +/// A RetainableLock provides extra safety and ease of use for locking +/// [Retainable](../retainable) objects. A retainable object can be locked +/// by multiple locks at once, and each lock re-exposes relevant properties +/// of the retained objects. +/// +/// #### Example +/// The code below will keep a retainable object alive for as long as the +/// RetainableLock exists. +/// +/// ```qml +/// RetainableLock { +/// object: aRetainableObject +/// locked: true +/// } +/// ``` +class RetainableLock: public QObject { + Q_OBJECT; + /// The object to lock. Must be [Retainable](../retainable). + Q_PROPERTY(QObject* object READ object WRITE setObject NOTIFY objectChanged); + /// If the object should be locked. + Q_PROPERTY(bool locked READ locked WRITE setLocked NOTIFY lockedChanged); + /// If the object is currently in a retained state. + Q_PROPERTY(bool retained READ isRetained NOTIFY retainedChanged); + QML_ELEMENT; + +public: + explicit RetainableLock(QObject* parent = nullptr): QObject(parent) {} + ~RetainableLock() override; + Q_DISABLE_COPY_MOVE(RetainableLock); + + [[nodiscard]] QObject* object() const; + void setObject(QObject* object); + + [[nodiscard]] bool locked() const; + void setLocked(bool locked); + + [[nodiscard]] bool isRetained() const; + +signals: + /// Rebroadcast of the object's `dropped()` signal. + void dropped(); + /// Rebroadcast of the object's `aboutToDestroy()` signal. + void aboutToDestroy(); + void retainedChanged(); + + void objectChanged(); + void lockedChanged(); + +private slots: + void onObjectDestroyed(); + +private: + QObject* mObject = nullptr; + RetainableHook* hook = nullptr; + bool mEnabled = false; +};