hyprland/ipc: expose Hyprland toplevels

This commit is contained in:
Maeeen 2025-06-20 04:09:37 -07:00 committed by outfoxxed
parent c115df8d34
commit 362c8e1b69
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
11 changed files with 685 additions and 43 deletions

View file

@ -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();

View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()));
}

View file

@ -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

View 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}`
}
}
}
}

View 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}`
}
}
}

View 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}`
}
}
}
}
}
}
}