core/retainable: add Retainable and RetainableLock

This commit is contained in:
outfoxxed 2024-07-12 21:21:35 -07:00
parent 7c5632ef5f
commit 609834d8f2
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
4 changed files with 328 additions and 1 deletions

162
src/core/retainable.hpp Normal file
View file

@ -0,0 +1,162 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
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;
};