From 3b6d1c3bd874619eb7e6186a03338ee38e7fa593 Mon Sep 17 00:00:00 2001 From: kossLAN Date: Sun, 19 May 2024 21:09:16 -0400 Subject: [PATCH 1/2] feat: mpris --- CMakeLists.txt | 4 +- src/services/CMakeLists.txt | 4 + src/services/mpris/CMakeLists.txt | 47 +++++ .../mpris/org.mpris.MediaPlayer2.Player.xml | 33 ++++ src/services/mpris/org.mpris.MprisWatcher.xml | 16 ++ src/services/mpris/player.cpp | 87 +++++++++ src/services/mpris/player.hpp | 67 +++++++ src/services/mpris/qml.cpp | 180 ++++++++++++++++++ src/services/mpris/qml.hpp | 113 +++++++++++ src/services/mpris/watcher.cpp | 146 ++++++++++++++ src/services/mpris/watcher.hpp | 53 ++++++ 11 files changed, 749 insertions(+), 1 deletion(-) create mode 100644 src/services/mpris/CMakeLists.txt create mode 100644 src/services/mpris/org.mpris.MediaPlayer2.Player.xml create mode 100644 src/services/mpris/org.mpris.MprisWatcher.xml create mode 100644 src/services/mpris/player.cpp create mode 100644 src/services/mpris/player.hpp create mode 100644 src/services/mpris/qml.cpp create mode 100644 src/services/mpris/qml.hpp create mode 100644 src/services/mpris/watcher.cpp create mode 100644 src/services/mpris/watcher.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bf20ab..2e5dffb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ option(HYPRLAND_GLOBAL_SHORTCUTS "Hyprland Global Shortcuts" ON) option(HYPRLAND_FOCUS_GRAB "Hyprland Focus Grabbing" ON) option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON) option(SERVICE_PIPEWIRE "PipeWire service" ON) +option(SERVICE_MPRIS "Mpris service" ON) message(STATUS "Quickshell configuration") message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}") @@ -34,6 +35,7 @@ message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") +message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") @@ -89,7 +91,7 @@ if (WAYLAND) list(APPEND QT_FPDEPS WaylandClient) endif() -if (SERVICE_STATUS_NOTIFIER) +if (SERVICE_STATUS_NOTIFIER OR SERVICE_MPRIS) set(DBUS ON) endif() diff --git a/src/services/CMakeLists.txt b/src/services/CMakeLists.txt index 091a7ec..4915762 100644 --- a/src/services/CMakeLists.txt +++ b/src/services/CMakeLists.txt @@ -5,3 +5,7 @@ endif() if (SERVICE_PIPEWIRE) add_subdirectory(pipewire) endif() + +if (SERVICE_MPRIS) + add_subdirectory(mpris) +endif() diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt new file mode 100644 index 0000000..ffe6d0a --- /dev/null +++ b/src/services/mpris/CMakeLists.txt @@ -0,0 +1,47 @@ +qt_add_dbus_adaptor(DBUS_INTERFACES + org.mpris.MprisWatcher.xml + watcher.hpp + qs::service::mp::MprisWatcher + dbus_watcher + MprisWatcherAdaptor +) + +set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES + CLASSNAME DBusMprisPlayer +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.mpris.MediaPlayer2.Player.xml + dbus_player +) + +set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES + CLASSNAME DBusMprisWatcher +) + +qt_add_dbus_interface(DBUS_INTERFACES + org.mpris.MprisWatcher.xml + dbus_watcher_interface +) + +qt_add_library(quickshell-service-mpris STATIC + qml.cpp + + watcher.cpp + player.cpp + ${DBUS_INTERFACES} +) + +# dbus headers +target_include_directories(quickshell-service-mpris PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +qt_add_qml_module(quickshell-service-mpris + URI Quickshell.Services.Mpris + VERSION 0.1 +) + +target_link_libraries(quickshell-service-mpris PRIVATE ${QT_DEPS} quickshell-dbus) +target_link_libraries(quickshell PRIVATE quickshell-service-mprisplugin) + +qs_pch(quickshell-service-mpris) +qs_pch(quickshell-service-mprisplugin) diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 0000000..846c539 --- /dev/null +++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/services/mpris/org.mpris.MprisWatcher.xml b/src/services/mpris/org.mpris.MprisWatcher.xml new file mode 100644 index 0000000..ab63155 --- /dev/null +++ b/src/services/mpris/org.mpris.MprisWatcher.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp new file mode 100644 index 0000000..b8676ba --- /dev/null +++ b/src/services/mpris/player.cpp @@ -0,0 +1,87 @@ +#include "player.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_player.h" + +using namespace qs::dbus; + +Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); + +namespace qs::service::mp { + +MprisPlayer::MprisPlayer(const QString& address, QObject* parent) + : QObject(parent) + , watcherId(address) { + // qDBusRegisterMetaType(); + // qDBusRegisterMetaType(); + // qDBusRegisterMetaType(); + // spec is unclear about what exactly an item address is, so account for both + auto splitIdx = address.indexOf('/'); + auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx); + auto path = splitIdx == -1 ? "/org/mpris/MediaPlayer2" : address.sliced(splitIdx); + + this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this); + + if (!this->player->isValid()) { + qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn; + return; + } + + // clang-format off + QObject::connect(&this->canControl, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->canPause, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->metadata, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->playbackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->position, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->minimumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->maximumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + + QObject::connect(&this->loopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->rate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->shuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->volume, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + + QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + // clang-format on + + this->properties.setInterface(this->player); + this->properties.updateAllViaGetAll(); +} + +bool MprisPlayer::isValid() const { return this->player->isValid(); } +bool MprisPlayer::isReady() const { return this->mReady; } + +void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT + this->player->SetPosition(trackId, position); +} +void MprisPlayer::next() { this->player->Next(); } +void MprisPlayer::previous() { this->player->Previous(); } +void MprisPlayer::pause() { this->player->Pause(); } +void MprisPlayer::playPause() { this->player->PlayPause(); } +void MprisPlayer::stop() { this->player->Stop(); } +void MprisPlayer::play() { this->player->Play(); } + +void MprisPlayer::onGetAllFinished() { + if (this->mReady) return; + this->mReady = true; + emit this->ready(); +} + +void MprisPlayer::updatePlayer() { // NOLINT + // TODO: emit signal here +} + +} // namespace qs::service::mp diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp new file mode 100644 index 0000000..168006a --- /dev/null +++ b/src/services/mpris/player.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "dbus_player.h" + +Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer); + +namespace qs::service::mp { + +class MprisPlayer; + +class MprisPlayer: public QObject { + Q_OBJECT; + +public: + explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + QString watcherId; // TODO: maybe can be private CHECK + + void setPosition(QDBusObjectPath trackId, qlonglong position); + void next(); + void previous(); + void pause(); + void playPause(); + void stop(); + void play(); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool isReady() const; + + // clang-format off + dbus::DBusPropertyGroup properties; + dbus::DBusProperty canControl {this->properties, "CanControl" }; + dbus::DBusProperty canGoNext {this->properties, "CanGoNext" }; + dbus::DBusProperty canGoPrevious {this->properties, "CanGoPrevious" }; + dbus::DBusProperty canPlay {this->properties, "CanPlay" }; + dbus::DBusProperty canPause {this->properties, "CanPause" }; + dbus::DBusProperty metadata {this->properties, "Metadata"}; + dbus::DBusProperty playbackStatus {this->properties, "PlaybackStatus" }; + dbus::DBusProperty position {this->properties, "Position" }; + dbus::DBusProperty minimumRate {this->properties, "MinimumRate" }; + dbus::DBusProperty maximumRate {this->properties, "MaximumRate" }; + + dbus::DBusProperty loopStatus {this->properties, "LoopStatus" }; + dbus::DBusProperty rate {this->properties, "Rate" }; + dbus::DBusProperty shuffle {this->properties, "Shuffle" }; + dbus::DBusProperty volume {this->properties, "Volume" }; + // clang-format on + +signals: + void ready(); + +private slots: + void onGetAllFinished(); + void updatePlayer(); + +private: + DBusMprisPlayer* player = nullptr; + bool mReady = false; +}; + +} // namespace qs::service::mp diff --git a/src/services/mpris/qml.cpp b/src/services/mpris/qml.cpp new file mode 100644 index 0000000..4e99a9f --- /dev/null +++ b/src/services/mpris/qml.cpp @@ -0,0 +1,180 @@ +#include "qml.hpp" + +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" +#include "player.hpp" +#include "watcher.hpp" + +using namespace qs::dbus; +using namespace qs::service::mp; + +Player::Player(qs::service::mp::MprisPlayer* player, QObject* parent) + : QObject(parent) + , player(player) { + + // clang-format off + QObject::connect(&this->player->canControl, &AbstractDBusProperty::changed, this, &Player::canControlChanged); + QObject::connect(&this->player->canGoNext, &AbstractDBusProperty::changed, this, &Player::canGoNextChanged); + QObject::connect(&this->player->canGoPrevious, &AbstractDBusProperty::changed, this, &Player::canGoPreviousChanged); + QObject::connect(&this->player->canPlay, &AbstractDBusProperty::changed, this, &Player::canPlayChanged); + QObject::connect(&this->player->canPause, &AbstractDBusProperty::changed, this, &Player::canPauseChanged); + QObject::connect(&this->player->metadata, &AbstractDBusProperty::changed, this, &Player::metadataChanged); + QObject::connect(&this->player->playbackStatus, &AbstractDBusProperty::changed, this, &Player::playbackStatusChanged); + QObject::connect(&this->player->position, &AbstractDBusProperty::changed, this, &Player::positionChanged); + QObject::connect(&this->player->minimumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); + QObject::connect(&this->player->maximumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); + + QObject::connect(&this->player->loopStatus, &AbstractDBusProperty::changed, this, &Player::loopStatusChanged); + QObject::connect(&this->player->rate, &AbstractDBusProperty::changed, this, &Player::rateChanged); + QObject::connect(&this->player->shuffle, &AbstractDBusProperty::changed, this, &Player::shuffleChanged); + QObject::connect(&this->player->volume, &AbstractDBusProperty::changed, this, &Player::volumeChanged); + // clang-format on +} + +bool Player::canControl() const { + if (this->player == nullptr) return false; + return this->player->canControl.get(); +} + +bool Player::canGoNext() const { + if (this->player == nullptr) return false; + return this->player->canGoNext.get(); +} + +bool Player::canGoPrevious() const { + if (this->player == nullptr) return false; + return this->player->canGoPrevious.get(); +} + +bool Player::canPlay() const { + if (this->player == nullptr) return false; + return this->player->canPlay.get(); +} + +bool Player::canPause() const { + if (this->player == nullptr) return false; + return this->player->canPause.get(); +} + +QVariantMap Player::metadata() const { + if (this->player == nullptr) return {}; + return this->player->metadata.get(); +} + +QString Player::playbackStatus() const { + if (this->player == nullptr) return ""; + + if (this->player->playbackStatus.get().isEmpty()) return "Unsupported"; + return this->player->playbackStatus.get(); +} + +qlonglong Player::position() const { + if (this->player == nullptr) return 0; + return this->player->position.get(); +} + +double Player::minimumRate() const { + if (this->player == nullptr) return 0.0; + return this->player->minimumRate.get(); +} + +double Player::maximumRate() const { + if (this->player == nullptr) return 0.0; + return this->player->maximumRate.get(); +} + +QString Player::loopStatus() const { + if (this->player == nullptr) return ""; + + if (this->player->loopStatus.get().isEmpty()) return "Unsupported"; + return this->player->loopStatus.get(); +} + +double Player::rate() const { + if (this->player == nullptr) return 0.0; + return this->player->rate.get(); +} + +bool Player::shuffle() const { + if (this->player == nullptr) return false; + return this->player->shuffle.get(); +} + +double Player::volume() const { + if (this->player == nullptr) return 0.0; + return this->player->volume.get(); +} + +// NOLINTBEGIN +void Player::setPosition(QDBusObjectPath trackId, qlonglong position) const { + this->player->setPosition(trackId, position); +} +void Player::next() const { this->player->next(); } +void Player::previous() const { this->player->previous(); } +void Player::pause() const { this->player->pause(); } +void Player::playPause() const { this->player->playPause(); } +void Player::stop() const { this->player->stop(); } +void Player::play() const { this->player->play(); } +// NOLINTEND + +Mpris::Mpris(QObject* parent): QObject(parent) { + auto* watcher = MprisWatcher::instance(); + + // clang-format off + QObject::connect(watcher, &MprisWatcher::MprisPlayerRegistered, this, &Mpris::onPlayerRegistered); + QObject::connect(watcher, &MprisWatcher::MprisPlayerUnregistered, this, &Mpris::onPlayerUnregistered); + // clang-format on + + for (QString& player: watcher->players) { + this->mPlayers.push_back(new Player(new MprisPlayer(player), this)); + } +} + +void Mpris::onPlayerRegistered(const QString& service) { + this->mPlayers.push_back(new Player(new MprisPlayer(service), this)); + emit this->playersChanged(); +} + +void Mpris::onPlayerUnregistered(const QString& service) { + Player* mprisPlayer = nullptr; + MprisPlayer* player = playerWithAddress(players(), service)->player; + + this->mPlayers.removeIf([player, &mprisPlayer](Player* testPlayer) { + if (testPlayer->player == player) { + mprisPlayer = testPlayer; + return true; + } else return false; + }); + + emit this->playersChanged(); + + delete mprisPlayer->player; + delete mprisPlayer; +} + +QQmlListProperty Mpris::players() { + return QQmlListProperty(this, nullptr, &Mpris::playersCount, &Mpris::playerAt); +} + +qsizetype Mpris::playersCount(QQmlListProperty* property) { + return reinterpret_cast(property->object)->mPlayers.count(); // NOLINT +} + +Player* Mpris::playerAt(QQmlListProperty* property, qsizetype index) { + return reinterpret_cast(property->object)->mPlayers.at(index); // NOLINT +} + +Player* Mpris::playerWithAddress(QQmlListProperty property, const QString& address) { + for (Player* player: reinterpret_cast(property.object)->mPlayers) { // NOLINT + if (player->player->watcherId == address) { + return player; + } + } + + return nullptr; +} diff --git a/src/services/mpris/qml.hpp b/src/services/mpris/qml.hpp new file mode 100644 index 0000000..4e7896b --- /dev/null +++ b/src/services/mpris/qml.hpp @@ -0,0 +1,113 @@ +#pragma once + +#include +#include +#include +#include + +#include "player.hpp" + + + +///! Mpris implementation for quickshell +/// mpris service, get useful information from apps that implement media player fucntionality [mpris spec] +/// (Beware of misuse of spec, it is just a suggestion for most) +/// +/// +/// [mpris spec]: https://specifications.freedesktop.org/mpris-spec +class Player: public QObject { + Q_OBJECT; + // READ-ONLY + Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); + Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); + Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); + Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); + Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged); + Q_PROPERTY(qlonglong position READ position NOTIFY positionChanged); + Q_PROPERTY(double minimumRate READ minimumRate NOTIFY minimumRateChanged); + Q_PROPERTY(double maximumRate READ maximumRate NOTIFY maximumRateChanged); + + // READ/WRITE - Write isn't implemented thus this need to fixed when that happens. + Q_PROPERTY(QString loopStatus READ loopStatus NOTIFY loopStatusChanged); + Q_PROPERTY(double rate READ rate NOTIFY rateChanged); + Q_PROPERTY(bool shuffle READ shuffle NOTIFY shuffleChanged); + Q_PROPERTY(double volume READ volume NOTIFY volumeChanged); + + QML_ELEMENT; + QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); + +public: + explicit Player(qs::service::mp::MprisPlayer* player, QObject* parent = nullptr); + + // These are all self-explanatory. + Q_INVOKABLE void setPosition(QDBusObjectPath trackId, qlonglong position) const; + Q_INVOKABLE void next() const; + Q_INVOKABLE void previous() const; + Q_INVOKABLE void pause() const; + Q_INVOKABLE void playPause() const; + Q_INVOKABLE void stop() const; + Q_INVOKABLE void play() const; + + [[nodiscard]] bool canControl() const; + [[nodiscard]] bool canGoNext() const; + [[nodiscard]] bool canGoPrevious() const; + [[nodiscard]] bool canPlay() const; + [[nodiscard]] bool canPause() const; + [[nodiscard]] QVariantMap metadata() const; + [[nodiscard]] QString playbackStatus() const; + [[nodiscard]] qlonglong position() const; + [[nodiscard]] double minimumRate() const; + [[nodiscard]] double maximumRate() const; + + [[nodiscard]] QString loopStatus() const; + [[nodiscard]] double rate() const; + [[nodiscard]] bool shuffle() const; + [[nodiscard]] double volume() const; + + qs::service::mp::MprisPlayer* player = nullptr; + +signals: + void canControlChanged(); + void canGoNextChanged(); + void canGoPreviousChanged(); + void canPlayChanged(); + void canPauseChanged(); + void metadataChanged(); + void playbackStatusChanged(); + void positionChanged(); + void minimumRateChanged(); + void maximumRateChanged(); + + void loopStatusChanged(); + void rateChanged(); + void shuffleChanged(); + void volumeChanged(); +}; + +class Mpris: public QObject { + Q_OBJECT; + Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); + QML_ELEMENT; + QML_SINGLETON; + +public: + explicit Mpris(QObject* parent = nullptr); + + [[nodiscard]] QQmlListProperty players(); + +signals: + void playersChanged(); + +private slots: + void onPlayerRegistered(const QString& service); + void onPlayerUnregistered(const QString& service); + +private: + static qsizetype playersCount(QQmlListProperty* property); + static Player* playerAt(QQmlListProperty* property, qsizetype index); + static Player* playerWithAddress(QQmlListProperty property, const QString& address); + + QList mPlayers; +}; diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp new file mode 100644 index 0000000..2a735f7 --- /dev/null +++ b/src/services/mpris/watcher.cpp @@ -0,0 +1,146 @@ +#include "watcher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg); + +namespace qs::service::mp { + +MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { + new MprisWatcherAdaptor(this); + + qCDebug(logMprisWatcher) << "Starting MprisWatcher"; + + auto bus = QDBusConnection::sessionBus(); + + if (!bus.isConnected()) { + qCWarning(logMprisWatcher) << "Could not connect to DBus. Mpris service will not work."; + return; + } + + if (!bus.registerObject("/MprisWatcher", this)) { + qCWarning(logMprisWatcher) << "Could not register MprisWatcher object with " + "DBus. Mpris service will not work."; + return; + } + + // clang-format off + QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered); + QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered); + + this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); + // clang-format on + + this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*"); + this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher"); + this->serviceWatcher.setConnection(bus); + + this->tryRegister(); +} + +void MprisWatcher::tryRegister() { // NOLINT + auto bus = QDBusConnection::sessionBus(); + auto success = bus.registerService("org.mpris.MprisWatcher"); + + if (success) { + qCDebug(logMprisWatcher) << "Registered watcher at org.mpris.MprisWatcher"; + emit this->MprisWatcherRegistered(); + registerExisting(bus); // Register services that already existed before creation. + } else { + qCDebug(logMprisWatcher) << "Could not register watcher at " + "org.mpris.MprisWatcher, presumably because one is " + "already registered."; + qCDebug(logMprisWatcher + ) << "Registration will be attempted again if the active service is unregistered."; + } +} + +void MprisWatcher::registerExisting(const QDBusConnection& connection) { + QStringList list = connection.interface()->registeredServiceNames(); + for (const QString& service: list) { + if (service.contains("org.mpris.MediaPlayer2")) { + qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service; + RegisterMprisPlayer(service); + } + } +} + +void MprisWatcher::onServiceRegistered(const QString& service) { + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "MprisWatcher"; + return; + } else if (service.contains("org.mpris.MediaPlayer2")) { + qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered."; + RegisterMprisPlayer(service); + } else { + qCWarning(logMprisWatcher) << "Got a registration event for a untracked service"; + } +} + +// TODO: This is getting triggered twice on unregistration, investigate. +void MprisWatcher::onServiceUnregistered(const QString& service) { + if (service == "org.mpris.MprisWatcher") { + qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration"; + this->tryRegister(); + return; + } else { + QString qualifiedPlayer; + this->players.removeIf([&](const QString& player) { + if (QString::compare(player, service) == 0) { + qualifiedPlayer = player; + return true; + } else return false; + }); + + if (!qualifiedPlayer.isEmpty()) { + qCDebug(logMprisWatcher).noquote() + << "Unregistered MprisPlayer" << qualifiedPlayer << "from watcher"; + + emit this->MprisPlayerUnregistered(qualifiedPlayer); + } else { + qCWarning(logMprisWatcher).noquote() + << "Got service unregister event for untracked service" << service; + } + } + + this->serviceWatcher.removeWatchedService(service); +} + +QList MprisWatcher::registeredPlayers() const { return this->players; } + +void MprisWatcher::RegisterMprisPlayer(const QString& player) { + if (this->players.contains(player)) { + qCDebug(logMprisWatcher).noquote() + << "Skipping duplicate registration of MprisPlayer" << player << "to watcher"; + return; + } + + if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) { + qCWarning(logMprisWatcher).noquote() + << "Ignoring invalid MprisPlayer registration of" << player << "to watcher"; + return; + } + + this->serviceWatcher.addWatchedService(player); + this->players.push_back(player); + + qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher"; + + emit this->MprisPlayerRegistered(player); +} + +MprisWatcher* MprisWatcher::instance() { + static MprisWatcher* instance = nullptr; // NOLINT + if (instance == nullptr) instance = new MprisWatcher(); + return instance; +} + +} // namespace qs::service::mp diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp new file mode 100644 index 0000000..b3e0bde --- /dev/null +++ b/src/services/mpris/watcher.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher); + +namespace qs::service::mp { + +class MprisWatcher + : public QObject + , protected QDBusContext { + Q_OBJECT; + Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); + Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers); + +public: + explicit MprisWatcher(QObject* parent = nullptr); + + void tryRegister(); + void registerExisting(const QDBusConnection &connection); + + [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT + [[nodiscard]] QList registeredPlayers() const; + + // NOLINTBEGIN + void RegisterMprisPlayer(const QString& player); + // NOLINTEND + + static MprisWatcher* instance(); + QList players; + +signals: + // NOLINTBEGIN + void MprisWatcherRegistered(); + void MprisPlayerRegistered(const QString& service); + void MprisPlayerUnregistered(const QString& service); + // NOLINTEND + +private slots: + void onServiceRegistered(const QString& service); + void onServiceUnregistered(const QString& service); + +private: + QDBusServiceWatcher serviceWatcher; +}; + +} // namespace qs::service::mp From 4ee9ac7f7caa79e08ab62df65c8fc980633371b1 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 21 May 2024 04:05:15 -0700 Subject: [PATCH 2/2] service/mpris: finish mpris implementation --- CMakeLists.txt | 2 +- src/dbus/dbusmenu/dbusmenu.hpp | 4 +- src/dbus/properties.cpp | 58 ++- src/dbus/properties.hpp | 37 +- src/services/mpris/CMakeLists.txt | 24 +- src/services/mpris/module.md | 7 + .../mpris/org.mpris.MediaPlayer2.Player.xml | 47 +- src/services/mpris/org.mpris.MediaPlayer2.xml | 6 + src/services/mpris/org.mpris.MprisWatcher.xml | 16 - src/services/mpris/player.cpp | 440 ++++++++++++++++-- src/services/mpris/player.hpp | 339 ++++++++++++-- src/services/mpris/qml.cpp | 180 ------- src/services/mpris/qml.hpp | 113 ----- src/services/mpris/watcher.cpp | 154 +++--- src/services/mpris/watcher.hpp | 54 +-- src/services/status_notifier/item.hpp | 8 +- 16 files changed, 911 insertions(+), 578 deletions(-) create mode 100644 src/services/mpris/module.md create mode 100644 src/services/mpris/org.mpris.MediaPlayer2.xml delete mode 100644 src/services/mpris/org.mpris.MprisWatcher.xml delete mode 100644 src/services/mpris/qml.cpp delete mode 100644 src/services/mpris/qml.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e5dffb..2d17758 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,7 @@ message(STATUS " X11: ${X11}") message(STATUS " Services") message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}") message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}") -message(STATUS " Mpris: ${SERVICE_MPRIS}") +message(STATUS " Mpris: ${SERVICE_MPRIS}") message(STATUS " Hyprland: ${HYPRLAND}") if (HYPRLAND) message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}") diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index b07919a..ab485c4 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -188,9 +188,9 @@ public: dbus::DBusPropertyGroup properties; dbus::DBusProperty version {this->properties, "Version"}; - dbus::DBusProperty textDirection {this->properties, "TextDirection"}; + dbus::DBusProperty textDirection {this->properties, "TextDirection", "", false}; dbus::DBusProperty status {this->properties, "Status"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", {}, false}; void prepareToShow(qint32 item, bool sendOpened); void updateLayout(qint32 parent, qint32 depth); diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 1e5e0bd..7dac84a 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -112,6 +112,8 @@ void asyncReadPropertyInternal( } void AbstractDBusProperty::tryUpdate(const QVariant& variant) { + this->mExists = true; + auto error = this->read(variant); if (error.isValid()) { qCWarning(logDbusProperties).noquote() @@ -159,6 +161,44 @@ void AbstractDBusProperty::update() { } } +void AbstractDBusProperty::write() { + if (this->group == nullptr) { + qFatal(logDbusProperties) << "Tried to write dbus property" << this->name + << "which is not attached to a group"; + } else { + const QString propStr = this->toString(); + + if (this->group->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to write property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; + + auto pendingCall = this->group->propertyInterface->Set( + this->group->interface->interface(), + this->name, + QDBusVariant(this->serialize()) + ); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + } +} + +bool AbstractDBusProperty::exists() const { return this->mExists; } + QString AbstractDBusProperty::toString() const { const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); return group + ':' + this->name; @@ -232,7 +272,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); - this->updatePropertySet(reply.value()); + this->updatePropertySet(reply.value(), true); } delete call; @@ -242,7 +282,7 @@ void DBusPropertyGroup::updateAllViaGetAll() { QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); } -void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { +void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { for (const auto [name, value]: properties.asKeyValueRange()) { auto prop = std::find_if( this->properties.begin(), @@ -251,11 +291,21 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties) { ); if (prop == this->properties.end()) { - qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this; + qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" + << this->toString(); } else { (*prop)->tryUpdate(value); } } + + if (complainMissing) { + for (const auto* prop: this->properties) { + if (prop->required && !properties.contains(prop->name)) { + qCWarning(logDbusProperties) + << prop->name << "missing from property set for" << this->toString(); + } + } + } } QString DBusPropertyGroup::toString() const { @@ -291,7 +341,7 @@ void DBusPropertyGroup::onPropertiesChanged( } } - this->updatePropertySet(changedProperties); + this->updatePropertySet(changedProperties, false); } } // namespace qs::dbus diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 3aac07f..e24d23f 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -79,22 +79,31 @@ class AbstractDBusProperty: public QObject { Q_OBJECT; public: - explicit AbstractDBusProperty(QString name, const QMetaType& type, QObject* parent = nullptr) + explicit AbstractDBusProperty( + QString name, + const QMetaType& type, + bool required, + QObject* parent = nullptr + ) : QObject(parent) , name(std::move(name)) - , type(type) {} + , type(type) + , required(required) {} + [[nodiscard]] bool exists() const; [[nodiscard]] QString toString() const; [[nodiscard]] virtual QString valueString() = 0; public slots: void update(); + void write(); signals: void changed(); protected: virtual QDBusError read(const QVariant& variant) = 0; + virtual QVariant serialize() = 0; private: void tryUpdate(const QVariant& variant); @@ -103,6 +112,8 @@ private: QString name; QMetaType type; + bool required; + bool mExists = false; friend class DBusPropertyGroup; }; @@ -133,7 +144,7 @@ private slots: ); private: - void updatePropertySet(const QVariantMap& properties); + void updatePropertySet(const QVariantMap& properties, bool complainMissing); DBusPropertiesInterface* propertyInterface = nullptr; QDBusAbstractInterface* interface = nullptr; @@ -145,17 +156,23 @@ private: template class DBusProperty: public AbstractDBusProperty { public: - explicit DBusProperty(QString name, QObject* parent = nullptr, T value = T()) - : AbstractDBusProperty(std::move(name), QMetaType::fromType(), parent) + explicit DBusProperty( + QString name, + T value = T(), + bool required = true, + QObject* parent = nullptr + ) + : AbstractDBusProperty(std::move(name), QMetaType::fromType(), required, parent) , value(std::move(value)) {} explicit DBusProperty( DBusPropertyGroup& group, QString name, - QObject* parent = nullptr, - T value = T() + T value = T(), + bool required = true, + QObject* parent = nullptr ) - : DBusProperty(std::move(name), parent, std::move(value)) { + : DBusProperty(std::move(name), std::move(value), required, parent) { group.attachProperty(this); } @@ -165,7 +182,7 @@ public: return str; } - [[nodiscard]] T get() const { return this->value; } + [[nodiscard]] const T& get() const { return this->value; } void set(T value) { this->value = std::move(value); @@ -183,6 +200,8 @@ protected: return result.error; } + QVariant serialize() override { return QVariant::fromValue(this->value); } + private: T value; diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index ffe6d0a..3ee9606 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -1,13 +1,6 @@ -qt_add_dbus_adaptor(DBUS_INTERFACES - org.mpris.MprisWatcher.xml - watcher.hpp - qs::service::mp::MprisWatcher - dbus_watcher - MprisWatcherAdaptor -) - set_source_files_properties(org.mpris.MediaPlayer2.Player.xml PROPERTIES - CLASSNAME DBusMprisPlayer + CLASSNAME DBusMprisPlayer + NO_NAMESPACE TRUE ) qt_add_dbus_interface(DBUS_INTERFACES @@ -15,20 +8,19 @@ qt_add_dbus_interface(DBUS_INTERFACES dbus_player ) -set_source_files_properties(org.mpris.MprisWatcher.xml PROPERTIES - CLASSNAME DBusMprisWatcher +set_source_files_properties(org.mpris.MediaPlayer2.xml PROPERTIES + CLASSNAME DBusMprisPlayerApp + NO_NAMESPACE TRUE ) qt_add_dbus_interface(DBUS_INTERFACES - org.mpris.MprisWatcher.xml - dbus_watcher_interface + org.mpris.MediaPlayer2.xml + dbus_player_app ) qt_add_library(quickshell-service-mpris STATIC - qml.cpp - - watcher.cpp player.cpp + watcher.cpp ${DBUS_INTERFACES} ) diff --git a/src/services/mpris/module.md b/src/services/mpris/module.md new file mode 100644 index 0000000..e2256e8 --- /dev/null +++ b/src/services/mpris/module.md @@ -0,0 +1,7 @@ +name = "Quickshell.Services.Mpris" +description = "Mpris Service" +headers = [ + "player.hpp", + "watcher.hpp", +] +----- diff --git a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml index 846c539..a009523 100644 --- a/src/services/mpris/org.mpris.MediaPlayer2.Player.xml +++ b/src/services/mpris/org.mpris.MediaPlayer2.Player.xml @@ -1,33 +1,24 @@ - - - - - - - - - - - - - - - - - - - - + + + - - + + + - - - - - - + + + + + + + + + + + + + diff --git a/src/services/mpris/org.mpris.MediaPlayer2.xml b/src/services/mpris/org.mpris.MediaPlayer2.xml new file mode 100644 index 0000000..fa880d0 --- /dev/null +++ b/src/services/mpris/org.mpris.MediaPlayer2.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/services/mpris/org.mpris.MprisWatcher.xml b/src/services/mpris/org.mpris.MprisWatcher.xml deleted file mode 100644 index ab63155..0000000 --- a/src/services/mpris/org.mpris.MprisWatcher.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index b8676ba..3b0c746 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -1,87 +1,427 @@ #include "player.hpp" +#include +#include +#include #include -#include #include #include -#include #include -#include #include +#include #include #include "../../dbus/properties.hpp" #include "dbus_player.h" +#include "dbus_player_app.h" using namespace qs::dbus; +namespace qs::service::mpris { + Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); -namespace qs::service::mp { +QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { + switch (status) { + case MprisPlaybackState::Stopped: return "Stopped"; + case MprisPlaybackState::Playing: return "Playing"; + case MprisPlaybackState::Paused: return "Paused"; + default: return "Unknown Status"; + } +} -MprisPlayer::MprisPlayer(const QString& address, QObject* parent) - : QObject(parent) - , watcherId(address) { - // qDBusRegisterMetaType(); - // qDBusRegisterMetaType(); - // qDBusRegisterMetaType(); - // spec is unclear about what exactly an item address is, so account for both - auto splitIdx = address.indexOf('/'); - auto conn = splitIdx == -1 ? address : address.sliced(0, splitIdx); - auto path = splitIdx == -1 ? "/org/mpris/MediaPlayer2" : address.sliced(splitIdx); +QString MprisLoopState::toString(MprisLoopState::Enum status) { + switch (status) { + case MprisLoopState::None: return "None"; + case MprisLoopState::Track: return "Track"; + case MprisLoopState::Playlist: return "Playlist"; + default: return "Unknown Status"; + } +} - this->player = new DBusMprisPlayer(conn, path, QDBusConnection::sessionBus(), this); +MprisPlayer::MprisPlayer(const QString& address, QObject* parent): QObject(parent) { + this->app = new DBusMprisPlayerApp( + address, + "/org/mpris/MediaPlayer2", + QDBusConnection::sessionBus(), + this + ); - if (!this->player->isValid()) { - qCWarning(logMprisPlayer).noquote() << "Cannot create MprisPlayer for" << conn; + this->player = + new DBusMprisPlayer(address, "/org/mpris/MediaPlayer2", QDBusConnection::sessionBus(), this); + + if (!this->player->isValid() || !this->app->isValid()) { + qCWarning(logMprisPlayer) << "Cannot create MprisPlayer for" << address; return; } // clang-format off - QObject::connect(&this->canControl, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->canPause, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->metadata, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->playbackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->position, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->minimumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->maximumRate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->pCanQuit, &AbstractDBusProperty::changed, this, &MprisPlayer::canQuitChanged); + 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->pFullscreen, &AbstractDBusProperty::changed, this, &MprisPlayer::fullscreenChanged); + QObject::connect(&this->pSupportedUriSchemes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedUriSchemesChanged); + QObject::connect(&this->pSupportedMimeTypes, &AbstractDBusProperty::changed, this, &MprisPlayer::supportedMimeTypesChanged); - QObject::connect(&this->loopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->rate, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->shuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); - QObject::connect(&this->volume, &AbstractDBusProperty::changed, this, &MprisPlayer::updatePlayer); + QObject::connect(&this->pCanControl, &AbstractDBusProperty::changed, this, &MprisPlayer::canControlChanged); + QObject::connect(&this->pCanSeek, &AbstractDBusProperty::changed, this, &MprisPlayer::canSeekChanged); + QObject::connect(&this->pCanGoNext, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoNextChanged); + QObject::connect(&this->pCanGoPrevious, &AbstractDBusProperty::changed, this, &MprisPlayer::canGoPreviousChanged); + QObject::connect(&this->pCanPlay, &AbstractDBusProperty::changed, this, &MprisPlayer::canPlayChanged); + QObject::connect(&this->pCanPause, &AbstractDBusProperty::changed, this, &MprisPlayer::canPauseChanged); + QObject::connect(&this->pPosition, &AbstractDBusProperty::changed, this, &MprisPlayer::onPositionChanged); + QObject::connect(this->player, &DBusMprisPlayer::Seeked, this, &MprisPlayer::onSeek); + QObject::connect(&this->pVolume, &AbstractDBusProperty::changed, this, &MprisPlayer::volumeChanged); + QObject::connect(&this->pMetadata, &AbstractDBusProperty::changed, this, &MprisPlayer::onMetadataChanged); + QObject::connect(&this->pPlaybackStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onPlaybackStatusChanged); + QObject::connect(&this->pLoopStatus, &AbstractDBusProperty::changed, this, &MprisPlayer::onLoopStatusChanged); + QObject::connect(&this->pRate, &AbstractDBusProperty::changed, this, &MprisPlayer::rateChanged); + QObject::connect(&this->pMinRate, &AbstractDBusProperty::changed, this, &MprisPlayer::minRateChanged); + QObject::connect(&this->pMaxRate, &AbstractDBusProperty::changed, this, &MprisPlayer::maxRateChanged); + QObject::connect(&this->pShuffle, &AbstractDBusProperty::changed, this, &MprisPlayer::shuffleChanged); - QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + QObject::connect(&this->playerProperties, &DBusPropertyGroup::getAllFinished, this, &MprisPlayer::onGetAllFinished); + + // Ensure user triggered position updates can update length. + QObject::connect(this, &MprisPlayer::positionChanged, this, &MprisPlayer::onExportedPositionChanged); // clang-format on - this->properties.setInterface(this->player); - this->properties.updateAllViaGetAll(); + this->appProperties.setInterface(this->app); + this->playerProperties.setInterface(this->player); + this->appProperties.updateAllViaGetAll(); + this->playerProperties.updateAllViaGetAll(); +} + +void MprisPlayer::raise() { + if (!this->canRaise()) { + qWarning() << "Cannot call raise() on" << this << "because canRaise is false."; + return; + } + + this->app->Raise(); +} + +void MprisPlayer::quit() { + if (!this->canQuit()) { + qWarning() << "Cannot call quit() on" << this << "because canQuit is false."; + return; + } + + this->app->Quit(); +} + +void MprisPlayer::openUri(const QString& uri) { this->player->OpenUri(uri); } + +void MprisPlayer::next() { + if (!this->canGoNext()) { + qWarning() << "Cannot call next() on" << this << "because canGoNext is false."; + return; + } + + this->player->Next(); +} + +void MprisPlayer::previous() { + if (!this->canGoPrevious()) { + qWarning() << "Cannot call previous() on" << this << "because canGoPrevious is false."; + return; + } + + this->player->Previous(); +} + +void MprisPlayer::seek(qreal offset) { + if (!this->canSeek()) { + qWarning() << "Cannot call seek() on" << this << "because canSeek is false."; + return; + } + + auto target = static_cast(offset * 1000) * 1000; + this->player->Seek(target); } bool MprisPlayer::isValid() const { return this->player->isValid(); } -bool MprisPlayer::isReady() const { return this->mReady; } +QString MprisPlayer::address() const { return this->player->service(); } -void MprisPlayer::setPosition(QDBusObjectPath trackId, qlonglong position) { // NOLINT - this->player->SetPosition(trackId, position); +bool MprisPlayer::canControl() const { return this->pCanControl.get(); } +bool MprisPlayer::canPlay() const { return this->canControl() && this->pCanPlay.get(); } +bool MprisPlayer::canPause() const { return this->canControl() && this->pCanPause.get(); } +bool MprisPlayer::canSeek() const { return this->canControl() && this->pCanSeek.get(); } +bool MprisPlayer::canGoNext() const { return this->canControl() && this->pCanGoNext.get(); } +bool MprisPlayer::canGoPrevious() const { return this->canControl() && this->pCanGoPrevious.get(); } +bool MprisPlayer::canQuit() const { return this->pCanQuit.get(); } +bool MprisPlayer::canRaise() const { return this->pCanRaise.get(); } +bool MprisPlayer::canSetFullscreen() const { return this->pCanSetFullscreen.get(); } + +QString MprisPlayer::identity() const { return this->pIdentity.get(); } + +qlonglong MprisPlayer::positionMs() const { + if (!this->positionSupported()) return 0; // unsupported + if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; + + auto paused = this->mPlaybackState == MprisPlaybackState::Paused; + auto time = paused ? this->pausedTime : QDateTime::currentDateTime(); + auto offset = time - this->lastPositionTimestamp; + auto rateMul = static_cast(this->pRate.get() * 1000); + offset = (offset * rateMul) / 1000; + + return (this->pPosition.get() / 1000) + offset.count(); } -void MprisPlayer::next() { this->player->Next(); } -void MprisPlayer::previous() { this->player->Previous(); } -void MprisPlayer::pause() { this->player->Pause(); } -void MprisPlayer::playPause() { this->player->PlayPause(); } -void MprisPlayer::stop() { this->player->Stop(); } -void MprisPlayer::play() { this->player->Play(); } + +qreal MprisPlayer::position() const { + if (!this->positionSupported()) return 0; // unsupported + if (this->mPlaybackState == MprisPlaybackState::Stopped) return 0; + + return static_cast(this->positionMs()) / 1000.0; // NOLINT +} + +bool MprisPlayer::positionSupported() const { return this->pPosition.exists(); } + +void MprisPlayer::setPosition(qreal position) { + if (this->pPosition.get() == -1) { + qWarning() << "Cannot set position of" << this << "because position is not supported."; + return; + } + + if (!this->canSeek()) { + qWarning() << "Cannot set position of" << this << "because canSeek is false."; + return; + } + + 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); + } +} + +void MprisPlayer::onPositionChanged() { + const bool firstChange = !this->lastPositionTimestamp.isValid(); + this->lastPositionTimestamp = QDateTime::currentDateTimeUtc(); + emit this->positionChanged(); + if (firstChange) emit this->positionSupportedChanged(); +} + +void MprisPlayer::onExportedPositionChanged() { + if (!this->lengthSupported()) emit this->lengthChanged(); +} + +void MprisPlayer::onSeek(qlonglong time) { this->pPosition.set(time); } + +qreal MprisPlayer::length() const { + if (this->mLength == -1) { + return this->position(); // unsupported + } else { + return static_cast(this->mLength / 1000) / 1000; // NOLINT + } +} + +bool MprisPlayer::lengthSupported() const { return this->mLength != -1; } + +qreal MprisPlayer::volume() const { return this->pVolume.get(); } +bool MprisPlayer::volumeSupported() const { return this->pVolume.exists(); } + +void MprisPlayer::setVolume(qreal volume) { + if (!this->canControl()) { + qWarning() << "Cannot set volume of" << this << "because canControl is false."; + return; + } + + if (!this->volumeSupported()) { + qWarning() << "Cannot set volume of" << this << "because volume is not supported."; + return; + } + + this->pVolume.set(volume); + this->pVolume.write(); +} + +QVariantMap MprisPlayer::metadata() const { return this->pMetadata.get(); } + +void MprisPlayer::onMetadataChanged() { + auto lengthVariant = this->pMetadata.get().value("mpris:length"); + qlonglong length = -1; + if (lengthVariant.isValid() && lengthVariant.canConvert()) { + length = lengthVariant.value(); + } + + if (length != this->mLength) { + this->mLength = length; + emit this->lengthChanged(); + } + + auto trackidVariant = this->pMetadata.get().value("mpris:trackid"); + if (trackidVariant.isValid() && trackidVariant.canConvert()) { + this->mTrackId = trackidVariant.value(); + this->onSeek(0); + } + + emit this->metadataChanged(); +} + +MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } + +void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { + if (playbackState == this->mPlaybackState) return; + + switch (playbackState) { + case MprisPlaybackState::Stopped: + if (!this->canControl()) { + qWarning() << "Cannot set playbackState of" << this + << "to Stopped because canControl is false."; + return; + } + + this->player->Stop(); + break; + case MprisPlaybackState::Playing: + if (!this->canPlay()) { + qWarning() << "Cannot set playbackState of" << this << "to Playing because canPlay is false."; + return; + } + + this->player->Play(); + break; + case MprisPlaybackState::Paused: + if (!this->canPause()) { + qWarning() << "Cannot set playbackState of" << this << "to Paused because canPause is false."; + return; + } + + this->player->Pause(); + break; + default: + qWarning() << "Cannot set playbackState of" << this << "to unknown value" << playbackState; + return; + } +} + +void MprisPlayer::onPlaybackStatusChanged() { + const auto& status = this->pPlaybackStatus.get(); + + if (status == "Playing") { + this->mPlaybackState = MprisPlaybackState::Playing; + } else if (status == "Paused") { + this->mPlaybackState = MprisPlaybackState::Paused; + } else if (status == "Stopped") { + this->mPlaybackState = MprisPlaybackState::Stopped; + } else { + this->mPlaybackState = MprisPlaybackState::Stopped; + qWarning() << "Received unexpected PlaybackStatus for" << this << status; + } + + emit this->playbackStateChanged(); +} + +MprisLoopState::Enum MprisPlayer::loopState() const { return this->mLoopState; } +bool MprisPlayer::loopSupported() const { return this->pLoopStatus.exists(); } + +void MprisPlayer::setLoopState(MprisLoopState::Enum loopState) { + if (!this->canControl()) { + qWarning() << "Cannot set loopState of" << this << "because canControl is false."; + return; + } + + if (!this->loopSupported()) { + qWarning() << "Cannot set loopState of" << this << "because loop state is not supported."; + return; + } + + if (loopState == this->mLoopState) return; + + QString loopStatusStr; + switch (loopState) { + case MprisLoopState::None: loopStatusStr = "None"; break; + case MprisLoopState::Track: loopStatusStr = "Track"; break; + case MprisLoopState::Playlist: loopStatusStr = "Playlist"; break; + default: + qWarning() << "Cannot set loopState of" << this << "to unknown value" << loopState; + return; + } + + this->pLoopStatus.set(loopStatusStr); + this->pLoopStatus.write(); +} + +void MprisPlayer::onLoopStatusChanged() { + const auto& status = this->pLoopStatus.get(); + + if (status == "None") { + this->mLoopState = MprisLoopState::None; + } else if (status == "Track") { + this->mLoopState = MprisLoopState::Track; + } else if (status == "Playlist") { + this->mLoopState = MprisLoopState::Playlist; + } else { + this->mLoopState = MprisLoopState::None; + qWarning() << "Received unexpected LoopStatus for" << this << status; + } + + emit this->loopStateChanged(); +} + +qreal MprisPlayer::rate() const { return this->pRate.get(); } +qreal MprisPlayer::minRate() const { return this->pMinRate.get(); } +qreal MprisPlayer::maxRate() const { return this->pMaxRate.get(); } + +void MprisPlayer::setRate(qreal rate) { + if (rate == this->pRate.get()) return; + + if (rate < this->pMinRate.get() || rate > this->pMaxRate.get()) { + qWarning() << "Cannot set rate for" << this << "to" << rate + << "which is outside of minRate and maxRate" << this->pMinRate.get() + << this->pMaxRate.get(); + return; + } + + this->pRate.set(rate); + this->pRate.write(); +} + +bool MprisPlayer::shuffle() const { return this->pShuffle.get(); } +bool MprisPlayer::shuffleSupported() const { return this->pShuffle.exists(); } + +void MprisPlayer::setShuffle(bool shuffle) { + if (!this->shuffleSupported()) { + qWarning() << "Cannot set shuffle for" << this << "because shuffle is not supported."; + return; + } + + if (!this->canControl()) { + qWarning() << "Cannot set shuffle state of" << this << "because canControl is false."; + return; + } + + this->pShuffle.set(shuffle); + this->pShuffle.write(); +} + +bool MprisPlayer::fullscreen() const { return this->pFullscreen.get(); } + +void MprisPlayer::setFullscreen(bool fullscreen) { + if (!this->canSetFullscreen()) { + qWarning() << "Cannot set fullscreen for" << this << "because canSetFullscreen is false."; + return; + } + + this->pFullscreen.set(fullscreen); + this->pFullscreen.write(); +} + +QList MprisPlayer::supportedUriSchemes() const { return this->pSupportedUriSchemes.get(); } +QList MprisPlayer::supportedMimeTypes() const { return this->pSupportedMimeTypes.get(); } void MprisPlayer::onGetAllFinished() { - if (this->mReady) return; - this->mReady = true; + if (this->volumeSupported()) emit this->volumeSupportedChanged(); + if (this->loopSupported()) emit this->loopSupportedChanged(); + if (this->shuffleSupported()) emit this->shuffleSupportedChanged(); emit this->ready(); } -void MprisPlayer::updatePlayer() { // NOLINT - // TODO: emit signal here -} - -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 168006a..0b18d78 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -1,67 +1,324 @@ #pragma once -#include -#include -#include +#include #include +#include +#include #include +#include "../../core/doc.hpp" #include "../../dbus/properties.hpp" #include "dbus_player.h" +#include "dbus_player_app.h" -Q_DECLARE_LOGGING_CATEGORY(logMprisPlayer); +namespace qs::service::mpris { -namespace qs::service::mp { - -class MprisPlayer; - -class MprisPlayer: public QObject { +class MprisPlaybackState: public QObject { Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; public: - explicit MprisPlayer(const QString& address, QObject* parent = nullptr); - QString watcherId; // TODO: maybe can be private CHECK + enum Enum { + Stopped = 0, + Playing = 1, + Paused = 2, + }; + Q_ENUM(Enum); - void setPosition(QDBusObjectPath trackId, qlonglong position); - void next(); - void previous(); - void pause(); - void playPause(); - void stop(); - void play(); + Q_INVOKABLE static QString toString(MprisPlaybackState::Enum status); +}; + +class MprisLoopState: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum { + None = 0, + Track = 1, + Playlist = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(MprisLoopState::Enum status); +}; + +///! A media player exposed over MPRIS. +/// A media player exposed over MPRIS. +/// +/// > [!WARNING] Support for various functionality and general compliance to +/// > the MPRIS specification varies wildly by player. +/// > Always check the associated `canXyz` and `xyzSupported` properties if available. +/// +/// > [!INFO] The TrackList and Playlist interfaces were not implemented as we could not +/// > find any media players using them to test against. +class MprisPlayer: public QObject { + Q_OBJECT; + // clang-format off + Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); + Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); + Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); + Q_PROPERTY(bool canSeek READ canSeek NOTIFY canSeekChanged); + Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); + Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); + Q_PROPERTY(bool canQuit READ canQuit NOTIFY canQuitChanged); + Q_PROPERTY(bool canRaise READ canRaise NOTIFY canRaiseChanged); + 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 current position in the playing track, as seconds, with millisecond precision, + /// or `0` if `positionSupported` is false. + /// + /// May only be written to if `canSeek` and `positionSupported` are true. + /// + /// > [!WARNING] To avoid excessive property updates wasting CPU while `position` is not + /// > actively monitored, `position` usually will not update reactively, unless a nonlinear + /// > change in position occurs, however reading it will always return the current position. + /// > + /// > If you want to actively monitor the position, the simplest way it to emit the `positionChanged` + /// > signal manually for the duration you are monitoring it, Using a [FrameAnimation] if you need + /// > the value to update smoothly, such as on a slider, or a [Timer] if not, as shown below. + /// > + /// > ```qml {filename="Using a FrameAnimation"} + /// > FrameAnimation { + /// > // only emit the signal when the position is actually changing. + /// > running: player.playbackState == MprisPlaybackState.Playing + /// > // emit the positionChanged signal every frame. + /// > onTriggered: player.positionChanged() + /// > } + /// > ``` + /// > + /// > ```qml {filename="Using a Timer"} + /// > Timer { + /// > // only emit the signal when the position is actually changing. + /// > running: player.playbackState == MprisPlaybackState.Playing + /// > // Make sure the position updates at least once per second. + /// > interval: 1000 + /// > repeat: true + /// > // emit the positionChanged signal every second. + /// > onTriggered: player.positionChanged() + /// > } + /// > ``` + /// + /// [FrameAnimation]: https://doc.qt.io/qt-6/qml-qtquick-frameanimation.html + /// [Timer]: https://doc.qt.io/qt-6/qml-qtqml-timer.html + Q_PROPERTY(qreal position READ position WRITE setPosition NOTIFY positionChanged); + Q_PROPERTY(bool positionSupported READ positionSupported NOTIFY positionSupportedChanged); + /// The length of the playing track, as seconds, with millisecond precision, + /// or the value of `position` if `lengthSupported` is false. + Q_PROPERTY(qreal length READ length NOTIFY lengthChanged); + Q_PROPERTY(bool lengthSupported READ lengthSupported NOTIFY lengthSupportedChanged); + /// The volume of the playing track from 0.0 to 1.0, or 1.0 if `volumeSupported` is false. + /// + /// May only be written to if `canControl` and `volumeSupported` are true. + Q_PROPERTY(qreal volume READ volume WRITE setVolume NOTIFY volumeChanged); + Q_PROPERTY(bool volumeSupported READ volumeSupported NOTIFY volumeSupportedChanged); + /// Metadata of the current track. + /// + /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). + /// Do not count on any of them actually being present. + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + /// The playback state of the media player. + /// + /// - If `canPlay` is false, you cannot assign the `Playing` state. + /// - If `canPause` is false, you cannot assign the `Paused` state. + /// - If `canControl` is false, you cannot assign the `Stopped` state. + /// (or any of the others, though their repsective properties will also be false) + Q_PROPERTY(MprisPlaybackState::Enum playbackState READ playbackState WRITE setPlaybackState NOTIFY playbackStateChanged); + /// The loop state of the media player, or `None` if `loopSupported` is false. + /// + /// May only be written to if `canControl` and `loopSupported` are true. + Q_PROPERTY(MprisLoopState::Enum loopState READ loopState WRITE setLoopState NOTIFY loopStateChanged); + Q_PROPERTY(bool loopSupported READ loopSupported NOTIFY loopSupportedChanged); + /// The speed the song is playing at, as a multiplier. + /// + /// Only values between `minRate` and `maxRate` (inclusive) may be written to the property. + /// Additionally, It is recommended that you only write common values such as `0.25`, `0.5`, `1.0`, `2.0` + /// to the property, as media players are free to ignore the value, and are more likely to + /// accept common ones. + Q_PROPERTY(qreal rate READ rate WRITE setRate NOTIFY rateChanged); + Q_PROPERTY(qreal minRate READ minRate NOTIFY minRateChanged); + Q_PROPERTY(qreal maxRate READ maxRate NOTIFY maxRateChanged); + /// If the play queue is currently being shuffled, or false if `shuffleSupported` is false. + /// + /// May only be written if `canControl` and `shuffleSupported` are true. + Q_PROPERTY(bool shuffle READ shuffle WRITE setShuffle NOTIFY shuffleChanged); + Q_PROPERTY(bool shuffleSupported READ shuffleSupported NOTIFY shuffleSupportedChanged); + /// If the player is currently shown in fullscreen. + /// + /// May only be written to if `canSetFullscreen` is true. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + /// Uri schemes supported by `openUri`. + Q_PROPERTY(QList supportedUriSchemes READ supportedUriSchemes NOTIFY supportedUriSchemesChanged); + /// Mime types supported by `openUri`. + Q_PROPERTY(QList supportedMimeTypes READ supportedMimeTypes NOTIFY supportedMimeTypesChanged); + // clang-format on + QML_ELEMENT; + QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); + +public: + explicit MprisPlayer(const QString& address, QObject* parent = nullptr); + + /// Bring the media player to the front of the window stack. + /// + /// May only be called if `canRaise` is true. + Q_INVOKABLE void raise(); + /// Quit the media player. + /// + /// May only be called if `canQuit` is true. + Q_INVOKABLE void quit(); + /// Open the given URI in the media player. + /// + /// Many players will silently ignore this, especially if the uri + /// does not match `supportedUriSchemes` and `supportedMimeTypes`. + Q_INVOKABLE void openUri(const QString& uri); + /// Play the next song. + /// + /// May only be called if `canGoNext` is true. + Q_INVOKABLE void next(); + /// Play the previous song, or go back to the beginning of the current one. + /// + /// May only be called if `canGoPrevious` is true. + Q_INVOKABLE void previous(); + /// Change `position` by an offset. + /// + /// Even if `positionSupported` is false and you cannot set `position`, + /// this function may work. + /// + /// May only be called if `canSeek` is true. + Q_INVOKABLE void seek(qreal offset); [[nodiscard]] bool isValid() const; - [[nodiscard]] bool isReady() const; - - // clang-format off - dbus::DBusPropertyGroup properties; - dbus::DBusProperty canControl {this->properties, "CanControl" }; - dbus::DBusProperty canGoNext {this->properties, "CanGoNext" }; - dbus::DBusProperty canGoPrevious {this->properties, "CanGoPrevious" }; - dbus::DBusProperty canPlay {this->properties, "CanPlay" }; - dbus::DBusProperty canPause {this->properties, "CanPause" }; - dbus::DBusProperty metadata {this->properties, "Metadata"}; - dbus::DBusProperty playbackStatus {this->properties, "PlaybackStatus" }; - dbus::DBusProperty position {this->properties, "Position" }; - dbus::DBusProperty minimumRate {this->properties, "MinimumRate" }; - dbus::DBusProperty maximumRate {this->properties, "MaximumRate" }; + [[nodiscard]] QString address() const; - dbus::DBusProperty loopStatus {this->properties, "LoopStatus" }; - dbus::DBusProperty rate {this->properties, "Rate" }; - dbus::DBusProperty shuffle {this->properties, "Shuffle" }; - dbus::DBusProperty volume {this->properties, "Volume" }; - // clang-format on + [[nodiscard]] bool canControl() const; + [[nodiscard]] bool canSeek() const; + [[nodiscard]] bool canGoNext() const; + [[nodiscard]] bool canGoPrevious() const; + [[nodiscard]] bool canPlay() const; + [[nodiscard]] bool canPause() const; + [[nodiscard]] bool canQuit() const; + [[nodiscard]] bool canRaise() const; + [[nodiscard]] bool canSetFullscreen() const; + + [[nodiscard]] QString identity() const; + + [[nodiscard]] qlonglong positionMs() const; + [[nodiscard]] qreal position() const; + [[nodiscard]] bool positionSupported() const; + void setPosition(qreal position); + + [[nodiscard]] qreal length() const; + [[nodiscard]] bool lengthSupported() const; + + [[nodiscard]] qreal volume() const; + [[nodiscard]] bool volumeSupported() const; + void setVolume(qreal volume); + + [[nodiscard]] QVariantMap metadata() const; + + [[nodiscard]] MprisPlaybackState::Enum playbackState() const; + void setPlaybackState(MprisPlaybackState::Enum playbackState); + + [[nodiscard]] MprisLoopState::Enum loopState() const; + [[nodiscard]] bool loopSupported() const; + void setLoopState(MprisLoopState::Enum loopState); + + [[nodiscard]] qreal rate() const; + [[nodiscard]] qreal minRate() const; + [[nodiscard]] qreal maxRate() const; + void setRate(qreal rate); + + [[nodiscard]] bool shuffle() const; + [[nodiscard]] bool shuffleSupported() const; + void setShuffle(bool shuffle); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + + [[nodiscard]] QList supportedUriSchemes() const; + [[nodiscard]] QList supportedMimeTypes() const; signals: - void ready(); + QSDOC_HIDE void ready(); + void canControlChanged(); + void canPlayChanged(); + void canPauseChanged(); + void canSeekChanged(); + void canGoNextChanged(); + void canGoPreviousChanged(); + void canQuitChanged(); + void canRaiseChanged(); + void canSetFullscreenChanged(); + void identityChanged(); + void positionChanged(); + void positionSupportedChanged(); + void lengthChanged(); + void lengthSupportedChanged(); + void volumeChanged(); + void volumeSupportedChanged(); + void metadataChanged(); + void playbackStateChanged(); + void loopStateChanged(); + void loopSupportedChanged(); + void rateChanged(); + void minRateChanged(); + void maxRateChanged(); + void shuffleChanged(); + void shuffleSupportedChanged(); + void fullscreenChanged(); + void supportedUriSchemesChanged(); + void supportedMimeTypesChanged(); private slots: void onGetAllFinished(); - void updatePlayer(); + void onPositionChanged(); + void onExportedPositionChanged(); + void onSeek(qlonglong time); + void onMetadataChanged(); + void onPlaybackStatusChanged(); + void onLoopStatusChanged(); private: + // clang-format off + dbus::DBusPropertyGroup appProperties; + dbus::DBusProperty pIdentity {this->appProperties, "Identity"}; + dbus::DBusProperty pCanQuit {this->appProperties, "CanQuit"}; + dbus::DBusProperty pCanRaise {this->appProperties, "CanRaise"}; + dbus::DBusProperty pFullscreen {this->appProperties, "Fullscreen", false, false}; + dbus::DBusProperty pCanSetFullscreen {this->appProperties, "CanSetFullscreen", false, false}; + dbus::DBusProperty> pSupportedUriSchemes {this->appProperties, "SupportedUriSchemes"}; + dbus::DBusProperty> pSupportedMimeTypes {this->appProperties, "SupportedMimeTypes"}; + + dbus::DBusPropertyGroup playerProperties; + dbus::DBusProperty pCanControl {this->playerProperties, "CanControl"}; + dbus::DBusProperty pCanPlay {this->playerProperties, "CanPlay"}; + dbus::DBusProperty pCanPause {this->playerProperties, "CanPause"}; + dbus::DBusProperty pCanSeek {this->playerProperties, "CanSeek"}; + dbus::DBusProperty pCanGoNext {this->playerProperties, "CanGoNext"}; + dbus::DBusProperty pCanGoPrevious {this->playerProperties, "CanGoPrevious"}; + dbus::DBusProperty pPosition {this->playerProperties, "Position", 0, false}; // "required" + dbus::DBusProperty pVolume {this->playerProperties, "Volume", 1, false}; // "required" + dbus::DBusProperty pMetadata {this->playerProperties, "Metadata"}; + dbus::DBusProperty pPlaybackStatus {this->playerProperties, "PlaybackStatus"}; + dbus::DBusProperty pLoopStatus {this->playerProperties, "LoopStatus", "", false}; + dbus::DBusProperty pRate {this->playerProperties, "Rate", 1, false}; // "required" + dbus::DBusProperty pMinRate {this->playerProperties, "MinimumRate", 1, false}; // "required" + dbus::DBusProperty pMaxRate {this->playerProperties, "MaximumRate", 1, false}; // "required" + dbus::DBusProperty pShuffle {this->playerProperties, "Shuffle", false, false}; + // clang-format on + + MprisPlaybackState::Enum mPlaybackState = MprisPlaybackState::Stopped; + MprisLoopState::Enum mLoopState = MprisLoopState::None; + QDateTime lastPositionTimestamp; + QDateTime pausedTime; + qlonglong mLength = -1; + + DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; - bool mReady = false; + QString mTrackId; }; -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/qml.cpp b/src/services/mpris/qml.cpp deleted file mode 100644 index 4e99a9f..0000000 --- a/src/services/mpris/qml.cpp +++ /dev/null @@ -1,180 +0,0 @@ -#include "qml.hpp" - -#include -#include -#include -#include -#include - -#include "../../dbus/properties.hpp" -#include "player.hpp" -#include "watcher.hpp" - -using namespace qs::dbus; -using namespace qs::service::mp; - -Player::Player(qs::service::mp::MprisPlayer* player, QObject* parent) - : QObject(parent) - , player(player) { - - // clang-format off - QObject::connect(&this->player->canControl, &AbstractDBusProperty::changed, this, &Player::canControlChanged); - QObject::connect(&this->player->canGoNext, &AbstractDBusProperty::changed, this, &Player::canGoNextChanged); - QObject::connect(&this->player->canGoPrevious, &AbstractDBusProperty::changed, this, &Player::canGoPreviousChanged); - QObject::connect(&this->player->canPlay, &AbstractDBusProperty::changed, this, &Player::canPlayChanged); - QObject::connect(&this->player->canPause, &AbstractDBusProperty::changed, this, &Player::canPauseChanged); - QObject::connect(&this->player->metadata, &AbstractDBusProperty::changed, this, &Player::metadataChanged); - QObject::connect(&this->player->playbackStatus, &AbstractDBusProperty::changed, this, &Player::playbackStatusChanged); - QObject::connect(&this->player->position, &AbstractDBusProperty::changed, this, &Player::positionChanged); - QObject::connect(&this->player->minimumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); - QObject::connect(&this->player->maximumRate, &AbstractDBusProperty::changed, this, &Player::positionChanged); - - QObject::connect(&this->player->loopStatus, &AbstractDBusProperty::changed, this, &Player::loopStatusChanged); - QObject::connect(&this->player->rate, &AbstractDBusProperty::changed, this, &Player::rateChanged); - QObject::connect(&this->player->shuffle, &AbstractDBusProperty::changed, this, &Player::shuffleChanged); - QObject::connect(&this->player->volume, &AbstractDBusProperty::changed, this, &Player::volumeChanged); - // clang-format on -} - -bool Player::canControl() const { - if (this->player == nullptr) return false; - return this->player->canControl.get(); -} - -bool Player::canGoNext() const { - if (this->player == nullptr) return false; - return this->player->canGoNext.get(); -} - -bool Player::canGoPrevious() const { - if (this->player == nullptr) return false; - return this->player->canGoPrevious.get(); -} - -bool Player::canPlay() const { - if (this->player == nullptr) return false; - return this->player->canPlay.get(); -} - -bool Player::canPause() const { - if (this->player == nullptr) return false; - return this->player->canPause.get(); -} - -QVariantMap Player::metadata() const { - if (this->player == nullptr) return {}; - return this->player->metadata.get(); -} - -QString Player::playbackStatus() const { - if (this->player == nullptr) return ""; - - if (this->player->playbackStatus.get().isEmpty()) return "Unsupported"; - return this->player->playbackStatus.get(); -} - -qlonglong Player::position() const { - if (this->player == nullptr) return 0; - return this->player->position.get(); -} - -double Player::minimumRate() const { - if (this->player == nullptr) return 0.0; - return this->player->minimumRate.get(); -} - -double Player::maximumRate() const { - if (this->player == nullptr) return 0.0; - return this->player->maximumRate.get(); -} - -QString Player::loopStatus() const { - if (this->player == nullptr) return ""; - - if (this->player->loopStatus.get().isEmpty()) return "Unsupported"; - return this->player->loopStatus.get(); -} - -double Player::rate() const { - if (this->player == nullptr) return 0.0; - return this->player->rate.get(); -} - -bool Player::shuffle() const { - if (this->player == nullptr) return false; - return this->player->shuffle.get(); -} - -double Player::volume() const { - if (this->player == nullptr) return 0.0; - return this->player->volume.get(); -} - -// NOLINTBEGIN -void Player::setPosition(QDBusObjectPath trackId, qlonglong position) const { - this->player->setPosition(trackId, position); -} -void Player::next() const { this->player->next(); } -void Player::previous() const { this->player->previous(); } -void Player::pause() const { this->player->pause(); } -void Player::playPause() const { this->player->playPause(); } -void Player::stop() const { this->player->stop(); } -void Player::play() const { this->player->play(); } -// NOLINTEND - -Mpris::Mpris(QObject* parent): QObject(parent) { - auto* watcher = MprisWatcher::instance(); - - // clang-format off - QObject::connect(watcher, &MprisWatcher::MprisPlayerRegistered, this, &Mpris::onPlayerRegistered); - QObject::connect(watcher, &MprisWatcher::MprisPlayerUnregistered, this, &Mpris::onPlayerUnregistered); - // clang-format on - - for (QString& player: watcher->players) { - this->mPlayers.push_back(new Player(new MprisPlayer(player), this)); - } -} - -void Mpris::onPlayerRegistered(const QString& service) { - this->mPlayers.push_back(new Player(new MprisPlayer(service), this)); - emit this->playersChanged(); -} - -void Mpris::onPlayerUnregistered(const QString& service) { - Player* mprisPlayer = nullptr; - MprisPlayer* player = playerWithAddress(players(), service)->player; - - this->mPlayers.removeIf([player, &mprisPlayer](Player* testPlayer) { - if (testPlayer->player == player) { - mprisPlayer = testPlayer; - return true; - } else return false; - }); - - emit this->playersChanged(); - - delete mprisPlayer->player; - delete mprisPlayer; -} - -QQmlListProperty Mpris::players() { - return QQmlListProperty(this, nullptr, &Mpris::playersCount, &Mpris::playerAt); -} - -qsizetype Mpris::playersCount(QQmlListProperty* property) { - return reinterpret_cast(property->object)->mPlayers.count(); // NOLINT -} - -Player* Mpris::playerAt(QQmlListProperty* property, qsizetype index) { - return reinterpret_cast(property->object)->mPlayers.at(index); // NOLINT -} - -Player* Mpris::playerWithAddress(QQmlListProperty property, const QString& address) { - for (Player* player: reinterpret_cast(property.object)->mPlayers) { // NOLINT - if (player->player->watcherId == address) { - return player; - } - } - - return nullptr; -} diff --git a/src/services/mpris/qml.hpp b/src/services/mpris/qml.hpp deleted file mode 100644 index 4e7896b..0000000 --- a/src/services/mpris/qml.hpp +++ /dev/null @@ -1,113 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "player.hpp" - - - -///! Mpris implementation for quickshell -/// mpris service, get useful information from apps that implement media player fucntionality [mpris spec] -/// (Beware of misuse of spec, it is just a suggestion for most) -/// -/// -/// [mpris spec]: https://specifications.freedesktop.org/mpris-spec -class Player: public QObject { - Q_OBJECT; - // READ-ONLY - Q_PROPERTY(bool canControl READ canControl NOTIFY canControlChanged); - Q_PROPERTY(bool canGoNext READ canGoNext NOTIFY canGoNextChanged); - Q_PROPERTY(bool canGoPrevious READ canGoPrevious NOTIFY canGoPreviousChanged); - Q_PROPERTY(bool canPlay READ canPlay NOTIFY canPlayChanged); - Q_PROPERTY(bool canPause READ canPause NOTIFY canPauseChanged); - Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); - Q_PROPERTY(QString playbackStatus READ playbackStatus NOTIFY playbackStatusChanged); - Q_PROPERTY(qlonglong position READ position NOTIFY positionChanged); - Q_PROPERTY(double minimumRate READ minimumRate NOTIFY minimumRateChanged); - Q_PROPERTY(double maximumRate READ maximumRate NOTIFY maximumRateChanged); - - // READ/WRITE - Write isn't implemented thus this need to fixed when that happens. - Q_PROPERTY(QString loopStatus READ loopStatus NOTIFY loopStatusChanged); - Q_PROPERTY(double rate READ rate NOTIFY rateChanged); - Q_PROPERTY(bool shuffle READ shuffle NOTIFY shuffleChanged); - Q_PROPERTY(double volume READ volume NOTIFY volumeChanged); - - QML_ELEMENT; - QML_UNCREATABLE("MprisPlayers can only be acquired from Mpris"); - -public: - explicit Player(qs::service::mp::MprisPlayer* player, QObject* parent = nullptr); - - // These are all self-explanatory. - Q_INVOKABLE void setPosition(QDBusObjectPath trackId, qlonglong position) const; - Q_INVOKABLE void next() const; - Q_INVOKABLE void previous() const; - Q_INVOKABLE void pause() const; - Q_INVOKABLE void playPause() const; - Q_INVOKABLE void stop() const; - Q_INVOKABLE void play() const; - - [[nodiscard]] bool canControl() const; - [[nodiscard]] bool canGoNext() const; - [[nodiscard]] bool canGoPrevious() const; - [[nodiscard]] bool canPlay() const; - [[nodiscard]] bool canPause() const; - [[nodiscard]] QVariantMap metadata() const; - [[nodiscard]] QString playbackStatus() const; - [[nodiscard]] qlonglong position() const; - [[nodiscard]] double minimumRate() const; - [[nodiscard]] double maximumRate() const; - - [[nodiscard]] QString loopStatus() const; - [[nodiscard]] double rate() const; - [[nodiscard]] bool shuffle() const; - [[nodiscard]] double volume() const; - - qs::service::mp::MprisPlayer* player = nullptr; - -signals: - void canControlChanged(); - void canGoNextChanged(); - void canGoPreviousChanged(); - void canPlayChanged(); - void canPauseChanged(); - void metadataChanged(); - void playbackStatusChanged(); - void positionChanged(); - void minimumRateChanged(); - void maximumRateChanged(); - - void loopStatusChanged(); - void rateChanged(); - void shuffleChanged(); - void volumeChanged(); -}; - -class Mpris: public QObject { - Q_OBJECT; - Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); - QML_ELEMENT; - QML_SINGLETON; - -public: - explicit Mpris(QObject* parent = nullptr); - - [[nodiscard]] QQmlListProperty players(); - -signals: - void playersChanged(); - -private slots: - void onPlayerRegistered(const QString& service); - void onPlayerUnregistered(const QString& service); - -private: - static qsizetype playersCount(QQmlListProperty* property); - static Player* playerAt(QQmlListProperty* property, qsizetype index); - static Player* playerWithAddress(QQmlListProperty property, const QString& address); - - QList mPlayers; -}; diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 2a735f7..1e10766 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -1,22 +1,24 @@ #include "watcher.hpp" -#include +#include #include #include #include #include #include #include -#include #include +#include +#include +#include -Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mp.watcher", QtWarningMsg); +#include "player.hpp" -namespace qs::service::mp { +namespace qs::service::mpris { + +Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { - new MprisWatcherAdaptor(this); - qCDebug(logMprisWatcher) << "Starting MprisWatcher"; auto bus = QDBusConnection::sessionBus(); @@ -26,121 +28,99 @@ MprisWatcher::MprisWatcher(QObject* parent): QObject(parent) { return; } - if (!bus.registerObject("/MprisWatcher", this)) { - qCWarning(logMprisWatcher) << "Could not register MprisWatcher object with " - "DBus. Mpris service will not work."; - return; - } - // clang-format off QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceRegistered, this, &MprisWatcher::onServiceRegistered); QObject::connect(&this->serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, &MprisWatcher::onServiceUnregistered); - - this->serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration); // clang-format on + this->serviceWatcher.setWatchMode( + QDBusServiceWatcher::WatchForUnregistration | QDBusServiceWatcher::WatchForRegistration + ); + this->serviceWatcher.addWatchedService("org.mpris.MediaPlayer2*"); - this->serviceWatcher.addWatchedService("org.mpris.MprisWatcher"); this->serviceWatcher.setConnection(bus); - this->tryRegister(); + this->registerExisting(); } -void MprisWatcher::tryRegister() { // NOLINT - auto bus = QDBusConnection::sessionBus(); - auto success = bus.registerService("org.mpris.MprisWatcher"); - - if (success) { - qCDebug(logMprisWatcher) << "Registered watcher at org.mpris.MprisWatcher"; - emit this->MprisWatcherRegistered(); - registerExisting(bus); // Register services that already existed before creation. - } else { - qCDebug(logMprisWatcher) << "Could not register watcher at " - "org.mpris.MprisWatcher, presumably because one is " - "already registered."; - qCDebug(logMprisWatcher - ) << "Registration will be attempted again if the active service is unregistered."; - } -} - -void MprisWatcher::registerExisting(const QDBusConnection& connection) { - QStringList list = connection.interface()->registeredServiceNames(); +void MprisWatcher::registerExisting() { + const QStringList& list = QDBusConnection::sessionBus().interface()->registeredServiceNames(); for (const QString& service: list) { - if (service.contains("org.mpris.MediaPlayer2")) { + if (service.startsWith("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Found Mpris service" << service; - RegisterMprisPlayer(service); + this->registerPlayer(service); } } } void MprisWatcher::onServiceRegistered(const QString& service) { - if (service == "org.mpris.MprisWatcher") { - qCDebug(logMprisWatcher) << "MprisWatcher"; - return; - } else if (service.contains("org.mpris.MediaPlayer2")) { + if (service.startsWith("org.mpris.MediaPlayer2")) { qCDebug(logMprisWatcher).noquote() << "Mpris service " << service << " registered."; - RegisterMprisPlayer(service); + this->registerPlayer(service); } else { - qCWarning(logMprisWatcher) << "Got a registration event for a untracked service"; + qCWarning(logMprisWatcher) << "Got a registration event for untracked service" << service; } } -// TODO: This is getting triggered twice on unregistration, investigate. void MprisWatcher::onServiceUnregistered(const QString& service) { - if (service == "org.mpris.MprisWatcher") { - qCDebug(logMprisWatcher) << "Active MprisWatcher unregistered, attempting registration"; - this->tryRegister(); - return; + if (auto* player = this->mPlayers.value(service)) { + player->deleteLater(); + this->mPlayers.remove(service); + qCDebug(logMprisWatcher) << "Unregistered MprisPlayer" << service; } else { - QString qualifiedPlayer; - this->players.removeIf([&](const QString& player) { - if (QString::compare(player, service) == 0) { - qualifiedPlayer = player; - return true; - } else return false; - }); - - if (!qualifiedPlayer.isEmpty()) { - qCDebug(logMprisWatcher).noquote() - << "Unregistered MprisPlayer" << qualifiedPlayer << "from watcher"; - - emit this->MprisPlayerUnregistered(qualifiedPlayer); - } else { - qCWarning(logMprisWatcher).noquote() - << "Got service unregister event for untracked service" << service; - } + qCWarning(logMprisWatcher) << "Got service unregister event for untracked service" << service; } - - this->serviceWatcher.removeWatchedService(service); } -QList MprisWatcher::registeredPlayers() const { return this->players; } +void MprisWatcher::onPlayerReady() { + auto* player = qobject_cast(this->sender()); + this->readyPlayers.push_back(player); + emit this->playersChanged(); +} -void MprisWatcher::RegisterMprisPlayer(const QString& player) { - if (this->players.contains(player)) { - qCDebug(logMprisWatcher).noquote() - << "Skipping duplicate registration of MprisPlayer" << player << "to watcher"; +void MprisWatcher::onPlayerDestroyed(QObject* object) { + auto* player = static_cast(object); // NOLINT + + if (this->readyPlayers.removeOne(player)) { + emit this->playersChanged(); + } +} + +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 +} + +void MprisWatcher::registerPlayer(const QString& address) { + if (this->mPlayers.contains(address)) { + qCDebug(logMprisWatcher) << "Skipping duplicate registration of MprisPlayer" << address; return; } - if (!QDBusConnection::sessionBus().interface()->serviceOwner(player).isValid()) { - qCWarning(logMprisWatcher).noquote() - << "Ignoring invalid MprisPlayer registration of" << player << "to watcher"; + auto* player = new MprisPlayer(address, this); + if (!player->isValid()) { + qCWarning(logMprisWatcher) << "Ignoring invalid MprisPlayer registration of" << address; + delete player; return; } - this->serviceWatcher.addWatchedService(player); - this->players.push_back(player); + this->mPlayers.insert(address, player); + QObject::connect(player, &MprisPlayer::ready, this, &MprisWatcher::onPlayerReady); + QObject::connect(player, &QObject::destroyed, this, &MprisWatcher::onPlayerDestroyed); - qCDebug(logMprisWatcher).noquote() << "Registered MprisPlayer" << player << "to watcher"; - - emit this->MprisPlayerRegistered(player); + qCDebug(logMprisWatcher) << "Registered MprisPlayer" << address; } -MprisWatcher* MprisWatcher::instance() { - static MprisWatcher* instance = nullptr; // NOLINT - if (instance == nullptr) instance = new MprisWatcher(); - return instance; -} - -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/mpris/watcher.hpp b/src/services/mpris/watcher.hpp index b3e0bde..a1e4df7 100644 --- a/src/services/mpris/watcher.hpp +++ b/src/services/mpris/watcher.hpp @@ -3,51 +3,51 @@ #include #include #include +#include #include #include #include +#include +#include +#include #include -Q_DECLARE_LOGGING_CATEGORY(logMprisWatcher); +#include "player.hpp" -namespace qs::service::mp { +namespace qs::service::mpris { -class MprisWatcher - : public QObject - , protected QDBusContext { +///! Provides access to MprisPlayers. +class MprisWatcher: public QObject { Q_OBJECT; - Q_PROPERTY(qint32 ProtocolVersion READ protocolVersion); - Q_PROPERTY(QList RegisteredMprisPlayers READ registeredPlayers); + QML_NAMED_ELEMENT(Mpris); + QML_SINGLETON; + /// All connected MPRIS players. + Q_PROPERTY(QQmlListProperty players READ players NOTIFY playersChanged); public: explicit MprisWatcher(QObject* parent = nullptr); - void tryRegister(); - void registerExisting(const QDBusConnection &connection); - - [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT - [[nodiscard]] QList registeredPlayers() const; - - // NOLINTBEGIN - void RegisterMprisPlayer(const QString& player); - // NOLINTEND - - static MprisWatcher* instance(); - QList players; + [[nodiscard]] QQmlListProperty players(); signals: - // NOLINTBEGIN - void MprisWatcherRegistered(); - void MprisPlayerRegistered(const QString& service); - void MprisPlayerUnregistered(const QString& service); - // NOLINTEND + void playersChanged(); private slots: - void onServiceRegistered(const QString& service); + void onServiceRegistered(const QString& service); void onServiceUnregistered(const QString& service); + void onPlayerReady(); + void onPlayerDestroyed(QObject* object); + +private: + static qsizetype playersCount(QQmlListProperty* property); + static MprisPlayer* playerAt(QQmlListProperty* property, qsizetype index); + + void registerExisting(); + void registerPlayer(const QString& address); -private: QDBusServiceWatcher serviceWatcher; + QHash mPlayers; + QList readyPlayers; }; -} // namespace qs::service::mp +} // namespace qs::service::mpris diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 9dbf02f..aa41190 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -54,14 +54,14 @@ public: dbus::DBusProperty status {this->properties, "Status"}; dbus::DBusProperty category {this->properties, "Category"}; dbus::DBusProperty windowId {this->properties, "WindowId"}; - dbus::DBusProperty iconThemePath {this->properties, "IconThemePath"}; - dbus::DBusProperty iconName {this->properties, "IconName"}; - dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap"}; + dbus::DBusProperty iconThemePath {this->properties, "IconThemePath", "", false}; + dbus::DBusProperty iconName {this->properties, "IconName", "", false}; // IconPixmap may be set + dbus::DBusProperty iconPixmaps {this->properties, "IconPixmap", {}, false}; // IconName may be set dbus::DBusProperty overlayIconName {this->properties, "OverlayIconName"}; dbus::DBusProperty overlayIconPixmaps {this->properties, "OverlayIconPixmap"}; dbus::DBusProperty attentionIconName {this->properties, "AttentionIconName"}; dbus::DBusProperty attentionIconPixmaps {this->properties, "AttentionIconPixmap"}; - dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName"}; + dbus::DBusProperty attentionMovieName {this->properties, "AttentionMovieName", "", false}; dbus::DBusProperty tooltip {this->properties, "ToolTip"}; dbus::DBusProperty isMenu {this->properties, "ItemIsMenu"}; dbus::DBusProperty menuPath {this->properties, "Menu"};