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