forked from quickshell/quickshell
		
	hyprland/ipc: expose Hyprland toplevels
This commit is contained in:
		
							parent
							
								
									c115df8d34
								
							
						
					
					
						commit
						362c8e1b69
					
				
					 11 changed files with 685 additions and 43 deletions
				
			
		| 
						 | 
				
			
			@ -14,6 +14,8 @@
 | 
			
		|||
#include <qlogging.h>
 | 
			
		||||
#include <qloggingcategory.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qproperty.h>
 | 
			
		||||
#include <qqml.h>
 | 
			
		||||
#include <qtenvironmentvariables.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +23,10 @@
 | 
			
		|||
 | 
			
		||||
#include "../../../core/model.hpp"
 | 
			
		||||
#include "../../../core/qmlscreen.hpp"
 | 
			
		||||
#include "../../toplevel_management/handle.hpp"
 | 
			
		||||
#include "hyprland_toplevel.hpp"
 | 
			
		||||
#include "monitor.hpp"
 | 
			
		||||
#include "toplevel_mapping.hpp"
 | 
			
		||||
#include "workspace.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
| 
						 | 
				
			
			@ -62,11 +67,16 @@ HyprlandIpc::HyprlandIpc() {
 | 
			
		|||
	QObject::connect(&this->eventSocket, &QLocalSocket::errorOccurred, this, &HyprlandIpc::eventSocketError);
 | 
			
		||||
	QObject::connect(&this->eventSocket, &QLocalSocket::stateChanged, this, &HyprlandIpc::eventSocketStateChanged);
 | 
			
		||||
	QObject::connect(&this->eventSocket, &QLocalSocket::readyRead, this, &HyprlandIpc::eventSocketReady);
 | 
			
		||||
 | 
			
		||||
	auto *instance = HyprlandToplevelMappingManager::instance();
 | 
			
		||||
	QObject::connect(instance, &HyprlandToplevelMappingManager::toplevelAddressed, this, &HyprlandIpc::toplevelAddressed);
 | 
			
		||||
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly);
 | 
			
		||||
	this->refreshMonitors(true);
 | 
			
		||||
	this->refreshWorkspaces(true);
 | 
			
		||||
	this->refreshToplevels();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; }
 | 
			
		||||
| 
						 | 
				
			
			@ -113,6 +123,36 @@ void HyprlandIpc::eventSocketReady() {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandIpc::toplevelAddressed(
 | 
			
		||||
    wayland::toplevel_management::impl::ToplevelHandle* handle,
 | 
			
		||||
    quint64 address
 | 
			
		||||
) {
 | 
			
		||||
	auto* waylandToplevel =
 | 
			
		||||
	    wayland::toplevel_management::ToplevelManager::instance()->forImpl(handle);
 | 
			
		||||
 | 
			
		||||
	if (!waylandToplevel) return;
 | 
			
		||||
 | 
			
		||||
	auto* attached = qobject_cast<HyprlandToplevel*>(
 | 
			
		||||
	    qmlAttachedPropertiesObject<HyprlandToplevel>(waylandToplevel, false)
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	auto* hyprToplevel = this->findToplevelByAddress(address, true);
 | 
			
		||||
 | 
			
		||||
	if (attached) {
 | 
			
		||||
		if (attached->address()) {
 | 
			
		||||
			qCDebug(logHyprlandIpc) << "Toplevel" << attached->addressStr() << "already has address"
 | 
			
		||||
			                        << address;
 | 
			
		||||
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		attached->setAddress(address);
 | 
			
		||||
		attached->setHyprlandHandle(hyprToplevel);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hyprToplevel->setWaylandHandle(waylandToplevel->implHandle());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandIpc::makeRequest(
 | 
			
		||||
    const QByteArray& request,
 | 
			
		||||
    const std::function<void(bool, QByteArray)>& callback
 | 
			
		||||
| 
						 | 
				
			
			@ -166,6 +206,8 @@ ObjectModel<HyprlandMonitor>* HyprlandIpc::monitors() { return &this->mMonitors;
 | 
			
		|||
 | 
			
		||||
ObjectModel<HyprlandWorkspace>* HyprlandIpc::workspaces() { return &this->mWorkspaces; }
 | 
			
		||||
 | 
			
		||||
ObjectModel<HyprlandToplevel>* HyprlandIpc::toplevels() { return &this->mToplevels; }
 | 
			
		||||
 | 
			
		||||
QVector<QByteArrayView> HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) {
 | 
			
		||||
	auto args = QVector<QByteArrayView>();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -218,6 +260,7 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
 | 
			
		|||
	if (event->name == "configreloaded") {
 | 
			
		||||
		this->refreshMonitors(true);
 | 
			
		||||
		this->refreshWorkspaces(true);
 | 
			
		||||
		this->refreshToplevels();
 | 
			
		||||
	} else if (event->name == "monitoraddedv2") {
 | 
			
		||||
		auto args = event->parseView(3);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -390,6 +433,133 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) {
 | 
			
		|||
		// the fullscreen state changed, but this falls apart if you move a fullscreen
 | 
			
		||||
		// window between workspaces.
 | 
			
		||||
		this->refreshWorkspaces(false);
 | 
			
		||||
	} else if (event->name == "openwindow") {
 | 
			
		||||
		auto args = event->parseView(4);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
 | 
			
		||||
		if (!ok) return;
 | 
			
		||||
 | 
			
		||||
		auto workspaceName = QString::fromUtf8(args.at(1));
 | 
			
		||||
		auto windowTitle = QString::fromUtf8(args.at(2));
 | 
			
		||||
		auto windowClass = QString::fromUtf8(args.at(3));
 | 
			
		||||
 | 
			
		||||
		auto* workspace = this->findWorkspaceByName(workspaceName, false);
 | 
			
		||||
		if (!workspace) {
 | 
			
		||||
			qCWarning(logHyprlandIpc) << "Got openwindow for workspace" << workspaceName
 | 
			
		||||
			                          << "which was not previously tracked.";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto* toplevel = this->findToplevelByAddress(windowAddress, false);
 | 
			
		||||
		const bool existed = toplevel != nullptr;
 | 
			
		||||
 | 
			
		||||
		if (!toplevel) toplevel = new HyprlandToplevel(this);
 | 
			
		||||
		toplevel->updateInitial(windowAddress, windowTitle, workspaceName);
 | 
			
		||||
 | 
			
		||||
		workspace->insertToplevel(toplevel);
 | 
			
		||||
 | 
			
		||||
		if (!existed) {
 | 
			
		||||
			this->mToplevels.insertObject(toplevel);
 | 
			
		||||
			qCDebug(logHyprlandIpc) << "New toplevel created with address" << windowAddress << ", title"
 | 
			
		||||
			                        << windowTitle << ", workspace" << workspaceName;
 | 
			
		||||
		}
 | 
			
		||||
	} else if (event->name == "closewindow") {
 | 
			
		||||
		auto args = event->parseView(1);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
 | 
			
		||||
		if (!ok) return;
 | 
			
		||||
 | 
			
		||||
		const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
		auto toplevelIter = std::ranges::find_if(mList, [windowAddress](HyprlandToplevel* m) {
 | 
			
		||||
			return m->address() == windowAddress;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (toplevelIter == mList.end()) {
 | 
			
		||||
			qCWarning(logHyprlandIpc) << "Got closewindow for address" << windowAddress
 | 
			
		||||
			                          << "which was not previously tracked.";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto* toplevel = *toplevelIter;
 | 
			
		||||
		auto index = toplevelIter - mList.begin();
 | 
			
		||||
		this->mToplevels.removeAt(index);
 | 
			
		||||
 | 
			
		||||
		// Remove from workspace
 | 
			
		||||
		auto* workspace = toplevel->bindableWorkspace().value();
 | 
			
		||||
		if (workspace) {
 | 
			
		||||
			workspace->toplevels()->removeObject(toplevel);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		delete toplevel;
 | 
			
		||||
	} else if (event->name == "movewindowv2") {
 | 
			
		||||
		auto args = event->parseView(3);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
		auto workspaceName = QString::fromUtf8(args.at(2));
 | 
			
		||||
 | 
			
		||||
		auto* toplevel = this->findToplevelByAddress(windowAddress, false);
 | 
			
		||||
		if (!toplevel) {
 | 
			
		||||
			qCWarning(logHyprlandIpc) << "Got movewindowv2 event for client with address" << windowAddress
 | 
			
		||||
			                          << "which was not previously tracked.";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		HyprlandWorkspace* workspace = this->findWorkspaceByName(workspaceName, false);
 | 
			
		||||
		if (!workspace) {
 | 
			
		||||
			qCWarning(logHyprlandIpc) << "Got movewindowv2 event for workspace" << args.at(2)
 | 
			
		||||
			                          << "which was not previously tracked.";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		auto* oldWorkspace = toplevel->bindableWorkspace().value();
 | 
			
		||||
		toplevel->setWorkspace(workspace);
 | 
			
		||||
 | 
			
		||||
		if (oldWorkspace) {
 | 
			
		||||
			oldWorkspace->removeToplevel(toplevel);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		workspace->insertToplevel(toplevel);
 | 
			
		||||
	} else if (event->name == "windowtitlev2") {
 | 
			
		||||
		auto args = event->parseView(2);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
		auto windowTitle = QString::fromUtf8(args.at(1));
 | 
			
		||||
 | 
			
		||||
		if (!ok) return;
 | 
			
		||||
 | 
			
		||||
		// It happens that Hyprland sends windowtitlev2 events before event
 | 
			
		||||
		// "openwindow" is emitted, so let's preemptively create it
 | 
			
		||||
		auto* toplevel = this->findToplevelByAddress(windowAddress, true);
 | 
			
		||||
		if (!toplevel) {
 | 
			
		||||
			qCWarning(logHyprlandIpc) << "Got windowtitlev2 event for client with address"
 | 
			
		||||
			                          << windowAddress << "which was not previously tracked.";
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		toplevel->bindableTitle().setValue(windowTitle);
 | 
			
		||||
	} else if (event->name == "activewindowv2") {
 | 
			
		||||
		auto args = event->parseView(1);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
 | 
			
		||||
		if (!ok) return;
 | 
			
		||||
 | 
			
		||||
		// Did not observe "activewindowv2" event before "openwindow",
 | 
			
		||||
		// but better safe than sorry, so create if missing.
 | 
			
		||||
		auto* toplevel = this->findToplevelByAddress(windowAddress, true);
 | 
			
		||||
		this->bActiveToplevel = toplevel;
 | 
			
		||||
	} else if (event->name == "urgent") {
 | 
			
		||||
		auto args = event->parseView(1);
 | 
			
		||||
		auto ok = false;
 | 
			
		||||
		auto windowAddress = args.at(0).toULongLong(&ok, 16);
 | 
			
		||||
 | 
			
		||||
		if (!ok) return;
 | 
			
		||||
 | 
			
		||||
		// It happens that Hyprland sends urgent before "openwindow"
 | 
			
		||||
		auto* toplevel = this->findToplevelByAddress(windowAddress, true);
 | 
			
		||||
		toplevel->bindableUrgent().setValue(true);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -496,6 +666,71 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) {
 | 
			
		|||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HyprlandToplevel* HyprlandIpc::findToplevelByAddress(quint64 address, bool createIfMissing) {
 | 
			
		||||
	const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
	HyprlandToplevel* toplevel = nullptr;
 | 
			
		||||
 | 
			
		||||
	auto toplevelIter =
 | 
			
		||||
	    std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; });
 | 
			
		||||
 | 
			
		||||
	toplevel = toplevelIter == mList.end() ? nullptr : *toplevelIter;
 | 
			
		||||
 | 
			
		||||
	if (!toplevel && createIfMissing) {
 | 
			
		||||
		qCDebug(logHyprlandIpc) << "Toplevel with address" << address
 | 
			
		||||
		                        << "requested before creation, performing early init";
 | 
			
		||||
 | 
			
		||||
		toplevel = new HyprlandToplevel(this);
 | 
			
		||||
		toplevel->updateInitial(address, "", "");
 | 
			
		||||
		this->mToplevels.insertObject(toplevel);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return toplevel;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandIpc::refreshToplevels() {
 | 
			
		||||
	if (this->requestingToplevels) return;
 | 
			
		||||
	this->requestingToplevels = true;
 | 
			
		||||
 | 
			
		||||
	this->makeRequest("j/clients", [this](bool success, const QByteArray& resp) {
 | 
			
		||||
		this->requestingToplevels = false;
 | 
			
		||||
		if (!success) return;
 | 
			
		||||
 | 
			
		||||
		qCDebug(logHyprlandIpc) << "Parsing j/clients response";
 | 
			
		||||
		auto json = QJsonDocument::fromJson(resp).array();
 | 
			
		||||
 | 
			
		||||
		const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
 | 
			
		||||
		for (auto entry: json) {
 | 
			
		||||
			auto object = entry.toObject().toVariantMap();
 | 
			
		||||
 | 
			
		||||
			bool ok = false;
 | 
			
		||||
			auto address = object.value("address").toString().toULongLong(&ok, 16);
 | 
			
		||||
 | 
			
		||||
			if (!ok) {
 | 
			
		||||
				qCWarning(logHyprlandIpc) << "Invalid address in j/clients entry:" << object;
 | 
			
		||||
				continue;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			auto toplevelsIter =
 | 
			
		||||
			    std::ranges::find_if(mList, [&](HyprlandToplevel* m) { return m->address() == address; });
 | 
			
		||||
 | 
			
		||||
			auto* toplevel = toplevelsIter == mList.end() ? nullptr : *toplevelsIter;
 | 
			
		||||
			auto exists = toplevel != nullptr;
 | 
			
		||||
 | 
			
		||||
			if (!exists) toplevel = new HyprlandToplevel(this);
 | 
			
		||||
			toplevel->updateFromObject(object);
 | 
			
		||||
 | 
			
		||||
			if (!exists) {
 | 
			
		||||
				qCDebug(logHyprlandIpc) << "New toplevel created with address" << address;
 | 
			
		||||
				this->mToplevels.insertObject(toplevel);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			auto* workspace = toplevel->bindableWorkspace().value();
 | 
			
		||||
			workspace->insertToplevel(toplevel);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HyprlandMonitor*
 | 
			
		||||
HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) {
 | 
			
		||||
	const auto& mList = this->mMonitors.valueList();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,16 +14,19 @@
 | 
			
		|||
 | 
			
		||||
#include "../../../core/model.hpp"
 | 
			
		||||
#include "../../../core/qmlscreen.hpp"
 | 
			
		||||
#include "../../../wayland/toplevel_management/handle.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
 | 
			
		||||
class HyprlandMonitor;
 | 
			
		||||
class HyprlandWorkspace;
 | 
			
		||||
class HyprlandToplevel;
 | 
			
		||||
 | 
			
		||||
} // namespace qs::hyprland::ipc
 | 
			
		||||
 | 
			
		||||
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*);
 | 
			
		||||
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*);
 | 
			
		||||
Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandToplevel*);
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -85,18 +88,25 @@ public:
 | 
			
		|||
		return &this->bFocusedWorkspace;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QBindable<HyprlandToplevel*> bindableActiveToplevel() const {
 | 
			
		||||
		return &this->bActiveToplevel;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	void setFocusedMonitor(HyprlandMonitor* monitor);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] ObjectModel<HyprlandMonitor>* monitors();
 | 
			
		||||
	[[nodiscard]] ObjectModel<HyprlandWorkspace>* workspaces();
 | 
			
		||||
	[[nodiscard]] ObjectModel<HyprlandToplevel>* toplevels();
 | 
			
		||||
 | 
			
		||||
	// No byId because these preemptively create objects. The given id is set if created.
 | 
			
		||||
	HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = -1);
 | 
			
		||||
	HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1);
 | 
			
		||||
	HyprlandToplevel* findToplevelByAddress(quint64 address, bool createIfMissing);
 | 
			
		||||
 | 
			
		||||
	// canCreate avoids making ghost workspaces when the connection races
 | 
			
		||||
	void refreshWorkspaces(bool canCreate);
 | 
			
		||||
	void refreshMonitors(bool canCreate);
 | 
			
		||||
	void refreshToplevels();
 | 
			
		||||
 | 
			
		||||
	// The last argument may contain commas, so the count is required.
 | 
			
		||||
	[[nodiscard]] static QVector<QByteArrayView> parseEventArgs(QByteArrayView event, quint16 count);
 | 
			
		||||
| 
						 | 
				
			
			@ -107,12 +117,18 @@ signals:
 | 
			
		|||
 | 
			
		||||
	void focusedMonitorChanged();
 | 
			
		||||
	void focusedWorkspaceChanged();
 | 
			
		||||
	void activeToplevelChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void eventSocketError(QLocalSocket::LocalSocketError error) const;
 | 
			
		||||
	void eventSocketStateChanged(QLocalSocket::LocalSocketState state);
 | 
			
		||||
	void eventSocketReady();
 | 
			
		||||
 | 
			
		||||
	void toplevelAddressed(
 | 
			
		||||
	    qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
 | 
			
		||||
	    quint64 address
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	void onFocusedMonitorDestroyed();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
| 
						 | 
				
			
			@ -128,10 +144,12 @@ private:
 | 
			
		|||
	bool valid = false;
 | 
			
		||||
	bool requestingMonitors = false;
 | 
			
		||||
	bool requestingWorkspaces = false;
 | 
			
		||||
	bool requestingToplevels = false;
 | 
			
		||||
	bool monitorsRequested = false;
 | 
			
		||||
 | 
			
		||||
	ObjectModel<HyprlandMonitor> mMonitors {this};
 | 
			
		||||
	ObjectModel<HyprlandWorkspace> mWorkspaces {this};
 | 
			
		||||
	ObjectModel<HyprlandToplevel> mToplevels {this};
 | 
			
		||||
 | 
			
		||||
	HyprlandIpcEvent event {this};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -148,6 +166,13 @@ private:
 | 
			
		|||
	    bFocusedWorkspace,
 | 
			
		||||
	    &HyprlandIpc::focusedWorkspaceChanged
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(
 | 
			
		||||
	    HyprlandIpc,
 | 
			
		||||
	    HyprlandToplevel*,
 | 
			
		||||
	    bActiveToplevel,
 | 
			
		||||
	    &HyprlandIpc::activeToplevelChanged
 | 
			
		||||
	);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::hyprland::ipc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,50 +2,159 @@
 | 
			
		|||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qproperty.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "toplevel_mapping.hpp"
 | 
			
		||||
#include "../../toplevel_management/handle.hpp"
 | 
			
		||||
#include "../../toplevel_management/qml.hpp"
 | 
			
		||||
#include "connection.hpp"
 | 
			
		||||
#include "toplevel_mapping.hpp"
 | 
			
		||||
#include "workspace.hpp"
 | 
			
		||||
 | 
			
		||||
using namespace qs::wayland::toplevel_management;
 | 
			
		||||
using namespace qs::wayland::toplevel_management::impl;
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
 | 
			
		||||
HyprlandToplevel::HyprlandToplevel(Toplevel* toplevel)
 | 
			
		||||
    : QObject(toplevel)
 | 
			
		||||
    , handle(toplevel->implHandle()) {
 | 
			
		||||
	auto* instance = HyprlandToplevelMappingManager::instance();
 | 
			
		||||
	auto addr = instance->getToplevelAddress(handle);
 | 
			
		||||
HyprlandToplevel::HyprlandToplevel(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {
 | 
			
		||||
	this->bMonitor.setBinding([this]() {
 | 
			
		||||
		return this->bWorkspace ? this->bWorkspace->bindableMonitor().value() : nullptr;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (addr != 0) this->setAddress(addr);
 | 
			
		||||
	else {
 | 
			
		||||
		QObject::connect(
 | 
			
		||||
		    instance,
 | 
			
		||||
		    &HyprlandToplevelMappingManager::toplevelAddressed,
 | 
			
		||||
		    this,
 | 
			
		||||
		    &HyprlandToplevel::onToplevelAddressed
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
	this->bActivated.setBinding([this]() {
 | 
			
		||||
		return this->ipc->bindableActiveToplevel().value() == this;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	QObject::connect(
 | 
			
		||||
	    this,
 | 
			
		||||
	    &HyprlandToplevel::activatedChanged,
 | 
			
		||||
	    this,
 | 
			
		||||
	    &HyprlandToplevel::onActivatedChanged
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::onToplevelAddressed(ToplevelHandle* handle, quint64 address) {
 | 
			
		||||
	if (handle == this->handle) {
 | 
			
		||||
		this->setAddress(address);
 | 
			
		||||
		QObject::disconnect(HyprlandToplevelMappingManager::instance(), nullptr, this, nullptr);
 | 
			
		||||
HyprlandToplevel::HyprlandToplevel(HyprlandIpc* ipc, Toplevel* toplevel): HyprlandToplevel(ipc) {
 | 
			
		||||
	this->mWaylandHandle = toplevel->implHandle();
 | 
			
		||||
	auto* instance = HyprlandToplevelMappingManager::instance();
 | 
			
		||||
	auto addr = instance->getToplevelAddress(this->mWaylandHandle);
 | 
			
		||||
 | 
			
		||||
	if (!addr) {
 | 
			
		||||
		// Address not available, will rely on HyprlandIpc to resolve it.
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->setAddress(addr);
 | 
			
		||||
 | 
			
		||||
	// Check if client is present in HyprlandIPC
 | 
			
		||||
	auto* hyprToplevel = ipc->findToplevelByAddress(addr, false);
 | 
			
		||||
	// HyprlandIpc will eventually resolve it
 | 
			
		||||
	if (!hyprToplevel) return;
 | 
			
		||||
 | 
			
		||||
	this->setHyprlandHandle(hyprToplevel);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::updateInitial(
 | 
			
		||||
    quint64 address,
 | 
			
		||||
    const QString& title,
 | 
			
		||||
    const QString& workspaceName
 | 
			
		||||
) {
 | 
			
		||||
	auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false);
 | 
			
		||||
	Qt::beginPropertyUpdateGroup();
 | 
			
		||||
	this->setAddress(address);
 | 
			
		||||
	this->bTitle = title;
 | 
			
		||||
	this->setWorkspace(workspace);
 | 
			
		||||
	Qt::endPropertyUpdateGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::updateFromObject(const QVariantMap& object) {
 | 
			
		||||
	auto addressStr = object.value("address").value<QString>();
 | 
			
		||||
	auto title = object.value("title").value<QString>();
 | 
			
		||||
 | 
			
		||||
	bool ok = false;
 | 
			
		||||
	auto address = addressStr.toULongLong(&ok, 16);
 | 
			
		||||
	if (!ok || !address) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->setAddress(address);
 | 
			
		||||
	this->bTitle = title;
 | 
			
		||||
 | 
			
		||||
	auto workspaceMap = object.value("workspace").toMap();
 | 
			
		||||
	auto workspaceName = workspaceMap.value("name").toString();
 | 
			
		||||
 | 
			
		||||
	auto* workspace = this->ipc->findWorkspaceByName(workspaceName, false);
 | 
			
		||||
	if (!workspace) return;
 | 
			
		||||
 | 
			
		||||
	this->setWorkspace(workspace);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::setWorkspace(HyprlandWorkspace* workspace) {
 | 
			
		||||
	auto* oldWorkspace = this->bWorkspace.value();
 | 
			
		||||
	if (oldWorkspace == workspace) return;
 | 
			
		||||
 | 
			
		||||
	if (oldWorkspace) {
 | 
			
		||||
		QObject::disconnect(oldWorkspace, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->bWorkspace = workspace;
 | 
			
		||||
 | 
			
		||||
	if (workspace) {
 | 
			
		||||
		QObject::connect(workspace, &QObject::destroyed, this, [this]() {
 | 
			
		||||
			this->bWorkspace = nullptr;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::setAddress(quint64 address) {
 | 
			
		||||
	this->mAddress = QString::number(address, 16);
 | 
			
		||||
	this->mAddress = address;
 | 
			
		||||
	emit this->addressChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Toplevel* HyprlandToplevel::waylandHandle() {
 | 
			
		||||
	return ToplevelManager::instance()->forImpl(this->mWaylandHandle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::setWaylandHandle(impl::ToplevelHandle* handle) {
 | 
			
		||||
	if (this->mWaylandHandle == handle) return;
 | 
			
		||||
	if (this->mWaylandHandle) {
 | 
			
		||||
		QObject::disconnect(this->mWaylandHandle, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mWaylandHandle = handle;
 | 
			
		||||
	if (handle) {
 | 
			
		||||
		QObject::connect(handle, &QObject::destroyed, this, [this]() {
 | 
			
		||||
			this->mWaylandHandle = nullptr;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	emit this->waylandHandleChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::setHyprlandHandle(HyprlandToplevel* handle) {
 | 
			
		||||
	if (this->mHyprlandHandle == handle) return;
 | 
			
		||||
	if (this->mHyprlandHandle) {
 | 
			
		||||
		QObject::disconnect(this->mHyprlandHandle, nullptr, this, nullptr);
 | 
			
		||||
	}
 | 
			
		||||
	this->mHyprlandHandle = handle;
 | 
			
		||||
	if (handle) {
 | 
			
		||||
		QObject::connect(handle, &QObject::destroyed, this, [this]() {
 | 
			
		||||
			this->mHyprlandHandle = nullptr;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	emit this->hyprlandHandleChanged();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandToplevel::onActivatedChanged() {
 | 
			
		||||
	if (this->bUrgent.value()) {
 | 
			
		||||
		// If was urgent, and now active, clear urgent state
 | 
			
		||||
		this->bUrgent = false;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HyprlandToplevel* HyprlandToplevel::qmlAttachedProperties(QObject* object) {
 | 
			
		||||
	if (auto* toplevel = qobject_cast<Toplevel*>(object)) {
 | 
			
		||||
		return new HyprlandToplevel(toplevel);
 | 
			
		||||
		auto* ipc = HyprlandIpc::instance();
 | 
			
		||||
		return new HyprlandToplevel(ipc, toplevel);
 | 
			
		||||
	} else {
 | 
			
		||||
		return nullptr;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,49 +2,108 @@
 | 
			
		|||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
#include <qobject.h>
 | 
			
		||||
#include <qproperty.h>
 | 
			
		||||
#include <qqmlintegration.h>
 | 
			
		||||
#include <qtmetamacros.h>
 | 
			
		||||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "../../toplevel_management/handle.hpp"
 | 
			
		||||
#include "../../toplevel_management/qml.hpp"
 | 
			
		||||
#include "connection.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
 | 
			
		||||
//! Exposes Hyprland window address for a Toplevel
 | 
			
		||||
/// Attached object of @@Quickshell.Wayland.Toplevel which exposes
 | 
			
		||||
/// a Hyprland window address for the window.
 | 
			
		||||
//! Hyprland Toplevel
 | 
			
		||||
/// Represents a window as Hyprland exposes it.
 | 
			
		||||
/// Can also be used as an attached object of a @@Quickshell.Wayland.Toplevel,
 | 
			
		||||
/// to resolve a handle to an Hyprland toplevel.
 | 
			
		||||
class HyprlandToplevel: public QObject {
 | 
			
		||||
	Q_OBJECT;
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_UNCREATABLE("");
 | 
			
		||||
	QML_ATTACHED(HyprlandToplevel);
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	/// Hexadecimal Hyprland window address. Will be an empty string until
 | 
			
		||||
	/// the address is reported.
 | 
			
		||||
	Q_PROPERTY(QString address READ address NOTIFY addressChanged);
 | 
			
		||||
	Q_PROPERTY(QString address READ addressStr NOTIFY addressChanged);
 | 
			
		||||
	/// The toplevel handle, exposing the Hyprland toplevel.
 | 
			
		||||
	/// Will be null until the address is reported
 | 
			
		||||
	Q_PROPERTY(HyprlandToplevel* handle READ hyprlandHandle NOTIFY hyprlandHandleChanged);
 | 
			
		||||
	/// The wayland toplevel handle. Will be null intil the address is reported
 | 
			
		||||
	Q_PROPERTY(qs::wayland::toplevel_management::Toplevel* wayland READ waylandHandle NOTIFY waylandHandleChanged);
 | 
			
		||||
	/// The title of the toplevel
 | 
			
		||||
	Q_PROPERTY(QString title READ default NOTIFY titleChanged BINDABLE bindableTitle);
 | 
			
		||||
	/// Whether the toplevel is active or not
 | 
			
		||||
	Q_PROPERTY(bool activated READ default NOTIFY activatedChanged BINDABLE bindableActivated);
 | 
			
		||||
	/// Whether the client is urgent or not
 | 
			
		||||
	Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent);
 | 
			
		||||
	/// The current workspace of the toplevel (might be null)
 | 
			
		||||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* workspace READ default NOTIFY workspaceChanged BINDABLE bindableWorkspace);
 | 
			
		||||
	/// The current monitor of the toplevel (might be null)
 | 
			
		||||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* monitor READ default NOTIFY monitorChanged BINDABLE bindableMonitor);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
public:
 | 
			
		||||
	explicit HyprlandToplevel(qs::wayland::toplevel_management::Toplevel* toplevel);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QString address() { return this->mAddress; }
 | 
			
		||||
	/// When invoked from HyprlandIpc, reacting to Hyprland's IPC events.
 | 
			
		||||
	explicit HyprlandToplevel(HyprlandIpc* ipc);
 | 
			
		||||
	/// When attached from a Toplevel
 | 
			
		||||
	explicit HyprlandToplevel(HyprlandIpc* ipc, qs::wayland::toplevel_management::Toplevel* toplevel);
 | 
			
		||||
 | 
			
		||||
	static HyprlandToplevel* qmlAttachedProperties(QObject* object);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void addressChanged();
 | 
			
		||||
	void updateInitial(quint64 address, const QString& title, const QString& workspaceName);
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onToplevelAddressed(
 | 
			
		||||
	    qs::wayland::toplevel_management::impl::ToplevelHandle* handle,
 | 
			
		||||
	    quint64 address
 | 
			
		||||
	);
 | 
			
		||||
	void updateFromObject(const QVariantMap& object);
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	[[nodiscard]] QString addressStr() const { return QString::number(this->mAddress, 16); }
 | 
			
		||||
	[[nodiscard]] quint64 address() const { return this->mAddress; }
 | 
			
		||||
	void setAddress(quint64 address);
 | 
			
		||||
 | 
			
		||||
	QString mAddress;
 | 
			
		||||
	// doesn't have to be nulled on destroy, only used for comparison
 | 
			
		||||
	qs::wayland::toplevel_management::impl::ToplevelHandle* handle;
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	[[nodiscard]] HyprlandToplevel* hyprlandHandle() { return this->mHyprlandHandle; }
 | 
			
		||||
	void setHyprlandHandle(HyprlandToplevel* handle);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] wayland::toplevel_management::Toplevel* waylandHandle();
 | 
			
		||||
	void setWaylandHandle(wayland::toplevel_management::impl::ToplevelHandle* handle);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QBindable<QString> bindableTitle() { return &this->bTitle; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableActivated() { return &this->bActivated; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableUrgent() { return &this->bUrgent; }
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QBindable<HyprlandWorkspace*> bindableWorkspace() { return &this->bWorkspace; }
 | 
			
		||||
	void setWorkspace(HyprlandWorkspace* workspace);
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QBindable<HyprlandMonitor*> bindableMonitor() { return &this->bMonitor; }
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void addressChanged();
 | 
			
		||||
	QSDOC_HIDE void waylandHandleChanged();
 | 
			
		||||
	QSDOC_HIDE void hyprlandHandleChanged();
 | 
			
		||||
 | 
			
		||||
	void titleChanged();
 | 
			
		||||
	void activatedChanged();
 | 
			
		||||
	void urgentChanged();
 | 
			
		||||
	void workspaceChanged();
 | 
			
		||||
	void monitorChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onActivatedChanged();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	quint64 mAddress = 0;
 | 
			
		||||
	HyprlandIpc* ipc;
 | 
			
		||||
 | 
			
		||||
	qs::wayland::toplevel_management::impl::ToplevelHandle* mWaylandHandle = nullptr;
 | 
			
		||||
	HyprlandToplevel* mHyprlandHandle = nullptr;
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, QString, bTitle, &HyprlandToplevel::titleChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, bool, bActivated, &HyprlandToplevel::activatedChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, bool, bUrgent, &HyprlandToplevel::urgentChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandWorkspace*, bWorkspace, &HyprlandToplevel::workspaceChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandToplevel, HyprlandMonitor*, bMonitor, &HyprlandToplevel::monitorChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::hyprland::ipc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,6 +28,13 @@ HyprlandIpcQml::HyprlandIpcQml() {
 | 
			
		|||
	    this,
 | 
			
		||||
	    &HyprlandIpcQml::focusedMonitorChanged
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	QObject::connect(
 | 
			
		||||
	    instance,
 | 
			
		||||
	    &HyprlandIpc::activeToplevelChanged,
 | 
			
		||||
	    this,
 | 
			
		||||
	    &HyprlandIpcQml::activeToplevelChanged
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandIpcQml::dispatch(const QString& request) {
 | 
			
		||||
| 
						 | 
				
			
			@ -51,6 +58,10 @@ QBindable<HyprlandWorkspace*> HyprlandIpcQml::bindableFocusedWorkspace() {
 | 
			
		|||
	return HyprlandIpc::instance()->bindableFocusedWorkspace();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
QBindable<HyprlandToplevel*> HyprlandIpcQml::bindableActiveToplevel() {
 | 
			
		||||
	return HyprlandIpc::instance()->bindableActiveToplevel();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ObjectModel<HyprlandMonitor>* HyprlandIpcQml::monitors() {
 | 
			
		||||
	return HyprlandIpc::instance()->monitors();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -59,4 +70,8 @@ ObjectModel<HyprlandWorkspace>* HyprlandIpcQml::workspaces() {
 | 
			
		|||
	return HyprlandIpc::instance()->workspaces();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ObjectModel<HyprlandToplevel>* HyprlandIpcQml::toplevels() {
 | 
			
		||||
	return HyprlandIpc::instance()->toplevels();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
} // namespace qs::hyprland::ipc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,8 @@ class HyprlandIpcQml: public QObject {
 | 
			
		|||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* focusedMonitor READ default NOTIFY focusedMonitorChanged BINDABLE bindableFocusedMonitor);
 | 
			
		||||
	/// The currently focused hyprland workspace. May be null.
 | 
			
		||||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandWorkspace* focusedWorkspace READ default NOTIFY focusedWorkspaceChanged BINDABLE bindableFocusedWorkspace);
 | 
			
		||||
	/// Currently active toplevel (might be null)
 | 
			
		||||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandToplevel* activeToplevel READ default NOTIFY activeToplevelChanged BINDABLE bindableActiveToplevel);
 | 
			
		||||
	/// All hyprland monitors.
 | 
			
		||||
	QSDOC_TYPE_OVERRIDE(ObjectModel<qs::hyprland::ipc::HyprlandMonitor>*);
 | 
			
		||||
	Q_PROPERTY(UntypedObjectModel* monitors READ monitors CONSTANT);
 | 
			
		||||
| 
						 | 
				
			
			@ -32,6 +34,9 @@ class HyprlandIpcQml: public QObject {
 | 
			
		|||
	/// > [!NOTE] Named workspaces have a negative id, and will appear before unnamed workspaces.
 | 
			
		||||
	QSDOC_TYPE_OVERRIDE(ObjectModel<qs::hyprland::ipc::HyprlandWorkspace>*);
 | 
			
		||||
	Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT);
 | 
			
		||||
	/// All hyprland toplevels
 | 
			
		||||
	QSDOC_TYPE_OVERRIDE(ObjectModel<qs::hyprland::ipc::HyprlandToplevel>*);
 | 
			
		||||
	Q_PROPERTY(UntypedObjectModel* toplevels READ toplevels CONSTANT);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_NAMED_ELEMENT(Hyprland);
 | 
			
		||||
	QML_SINGLETON;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,8 +66,10 @@ public:
 | 
			
		|||
	[[nodiscard]] static QString eventSocketPath();
 | 
			
		||||
	[[nodiscard]] static QBindable<HyprlandMonitor*> bindableFocusedMonitor();
 | 
			
		||||
	[[nodiscard]] static QBindable<HyprlandWorkspace*> bindableFocusedWorkspace();
 | 
			
		||||
	[[nodiscard]] static QBindable<HyprlandToplevel*> bindableActiveToplevel();
 | 
			
		||||
	[[nodiscard]] static ObjectModel<HyprlandMonitor>* monitors();
 | 
			
		||||
	[[nodiscard]] static ObjectModel<HyprlandWorkspace>* workspaces();
 | 
			
		||||
	[[nodiscard]] static ObjectModel<HyprlandToplevel>* toplevels();
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	/// Emitted for every event that comes in through the hyprland event socket (socket2).
 | 
			
		||||
| 
						 | 
				
			
			@ -72,6 +79,7 @@ signals:
 | 
			
		|||
 | 
			
		||||
	void focusedMonitorChanged();
 | 
			
		||||
	void focusedWorkspaceChanged();
 | 
			
		||||
	void activeToplevelChanged();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
} // namespace qs::hyprland::ipc
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
#include "workspace.hpp"
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
#include <qcontainerfwd.h>
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +26,12 @@ HyprlandWorkspace::HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {
 | 
			
		|||
		return this->ipc->bindableFocusedWorkspace().value() == this;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	QObject::connect(this, &HyprlandWorkspace::focusedChanged, this, [this]() {
 | 
			
		||||
		if (this->bFocused.value()) {
 | 
			
		||||
			this->updateUrgent();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	Qt::endPropertyUpdateGroup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +89,67 @@ void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) {
 | 
			
		|||
 | 
			
		||||
void HyprlandWorkspace::onMonitorDestroyed() { this->bMonitor = nullptr; }
 | 
			
		||||
 | 
			
		||||
void HyprlandWorkspace::insertToplevel(HyprlandToplevel* toplevel) {
 | 
			
		||||
	if (!toplevel) return;
 | 
			
		||||
 | 
			
		||||
	const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
 | 
			
		||||
	if (std::ranges::find(mList, toplevel) != mList.end()) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	this->mToplevels.insertObject(toplevel);
 | 
			
		||||
 | 
			
		||||
	QObject::connect(toplevel, &QObject::destroyed, this, [this, toplevel]() {
 | 
			
		||||
		this->removeToplevel(toplevel);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	QObject::connect(
 | 
			
		||||
	    toplevel,
 | 
			
		||||
	    &HyprlandToplevel::urgentChanged,
 | 
			
		||||
	    this,
 | 
			
		||||
	    &HyprlandWorkspace::updateUrgent
 | 
			
		||||
	);
 | 
			
		||||
 | 
			
		||||
	this->updateUrgent();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandWorkspace::removeToplevel(HyprlandToplevel* toplevel) {
 | 
			
		||||
	if (!toplevel) return;
 | 
			
		||||
 | 
			
		||||
	this->mToplevels.removeObject(toplevel);
 | 
			
		||||
	emit this->updateUrgent();
 | 
			
		||||
	QObject::disconnect(toplevel, nullptr, this, nullptr);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Triggered when there is an update either on the toplevel list, on a toplevel's urgent state
 | 
			
		||||
void HyprlandWorkspace::updateUrgent() {
 | 
			
		||||
	const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
 | 
			
		||||
	const bool hasUrgentToplevel = std::ranges::any_of(mList, [&](HyprlandToplevel* toplevel) {
 | 
			
		||||
		return toplevel->bindableUrgent().value();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (this->bFocused && hasUrgentToplevel) {
 | 
			
		||||
		this->clearUrgent();
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (hasUrgentToplevel != this->bUrgent.value()) {
 | 
			
		||||
		this->bUrgent = hasUrgentToplevel;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandWorkspace::clearUrgent() {
 | 
			
		||||
	this->bUrgent = false;
 | 
			
		||||
 | 
			
		||||
	// Clear all urgent toplevels
 | 
			
		||||
	const auto& mList = this->mToplevels.valueList();
 | 
			
		||||
	for (auto* toplevel: mList) {
 | 
			
		||||
		toplevel->bindableUrgent().setValue(false);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HyprlandWorkspace::activate() {
 | 
			
		||||
	this->ipc->dispatch(QString("workspace %1").arg(this->bId.value()));
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@
 | 
			
		|||
#include <qtypes.h>
 | 
			
		||||
 | 
			
		||||
#include "connection.hpp"
 | 
			
		||||
#include "hyprland_toplevel.hpp"
 | 
			
		||||
 | 
			
		||||
namespace qs::hyprland::ipc {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -24,8 +25,11 @@ class HyprlandWorkspace: public QObject {
 | 
			
		|||
	/// If this workspace is currently active on a monitor and that monitor is currently
 | 
			
		||||
	/// focused. See also @@active.
 | 
			
		||||
	Q_PROPERTY(bool focused READ default NOTIFY focusedChanged BINDABLE bindableFocused);
 | 
			
		||||
	/// If this workspace has a window that is urgent.
 | 
			
		||||
	/// Becomes always falsed after the workspace is @@focused.
 | 
			
		||||
	Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent);
 | 
			
		||||
	/// If this workspace currently has a fullscreen client.
 | 
			
		||||
	Q_PROPERTY(bool hasFullscreen READ default NOTIFY focusedChanged BINDABLE bindableHasFullscreen);
 | 
			
		||||
	Q_PROPERTY(bool hasFullscreen READ default NOTIFY hasFullscreenChanged BINDABLE bindableHasFullscreen);
 | 
			
		||||
	/// Last json returned for this workspace, as a javascript object.
 | 
			
		||||
	///
 | 
			
		||||
	/// > [!WARNING] This is *not* updated unless the workspace object is fetched again from
 | 
			
		||||
| 
						 | 
				
			
			@ -33,6 +37,9 @@ class HyprlandWorkspace: public QObject {
 | 
			
		|||
	/// > property, run @@Hyprland.refreshWorkspaces() and wait for this property to update.
 | 
			
		||||
	Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged);
 | 
			
		||||
	Q_PROPERTY(qs::hyprland::ipc::HyprlandMonitor* monitor READ default NOTIFY monitorChanged BINDABLE bindableMonitor);
 | 
			
		||||
	/// List of toplevels on this workspace.
 | 
			
		||||
	QSDOC_TYPE_OVERRIDE(ObjectModel<qs::hyprland::ipc::HyprlandToplevel*);
 | 
			
		||||
	Q_PROPERTY(UntypedObjectModel* toplevels READ toplevels CONSTANT);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
	QML_ELEMENT;
 | 
			
		||||
	QML_UNCREATABLE("HyprlandWorkspaces must be retrieved from the HyprlandIpc object.");
 | 
			
		||||
| 
						 | 
				
			
			@ -55,35 +62,46 @@ public:
 | 
			
		|||
	[[nodiscard]] QBindable<QString> bindableName() { return &this->bName; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableActive() { return &this->bActive; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableFocused() { return &this->bFocused; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableUrgent() { return &this->bUrgent; }
 | 
			
		||||
	[[nodiscard]] QBindable<bool> bindableHasFullscreen() { return &this->bHasFullscreen; }
 | 
			
		||||
	[[nodiscard]] QBindable<HyprlandMonitor*> bindableMonitor() { return &this->bMonitor; }
 | 
			
		||||
	[[nodiscard]] ObjectModel<HyprlandToplevel>* toplevels() { return &this->mToplevels; }
 | 
			
		||||
 | 
			
		||||
	[[nodiscard]] QVariantMap lastIpcObject() const;
 | 
			
		||||
 | 
			
		||||
	void setMonitor(HyprlandMonitor* monitor);
 | 
			
		||||
 | 
			
		||||
	void insertToplevel(HyprlandToplevel* toplevel);
 | 
			
		||||
	void removeToplevel(HyprlandToplevel* toplevel);
 | 
			
		||||
 | 
			
		||||
signals:
 | 
			
		||||
	void idChanged();
 | 
			
		||||
	void nameChanged();
 | 
			
		||||
	void activeChanged();
 | 
			
		||||
	void focusedChanged();
 | 
			
		||||
	void urgentChanged();
 | 
			
		||||
	void hasFullscreenChanged();
 | 
			
		||||
	void lastIpcObjectChanged();
 | 
			
		||||
	void monitorChanged();
 | 
			
		||||
 | 
			
		||||
private slots:
 | 
			
		||||
	void onMonitorDestroyed();
 | 
			
		||||
	void updateUrgent();
 | 
			
		||||
 | 
			
		||||
private:
 | 
			
		||||
	HyprlandIpc* ipc;
 | 
			
		||||
	void clearUrgent();
 | 
			
		||||
 | 
			
		||||
	HyprlandIpc* ipc;
 | 
			
		||||
	QVariantMap mLastIpcObject;
 | 
			
		||||
 | 
			
		||||
	ObjectModel<HyprlandToplevel> mToplevels {this};
 | 
			
		||||
 | 
			
		||||
	// clang-format off
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(HyprlandWorkspace, qint32, bId, -1, &HyprlandWorkspace::idChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, QString, bName, &HyprlandWorkspace::nameChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bActive, &HyprlandWorkspace::activeChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bFocused, &HyprlandWorkspace::focusedChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bUrgent, &HyprlandWorkspace::urgentChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, bool, bHasFullscreen, &HyprlandWorkspace::hasFullscreenChanged);
 | 
			
		||||
	Q_OBJECT_BINDABLE_PROPERTY(HyprlandWorkspace, HyprlandMonitor*, bMonitor, &HyprlandWorkspace::monitorChanged);
 | 
			
		||||
	// clang-format on
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										37
									
								
								src/wayland/hyprland/test/manual/toplevel-association.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/wayland/hyprland/test/manual/toplevel-association.qml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,37 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
import Quickshell.Wayland
 | 
			
		||||
 | 
			
		||||
FloatingWindow {
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		Text { text: "Hyprland -> Wayland" }
 | 
			
		||||
 | 
			
		||||
		ListView {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			Layout.fillHeight: true
 | 
			
		||||
			clip: true
 | 
			
		||||
			model: Hyprland.toplevels
 | 
			
		||||
			delegate: Text {
 | 
			
		||||
				required property HyprlandToplevel modelData
 | 
			
		||||
				text: `${modelData} -> ${modelData.wayland}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Text { text: "Wayland -> Hyprland" }
 | 
			
		||||
 | 
			
		||||
		ListView {
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			Layout.fillHeight: true
 | 
			
		||||
			clip: true
 | 
			
		||||
			model: ToplevelManager.toplevels
 | 
			
		||||
			delegate: Text {
 | 
			
		||||
				required property Toplevel modelData
 | 
			
		||||
				text: `${modelData} -> ${modelData.HyprlandToplevel.handle}`
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/wayland/hyprland/test/manual/toplevels.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/wayland/hyprland/test/manual/toplevels.qml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
 | 
			
		||||
FloatingWindow {
 | 
			
		||||
	ColumnLayout {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
 | 
			
		||||
		Text { text: "Current toplevel:" }
 | 
			
		||||
 | 
			
		||||
		ToplevelFromHyprland {
 | 
			
		||||
			modelData: Hyprland.activeToplevel
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		Text { text: "\nAll toplevels:" }
 | 
			
		||||
 | 
			
		||||
		ListView {
 | 
			
		||||
			Layout.fillHeight: true
 | 
			
		||||
			Layout.fillWidth: true
 | 
			
		||||
			clip: true
 | 
			
		||||
			model: Hyprland.toplevels
 | 
			
		||||
			delegate: ToplevelFromHyprland {}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	component ToplevelFromHyprland: ColumnLayout {
 | 
			
		||||
		required property HyprlandToplevel modelData
 | 
			
		||||
 | 
			
		||||
		Text {
 | 
			
		||||
			text: `Window 0x${modelData.address}, title: ${modelData.title}, activated: ${modelData.activated}, workspace id: ${modelData.workspace.id}, monitor name: ${modelData.monitor.name}, urgent: ${modelData.urgent}`
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								src/wayland/hyprland/test/manual/workspaces.qml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/wayland/hyprland/test/manual/workspaces.qml
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import QtQuick
 | 
			
		||||
import QtQuick.Layouts
 | 
			
		||||
import Quickshell
 | 
			
		||||
import Quickshell.Widgets
 | 
			
		||||
import Quickshell.Hyprland
 | 
			
		||||
 | 
			
		||||
FloatingWindow {
 | 
			
		||||
	ListView {
 | 
			
		||||
		anchors.fill: parent
 | 
			
		||||
		model: Hyprland.workspaces
 | 
			
		||||
		spacing: 5
 | 
			
		||||
 | 
			
		||||
		delegate: WrapperRectangle {
 | 
			
		||||
			id: wsDelegate
 | 
			
		||||
			required property HyprlandWorkspace modelData
 | 
			
		||||
			color: "lightgray"
 | 
			
		||||
 | 
			
		||||
			ColumnLayout {
 | 
			
		||||
				Text { text: `Workspace ${wsDelegate.modelData.id} on ${wsDelegate.modelData.monitor} | urgent: ${wsDelegate.modelData.urgent}`}
 | 
			
		||||
 | 
			
		||||
				ColumnLayout {
 | 
			
		||||
					Repeater {
 | 
			
		||||
						model: wsDelegate.modelData.toplevels
 | 
			
		||||
						Text {
 | 
			
		||||
							id: tDelegate
 | 
			
		||||
							required property HyprlandToplevel modelData;
 | 
			
		||||
							text: `${tDelegate.modelData}: ${tDelegate.modelData.title}`
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue