diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b40b807..88c2624 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 b619b0a..e1f2ee4 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 0000000..74c7c28 --- /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 0000000..bcf5ab6 --- /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 8eb9b63..dc1f204 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 1e10766..8c67a4d 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 a1e4df7..91275c7 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 a6617d2..b40de68 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 9b45272..8d45641 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 cea5646..f81a638 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 01f6bb0..e55509d 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.