Compare commits

..

3 commits

5 changed files with 180 additions and 21 deletions

View file

@ -1,7 +1,9 @@
#pragma once #pragma once
#include <type_traits> #include <type_traits>
#include <qobject.h>
#include <qtclasshelpermacros.h> #include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
// NOLINTBEGIN // NOLINTBEGIN
#define DROP_EMIT(object, func) \ #define DROP_EMIT(object, func) \
@ -211,3 +213,34 @@ public:
GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); } GuardedEmitBlocker block() { return GuardedEmitBlocker(&this->blocked); }
}; };
template <auto member, auto destroyedSlot, auto changedSignal>
class SimpleObjectHandleOps {
using Traits = MemberPointerTraits<decltype(member)>;
public:
static bool setObject(Traits::Class* parent, Traits::Type value) {
if (value == parent->*member) return false;
if (parent->*member != nullptr) {
QObject::disconnect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
parent->*member = value;
if (value != nullptr) {
QObject::connect(parent->*member, &QObject::destroyed, parent, destroyedSlot);
}
if constexpr (changedSignal != nullptr) {
emit(parent->*changedSignal)();
}
return true;
}
};
template <auto member, auto destroyedSlot, auto changedSignal = nullptr>
bool setSimpleObjectHandle(auto* parent, auto* value) {
return SimpleObjectHandleOps<member, destroyedSlot, changedSignal>::setObject(parent, value);
}

View file

@ -2,9 +2,12 @@
#include <array> #include <array>
#include <cstring> #include <cstring>
#include <qjsondocument.h>
#include <qjsonobject.h>
#include <qlogging.h> #include <qlogging.h>
#include <qloggingcategory.h> #include <qloggingcategory.h>
#include <qobject.h> #include <qobject.h>
#include <qstringview.h>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <spa/utils/json.h> #include <spa/utils/json.h>
@ -63,7 +66,7 @@ void PwDefaultTracker::onMetadataProperty(const char* key, const char* type, con
} else return; } else return;
QString name; QString name;
if (strcmp(type, "Spa:String:JSON") == 0) { if (type != nullptr && value != nullptr && strcmp(type, "Spa:String:JSON") == 0) {
auto failed = true; auto failed = true;
auto iter = std::array<spa_json, 2>(); auto iter = std::array<spa_json, 2>();
spa_json_init(&iter[0], value, strlen(value)); spa_json_init(&iter[0], value, strlen(value));
@ -138,6 +141,70 @@ void PwDefaultTracker::onNodeDestroyed(QObject* node) {
} }
} }
void PwDefaultTracker::changeConfiguredSink(PwNode* node) {
if (node != nullptr) {
if (!node->isSink) {
qCCritical(logDefaults) << "Cannot change default sink to a node that is not a sink.";
return;
}
this->changeConfiguredSinkName(node->name);
} else {
this->changeConfiguredSinkName("");
}
}
void PwDefaultTracker::changeConfiguredSinkName(const QString& sink) {
if (sink == this->mDefaultConfiguredSinkName) return;
if (this->setConfiguredDefault("default.configured.audio.sink", sink)) {
this->mDefaultConfiguredSinkName = sink;
qCInfo(logDefaults) << "Set default configured sink to" << sink;
}
}
void PwDefaultTracker::changeConfiguredSource(PwNode* node) {
if (node != nullptr) {
if (node->isSink) {
qCCritical(logDefaults) << "Cannot change default source to a node that is not a source.";
return;
}
this->changeConfiguredSourceName(node->name);
} else {
this->changeConfiguredSourceName("");
}
}
void PwDefaultTracker::changeConfiguredSourceName(const QString& source) {
if (source == this->mDefaultConfiguredSourceName) return;
if (this->setConfiguredDefault("default.configured.audio.source", source)) {
this->mDefaultConfiguredSourceName = source;
qCInfo(logDefaults) << "Set default configured source to" << source;
}
}
bool PwDefaultTracker::setConfiguredDefault(const char* key, const QString& value) {
auto* meta = this->defaultsMetadata.object();
if (!meta || !meta->proxy()) {
qCCritical(logDefaults) << "Cannot set default node as metadata is not ready.";
return false;
}
if (value.isEmpty()) {
meta->setProperty(key, "Spa:String:JSON", nullptr);
} else {
// Spa json is a superset of json so we can avoid the awful spa json api when serializing.
auto json = QJsonDocument({{"name", value}}).toJson(QJsonDocument::Compact);
meta->setProperty(key, "Spa:String:JSON", json.toStdString().c_str());
}
return true;
}
void PwDefaultTracker::setDefaultSink(PwNode* node) { void PwDefaultTracker::setDefaultSink(PwNode* node) {
if (node == this->mDefaultSink) return; if (node == this->mDefaultSink) return;
qCInfo(logDefaults) << "Default sink changed to" << node; qCInfo(logDefaults) << "Default sink changed to" << node;

View file

@ -18,9 +18,13 @@ public:
[[nodiscard]] PwNode* defaultConfiguredSink() const; [[nodiscard]] PwNode* defaultConfiguredSink() const;
[[nodiscard]] const QString& defaultConfiguredSinkName() const; [[nodiscard]] const QString& defaultConfiguredSinkName() const;
void changeConfiguredSink(PwNode* node);
void changeConfiguredSinkName(const QString& sink);
[[nodiscard]] PwNode* defaultConfiguredSource() const; [[nodiscard]] PwNode* defaultConfiguredSource() const;
[[nodiscard]] const QString& defaultConfiguredSourceName() const; [[nodiscard]] const QString& defaultConfiguredSourceName() const;
void changeConfiguredSource(PwNode* node);
void changeConfiguredSourceName(const QString& source);
signals: signals:
void defaultSinkChanged(); void defaultSinkChanged();
@ -54,6 +58,8 @@ private:
void setDefaultConfiguredSource(PwNode* node); void setDefaultConfiguredSource(PwNode* node);
void setDefaultConfiguredSourceName(const QString& name); void setDefaultConfiguredSourceName(const QString& name);
bool setConfiguredDefault(const char* key, const QString& value);
PwRegistry* registry; PwRegistry* registry;
PwBindableRef<PwMetadata> defaultsMetadata; PwBindableRef<PwMetadata> defaultsMetadata;

View file

@ -143,11 +143,19 @@ PwNodeIface* Pipewire::defaultConfiguredAudioSink() const { // NOLINT
return PwNodeIface::instance(node); return PwNodeIface::instance(node);
} }
void Pipewire::setDefaultConfiguredAudioSink(PwNodeIface* node) {
PwConnection::instance()->defaults.changeConfiguredSink(node ? node->node() : nullptr);
}
PwNodeIface* Pipewire::defaultConfiguredAudioSource() const { // NOLINT PwNodeIface* Pipewire::defaultConfiguredAudioSource() const { // NOLINT
auto* node = PwConnection::instance()->defaults.defaultConfiguredSource(); auto* node = PwConnection::instance()->defaults.defaultConfiguredSource();
return PwNodeIface::instance(node); return PwNodeIface::instance(node);
} }
void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) {
PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr);
}
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
void PwNodeLinkTracker::setNode(PwNodeIface* node) { void PwNodeLinkTracker::setNode(PwNodeIface* node) {

View file

@ -52,36 +52,66 @@ private:
class Pipewire: public QObject { class Pipewire: public QObject {
Q_OBJECT; Q_OBJECT;
// clang-format off // clang-format off
/// All pipewire nodes. /// All nodes present in pipewire.
///
/// This list contains every node on the system.
/// To find a useful subset, filtering with the following properties may be helpful:
/// - @@PwNode.isStream - if the node is an application or hardware device.
/// - @@PwNode.isSink - if the node is a sink or source.
/// - @@PwNode.audio - if non null the node is an audio node.
Q_PROPERTY(ObjectModel<PwNodeIface>* nodes READ nodes CONSTANT); Q_PROPERTY(ObjectModel<PwNodeIface>* nodes READ nodes CONSTANT);
/// All pipewire links. /// All links present in pipewire.
///
/// Links connect pipewire nodes to each other, and can be used to determine
/// their relationship.
///
/// If you already have a node you want to check for connections to,
/// use @@PwNodeLinkTracker instead of filtering this list.
///
/// > [!INFO] Multiple links may exist between the same nodes. See @@linkGroups
/// > for a deduplicated list containing only one entry per link between nodes.
Q_PROPERTY(ObjectModel<PwLinkIface>* links READ links CONSTANT); Q_PROPERTY(ObjectModel<PwLinkIface>* links READ links CONSTANT);
/// All pipewire link groups. /// All link groups present in pipewire.
///
/// The same as @@links but deduplicated.
///
/// If you already have a node you want to check for connections to,
/// use @@PwNodeLinkTracker instead of filtering this list.
Q_PROPERTY(ObjectModel<PwLinkGroupIface>* linkGroups READ linkGroups CONSTANT); Q_PROPERTY(ObjectModel<PwLinkGroupIface>* linkGroups READ linkGroups CONSTANT);
/// The default audio sink (output) or `null`. /// The default audio sink (output) or `null`.
/// ///
/// This is the default sink currently in use by pipewire, and the one applications
/// are currently using.
///
/// To set the default sink, use @@preferredDefaultAudioSink.
///
/// > [!INFO] When the default sink changes, this property may breifly become null. /// > [!INFO] When the default sink changes, this property may breifly become null.
/// > This depends on your hardware. /// > This depends on your hardware.
Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged); Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
/// The default audio source (input) or `null`. /// The default audio source (input) or `null`.
/// ///
/// This is the default source currently in use by pipewire, and the one applications
/// are currently using.
///
/// To set the default source, use @@preferredDefaultAudioSource.
///
/// > [!INFO] When the default source changes, this property may breifly become null. /// > [!INFO] When the default source changes, this property may breifly become null.
/// > This depends on your hardware. /// > This depends on your hardware.
Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged); Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged);
/// The configured default audio sink (output) or `null`. /// The preferred default audio sink (output) or `null`.
/// ///
/// This is not the same as @@defaultAudioSink. While @@defaultAudioSink is the /// This is a hint to pipewire telling it which sink should be the default when possible.
/// sink that will be used by applications, @@defaultConfiguredAudioSink is the /// @@defaultAudioSink may differ when it is not possible for pipewire to pick this node.
/// sink requested to be the default by quickshell or another configuration tool,
/// which might not exist or be valid.
Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSink READ defaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged);
/// The configured default audio source (input) or `null`.
/// ///
/// This is not the same as @@defaultAudioSource. While @@defaultAudioSource is the /// See @@defaultAudioSink for the current default sink, regardless of preference.
/// source that will be used by applications, @@defaultConfiguredAudioSource is the Q_PROPERTY(PwNodeIface* preferredDefaultAudioSink READ defaultConfiguredAudioSink WRITE setDefaultConfiguredAudioSink NOTIFY defaultConfiguredAudioSinkChanged);
/// source requested to be the default by quickshell or another configuration tool, /// The preferred default audio source (input) or `null`.
/// which might not exist or be valid. ///
Q_PROPERTY(PwNodeIface* defaultConfiguredAudioSource READ defaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); /// This is a hint to pipewire telling it which source should be the default when possible.
/// @@defaultAudioSource may differ when it is not possible for pipewire to pick this node.
///
/// See @@defaultAudioSource for the current default source, regardless of preference.
Q_PROPERTY(PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged);
// clang-format on // clang-format on
QML_ELEMENT; QML_ELEMENT;
QML_SINGLETON; QML_SINGLETON;
@ -97,7 +127,10 @@ public:
[[nodiscard]] PwNodeIface* defaultAudioSource() const; [[nodiscard]] PwNodeIface* defaultAudioSource() const;
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSink() const; [[nodiscard]] PwNodeIface* defaultConfiguredAudioSink() const;
static void setDefaultConfiguredAudioSink(PwNodeIface* node);
[[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const;
static void setDefaultConfiguredAudioSource(PwNodeIface* node);
signals: signals:
void defaultAudioSinkChanged(); void defaultAudioSinkChanged();
@ -218,7 +251,7 @@ class PwNodeIface: public PwObjectIface {
Q_OBJECT; Q_OBJECT;
/// The pipewire object id of the node. /// The pipewire object id of the node.
/// ///
/// Mainly useful for debugging. you can inspect the node directly /// Mainly useful for debugging. You can inspect the node directly
/// with `pw-cli i <id>`. /// with `pw-cli i <id>`.
Q_PROPERTY(quint32 id READ id CONSTANT); Q_PROPERTY(quint32 id READ id CONSTANT);
/// The node's name, corrosponding to the object's `node.name` property. /// The node's name, corrosponding to the object's `node.name` property.
@ -234,7 +267,8 @@ class PwNodeIface: public PwObjectIface {
/// If `true`, then the node accepts audio input from other nodes, /// If `true`, then the node accepts audio input from other nodes,
/// if `false` the node outputs audio to other nodes. /// if `false` the node outputs audio to other nodes.
Q_PROPERTY(bool isSink READ isSink CONSTANT); Q_PROPERTY(bool isSink READ isSink CONSTANT);
/// If `true` then the node is likely to be a program, if false it is liekly to be hardware. /// If `true` then the node is likely to be a program, if `false` it is likely to be
/// a hardware device.
Q_PROPERTY(bool isStream READ isStream CONSTANT); Q_PROPERTY(bool isStream READ isStream CONSTANT);
/// The property set present on the node, as an object containing key-value pairs. /// The property set present on the node, as an object containing key-value pairs.
/// You can inspect this directly with `pw-cli i <id>`. /// You can inspect this directly with `pw-cli i <id>`.
@ -250,6 +284,9 @@ class PwNodeIface: public PwObjectIface {
/// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker). /// > [!WARNING] This property is invalid unless the node is [bound](../pwobjecttracker).
Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged); Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged);
/// Extra information present only if the node sends or receives audio. /// Extra information present only if the node sends or receives audio.
///
/// The presence or absence of this property can be used to determine if a node
/// manages audio, regardless of if it is bound. If non null, the node is an audio node.
Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT); Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT);
QML_NAMED_ELEMENT(PwNode); QML_NAMED_ELEMENT(PwNode);
QML_UNCREATABLE("PwNodes cannot be created directly"); QML_UNCREATABLE("PwNodes cannot be created directly");
@ -357,11 +394,19 @@ private:
}; };
///! Binds pipewire objects. ///! Binds pipewire objects.
/// If the object list of at least one PwObjectTracker contains a given pipewire object, /// PwObjectTracker binds every node given in its @@objects list.
/// it will become *bound* and you will be able to interact with bound-only properties. ///
/// #### Object Binding
/// By default, pipewire objects are unbound. Unbound objects only have a subset of
/// information available for use or modification. **Binding an object makes all of its
/// properties available for use or modification if applicable.**
///
/// Properties that require their object be bound to use are clearly marked. You do not
/// need to bind the object unless mentioned in the description of the property you
/// want to use.
class PwObjectTracker: public QObject { class PwObjectTracker: public QObject {
Q_OBJECT; Q_OBJECT;
/// The list of objects to bind. /// The list of objects to bind. May contain nulls.
Q_PROPERTY(QList<QObject*> objects READ objects WRITE setObjects NOTIFY objectsChanged); Q_PROPERTY(QList<QObject*> objects READ objects WRITE setObjects NOTIFY objectsChanged);
QML_ELEMENT; QML_ELEMENT;