From ed3708f5cb9558ad9024446087b36d019de2df80 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 21 May 2024 05:07:24 -0700 Subject: [PATCH 01/31] service/mpris: add trackChanged signal --- src/services/mpris/player.cpp | 8 +++++++- src/services/mpris/player.hpp | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 3b0c7463..ebdbfd64 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -258,7 +258,13 @@ void MprisPlayer::onMetadataChanged() { auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { - this->mTrackId = trackidVariant.value(); + auto trackId = trackidVariant.value(); + + if (trackId != this->mTrackId) { + this->mTrackId = trackId; + emit this->trackChanged(); + } + this->onSeek(0); } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 0b18d78c..97181a59 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -242,6 +242,8 @@ public: [[nodiscard]] QList supportedMimeTypes() const; signals: + void trackChanged(); + QSDOC_HIDE void ready(); void canControlChanged(); void canPlayChanged(); From f2df3da596adf3b42a36759ed09d0cc8d4bc46f8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 May 2024 04:34:56 -0700 Subject: [PATCH 02/31] service/mpris: fix position being incorrect after pausing --- src/services/mpris/player.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index ebdbfd64..1bb9f7be 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -312,8 +312,11 @@ void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); if (status == "Playing") { + // update the timestamp + this->onSeek(this->positionMs() * 1000); this->mPlaybackState = MprisPlaybackState::Playing; } else if (status == "Paused") { + this->pausedTime = QDateTime::currentDateTimeUtc(); this->mPlaybackState = MprisPlaybackState::Paused; } else if (status == "Stopped") { this->mPlaybackState = MprisPlaybackState::Stopped; From ac339cb23b5d07de381e16ad7c939507cf0d59f7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 May 2024 05:40:03 -0700 Subject: [PATCH 03/31] service/mpris: expose desktopEntry property --- src/services/mpris/player.cpp | 2 ++ src/services/mpris/player.hpp | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 1bb9f7be..27ba34c4 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -60,6 +60,7 @@ MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(paren QObject::connect(&this->pCanRaise, &AbstractDBusProperty::changed, this, &MprisPlayer::canRaiseChanged); QObject::connect(&this->pCanSetFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::canSetFullscreenChanged); QObject::connect(&this->pIdentity, &AbstractDBusProperty::changed, this, &MprisPlayer::identityChanged); + QObject::connect(&this->pDesktopEntry, &AbstractDBusProperty::changed, this, &MprisPlayer::desktopEntryChanged); QObject::connect(&this->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); @@ -155,6 +156,7 @@ bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } QString MprisPlayer::identity() const { return this->pIdentity.get(); } +QString MprisPlayer::desktopEntry() const { return this->pDesktopEntry.get(); } qlonglong MprisPlayer::positionMs() const { if (!this->positionSupported()) return 0; // unsupported diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 97181a59..ddbb87cb 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -68,6 +68,8 @@ class MprisPlayer: public QObject { Q_PROPERTY(bool canSetFullscreen READ canSetFullscreen NOTIFY canSetFullscreenChanged); /// The human readable name of the media player. Q_PROPERTY(QString identity READ identity NOTIFY identityChanged); + /// The name of the desktop entry for the media player, or an empty string if not provided. + Q_PROPERTY(QString desktopEntry READ desktopEntry NOTIFY desktopEntryChanged); /// The current position in the playing track, as seconds, with millisecond precision, /// or `0` if `positionSupported` is false. /// @@ -204,6 +206,7 @@ public: [[nodiscard]] bool canSetFullscreen() const; [[nodiscard]] QString identity() const; + [[nodiscard]] QString desktopEntry() const; [[nodiscard]] qlonglong positionMs() const; [[nodiscard]] qreal position() const; @@ -255,6 +258,7 @@ signals: void canRaiseChanged(); void canSetFullscreenChanged(); void identityChanged(); + void desktopEntryChanged(); void positionChanged(); void positionSupportedChanged(); void lengthChanged(); @@ -287,6 +291,7 @@ private: // clang-format off dbus::DBusPropertyGroup appProperties; dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; + dbus::DBusProperty pDesktopEntry {this->appProperties, "DesktopEntry", "", false}; dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; From 6326f60ce23c9c95494cbbe0511d42f15e12d736 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 02:38:26 -0700 Subject: [PATCH 04/31] service/mpris: re-query position on playback and metadata change --- src/services/mpris/player.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 27ba34c4..e17f3e82 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -267,7 +267,8 @@ void MprisPlayer::onMetadataChanged() { emit this->trackChanged(); } - this->onSeek(0); + // Some players don't seem to send position updats or seeks on track change. + this->pPosition.update(); } emit this->metadataChanged(); @@ -313,21 +314,25 @@ void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { void MprisPlayer::onPlaybackStatusChanged() { const auto& status = this->pPlaybackStatus.get(); + auto state = MprisPlaybackState::Stopped; if (status == "Playing") { - // update the timestamp - this->onSeek(this->positionMs() * 1000); - this->mPlaybackState = MprisPlaybackState::Playing; + state = MprisPlaybackState::Playing; } else if (status == "Paused") { this->pausedTime = QDateTime::currentDateTimeUtc(); - this->mPlaybackState = MprisPlaybackState::Paused; + state = MprisPlaybackState::Paused; } else if (status == "Stopped") { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; } else { - this->mPlaybackState = MprisPlaybackState::Stopped; + state = MprisPlaybackState::Stopped; qWarning() << "Received unexpected PlaybackStatus for" << this << status; } - emit this->playbackStateChanged(); + if (state != this->mPlaybackState) { + // make sure we're in sync at least on play/pause. Some players don't automatically send this. + this->pPosition.update(); + this->mPlaybackState = state; + emit this->playbackStateChanged(); + } } MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } From 5016dbf0d4c9e12f7e3d5f872b3063785d20b2bf Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 17:28:07 -0700 Subject: [PATCH 05/31] all: replace list properties with ObjectModels --- src/core/CMakeLists.txt | 1 + src/core/doc.hpp | 3 + src/core/model.cpp | 67 ++++++++++++++++++++++ src/core/model.hpp | 86 ++++++++++++++++++++++++++++ src/core/module.md | 1 + src/services/mpris/watcher.cpp | 29 ++-------- src/services/mpris/watcher.hpp | 15 ++--- src/services/pipewire/qml.cpp | 64 ++++----------------- src/services/pipewire/qml.hpp | 30 ++++------ src/services/status_notifier/qml.cpp | 42 ++++---------- src/services/status_notifier/qml.hpp | 15 ++--- 11 files changed, 201 insertions(+), 152 deletions(-) create mode 100644 src/core/model.cpp create mode 100644 src/core/model.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b40b807f..88c26241 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -26,6 +26,7 @@ qt_add_library(quickshell-core STATIC imageprovider.cpp transformwatcher.cpp boundcomponent.cpp + model.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/doc.hpp b/src/core/doc.hpp index b619b0a6..e1f2ee4c 100644 --- a/src/core/doc.hpp +++ b/src/core/doc.hpp @@ -10,5 +10,8 @@ #define QSDOC_ELEMENT #define QSDOC_NAMED_ELEMENT(name) +// change the cname used for this type +#define QSDOC_CNAME(name) + // overridden properties #define QSDOC_PROPERTY_OVERRIDE(...) diff --git a/src/core/model.cpp b/src/core/model.cpp new file mode 100644 index 00000000..74c7c284 --- /dev/null +++ b/src/core/model.cpp @@ -0,0 +1,67 @@ +#include "model.hpp" + +#include +#include +#include +#include +#include +#include +#include + +qint32 UntypedObjectModel::rowCount(const QModelIndex& parent) const { + if (parent != QModelIndex()) return 0; + return static_cast(this->valuesList.length()); +} + +QVariant UntypedObjectModel::data(const QModelIndex& index, qint32 role) const { + if (role != 0) return QVariant(); + return QVariant::fromValue(this->valuesList.at(index.row())); +} + +QHash UntypedObjectModel::roleNames() const { return {{0, "modelData"}}; } + +QQmlListProperty UntypedObjectModel::values() { + return QQmlListProperty( + this, + nullptr, + &UntypedObjectModel::valuesCount, + &UntypedObjectModel::valueAt + ); +} + +qsizetype UntypedObjectModel::valuesCount(QQmlListProperty* property) { + return static_cast(property->object)->valuesList.count(); // NOLINT +} + +QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizetype index) { + return static_cast(property->object)->valuesList.at(index); // NOLINT +} + +void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { + auto iindex = index == -1 ? this->valuesList.length() : index; + auto intIndex = static_cast(iindex); + + this->beginInsertRows(QModelIndex(), intIndex, intIndex); + this->valuesList.insert(iindex, object); + this->endInsertRows(); + emit this->valuesChanged(); +} + +void UntypedObjectModel::removeAt(qsizetype index) { + auto intIndex = static_cast(index); + + this->beginRemoveRows(QModelIndex(), intIndex, intIndex); + this->valuesList.removeAt(index); + this->endRemoveRows(); + emit this->valuesChanged(); +} + +bool UntypedObjectModel::removeObject(const QObject* object) { + auto index = this->valuesList.indexOf(object); + if (index == -1) return false; + + this->removeAt(index); + return true; +} + +qsizetype UntypedObjectModel::indexOf(QObject* object) { return this->valuesList.indexOf(object); } diff --git a/src/core/model.hpp b/src/core/model.hpp new file mode 100644 index 00000000..bcf5ab62 --- /dev/null +++ b/src/core/model.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "doc.hpp" + +///! View into a list of objets +/// Typed view into a list of objects. +/// +/// An ObjectModel works as a QML [Data Model], allowing efficient interaction with +/// components that act on models. It has a single role named `modelData`, to match the +/// behavior of lists. +/// The same information contained in the list model is available as a normal list +/// via the `values` property. +/// +/// #### Differences from a list +/// Unlike with a list, the following property binding will never be updated when `model[3]` changes. +/// ```qml +/// // will not update reactively +/// property var foo: model[3] +/// ``` +/// +/// You can work around this limitation using the `values` property of the model to view it as a list. +/// ```qml +/// // will update reactively +/// property var foo: model.values[3] +/// ``` +/// +/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models +class UntypedObjectModel: public QAbstractListModel { + QSDOC_CNAME(ObjectModel); + Q_OBJECT; + /// The content of the object model, as a QML list. + /// The values of this property will always be of the type of the model. + Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged); + QML_NAMED_ELEMENT(ObjectModel); + QML_UNCREATABLE("ObjectModels cannot be created directly."); + +public: + explicit UntypedObjectModel(QObject* parent): QAbstractListModel(parent) {} + + [[nodiscard]] qint32 rowCount(const QModelIndex& parent) const override; + [[nodiscard]] QVariant data(const QModelIndex& index, qint32 role) const override; + [[nodiscard]] QHash roleNames() const override; + + [[nodiscard]] QQmlListProperty values(); + void removeAt(qsizetype index); + + Q_INVOKABLE qsizetype indexOf(QObject* object); + +signals: + void valuesChanged(); + +protected: + void insertObject(QObject* object, qsizetype index = -1); + bool removeObject(const QObject* object); + + QVector valuesList; + +private: + static qsizetype valuesCount(QQmlListProperty* property); + static QObject* valueAt(QQmlListProperty* property, qsizetype index); +}; + +template +class ObjectModel: public UntypedObjectModel { +public: + explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} + + [[nodiscard]] const QVector& valueList() const { + return *reinterpret_cast*>(&this->valuesList); // NOLINT + } + + void insertObject(T* object, qsizetype index = -1) { + this->UntypedObjectModel::insertObject(object, index); + } + + void removeObject(const T* object) { this->UntypedObjectModel::removeObject(object); } +}; diff --git a/src/core/module.md b/src/core/module.md index 8eb9b638..dc1f204d 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -18,5 +18,6 @@ headers = [ "easingcurve.hpp", "transformwatcher.hpp", "boundcomponent.hpp", + "model.hpp", ] ----- diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 1e107660..8c67a4d6 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -4,14 +4,12 @@ #include #include #include -#include #include #include #include #include -#include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { @@ -74,34 +72,15 @@ void MprisWatcher::onServiceUnregistered(const QString& service) { void MprisWatcher::onPlayerReady() { auto* player = qobject_cast(this->sender()); - this->readyPlayers.push_back(player); - emit this->playersChanged(); + this->readyPlayers.insertObject(player); } void MprisWatcher::onPlayerDestroyed(QObject* object) { auto* player = static_cast(object); // NOLINT - - if (this->readyPlayers.removeOne(player)) { - emit this->playersChanged(); - } + this->readyPlayers.removeObject(player); } -QQmlListProperty MprisWatcher::players() { - return QQmlListProperty( - this, - nullptr, - &MprisWatcher::playersCount, - &MprisWatcher::playerAt - ); -} - -qsizetype MprisWatcher::playersCount(QQmlListProperty* property) { - return static_cast(property->object)->readyPlayers.count(); // NOLINT -} - -MprisPlayer* MprisWatcher::playerAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->readyPlayers.at(index); // NOLINT -} +ObjectModel* MprisWatcher::players() { return &this->readyPlayers; } void MprisWatcher::registerPlayer(const QString& address) { if (this->mPlayers.contains(address)) { diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index a1e4df7c..91275c7e 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -4,14 +4,13 @@ #include #include #include -#include #include #include #include #include #include -#include +#include "../../core/model.hpp" #include "player.hpp" namespace qs::service::mpris { @@ -22,15 +21,12 @@ class MprisWatcher: public QObject { QML_NAMED_ELEMENT(Mpris); QML_SINGLETON; /// All connected MPRIS players. - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); + Q_PROPERTY(ObjectModel* players READ players CONSTANT); public: explicit MprisWatcher(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty players(); - -signals: - void playersChanged(); + [[nodiscard]] ObjectModel* players(); private slots: void onServiceRegistered(const QString& service); @@ -39,15 +35,12 @@ private slots: void onPlayerDestroyed(QObject* object); private: - static qsizetype playersCount(QQmlListProperty* property); - static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); - void registerExisting(); void registerPlayer(const QString& address); QDBusServiceWatcher serviceWatcher; QHash mPlayers; - QList readyPlayers; + ObjectModel readyPlayers {this}; }; } // namespace qs::service::mpris diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index a6617d29..b40de687 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "connection.hpp" #include "link.hpp" #include "metadata.hpp" @@ -65,88 +66,43 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { // clang-format on } -QQmlListProperty Pipewire::nodes() { - return QQmlListProperty(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt); -} - -qsizetype Pipewire::nodesCount(QQmlListProperty* property) { - return static_cast(property->object)->mNodes.count(); // NOLINT -} - -PwNodeIface* Pipewire::nodeAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mNodes.at(index); // NOLINT -} +ObjectModel* Pipewire::nodes() { return &this->mNodes; } void Pipewire::onNodeAdded(PwNode* node) { auto* iface = PwNodeIface::instance(node); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved); - - this->mNodes.push_back(iface); - emit this->nodesChanged(); + this->mNodes.insertObject(iface); } void Pipewire::onNodeRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mNodes.removeOne(iface); - emit this->nodesChanged(); + this->mNodes.removeObject(iface); } -QQmlListProperty Pipewire::links() { - return QQmlListProperty(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt); -} - -qsizetype Pipewire::linksCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinks.count(); // NOLINT -} - -PwLinkIface* Pipewire::linkAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinks.at(index); // NOLINT -} +ObjectModel* Pipewire::links() { return &this->mLinks; } void Pipewire::onLinkAdded(PwLink* link) { auto* iface = PwLinkIface::instance(link); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved); - - this->mLinks.push_back(iface); - emit this->linksChanged(); + this->mLinks.insertObject(iface); } void Pipewire::onLinkRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinks.removeOne(iface); - emit this->linksChanged(); + this->mLinks.removeObject(iface); } -QQmlListProperty Pipewire::linkGroups() { - return QQmlListProperty( - this, - nullptr, - &Pipewire::linkGroupsCount, - &Pipewire::linkGroupAt - ); -} - -qsizetype Pipewire::linkGroupsCount(QQmlListProperty* property) { - return static_cast(property->object)->mLinkGroups.count(); // NOLINT -} - -PwLinkGroupIface* -Pipewire::linkGroupAt(QQmlListProperty* property, qsizetype index) { - return static_cast(property->object)->mLinkGroups.at(index); // NOLINT -} +ObjectModel* Pipewire::linkGroups() { return &this->mLinkGroups; } void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) { auto* iface = PwLinkGroupIface::instance(linkGroup); QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved); - - this->mLinkGroups.push_back(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.insertObject(iface); } void Pipewire::onLinkGroupRemoved(QObject* object) { auto* iface = static_cast(object); // NOLINT - this->mLinkGroups.removeOne(iface); - emit this->linkGroupsChanged(); + this->mLinkGroups.removeObject(iface); } PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 9b452727..8d456419 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -8,6 +8,7 @@ #include #include +#include "../../core/model.hpp" #include "link.hpp" #include "node.hpp" #include "registry.hpp" @@ -52,11 +53,11 @@ class Pipewire: public QObject { Q_OBJECT; // clang-format off /// All pipewire nodes. - Q_PROPERTY(QQmlListProperty nodes READ nodes NOTIFY nodesChanged); + Q_PROPERTY(ObjectModel* nodes READ nodes CONSTANT); /// All pipewire links. - Q_PROPERTY(QQmlListProperty links READ links NOTIFY linksChanged); + Q_PROPERTY(ObjectModel* links READ links CONSTANT); /// All pipewire link groups. - Q_PROPERTY(QQmlListProperty linkGroups READ linkGroups NOTIFY linkGroupsChanged); + Q_PROPERTY(ObjectModel* linkGroups READ linkGroups CONSTANT); /// The default audio sink or `null`. Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); /// The default audio source or `null`. @@ -68,16 +69,13 @@ class Pipewire: public QObject { public: explicit Pipewire(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty nodes(); - [[nodiscard]] QQmlListProperty links(); - [[nodiscard]] QQmlListProperty linkGroups(); + [[nodiscard]] ObjectModel* nodes(); + [[nodiscard]] ObjectModel* links(); + [[nodiscard]] ObjectModel* linkGroups(); [[nodiscard]] PwNodeIface* defaultAudioSink() const; [[nodiscard]] PwNodeIface* defaultAudioSource() const; signals: - void nodesChanged(); - void linksChanged(); - void linkGroupsChanged(); void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); @@ -90,17 +88,9 @@ private slots: void onLinkGroupRemoved(QObject* object); private: - static qsizetype nodesCount(QQmlListProperty* property); - static PwNodeIface* nodeAt(QQmlListProperty* property, qsizetype index); - static qsizetype linksCount(QQmlListProperty* property); - static PwLinkIface* linkAt(QQmlListProperty* property, qsizetype index); - static qsizetype linkGroupsCount(QQmlListProperty* property); - static PwLinkGroupIface* - linkGroupAt(QQmlListProperty* property, qsizetype index); - - QVector mNodes; - QVector mLinks; - QVector mLinkGroups; + ObjectModel mNodes {this}; + ObjectModel mLinks {this}; + ObjectModel mLinkGroups {this}; }; ///! Tracks all link connections to a given node. diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index cea5646e..f81a6381 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -9,6 +9,7 @@ #include #include +#include "../../core/model.hpp" #include "../../dbus/dbusmenu/dbusmenu.hpp" #include "../../dbus/properties.hpp" #include "host.hpp" @@ -106,46 +107,25 @@ SystemTray::SystemTray(QObject* parent): QObject(parent) { // clang-format on for (auto* item: host->items()) { - this->mItems.push_back(new SystemTrayItem(item, this)); + this->mItems.insertObject(new SystemTrayItem(item, this)); } } void SystemTray::onItemRegistered(StatusNotifierItem* item) { - this->mItems.push_back(new SystemTrayItem(item, this)); - emit this->itemsChanged(); + this->mItems.insertObject(new SystemTrayItem(item, this)); } void SystemTray::onItemUnregistered(StatusNotifierItem* item) { - SystemTrayItem* trayItem = nullptr; - - this->mItems.removeIf([item, &trayItem](SystemTrayItem* testItem) { - if (testItem->item == item) { - trayItem = testItem; - return true; - } else return false; - }); - - emit this->itemsChanged(); - - delete trayItem; + for (const auto* storedItem: this->mItems.valueList()) { + if (storedItem->item == item) { + this->mItems.removeObject(storedItem); + delete storedItem; + break; + } + } } -QQmlListProperty SystemTray::items() { - return QQmlListProperty( - this, - nullptr, - &SystemTray::itemsCount, - &SystemTray::itemAt - ); -} - -qsizetype SystemTray::itemsCount(QQmlListProperty* property) { - return reinterpret_cast(property->object)->mItems.count(); // NOLINT -} - -SystemTrayItem* SystemTray::itemAt(QQmlListProperty* property, qsizetype index) { - return reinterpret_cast(property->object)->mItems.at(index); // NOLINT -} +ObjectModel* SystemTray::items() { return &this->mItems; } SystemTrayItem* SystemTrayMenuWatcher::trayItem() const { return this->item; } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index 01f6bb05..e55509df 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -2,10 +2,9 @@ #include #include -#include #include -#include +#include "../../core/model.hpp" #include "item.hpp" namespace SystemTrayStatus { // NOLINT @@ -108,27 +107,21 @@ signals: class SystemTray: public QObject { Q_OBJECT; /// List of all system tray icons. - Q_PROPERTY(QQmlListProperty items READ items NOTIFY itemsChanged); + Q_PROPERTY(ObjectModel* items READ items CONSTANT); QML_ELEMENT; QML_SINGLETON; public: explicit SystemTray(QObject* parent = nullptr); - [[nodiscard]] QQmlListProperty items(); - -signals: - void itemsChanged(); + [[nodiscard]] ObjectModel* items(); private slots: void onItemRegistered(qs::service::sni::StatusNotifierItem* item); void onItemUnregistered(qs::service::sni::StatusNotifierItem* item); private: - static qsizetype itemsCount(QQmlListProperty* property); - static SystemTrayItem* itemAt(QQmlListProperty* property, qsizetype index); - - QList mItems; + ObjectModel mItems {this}; }; ///! Accessor for SystemTrayItem menus. From 06240ccf8027a38e72eaae2d598d262258688b27 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 18:15:49 -0700 Subject: [PATCH 06/31] service/mpris: improve compatibility with noncompliant players --- src/services/mpris/player.cpp | 30 +++++++++++++++++++++++------- src/services/mpris/player.hpp | 1 + 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index e17f3e82..b659badf 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -192,15 +192,15 @@ void MprisPlayer::setPosition(qreal position) { } auto target = static_cast(position * 1000) * 1000; - this->pPosition.set(target); if (!this->mTrackId.isEmpty()) { this->player->SetPosition(QDBusObjectPath(this->mTrackId), target); - return; } else { auto pos = this->positionMs() * 1000; this->player->Seek(target - pos); } + + this->pPosition.set(target); } void MprisPlayer::onPositionChanged() { @@ -247,6 +247,8 @@ void MprisPlayer::setVolume(qreal volume) { QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } void MprisPlayer::onMetadataChanged() { + emit this->metadataChanged(); + auto lengthVariant = this->pMetadata.get().value("mpris:length"); qlonglong length = -1; if (lengthVariant.isValid() && lengthVariant.canConvert()) { @@ -258,20 +260,34 @@ void MprisPlayer::onMetadataChanged() { emit this->lengthChanged(); } + auto trackChanged = false; + auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); if (trackidVariant.isValid() && trackidVariant.canConvert()) { auto trackId = trackidVariant.value(); if (trackId != this->mTrackId) { this->mTrackId = trackId; - emit this->trackChanged(); + trackChanged = true; } - - // Some players don't seem to send position updats or seeks on track change. - this->pPosition.update(); } - emit this->metadataChanged(); + // Helps to catch players without trackid. + auto urlVariant = this->pMetadata.get().value("xesam:url"); + if (urlVariant.isValid() && urlVariant.canConvert()) { + auto url = urlVariant.value(); + + if (url != this->mUrl) { + this->mUrl = url; + trackChanged = true; + } + } + + if (trackChanged) { + // Some players don't seem to send position updates or seeks on track change. + this->pPosition.update(); + emit this->trackChanged(); + } } MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index ddbb87cb..1172505a 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -326,6 +326,7 @@ private: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; QString mTrackId; + QString mUrl; }; } // namespace qs::service::mpris From 5a84e734426fc73e6a4bef993244ea422782e68b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 May 2024 19:12:21 -0700 Subject: [PATCH 07/31] core/objectmodel: add signals for changes to the list --- src/core/model.cpp | 11 +++++++++-- src/core/model.hpp | 8 ++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/core/model.cpp b/src/core/model.cpp index 74c7c284..64f7d765 100644 --- a/src/core/model.cpp +++ b/src/core/model.cpp @@ -39,21 +39,28 @@ QObject* UntypedObjectModel::valueAt(QQmlListProperty* property, qsizet void UntypedObjectModel::insertObject(QObject* object, qsizetype index) { auto iindex = index == -1 ? this->valuesList.length() : index; - auto intIndex = static_cast(iindex); + emit this->objectInsertedPre(object, index); + auto intIndex = static_cast(iindex); this->beginInsertRows(QModelIndex(), intIndex, intIndex); this->valuesList.insert(iindex, object); this->endInsertRows(); + emit this->valuesChanged(); + emit this->objectInsertedPost(object, index); } void UntypedObjectModel::removeAt(qsizetype index) { - auto intIndex = static_cast(index); + auto* object = this->valuesList.at(index); + emit this->objectRemovedPre(object, index); + auto intIndex = static_cast(index); this->beginRemoveRows(QModelIndex(), intIndex, intIndex); this->valuesList.removeAt(index); this->endRemoveRows(); + emit this->valuesChanged(); + emit this->objectRemovedPost(object, index); } bool UntypedObjectModel::removeObject(const QObject* object) { diff --git a/src/core/model.hpp b/src/core/model.hpp index bcf5ab62..10465bba 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -57,6 +57,14 @@ public: signals: void valuesChanged(); + /// Sent immediately before an object is inserted into the list. + void objectInsertedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is inserted into the list. + void objectInsertedPost(QObject* object, qsizetype index); + /// Sent immediately before an object is removed from the list. + void objectRemovedPre(QObject* object, qsizetype index); + /// Sent immediately after an object is removed from the list. + void objectRemovedPost(QObject* object, qsizetype index); protected: void insertObject(QObject* object, qsizetype index = -1); From 4e92d8299299c926f837fca98513599220e89e90 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 27 May 2024 22:51:49 -0700 Subject: [PATCH 08/31] core: add options to enable QML debugging --- src/core/main.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index 2cfd4d9c..24acdfc0 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,9 @@ int qs_main(int argc, char** argv) { auto desktopSettingsAware = true; QHash envOverrides; + int debugPort = -1; + bool waitForDebug = false; + { const auto app = QCoreApplication(argc, argv); QCoreApplication::setApplicationName("quickshell"); @@ -44,6 +48,8 @@ int qs_main(int argc, char** argv) { auto configOption = QCommandLineOption({"c", "config"}, "Name of a configuration in the manifest.", "name"); auto pathOption = QCommandLineOption({"p", "path"}, "Path to a configuration file.", "path"); auto workdirOption = QCommandLineOption({"d", "workdir"}, "Initial working directory.", "path"); + auto debugPortOption = QCommandLineOption("debugport", "Enable the QML debugger.", "port"); + auto debugWaitOption = QCommandLineOption("waitfordebug", "Wait for debugger connection before launching."); // clang-format on parser.addOption(currentOption); @@ -51,8 +57,30 @@ int qs_main(int argc, char** argv) { parser.addOption(configOption); parser.addOption(pathOption); parser.addOption(workdirOption); + parser.addOption(debugPortOption); + parser.addOption(debugWaitOption); parser.process(app); + auto debugPortStr = parser.value(debugPortOption); + if (!debugPortStr.isEmpty()) { + auto ok = false; + debugPort = debugPortStr.toInt(&ok); + + if (!ok) { + qCritical() << "Debug port must be a valid port number."; + return -1; + } + } + + if (parser.isSet(debugWaitOption)) { + if (debugPort == -1) { + qCritical() << "Cannot wait for debugger without a debug port set."; + return -1; + } + + waitForDebug = true; + } + { auto printCurrent = parser.isSet(currentOption); @@ -308,6 +336,13 @@ int qs_main(int argc, char** argv) { app = new QGuiApplication(argc, argv); } + if (debugPort != -1) { + QQmlDebuggingEnabler::enableDebugging(true); + auto wait = waitForDebug ? QQmlDebuggingEnabler::WaitForClient + : QQmlDebuggingEnabler::DoNotWaitForClient; + QQmlDebuggingEnabler::startTcpDebugServer(debugPort, wait); + } + if (!workingDirectory.isEmpty()) { QDir::setCurrent(workingDirectory); } From 7ad3671dd1d3884e45b53212b3cd90065c86ee8b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 28 May 2024 15:36:25 -0700 Subject: [PATCH 09/31] core/reloader: fix file watcher compatibility with vim --- src/core/generation.cpp | 30 +++++++++++++++++++++++++++++- src/core/generation.hpp | 3 +++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 77e4a9cb..a0b465f8 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include @@ -117,13 +119,21 @@ void EngineGeneration::setWatchingFiles(bool watching) { for (auto& file: this->scanner.scannedFiles) { this->watcher->addPath(file); + this->watcher->addPath(QFileInfo(file).dir().absolutePath()); } QObject::connect( this->watcher, &QFileSystemWatcher::fileChanged, this, - &EngineGeneration::filesChanged + &EngineGeneration::onFileChanged + ); + + QObject::connect( + this->watcher, + &QFileSystemWatcher::directoryChanged, + this, + &EngineGeneration::onDirectoryChanged ); } } else { @@ -134,6 +144,24 @@ void EngineGeneration::setWatchingFiles(bool watching) { } } +void EngineGeneration::onFileChanged(const QString& name) { + if (!this->watcher->files().contains(name)) { + this->deletedWatchedFiles.push_back(name); + } else { + emit this->filesChanged(); + } +} + +void EngineGeneration::onDirectoryChanged() { + // try to find any files that were just deleted from a replace operation + for (auto& file: this->deletedWatchedFiles) { + if (QFileInfo(file).exists()) { + emit this->filesChanged(); + break; + } + } +} + void EngineGeneration::registerIncubationController(QQmlIncubationController* controller) { auto* obj = dynamic_cast(controller); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 11ebf0be..3c8f3997 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -40,6 +40,7 @@ public: ShellRoot* root = nullptr; SingletonRegistry singletonRegistry; QFileSystemWatcher* watcher = nullptr; + QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; @@ -50,6 +51,8 @@ signals: void reloadFinished(); private slots: + void onFileChanged(const QString& name); + void onDirectoryChanged(); void incubationControllerDestroyed(); private: From 33fac6779815fa9d14b588186c700b0529c9e3ec Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 28 May 2024 20:22:01 -0700 Subject: [PATCH 10/31] core: use the simple animation driver Seems to provide much higher quality animations. --- src/core/main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/main.cpp b/src/core/main.cpp index 24acdfc0..220bde30 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -326,6 +326,13 @@ int qs_main(int argc, char** argv) { qputenv(var.toUtf8(), val.toUtf8()); } + // The simple animation driver seems to work far better than the default one + // when more than one window is in use, and even with a single window appears + // to improve animation quality. + if (!qEnvironmentVariableIsSet("QSG_USE_SIMPLE_ANIMATION_DRIVER")) { + qputenv("QSG_USE_SIMPLE_ANIMATION_DRIVER", "1"); + } + QGuiApplication::setDesktopSettingsAware(desktopSettingsAware); QGuiApplication* app = nullptr; From 0519acf1d6625a9a423fcc49ef90be1a55570091 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 29 May 2024 15:07:10 -0700 Subject: [PATCH 11/31] core: support `root:` and `root:/` paths for the config root This works everywhere urls are accepted and rewrites them from the config root as a qsintercept url. --- src/core/generation.cpp | 6 ++++-- src/core/generation.hpp | 4 +++- src/core/qsintercept.cpp | 17 ++++++++++++++++- src/core/qsintercept.hpp | 8 +++++++- src/core/rootwrapper.cpp | 5 +++-- src/core/scan.cpp | 10 +++++++++- src/core/scan.hpp | 6 ++++++ 7 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index a0b465f8..8dbad323 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -27,8 +27,10 @@ static QHash g_generations; // NOLINT -EngineGeneration::EngineGeneration(QmlScanner scanner) - : scanner(std::move(scanner)) +EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) + : rootPath(rootPath) + , scanner(std::move(scanner)) + , urlInterceptor(this->rootPath) , interceptNetFactory(this->scanner.qmldirIntercepts) , engine(new QQmlEngine()) { g_generations.insert(this->engine, this); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 3c8f3997..c077c1bf 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -19,7 +20,7 @@ class EngineGeneration: public QObject { Q_OBJECT; public: - explicit EngineGeneration(QmlScanner scanner); + explicit EngineGeneration(const QDir& rootPath, QmlScanner scanner); ~EngineGeneration() override; Q_DISABLE_COPY_MOVE(EngineGeneration); @@ -33,6 +34,7 @@ public: static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; + QDir rootPath; QmlScanner scanner; QsUrlInterceptor urlInterceptor; QsInterceptNetworkAccessManagerFactory interceptNetFactory; diff --git a/src/core/qsintercept.cpp b/src/core/qsintercept.cpp index 2eaf498e..ba46ab7b 100644 --- a/src/core/qsintercept.cpp +++ b/src/core/qsintercept.cpp @@ -16,7 +16,22 @@ Q_LOGGING_CATEGORY(logQsIntercept, "quickshell.interceptor", QtWarningMsg); -QUrl QsUrlInterceptor::intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) { +QUrl QsUrlInterceptor::intercept( + const QUrl& originalUrl, + QQmlAbstractUrlInterceptor::DataType type +) { + auto url = originalUrl; + + if (url.scheme() == "root") { + url.setScheme("qsintercept"); + + auto path = url.path(); + if (path.startsWith('/')) path = path.sliced(1); + url.setPath(this->configRoot.filePath(path)); + + qCDebug(logQsIntercept) << "Rewrote root intercept" << originalUrl << "to" << url; + } + // Some types such as Image take into account where they are loading from, and force // asynchronous loading over a network. qsintercept is considered to be over a network. if (type == QQmlAbstractUrlInterceptor::DataType::UrlString && url.scheme() == "qsintercept") { diff --git a/src/core/qsintercept.hpp b/src/core/qsintercept.hpp index d51b78e6..57923568 100644 --- a/src/core/qsintercept.hpp +++ b/src/core/qsintercept.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -13,7 +14,12 @@ Q_DECLARE_LOGGING_CATEGORY(logQsIntercept); class QsUrlInterceptor: public QQmlAbstractUrlInterceptor { public: - QUrl intercept(const QUrl& url, QQmlAbstractUrlInterceptor::DataType type) override; + explicit QsUrlInterceptor(const QDir& configRoot): configRoot(configRoot) {} + + QUrl intercept(const QUrl& originalUrl, QQmlAbstractUrlInterceptor::DataType type) override; + +private: + QDir configRoot; }; class QsInterceptDataReply: public QNetworkReply { diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ed2ef4b7..ea2adf18 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -42,10 +42,11 @@ RootWrapper::~RootWrapper() { } void RootWrapper::reloadGraph(bool hard) { - auto scanner = QmlScanner(); + auto rootPath = QFileInfo(this->rootPath).dir(); + auto scanner = QmlScanner(rootPath); scanner.scanQmlFile(this->rootPath); - auto* generation = new EngineGeneration(std::move(scanner)); + auto* generation = new EngineGeneration(rootPath, std::move(scanner)); generation->wrapper = this; // todo: move into EngineGeneration diff --git a/src/core/scan.cpp b/src/core/scan.cpp index f5f078aa..59ec05b6 100644 --- a/src/core/scan.cpp +++ b/src/core/scan.cpp @@ -103,7 +103,15 @@ bool QmlScanner::scanQmlFile(const QString& path) { this->scanDir(currentdir.path()); for (auto& import: imports) { - auto ipath = currentdir.filePath(import); + QString ipath; + if (import.startsWith("root:")) { + auto path = import.sliced(5); + if (path.startsWith('/')) path = path.sliced(1); + ipath = this->rootPath.filePath(path); + } else { + ipath = currentdir.filePath(import); + } + auto cpath = QFileInfo(ipath).canonicalFilePath(); if (cpath.isEmpty()) { diff --git a/src/core/scan.hpp b/src/core/scan.hpp index 32a6166d..e3071a88 100644 --- a/src/core/scan.hpp +++ b/src/core/scan.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -10,6 +11,8 @@ Q_DECLARE_LOGGING_CATEGORY(logQmlScanner); // expects canonical paths class QmlScanner { public: + QmlScanner(const QDir& rootPath): rootPath(rootPath) {} + void scanDir(const QString& path); // returns if the file has a singleton bool scanQmlFile(const QString& path); @@ -17,4 +20,7 @@ public: QVector scannedDirs; QVector scannedFiles; QHash qmldirIntercepts; + +private: + QDir rootPath; }; From 569c40494d8792b161c7141f674b14aeafd99fb6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 29 May 2024 19:29:57 -0700 Subject: [PATCH 12/31] all: import module dependencies via qmldir Improves compatibility with qml tooling. --- src/wayland/CMakeLists.txt | 11 +++++++++- src/wayland/hyprland/CMakeLists.txt | 13 ++++++++++-- .../hyprland/focus_grab/CMakeLists.txt | 9 +-------- src/wayland/hyprland/focus_grab/init.cpp | 20 ------------------- .../hyprland/global_shortcuts/CMakeLists.txt | 9 +-------- .../hyprland/global_shortcuts/init.cpp | 20 ------------------- src/wayland/init.cpp | 7 ------- 7 files changed, 23 insertions(+), 66 deletions(-) delete mode 100644 src/wayland/hyprland/focus_grab/init.cpp delete mode 100644 src/wayland/hyprland/global_shortcuts/init.cpp diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 48140a91..f20bc11d 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -51,16 +51,19 @@ endfunction() # ----- qt_add_library(quickshell-wayland STATIC) -qt_add_qml_module(quickshell-wayland URI Quickshell.Wayland VERSION 0.1) # required to make sure the constructor is linked add_library(quickshell-wayland-init OBJECT init.cpp) +set(WAYLAND_MODULES) + if (WAYLAND_WLR_LAYERSHELL) target_sources(quickshell-wayland PRIVATE wlr_layershell.cpp) add_subdirectory(wlr_layershell) target_compile_definitions(quickshell-wayland PRIVATE QS_WAYLAND_WLR_LAYERSHELL) target_compile_definitions(quickshell-wayland-init PRIVATE QS_WAYLAND_WLR_LAYERSHELL) + + list(APPEND WAYLAND_MODULES Quickshell.Wayland._WlrLayerShell) endif() if (WAYLAND_SESSION_LOCK) @@ -75,6 +78,12 @@ endif() target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS}) target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-wayland + URI Quickshell.Wayland + VERSION 0.1 + IMPORTS ${WAYLAND_MODULES} +) + qs_pch(quickshell-wayland) qs_pch(quickshell-waylandplugin) qs_pch(quickshell-wayland-init) diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index 06121a7e..be6bf49c 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -1,15 +1,24 @@ qt_add_library(quickshell-hyprland STATIC) -qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1) + +target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) + +set(HYPRLAND_MODULES) if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) endif() if (HYPRLAND_GLOBAL_SHORTCUTS) add_subdirectory(global_shortcuts) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._GlobalShortcuts) endif() -target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) +qt_add_qml_module(quickshell-hyprland + URI Quickshell.Hyprland + VERSION 0.1 + IMPORTS ${HYPRLAND_MODULES} +) qs_pch(quickshell-hyprland) qs_pch(quickshell-hyprlandplugin) diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 587ae939..1e37c9fe 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-focus-grab VERSION 0.1 ) -add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" ) target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-focus-grab) qs_pch(quickshell-hyprland-focus-grabplugin) -qs_pch(quickshell-hyprland-focus-grab-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-focus-grabplugin - quickshell-hyprland-focus-grab-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin) diff --git a/src/wayland/hyprland/focus_grab/init.cpp b/src/wayland/hyprland/focus_grab/init.cpp deleted file mode 100644 index 784c7f26..00000000 --- a/src/wayland/hyprland/focus_grab/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._FocusGrab", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 804c0a3c..2ccfb74d 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -9,21 +9,14 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts VERSION 0.1 ) -add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp) - wl_proto(quickshell-hyprland-global-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" ) target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client) -target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS}) qs_pch(quickshell-hyprland-global-shortcuts) qs_pch(quickshell-hyprland-global-shortcutsplugin) -qs_pch(quickshell-hyprland-global-shortcuts-init) -target_link_libraries(quickshell PRIVATE - quickshell-hyprland-global-shortcutsplugin - quickshell-hyprland-global-shortcuts-init -) +target_link_libraries(quickshell PRIVATE quickshell-hyprland-global-shortcutsplugin) diff --git a/src/wayland/hyprland/global_shortcuts/init.cpp b/src/wayland/hyprland/global_shortcuts/init.cpp deleted file mode 100644 index 12fed07f..00000000 --- a/src/wayland/hyprland/global_shortcuts/init.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include - -#include "../../../core/plugin.hpp" - -namespace { - -class HyprlandFocusGrabPlugin: public QuickshellPlugin { - void registerTypes() override { - qmlRegisterModuleImport( - "Quickshell.Hyprland", - QQmlModuleImportModuleAny, - "Quickshell.Hyprland._GlobalShortcuts", - QQmlModuleImportLatest - ); - } -}; - -QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin); - -} // namespace diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 194bad4c..95adb248 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -34,13 +34,6 @@ class WaylandPlugin: public QuickshellPlugin { // will not be registered. This can be worked around with a module import which makes // the QML_ELMENT module import the old register-type style module. - qmlRegisterModuleImport( - "Quickshell.Wayland", - QQmlModuleImportModuleAny, - "Quickshell.Wayland._WlrLayerShell", - QQmlModuleImportLatest - ); - qmlRegisterModuleImport( "Quickshell", QQmlModuleImportModuleAny, From 7feae55ebe276d1fb0f68fa711758dc8cb0a6393 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 30 May 2024 02:39:37 -0700 Subject: [PATCH 13/31] core/reloader: add reload signals for visual notifications --- src/core/generation.cpp | 6 +++++- src/core/generation.hpp | 4 ++++ src/core/qmlglobal.cpp | 11 +++++++++++ src/core/qmlglobal.hpp | 10 ++++++++-- src/core/rootwrapper.cpp | 23 ++++++++++++++++++++--- 5 files changed, 48 insertions(+), 6 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 8dbad323..1021566b 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -265,12 +265,16 @@ void EngineGeneration::assignIncubationController() { this->engine->setIncubationController(controller); } +EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { + return g_generations.value(engine); +} + EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); if (context != nullptr) { - if (auto* generation = g_generations.value(context->engine())) { + if (auto* generation = EngineGeneration::findEngineGeneration(context->engine())) { return generation; } } diff --git a/src/core/generation.hpp b/src/core/generation.hpp index c077c1bf..f757113e 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -15,6 +16,7 @@ #include "singleton.hpp" class RootWrapper; +class QuickshellGlobal; class EngineGeneration: public QObject { Q_OBJECT; @@ -31,6 +33,7 @@ public: void registerIncubationController(QQmlIncubationController* controller); void deregisterIncubationController(QQmlIncubationController* controller); + static EngineGeneration* findEngineGeneration(QQmlEngine* engine); static EngineGeneration* findObjectGeneration(QObject* object); RootWrapper* wrapper = nullptr; @@ -45,6 +48,7 @@ public: QVector deletedWatchedFiles; DelayedQmlIncubationController delayedIncubationController; bool reloadComplete = false; + QuickshellGlobal* qsgInstance = nullptr; void destroy(); diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 70d7b416..05197f26 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -187,3 +187,14 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT return qEnvironmentVariable(vstr.data()); } + +QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { + auto* qsg = new QuickshellGlobal(); + auto* generation = EngineGeneration::findEngineGeneration(engine); + + if (generation->qsgInstance == nullptr) { + generation->qsgInstance = qsg; + } + + return qsg; +} diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 83ef68d4..8de55fc2 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -110,8 +110,6 @@ class QuickshellGlobal: public QObject { public: [[nodiscard]] qint32 processId() const; - QuickshellGlobal(QObject* parent = nullptr); - QQmlListProperty screens(); /// Reload the shell from the [ShellRoot]. @@ -133,17 +131,25 @@ public: [[nodiscard]] bool watchFiles() const; void setWatchFiles(bool watchFiles); + static QuickshellGlobal* create(QQmlEngine* engine, QJSEngine* /*unused*/); + signals: /// Sent when the last window is closed. /// /// To make the application exit when the last window is closed run `Qt.quit()`. void lastWindowClosed(); + /// The reload sequence has completed successfully. + void reloadCompleted(); + /// The reload sequence has failed. + void reloadFailed(QString errorString); void screensChanged(); void workingDirectoryChanged(); void watchFilesChanged(); private: + QuickshellGlobal(QObject* parent = nullptr); + static qsizetype screensCount(QQmlListProperty* prop); static QuickshellScreenInfo* screenAt(QQmlListProperty* prop, qsizetype i); }; diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index ea2adf18..35060bec 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include "generation.hpp" @@ -64,17 +65,28 @@ void RootWrapper::reloadGraph(bool hard) { auto* obj = component.beginCreate(generation->engine->rootContext()); if (obj == nullptr) { - qWarning() << component.errorString().toStdString().c_str(); - qWarning() << "failed to create root component"; + QString error = "failed to create root component\n" + component.errorString(); + qWarning().noquote() << error; delete generation; + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } auto* newRoot = qobject_cast(obj); if (newRoot == nullptr) { - qWarning() << "root component was not a Quickshell.ShellRoot"; + QString error = "root component was not a Quickshell.ShellRoot"; + qWarning().noquote() << error; delete obj; delete generation; + + if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadFailed(error); + } + return; } @@ -82,6 +94,7 @@ void RootWrapper::reloadGraph(bool hard) { component.completeCreate(); + auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); if (hard) delete this->generation; this->generation = generation; @@ -96,6 +109,10 @@ void RootWrapper::reloadGraph(bool hard) { ); this->onWatchFilesChanged(); + + if (isReload && this->generation->qsgInstance != nullptr) { + emit this->generation->qsgInstance->reloadCompleted(); + } } void RootWrapper::onWatchFilesChanged() { From 6c9526761cd5d17b732dc00b7bbcdb7b0f5e3259 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:24:58 -0700 Subject: [PATCH 14/31] wayland: fix UAF in layershell surface destructor --- src/wayland/wlr_layershell/surface.cpp | 11 +++++++++-- src/wayland/wlr_layershell/window.cpp | 6 ++++++ src/wayland/wlr_layershell/window.hpp | 3 +++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index ac80ebd0..5c369f2b 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -18,6 +17,10 @@ #include "shell_integration.hpp" #include "window.hpp" +#if QT_VERSION < QT_VERSION_CHECK(6, 7, 0) +#include +#endif + // clang-format off [[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept; [[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept; @@ -72,7 +75,10 @@ QSWaylandLayerSurface::QSWaylandLayerSurface( } QSWaylandLayerSurface::~QSWaylandLayerSurface() { - this->ext->surface = nullptr; + if (this->ext != nullptr) { + this->ext->surface = nullptr; + } + this->destroy(); } @@ -106,6 +112,7 @@ void QSWaylandLayerSurface::applyConfigure() { } void QSWaylandLayerSurface::setWindowGeometry(const QRect& geometry) { + if (this->ext == nullptr) return; auto size = constrainedSize(this->ext->mAnchors, geometry.size()); this->set_size(size.width(), size.height()); } diff --git a/src/wayland/wlr_layershell/window.cpp b/src/wayland/wlr_layershell/window.cpp index 035bae1d..a671d59e 100644 --- a/src/wayland/wlr_layershell/window.cpp +++ b/src/wayland/wlr_layershell/window.cpp @@ -13,6 +13,12 @@ #include "shell_integration.hpp" #include "surface.hpp" +LayershellWindowExtension::~LayershellWindowExtension() { + if (this->surface != nullptr) { + this->surface->ext = nullptr; + } +} + LayershellWindowExtension* LayershellWindowExtension::get(QWindow* window) { auto v = window->property("layershell_ext"); diff --git a/src/wayland/wlr_layershell/window.hpp b/src/wayland/wlr_layershell/window.hpp index 163f3aa7..37092a6a 100644 --- a/src/wayland/wlr_layershell/window.hpp +++ b/src/wayland/wlr_layershell/window.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -56,6 +57,8 @@ class LayershellWindowExtension: public QObject { public: LayershellWindowExtension(QObject* parent = nullptr): QObject(parent) {} + ~LayershellWindowExtension() override; + Q_DISABLE_COPY_MOVE(LayershellWindowExtension); // returns the layershell extension if attached, otherwise nullptr static LayershellWindowExtension* get(QWindow* window); From 84bb4098adb8cf9a559772ad42a05ed0cdf8e7b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:26:34 -0700 Subject: [PATCH 15/31] core/reloader: fix incorrect generation teardown on hard reload --- src/core/rootwrapper.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 35060bec..3c69615f 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -65,7 +65,7 @@ void RootWrapper::reloadGraph(bool hard) { auto* obj = component.beginCreate(generation->engine->rootContext()); if (obj == nullptr) { - QString error = "failed to create root component\n" + component.errorString(); + const QString error = "failed to create root component\n" + component.errorString(); qWarning().noquote() << error; delete generation; @@ -78,7 +78,7 @@ void RootWrapper::reloadGraph(bool hard) { auto* newRoot = qobject_cast(obj); if (newRoot == nullptr) { - QString error = "root component was not a Quickshell.ShellRoot"; + const QString error = "root component was not a Quickshell.ShellRoot"; qWarning().noquote() << error; delete obj; delete generation; @@ -96,7 +96,11 @@ void RootWrapper::reloadGraph(bool hard) { auto isReload = this->generation != nullptr; generation->onReload(hard ? nullptr : this->generation); - if (hard) delete this->generation; + + if (hard && this->generation != nullptr) { + this->generation->destroy(); + } + this->generation = generation; qInfo() << "Configuration Loaded"; From d56c07ceb3389229d0bb52c71c80ae1866fb7656 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 00:27:18 -0700 Subject: [PATCH 16/31] core/reloader: simplify generation teardown The extra complexity previously masked the use after free in 6c95267. --- src/core/generation.cpp | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 1021566b..71530430 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include "iconimageprovider.hpp" @@ -47,32 +46,30 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) } EngineGeneration::~EngineGeneration() { - g_generations.remove(this->engine); - delete this->engine; + if (this->engine != nullptr) { + qFatal() << this << "destroyed without calling destroy()"; + } } void EngineGeneration::destroy() { // Multiple generations can detect a reload at the same time. - delete this->watcher; + QObject::disconnect(this->watcher, nullptr, this, nullptr); + this->watcher->deleteLater(); this->watcher = nullptr; - // Yes all of this is actually necessary. if (this->engine != nullptr && this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { - // The timer seems to fix *one* of the possible qml item destructor crashes. - QTimer::singleShot(0, [this]() { - // Garbage is not collected during engine destruction. - this->engine->collectGarbage(); + // prevent further js execution between garbage collection and engine destruction. + this->engine->setInterrupted(true); - QObject::connect(this->engine, &QObject::destroyed, this, [this]() { delete this; }); + g_generations.remove(this->engine); - // Even after all of that there's still multiple failing assertions and segfaults. - // Pray you don't hit one. - // Note: it appeats *some* of the crashes are related to values owned by the generation. - // Test by commenting the connect() above. - this->engine->deleteLater(); - this->engine = nullptr; - }); + // Garbage is not collected during engine destruction. + this->engine->collectGarbage(); + + delete this->engine; + this->engine = nullptr; + delete this; }); this->root->deleteLater(); From a8506edbb931867d881d5b854f7d15cb74e9086f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 01:28:35 -0700 Subject: [PATCH 17/31] build: link jemalloc by default to reduce heap fragmentation The QML engine and the quickshell reloader both cause large amounts of heap fragmentation that stacks up over time, leading to a perceived memory leak. Jemalloc is able to handle the fragmentation much better, leading to lower user facing memory usage. --- CMakeLists.txt | 9 +++++++++ README.md | 5 ++++- default.nix | 17 +++++++++++------ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d17758b..e790ec0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,7 @@ option(BUILD_TESTING "Build tests" OFF) option(ASAN "Enable ASAN" OFF) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) +option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) @@ -23,6 +24,7 @@ option(SERVICE_PIPEWIRE "PipeWire service" ON) option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") +message(STATUS " Jemalloc: ${USE_JEMALLOC}") message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") @@ -137,3 +139,10 @@ if (NVIDIA_COMPAT) endif() add_subdirectory(src) + +if (USE_JEMALLOC) + find_package(PkgConfig REQUIRED) + # IMPORTED_TARGET not working for some reason + pkg_check_modules(JEMALLOC REQUIRED jemalloc) + target_link_libraries(quickshell PRIVATE ${JEMALLOC_LIBRARIES}) +endif() diff --git a/README.md b/README.md index c17af3a8..173ddd17 100644 --- a/README.md +++ b/README.md @@ -63,10 +63,13 @@ To build quickshell at all, you will need the following packages (names may vary - just - cmake - ninja +- pkg-config - Qt6 [ QtBase, QtDeclarative ] +Jemalloc is recommended, in which case you will need: +- jemalloc + To build with wayland support you will additionally need: -- pkg-config - wayland - wayland-scanner (may be part of wayland on some distros) - wayland-protocols diff --git a/default.nix b/default.nix index 0985d843..048e181e 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + jemalloc, wayland, wayland-protocols, xorg, @@ -29,7 +30,8 @@ enableX11 ? true, enablePipewire ? true, nvidiaCompat ? false, - svgSupport ? true, # you almost always want this + withQtSvg ? true, # svg support + withJemalloc ? true, # masks heap fragmentation }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -39,8 +41,8 @@ cmake ninja qt6.wrapQtAppsHook - ] ++ (lib.optionals enableWayland [ pkg-config + ] ++ (lib.optionals enableWayland [ wayland-protocols wayland-scanner ]); @@ -49,10 +51,11 @@ qt6.qtbase qt6.qtdeclarative ] + ++ (lib.optional withJemalloc jemalloc) + ++ (lib.optional withQtSvg qt6.qtsvg) ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optionals enableX11 [ xorg.libxcb ]) - ++ (lib.optionals svgSupport [ qt6.qtsvg ]) - ++ (lib.optionals enablePipewire [ pipewire ]); + ++ (lib.optional enableX11 xorg.libxcb) + ++ (lib.optional enablePipewire pipewire); QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; @@ -67,7 +70,9 @@ cmakeFlags = [ "-DGIT_REVISION=${gitRev}" - ] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" + ] + ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" + ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; From 238ca8cf0bf02453cb47a93e5310934ba395f23b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 31 May 2024 04:03:00 -0700 Subject: [PATCH 18/31] core/reloader: fix crashing on failed reload --- src/core/generation.cpp | 17 ++++++++++++----- src/core/rootwrapper.cpp | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 71530430..e43db6ee 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -52,12 +52,14 @@ EngineGeneration::~EngineGeneration() { } void EngineGeneration::destroy() { - // Multiple generations can detect a reload at the same time. - QObject::disconnect(this->watcher, nullptr, this, nullptr); - this->watcher->deleteLater(); - this->watcher = nullptr; + if (this->watcher != nullptr) { + // Multiple generations can detect a reload at the same time. + QObject::disconnect(this->watcher, nullptr, this, nullptr); + this->watcher->deleteLater(); + this->watcher = nullptr; + } - if (this->engine != nullptr && this->root != nullptr) { + if (this->root != nullptr) { QObject::connect(this->root, &QObject::destroyed, this, [this]() { // prevent further js execution between garbage collection and engine destruction. this->engine->setInterrupted(true); @@ -74,6 +76,11 @@ void EngineGeneration::destroy() { this->root->deleteLater(); this->root = nullptr; + } else { + // the engine has never been used, no need to clean up + delete this->engine; + this->engine = nullptr; + delete this; } } diff --git a/src/core/rootwrapper.cpp b/src/core/rootwrapper.cpp index 3c69615f..1afb30cf 100644 --- a/src/core/rootwrapper.cpp +++ b/src/core/rootwrapper.cpp @@ -67,7 +67,7 @@ void RootWrapper::reloadGraph(bool hard) { if (obj == nullptr) { const QString error = "failed to create root component\n" + component.errorString(); qWarning().noquote() << error; - delete generation; + generation->destroy(); if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { emit this->generation->qsgInstance->reloadFailed(error); @@ -81,7 +81,7 @@ void RootWrapper::reloadGraph(bool hard) { const QString error = "root component was not a Quickshell.ShellRoot"; qWarning().noquote() << error; delete obj; - delete generation; + generation->destroy(); if (this->generation != nullptr && this->generation->qsgInstance != nullptr) { emit this->generation->qsgInstance->reloadFailed(error); From bd504daf56b6fe6ccbc26c40ed1b155dd850797e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 14:37:48 -0700 Subject: [PATCH 19/31] docs: add build, packaging and development instructions --- BUILD.md | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 69 ++++++++++++++++++++++ README.md | 94 +++++++----------------------- 3 files changed, 241 insertions(+), 73 deletions(-) create mode 100644 BUILD.md create mode 100644 CONTRIBUTING.md diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 00000000..d7844c6a --- /dev/null +++ b/BUILD.md @@ -0,0 +1,151 @@ +# Build instructions +Instructions for building from source and distro packagers. We highly recommend +distro packagers read through this page fully. + +## Dependencies +Quickshell has a set of base dependencies you will always need, names vary by distro: + +- `cmake` +- `qt6base` +- `qt6declarative` +- `pkg-config` + +At least Qt 6.6 is required. + +All features are enabled by default and some have their own dependencies. + +##### Additional note to packagers: +If your package manager supports enabling some features but not others, +we recommend not exposing the subfeatures and just the main ones that introduce +new dependencies: `wayland`, `x11`, `pipewire`, `hyprland` + +### Jemalloc +We recommend leaving Jemalloc enabled as it will mask memory fragmentation caused +by the QML engine, which results in much lower memory usage. Without this you +will get a perceived memory leak. + +To disable: `-DUSE_JEMALLOC=OFF` + +Dependencies: `jemalloc` + +### Unix Sockets +This feature allows interaction with unix sockets and creating socket servers +which is useful for IPC and has no additional dependencies. + +WARNING: Disabling unix sockets will NOT make it safe to run arbitrary code using quickshell. +There are many vectors which mallicious code can use to escape into your system. + +To disable: `-DSOCKETS=OFF` + +### Wayland +This feature enables wayland support. Subfeatures exist for each particular wayland integration. + +WARNING: Wayland integration relies on featurs that are not part of the public Qt API and which +may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring +that the current Qt version is supported WILL result in quickshell failing to build or misbehaving +at runtime. + +Currently supported Qt versions: `6.6`, `6.7`. + +To disable: `-DWAYLAND=OFF` + +Dependencies: + - `qt6wayland` + - `wayland` (libwayland-client) + - `wayland-scanner` (may be part of your distro's wayland package) + - `wayland-protocols` + +#### Wlroots Layershell +Enables wlroots layershell integration through the [wlr-layer-shell-unstable-v1] protocol, +enabling use cases such as bars overlays and backgrounds. +This feature has no extra dependencies. + +To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` + +[wlr-layer-shell-unstable-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 + +#### Session Lock +Enables session lock support through the [ext-session-lock-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + +### X11 +This feature enables x11 support. Currently this implements panel windows for X11 similarly +to the wlroots layershell above. + +To disable: `-DX11=OFF` + +Dependencies: `libxcb` + +### Pipewire +This features enables viewing and management of pipewire nodes. + +To disable: `-DSERVICE_PIPEWIRE=OFF` + +Dependencies: `libpipewire` + +### StatusNotifier / System Tray +This feature enables system tray support using the status notifier dbus protocol. + +To disable: `-DSERVICE_STATUS_NOTIFIER=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### MPRIS +This feature enables access to MPRIS compatible media players using its dbus protocol. + +To disable: `-DSERVICE_MPRIS=OFF` + +Dependencies: `qt6dbus` (usually part of qt6base) + +### Hyprland +This feature enables hyprland specific integrations. It requires wayland support +but has no extra dependencies. + +To disable: `-DHYPRLAND=OFF` + +#### Hyprland Global Shortcuts +Enables creation of global shortcuts under hyprland through the [hyprland-global-shortcuts-v1] +protocol. Generally a much nicer alternative to using unix sockets to implement the same thing. +This feature has no extra dependencies. + +To disable: `-DHYPRLAND_GLOBAL_SHORTCUTS=OFF` + +[hyprland-global-shortcuts-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml + +#### Hyprland Focus Grab +Enables windows to grab focus similarly to a context menu undr hyprland through the +[hyprland-focus-grab-v1] protocol. This feature has no extra dependencies. + +To disable: `-DHYPRLAND_FOCUS_GRAB=OFF` + +[hyprland-focus-grab-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-focus-grab-v1.xml + +## Building +*For developers and prospective contributors: See [CONTRIBUTING.md](CONTRIBUTING.md).* + +We highly recommend using `ninja` to run the build, but you can use makefiles if you must. + +#### Configuring the build +```sh +$ cmake -GNinja -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo [additional disable flags from above here] +``` + +Note that features you do not supply dependencies for MUST be disabled with their associated flags +or quickshell will fail to build. + +Additionally, note that clang builds much faster than gcc if you care. + +You may disable debug information but it's only a couple megabytes and is extremely helpful +for helping us fix problems when they do arise. + +#### Building +```sh +$ cmake --build build +``` + +#### Installing +```sh +$ cmake --install build +``` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a5fd4836 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing / Development +Instructions for development setup and upstreaming patches. + +If you just want to build or package quickshell see [BUILD.md](BUILD.md). + +## Development +Install the dependencies listed in [BUILD.md](BUILD.md). +You probably want all of them even if you don't use all of them +to ensure tests work correctly and avoid passing a bunch of configure +flags when you need to wipe the build directory. + +Quickshell also uses `just` for common development command aliases. + +The dependencies are also available as a nix shell or nix flake which we recommend +using with nix-direnv. + +Common aliases: +- `just configure [ [extra cmake args]]` (note that you must specify debug/release to specify extra args) +- `just build` - runs the build, configuring if not configured already. +- `just run [args]` - runs quickshell with the given arguments +- `just clean` - clean up build artifacts. `just clean build` is somewhat common. + +### Formatting +All contributions should be formatted similarly to what already exists. +Group related functionality together. + +Run the formatter using `just fmt`. +If the results look stupid, fix the clang-format file if possible, +or disable clang-format in the affected area +using `// clang-format off` and `// clang-format on`. + +### Linter +All contributions should pass the linter. + +Note that running the linter requires disabling precompiled +headers and including the test codepaths: +```sh +$ just configure debug -DNO_PCH=ON -DBUILD_TESTING=ON +$ just lint +``` + +If the linter is complaining about something that you think it should not, +please disable the lint in your MR and explain your reasoning. + +### Tests +If you feel like the feature you are working on is very complex or likely to break, +please write some tests. We will ask you to directly if you send in an MR for an +overly complex or breakable feature. + +At least all tests that passed before your changes should still be passing +by the time your contribution is ready. + +You can run the tests using `just test` but you must enable them first +using `-DBUILD_TESTING=ON`. + +### Documentation +Most of quickshell's documentation is automatically generated from the source code. +You should annotate `Q_PROPERTY`s and `Q_INVOKABLE`s with doc comments. Note that the parser +cannot handle random line breaks and will usually require you to disable clang-format if the +lines are too long. + +Before submitting an MR, if adding new features please make sure the documentation is generated +reasonably using the `quickshell-docs` repo. + +Doc comments take the form `///` or `///!` (summary) and work with markdown. +Look at existing code for how it works. + +Quickshell modules additionally have a `module.md` file which contains a summary, description, +and list of headers to scan for documentation. diff --git a/README.md b/README.md index 173ddd17..012a33a6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Hosted on: [outfoxxed's gitea], [github] Documentation available at [quickshell.outfoxxed.me](https://quickshell.outfoxxed.me) or can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quickshell-docs) repo. -Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) +Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. Both the documentation and examples are included as submodules with revisions that work with the current @@ -48,84 +48,32 @@ This repo has a nix flake you can use to install the package directly: Quickshell's binary is available at `quickshell.packages..default` to be added to lists such as `environment.systemPackages` or `home.packages`. -`quickshell.packages..nvidia` is also available for nvidia users which fixes some -common crashes. +The package contains several features detailed in [BUILD.md](BUILD.md) which can be enabled +or disabled with overrides: + +```nix +quickshell.packages..default.override { + enableWayland = true; + enableX11 = true; + enablePipewire = true; + withQtSvg = true; + withJemalloc = true; +} +``` Note: by default this package is built with clang as it is significantly faster. -## Manual +## Arch (AUR) +Quickshell has a third party [AUR package] available under the same name. +As is usual with the AUR it is not maintained by us and should be looked over before use. -If not using nix, you'll have to build from source. +[AUR package]: https://aur.archlinux.org/packages/quickshell -### Dependencies -To build quickshell at all, you will need the following packages (names may vary by distro) +## Anything else +See [BUILD.md](BUILD.md) for instructions on building and packaging quickshell. -- just -- cmake -- ninja -- pkg-config -- Qt6 [ QtBase, QtDeclarative ] - -Jemalloc is recommended, in which case you will need: -- jemalloc - -To build with wayland support you will additionally need: -- wayland -- wayland-scanner (may be part of wayland on some distros) -- wayland-protocols -- Qt6 [ QtWayland ] - -To build with x11 support you will additionally need: -- libxcb - -To build with pipewire support you will additionally need: -- libpipewire - -### Building - -To make a release build of quickshell run: -```sh -$ just release -``` - -If running an nvidia GPU, instead run: -```sh -$ just configure release -DNVIDIA_COMPAT=ON -$ just build -``` - -(These commands are just aliases for cmake commands you can run directly, -see the Justfile for more information.) - -If you have all the dependencies installed and they are in expected -locations this will build correctly. - -To install to /usr/local/bin run as root (usually `sudo`) in the same folder: -``` -$ just install -``` - -### Building (Nix) - -You can build directly using the provided nix flake or nix package. -``` -nix build -nix build -f package.nix # calls default.nix with a basic callPackage expression -``` - -# Development - -For nix there is a devshell available from `shell.nix` and as a devShell -output from the flake. - -The Justfile contains various useful aliases: -- `just configure [ [extra cmake args]]` -- `just build` (runs configure for debug mode) -- `just run [args]` -- `just clean` -- `just test [args]` (configure with `-DBUILD_TESTING=ON` first) -- `just fmt` -- `just lint` +# Contributing / Development +See [CONTRIBUTING.md](CONTRIBUTING.md) for details. #### License From 7d20b472dd01ec9ae4f3c4f2bda2c808b5631d83 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:23:19 -0700 Subject: [PATCH 20/31] misc: remove the docs and examples submodules They have not been correctly updated in lock-step for a while now. --- .gitignore | 4 ++++ .gitmodules | 6 ------ CONTRIBUTING.md | 2 +- README.md | 13 ------------- docs | 1 - examples | 1 - 6 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 .gitmodules delete mode 160000 docs delete mode 160000 examples diff --git a/.gitignore b/.gitignore index 1933837e..dcdefe39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# related repos +/docs +/examples + # build files /result /build/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 74013769..00000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "docs"] - path = docs - url = https://git.outfoxxed.me/outfoxxed/quickshell-docs -[submodule "examples"] - path = examples - url = https://git.outfoxxed.me/outfoxxed/quickshell-examples diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5fd4836..2aad2b3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -60,7 +60,7 @@ cannot handle random line breaks and will usually require you to disable clang-f lines are too long. Before submitting an MR, if adding new features please make sure the documentation is generated -reasonably using the `quickshell-docs` repo. +reasonably using the `quickshell-docs` repo. We recommend checking it out at `/docs` in this repo. Doc comments take the form `///` or `///!` (summary) and work with markdown. Look at existing code for how it works. diff --git a/README.md b/README.md index 012a33a6..1959583f 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,6 @@ can be built from the [quickshell-docs](https://git.outfoxxed.me/outfoxxed/quick Some fully working examples can be found in the [quickshell-examples](https://git.outfoxxed.me/outfoxxed/quickshell-examples) repo. -Both the documentation and examples are included as submodules with revisions that work with the current -version of quickshell. - -You can clone everything with -``` -$ git clone --recursive https://git.outfoxxed.me/outfoxxed/quickshell.git -``` - -Or clone missing submodules later with -``` -$ git submodule update --init --recursive -``` - # Installation ## Nix diff --git a/docs b/docs deleted file mode 160000 index ff5da84a..00000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903 diff --git a/examples b/examples deleted file mode 160000 index b9e744b5..00000000 --- a/examples +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b9e744b50673304dfddb68f3da2a2e906d028b96 From 29f02d837d4e9902a9efa1f6295e98d042f38341 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:36:33 -0700 Subject: [PATCH 21/31] all: remove NVIDIA workarounds They fixed the driver. --- CMakeLists.txt | 6 ------ default.nix | 2 -- flake.nix | 2 -- src/core/proxywindow.cpp | 7 ------- 4 files changed, 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e790ec0c..a386f5a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,6 @@ option(ASAN "Enable ASAN" OFF) option(FRAME_POINTERS "Always keep frame pointers" ${ASAN}) option(USE_JEMALLOC "Use jemalloc over the system malloc implementation" ON) -option(NVIDIA_COMPAT "Workarounds for nvidia gpus" OFF) option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) @@ -25,7 +24,6 @@ option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") message(STATUS " Jemalloc: ${USE_JEMALLOC}") -message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") message(STATUS " Build tests: ${BUILD_TESTING}") message(STATUS " Sockets: ${SOCKETS}") message(STATUS " Wayland: ${WAYLAND}") @@ -134,10 +132,6 @@ function (qs_pch target) endif() endfunction() -if (NVIDIA_COMPAT) - add_compile_definitions(NVIDIA_COMPAT) -endif() - add_subdirectory(src) if (USE_JEMALLOC) diff --git a/default.nix b/default.nix index 048e181e..d96ff3d1 100644 --- a/default.nix +++ b/default.nix @@ -29,7 +29,6 @@ enableWayland ? true, enableX11 ? true, enablePipewire ? true, - nvidiaCompat ? false, withQtSvg ? true, # svg support withJemalloc ? true, # masks heap fragmentation }: buildStdenv.mkDerivation { @@ -73,7 +72,6 @@ ] ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON" ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; buildPhase = "ninjaBuildPhase"; diff --git a/flake.nix b/flake.nix index 5bb5069e..a0bc18d4 100644 --- a/flake.nix +++ b/flake.nix @@ -12,10 +12,8 @@ quickshell = pkgs.callPackage ./default.nix { gitRev = self.rev or self.dirtyRev; }; - quickshell-nvidia = quickshell.override { nvidiaCompat = true; }; default = quickshell; - nvidia = quickshell-nvidia; }); devShells = forEachSystem (system: pkgs: rec { diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index 50370d9d..c2961c24 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -157,14 +157,7 @@ void ProxyWindowBase::completeWindow() { } bool ProxyWindowBase::deleteOnInvisible() const { -#ifdef NVIDIA_COMPAT - // Nvidia drivers and Qt do not play nice when hiding and showing a window - // so for nvidia compatibility we can never reuse windows if they have been - // hidden. - return true; -#else return false; -#endif } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } From 9d5dd402b916cea2842a7186ca09159442afdccc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 15:37:47 -0700 Subject: [PATCH 22/31] docs: recommend packagers add a dependency on qtsvg --- BUILD.md | 3 +++ README.md | 9 +++++---- default.nix | 24 +++++++++++++----------- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/BUILD.md b/BUILD.md index d7844c6a..92589ccc 100644 --- a/BUILD.md +++ b/BUILD.md @@ -10,6 +10,9 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6declarative` - `pkg-config` +We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and +svg icons will not work, including system ones. + At least Qt 6.6 is required. All features are enabled by default and some have their own dependencies. diff --git a/README.md b/README.md index 1959583f..4def09ed 100644 --- a/README.md +++ b/README.md @@ -40,11 +40,12 @@ or disabled with overrides: ```nix quickshell.packages..default.override { - enableWayland = true; - enableX11 = true; - enablePipewire = true; - withQtSvg = true; withJemalloc = true; + withQtSvg = true; + withWayland = true; + withX11 = true; + withPipewire = true; + withHyprland = true; } ``` diff --git a/default.nix b/default.nix index d96ff3d1..01624c4a 100644 --- a/default.nix +++ b/default.nix @@ -26,11 +26,12 @@ else "unknown"), debug ? false, - enableWayland ? true, - enableX11 ? true, - enablePipewire ? true, - withQtSvg ? true, # svg support withJemalloc ? true, # masks heap fragmentation + withQtSvg ? true, + withWayland ? true, + withX11 ? true, + withPipewire ? true, + withHyprland ? true, }: buildStdenv.mkDerivation { pname = "quickshell${lib.optionalString debug "-debug"}"; version = "0.1.0"; @@ -41,7 +42,7 @@ ninja qt6.wrapQtAppsHook pkg-config - ] ++ (lib.optionals enableWayland [ + ] ++ (lib.optionals withWayland [ wayland-protocols wayland-scanner ]); @@ -52,11 +53,11 @@ ] ++ (lib.optional withJemalloc jemalloc) ++ (lib.optional withQtSvg qt6.qtsvg) - ++ (lib.optionals enableWayland [ qt6.qtwayland wayland ]) - ++ (lib.optional enableX11 xorg.libxcb) - ++ (lib.optional enablePipewire pipewire); + ++ (lib.optionals withWayland [ qt6.qtwayland wayland ]) + ++ (lib.optional withX11 xorg.libxcb) + ++ (lib.optional withPipewire pipewire); - QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; + QTWAYLANDSCANNER = lib.optionalString withWayland "${qt6.qtwayland}/libexec/qtwaylandscanner"; configurePhase = let cmakeBuildType = if debug @@ -71,8 +72,9 @@ "-DGIT_REVISION=${gitRev}" ] ++ lib.optional (!withJemalloc) "-DUSE_JEMALLOC=OFF" - ++ lib.optional (!enableWayland) "-DWAYLAND=OFF" - ++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF"; + ++ lib.optional (!withWayland) "-DWAYLAND=OFF" + ++ lib.optional (!withPipewire) "-DSERVICE_PIPEWIRE=OFF" + ++ lib.optional (!withHyprland) "-DHYPRLAND=OFF"; buildPhase = "ninjaBuildPhase"; enableParallelBuilding = true; From b1f5a5eb94badd180f182cd2bc1fb9687cb2f672 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 2 Jun 2024 16:18:45 -0700 Subject: [PATCH 23/31] service/mpris: preserve mpris watcher and players across reload --- BUILD.md | 2 +- src/core/proxywindow.cpp | 4 +--- src/services/mpris/watcher.cpp | 11 ++++++++++- src/services/mpris/watcher.hpp | 23 +++++++++++++++++------ 4 files changed, 29 insertions(+), 11 deletions(-) diff --git a/BUILD.md b/BUILD.md index 92589ccc..c9909598 100644 --- a/BUILD.md +++ b/BUILD.md @@ -43,7 +43,7 @@ To disable: `-DSOCKETS=OFF` ### Wayland This feature enables wayland support. Subfeatures exist for each particular wayland integration. -WARNING: Wayland integration relies on featurs that are not part of the public Qt API and which +WARNING: Wayland integration relies on features that are not part of the public Qt API and which may break in minor releases. Updating quickshell's dependencies without ensuring without ensuring that the current Qt version is supported WILL result in quickshell failing to build or misbehaving at runtime. diff --git a/src/core/proxywindow.cpp b/src/core/proxywindow.cpp index c2961c24..4eef5f38 100644 --- a/src/core/proxywindow.cpp +++ b/src/core/proxywindow.cpp @@ -156,9 +156,7 @@ void ProxyWindowBase::completeWindow() { emit this->screenChanged(); } -bool ProxyWindowBase::deleteOnInvisible() const { - return false; -} +bool ProxyWindowBase::deleteOnInvisible() const { return false; } QQuickWindow* ProxyWindowBase::backingWindow() const { return this->window; } QQuickItem* ProxyWindowBase::contentItem() const { return this->mContentItem; } diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 8c67a4d6..8a788933 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -16,7 +16,7 @@ namespace qs::service::mpris { Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); -MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { +MprisWatcher::MprisWatcher() { qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -102,4 +102,13 @@ void MprisWatcher::registerPlayer(const QString& address) { qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; } +MprisWatcher* MprisWatcher::instance() { + static MprisWatcher* instance = new MprisWatcher(); // NOLINT + return instance; +} + +ObjectModel* MprisQml::players() { // NOLINT + return MprisWatcher::instance()->players(); +} + } // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index 91275c7e..d60471cc 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -18,16 +18,12 @@ namespace qs::service::mpris { ///! Provides access to MprisPlayers. class MprisWatcher: public QObject { Q_OBJECT; - QML_NAMED_ELEMENT(Mpris); - QML_SINGLETON; - /// All connected MPRIS players. - Q_PROPERTY(ObjectModel* players READ players CONSTANT); public: - explicit MprisWatcher(QObject* parent = nullptr); - [[nodiscard]] ObjectModel* players(); + static MprisWatcher* instance(); + private slots: void onServiceRegistered(const QString& service); void onServiceUnregistered(const QString& service); @@ -35,6 +31,8 @@ private slots: void onPlayerDestroyed(QObject* object); private: + explicit MprisWatcher(); + void registerExisting(); void registerPlayer(const QString& address); @@ -43,4 +41,17 @@ private: ObjectModel readyPlayers {this}; }; +class MprisQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(Mpris); + QML_SINGLETON; + /// All connected MPRIS players. + Q_PROPERTY(ObjectModel* players READ players CONSTANT); + +public: + explicit MprisQml(QObject* parent = nullptr): QObject(parent) {}; + + [[nodiscard]] ObjectModel* players(); +}; + } // namespace qs::service::mpris From 37fecfc9905c563083fd4169b921188a9b16bba0 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 3 Jun 2024 00:38:22 -0700 Subject: [PATCH 24/31] docs: add commit style instructions --- CONTRIBUTING.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2aad2b3c..6fdef09c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,7 @@ Instructions for development setup and upstreaming patches. If you just want to build or package quickshell see [BUILD.md](BUILD.md). ## Development + Install the dependencies listed in [BUILD.md](BUILD.md). You probably want all of them even if you don't use all of them to ensure tests work correctly and avoid passing a bunch of configure @@ -67,3 +68,32 @@ Look at existing code for how it works. Quickshell modules additionally have a `module.md` file which contains a summary, description, and list of headers to scan for documentation. + +## Contributing + +### Commits +Please structure your commit messages as `scope[!]: commit` where +the scope is something like `core` or `service/mpris`. (pick what has been +used historically or what makes sense if new.) Add `!` for changes that break +existing APIs or functionality. + +Commit descriptions should contain a summary of the changes if they are not +sufficiently addressed in the commit message. + +Please squash/rebase additions or edits to previous changes and follow the +commit style to keep the history easily searchable at a glance. +Depending on the change, it is often reasonable to squash it into just +a single commit. (If you do not follow this we will squash your changes +for you.) + +### Sending patches +You may contribute by submitting a pull request on github, asking for +an account on our git server, or emailing patches / git bundles +directly to `outfoxxed@outfoxxed.me`. + +### Getting help +If you're getting stuck, you can come talk to us in the +[quickshell-development matrix room](https://matrix.to/#/#quickshell-development:outfoxxed.me) +for help on implementation, conventions, etc. +Feel free to ask for advice early in your implementation if you are +unsure. From be237b6ab5f4e3ec875d11359b71d4cbb543314d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 4 Jun 2024 13:14:39 -0700 Subject: [PATCH 25/31] core/elapsedtimer: add ElapsedTimer --- src/core/CMakeLists.txt | 1 + src/core/elapsedtimer.cpp | 22 +++++++++++++++++++ src/core/elapsedtimer.hpp | 45 +++++++++++++++++++++++++++++++++++++++ src/core/module.md | 1 + 4 files changed, 69 insertions(+) create mode 100644 src/core/elapsedtimer.cpp create mode 100644 src/core/elapsedtimer.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 88c26241..24d2e685 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -27,6 +27,7 @@ qt_add_library(quickshell-core STATIC transformwatcher.cpp boundcomponent.cpp model.cpp + elapsedtimer.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/elapsedtimer.cpp b/src/core/elapsedtimer.cpp new file mode 100644 index 00000000..91321122 --- /dev/null +++ b/src/core/elapsedtimer.cpp @@ -0,0 +1,22 @@ +#include "elapsedtimer.hpp" + +#include + +ElapsedTimer::ElapsedTimer() { this->timer.start(); } + +qreal ElapsedTimer::elapsed() { return static_cast(this->elapsedNs()) / 1000000000.0; } + +qreal ElapsedTimer::restart() { return static_cast(this->restartNs()) / 1000000000.0; } + +qint64 ElapsedTimer::elapsedMs() { return this->timer.elapsed(); } + +qint64 ElapsedTimer::restartMs() { return this->timer.restart(); } + +qint64 ElapsedTimer::elapsedNs() { return this->timer.nsecsElapsed(); } + +qint64 ElapsedTimer::restartNs() { + // see qelapsedtimer.cpp + auto old = this->timer; + this->timer.start(); + return old.durationTo(this->timer).count(); +} diff --git a/src/core/elapsedtimer.hpp b/src/core/elapsedtimer.hpp new file mode 100644 index 00000000..85850963 --- /dev/null +++ b/src/core/elapsedtimer.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +///! Measures time between events +/// The ElapsedTimer measures time since its last restart, and is useful +/// for determining the time between events that don't supply it. +class ElapsedTimer: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + explicit ElapsedTimer(); + + /// Return the number of seconds since the timer was last + /// started or restarted, with nanosecond precision. + Q_INVOKABLE qreal elapsed(); + + /// Restart the timer, returning the number of seconds since + /// the timer was last started or restarted, with nanosecond precision. + Q_INVOKABLE qreal restart(); + + /// Return the number of milliseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedMs(); + + /// Restart the timer, returning the number of milliseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartMs(); + + /// Return the number of nanoseconds since the timer was last + /// started or restarted. + Q_INVOKABLE qint64 elapsedNs(); + + /// Restart the timer, returning the number of nanoseconds since + /// the timer was last started or restarted. + Q_INVOKABLE qint64 restartNs(); + +private: + QElapsedTimer timer; +}; diff --git a/src/core/module.md b/src/core/module.md index dc1f204d..13218610 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -19,5 +19,6 @@ headers = [ "transformwatcher.hpp", "boundcomponent.hpp", "model.hpp", + "elapsedtimer.hpp", ] ----- From d14ca709849ba0a0e3de5126b77cfc7819c9b100 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 5 Jun 2024 19:26:20 -0700 Subject: [PATCH 26/31] hyprland/ipc: add hyprland ipc Only monitors and workspaces are fully tracked for now. --- CMakeLists.txt | 2 + src/wayland/hyprland/CMakeLists.txt | 5 + src/wayland/hyprland/ipc/CMakeLists.txt | 18 + src/wayland/hyprland/ipc/connection.cpp | 542 ++++++++++++++++++++++++ src/wayland/hyprland/ipc/connection.hpp | 123 ++++++ src/wayland/hyprland/ipc/monitor.cpp | 136 ++++++ src/wayland/hyprland/ipc/monitor.hpp | 85 ++++ src/wayland/hyprland/ipc/qml.cpp | 52 +++ src/wayland/hyprland/ipc/qml.hpp | 66 +++ src/wayland/hyprland/ipc/workspace.cpp | 79 ++++ src/wayland/hyprland/ipc/workspace.hpp | 59 +++ src/wayland/hyprland/module.md | 4 + 12 files changed, 1171 insertions(+) create mode 100644 src/wayland/hyprland/ipc/CMakeLists.txt create mode 100644 src/wayland/hyprland/ipc/connection.cpp create mode 100644 src/wayland/hyprland/ipc/connection.hpp create mode 100644 src/wayland/hyprland/ipc/monitor.cpp create mode 100644 src/wayland/hyprland/ipc/monitor.hpp create mode 100644 src/wayland/hyprland/ipc/qml.cpp create mode 100644 src/wayland/hyprland/ipc/qml.hpp create mode 100644 src/wayland/hyprland/ipc/workspace.cpp create mode 100644 src/wayland/hyprland/ipc/workspace.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a386f5a8..246428ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) +option(HYPRLAND_IPC "Hyprland IPC" ON) option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) @@ -38,6 +39,7 @@ message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) + message(STATUS " IPC: ${HYPRLAND_IPC}") message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}") endif() diff --git a/src/wayland/hyprland/CMakeLists.txt b/src/wayland/hyprland/CMakeLists.txt index be6bf49c..be2f0c59 100644 --- a/src/wayland/hyprland/CMakeLists.txt +++ b/src/wayland/hyprland/CMakeLists.txt @@ -4,6 +4,11 @@ target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS}) set(HYPRLAND_MODULES) +if (HYPRLAND_IPC) + add_subdirectory(ipc) + list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._Ipc) +endif() + if (HYPRLAND_FOCUS_GRAB) add_subdirectory(focus_grab) list(APPEND HYPRLAND_MODULES Quickshell.Hyprland._FocusGrab) diff --git a/src/wayland/hyprland/ipc/CMakeLists.txt b/src/wayland/hyprland/ipc/CMakeLists.txt new file mode 100644 index 00000000..59200462 --- /dev/null +++ b/src/wayland/hyprland/ipc/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-hyprland-ipc STATIC + connection.cpp + monitor.cpp + workspace.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-hyprland-ipc + URI Quickshell.Hyprland._Ipc + VERSION 0.1 +) + +target_link_libraries(quickshell-hyprland-ipc PRIVATE ${QT_DEPS}) + +qs_pch(quickshell-hyprland-ipc) +qs_pch(quickshell-hyprland-ipcplugin) + +target_link_libraries(quickshell PRIVATE quickshell-hyprland-ipcplugin) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp new file mode 100644 index 00000000..e7265c70 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -0,0 +1,542 @@ +#include "connection.hpp" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "monitor.hpp" +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); +Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); + +HyprlandIpc::HyprlandIpc() { + auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { + qWarning() << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Cannot connect to hyprland."; + return; + } + + auto runtimeDir = qEnvironmentVariable("XDG_RUNTIME_DIR"); + auto hyprlandDir = runtimeDir + "/hypr/" + his; + + if (!QFileInfo(hyprlandDir).isDir()) { + hyprlandDir = "/tmp/hypr/" + his; + } + + if (!QFileInfo(hyprlandDir).isDir()) { + qWarning() << "Unable to find hyprland socket. Cannot connect to hyprland."; + return; + } + + this->mRequestSocketPath = hyprlandDir + "/.socket.sock"; + this->mEventSocketPath = hyprlandDir + "/.socket2.sock"; + + // clang-format off + 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); + // clang-format on + + // Sockets don't appear to be able to send data in the first event loop + // cycle of the program, so delay it by one. No idea why this is the case. + QTimer::singleShot(0, [this]() { + this->eventSocket.connectToServer(this->mEventSocketPath, QLocalSocket::ReadOnly); + this->refreshMonitors(true); + this->refreshWorkspaces(true); + }); +} + +QString HyprlandIpc::requestSocketPath() const { return this->mRequestSocketPath; } +QString HyprlandIpc::eventSocketPath() const { return this->mEventSocketPath; } + +void HyprlandIpc::eventSocketError(QLocalSocket::LocalSocketError error) const { + if (!this->valid) { + qWarning() << "Unable to connect to hyprland event socket:" << error; + } else { + qWarning() << "Hyprland event socket error:" << error; + } +} + +void HyprlandIpc::eventSocketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::ConnectedState) { + qCInfo(logHyprlandIpc) << "Hyprland event socket connected."; + emit this->connected(); + } else if (state == QLocalSocket::UnconnectedState && this->valid) { + qCWarning(logHyprlandIpc) << "Hyprland event socket disconnected."; + } + + this->valid = state == QLocalSocket::ConnectedState; +} + +void HyprlandIpc::eventSocketReady() { + while (true) { + auto rawEvent = this->eventSocket.readLine(); + if (rawEvent.isEmpty()) break; + + // remove trailing \n + rawEvent.truncate(rawEvent.length() - 1); + auto splitIdx = rawEvent.indexOf(">>"); + auto event = QByteArrayView(rawEvent.data(), splitIdx); + auto data = QByteArrayView( + rawEvent.data() + splitIdx + 2, // NOLINT + rawEvent.data() + rawEvent.length() // NOLINT + ); + qCDebug(logHyprlandIpcEvents) << "Received event:" << rawEvent << "parsed as" << event << data; + + this->event.name = event; + this->event.data = data; + this->onEvent(&this->event); + emit this->rawEvent(&this->event); + } +} + +void HyprlandIpc::makeRequest( + const QByteArray& request, + const std::function& callback +) { + auto* requestSocket = new QLocalSocket(this); + qCDebug(logHyprlandIpc) << "Making request:" << request; + + auto connectedCallback = [this, request, requestSocket, callback]() { + auto responseCallback = [requestSocket, callback]() { + auto response = requestSocket->readAll(); + callback(true, std::move(response)); + delete requestSocket; + }; + + QObject::connect(requestSocket, &QLocalSocket::readyRead, this, responseCallback); + + requestSocket->write(request); + }; + + auto errorCallback = [=](QLocalSocket::LocalSocketError error) { + qCWarning(logHyprlandIpc) << "Error making request:" << error << "request:" << request; + requestSocket->deleteLater(); + callback(false, {}); + }; + + QObject::connect(requestSocket, &QLocalSocket::connected, this, connectedCallback); + QObject::connect(requestSocket, &QLocalSocket::errorOccurred, this, errorCallback); + + requestSocket->connectToServer(this->mRequestSocketPath); +} + +void HyprlandIpc::dispatch(const QString& request) { + this->makeRequest( + ("dispatch " + request).toUtf8(), + [request](bool success, const QByteArray& response) { + if (!success) { + qCWarning(logHyprlandIpc) << "Failed to request dispatch of" << request; + return; + } + + if (response != "ok") { + qCWarning(logHyprlandIpc) + << "Dispatch request" << request << "failed with error" << response; + } + } + ); +} + +ObjectModel* HyprlandIpc::monitors() { return &this->mMonitors; } + +ObjectModel* HyprlandIpc::workspaces() { return &this->mWorkspaces; } + +QVector HyprlandIpc::parseEventArgs(QByteArrayView event, quint16 count) { + auto args = QVector(); + + for (auto i = 0; i < count - 1; i++) { + auto splitIdx = event.indexOf(','); + if (splitIdx == -1) break; + args.push_back(event.sliced(0, splitIdx)); + event = event.sliced(splitIdx + 1); + } + + if (!event.isEmpty()) { + args.push_back(event); + } + + return args; +} + +QVector HyprlandIpcEvent::parse(qint32 argumentCount) const { + auto args = QVector(); + + for (auto arg: this->parseView(argumentCount)) { + args.push_back(QString::fromUtf8(arg)); + } + + return args; +} + +QVector HyprlandIpcEvent::parseView(qint32 argumentCount) const { + return HyprlandIpc::parseEventArgs(this->data, argumentCount); +} + +QString HyprlandIpcEvent::nameStr() const { return QString::fromUtf8(this->name); } +QString HyprlandIpcEvent::dataStr() const { return QString::fromUtf8(this->data); } + +HyprlandIpc* HyprlandIpc::instance() { + static HyprlandIpc* instance = nullptr; // NOLINT + + if (instance == nullptr) { + instance = new HyprlandIpc(); + } + + return instance; +} + +void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { + if (event->name == "configreloaded") { + this->refreshMonitors(true); + this->refreshWorkspaces(true); + } else if (event->name == "monitoraddedv2") { + auto args = event->parseView(3); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + // hyprland will often reference the monitor before creation, in which case + // it will already exist. + auto* monitor = this->findMonitorByName(name, false); + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + monitor = new HyprlandMonitor(this); + } + + qCDebug(logHyprlandIpc) << "Monitor added with id" << id << "name" << name + << "preemptively created:" << existed; + + monitor->updateInitial(id, name, QString::fromUtf8(args.at(2))); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + // refresh even if it already existed because workspace focus might have changed. + this->refreshMonitors(false); + } else if (event->name == "monitorremoved") { + const auto& mList = this->mMonitors.valueList(); + auto name = QString::fromUtf8(event->data); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for monitor" << name + << "which was not previously tracked."; + return; + } + + auto index = monitorIter - mList.begin(); + auto* monitor = *monitorIter; + + qCDebug(logHyprlandIpc) << "Monitor removed with id" << monitor->id() << "name" + << monitor->name(); + this->mMonitors.removeAt(index); + + // delete the monitor object in the next event loop cycle so it's likely to + // still exist when future events reference it after destruction. + // If we get to the next cycle and things still reference it (unlikely), nulls + // can make it to the frontend. + monitor->deleteLater(); + } else if (event->name == "createworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + qCDebug(logHyprlandIpc) << "Workspace created with id" << id << "name" << name; + + auto* workspace = this->findWorkspaceByName(name, false); + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + workspace = new HyprlandWorkspace(this); + } + + workspace->updateInitial(id, name); + + if (!existed) { + this->refreshWorkspaces(false); + this->mWorkspaces.insertObject(workspace); + } + } else if (event->name == "destroyworkspacev2") { + auto args = event->parseView(2); + + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { + return m->id() == id; + }); + + if (workspaceIter == mList.end()) { + qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name + << "which was not previously tracked."; + return; + } + + auto index = workspaceIter - mList.begin(); + auto* workspace = *workspaceIter; + + qCDebug(logHyprlandIpc) << "Workspace removed with id" << id << "name" << name; + this->mWorkspaces.removeAt(index); + + // workspaces have not been observed to be referenced after deletion + delete workspace; + + for (auto* monitor: this->mMonitors.valueList()) { + if (monitor->activeWorkspace() == nullptr) { + // removing a monitor will cause a new workspace to be created and destroyed after removal, + // but it won't go back to a real workspace afterwards and just leaves a null, so we + // re-query monitors if this appears to be the case. + this->refreshMonitors(false); + break; + } + } + } else if (event->name == "focusedmon") { + auto args = event->parseView(2); + auto name = QString::fromUtf8(args.at(0)); + auto workspaceName = QString::fromUtf8(args.at(1)); + + HyprlandWorkspace* workspace = nullptr; + if (workspaceName != "?") { // what the fuck + workspace = this->findWorkspaceByName(workspaceName, false); + } + + auto* monitor = this->findMonitorByName(name, true); + this->setFocusedMonitor(monitor); + monitor->setActiveWorkspace(workspace); + } else if (event->name == "workspacev2") { + auto args = event->parseView(2); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + if (this->mFocusedMonitor != nullptr) { + auto* workspace = this->findWorkspaceByName(name, true, id); + this->mFocusedMonitor->setActiveWorkspace(workspace); + } + } else if (event->name == "moveworkspacev2") { + auto args = event->parseView(3); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + auto monitorName = QString::fromUtf8(args.at(2)); + + auto* workspace = this->findWorkspaceByName(name, true, id); + auto* monitor = this->findMonitorByName(monitorName, true); + + workspace->setMonitor(monitor); + } +} + +HyprlandWorkspace* +HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + if (workspaceIter != mList.end()) { + return *workspaceIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Workspace" << name + << "requested before creation, performing early init"; + auto* workspace = new HyprlandWorkspace(this); + workspace->updateInitial(id, name); + this->mWorkspaces.insertObject(workspace); + return workspace; + } else { + return nullptr; + } +} + +void HyprlandIpc::refreshWorkspaces(bool canCreate) { + if (this->requestingWorkspaces) return; + this->requestingWorkspaces = true; + + this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); + + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); + + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; + + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } + + workspace->updateFromObject(object); + + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } + + names.push_back(name); + } + + auto removedWorkspaces = QVector(); + + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } + + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + }); +} + +HyprlandMonitor* +HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { + const auto& mList = this->mMonitors.valueList(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; + }); + + if (monitorIter != mList.end()) { + return *monitorIter; + } else if (createIfMissing) { + qCDebug(logHyprlandIpc) << "Monitor" << name + << "requested before creation, performing early init"; + auto* monitor = new HyprlandMonitor(this); + monitor->updateInitial(id, name, ""); + this->mMonitors.insertObject(monitor); + return monitor; + } else { + return nullptr; + } +} + +HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMonitor; } + +HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { + // Wayland monitors appear after hyprland ones are created and disappear after destruction + // so simply not doing any preemptive creation is enough. + + if (screen == nullptr) return nullptr; + return this->findMonitorByName(screen->name(), false); +} + +void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mFocusedMonitor) return; + + if (this->mFocusedMonitor != nullptr) { + QObject::disconnect(this->mFocusedMonitor, nullptr, this, nullptr); + } + + this->mFocusedMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandIpc::onFocusedMonitorDestroyed); + } + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::onFocusedMonitorDestroyed() { + this->mFocusedMonitor = nullptr; + emit this->focusedMonitorChanged(); +} + +void HyprlandIpc::refreshMonitors(bool canCreate) { + if (this->requestingMonitors) return; + this->requestingMonitors = true; + + this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) return; + + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); + + const auto& mList = this->mMonitors.valueList(); + auto ids = QVector(); + + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto id = object.value("id").toInt(); + + auto monitorIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { + return m->id() == id; + }); + + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; + + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } + + monitor->updateFromObject(object); + + if (!existed) { + this->mMonitors.insertObject(monitor); + } + + ids.push_back(id); + } + + auto removedMonitors = QVector(); + + for (auto* monitor: mList) { + if (!ids.contains(monitor->id())) { + removedMonitors.push_back(monitor); + } + } + + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + }); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp new file mode 100644 index 00000000..d566a866 --- /dev/null +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -0,0 +1,123 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor; +class HyprlandWorkspace; + +} // namespace qs::hyprland::ipc + +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandWorkspace*); +Q_DECLARE_OPAQUE_POINTER(qs::hyprland::ipc::HyprlandMonitor*); + +namespace qs::hyprland::ipc { + +///! Live Hyprland IPC event. +/// Live Hyprland IPC event. Holding this object after the +/// signal handler exits is undefined as the event instance +/// is reused. +class HyprlandIpcEvent: public QObject { + Q_OBJECT; + /// The name of the event. + Q_PROPERTY(QString name READ nameStr CONSTANT); + /// The unparsed data of the event. + Q_PROPERTY(QString data READ dataStr CONSTANT); + QML_NAMED_ELEMENT(HyprlandEvent); + QML_UNCREATABLE("HyprlandIpcEvents cannot be created."); + +public: + HyprlandIpcEvent(QObject* parent): QObject(parent) {} + + /// Parse this event with a known number of arguments. + /// + /// Argument count is required as some events can contain commas + /// in the last argument, which can be ignored as long as the count is known. + Q_INVOKABLE [[nodiscard]] QVector parse(qint32 argumentCount) const; + [[nodiscard]] QVector parseView(qint32 argumentCount) const; + + [[nodiscard]] QString nameStr() const; + [[nodiscard]] QString dataStr() const; + + void reset(); + QByteArrayView name; + QByteArrayView data; +}; + +class HyprlandIpc: public QObject { + Q_OBJECT; + +public: + static HyprlandIpc* instance(); + + [[nodiscard]] QString requestSocketPath() const; + [[nodiscard]] QString eventSocketPath() const; + + void + makeRequest(const QByteArray& request, const std::function& callback); + void dispatch(const QString& request); + + [[nodiscard]] HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + [[nodiscard]] HyprlandMonitor* focusedMonitor() const; + void setFocusedMonitor(HyprlandMonitor* monitor); + + [[nodiscard]] ObjectModel* monitors(); + [[nodiscard]] ObjectModel* workspaces(); + + // No byId because these preemptively create objects. The given id is set if created. + HyprlandWorkspace* findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id = 0); + HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); + + // canCreate avoids making ghost workspaces when the connection races + void refreshWorkspaces(bool canCreate); + void refreshMonitors(bool canCreate); + + // The last argument may contain commas, so the count is required. + [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); + +signals: + void connected(); + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); + +private slots: + void eventSocketError(QLocalSocket::LocalSocketError error) const; + void eventSocketStateChanged(QLocalSocket::LocalSocketState state); + void eventSocketReady(); + + void onFocusedMonitorDestroyed(); + +private: + explicit HyprlandIpc(); + + void onEvent(HyprlandIpcEvent* event); + + QLocalSocket eventSocket; + QString mRequestSocketPath; + QString mEventSocketPath; + bool valid = false; + bool requestingMonitors = false; + bool requestingWorkspaces = false; + + ObjectModel mMonitors {this}; + ObjectModel mWorkspaces {this}; + HyprlandMonitor* mFocusedMonitor = nullptr; + //HyprlandWorkspace* activeWorkspace = nullptr; + + HyprlandIpcEvent event {this}; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.cpp b/src/wayland/hyprland/ipc/monitor.cpp new file mode 100644 index 00000000..8ee5e207 --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.cpp @@ -0,0 +1,136 @@ +#include "monitor.hpp" +#include + +#include +#include +#include +#include + +#include "workspace.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandMonitor::id() const { return this->mId; } +QString HyprlandMonitor::name() const { return this->mName; } +QString HyprlandMonitor::description() const { return this->mDescription; } +qint32 HyprlandMonitor::x() const { return this->mX; } +qint32 HyprlandMonitor::y() const { return this->mY; } +qint32 HyprlandMonitor::width() const { return this->mWidth; } +qint32 HyprlandMonitor::height() const { return this->mHeight; } +qreal HyprlandMonitor::scale() const { return this->mScale; } +QVariantMap HyprlandMonitor::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandMonitor::updateInitial(qint32 id, QString name, QString description) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } +} + +void HyprlandMonitor::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto description = object.value("description").value(); + auto x = object.value("x").value(); + auto y = object.value("y").value(); + auto width = object.value("width").value(); + auto height = object.value("height").value(); + auto scale = object.value("height").value(); + auto activeWorkspaceObj = object.value("activeWorkspace").value(); + auto activeWorkspaceId = activeWorkspaceObj.value("id").value(); + auto activeWorkspaceName = activeWorkspaceObj.value("name").value(); + auto focused = object.value("focused").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (description != this->mDescription) { + this->mDescription = std::move(description); + emit this->descriptionChanged(); + } + + if (x != this->mX) { + this->mX = x; + emit this->xChanged(); + } + + if (y != this->mY) { + this->mY = y; + emit this->yChanged(); + } + + if (width != this->mWidth) { + this->mWidth = width; + emit this->widthChanged(); + } + + if (height != this->mHeight) { + this->mHeight = height; + emit this->heightChanged(); + } + + if (scale != this->mScale) { + this->mScale = scale; + emit this->scaleChanged(); + } + + if (this->mActiveWorkspace == nullptr || this->mActiveWorkspace->name() != activeWorkspaceName) { + auto* workspace = this->ipc->findWorkspaceByName(activeWorkspaceName, true, activeWorkspaceId); + workspace->setMonitor(this); + this->setActiveWorkspace(workspace); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); + + if (focused) { + this->ipc->setFocusedMonitor(this); + } +} + +HyprlandWorkspace* HyprlandMonitor::activeWorkspace() const { return this->mActiveWorkspace; } + +void HyprlandMonitor::setActiveWorkspace(HyprlandWorkspace* workspace) { + if (workspace == this->mActiveWorkspace) return; + + if (this->mActiveWorkspace != nullptr) { + QObject::disconnect(this->mActiveWorkspace, nullptr, this, nullptr); + } + + this->mActiveWorkspace = workspace; + + if (workspace != nullptr) { + QObject::connect( + workspace, + &QObject::destroyed, + this, + &HyprlandMonitor::onActiveWorkspaceDestroyed + ); + } + + emit this->activeWorkspaceChanged(); +} + +void HyprlandMonitor::onActiveWorkspaceDestroyed() { + this->mActiveWorkspace = nullptr; + emit this->activeWorkspaceChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/monitor.hpp b/src/wayland/hyprland/ipc/monitor.hpp new file mode 100644 index 00000000..6b5d2ecc --- /dev/null +++ b/src/wayland/hyprland/ipc/monitor.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandMonitor: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + Q_PROPERTY(QString description READ description NOTIFY descriptionChanged); + Q_PROPERTY(qint32 x READ x NOTIFY xChanged); + Q_PROPERTY(qint32 y READ y NOTIFY yChanged); + Q_PROPERTY(qint32 width READ width NOTIFY widthChanged); + Q_PROPERTY(qint32 height READ height NOTIFY heightChanged); + Q_PROPERTY(qreal scale READ scale NOTIFY scaleChanged); + /// Last json returned for this monitor, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the monitor object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshMonitors()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + /// The currently active workspace on this monitor. May be null. + Q_PROPERTY(HyprlandWorkspace* activeWorkspace READ activeWorkspace NOTIFY activeWorkspaceChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandMonitors must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandMonitor(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name, QString description); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QString description() const; + [[nodiscard]] qint32 x() const; + [[nodiscard]] qint32 y() const; + [[nodiscard]] qint32 width() const; + [[nodiscard]] qint32 height() const; + [[nodiscard]] qreal scale() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setActiveWorkspace(HyprlandWorkspace* workspace); + [[nodiscard]] HyprlandWorkspace* activeWorkspace() const; + +signals: + void idChanged(); + void nameChanged(); + void descriptionChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void scaleChanged(); + void lastIpcObjectChanged(); + void activeWorkspaceChanged(); + +private slots: + void onActiveWorkspaceDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QString mDescription; + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + qreal mScale = 0; + QVariantMap mLastIpcObject; + + HyprlandWorkspace* mActiveWorkspace = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.cpp b/src/wayland/hyprland/ipc/qml.cpp new file mode 100644 index 00000000..1e75ee9c --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.cpp @@ -0,0 +1,52 @@ +#include "qml.hpp" + +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +HyprlandIpcQml::HyprlandIpcQml() { + auto* instance = HyprlandIpc::instance(); + + QObject::connect(instance, &HyprlandIpc::rawEvent, this, &HyprlandIpcQml::rawEvent); + QObject::connect( + instance, + &HyprlandIpc::focusedMonitorChanged, + this, + &HyprlandIpcQml::focusedMonitorChanged + ); +} + +void HyprlandIpcQml::dispatch(const QString& request) { + HyprlandIpc::instance()->dispatch(request); +} + +HyprlandMonitor* HyprlandIpcQml::monitorFor(QuickshellScreenInfo* screen) { + return HyprlandIpc::instance()->monitorFor(screen); +} + +void HyprlandIpcQml::refreshMonitors() { HyprlandIpc::instance()->refreshMonitors(false); } + +void HyprlandIpcQml::refreshWorkspaces() { HyprlandIpc::instance()->refreshWorkspaces(false); } + +QString HyprlandIpcQml::requestSocketPath() { return HyprlandIpc::instance()->requestSocketPath(); } + +QString HyprlandIpcQml::eventSocketPath() { return HyprlandIpc::instance()->eventSocketPath(); } + +HyprlandMonitor* HyprlandIpcQml::focusedMonitor() { + return HyprlandIpc::instance()->focusedMonitor(); +} + +ObjectModel* HyprlandIpcQml::monitors() { + return HyprlandIpc::instance()->monitors(); +} + +ObjectModel* HyprlandIpcQml::workspaces() { + return HyprlandIpc::instance()->workspaces(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/qml.hpp b/src/wayland/hyprland/ipc/qml.hpp new file mode 100644 index 00000000..2d39623f --- /dev/null +++ b/src/wayland/hyprland/ipc/qml.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../../core/model.hpp" +#include "../../../core/qmlscreen.hpp" +#include "connection.hpp" +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandIpcQml: public QObject { + Q_OBJECT; + /// Path to the request socket (.socket.sock) + Q_PROPERTY(QString requestSocketPath READ requestSocketPath CONSTANT); + /// Path to the event socket (.socket2.sock) + Q_PROPERTY(QString eventSocketPath READ eventSocketPath CONSTANT); + /// The currently focused hyprland monitor. May be null. + Q_PROPERTY(HyprlandMonitor* focusedMonitor READ focusedMonitor NOTIFY focusedMonitorChanged); + /// All hyprland monitors. + Q_PROPERTY(ObjectModel* monitors READ monitors CONSTANT); + /// All hyprland workspaces. + Q_PROPERTY(ObjectModel* workspaces READ workspaces CONSTANT); + QML_NAMED_ELEMENT(Hyprland); + QML_SINGLETON; + +public: + explicit HyprlandIpcQml(); + + /// Execute a hyprland [dispatcher](https://wiki.hyprland.org/Configuring/Dispatchers). + Q_INVOKABLE static void dispatch(const QString& request); + + /// Get the HyprlandMonitor object that corrosponds to a quickshell screen. + Q_INVOKABLE static HyprlandMonitor* monitorFor(QuickshellScreenInfo* screen); + + /// Refresh monitor information. + /// + /// Many actions that will invalidate monitor state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshMonitors(); + + /// Refresh workspace information. + /// + /// Many actions that will invalidate workspace state don't send events, + /// so this function is available if required. + Q_INVOKABLE static void refreshWorkspaces(); + + [[nodiscard]] static QString requestSocketPath(); + [[nodiscard]] static QString eventSocketPath(); + [[nodiscard]] static HyprlandMonitor* focusedMonitor(); + [[nodiscard]] static ObjectModel* monitors(); + [[nodiscard]] static ObjectModel* workspaces(); + +signals: + /// Emitted for every event that comes in through the hyprland event socket (socket2). + /// + /// See [Hyprland Wiki: IPC](https://wiki.hyprland.org/IPC/) for a list of events. + void rawEvent(HyprlandIpcEvent* event); + + void focusedMonitorChanged(); +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp new file mode 100644 index 00000000..fbf8477f --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -0,0 +1,79 @@ +#include "workspace.hpp" +#include + +#include +#include +#include +#include + +#include "monitor.hpp" + +namespace qs::hyprland::ipc { + +qint32 HyprlandWorkspace::id() const { return this->mId; } +QString HyprlandWorkspace::name() const { return this->mName; } +QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } + +void HyprlandWorkspace::updateInitial(qint32 id, QString name) { + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } +} + +void HyprlandWorkspace::updateFromObject(QVariantMap object) { + auto id = object.value("id").value(); + auto name = object.value("name").value(); + auto monitorId = object.value("monitorID").value(); + auto monitorName = object.value("monitor").value(); + + if (id != this->mId) { + this->mId = id; + emit this->idChanged(); + } + + if (name != this->mName) { + this->mName = std::move(name); + emit this->nameChanged(); + } + + if (!monitorName.isEmpty() + && (this->mMonitor == nullptr || this->mMonitor->name() != monitorName)) + { + auto* monitor = this->ipc->findMonitorByName(monitorName, true, monitorId); + this->setMonitor(monitor); + } + + this->mLastIpcObject = std::move(object); + emit this->lastIpcObjectChanged(); +} + +HyprlandMonitor* HyprlandWorkspace::monitor() const { return this->mMonitor; } + +void HyprlandWorkspace::setMonitor(HyprlandMonitor* monitor) { + if (monitor == this->mMonitor) return; + + if (this->mMonitor != nullptr) { + QObject::disconnect(this->mMonitor, nullptr, this, nullptr); + } + + this->mMonitor = monitor; + + if (monitor != nullptr) { + QObject::connect(monitor, &QObject::destroyed, this, &HyprlandWorkspace::onMonitorDestroyed); + } + + emit this->monitorChanged(); +} + +void HyprlandWorkspace::onMonitorDestroyed() { + this->mMonitor = nullptr; + emit this->monitorChanged(); +} + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp new file mode 100644 index 00000000..a63901e6 --- /dev/null +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" + +namespace qs::hyprland::ipc { + +class HyprlandWorkspace: public QObject { + Q_OBJECT; + Q_PROPERTY(qint32 id READ id NOTIFY idChanged); + Q_PROPERTY(QString name READ name NOTIFY nameChanged); + /// Last json returned for this workspace, as a javascript object. + /// + /// > [!WARNING] This is *not* updated unless the workspace object is fetched again from + /// > Hyprland. If you need a value that is subject to change and does not have a dedicated + /// > property, run `HyprlandIpc.refreshWorkspaces()` and wait for this property to update. + Q_PROPERTY(QVariantMap lastIpcObject READ lastIpcObject NOTIFY lastIpcObjectChanged); + Q_PROPERTY(HyprlandMonitor* monitor READ monitor NOTIFY monitorChanged); + QML_ELEMENT; + QML_UNCREATABLE("HyprlandWorkspaces must be retrieved from the HyprlandIpc object."); + +public: + explicit HyprlandWorkspace(HyprlandIpc* ipc): QObject(ipc), ipc(ipc) {} + + void updateInitial(qint32 id, QString name); + void updateFromObject(QVariantMap object); + + [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + [[nodiscard]] QVariantMap lastIpcObject() const; + + void setMonitor(HyprlandMonitor* monitor); + [[nodiscard]] HyprlandMonitor* monitor() const; + +signals: + void idChanged(); + void nameChanged(); + void lastIpcObjectChanged(); + void monitorChanged(); + +private slots: + void onMonitorDestroyed(); + +private: + HyprlandIpc* ipc; + + qint32 mId = -1; + QString mName; + QVariantMap mLastIpcObject; + HyprlandMonitor* mMonitor = nullptr; +}; + +} // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/module.md b/src/wayland/hyprland/module.md index 1b3e2fbf..6c2de249 100644 --- a/src/wayland/hyprland/module.md +++ b/src/wayland/hyprland/module.md @@ -1,6 +1,10 @@ name = "Quickshell.Hyprland" description = "Hyprland specific Quickshell types" headers = [ + "ipc/connection.hpp", + "ipc/monitor.hpp", + "ipc/workspace.hpp", + "ipc/qml.hpp", "focus_grab/qml.hpp", "global_shortcuts/qml.hpp", ] From ef1a4134f052998c15369e03cb3b2cb18adfb553 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:46:38 -0700 Subject: [PATCH 27/31] hyprland/ipc: re-request monitors and workspaces on fail --- src/wayland/hyprland/ipc/connection.cpp | 167 +++++++++++++----------- src/wayland/hyprland/ipc/connection.hpp | 4 +- 2 files changed, 93 insertions(+), 78 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index e7265c70..dcb57654 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -377,59 +377,66 @@ HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint } } -void HyprlandIpc::refreshWorkspaces(bool canCreate) { +void HyprlandIpc::refreshWorkspaces(bool canCreate, bool tryAgain) { if (this->requestingWorkspaces) return; this->requestingWorkspaces = true; - this->makeRequest("j/workspaces", [this, canCreate](bool success, const QByteArray& resp) { - this->requestingWorkspaces = false; - if (!success) return; + this->makeRequest( + "j/workspaces", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingWorkspaces = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshWorkspaces(canCreate, false); + return; + } - qCDebug(logHyprlandIpc) << "parsing workspaces response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing workspaces response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mWorkspaces.valueList(); - auto names = QVector(); + const auto& mList = this->mWorkspaces.valueList(); + auto names = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto name = object.value("name").toString(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto name = object.value("name").toString(); - auto workspaceIter = - std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); - auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; - auto existed = workspace != nullptr; + auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; + auto existed = workspace != nullptr; - if (workspace == nullptr) { - if (!canCreate) continue; - workspace = new HyprlandWorkspace(this); - } + if (workspace == nullptr) { + if (!canCreate) continue; + workspace = new HyprlandWorkspace(this); + } - workspace->updateFromObject(object); + workspace->updateFromObject(object); - if (!existed) { - this->mWorkspaces.insertObject(workspace); - } + if (!existed) { + this->mWorkspaces.insertObject(workspace); + } - names.push_back(name); - } + names.push_back(name); + } - auto removedWorkspaces = QVector(); + auto removedWorkspaces = QVector(); - for (auto* workspace: mList) { - if (!names.contains(workspace->name())) { - removedWorkspaces.push_back(workspace); - } - } + for (auto* workspace: mList) { + if (!names.contains(workspace->name())) { + removedWorkspaces.push_back(workspace); + } + } - for (auto* workspace: removedWorkspaces) { - this->mWorkspaces.removeObject(workspace); - delete workspace; - } - }); + for (auto* workspace: removedWorkspaces) { + this->mWorkspaces.removeObject(workspace); + delete workspace; + } + } + ); } HyprlandMonitor* @@ -484,59 +491,67 @@ void HyprlandIpc::onFocusedMonitorDestroyed() { emit this->focusedMonitorChanged(); } -void HyprlandIpc::refreshMonitors(bool canCreate) { +void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { if (this->requestingMonitors) return; this->requestingMonitors = true; - this->makeRequest("j/monitors", [this, canCreate](bool success, const QByteArray& resp) { - this->requestingMonitors = false; - if (!success) return; + this->makeRequest( + "j/monitors", + [this, canCreate, tryAgain](bool success, const QByteArray& resp) { + this->requestingMonitors = false; + if (!success) { + // sometimes fails randomly, so we give it another shot. + if (tryAgain) this->refreshMonitors(canCreate, false); + return; + } - qCDebug(logHyprlandIpc) << "parsing monitors response"; - auto json = QJsonDocument::fromJson(resp).array(); + qCDebug(logHyprlandIpc) << "parsing monitors response"; + auto json = QJsonDocument::fromJson(resp).array(); - const auto& mList = this->mMonitors.valueList(); - auto ids = QVector(); + const auto& mList = this->mMonitors.valueList(); + auto ids = QVector(); - for (auto entry: json) { - auto object = entry.toObject().toVariantMap(); - auto id = object.value("id").toInt(); + for (auto entry: json) { + auto object = entry.toObject().toVariantMap(); + auto id = object.value("id").toInt(); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { - return m->id() == id; - }); + auto monitorIter = + std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { + return m->id() == id; + }); - auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; - auto existed = monitor != nullptr; + auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; + auto existed = monitor != nullptr; - if (monitor == nullptr) { - if (!canCreate) continue; - monitor = new HyprlandMonitor(this); - } + if (monitor == nullptr) { + if (!canCreate) continue; + monitor = new HyprlandMonitor(this); + } - monitor->updateFromObject(object); + monitor->updateFromObject(object); - if (!existed) { - this->mMonitors.insertObject(monitor); - } + if (!existed) { + this->mMonitors.insertObject(monitor); + } - ids.push_back(id); - } + ids.push_back(id); + } - auto removedMonitors = QVector(); + auto removedMonitors = QVector(); - for (auto* monitor: mList) { - if (!ids.contains(monitor->id())) { - removedMonitors.push_back(monitor); - } - } + for (auto* monitor: mList) { + if (!ids.contains(monitor->id())) { + removedMonitors.push_back(monitor); + } + } - for (auto* monitor: removedMonitors) { - this->mMonitors.removeObject(monitor); - // see comment in onEvent - monitor->deleteLater(); - } - }); + for (auto* monitor: removedMonitors) { + this->mMonitors.removeObject(monitor); + // see comment in onEvent + monitor->deleteLater(); + } + } + ); } } // namespace qs::hyprland::ipc diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index d566a866..0144ab43 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -81,8 +81,8 @@ public: HyprlandMonitor* findMonitorByName(const QString& name, bool createIfMissing, qint32 id = -1); // canCreate avoids making ghost workspaces when the connection races - void refreshWorkspaces(bool canCreate); - void refreshMonitors(bool canCreate); + void refreshWorkspaces(bool canCreate, bool tryAgain = true); + void refreshMonitors(bool canCreate, bool tryAgain = true); // The last argument may contain commas, so the count is required. [[nodiscard]] static QVector parseEventArgs(QByteArrayView event, quint16 count); From bc349998dfcd155951cde962cb09dae0548b8508 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:58:10 -0700 Subject: [PATCH 28/31] hyprland/ipc: match by name in refreshMonitors instead of id Was causing ghost/duplicate monitors from usages where the id was not known. --- src/wayland/hyprland/ipc/connection.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index dcb57654..6dcba3ea 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -509,15 +509,15 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { auto json = QJsonDocument::fromJson(resp).array(); const auto& mList = this->mMonitors.valueList(); - auto ids = QVector(); + auto names = QVector(); for (auto entry: json) { auto object = entry.toObject().toVariantMap(); - auto id = object.value("id").toInt(); + auto name = object.value("name").toString(); auto monitorIter = - std::find_if(mList.begin(), mList.end(), [id](const HyprlandMonitor* m) { - return m->id() == id; + std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + return m->name() == name; }); auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; @@ -534,13 +534,13 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { this->mMonitors.insertObject(monitor); } - ids.push_back(id); + names.push_back(name); } auto removedMonitors = QVector(); for (auto* monitor: mList) { - if (!ids.contains(monitor->id())) { + if (!names.contains(monitor->name())) { removedMonitors.push_back(monitor); } } From 5d1def3e49be3ed4abc611247d0e9fb084bfdba8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 6 Jun 2024 00:59:17 -0700 Subject: [PATCH 29/31] hyprland/ipc: fix monitorFor returning null during HyprlandIpc init --- src/wayland/hyprland/ipc/connection.cpp | 8 ++++++-- src/wayland/hyprland/ipc/connection.hpp | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 6dcba3ea..5ee8fffe 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -465,10 +465,12 @@ HyprlandMonitor* HyprlandIpc::focusedMonitor() const { return this->mFocusedMoni HyprlandMonitor* HyprlandIpc::monitorFor(QuickshellScreenInfo* screen) { // Wayland monitors appear after hyprland ones are created and disappear after destruction - // so simply not doing any preemptive creation is enough. + // so simply not doing any preemptive creation is enough, however if this call creates + // the HyprlandIpc singleton then monitors won't be initialized, in which case we + // preemptively create one. if (screen == nullptr) return nullptr; - return this->findMonitorByName(screen->name(), false); + return this->findMonitorByName(screen->name(), !this->monitorsRequested); } void HyprlandIpc::setFocusedMonitor(HyprlandMonitor* monitor) { @@ -505,6 +507,8 @@ void HyprlandIpc::refreshMonitors(bool canCreate, bool tryAgain) { return; } + this->monitorsRequested = true; + qCDebug(logHyprlandIpc) << "parsing monitors response"; auto json = QJsonDocument::fromJson(resp).array(); diff --git a/src/wayland/hyprland/ipc/connection.hpp b/src/wayland/hyprland/ipc/connection.hpp index 0144ab43..1778460a 100644 --- a/src/wayland/hyprland/ipc/connection.hpp +++ b/src/wayland/hyprland/ipc/connection.hpp @@ -111,6 +111,7 @@ private: bool valid = false; bool requestingMonitors = false; bool requestingWorkspaces = false; + bool monitorsRequested = false; ObjectModel mMonitors {this}; ObjectModel mWorkspaces {this}; From b5b9c1f6c352f5e495f580618f5d176497f7814b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 7 Jun 2024 04:31:20 -0700 Subject: [PATCH 30/31] wayland/toplevel_management: add foreign toplevel management --- .clang-tidy | 1 + BUILD.md | 15 +- CMakeLists.txt | 2 + src/wayland/CMakeLists.txt | 5 + src/wayland/module.md | 1 + .../toplevel_management/CMakeLists.txt | 22 ++ src/wayland/toplevel_management/handle.cpp | 228 +++++++++++++++ src/wayland/toplevel_management/handle.hpp | 77 +++++ src/wayland/toplevel_management/manager.cpp | 67 +++++ src/wayland/toplevel_management/manager.hpp | 47 +++ src/wayland/toplevel_management/qml.cpp | 153 ++++++++++ src/wayland/toplevel_management/qml.hpp | 140 +++++++++ ...oreign-toplevel-management-unstable-v1.xml | 270 ++++++++++++++++++ 13 files changed, 1026 insertions(+), 2 deletions(-) create mode 100644 src/wayland/toplevel_management/CMakeLists.txt create mode 100644 src/wayland/toplevel_management/handle.cpp create mode 100644 src/wayland/toplevel_management/handle.hpp create mode 100644 src/wayland/toplevel_management/manager.cpp create mode 100644 src/wayland/toplevel_management/manager.hpp create mode 100644 src/wayland/toplevel_management/qml.cpp create mode 100644 src/wayland/toplevel_management/qml.hpp create mode 100644 src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml diff --git a/.clang-tidy b/.clang-tidy index 6362e662..1da445cd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,6 +5,7 @@ Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, + -bugprone-forward-declararion-namespace, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, diff --git a/BUILD.md b/BUILD.md index c9909598..3c3e7125 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,20 +59,31 @@ Dependencies: - `wayland-protocols` #### Wlroots Layershell -Enables wlroots layershell integration through the [wlr-layer-shell-unstable-v1] protocol, +Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, enabling use cases such as bars overlays and backgrounds. This feature has no extra dependencies. To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` -[wlr-layer-shell-unstable-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 +[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 #### Session Lock Enables session lock support through the [ext-session-lock-v1] protocol, which allows quickshell to be used as a session lock under compatible wayland compositors. +To disable: `-DWAYLAND_SESSION_LOCK=OFF` + [ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + +#### Foreign Toplevel Management +Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1 + +To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` + ### X11 This feature enables x11 support. Currently this implements panel windows for X11 similarly to the wlroots layershell above. diff --git a/CMakeLists.txt b/CMakeLists.txt index 246428ec..7af6b6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON) option(WAYLAND "Enable wayland support" ON) option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON) option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON) +option(WAYLAND_TOPLEVEL_MANAGEMENT "Support the zwlr_foreign_toplevel_management_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_IPC "Hyprland IPC" ON) @@ -31,6 +32,7 @@ message(STATUS " Wayland: ${WAYLAND}") if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") + message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}") endif () message(STATUS " X11: ${X11}") message(STATUS " Services") diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index f20bc11d..ac8f42bb 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -71,6 +71,11 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (WAYLAND_TOPLEVEL_MANAGEMENT) + add_subdirectory(toplevel_management) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) +endif() + if (HYPRLAND) add_subdirectory(hyprland) endif() diff --git a/src/wayland/module.md b/src/wayland/module.md index 7a427df9..d6376e39 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -4,5 +4,6 @@ headers = [ "wlr_layershell/window.hpp", "wlr_layershell.hpp", "session_lock.hpp", + "toplevel_management/qml.hpp", ] ----- diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt new file mode 100644 index 00000000..4537c201 --- /dev/null +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -0,0 +1,22 @@ +qt_add_library(quickshell-wayland-toplevel-management STATIC + manager.cpp + handle.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-toplevel-management + URI Quickshell.Wayland._ToplevelManagement + VERSION 0.1 +) + +wl_proto(quickshell-wayland-toplevel-management + wlr-foreign-toplevel-management-unstable-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" +) + +target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client) + +qs_pch(quickshell-wayland-toplevel-management) +qs_pch(quickshell-wayland-toplevel-managementplugin) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin) diff --git a/src/wayland/toplevel_management/handle.cpp b/src/wayland/toplevel_management/handle.cpp new file mode 100644 index 00000000..8c2886b4 --- /dev/null +++ b/src/wayland/toplevel_management/handle.cpp @@ -0,0 +1,228 @@ +#include "handle.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qwayland-wlr-foreign-toplevel-management-unstable-v1.h" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +QString ToplevelHandle::appId() const { return this->mAppId; } +QString ToplevelHandle::title() const { return this->mTitle; } +QVector ToplevelHandle::visibleScreens() const { return this->mVisibleScreens; } +ToplevelHandle* ToplevelHandle::parent() const { return this->mParent; } +bool ToplevelHandle::activated() const { return this->mActivated; } +bool ToplevelHandle::maximized() const { return this->mMaximized; } +bool ToplevelHandle::minimized() const { return this->mMinimized; } +bool ToplevelHandle::fullscreen() const { return this->mFullscreen; } + +void ToplevelHandle::activate() { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) return; + this->QtWayland::zwlr_foreign_toplevel_handle_v1::activate(inputDevice->object()); +} + +void ToplevelHandle::setMaximized(bool maximized) { + if (maximized) this->set_maximized(); + else this->unset_maximized(); +} + +void ToplevelHandle::setMinimized(bool minimized) { + if (minimized) this->set_minimized(); + else this->unset_minimized(); +} + +void ToplevelHandle::setFullscreen(bool fullscreen) { + if (fullscreen) this->set_fullscreen(nullptr); + else this->unset_fullscreen(); +} + +void ToplevelHandle::fullscreenOn(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + this->set_fullscreen(waylandScreen != nullptr ? waylandScreen->output() : nullptr); +} + +void ToplevelHandle::setRectangle(QWindow* window, QRect rect) { + if (window == nullptr) { + // will be cleared by the compositor if the surface is destroyed + if (this->rectWindow != nullptr) { + auto* waylandWindow = + dynamic_cast(this->rectWindow->handle()); + + if (waylandWindow != nullptr) { + this->set_rectangle(waylandWindow->surface(), 0, 0, 0, 0); + } + } + + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + this->rectWindow = nullptr; + return; + } + + if (this->rectWindow != window) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = window; + QObject::connect(window, &QObject::destroyed, this, &ToplevelHandle::onRectWindowDestroyed); + } + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->set_rectangle(waylandWindow->surface(), rect.x(), rect.y(), rect.width(), rect.height()); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window, rect]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->set_rectangle( + waylandWindow->surface(), + rect.x(), + rect.y(), + rect.width(), + rect.height() + ); + } + }); + } +} + +void ToplevelHandle::onRectWindowDestroyed() { this->rectWindow = nullptr; } + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_done() { + qCDebug(logToplevelManagement) << this << "got done"; + auto wasReady = this->isReady; + this->isReady = true; + + if (!wasReady) { + emit this->ready(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_closed() { + qCDebug(logToplevelManagement) << this << "closed"; + this->destroy(); + emit this->closed(); + delete this; +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) { + qCDebug(logToplevelManagement) << this << "got appid" << appId; + this->mAppId = appId; + emit this->appIdChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_title(const QString& title) { + qCDebug(logToplevelManagement) << this << "got toplevel" << title; + this->mTitle = title; + emit this->titleChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) { + auto activated = false; + auto maximized = false; + auto minimized = false; + auto fullscreen = false; + + // wl_array_for_each is illegal in C++ so it is manually expanded. + auto* state = static_cast<::zwlr_foreign_toplevel_handle_v1_state*>(stateArray->data); + auto size = stateArray->size / sizeof(::zwlr_foreign_toplevel_handle_v1_state); + for (size_t i = 0; i < size; i++) { + auto flag = state[i]; // NOLINT + switch (flag) { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: activated = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: maximized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: minimized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: fullscreen = true; break; + } + } + + qCDebug(logToplevelManagement) << this << "got state update - activated:" << activated + << "maximized:" << maximized << "minimized:" << minimized + << "fullscreen:" << fullscreen; + + if (activated != this->mActivated) { + this->mActivated = activated; + emit this->activatedChanged(); + } + + if (maximized != this->mMaximized) { + this->mMaximized = maximized; + emit this->maximizedChanged(); + } + + if (minimized != this->mMinimized) { + this->mMinimized = minimized; + emit this->minimizedChanged(); + } + + if (fullscreen != this->mFullscreen) { + this->mFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output enter" << screen; + + this->mVisibleScreens.push_back(screen); + emit this->visibleScreenAdded(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output leave" << screen; + + emit this->visibleScreenRemoved(screen); + this->mVisibleScreens.removeOne(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_parent( + ::zwlr_foreign_toplevel_handle_v1* parent +) { + auto* handle = ToplevelManager::instance()->handleFor(parent); + qCDebug(logToplevelManagement) << this << "got parent" << handle; + + if (handle != this->mParent) { + if (this->mParent != nullptr) { + QObject::disconnect(this->mParent, nullptr, this, nullptr); + } + + this->mParent = handle; + + if (handle != nullptr) { + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelHandle::onParentClosed); + } + + emit this->parentChanged(); + } +} + +void ToplevelHandle::onParentClosed() { + this->mParent = nullptr; + emit this->parentChanged(); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/handle.hpp b/src/wayland/toplevel_management/handle.hpp new file mode 100644 index 00000000..a49afe82 --- /dev/null +++ b/src/wayland/toplevel_management/handle.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle + : public QObject + , public QtWayland::zwlr_foreign_toplevel_handle_v1 { + Q_OBJECT; + +public: + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QVector visibleScreens() const; + [[nodiscard]] ToplevelHandle* parent() const; + [[nodiscard]] bool activated() const; + [[nodiscard]] bool maximized() const; + [[nodiscard]] bool minimized() const; + [[nodiscard]] bool fullscreen() const; + + void activate(); + void setMaximized(bool maximized); + void setMinimized(bool minimized); + void setFullscreen(bool fullscreen); + void fullscreenOn(QScreen* screen); + void setRectangle(QWindow* window, QRect rect); + +signals: + // sent after the first done event. + void ready(); + // sent right before delete this. + void closed(); + + void appIdChanged(); + void titleChanged(); + void visibleScreenAdded(QScreen* screen); + void visibleScreenRemoved(QScreen* screen); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onParentClosed(); + void onRectWindowDestroyed(); + +private: + void zwlr_foreign_toplevel_handle_v1_done() override; + void zwlr_foreign_toplevel_handle_v1_closed() override; + void zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) override; + void zwlr_foreign_toplevel_handle_v1_title(const QString& title) override; + void zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) override; + void zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_parent(::zwlr_foreign_toplevel_handle_v1* parent) override; + + bool isReady = false; + QString mAppId; + QString mTitle; + QVector mVisibleScreens; + ToplevelHandle* mParent = nullptr; + bool mActivated = false; + bool mMaximized = false; + bool mMinimized = false; + bool mFullscreen = false; + QWindow* rectWindow = nullptr; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.cpp b/src/wayland/toplevel_management/manager.cpp new file mode 100644 index 00000000..bd477b49 --- /dev/null +++ b/src/wayland/toplevel_management/manager.cpp @@ -0,0 +1,67 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include + +#include "handle.hpp" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +Q_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); + +ToplevelManager::ToplevelManager(): QWaylandClientExtensionTemplate(3) { this->initialize(); } + +bool ToplevelManager::available() const { return this->isActive(); } + +const QVector& ToplevelManager::readyToplevels() const { + return this->mReadyToplevels; +} + +ToplevelHandle* ToplevelManager::handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel) { + if (toplevel == nullptr) return nullptr; + + for (auto* other: this->mToplevels) { + if (other->object() == toplevel) return other; + } + + return nullptr; +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +void ToplevelManager::zwlr_foreign_toplevel_manager_v1_toplevel( + ::zwlr_foreign_toplevel_handle_v1* toplevel +) { + auto* handle = new ToplevelHandle(); + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelManager::onToplevelClosed); + QObject::connect(handle, &ToplevelHandle::ready, this, &ToplevelManager::onToplevelReady); + + qCDebug(logToplevelManagement) << "Toplevel handle created" << handle; + this->mToplevels.push_back(handle); + + // Not done in constructor as a close could technically be picked up immediately on init, + // making touching the handle a UAF. + handle->init(toplevel); +} + +void ToplevelManager::onToplevelReady() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.push_back(handle); + emit this->toplevelReady(handle); +} + +void ToplevelManager::onToplevelClosed() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.removeOne(handle); + this->mToplevels.removeOne(handle); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp new file mode 100644 index 00000000..41848de1 --- /dev/null +++ b/src/wayland/toplevel_management/manager.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle; + +Q_DECLARE_LOGGING_CATEGORY(logToplevelManagement); + +class ToplevelManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_foreign_toplevel_manager_v1 { + Q_OBJECT; + +public: + [[nodiscard]] bool available() const; + [[nodiscard]] const QVector& readyToplevels() const; + [[nodiscard]] ToplevelHandle* handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel); + + static ToplevelManager* instance(); + +signals: + void toplevelReady(ToplevelHandle* toplevel); + +protected: + explicit ToplevelManager(); + + void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel + ) override; + +private slots: + void onToplevelReady(); + void onToplevelClosed(); + +private: + QVector mToplevels; + QVector mReadyToplevels; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp new file mode 100644 index 00000000..2042262b --- /dev/null +++ b/src/wayland/toplevel_management/qml.cpp @@ -0,0 +1,153 @@ +#include "qml.hpp" + +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" +#include "../../core/windowinterface.hpp" +#include "handle.hpp" +#include "manager.hpp" + +namespace qs::wayland::toplevel_management { + +Toplevel::Toplevel(impl::ToplevelHandle* handle, QObject* parent): QObject(parent), handle(handle) { + // clang-format off + QObject::connect(handle, &impl::ToplevelHandle::closed, this, &Toplevel::onClosed); + QObject::connect(handle, &impl::ToplevelHandle::appIdChanged, this, &Toplevel::appIdChanged); + QObject::connect(handle, &impl::ToplevelHandle::titleChanged, this, &Toplevel::titleChanged); + QObject::connect(handle, &impl::ToplevelHandle::parentChanged, this, &Toplevel::parentChanged); + QObject::connect(handle, &impl::ToplevelHandle::activatedChanged, this, &Toplevel::activatedChanged); + QObject::connect(handle, &impl::ToplevelHandle::maximizedChanged, this, &Toplevel::maximizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::minimizedChanged, this, &Toplevel::minimizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::fullscreenChanged, this, &Toplevel::fullscreenChanged); + // clang-format on +} + +void Toplevel::onClosed() { + emit this->closed(); + delete this; +} + +void Toplevel::activate() { this->handle->activate(); } + +QString Toplevel::appId() const { return this->handle->appId(); } +QString Toplevel::title() const { return this->handle->title(); } + +Toplevel* Toplevel::parent() const { + return ToplevelManager::instance()->forImpl(this->handle->parent()); +} + +bool Toplevel::activated() const { return this->handle->activated(); } + +bool Toplevel::maximized() const { return this->handle->maximized(); } +void Toplevel::setMaximized(bool maximized) { this->handle->setMaximized(maximized); } + +bool Toplevel::minimized() const { return this->handle->minimized(); } +void Toplevel::setMinimized(bool minimized) { this->handle->setMinimized(minimized); } + +bool Toplevel::fullscreen() const { return this->handle->fullscreen(); } +void Toplevel::setFullscreen(bool fullscreen) { this->handle->setFullscreen(fullscreen); } + +void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { + auto* qscreen = screen != nullptr ? screen->screen : nullptr; + this->handle->fullscreenOn(qscreen); +} + +void Toplevel::setRectangle(QObject* window, QRect rect) { + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != this->rectWindow) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = proxyWindow; + + if (proxyWindow != nullptr) { + QObject::connect( + proxyWindow, + &QObject::destroyed, + this, + &Toplevel::onRectangleProxyDestroyed + ); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::windowConnected, + this, + &Toplevel::onRectangleProxyConnected + ); + } + } + + this->rectangle = rect; + this->handle->setRectangle(proxyWindow->backingWindow(), rect); +} + +void Toplevel::unsetRectangle() { this->setRectangle(nullptr, QRect()); } + +void Toplevel::onRectangleProxyConnected() { + this->handle->setRectangle(this->rectWindow->backingWindow(), this->rectangle); +} + +void Toplevel::onRectangleProxyDestroyed() { + this->rectWindow = nullptr; + this->rectangle = QRect(); +} + +ToplevelManager::ToplevelManager() { + auto* manager = impl::ToplevelManager::instance(); + + QObject::connect( + manager, + &impl::ToplevelManager::toplevelReady, + this, + &ToplevelManager::onToplevelReady + ); + + for (auto* handle: manager->readyToplevels()) { + this->onToplevelReady(handle); + } +} + +Toplevel* ToplevelManager::forImpl(impl::ToplevelHandle* impl) const { + if (impl == nullptr) return nullptr; + + for (auto* toplevel: this->mToplevels.valueList()) { + if (toplevel->handle == impl) return toplevel; + } + + return nullptr; +} + +ObjectModel* ToplevelManager::toplevels() { return &this->mToplevels; } + +void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { + auto* toplevel = new Toplevel(handle, this); + QObject::connect(toplevel, &Toplevel::closed, this, &ToplevelManager::onToplevelClosed); + this->mToplevels.insertObject(toplevel); +} + +void ToplevelManager::onToplevelClosed() { + auto* toplevel = qobject_cast(this->sender()); + this->mToplevels.removeObject(toplevel); +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +ObjectModel* ToplevelManagerQml::toplevels() { + return ToplevelManager::instance()->toplevels(); +} + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp new file mode 100644 index 00000000..8bb1d551 --- /dev/null +++ b/src/wayland/toplevel_management/qml.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" + +namespace qs::wayland::toplevel_management { + +namespace impl { +class ToplevelManager; +class ToplevelHandle; +} // namespace impl + +///! Window from another application. +/// A window/toplevel from another application, retrievable from +/// the [ToplevelManager](../toplevelmanager). +class Toplevel: public QObject { + Q_OBJECT; + Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); + Q_PROPERTY(QString title READ title NOTIFY titleChanged); + /// Parent toplevel if this toplevel is a modal/dialog, otherwise null. + Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); + /// If the window is currently activated or focused. + /// + /// Activation can be requested with the `activate()` function. + Q_PROPERTY(bool activated READ activated NOTIFY activatedChanged); + /// If the window is currently maximized. + /// + /// Maximization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool maximized READ maximized WRITE setMaximized NOTIFY maximizedChanged); + /// If the window is currently minimized. + /// + /// Minimization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool minimized READ minimized WRITE setMinimized NOTIFY minimizedChanged); + /// If the window is currently fullscreen. + /// + /// Fullscreen can be requested by setting this property, though it may + /// be ignored by the compositor. + /// Fullscreen can be requested on a specific screen with the `fullscreenOn()` function. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + QML_ELEMENT; + QML_UNCREATABLE("Toplevels must be acquired from the ToplevelManager."); + +public: + explicit Toplevel(impl::ToplevelHandle* handle, QObject* parent); + + /// Request that this toplevel is activated. + /// The request may be ignored by the compositor. + Q_INVOKABLE void activate(); + + /// Request that this toplevel is fullscreened on a specific screen. + /// The request may be ignored by the compositor. + Q_INVOKABLE void fullscreenOn(QuickshellScreenInfo* screen); + + /// Provide a hint to the compositor where the visual representation + /// of this toplevel is relative to a quickshell window. + /// This hint can be used visually in operations like minimization. + Q_INVOKABLE void setRectangle(QObject* window, QRect rect); + Q_INVOKABLE void unsetRectangle(); + + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] Toplevel* parent() const; + [[nodiscard]] bool activated() const; + + [[nodiscard]] bool maximized() const; + void setMaximized(bool maximized); + + [[nodiscard]] bool minimized() const; + void setMinimized(bool minimized); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + +signals: + void closed(); + void appIdChanged(); + void titleChanged(); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onClosed(); + void onRectangleProxyConnected(); + void onRectangleProxyDestroyed(); + +private: + impl::ToplevelHandle* handle; + ProxyWindowBase* rectWindow = nullptr; + QRect rectangle; + + friend class ToplevelManager; +}; + +class ToplevelManager: public QObject { + Q_OBJECT; + +public: + Toplevel* forImpl(impl::ToplevelHandle* impl) const; + + [[nodiscard]] ObjectModel* toplevels(); + + static ToplevelManager* instance(); + +private slots: + void onToplevelReady(impl::ToplevelHandle* handle); + void onToplevelClosed(); + +private: + explicit ToplevelManager(); + + ObjectModel mToplevels {this}; +}; + +///! Exposes a list of Toplevels. +/// Exposes a list of windows from other applications as [Toplevel](../toplevel)s via the +/// [zwlr-foreign-toplevel-management-v1](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1) +/// wayland protocol. +class ToplevelManagerQml: public QObject { + Q_OBJECT; + Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + QML_NAMED_ELEMENT(ToplevelManager); + QML_SINGLETON; + +public: + explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] static ObjectModel* toplevels(); +}; + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..44505bbb --- /dev/null +++ b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + From 67783ec24c61030abff8ba5008a5bbd2822c6eca Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 9 Jun 2024 15:42:38 -0700 Subject: [PATCH 31/31] core/transformwatcher: fix crash when a or b is destroyed Usually happens during reload. --- src/core/transformwatcher.cpp | 50 +++++++++++++++++++++++++++++++++-- src/core/transformwatcher.hpp | 3 +++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/core/transformwatcher.cpp b/src/core/transformwatcher.cpp index 697dfc56..2a33bad0 100644 --- a/src/core/transformwatcher.cpp +++ b/src/core/transformwatcher.cpp @@ -82,7 +82,10 @@ void TransformWatcher::linkItem(QQuickItem* item) const { QObject::connect(item, &QQuickItem::parentChanged, this, &TransformWatcher::recalcChains); QObject::connect(item, &QQuickItem::windowChanged, this, &TransformWatcher::recalcChains); - QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::recalcChains); + + if (item != this->mA && item != this->mB) { + QObject::connect(item, &QObject::destroyed, this, &TransformWatcher::itemDestroyed); + } } void TransformWatcher::linkChains() { @@ -103,6 +106,18 @@ void TransformWatcher::unlinkChains() { for (auto* item: this->childChain) { QObject::disconnect(item, nullptr, this, nullptr); } + + // relink a and b destruction notifications + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + + this->parentChain.clear(); + this->childChain.clear(); } void TransformWatcher::recalcChains() { @@ -111,26 +126,57 @@ void TransformWatcher::recalcChains() { this->linkChains(); } +void TransformWatcher::itemDestroyed() { + auto destroyed = + this->parentChain.removeOne(this->sender()) || this->childChain.removeOne(this->sender()); + + if (destroyed) this->recalcChains(); +} + QQuickItem* TransformWatcher::a() const { return this->mA; } void TransformWatcher::setA(QQuickItem* a) { if (this->mA == a) return; + if (this->mA != nullptr) QObject::disconnect(this->mA, nullptr, this, nullptr); this->mA = a; + + if (this->mA != nullptr) { + QObject::connect(this->mA, &QObject::destroyed, this, &TransformWatcher::aDestroyed); + } + this->recalcChains(); } +void TransformWatcher::aDestroyed() { + this->mA = nullptr; + this->unlinkChains(); + emit this->aChanged(); +} + QQuickItem* TransformWatcher::b() const { return this->mB; } void TransformWatcher::setB(QQuickItem* b) { if (this->mB == b) return; + if (this->mB != nullptr) QObject::disconnect(this->mB, nullptr, this, nullptr); this->mB = b; + + if (this->mB != nullptr) { + QObject::connect(this->mB, &QObject::destroyed, this, &TransformWatcher::bDestroyed); + } + this->recalcChains(); } +void TransformWatcher::bDestroyed() { + this->mB = nullptr; + this->unlinkChains(); + emit this->bChanged(); +} + QQuickItem* TransformWatcher::commonParent() const { return this->mCommonParent; } void TransformWatcher::setCommonParent(QQuickItem* commonParent) { if (this->mCommonParent == commonParent) return; this->mCommonParent = commonParent; - this->resolveChains(); + this->recalcChains(); } diff --git a/src/core/transformwatcher.hpp b/src/core/transformwatcher.hpp index d7174e4c..64bac4a1 100644 --- a/src/core/transformwatcher.hpp +++ b/src/core/transformwatcher.hpp @@ -60,6 +60,9 @@ signals: private slots: void recalcChains(); + void itemDestroyed(); + void aDestroyed(); + void bDestroyed(); private: void resolveChains(QQuickItem* a, QQuickItem* b, QQuickItem* commonParent);