forked from quickshell/quickshell
core/retainable: add Retainable and RetainableLock
This commit is contained in:
parent
7c5632ef5f
commit
609834d8f2
4 changed files with 328 additions and 1 deletions
|
@ -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}")
|
||||
|
|
|
@ -22,6 +22,7 @@ headers = [
|
|||
"elapsedtimer.hpp",
|
||||
"desktopentry.hpp",
|
||||
"objectrepeater.hpp",
|
||||
"qsmenu.hpp"
|
||||
"qsmenu.hpp",
|
||||
"retainable.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
163
src/core/retainable.cpp
Normal file
163
src/core/retainable.cpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#include "retainable.hpp"
|
||||
|
||||
#include <qlogging.h>
|
||||
#include <qobject.h>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qvariant.h>
|
||||
|
||||
RetainableHook* RetainableHook::getHook(QObject* object, bool create) {
|
||||
auto v = object->property("__qs_retainable");
|
||||
|
||||
if (v.canConvert<RetainableHook*>()) {
|
||||
return v.value<RetainableHook*>();
|
||||
} else if (create) {
|
||||
auto* retainable = dynamic_cast<Retainable*>(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<QObject*>(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<QObject*>(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(); }
|
162
src/core/retainable.hpp
Normal file
162
src/core/retainable.hpp
Normal 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;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue