forked from quickshell/quickshell
Compare commits
1 commit
master
...
hyprland-f
Author | SHA1 | Date | |
---|---|---|---|
outfoxxed | 6e9bb4183c |
|
@ -36,7 +36,6 @@ Checks: >
|
||||||
-readability-braces-around-statements,
|
-readability-braces-around-statements,
|
||||||
-readability-redundant-access-specifiers,
|
-readability-redundant-access-specifiers,
|
||||||
-readability-else-after-return,
|
-readability-else-after-return,
|
||||||
-readability-container-data-pointer,
|
|
||||||
tidyfox-*,
|
tidyfox-*,
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
performance-for-range-copy.WarnOnAllAutoCopies: true
|
performance-for-range-copy.WarnOnAllAutoCopies: true
|
||||||
|
|
|
@ -15,10 +15,7 @@ option(WAYLAND "Enable wayland support" ON)
|
||||||
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
|
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
|
||||||
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
|
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
|
||||||
option(HYPRLAND "Support hyprland specific features" ON)
|
option(HYPRLAND "Support hyprland specific features" ON)
|
||||||
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_STATUS_NOTIFIER "StatusNotifierItem service" ON)
|
||||||
option(SERVICE_PIPEWIRE "PipeWire service" ON)
|
|
||||||
|
|
||||||
message(STATUS "Quickshell configuration")
|
message(STATUS "Quickshell configuration")
|
||||||
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
message(STATUS " NVIDIA workarounds: ${NVIDIA_COMPAT}")
|
||||||
|
@ -31,12 +28,7 @@ if (WAYLAND)
|
||||||
endif ()
|
endif ()
|
||||||
message(STATUS " Services")
|
message(STATUS " Services")
|
||||||
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
|
||||||
message(STATUS " PipeWire: ${SERVICE_PIPEWIRE}")
|
|
||||||
message(STATUS " Hyprland: ${HYPRLAND}")
|
message(STATUS " Hyprland: ${HYPRLAND}")
|
||||||
if (HYPRLAND)
|
|
||||||
message(STATUS " Focus Grabbing: ${HYPRLAND_FOCUS_GRAB}")
|
|
||||||
message(STATUS " Global Shortcuts: ${HYPRLAND_GLOBAL_SHORTCUTS}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (NOT DEFINED GIT_REVISION)
|
if (NOT DEFINED GIT_REVISION)
|
||||||
execute_process(
|
execute_process(
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
|
|
||||||
debug ? false,
|
debug ? false,
|
||||||
enableWayland ? true,
|
enableWayland ? true,
|
||||||
enablePipewire ? true,
|
|
||||||
nvidiaCompat ? false,
|
nvidiaCompat ? false,
|
||||||
svgSupport ? true, # you almost always want this
|
svgSupport ? true, # you almost always want this
|
||||||
}: buildStdenv.mkDerivation {
|
}: buildStdenv.mkDerivation {
|
||||||
|
@ -47,8 +46,7 @@
|
||||||
qt6.qtdeclarative
|
qt6.qtdeclarative
|
||||||
]
|
]
|
||||||
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
++ (lib.optionals enableWayland [ qt6.qtwayland wayland ])
|
||||||
++ (lib.optionals svgSupport [ qt6.qtsvg ])
|
++ (lib.optionals svgSupport [ qt6.qtsvg ]);
|
||||||
++ (lib.optionals enablePipewire [ pipewire ]);
|
|
||||||
|
|
||||||
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
|
QTWAYLANDSCANNER = lib.optionalString enableWayland "${qt6.qtwayland}/libexec/qtwaylandscanner";
|
||||||
|
|
||||||
|
@ -64,8 +62,7 @@
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
"-DGIT_REVISION=${gitRev}"
|
"-DGIT_REVISION=${gitRev}"
|
||||||
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
|
] ++ lib.optional (!enableWayland) "-DWAYLAND=OFF"
|
||||||
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON"
|
++ lib.optional nvidiaCompat "-DNVIDIA_COMPAT=ON";
|
||||||
++ lib.optional (!enablePipewire) "-DSERVICE_PIPEWIRE=OFF";
|
|
||||||
|
|
||||||
buildPhase = "ninjaBuildPhase";
|
buildPhase = "ninjaBuildPhase";
|
||||||
enableParallelBuilding = true;
|
enableParallelBuilding = true;
|
||||||
|
|
2
docs
2
docs
|
@ -1 +1 @@
|
||||||
Subproject commit ff5da84a8b258a9b2caaf978ddb6de23635d8903
|
Subproject commit 149b784a5a4c40ada67cb9f6af5a5350678ab6d4
|
|
@ -1,7 +1 @@
|
||||||
if (SERVICE_STATUS_NOTIFIER)
|
add_subdirectory(status_notifier)
|
||||||
add_subdirectory(status_notifier)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (SERVICE_PIPEWIRE)
|
|
||||||
add_subdirectory(pipewire)
|
|
||||||
endif()
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(pipewire REQUIRED IMPORTED_TARGET libpipewire-0.3)
|
|
||||||
|
|
||||||
qt_add_library(quickshell-service-pipewire STATIC
|
|
||||||
qml.cpp
|
|
||||||
core.cpp
|
|
||||||
connection.cpp
|
|
||||||
registry.cpp
|
|
||||||
node.cpp
|
|
||||||
metadata.cpp
|
|
||||||
link.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_qml_module(quickshell-service-pipewire
|
|
||||||
URI Quickshell.Services.Pipewire
|
|
||||||
VERSION 0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell-service-pipewire PRIVATE ${QT_DEPS} PkgConfig::pipewire)
|
|
||||||
|
|
||||||
qs_pch(quickshell-service-pipewire)
|
|
||||||
qs_pch(quickshell-service-pipewireplugin)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE quickshell-service-pipewireplugin)
|
|
|
@ -1,23 +0,0 @@
|
||||||
#include "connection.hpp"
|
|
||||||
|
|
||||||
#include <qobject.h>
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
PwConnection::PwConnection(QObject* parent): QObject(parent) {
|
|
||||||
if (this->core.isValid()) {
|
|
||||||
this->registry.init(this->core);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PwConnection* PwConnection::instance() {
|
|
||||||
static PwConnection* instance = nullptr; // NOLINT
|
|
||||||
|
|
||||||
if (instance == nullptr) {
|
|
||||||
instance = new PwConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,25 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "core.hpp"
|
|
||||||
#include "metadata.hpp"
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
class PwConnection: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwConnection(QObject* parent = nullptr);
|
|
||||||
|
|
||||||
PwRegistry registry;
|
|
||||||
PwDefaultsMetadata defaults {&this->registry};
|
|
||||||
|
|
||||||
static PwConnection* instance();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// init/destroy order is important. do not rearrange.
|
|
||||||
PwCore core;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,87 +0,0 @@
|
||||||
#include "core.hpp"
|
|
||||||
#include <cerrno>
|
|
||||||
|
|
||||||
#include <pipewire/context.h>
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/loop.h>
|
|
||||||
#include <pipewire/pipewire.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qsocketnotifier.h>
|
|
||||||
#include <spa/utils/defs.h>
|
|
||||||
#include <spa/utils/hook.h>
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg);
|
|
||||||
|
|
||||||
PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) {
|
|
||||||
qCInfo(logLoop) << "Creating pipewire event loop.";
|
|
||||||
pw_init(nullptr, nullptr);
|
|
||||||
|
|
||||||
this->loop = pw_loop_new(nullptr);
|
|
||||||
if (this->loop == nullptr) {
|
|
||||||
qCCritical(logLoop) << "Failed to create pipewire event loop.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->context = pw_context_new(this->loop, nullptr, 0);
|
|
||||||
if (this->context == nullptr) {
|
|
||||||
qCCritical(logLoop) << "Failed to create pipewire context.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCInfo(logLoop) << "Connecting to pipewire server.";
|
|
||||||
this->core = pw_context_connect(this->context, nullptr, 0);
|
|
||||||
if (this->core == nullptr) {
|
|
||||||
qCCritical(logLoop) << "Failed to connect pipewire context. Errno:" << errno;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCInfo(logLoop) << "Linking pipewire event loop.";
|
|
||||||
// Tie the pw event loop into qt.
|
|
||||||
auto fd = pw_loop_get_fd(this->loop);
|
|
||||||
this->notifier.setSocket(fd);
|
|
||||||
QObject::connect(&this->notifier, &QSocketNotifier::activated, this, &PwCore::poll);
|
|
||||||
this->notifier.setEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwCore::~PwCore() {
|
|
||||||
qCInfo(logLoop) << "Destroying PwCore.";
|
|
||||||
|
|
||||||
if (this->loop != nullptr) {
|
|
||||||
if (this->context != nullptr) {
|
|
||||||
if (this->core != nullptr) {
|
|
||||||
pw_core_disconnect(this->core);
|
|
||||||
}
|
|
||||||
|
|
||||||
pw_context_destroy(this->context);
|
|
||||||
}
|
|
||||||
|
|
||||||
pw_loop_destroy(this->loop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PwCore::isValid() const {
|
|
||||||
// others must init first
|
|
||||||
return this->core != nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwCore::poll() const {
|
|
||||||
qCDebug(logLoop) << "Pipewire event loop received new events, iterating.";
|
|
||||||
// Spin pw event loop.
|
|
||||||
pw_loop_iterate(this->loop, 0);
|
|
||||||
qCDebug(logLoop) << "Done iterating pipewire event loop.";
|
|
||||||
}
|
|
||||||
|
|
||||||
SpaHook::SpaHook() { // NOLINT
|
|
||||||
spa_zero(this->hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
void SpaHook::remove() {
|
|
||||||
spa_hook_remove(&this->hook);
|
|
||||||
spa_zero(this->hook);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,59 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <pipewire/context.h>
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/loop.h>
|
|
||||||
#include <pipewire/proxy.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qsocketnotifier.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <spa/utils/hook.h>
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
class PwCore: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwCore(QObject* parent = nullptr);
|
|
||||||
~PwCore() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwCore);
|
|
||||||
|
|
||||||
[[nodiscard]] bool isValid() const;
|
|
||||||
|
|
||||||
pw_loop* loop = nullptr;
|
|
||||||
pw_context* context = nullptr;
|
|
||||||
pw_core* core = nullptr;
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void poll() const;
|
|
||||||
|
|
||||||
private:
|
|
||||||
QSocketNotifier notifier;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
class PwObject {
|
|
||||||
public:
|
|
||||||
explicit PwObject(T* object = nullptr): object(object) {}
|
|
||||||
~PwObject() {
|
|
||||||
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(this->object)); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
Q_DISABLE_COPY_MOVE(PwObject);
|
|
||||||
|
|
||||||
T* object;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SpaHook {
|
|
||||||
public:
|
|
||||||
explicit SpaHook();
|
|
||||||
|
|
||||||
void remove();
|
|
||||||
spa_hook hook;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,184 +0,0 @@
|
||||||
#include "link.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <pipewire/link.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <spa/utils/dict.h>
|
|
||||||
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg);
|
|
||||||
|
|
||||||
QString PwLinkState::toString(Enum value) {
|
|
||||||
return QString(pw_link_state_as_string(static_cast<pw_link_state>(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLink::bindHooks() {
|
|
||||||
pw_link_add_listener(this->proxy(), &this->listener.hook, &PwLink::EVENTS, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLink::unbindHooks() {
|
|
||||||
this->listener.remove();
|
|
||||||
this->setState(PW_LINK_STATE_UNLINKED);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLink::initProps(const spa_dict* props) {
|
|
||||||
qCDebug(logLink) << "Parsing initial SPA props of link" << this;
|
|
||||||
|
|
||||||
const spa_dict_item* item = nullptr;
|
|
||||||
spa_dict_for_each(item, props) {
|
|
||||||
if (strcmp(item->key, "link.output.node") == 0) {
|
|
||||||
auto str = QString(item->value);
|
|
||||||
auto ok = false;
|
|
||||||
auto value = str.toInt(&ok);
|
|
||||||
if (ok) this->setOutputNode(value);
|
|
||||||
else {
|
|
||||||
qCWarning(logLink) << "Could not parse link.output.node for" << this << ":" << item->value;
|
|
||||||
}
|
|
||||||
} else if (strcmp(item->key, "link.input.node") == 0) {
|
|
||||||
auto str = QString(item->value);
|
|
||||||
auto ok = false;
|
|
||||||
auto value = str.toInt(&ok);
|
|
||||||
if (ok) this->setInputNode(value);
|
|
||||||
else {
|
|
||||||
qCWarning(logLink) << "Could not parse link.input.node for" << this << ":" << item->value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pw_link_events PwLink::EVENTS = {
|
|
||||||
.version = PW_VERSION_LINK_EVENTS,
|
|
||||||
.info = &PwLink::onInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
void PwLink::onInfo(void* data, const struct pw_link_info* info) {
|
|
||||||
auto* self = static_cast<PwLink*>(data);
|
|
||||||
qCDebug(logLink) << "Got link info update for" << self << "with mask" << info->change_mask;
|
|
||||||
self->setOutputNode(info->output_node_id);
|
|
||||||
self->setInputNode(info->input_node_id);
|
|
||||||
|
|
||||||
if ((info->change_mask & PW_LINK_CHANGE_MASK_STATE) != 0) {
|
|
||||||
self->setState(info->state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quint32 PwLink::outputNode() const { return this->mOutputNode; }
|
|
||||||
quint32 PwLink::inputNode() const { return this->mInputNode; }
|
|
||||||
PwLinkState::Enum PwLink::state() const { return static_cast<PwLinkState::Enum>(this->mState); }
|
|
||||||
|
|
||||||
void PwLink::setOutputNode(quint32 outputNode) {
|
|
||||||
if (outputNode == this->mOutputNode) return;
|
|
||||||
|
|
||||||
if (this->mOutputNode != 0) {
|
|
||||||
qCWarning(logLink) << "Got unexpected output node update for" << this << "to" << outputNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mOutputNode = outputNode;
|
|
||||||
qCDebug(logLink) << "Updated output node of" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLink::setInputNode(quint32 inputNode) {
|
|
||||||
if (inputNode == this->mInputNode) return;
|
|
||||||
|
|
||||||
if (this->mInputNode != 0) {
|
|
||||||
qCWarning(logLink) << "Got unexpected input node update for" << this << "to" << inputNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mInputNode = inputNode;
|
|
||||||
qCDebug(logLink) << "Updated input node of" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLink::setState(pw_link_state state) {
|
|
||||||
if (state == this->mState) return;
|
|
||||||
|
|
||||||
this->mState = state;
|
|
||||||
qCDebug(logLink) << "Updated state of" << this;
|
|
||||||
emit this->stateChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const PwLink* link) {
|
|
||||||
if (link == nullptr) {
|
|
||||||
debug << "PwLink(0x0)";
|
|
||||||
} else {
|
|
||||||
auto saver = QDebugStateSaver(debug);
|
|
||||||
debug.nospace() << "PwLink(" << link->outputNode() << " -> " << link->inputNode() << ", "
|
|
||||||
<< static_cast<const void*>(link) << ", id=";
|
|
||||||
link->debugId(debug);
|
|
||||||
debug << ", state=" << link->state() << ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkGroup::PwLinkGroup(PwLink* firstLink, QObject* parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, mOutputNode(firstLink->outputNode())
|
|
||||||
, mInputNode(firstLink->inputNode()) {
|
|
||||||
this->tryAddLink(firstLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLinkGroup::ref() {
|
|
||||||
this->refcount++;
|
|
||||||
|
|
||||||
if (this->refcount == 1) {
|
|
||||||
this->trackedLink = *this->links.begin();
|
|
||||||
this->trackedLink->ref();
|
|
||||||
QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged);
|
|
||||||
emit this->stateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLinkGroup::unref() {
|
|
||||||
if (this->refcount == 0) return;
|
|
||||||
this->refcount--;
|
|
||||||
|
|
||||||
if (this->refcount == 0) {
|
|
||||||
this->trackedLink->unref();
|
|
||||||
this->trackedLink = nullptr;
|
|
||||||
emit this->stateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
quint32 PwLinkGroup::outputNode() const { return this->mOutputNode; }
|
|
||||||
|
|
||||||
quint32 PwLinkGroup::inputNode() const { return this->mInputNode; }
|
|
||||||
|
|
||||||
PwLinkState::Enum PwLinkGroup::state() const {
|
|
||||||
if (this->trackedLink == nullptr) {
|
|
||||||
return PwLinkState::Unlinked;
|
|
||||||
} else {
|
|
||||||
return this->trackedLink->state();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PwLinkGroup::tryAddLink(PwLink* link) {
|
|
||||||
if (link->outputNode() != this->mOutputNode || link->inputNode() != this->mInputNode)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
this->links.insert(link->id, link);
|
|
||||||
QObject::connect(link, &PwBindableObject::destroying, this, &PwLinkGroup::onLinkRemoved);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLinkGroup::onLinkRemoved(QObject* object) {
|
|
||||||
auto* link = static_cast<PwLink*>(object); // NOLINT
|
|
||||||
this->links.remove(link->id);
|
|
||||||
|
|
||||||
if (this->links.empty()) {
|
|
||||||
delete this;
|
|
||||||
} else if (link == this->trackedLink) {
|
|
||||||
this->trackedLink = *this->links.begin();
|
|
||||||
QObject::connect(this->trackedLink, &PwLink::stateChanged, this, &PwLinkGroup::stateChanged);
|
|
||||||
emit this->stateChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,99 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <pipewire/link.h>
|
|
||||||
#include <pipewire/type.h>
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
class PwLinkState: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_SINGLETON;
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Enum {
|
|
||||||
Error = PW_LINK_STATE_ERROR,
|
|
||||||
Unlinked = PW_LINK_STATE_UNLINKED,
|
|
||||||
Init = PW_LINK_STATE_INIT,
|
|
||||||
Negotiating = PW_LINK_STATE_NEGOTIATING,
|
|
||||||
Allocating = PW_LINK_STATE_ALLOCATING,
|
|
||||||
Paused = PW_LINK_STATE_PAUSED,
|
|
||||||
Active = PW_LINK_STATE_ACTIVE,
|
|
||||||
};
|
|
||||||
Q_ENUM(Enum);
|
|
||||||
|
|
||||||
Q_INVOKABLE static QString toString(PwLinkState::Enum value);
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link; // NOLINT
|
|
||||||
class PwLink: public PwBindable<pw_link, TYPE_INTERFACE_Link, PW_VERSION_LINK> { // NOLINT
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void bindHooks() override;
|
|
||||||
void unbindHooks() override;
|
|
||||||
void initProps(const spa_dict* props) override;
|
|
||||||
|
|
||||||
[[nodiscard]] quint32 outputNode() const;
|
|
||||||
[[nodiscard]] quint32 inputNode() const;
|
|
||||||
[[nodiscard]] PwLinkState::Enum state() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void stateChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static const pw_link_events EVENTS;
|
|
||||||
static void onInfo(void* data, const struct pw_link_info* info);
|
|
||||||
|
|
||||||
void setOutputNode(quint32 outputNode);
|
|
||||||
void setInputNode(quint32 inputNode);
|
|
||||||
void setState(pw_link_state state);
|
|
||||||
|
|
||||||
SpaHook listener;
|
|
||||||
|
|
||||||
quint32 mOutputNode = 0;
|
|
||||||
quint32 mInputNode = 0;
|
|
||||||
pw_link_state mState = PW_LINK_STATE_UNLINKED;
|
|
||||||
};
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const PwLink* link);
|
|
||||||
|
|
||||||
class PwLinkGroup: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwLinkGroup(PwLink* firstLink, QObject* parent = nullptr);
|
|
||||||
|
|
||||||
void ref();
|
|
||||||
void unref();
|
|
||||||
|
|
||||||
[[nodiscard]] quint32 outputNode() const;
|
|
||||||
[[nodiscard]] quint32 inputNode() const;
|
|
||||||
[[nodiscard]] PwLinkState::Enum state() const;
|
|
||||||
|
|
||||||
QHash<quint32, PwLink*> links;
|
|
||||||
|
|
||||||
bool tryAddLink(PwLink* link);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void stateChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onLinkRemoved(QObject* object);
|
|
||||||
|
|
||||||
private:
|
|
||||||
quint32 mOutputNode = 0;
|
|
||||||
quint32 mInputNode = 0;
|
|
||||||
PwLink* trackedLink = nullptr;
|
|
||||||
quint32 refcount = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,146 +0,0 @@
|
||||||
#include "metadata.hpp"
|
|
||||||
#include <array>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <pipewire/extensions/metadata.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <spa/utils/json.h>
|
|
||||||
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg);
|
|
||||||
|
|
||||||
void PwMetadata::bindHooks() {
|
|
||||||
pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwMetadata::unbindHooks() { this->listener.remove(); }
|
|
||||||
|
|
||||||
const pw_metadata_events PwMetadata::EVENTS = {
|
|
||||||
.version = PW_VERSION_METADATA_EVENTS,
|
|
||||||
.property = &PwMetadata::onProperty,
|
|
||||||
};
|
|
||||||
|
|
||||||
int PwMetadata::onProperty(
|
|
||||||
void* data,
|
|
||||||
quint32 subject,
|
|
||||||
const char* key,
|
|
||||||
const char* type,
|
|
||||||
const char* value
|
|
||||||
) {
|
|
||||||
auto* self = static_cast<PwMetadata*>(data);
|
|
||||||
qCDebug(logMeta) << "Received metadata for" << self << "- subject:" << subject
|
|
||||||
<< "key:" << QString(key) << "type:" << QString(type)
|
|
||||||
<< "value:" << QString(value);
|
|
||||||
|
|
||||||
emit self->registry->metadataUpdate(self, subject, key, type, value);
|
|
||||||
|
|
||||||
// ideally we'd dealloc metadata that wasn't picked up but there's no information
|
|
||||||
// available about if updates can come in later, so I assume they can.
|
|
||||||
|
|
||||||
return 0; // ??? - no docs and no reason for a callback to return an int
|
|
||||||
}
|
|
||||||
|
|
||||||
PwDefaultsMetadata::PwDefaultsMetadata(PwRegistry* registry) {
|
|
||||||
QObject::connect(
|
|
||||||
registry,
|
|
||||||
&PwRegistry::metadataUpdate,
|
|
||||||
this,
|
|
||||||
&PwDefaultsMetadata::onMetadataUpdate
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PwDefaultsMetadata::defaultSink() const { return this->mDefaultSink; }
|
|
||||||
|
|
||||||
QString PwDefaultsMetadata::defaultSource() const { return this->mDefaultSource; }
|
|
||||||
|
|
||||||
// we don't really care if the metadata objects are destroyed, but try to ref them so we get property updates
|
|
||||||
void PwDefaultsMetadata::onMetadataUpdate(
|
|
||||||
PwMetadata* metadata,
|
|
||||||
quint32 subject,
|
|
||||||
const char* key,
|
|
||||||
const char* /*type*/,
|
|
||||||
const char* value
|
|
||||||
) {
|
|
||||||
if (subject != 0) return;
|
|
||||||
|
|
||||||
// non "configured" sinks and sources have lower priority as wireplumber seems to only change
|
|
||||||
// the "configured" ones.
|
|
||||||
|
|
||||||
bool sink = false;
|
|
||||||
if (strcmp(key, "default.configured.audio.sink") == 0) {
|
|
||||||
sink = true;
|
|
||||||
this->sinkConfigured = true;
|
|
||||||
} else if ((!this->sinkConfigured && strcmp(key, "default.audio.sink") == 0)) {
|
|
||||||
sink = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sink) {
|
|
||||||
this->defaultSinkHolder.setObject(metadata);
|
|
||||||
|
|
||||||
auto newSink = PwDefaultsMetadata::parseNameSpaJson(value);
|
|
||||||
qCInfo(logMeta) << "Got default sink" << newSink << "configured:" << this->sinkConfigured;
|
|
||||||
if (newSink == this->mDefaultSink) return;
|
|
||||||
|
|
||||||
this->mDefaultSink = newSink;
|
|
||||||
emit this->defaultSinkChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool source = false;
|
|
||||||
if (strcmp(key, "default.configured.audio.source") == 0) {
|
|
||||||
source = true;
|
|
||||||
this->sourceConfigured = true;
|
|
||||||
} else if ((!this->sourceConfigured && strcmp(key, "default.audio.source") == 0)) {
|
|
||||||
source = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (source) {
|
|
||||||
this->defaultSourceHolder.setObject(metadata);
|
|
||||||
|
|
||||||
auto newSource = PwDefaultsMetadata::parseNameSpaJson(value);
|
|
||||||
qCInfo(logMeta) << "Got default source" << newSource << "configured:" << this->sourceConfigured;
|
|
||||||
if (newSource == this->mDefaultSource) return;
|
|
||||||
|
|
||||||
this->mDefaultSource = newSource;
|
|
||||||
emit this->defaultSourceChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QString PwDefaultsMetadata::parseNameSpaJson(const char* spaJson) {
|
|
||||||
auto iter = std::array<spa_json, 2>();
|
|
||||||
spa_json_init(&iter[0], spaJson, strlen(spaJson));
|
|
||||||
|
|
||||||
if (spa_json_enter_object(&iter[0], &iter[1]) < 0) {
|
|
||||||
qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to enter object of"
|
|
||||||
<< QString(spaJson);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto buf = std::array<char, 512>();
|
|
||||||
while (spa_json_get_string(&iter[1], buf.data(), buf.size()) > 0) {
|
|
||||||
if (strcmp(buf.data(), "name") != 0) continue;
|
|
||||||
|
|
||||||
if (spa_json_get_string(&iter[1], buf.data(), buf.size()) < 0) {
|
|
||||||
qCWarning(logMeta
|
|
||||||
) << "Failed to parse source/sink SPA json - failed to read value of name property"
|
|
||||||
<< QString(spaJson);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return QString(buf.data());
|
|
||||||
}
|
|
||||||
|
|
||||||
qCWarning(logMeta) << "Failed to parse source/sink SPA json - failed to find name property of"
|
|
||||||
<< QString(spaJson);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,64 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <pipewire/extensions/metadata.h>
|
|
||||||
#include <pipewire/type.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "core.hpp"
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
constexpr const char TYPE_INTERFACE_Metadata[] = PW_TYPE_INTERFACE_Metadata; // NOLINT
|
|
||||||
class PwMetadata
|
|
||||||
: public PwBindable<pw_metadata, TYPE_INTERFACE_Metadata, PW_VERSION_METADATA> { // NOLINT
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void bindHooks() override;
|
|
||||||
void unbindHooks() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
static const pw_metadata_events EVENTS;
|
|
||||||
static int
|
|
||||||
onProperty(void* data, quint32 subject, const char* key, const char* type, const char* value);
|
|
||||||
|
|
||||||
SpaHook listener;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwDefaultsMetadata: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwDefaultsMetadata(PwRegistry* registry);
|
|
||||||
|
|
||||||
[[nodiscard]] QString defaultSource() const;
|
|
||||||
[[nodiscard]] QString defaultSink() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void defaultSourceChanged();
|
|
||||||
void defaultSinkChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onMetadataUpdate(
|
|
||||||
PwMetadata* metadata,
|
|
||||||
quint32 subject,
|
|
||||||
const char* key,
|
|
||||||
const char* type,
|
|
||||||
const char* value
|
|
||||||
);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static QString parseNameSpaJson(const char* spaJson);
|
|
||||||
|
|
||||||
PwBindableRef<PwMetadata> defaultSinkHolder;
|
|
||||||
PwBindableRef<PwMetadata> defaultSourceHolder;
|
|
||||||
|
|
||||||
bool sinkConfigured = false;
|
|
||||||
QString mDefaultSink;
|
|
||||||
bool sourceConfigured = false;
|
|
||||||
QString mDefaultSource;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,8 +0,0 @@
|
||||||
name = "Quickshell.Services.PipeWire"
|
|
||||||
description = "Pipewire API"
|
|
||||||
headers = [
|
|
||||||
"qml.hpp",
|
|
||||||
"link.hpp",
|
|
||||||
"node.hpp",
|
|
||||||
]
|
|
||||||
-----
|
|
|
@ -1,384 +0,0 @@
|
||||||
#include "node.hpp"
|
|
||||||
#include <array>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/node.h>
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <spa/node/keys.h>
|
|
||||||
#include <spa/param/param.h>
|
|
||||||
#include <spa/param/props.h>
|
|
||||||
#include <spa/pod/builder.h>
|
|
||||||
#include <spa/pod/iter.h>
|
|
||||||
#include <spa/pod/pod.h>
|
|
||||||
#include <spa/pod/vararg.h>
|
|
||||||
#include <spa/utils/dict.h>
|
|
||||||
#include <spa/utils/keys.h>
|
|
||||||
#include <spa/utils/type.h>
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg);
|
|
||||||
|
|
||||||
QString PwAudioChannel::toString(Enum value) {
|
|
||||||
switch (value) {
|
|
||||||
case Unknown: return "Unknown";
|
|
||||||
case NA: return "N/A";
|
|
||||||
case Mono: return "Mono";
|
|
||||||
case FrontCenter: return "Front Center";
|
|
||||||
case FrontLeft: return "Front Left";
|
|
||||||
case FrontRight: return "Front Right";
|
|
||||||
case FrontLeftCenter: return "Front Left Center";
|
|
||||||
case FrontRightCenter: return "Front Right Center";
|
|
||||||
case FrontLeftWide: return "Front Left Wide";
|
|
||||||
case FrontRightWide: return "Front Right Wide";
|
|
||||||
case FrontCenterHigh: return "Front Center High";
|
|
||||||
case FrontLeftHigh: return "Front Left High";
|
|
||||||
case FrontRightHigh: return "Front Right High";
|
|
||||||
case LowFrequencyEffects: return "Low Frequency Effects";
|
|
||||||
case LowFrequencyEffects2: return "Low Frequency Effects 2";
|
|
||||||
case LowFrequencyEffectsLeft: return "Low Frequency Effects Left";
|
|
||||||
case LowFrequencyEffectsRight: return "Low Frequency Effects Right";
|
|
||||||
case SideLeft: return "Side Left";
|
|
||||||
case SideRight: return "Side Right";
|
|
||||||
case RearCenter: return "Rear Center";
|
|
||||||
case RearLeft: return "Rear Left";
|
|
||||||
case RearRight: return "Rear Right";
|
|
||||||
case RearLeftCenter: return "Rear Left Center";
|
|
||||||
case RearRightCenter: return "Rear Right Center";
|
|
||||||
case TopCenter: return "Top Center";
|
|
||||||
case TopFrontCenter: return "Top Front Center";
|
|
||||||
case TopFrontLeft: return "Top Front Left";
|
|
||||||
case TopFrontRight: return "Top Front Right";
|
|
||||||
case TopFrontLeftCenter: return "Top Front Left Center";
|
|
||||||
case TopFrontRightCenter: return "Top Front Right Center";
|
|
||||||
case TopSideLeft: return "Top Side Left";
|
|
||||||
case TopSideRight: return "Top Side Right";
|
|
||||||
case TopRearCenter: return "Top Rear Center";
|
|
||||||
case TopRearLeft: return "Top Rear Left";
|
|
||||||
case TopRearRight: return "Top Rear Right";
|
|
||||||
case BottomCenter: return "Bottom Center";
|
|
||||||
case BottomLeftCenter: return "Bottom Left Center";
|
|
||||||
case BottomRightCenter: return "Bottom Right Center";
|
|
||||||
default:
|
|
||||||
if (value >= AuxRangeStart && value <= AuxRangeEnd) {
|
|
||||||
return QString("Aux %1").arg(value - AuxRangeStart + 1);
|
|
||||||
} else if (value >= CustomRangeStart) {
|
|
||||||
return QString("Custom %1").arg(value - CustomRangeStart + 1);
|
|
||||||
} else {
|
|
||||||
return "Unknown";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNode::bindHooks() {
|
|
||||||
pw_node_add_listener(this->proxy(), &this->listener.hook, &PwNode::EVENTS, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNode::unbindHooks() {
|
|
||||||
this->listener.remove();
|
|
||||||
this->properties.clear();
|
|
||||||
emit this->propertiesChanged();
|
|
||||||
|
|
||||||
if (this->boundData != nullptr) {
|
|
||||||
this->boundData->onUnbind();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNode::initProps(const spa_dict* props) {
|
|
||||||
if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
|
|
||||||
if (strcmp(mediaClass, "Audio/Sink") == 0) {
|
|
||||||
this->type = PwNodeType::Audio;
|
|
||||||
this->isSink = true;
|
|
||||||
this->isStream = false;
|
|
||||||
} else if (strcmp(mediaClass, "Audio/Source") == 0) {
|
|
||||||
this->type = PwNodeType::Audio;
|
|
||||||
this->isSink = false;
|
|
||||||
this->isStream = false;
|
|
||||||
} else if (strcmp(mediaClass, "Stream/Output/Audio") == 0) {
|
|
||||||
this->type = PwNodeType::Audio;
|
|
||||||
this->isSink = false;
|
|
||||||
this->isStream = true;
|
|
||||||
} else if (strcmp(mediaClass, "Stream/Input/Audio") == 0) {
|
|
||||||
this->type = PwNodeType::Audio;
|
|
||||||
this->isSink = true;
|
|
||||||
this->isStream = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto* nodeName = spa_dict_lookup(props, SPA_KEY_NODE_NAME)) {
|
|
||||||
this->name = nodeName;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto* nodeDesc = spa_dict_lookup(props, SPA_KEY_NODE_DESCRIPTION)) {
|
|
||||||
this->description = nodeDesc;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const auto* nodeNick = spa_dict_lookup(props, "node.nick")) {
|
|
||||||
this->nick = nodeNick;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->type == PwNodeType::Audio) {
|
|
||||||
this->boundData = new PwNodeBoundAudio(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const pw_node_events PwNode::EVENTS = {
|
|
||||||
.version = PW_VERSION_NODE_EVENTS,
|
|
||||||
.info = &PwNode::onInfo,
|
|
||||||
.param = &PwNode::onParam,
|
|
||||||
};
|
|
||||||
|
|
||||||
void PwNode::onInfo(void* data, const pw_node_info* info) {
|
|
||||||
auto* self = static_cast<PwNode*>(data);
|
|
||||||
|
|
||||||
if ((info->change_mask & PW_NODE_CHANGE_MASK_PROPS) != 0) {
|
|
||||||
auto properties = QMap<QString, QString>();
|
|
||||||
|
|
||||||
const spa_dict_item* item = nullptr;
|
|
||||||
spa_dict_for_each(item, info->props) { properties.insert(item->key, item->value); }
|
|
||||||
|
|
||||||
self->properties = properties;
|
|
||||||
emit self->propertiesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self->boundData != nullptr) {
|
|
||||||
self->boundData->onInfo(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNode::onParam(
|
|
||||||
void* data,
|
|
||||||
qint32 /*seq*/,
|
|
||||||
quint32 id,
|
|
||||||
quint32 index,
|
|
||||||
quint32 /*next*/,
|
|
||||||
const spa_pod* param
|
|
||||||
) {
|
|
||||||
auto* self = static_cast<PwNode*>(data);
|
|
||||||
if (self->boundData != nullptr) {
|
|
||||||
self->boundData->onSpaParam(id, index, param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::onInfo(const pw_node_info* info) {
|
|
||||||
if ((info->change_mask & PW_NODE_CHANGE_MASK_PARAMS) != 0) {
|
|
||||||
for (quint32 i = 0; i < info->n_params; i++) {
|
|
||||||
auto& param = info->params[i]; // NOLINT
|
|
||||||
|
|
||||||
if (param.id == SPA_PARAM_Props && (param.flags & SPA_PARAM_INFO_READ) != 0) {
|
|
||||||
pw_node_enum_params(this->node->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::onSpaParam(quint32 id, quint32 index, const spa_pod* param) {
|
|
||||||
if (id == SPA_PARAM_Props && index == 0) {
|
|
||||||
this->updateVolumeFromParam(param);
|
|
||||||
this->updateMutedFromParam(param);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::updateVolumeFromParam(const spa_pod* param) {
|
|
||||||
const auto* volumesProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelVolumes);
|
|
||||||
const auto* channelsProp = spa_pod_find_prop(param, nullptr, SPA_PROP_channelMap);
|
|
||||||
|
|
||||||
if (volumesProp == nullptr) {
|
|
||||||
qCWarning(logNode) << "Cannot update volume props of" << this->node
|
|
||||||
<< "- channelVolumes was null.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channelsProp == nullptr) {
|
|
||||||
qCWarning(logNode) << "Cannot update volume props of" << this->node << "- channelMap was null.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spa_pod_is_array(&volumesProp->value) == 0) {
|
|
||||||
qCWarning(logNode) << "Cannot update volume props of" << this->node
|
|
||||||
<< "- channelVolumes was not an array.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spa_pod_is_array(&channelsProp->value) == 0) {
|
|
||||||
qCWarning(logNode) << "Cannot update volume props of" << this->node
|
|
||||||
<< "- channelMap was not an array.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto* volumes = reinterpret_cast<const spa_pod_array*>(&volumesProp->value); // NOLINT
|
|
||||||
const auto* channels = reinterpret_cast<const spa_pod_array*>(&channelsProp->value); // NOLINT
|
|
||||||
|
|
||||||
auto volumesVec = QVector<float>();
|
|
||||||
auto channelsVec = QVector<PwAudioChannel::Enum>();
|
|
||||||
|
|
||||||
spa_pod* iter = nullptr;
|
|
||||||
SPA_POD_ARRAY_FOREACH(volumes, iter) {
|
|
||||||
// Cubing behavior found in MPD source, and appears to corrospond to everyone else's measurements correctly.
|
|
||||||
auto linear = *reinterpret_cast<float*>(iter); // NOLINT
|
|
||||||
auto visual = std::cbrt(linear);
|
|
||||||
volumesVec.push_back(visual);
|
|
||||||
}
|
|
||||||
|
|
||||||
SPA_POD_ARRAY_FOREACH(channels, iter) {
|
|
||||||
channelsVec.push_back(*reinterpret_cast<PwAudioChannel::Enum*>(iter)); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
if (volumesVec.size() != channelsVec.size()) {
|
|
||||||
qCWarning(logNode) << "Cannot update volume props of" << this->node
|
|
||||||
<< "- channelVolumes and channelMap are not the same size. Sizes:"
|
|
||||||
<< volumesVec.size() << channelsVec.size();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is important that the lengths of channels and volumes stay in sync whenever you read them.
|
|
||||||
auto channelsChanged = false;
|
|
||||||
auto volumesChanged = false;
|
|
||||||
|
|
||||||
if (this->mChannels != channelsVec) {
|
|
||||||
this->mChannels = channelsVec;
|
|
||||||
channelsChanged = true;
|
|
||||||
qCDebug(logNode) << "Got updated channels of" << this->node << '-' << this->mChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->mVolumes != volumesVec) {
|
|
||||||
this->mVolumes = volumesVec;
|
|
||||||
volumesChanged = true;
|
|
||||||
qCDebug(logNode) << "Got updated volumes of" << this->node << '-' << this->mVolumes;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (channelsChanged) emit this->channelsChanged();
|
|
||||||
if (volumesChanged) emit this->volumesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::updateMutedFromParam(const spa_pod* param) {
|
|
||||||
const auto* mutedProp = spa_pod_find_prop(param, nullptr, SPA_PROP_mute);
|
|
||||||
|
|
||||||
if (mutedProp == nullptr) {
|
|
||||||
qCWarning(logNode) << "Cannot update muted state of" << this->node
|
|
||||||
<< "- mute property was null.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spa_pod_is_bool(&mutedProp->value) == 0) {
|
|
||||||
qCWarning(logNode) << "Cannot update muted state of" << this->node
|
|
||||||
<< "- mute property was not a boolean.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool muted = false;
|
|
||||||
spa_pod_get_bool(&mutedProp->value, &muted);
|
|
||||||
|
|
||||||
if (muted != this->mMuted) {
|
|
||||||
qCDebug(logNode) << "Got updated mute status of" << this->node << '-' << muted;
|
|
||||||
this->mMuted = muted;
|
|
||||||
emit this->mutedChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::onUnbind() {
|
|
||||||
this->mChannels.clear();
|
|
||||||
this->mVolumes.clear();
|
|
||||||
emit this->channelsChanged();
|
|
||||||
emit this->volumesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PwNodeBoundAudio::isMuted() const { return this->mMuted; }
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::setMuted(bool muted) {
|
|
||||||
if (this->node->proxy() == nullptr) {
|
|
||||||
qCWarning(logNode) << "Tried to change mute state for" << this->node << "which is not bound.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (muted == this->mMuted) return;
|
|
||||||
|
|
||||||
auto buffer = std::array<quint32, 1024>();
|
|
||||||
auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
|
||||||
|
|
||||||
// is this a leak? seems like probably not? docs don't say, as usual.
|
|
||||||
// clang-format off
|
|
||||||
auto* pod = spa_pod_builder_add_object(
|
|
||||||
&builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
|
|
||||||
SPA_PROP_mute, SPA_POD_Bool(muted)
|
|
||||||
);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
qCDebug(logNode) << "Changed muted state of" << this->node << "to" << muted;
|
|
||||||
this->mMuted = muted;
|
|
||||||
pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast<spa_pod*>(pod));
|
|
||||||
emit this->mutedChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
float PwNodeBoundAudio::averageVolume() const {
|
|
||||||
float total = 0;
|
|
||||||
|
|
||||||
for (auto volume: this->mVolumes) {
|
|
||||||
total += volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
return total / static_cast<float>(this->mVolumes.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::setAverageVolume(float volume) {
|
|
||||||
auto oldAverage = this->averageVolume();
|
|
||||||
auto mul = oldAverage == 0 ? 0 : volume / oldAverage;
|
|
||||||
auto volumes = QVector<float>();
|
|
||||||
|
|
||||||
for (auto oldVolume: this->mVolumes) {
|
|
||||||
volumes.push_back(mul == 0 ? volume : oldVolume * mul);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->setVolumes(volumes);
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<PwAudioChannel::Enum> PwNodeBoundAudio::channels() const { return this->mChannels; }
|
|
||||||
|
|
||||||
QVector<float> PwNodeBoundAudio::volumes() const { return this->mVolumes; }
|
|
||||||
|
|
||||||
void PwNodeBoundAudio::setVolumes(const QVector<float>& volumes) {
|
|
||||||
if (this->node->proxy() == nullptr) {
|
|
||||||
qCWarning(logNode) << "Tried to change node volumes for" << this->node << "which is not bound.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (volumes == this->mVolumes) return;
|
|
||||||
|
|
||||||
if (volumes.length() != this->mVolumes.length()) {
|
|
||||||
qCWarning(logNode) << "Tried to change node volumes for" << this->node << "from"
|
|
||||||
<< this->mVolumes << "to" << volumes
|
|
||||||
<< "which has a different length than the list of channels"
|
|
||||||
<< this->mChannels;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto buffer = std::array<quint32, 1024>();
|
|
||||||
auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
|
|
||||||
|
|
||||||
auto cubedVolumes = QVector<float>();
|
|
||||||
for (auto volume: volumes) {
|
|
||||||
cubedVolumes.push_back(volume * volume * volume);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
auto* pod = spa_pod_builder_add_object(
|
|
||||||
&builder, SPA_TYPE_OBJECT_Props, SPA_PARAM_Props,
|
|
||||||
SPA_PROP_channelVolumes, SPA_POD_Array(sizeof(float), SPA_TYPE_Float, cubedVolumes.length(), cubedVolumes.data())
|
|
||||||
);
|
|
||||||
// clang-format on
|
|
||||||
|
|
||||||
qCDebug(logNode) << "Changed volumes of" << this->node << "to" << volumes;
|
|
||||||
this->mVolumes = volumes;
|
|
||||||
pw_node_set_param(this->node->proxy(), SPA_PARAM_Props, 0, static_cast<spa_pod*>(pod));
|
|
||||||
emit this->volumesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,174 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/node.h>
|
|
||||||
#include <pipewire/type.h>
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qmap.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <spa/param/audio/raw.h>
|
|
||||||
#include <spa/pod/pod.h>
|
|
||||||
|
|
||||||
#include "core.hpp"
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
class PwAudioChannel: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_SINGLETON;
|
|
||||||
|
|
||||||
public:
|
|
||||||
enum Enum {
|
|
||||||
Unknown = SPA_AUDIO_CHANNEL_UNKNOWN,
|
|
||||||
NA = SPA_AUDIO_CHANNEL_NA,
|
|
||||||
Mono = SPA_AUDIO_CHANNEL_MONO,
|
|
||||||
FrontCenter = SPA_AUDIO_CHANNEL_FC,
|
|
||||||
FrontLeft = SPA_AUDIO_CHANNEL_FL,
|
|
||||||
FrontRight = SPA_AUDIO_CHANNEL_FR,
|
|
||||||
FrontLeftCenter = SPA_AUDIO_CHANNEL_FLC,
|
|
||||||
FrontRightCenter = SPA_AUDIO_CHANNEL_FRC,
|
|
||||||
FrontLeftWide = SPA_AUDIO_CHANNEL_FLW,
|
|
||||||
FrontRightWide = SPA_AUDIO_CHANNEL_FRW,
|
|
||||||
FrontCenterHigh = SPA_AUDIO_CHANNEL_FCH,
|
|
||||||
FrontLeftHigh = SPA_AUDIO_CHANNEL_FLH,
|
|
||||||
FrontRightHigh = SPA_AUDIO_CHANNEL_FRH,
|
|
||||||
LowFrequencyEffects = SPA_AUDIO_CHANNEL_LFE,
|
|
||||||
LowFrequencyEffects2 = SPA_AUDIO_CHANNEL_LFE2,
|
|
||||||
LowFrequencyEffectsLeft = SPA_AUDIO_CHANNEL_LLFE,
|
|
||||||
LowFrequencyEffectsRight = SPA_AUDIO_CHANNEL_RLFE,
|
|
||||||
SideLeft = SPA_AUDIO_CHANNEL_SL,
|
|
||||||
SideRight = SPA_AUDIO_CHANNEL_SR,
|
|
||||||
RearCenter = SPA_AUDIO_CHANNEL_RC,
|
|
||||||
RearLeft = SPA_AUDIO_CHANNEL_RL,
|
|
||||||
RearRight = SPA_AUDIO_CHANNEL_RR,
|
|
||||||
RearLeftCenter = SPA_AUDIO_CHANNEL_RLC,
|
|
||||||
RearRightCenter = SPA_AUDIO_CHANNEL_RRC,
|
|
||||||
TopCenter = SPA_AUDIO_CHANNEL_TC,
|
|
||||||
TopFrontCenter = SPA_AUDIO_CHANNEL_TFC,
|
|
||||||
TopFrontLeft = SPA_AUDIO_CHANNEL_TFL,
|
|
||||||
TopFrontRight = SPA_AUDIO_CHANNEL_TFR,
|
|
||||||
TopFrontLeftCenter = SPA_AUDIO_CHANNEL_TFLC,
|
|
||||||
TopFrontRightCenter = SPA_AUDIO_CHANNEL_TFRC,
|
|
||||||
TopSideLeft = SPA_AUDIO_CHANNEL_TSL,
|
|
||||||
TopSideRight = SPA_AUDIO_CHANNEL_TSR,
|
|
||||||
TopRearCenter = SPA_AUDIO_CHANNEL_TRC,
|
|
||||||
TopRearLeft = SPA_AUDIO_CHANNEL_TRL,
|
|
||||||
TopRearRight = SPA_AUDIO_CHANNEL_TRR,
|
|
||||||
BottomCenter = SPA_AUDIO_CHANNEL_BC,
|
|
||||||
BottomLeftCenter = SPA_AUDIO_CHANNEL_BLC,
|
|
||||||
BottomRightCenter = SPA_AUDIO_CHANNEL_BRC,
|
|
||||||
/// The start of the aux channel range.
|
|
||||||
///
|
|
||||||
/// Values between AuxRangeStart and AuxRangeEnd are valid.
|
|
||||||
AuxRangeStart = SPA_AUDIO_CHANNEL_START_Aux,
|
|
||||||
/// The end of the aux channel range.
|
|
||||||
///
|
|
||||||
/// Values between AuxRangeStart and AuxRangeEnd are valid.
|
|
||||||
AuxRangeEnd = SPA_AUDIO_CHANNEL_LAST_Aux,
|
|
||||||
/// The end of the custom channel range.
|
|
||||||
///
|
|
||||||
/// Values starting at CustomRangeStart are valid.
|
|
||||||
CustomRangeStart = SPA_AUDIO_CHANNEL_START_Custom,
|
|
||||||
};
|
|
||||||
Q_ENUM(Enum);
|
|
||||||
|
|
||||||
/// Print a human readable representation of the given channel,
|
|
||||||
/// including aux and custom channel ranges.
|
|
||||||
Q_INVOKABLE static QString toString(PwAudioChannel::Enum value);
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class PwNodeType {
|
|
||||||
Untracked,
|
|
||||||
Audio,
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwNode;
|
|
||||||
|
|
||||||
class PwNodeBoundData {
|
|
||||||
public:
|
|
||||||
PwNodeBoundData() = default;
|
|
||||||
virtual ~PwNodeBoundData() = default;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwNodeBoundData);
|
|
||||||
|
|
||||||
virtual void onInfo(const pw_node_info* /*info*/) {}
|
|
||||||
virtual void onSpaParam(quint32 /*id*/, quint32 /*index*/, const spa_pod* /*param*/) {}
|
|
||||||
virtual void onUnbind() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwNodeBoundAudio
|
|
||||||
: public QObject
|
|
||||||
, public PwNodeBoundData {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwNodeBoundAudio(PwNode* node): node(node) {}
|
|
||||||
|
|
||||||
void onInfo(const pw_node_info* info) override;
|
|
||||||
void onSpaParam(quint32 id, quint32 index, const spa_pod* param) override;
|
|
||||||
void onUnbind() override;
|
|
||||||
|
|
||||||
[[nodiscard]] bool isMuted() const;
|
|
||||||
void setMuted(bool muted);
|
|
||||||
|
|
||||||
[[nodiscard]] float averageVolume() const;
|
|
||||||
void setAverageVolume(float volume);
|
|
||||||
|
|
||||||
[[nodiscard]] QVector<PwAudioChannel::Enum> channels() const;
|
|
||||||
|
|
||||||
[[nodiscard]] QVector<float> volumes() const;
|
|
||||||
void setVolumes(const QVector<float>& volumes);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void volumesChanged();
|
|
||||||
void channelsChanged();
|
|
||||||
void mutedChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
void updateVolumeFromParam(const spa_pod* param);
|
|
||||||
void updateMutedFromParam(const spa_pod* param);
|
|
||||||
|
|
||||||
bool mMuted = false;
|
|
||||||
QVector<PwAudioChannel::Enum> mChannels;
|
|
||||||
QVector<float> mVolumes;
|
|
||||||
PwNode* node;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr const char TYPE_INTERFACE_Node[] = PW_TYPE_INTERFACE_Node; // NOLINT
|
|
||||||
class PwNode: public PwBindable<pw_node, TYPE_INTERFACE_Node, PW_VERSION_NODE> { // NOLINT
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void bindHooks() override;
|
|
||||||
void unbindHooks() override;
|
|
||||||
void initProps(const spa_dict* props) override;
|
|
||||||
|
|
||||||
QString name;
|
|
||||||
QString description;
|
|
||||||
QString nick;
|
|
||||||
QMap<QString, QString> properties;
|
|
||||||
|
|
||||||
PwNodeType type = PwNodeType::Untracked;
|
|
||||||
bool isSink = false;
|
|
||||||
bool isStream = false;
|
|
||||||
|
|
||||||
PwNodeBoundData* boundData = nullptr;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void propertiesChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
static const pw_node_events EVENTS;
|
|
||||||
static void onInfo(void* data, const pw_node_info* info);
|
|
||||||
static void
|
|
||||||
onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param);
|
|
||||||
|
|
||||||
SpaHook listener;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,472 +0,0 @@
|
||||||
#include "qml.hpp"
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qlist.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmllist.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <qvariant.h>
|
|
||||||
|
|
||||||
#include "connection.hpp"
|
|
||||||
#include "link.hpp"
|
|
||||||
#include "metadata.hpp"
|
|
||||||
#include "node.hpp"
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
void PwObjectIface::ref() {
|
|
||||||
this->refcount++;
|
|
||||||
|
|
||||||
if (this->refcount == 1) {
|
|
||||||
this->object->ref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwObjectIface::unref() {
|
|
||||||
if (this->refcount == 0) return;
|
|
||||||
this->refcount--;
|
|
||||||
|
|
||||||
if (this->refcount == 0) {
|
|
||||||
this->object->unref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Pipewire::Pipewire(QObject* parent): QObject(parent) {
|
|
||||||
auto* connection = PwConnection::instance();
|
|
||||||
|
|
||||||
for (auto* node: connection->registry.nodes.values()) {
|
|
||||||
this->onNodeAdded(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
|
|
||||||
|
|
||||||
for (auto* link: connection->registry.links.values()) {
|
|
||||||
this->onLinkAdded(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(&connection->registry, &PwRegistry::linkAdded, this, &Pipewire::onLinkAdded);
|
|
||||||
|
|
||||||
for (auto* group: connection->registry.linkGroups) {
|
|
||||||
this->onLinkGroupAdded(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(
|
|
||||||
&connection->registry,
|
|
||||||
&PwRegistry::linkGroupAdded,
|
|
||||||
this,
|
|
||||||
&Pipewire::onLinkGroupAdded
|
|
||||||
);
|
|
||||||
|
|
||||||
// clang-format off
|
|
||||||
QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSinkChanged, this, &Pipewire::defaultAudioSinkChanged);
|
|
||||||
QObject::connect(&connection->defaults, &PwDefaultsMetadata::defaultSourceChanged, this, &Pipewire::defaultAudioSourceChanged);
|
|
||||||
// clang-format on
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlListProperty<PwNodeIface> Pipewire::nodes() {
|
|
||||||
return QQmlListProperty<PwNodeIface>(this, nullptr, &Pipewire::nodesCount, &Pipewire::nodeAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype Pipewire::nodesCount(QQmlListProperty<PwNodeIface>* property) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mNodes.count(); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* Pipewire::nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mNodes.at(index); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onNodeAdded(PwNode* node) {
|
|
||||||
auto* iface = PwNodeIface::instance(node);
|
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onNodeRemoved);
|
|
||||||
|
|
||||||
this->mNodes.push_back(iface);
|
|
||||||
emit this->nodesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onNodeRemoved(QObject* object) {
|
|
||||||
auto* iface = static_cast<PwNodeIface*>(object); // NOLINT
|
|
||||||
this->mNodes.removeOne(iface);
|
|
||||||
emit this->nodesChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlListProperty<PwLinkIface> Pipewire::links() {
|
|
||||||
return QQmlListProperty<PwLinkIface>(this, nullptr, &Pipewire::linksCount, &Pipewire::linkAt);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype Pipewire::linksCount(QQmlListProperty<PwLinkIface>* property) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mLinks.count(); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkIface* Pipewire::linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mLinks.at(index); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkAdded(PwLink* link) {
|
|
||||||
auto* iface = PwLinkIface::instance(link);
|
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkRemoved);
|
|
||||||
|
|
||||||
this->mLinks.push_back(iface);
|
|
||||||
emit this->linksChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkRemoved(QObject* object) {
|
|
||||||
auto* iface = static_cast<PwLinkIface*>(object); // NOLINT
|
|
||||||
this->mLinks.removeOne(iface);
|
|
||||||
emit this->linksChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlListProperty<PwLinkGroupIface> Pipewire::linkGroups() {
|
|
||||||
return QQmlListProperty<PwLinkGroupIface>(
|
|
||||||
this,
|
|
||||||
nullptr,
|
|
||||||
&Pipewire::linkGroupsCount,
|
|
||||||
&Pipewire::linkGroupAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype Pipewire::linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mLinkGroups.count(); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkGroupIface*
|
|
||||||
Pipewire::linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index) {
|
|
||||||
return static_cast<Pipewire*>(property->object)->mLinkGroups.at(index); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkGroupAdded(PwLinkGroup* linkGroup) {
|
|
||||||
auto* iface = PwLinkGroupIface::instance(linkGroup);
|
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onLinkGroupRemoved);
|
|
||||||
|
|
||||||
this->mLinkGroups.push_back(iface);
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Pipewire::onLinkGroupRemoved(QObject* object) {
|
|
||||||
auto* iface = static_cast<PwLinkGroupIface*>(object); // NOLINT
|
|
||||||
this->mLinkGroups.removeOne(iface);
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* Pipewire::defaultAudioSink() const { // NOLINT
|
|
||||||
auto* connection = PwConnection::instance();
|
|
||||||
auto name = connection->defaults.defaultSink();
|
|
||||||
|
|
||||||
for (auto* node: connection->registry.nodes.values()) {
|
|
||||||
if (name == node->name) {
|
|
||||||
return PwNodeIface::instance(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* Pipewire::defaultAudioSource() const { // NOLINT
|
|
||||||
auto* connection = PwConnection::instance();
|
|
||||||
auto name = connection->defaults.defaultSource();
|
|
||||||
|
|
||||||
for (auto* node: connection->registry.nodes.values()) {
|
|
||||||
if (name == node->name) {
|
|
||||||
return PwNodeIface::instance(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; }
|
|
||||||
|
|
||||||
void PwNodeLinkTracker::setNode(PwNodeIface* node) {
|
|
||||||
if (node == this->mNode) return;
|
|
||||||
|
|
||||||
if (this->mNode != nullptr) {
|
|
||||||
if (node == nullptr) {
|
|
||||||
QObject::disconnect(&PwConnection::instance()->registry, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::disconnect(this->mNode, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node != nullptr) {
|
|
||||||
if (this->mNode == nullptr) {
|
|
||||||
QObject::connect(
|
|
||||||
&PwConnection::instance()->registry,
|
|
||||||
&PwRegistry::linkGroupAdded,
|
|
||||||
this,
|
|
||||||
&PwNodeLinkTracker::onLinkGroupCreated
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QObject::connect(node, &QObject::destroyed, this, &PwNodeLinkTracker::onNodeDestroyed);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mNode = node;
|
|
||||||
this->updateLinks();
|
|
||||||
emit this->nodeChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeLinkTracker::updateLinks() {
|
|
||||||
// done first to avoid unref->reref of nodes
|
|
||||||
auto newLinks = QVector<PwLinkGroupIface*>();
|
|
||||||
if (this->mNode != nullptr) {
|
|
||||||
auto* connection = PwConnection::instance();
|
|
||||||
|
|
||||||
for (auto* link: connection->registry.linkGroups) {
|
|
||||||
if ((!this->mNode->isSink() && link->outputNode() == this->mNode->id())
|
|
||||||
|| (this->mNode->isSink() && link->inputNode() == this->mNode->id()))
|
|
||||||
{
|
|
||||||
auto* iface = PwLinkGroupIface::instance(link);
|
|
||||||
|
|
||||||
// do not connect twice
|
|
||||||
if (!this->mLinkGroups.contains(iface)) {
|
|
||||||
QObject::connect(
|
|
||||||
iface,
|
|
||||||
&QObject::destroyed,
|
|
||||||
this,
|
|
||||||
&PwNodeLinkTracker::onLinkGroupDestroyed
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
newLinks.push_back(iface);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto* iface: this->mLinkGroups) {
|
|
||||||
// only disconnect no longer used nodes
|
|
||||||
if (!newLinks.contains(iface)) {
|
|
||||||
QObject::disconnect(iface, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mLinkGroups = newLinks;
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QQmlListProperty<PwLinkGroupIface> PwNodeLinkTracker::linkGroups() {
|
|
||||||
return QQmlListProperty<PwLinkGroupIface>(
|
|
||||||
this,
|
|
||||||
nullptr,
|
|
||||||
&PwNodeLinkTracker::linkGroupsCount,
|
|
||||||
&PwNodeLinkTracker::linkGroupAt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
qsizetype PwNodeLinkTracker::linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property) {
|
|
||||||
return static_cast<PwNodeLinkTracker*>(property->object)->mLinkGroups.count(); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkGroupIface*
|
|
||||||
PwNodeLinkTracker::linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index) {
|
|
||||||
return static_cast<PwNodeLinkTracker*>(property->object)->mLinkGroups.at(index); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeLinkTracker::onNodeDestroyed() {
|
|
||||||
this->mNode = nullptr;
|
|
||||||
this->updateLinks();
|
|
||||||
emit this->nodeChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeLinkTracker::onLinkGroupCreated(PwLinkGroup* linkGroup) {
|
|
||||||
if ((!this->mNode->isSink() && linkGroup->outputNode() == this->mNode->id())
|
|
||||||
|| (this->mNode->isSink() && linkGroup->inputNode() == this->mNode->id()))
|
|
||||||
{
|
|
||||||
auto* iface = PwLinkGroupIface::instance(linkGroup);
|
|
||||||
QObject::connect(iface, &QObject::destroyed, this, &PwNodeLinkTracker::onLinkGroupDestroyed);
|
|
||||||
this->mLinkGroups.push_back(iface);
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwNodeLinkTracker::onLinkGroupDestroyed(QObject* object) {
|
|
||||||
if (this->mLinkGroups.removeOne(object)) {
|
|
||||||
emit this->linkGroupsChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeAudioIface::PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent)
|
|
||||||
: QObject(parent)
|
|
||||||
, boundData(boundData) {
|
|
||||||
// clang-format off
|
|
||||||
QObject::connect(boundData, &PwNodeBoundAudio::mutedChanged, this, &PwNodeAudioIface::mutedChanged);
|
|
||||||
QObject::connect(boundData, &PwNodeBoundAudio::channelsChanged, this, &PwNodeAudioIface::channelsChanged);
|
|
||||||
QObject::connect(boundData, &PwNodeBoundAudio::volumesChanged, this, &PwNodeAudioIface::volumesChanged);
|
|
||||||
// clang-format on
|
|
||||||
}
|
|
||||||
|
|
||||||
bool PwNodeAudioIface::isMuted() const { return this->boundData->isMuted(); }
|
|
||||||
|
|
||||||
void PwNodeAudioIface::setMuted(bool muted) { this->boundData->setMuted(muted); }
|
|
||||||
|
|
||||||
float PwNodeAudioIface::averageVolume() const { return this->boundData->averageVolume(); }
|
|
||||||
|
|
||||||
void PwNodeAudioIface::setAverageVolume(float volume) { this->boundData->setAverageVolume(volume); }
|
|
||||||
|
|
||||||
QVector<PwAudioChannel::Enum> PwNodeAudioIface::channels() const {
|
|
||||||
return this->boundData->channels();
|
|
||||||
}
|
|
||||||
|
|
||||||
QVector<float> PwNodeAudioIface::volumes() const { return this->boundData->volumes(); }
|
|
||||||
|
|
||||||
void PwNodeAudioIface::setVolumes(const QVector<float>& volumes) {
|
|
||||||
this->boundData->setVolumes(volumes);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) {
|
|
||||||
QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged);
|
|
||||||
|
|
||||||
if (auto* audioBoundData = dynamic_cast<PwNodeBoundAudio*>(node->boundData)) {
|
|
||||||
this->audioIface = new PwNodeAudioIface(audioBoundData, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNode* PwNodeIface::node() const { return this->mNode; }
|
|
||||||
|
|
||||||
QString PwNodeIface::name() const { return this->mNode->name; }
|
|
||||||
|
|
||||||
quint32 PwNodeIface::id() const { return this->mNode->id; }
|
|
||||||
|
|
||||||
QString PwNodeIface::description() const { return this->mNode->description; }
|
|
||||||
|
|
||||||
QString PwNodeIface::nickname() const { return this->mNode->nick; }
|
|
||||||
|
|
||||||
bool PwNodeIface::isSink() const { return this->mNode->isSink; }
|
|
||||||
|
|
||||||
bool PwNodeIface::isStream() const { return this->mNode->isStream; }
|
|
||||||
|
|
||||||
QVariantMap PwNodeIface::properties() const {
|
|
||||||
auto map = QVariantMap();
|
|
||||||
for (auto [k, v]: this->mNode->properties.asKeyValueRange()) {
|
|
||||||
map.insert(k, QVariant::fromValue(v));
|
|
||||||
}
|
|
||||||
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
|
|
||||||
|
|
||||||
PwNodeIface* PwNodeIface::instance(PwNode* node) {
|
|
||||||
auto v = node->property("iface");
|
|
||||||
if (v.canConvert<PwNodeIface*>()) {
|
|
||||||
return v.value<PwNodeIface*>();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* instance = new PwNodeIface(node);
|
|
||||||
node->setProperty("iface", QVariant::fromValue(instance));
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkIface::PwLinkIface(PwLink* link): PwObjectIface(link), mLink(link) {
|
|
||||||
QObject::connect(link, &PwLink::stateChanged, this, &PwLinkIface::stateChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLink* PwLinkIface::link() const { return this->mLink; }
|
|
||||||
|
|
||||||
quint32 PwLinkIface::id() const { return this->mLink->id; }
|
|
||||||
|
|
||||||
PwNodeIface* PwLinkIface::target() const {
|
|
||||||
return PwNodeIface::instance(
|
|
||||||
PwConnection::instance()->registry.nodes.value(this->mLink->inputNode())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* PwLinkIface::source() const {
|
|
||||||
return PwNodeIface::instance(
|
|
||||||
PwConnection::instance()->registry.nodes.value(this->mLink->outputNode())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkState::Enum PwLinkIface::state() const { return this->mLink->state(); }
|
|
||||||
|
|
||||||
PwLinkIface* PwLinkIface::instance(PwLink* link) {
|
|
||||||
auto v = link->property("iface");
|
|
||||||
if (v.canConvert<PwLinkIface*>()) {
|
|
||||||
return v.value<PwLinkIface*>();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* instance = new PwLinkIface(link);
|
|
||||||
link->setProperty("iface", QVariant::fromValue(instance));
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkGroupIface::PwLinkGroupIface(PwLinkGroup* group): QObject(group), mGroup(group) {
|
|
||||||
QObject::connect(group, &PwLinkGroup::stateChanged, this, &PwLinkGroupIface::stateChanged);
|
|
||||||
QObject::connect(group, &QObject::destroyed, this, [this]() { delete this; });
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwLinkGroupIface::ref() { this->mGroup->ref(); }
|
|
||||||
|
|
||||||
void PwLinkGroupIface::unref() { this->mGroup->unref(); }
|
|
||||||
|
|
||||||
PwLinkGroup* PwLinkGroupIface::group() const { return this->mGroup; }
|
|
||||||
|
|
||||||
PwNodeIface* PwLinkGroupIface::target() const {
|
|
||||||
return PwNodeIface::instance(
|
|
||||||
PwConnection::instance()->registry.nodes.value(this->mGroup->inputNode())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwNodeIface* PwLinkGroupIface::source() const {
|
|
||||||
return PwNodeIface::instance(
|
|
||||||
PwConnection::instance()->registry.nodes.value(this->mGroup->outputNode())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
PwLinkState::Enum PwLinkGroupIface::state() const { return this->mGroup->state(); }
|
|
||||||
|
|
||||||
PwLinkGroupIface* PwLinkGroupIface::instance(PwLinkGroup* group) {
|
|
||||||
auto v = group->property("iface");
|
|
||||||
if (v.canConvert<PwLinkGroupIface*>()) {
|
|
||||||
return v.value<PwLinkGroupIface*>();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* instance = new PwLinkGroupIface(group);
|
|
||||||
group->setProperty("iface", QVariant::fromValue(instance));
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwObjectTracker::~PwObjectTracker() { this->clearList(); }
|
|
||||||
|
|
||||||
QList<QObject*> PwObjectTracker::objects() const { return this->trackedObjects; }
|
|
||||||
|
|
||||||
void PwObjectTracker::setObjects(const QList<QObject*>& objects) {
|
|
||||||
// +1 ref before removing old refs to avoid an unbind->bind.
|
|
||||||
for (auto* object: objects) {
|
|
||||||
if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
|
|
||||||
pwObject->ref();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->clearList();
|
|
||||||
|
|
||||||
// connect destroy
|
|
||||||
for (auto* object: objects) {
|
|
||||||
if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
|
|
||||||
QObject::connect(object, &QObject::destroyed, this, &PwObjectTracker::objectDestroyed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->trackedObjects = objects;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwObjectTracker::clearList() {
|
|
||||||
for (auto* object: this->trackedObjects) {
|
|
||||||
if (auto* pwObject = dynamic_cast<PwObjectRefIface*>(object)) {
|
|
||||||
pwObject->unref();
|
|
||||||
QObject::disconnect(object, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this->trackedObjects.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwObjectTracker::objectDestroyed(QObject* object) {
|
|
||||||
this->trackedObjects.removeOne(object);
|
|
||||||
emit this->objectsChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,368 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qqmllist.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "link.hpp"
|
|
||||||
#include "node.hpp"
|
|
||||||
#include "registry.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
class PwNodeIface;
|
|
||||||
class PwLinkIface;
|
|
||||||
class PwLinkGroupIface;
|
|
||||||
|
|
||||||
class PwObjectRefIface {
|
|
||||||
public:
|
|
||||||
PwObjectRefIface() = default;
|
|
||||||
virtual ~PwObjectRefIface() = default;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwObjectRefIface);
|
|
||||||
|
|
||||||
virtual void ref() = 0;
|
|
||||||
virtual void unref() = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwObjectIface
|
|
||||||
: public QObject
|
|
||||||
, public PwObjectRefIface {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwObjectIface(PwBindableObject* object): QObject(object), object(object) {};
|
|
||||||
// destructor should ONLY be called by the pw object destructor, making an unref unnecessary
|
|
||||||
~PwObjectIface() override = default;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwObjectIface);
|
|
||||||
|
|
||||||
void ref() override;
|
|
||||||
void unref() override;
|
|
||||||
|
|
||||||
private:
|
|
||||||
quint32 refcount = 0;
|
|
||||||
PwBindableObject* object;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! Contains links to all pipewire objects.
|
|
||||||
class Pipewire: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
// clang-format off
|
|
||||||
/// All pipewire nodes.
|
|
||||||
Q_PROPERTY(QQmlListProperty<PwNodeIface> nodes READ nodes NOTIFY nodesChanged);
|
|
||||||
/// All pipewire links.
|
|
||||||
Q_PROPERTY(QQmlListProperty<PwLinkIface> links READ links NOTIFY linksChanged);
|
|
||||||
/// All pipewire link groups.
|
|
||||||
Q_PROPERTY(QQmlListProperty<PwLinkGroupIface> linkGroups READ linkGroups NOTIFY linkGroupsChanged);
|
|
||||||
/// The default audio sink or `null`.
|
|
||||||
Q_PROPERTY(PwNodeIface* defaultAudioSink READ defaultAudioSink NOTIFY defaultAudioSinkChanged);
|
|
||||||
/// The default audio source or `null`.
|
|
||||||
Q_PROPERTY(PwNodeIface* defaultAudioSource READ defaultAudioSource NOTIFY defaultAudioSourceChanged);
|
|
||||||
// clang-format on
|
|
||||||
QML_ELEMENT;
|
|
||||||
QML_SINGLETON;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit Pipewire(QObject* parent = nullptr);
|
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<PwNodeIface> nodes();
|
|
||||||
[[nodiscard]] QQmlListProperty<PwLinkIface> links();
|
|
||||||
[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
|
|
||||||
[[nodiscard]] PwNodeIface* defaultAudioSink() const;
|
|
||||||
[[nodiscard]] PwNodeIface* defaultAudioSource() const;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nodesChanged();
|
|
||||||
void linksChanged();
|
|
||||||
void linkGroupsChanged();
|
|
||||||
void defaultAudioSinkChanged();
|
|
||||||
void defaultAudioSourceChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onNodeAdded(PwNode* node);
|
|
||||||
void onNodeRemoved(QObject* object);
|
|
||||||
void onLinkAdded(PwLink* link);
|
|
||||||
void onLinkRemoved(QObject* object);
|
|
||||||
void onLinkGroupAdded(PwLinkGroup* group);
|
|
||||||
void onLinkGroupRemoved(QObject* object);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static qsizetype nodesCount(QQmlListProperty<PwNodeIface>* property);
|
|
||||||
static PwNodeIface* nodeAt(QQmlListProperty<PwNodeIface>* property, qsizetype index);
|
|
||||||
static qsizetype linksCount(QQmlListProperty<PwLinkIface>* property);
|
|
||||||
static PwLinkIface* linkAt(QQmlListProperty<PwLinkIface>* property, qsizetype index);
|
|
||||||
static qsizetype linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property);
|
|
||||||
static PwLinkGroupIface*
|
|
||||||
linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index);
|
|
||||||
|
|
||||||
QVector<PwNodeIface*> mNodes;
|
|
||||||
QVector<PwLinkIface*> mLinks;
|
|
||||||
QVector<PwLinkGroupIface*> mLinkGroups;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! Tracks all link connections to a given node.
|
|
||||||
class PwNodeLinkTracker: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
// clang-format off
|
|
||||||
/// The node to track connections to.
|
|
||||||
Q_PROPERTY(PwNodeIface* node READ node WRITE setNode NOTIFY nodeChanged);
|
|
||||||
/// Link groups connected to the given node.
|
|
||||||
///
|
|
||||||
/// If the node is a sink, links which target the node will be tracked.
|
|
||||||
/// If the node is a source, links which source the node will be tracked.
|
|
||||||
Q_PROPERTY(QQmlListProperty<PwLinkGroupIface> linkGroups READ linkGroups NOTIFY linkGroupsChanged);
|
|
||||||
// clang-format on
|
|
||||||
QML_ELEMENT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwNodeLinkTracker(QObject* parent = nullptr): QObject(parent) {}
|
|
||||||
|
|
||||||
[[nodiscard]] PwNodeIface* node() const;
|
|
||||||
void setNode(PwNodeIface* node);
|
|
||||||
|
|
||||||
[[nodiscard]] QQmlListProperty<PwLinkGroupIface> linkGroups();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nodeChanged();
|
|
||||||
void linkGroupsChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onNodeDestroyed();
|
|
||||||
void onLinkGroupCreated(PwLinkGroup* linkGroup);
|
|
||||||
void onLinkGroupDestroyed(QObject* object);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static qsizetype linkGroupsCount(QQmlListProperty<PwLinkGroupIface>* property);
|
|
||||||
static PwLinkGroupIface*
|
|
||||||
linkGroupAt(QQmlListProperty<PwLinkGroupIface>* property, qsizetype index);
|
|
||||||
|
|
||||||
void updateLinks();
|
|
||||||
|
|
||||||
PwNodeIface* mNode = nullptr;
|
|
||||||
QVector<PwLinkGroupIface*> mLinkGroups;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! Audio specific properties of pipewire nodes.
|
|
||||||
class PwNodeAudioIface: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
/// If the node is currently muted. Setting this property changes the mute state.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged);
|
|
||||||
/// The average volume over all channels of the node.
|
|
||||||
/// Setting this property modifies the volume of all channels proportionately.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(float volume READ averageVolume WRITE setAverageVolume NOTIFY volumesChanged);
|
|
||||||
/// The audio channels present on the node.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(QVector<PwAudioChannel::Enum> channels READ channels NOTIFY channelsChanged);
|
|
||||||
/// The volumes of each audio channel individually. Each entry corrosponds to
|
|
||||||
/// the channel at the same index in `channels`. `volumes` and `channels` will always be
|
|
||||||
/// the same length.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(QVector<float> volumes READ volumes WRITE setVolumes NOTIFY volumesChanged);
|
|
||||||
QML_NAMED_ELEMENT(PwNodeAudio);
|
|
||||||
QML_UNCREATABLE("PwNodeAudio cannot be created directly");
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwNodeAudioIface(PwNodeBoundAudio* boundData, QObject* parent);
|
|
||||||
|
|
||||||
[[nodiscard]] bool isMuted() const;
|
|
||||||
void setMuted(bool muted);
|
|
||||||
|
|
||||||
[[nodiscard]] float averageVolume() const;
|
|
||||||
void setAverageVolume(float volume);
|
|
||||||
|
|
||||||
[[nodiscard]] QVector<PwAudioChannel::Enum> channels() const;
|
|
||||||
|
|
||||||
[[nodiscard]] QVector<float> volumes() const;
|
|
||||||
void setVolumes(const QVector<float>& volumes);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void mutedChanged();
|
|
||||||
void channelsChanged();
|
|
||||||
void volumesChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
PwNodeBoundAudio* boundData;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! A node in the pipewire connection graph.
|
|
||||||
class PwNodeIface: public PwObjectIface {
|
|
||||||
Q_OBJECT;
|
|
||||||
/// The pipewire object id of the node.
|
|
||||||
///
|
|
||||||
/// Mainly useful for debugging. you can inspect the node directly
|
|
||||||
/// with `pw-cli i <id>`.
|
|
||||||
Q_PROPERTY(quint32 id READ id CONSTANT);
|
|
||||||
/// The node's name, corrosponding to the object's `node.name` property.
|
|
||||||
Q_PROPERTY(QString name READ name CONSTANT);
|
|
||||||
/// The node's description, corrosponding to the object's `node.description` property.
|
|
||||||
///
|
|
||||||
/// May be empty. Generally more human readable than `name`.
|
|
||||||
Q_PROPERTY(QString description READ description CONSTANT);
|
|
||||||
/// The node's nickname, corrosponding to the object's `node.nickname` property.
|
|
||||||
///
|
|
||||||
/// May be empty. Generally but not always more human readable than `description`.
|
|
||||||
Q_PROPERTY(QString nickname READ nickname CONSTANT);
|
|
||||||
/// If `true`, then the node accepts audio input from other nodes,
|
|
||||||
/// if `false` the node outputs audio to other nodes.
|
|
||||||
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.
|
|
||||||
Q_PROPERTY(bool isStream READ isStream CONSTANT);
|
|
||||||
/// The property set present on the node, as an object containing key-value pairs.
|
|
||||||
/// You can inspect this directly with `pw-cli i <id>`.
|
|
||||||
///
|
|
||||||
/// A few properties of note, which may or may not be present:
|
|
||||||
/// - `application.name` - A suggested human readable name for the node.
|
|
||||||
/// - `application.icon-name` - The name of an icon recommended to display for the node.
|
|
||||||
/// - `media.name` - A description of the currently playing media.
|
|
||||||
/// (more likely to be present than `media.title` and `media.artist`)
|
|
||||||
/// - `media.title` - The title of the currently playing media.
|
|
||||||
/// - `media.artist` - The artist of the currently playing media.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the node is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(QVariantMap properties READ properties NOTIFY propertiesChanged);
|
|
||||||
/// Extra information present only if the node sends or receives audio.
|
|
||||||
Q_PROPERTY(PwNodeAudioIface* audio READ audio CONSTANT);
|
|
||||||
QML_NAMED_ELEMENT(PwNode);
|
|
||||||
QML_UNCREATABLE("PwNodes cannot be created directly");
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwNodeIface(PwNode* node);
|
|
||||||
|
|
||||||
[[nodiscard]] PwNode* node() const;
|
|
||||||
[[nodiscard]] quint32 id() const;
|
|
||||||
[[nodiscard]] QString name() const;
|
|
||||||
[[nodiscard]] QString description() const;
|
|
||||||
[[nodiscard]] QString nickname() const;
|
|
||||||
[[nodiscard]] bool isSink() const;
|
|
||||||
[[nodiscard]] bool isStream() const;
|
|
||||||
[[nodiscard]] QVariantMap properties() const;
|
|
||||||
[[nodiscard]] PwNodeAudioIface* audio() const;
|
|
||||||
|
|
||||||
static PwNodeIface* instance(PwNode* node);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void propertiesChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
PwNode* mNode;
|
|
||||||
PwNodeAudioIface* audioIface = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! A connection between pipewire nodes.
|
|
||||||
/// Note that there is one link per *channel* of a connection between nodes.
|
|
||||||
/// You usually want [PwLinkGroup](../pwlinkgroup).
|
|
||||||
class PwLinkIface: public PwObjectIface {
|
|
||||||
Q_OBJECT;
|
|
||||||
/// The pipewire object id of the link.
|
|
||||||
///
|
|
||||||
/// Mainly useful for debugging. you can inspect the link directly
|
|
||||||
/// with `pw-cli i <id>`.
|
|
||||||
Q_PROPERTY(quint32 id READ id CONSTANT);
|
|
||||||
/// The node that is *receiving* information. (the sink)
|
|
||||||
Q_PROPERTY(PwNodeIface* target READ target CONSTANT);
|
|
||||||
/// The node that is *sending* information. (the source)
|
|
||||||
Q_PROPERTY(PwNodeIface* source READ source CONSTANT);
|
|
||||||
/// The current state of the link.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the link is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged);
|
|
||||||
QML_NAMED_ELEMENT(PwLink);
|
|
||||||
QML_UNCREATABLE("PwLinks cannot be created directly");
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwLinkIface(PwLink* link);
|
|
||||||
|
|
||||||
[[nodiscard]] PwLink* link() const;
|
|
||||||
[[nodiscard]] quint32 id() const;
|
|
||||||
[[nodiscard]] PwNodeIface* target() const;
|
|
||||||
[[nodiscard]] PwNodeIface* source() const;
|
|
||||||
[[nodiscard]] PwLinkState::Enum state() const;
|
|
||||||
|
|
||||||
static PwLinkIface* instance(PwLink* link);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void stateChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
PwLink* mLink;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! A group of connections between pipewire nodes.
|
|
||||||
/// A group of connections between pipewire nodes, one per source->target pair.
|
|
||||||
class PwLinkGroupIface
|
|
||||||
: public QObject
|
|
||||||
, public PwObjectRefIface {
|
|
||||||
Q_OBJECT;
|
|
||||||
/// The node that is *receiving* information. (the sink)
|
|
||||||
Q_PROPERTY(PwNodeIface* target READ target CONSTANT);
|
|
||||||
/// The node that is *sending* information. (the source)
|
|
||||||
Q_PROPERTY(PwNodeIface* source READ source CONSTANT);
|
|
||||||
/// The current state of the link group.
|
|
||||||
///
|
|
||||||
/// **This property is invalid unless the link is [bound](../pwobjecttracker).**
|
|
||||||
Q_PROPERTY(PwLinkState::Enum state READ state NOTIFY stateChanged);
|
|
||||||
QML_NAMED_ELEMENT(PwLinkGroup);
|
|
||||||
QML_UNCREATABLE("PwLinkGroups cannot be created directly");
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwLinkGroupIface(PwLinkGroup* group);
|
|
||||||
// destructor should ONLY be called by the pw object destructor, making an unref unnecessary
|
|
||||||
~PwLinkGroupIface() override = default;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwLinkGroupIface);
|
|
||||||
|
|
||||||
void ref() override;
|
|
||||||
void unref() override;
|
|
||||||
|
|
||||||
[[nodiscard]] PwLinkGroup* group() const;
|
|
||||||
[[nodiscard]] PwNodeIface* target() const;
|
|
||||||
[[nodiscard]] PwNodeIface* source() const;
|
|
||||||
[[nodiscard]] PwLinkState::Enum state() const;
|
|
||||||
|
|
||||||
static PwLinkGroupIface* instance(PwLinkGroup* group);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void stateChanged();
|
|
||||||
|
|
||||||
private:
|
|
||||||
PwLinkGroup* mGroup;
|
|
||||||
};
|
|
||||||
|
|
||||||
///! Binds pipewire objects.
|
|
||||||
/// If the object list of at least one PwObjectTracker contains a given pipewire object,
|
|
||||||
/// it will become *bound* and you will be able to interact with bound-only properties.
|
|
||||||
class PwObjectTracker: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
/// The list of objects to bind.
|
|
||||||
Q_PROPERTY(QList<QObject*> objects READ objects WRITE setObjects NOTIFY objectsChanged);
|
|
||||||
QML_ELEMENT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwObjectTracker(QObject* parent = nullptr): QObject(parent) {}
|
|
||||||
~PwObjectTracker() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwObjectTracker);
|
|
||||||
|
|
||||||
[[nodiscard]] QList<QObject*> objects() const;
|
|
||||||
void setObjects(const QList<QObject*>& objects);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void objectsChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void objectDestroyed(QObject* object);
|
|
||||||
|
|
||||||
private:
|
|
||||||
void clearList();
|
|
||||||
|
|
||||||
QList<QObject*> trackedObjects;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,193 +0,0 @@
|
||||||
#include "registry.hpp"
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/extensions/metadata.h>
|
|
||||||
#include <pipewire/link.h>
|
|
||||||
#include <pipewire/node.h>
|
|
||||||
#include <pipewire/proxy.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "core.hpp"
|
|
||||||
#include "link.hpp"
|
|
||||||
#include "metadata.hpp"
|
|
||||||
#include "node.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(logRegistry, "quickshell.service.pipewire.registry", QtWarningMsg);
|
|
||||||
|
|
||||||
PwBindableObject::~PwBindableObject() {
|
|
||||||
if (this->id != 0) {
|
|
||||||
qCFatal(logRegistry) << "Destroyed pipewire object" << this
|
|
||||||
<< "without causing safeDestroy. THIS IS UNDEFINED BEHAVIOR.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::init(PwRegistry* registry, quint32 id, quint32 perms) {
|
|
||||||
this->id = id;
|
|
||||||
this->perms = perms;
|
|
||||||
this->registry = registry;
|
|
||||||
this->setParent(registry);
|
|
||||||
qCDebug(logRegistry) << "Creating object" << this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::safeDestroy() {
|
|
||||||
this->unbind();
|
|
||||||
qCDebug(logRegistry) << "Destroying object" << this;
|
|
||||||
emit this->destroying(this);
|
|
||||||
this->id = 0;
|
|
||||||
delete this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::debugId(QDebug& debug) const {
|
|
||||||
auto saver = QDebugStateSaver(debug);
|
|
||||||
debug.nospace() << this->id << "/" << (this->object == nullptr ? "unbound" : "bound");
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::ref() {
|
|
||||||
this->refcount++;
|
|
||||||
if (this->refcount == 1) this->bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::unref() {
|
|
||||||
this->refcount--;
|
|
||||||
if (this->refcount == 0) this->unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::bind() {
|
|
||||||
qCDebug(logRegistry) << "Bound object" << this;
|
|
||||||
this->bindHooks();
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObject::unbind() {
|
|
||||||
if (this->object == nullptr) return;
|
|
||||||
qCDebug(logRegistry) << "Unbinding object" << this;
|
|
||||||
this->unbindHooks();
|
|
||||||
pw_proxy_destroy(this->object);
|
|
||||||
this->object = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const PwBindableObject* object) {
|
|
||||||
if (object == nullptr) {
|
|
||||||
debug << "PwBindableObject(0x0)";
|
|
||||||
} else {
|
|
||||||
auto saver = QDebugStateSaver(debug);
|
|
||||||
// 0 if not present, start of class name if present
|
|
||||||
auto idx = QString(object->metaObject()->className()).lastIndexOf(':') + 1;
|
|
||||||
debug.nospace() << (object->metaObject()->className() + idx) << '(' // NOLINT
|
|
||||||
<< static_cast<const void*>(object) << ", id=";
|
|
||||||
object->debugId(debug);
|
|
||||||
debug << ')';
|
|
||||||
}
|
|
||||||
|
|
||||||
return debug;
|
|
||||||
}
|
|
||||||
|
|
||||||
PwBindableObjectRef::PwBindableObjectRef(PwBindableObject* object) { this->setObject(object); }
|
|
||||||
|
|
||||||
PwBindableObjectRef::~PwBindableObjectRef() { this->setObject(nullptr); }
|
|
||||||
|
|
||||||
void PwBindableObjectRef::setObject(PwBindableObject* object) {
|
|
||||||
if (this->mObject != nullptr) {
|
|
||||||
this->mObject->unref();
|
|
||||||
QObject::disconnect(this->mObject, nullptr, this, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
this->mObject = object;
|
|
||||||
|
|
||||||
if (object != nullptr) {
|
|
||||||
this->mObject->ref();
|
|
||||||
QObject::connect(object, &QObject::destroyed, this, &PwBindableObjectRef::onObjectDestroyed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwBindableObjectRef::onObjectDestroyed() {
|
|
||||||
// allow references to it so consumers can disconnect themselves
|
|
||||||
emit this->objectDestroyed();
|
|
||||||
this->mObject = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwRegistry::init(PwCore& core) {
|
|
||||||
this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0);
|
|
||||||
pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pw_registry_events PwRegistry::EVENTS = {
|
|
||||||
.version = PW_VERSION_REGISTRY_EVENTS,
|
|
||||||
.global = &PwRegistry::onGlobal,
|
|
||||||
.global_remove = &PwRegistry::onGlobalRemoved,
|
|
||||||
};
|
|
||||||
|
|
||||||
void PwRegistry::onGlobal(
|
|
||||||
void* data,
|
|
||||||
quint32 id,
|
|
||||||
quint32 permissions,
|
|
||||||
const char* type,
|
|
||||||
quint32 /*version*/,
|
|
||||||
const spa_dict* props
|
|
||||||
) {
|
|
||||||
auto* self = static_cast<PwRegistry*>(data);
|
|
||||||
|
|
||||||
if (strcmp(type, PW_TYPE_INTERFACE_Metadata) == 0) {
|
|
||||||
auto* meta = new PwMetadata();
|
|
||||||
meta->init(self, id, permissions);
|
|
||||||
meta->initProps(props);
|
|
||||||
|
|
||||||
self->metadata.emplace(id, meta);
|
|
||||||
meta->bind();
|
|
||||||
} else if (strcmp(type, PW_TYPE_INTERFACE_Link) == 0) {
|
|
||||||
auto* link = new PwLink();
|
|
||||||
link->init(self, id, permissions);
|
|
||||||
link->initProps(props);
|
|
||||||
|
|
||||||
self->links.emplace(id, link);
|
|
||||||
self->addLinkToGroup(link);
|
|
||||||
emit self->linkAdded(link);
|
|
||||||
} else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) {
|
|
||||||
auto* node = new PwNode();
|
|
||||||
node->init(self, id, permissions);
|
|
||||||
node->initProps(props);
|
|
||||||
|
|
||||||
self->nodes.emplace(id, node);
|
|
||||||
emit self->nodeAdded(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
|
|
||||||
auto* self = static_cast<PwRegistry*>(data);
|
|
||||||
|
|
||||||
if (auto* meta = self->metadata.value(id)) {
|
|
||||||
self->metadata.remove(id);
|
|
||||||
meta->safeDestroy();
|
|
||||||
} else if (auto* link = self->links.value(id)) {
|
|
||||||
self->links.remove(id);
|
|
||||||
link->safeDestroy();
|
|
||||||
} else if (auto* node = self->nodes.value(id)) {
|
|
||||||
self->nodes.remove(id);
|
|
||||||
node->safeDestroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwRegistry::addLinkToGroup(PwLink* link) {
|
|
||||||
for (auto* group: this->linkGroups) {
|
|
||||||
if (group->tryAddLink(link)) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* group = new PwLinkGroup(link);
|
|
||||||
QObject::connect(group, &QObject::destroyed, this, &PwRegistry::onLinkGroupDestroyed);
|
|
||||||
this->linkGroups.push_back(group);
|
|
||||||
emit this->linkGroupAdded(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
void PwRegistry::onLinkGroupDestroyed(QObject* object) {
|
|
||||||
auto* group = static_cast<PwLinkGroup*>(object); // NOLINT
|
|
||||||
this->linkGroups.removeOne(group);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,160 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <pipewire/core.h>
|
|
||||||
#include <pipewire/proxy.h>
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qdebug.h>
|
|
||||||
#include <qhash.h>
|
|
||||||
#include <qloggingcategory.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
|
|
||||||
#include "core.hpp"
|
|
||||||
|
|
||||||
namespace qs::service::pipewire {
|
|
||||||
|
|
||||||
Q_DECLARE_LOGGING_CATEGORY(logRegistry);
|
|
||||||
|
|
||||||
class PwRegistry;
|
|
||||||
class PwMetadata;
|
|
||||||
class PwNode;
|
|
||||||
class PwLink;
|
|
||||||
class PwLinkGroup;
|
|
||||||
|
|
||||||
class PwBindableObject: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
PwBindableObject() = default;
|
|
||||||
~PwBindableObject() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwBindableObject);
|
|
||||||
|
|
||||||
// constructors and destructors can't do virtual calls.
|
|
||||||
virtual void init(PwRegistry* registry, quint32 id, quint32 perms);
|
|
||||||
virtual void initProps(const spa_dict* /*props*/) {}
|
|
||||||
virtual void safeDestroy();
|
|
||||||
|
|
||||||
quint32 id = 0;
|
|
||||||
quint32 perms = 0;
|
|
||||||
|
|
||||||
void debugId(QDebug& debug) const;
|
|
||||||
void ref();
|
|
||||||
void unref();
|
|
||||||
|
|
||||||
signals:
|
|
||||||
// goes with safeDestroy
|
|
||||||
void destroying(PwBindableObject* self);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual void bind();
|
|
||||||
void unbind();
|
|
||||||
virtual void bindHooks() {};
|
|
||||||
virtual void unbindHooks() {};
|
|
||||||
|
|
||||||
quint32 refcount = 0;
|
|
||||||
pw_proxy* object = nullptr;
|
|
||||||
PwRegistry* registry = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
QDebug operator<<(QDebug debug, const PwBindableObject* object);
|
|
||||||
|
|
||||||
template <typename T, const char* INTERFACE, quint32 VERSION>
|
|
||||||
class PwBindable: public PwBindableObject {
|
|
||||||
public:
|
|
||||||
T* proxy() {
|
|
||||||
return reinterpret_cast<T*>(this->object); // NOLINT
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void bind() override {
|
|
||||||
if (this->object != nullptr) return;
|
|
||||||
auto* object =
|
|
||||||
pw_registry_bind(this->registry->object, this->id, INTERFACE, VERSION, 0); // NOLINT
|
|
||||||
this->object = static_cast<pw_proxy*>(object);
|
|
||||||
this->PwBindableObject::bind();
|
|
||||||
}
|
|
||||||
|
|
||||||
friend class PwRegistry;
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwBindableObjectRef: public QObject {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit PwBindableObjectRef(PwBindableObject* object = nullptr);
|
|
||||||
~PwBindableObjectRef() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(PwBindableObjectRef);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void objectDestroyed();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onObjectDestroyed();
|
|
||||||
|
|
||||||
protected:
|
|
||||||
void setObject(PwBindableObject* object);
|
|
||||||
|
|
||||||
PwBindableObject* mObject = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename T>
|
|
||||||
class PwBindableRef: public PwBindableObjectRef {
|
|
||||||
public:
|
|
||||||
explicit PwBindableRef(T* object = nullptr): PwBindableObjectRef(object) {}
|
|
||||||
|
|
||||||
void setObject(T* object) { this->PwBindableObjectRef::setObject(object); }
|
|
||||||
|
|
||||||
T* object() { return this->mObject; }
|
|
||||||
};
|
|
||||||
|
|
||||||
class PwRegistry
|
|
||||||
: public QObject
|
|
||||||
, public PwObject<pw_registry> {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
void init(PwCore& core);
|
|
||||||
|
|
||||||
//QHash<quint32, PwClient*> clients;
|
|
||||||
QHash<quint32, PwMetadata*> metadata;
|
|
||||||
QHash<quint32, PwNode*> nodes;
|
|
||||||
QHash<quint32, PwLink*> links;
|
|
||||||
QVector<PwLinkGroup*> linkGroups;
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void nodeAdded(PwNode* node);
|
|
||||||
void linkAdded(PwLink* link);
|
|
||||||
void linkGroupAdded(PwLinkGroup* group);
|
|
||||||
void metadataUpdate(
|
|
||||||
PwMetadata* owner,
|
|
||||||
quint32 subject,
|
|
||||||
const char* key,
|
|
||||||
const char* type,
|
|
||||||
const char* value
|
|
||||||
);
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onLinkGroupDestroyed(QObject* object);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static const pw_registry_events EVENTS;
|
|
||||||
|
|
||||||
static void onGlobal(
|
|
||||||
void* data,
|
|
||||||
quint32 id,
|
|
||||||
quint32 permissions,
|
|
||||||
const char* type,
|
|
||||||
quint32 version,
|
|
||||||
const spa_dict* props
|
|
||||||
);
|
|
||||||
|
|
||||||
static void onGlobalRemoved(void* data, quint32 id);
|
|
||||||
|
|
||||||
void addLinkToGroup(PwLink* link);
|
|
||||||
|
|
||||||
SpaHook listener;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::service::pipewire
|
|
|
@ -1,4 +1,4 @@
|
||||||
name = "Quickshell.Services.SystemTray"
|
name = "Quickshell.Service.SystemTray"
|
||||||
description = "Types for implementing a system tray"
|
description = "Types for implementing a system tray"
|
||||||
headers = [ "qml.hpp" ]
|
headers = [ "qml.hpp" ]
|
||||||
-----
|
-----
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
qt_add_library(quickshell-hyprland STATIC)
|
qt_add_library(quickshell-hyprland STATIC)
|
||||||
qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1)
|
qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1)
|
||||||
|
|
||||||
if (HYPRLAND_FOCUS_GRAB)
|
add_subdirectory(focus_grab)
|
||||||
add_subdirectory(focus_grab)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if (HYPRLAND_GLOBAL_SHORTCUTS)
|
|
||||||
add_subdirectory(global_shortcuts)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS})
|
target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS})
|
||||||
|
|
||||||
|
|
|
@ -4,18 +4,11 @@ qt_add_library(quickshell-hyprland-focus-grab STATIC
|
||||||
qml.cpp
|
qml.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
qt_add_qml_module(quickshell-hyprland-focus-grab
|
qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1)
|
||||||
URI Quickshell.Hyprland._FocusGrab
|
|
||||||
VERSION 0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp)
|
add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp)
|
||||||
|
|
||||||
wl_proto(quickshell-hyprland-focus-grab
|
wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml")
|
||||||
hyprland-focus-grab-v1
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client)
|
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client)
|
||||||
target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS})
|
target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS})
|
||||||
|
|
||||||
|
@ -23,7 +16,4 @@ qs_pch(quickshell-hyprland-focus-grab)
|
||||||
qs_pch(quickshell-hyprland-focus-grabplugin)
|
qs_pch(quickshell-hyprland-focus-grabplugin)
|
||||||
qs_pch(quickshell-hyprland-focus-grab-init)
|
qs_pch(quickshell-hyprland-focus-grab-init)
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE
|
target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init)
|
||||||
quickshell-hyprland-focus-grabplugin
|
|
||||||
quickshell-hyprland-focus-grab-init
|
|
||||||
)
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ void HyprlandFocusGrab::onProxyConnected() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void HyprlandFocusGrab::tryActivate() {
|
void HyprlandFocusGrab::tryActivate() {
|
||||||
|
qDebug() << "tryactivate";
|
||||||
if (!this->targetActive || this->isActive()) return;
|
if (!this->targetActive || this->isActive()) return;
|
||||||
|
|
||||||
auto* manager = FocusGrabManager::instance();
|
auto* manager = FocusGrabManager::instance();
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
qt_add_library(quickshell-hyprland-global-shortcuts STATIC
|
|
||||||
qml.cpp
|
|
||||||
manager.cpp
|
|
||||||
shortcut.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
qt_add_qml_module(quickshell-hyprland-global-shortcuts
|
|
||||||
URI Quickshell.Hyprland._GlobalShortcuts
|
|
||||||
VERSION 0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
add_library(quickshell-hyprland-global-shortcuts-init OBJECT init.cpp)
|
|
||||||
|
|
||||||
wl_proto(quickshell-hyprland-global-shortcuts
|
|
||||||
hyprland-global-shortcuts-v1
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE ${QT_DEPS} wayland-client)
|
|
||||||
target_link_libraries(quickshell-hyprland-global-shortcuts-init PRIVATE ${QT_DEPS})
|
|
||||||
|
|
||||||
qs_pch(quickshell-hyprland-global-shortcuts)
|
|
||||||
qs_pch(quickshell-hyprland-global-shortcutsplugin)
|
|
||||||
qs_pch(quickshell-hyprland-global-shortcuts-init)
|
|
||||||
|
|
||||||
target_link_libraries(quickshell PRIVATE
|
|
||||||
quickshell-hyprland-global-shortcutsplugin
|
|
||||||
quickshell-hyprland-global-shortcuts-init
|
|
||||||
)
|
|
|
@ -1,112 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<protocol name="hyprland_global_shortcuts_v1">
|
|
||||||
<copyright>
|
|
||||||
Copyright © 2022 Vaxry
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are met:
|
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
||||||
this list of conditions and the following disclaimer in the documentation
|
|
||||||
and/or other materials provided with the distribution.
|
|
||||||
|
|
||||||
3. Neither the name of the copyright holder nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
</copyright>
|
|
||||||
|
|
||||||
<description summary="registering global shortcuts">
|
|
||||||
This protocol allows a client to register triggerable actions,
|
|
||||||
meant to be global shortcuts.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<interface name="hyprland_global_shortcuts_manager_v1" version="1">
|
|
||||||
<description summary="manager to register global shortcuts">
|
|
||||||
This object is a manager which offers requests to create global shortcuts.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<request name="register_shortcut">
|
|
||||||
<description summary="register a shortcut">
|
|
||||||
Register a new global shortcut.
|
|
||||||
|
|
||||||
A global shortcut is anonymous, meaning the app does not know what key(s) trigger it.
|
|
||||||
|
|
||||||
The shortcut's keybinding shall be dealt with by the compositor.
|
|
||||||
|
|
||||||
In the case of a duplicate app_id + id combination, the already_taken protocol error is raised.
|
|
||||||
</description>
|
|
||||||
<arg name="shortcut" type="new_id" interface="hyprland_global_shortcut_v1"/>
|
|
||||||
<arg name="id" type="string" summary="a unique id for the shortcut"/>
|
|
||||||
<arg name="app_id" type="string" summary="the app_id of the application requesting the shortcut"/>
|
|
||||||
<arg name="description" type="string" summary="user-readable text describing what the shortcut does."/>
|
|
||||||
<arg name="trigger_description" type="string" summary="user-readable text describing how to trigger the shortcut for the client to render."/>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="destroy the manager">
|
|
||||||
All objects created by the manager will still remain valid, until their
|
|
||||||
appropriate destroy request has been called.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
|
|
||||||
<enum name="error">
|
|
||||||
<entry name="already_taken" value="0"
|
|
||||||
summary="the app_id + id combination has already been registered."/>
|
|
||||||
</enum>
|
|
||||||
</interface>
|
|
||||||
|
|
||||||
<interface name="hyprland_global_shortcut_v1" version="1">
|
|
||||||
<description summary="a shortcut">
|
|
||||||
This object represents a single shortcut.
|
|
||||||
</description>
|
|
||||||
|
|
||||||
<event name="pressed">
|
|
||||||
<description summary="keystroke pressed">
|
|
||||||
The keystroke was pressed.
|
|
||||||
|
|
||||||
tv_ values hold the timestamp of the occurrence.
|
|
||||||
</description>
|
|
||||||
<arg name="tv_sec_hi" type="uint"
|
|
||||||
summary="high 32 bits of the seconds part of the timestamp"/>
|
|
||||||
<arg name="tv_sec_lo" type="uint"
|
|
||||||
summary="low 32 bits of the seconds part of the timestamp"/>
|
|
||||||
<arg name="tv_nsec" type="uint"
|
|
||||||
summary="nanoseconds part of the timestamp"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<event name="released">
|
|
||||||
<description summary="keystroke released">
|
|
||||||
The keystroke was released.
|
|
||||||
|
|
||||||
tv_ values hold the timestamp of the occurrence.
|
|
||||||
</description>
|
|
||||||
<arg name="tv_sec_hi" type="uint"
|
|
||||||
summary="high 32 bits of the seconds part of the timestamp"/>
|
|
||||||
<arg name="tv_sec_lo" type="uint"
|
|
||||||
summary="low 32 bits of the seconds part of the timestamp"/>
|
|
||||||
<arg name="tv_nsec" type="uint"
|
|
||||||
summary="nanoseconds part of the timestamp"/>
|
|
||||||
</event>
|
|
||||||
|
|
||||||
<request name="destroy" type="destructor">
|
|
||||||
<description summary="delete this object, used or not">
|
|
||||||
Destroys the shortcut. Can be sent at any time by the client.
|
|
||||||
</description>
|
|
||||||
</request>
|
|
||||||
</interface>
|
|
||||||
</protocol>
|
|
|
@ -1,20 +0,0 @@
|
||||||
#include <qqml.h>
|
|
||||||
|
|
||||||
#include "../../../core/plugin.hpp"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
class HyprlandFocusGrabPlugin: public QuickshellPlugin {
|
|
||||||
void registerTypes() override {
|
|
||||||
qmlRegisterModuleImport(
|
|
||||||
"Quickshell.Hyprland",
|
|
||||||
QQmlModuleImportModuleAny,
|
|
||||||
"Quickshell.Hyprland._GlobalShortcuts",
|
|
||||||
QQmlModuleImportLatest
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin);
|
|
||||||
|
|
||||||
} // namespace
|
|
|
@ -1,55 +0,0 @@
|
||||||
#include "manager.hpp"
|
|
||||||
|
|
||||||
#include <qstring.h>
|
|
||||||
#include <qwaylandclientextension.h>
|
|
||||||
|
|
||||||
#include "shortcut.hpp"
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts::impl {
|
|
||||||
|
|
||||||
GlobalShortcutManager::GlobalShortcutManager()
|
|
||||||
: QWaylandClientExtensionTemplate<GlobalShortcutManager>(1) {
|
|
||||||
this->initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalShortcut* GlobalShortcutManager::registerShortcut(
|
|
||||||
const QString& appid,
|
|
||||||
const QString& name,
|
|
||||||
const QString& description,
|
|
||||||
const QString& triggerDescription
|
|
||||||
) {
|
|
||||||
auto shortcut = this->shortcuts.value({appid, name});
|
|
||||||
|
|
||||||
if (shortcut.second != nullptr) {
|
|
||||||
this->shortcuts.insert({appid, name}, {shortcut.first + 1, shortcut.second});
|
|
||||||
return shortcut.second;
|
|
||||||
} else {
|
|
||||||
auto* shortcutObj = this->register_shortcut(name, appid, description, triggerDescription);
|
|
||||||
auto* managedObj = new GlobalShortcut(shortcutObj);
|
|
||||||
this->shortcuts.insert({appid, name}, {1, managedObj});
|
|
||||||
return managedObj;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcutManager::unregisterShortcut(const QString& appid, const QString& name) {
|
|
||||||
auto shortcut = this->shortcuts.value({appid, name});
|
|
||||||
|
|
||||||
if (shortcut.first > 1) {
|
|
||||||
this->shortcuts.insert({appid, name}, {shortcut.first - 1, shortcut.second});
|
|
||||||
} else {
|
|
||||||
delete shortcut.second;
|
|
||||||
this->shortcuts.remove({appid, name});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GlobalShortcutManager* GlobalShortcutManager::instance() {
|
|
||||||
static GlobalShortcutManager* instance = nullptr; // NOLINT
|
|
||||||
|
|
||||||
if (instance == nullptr) {
|
|
||||||
instance = new GlobalShortcutManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts::impl
|
|
|
@ -1,34 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qcontainerfwd.h>
|
|
||||||
#include <qhash.h>
|
|
||||||
#include <qstring.h>
|
|
||||||
#include <qwayland-hyprland-global-shortcuts-v1.h>
|
|
||||||
#include <qwaylandclientextension.h>
|
|
||||||
|
|
||||||
#include "shortcut.hpp"
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts::impl {
|
|
||||||
|
|
||||||
class GlobalShortcutManager
|
|
||||||
: public QWaylandClientExtensionTemplate<GlobalShortcutManager>
|
|
||||||
, public QtWayland::hyprland_global_shortcuts_manager_v1 {
|
|
||||||
public:
|
|
||||||
explicit GlobalShortcutManager();
|
|
||||||
|
|
||||||
GlobalShortcut* registerShortcut(
|
|
||||||
const QString& appid,
|
|
||||||
const QString& name,
|
|
||||||
const QString& description,
|
|
||||||
const QString& triggerDescription
|
|
||||||
);
|
|
||||||
|
|
||||||
void unregisterShortcut(const QString& appid, const QString& name);
|
|
||||||
|
|
||||||
static GlobalShortcutManager* instance();
|
|
||||||
|
|
||||||
private:
|
|
||||||
QHash<QPair<QString, QString>, QPair<qint32, GlobalShortcut*>> shortcuts;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts::impl
|
|
|
@ -1,115 +0,0 @@
|
||||||
#include "qml.hpp"
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include <qlogging.h>
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "manager.hpp"
|
|
||||||
#include "shortcut.hpp"
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts {
|
|
||||||
using impl::GlobalShortcutManager;
|
|
||||||
|
|
||||||
GlobalShortcut::~GlobalShortcut() {
|
|
||||||
auto* manager = GlobalShortcutManager::instance();
|
|
||||||
if (manager != nullptr) {
|
|
||||||
manager->unregisterShortcut(this->mAppid, this->mName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcut::onPostReload() {
|
|
||||||
if (this->mName.isEmpty()) {
|
|
||||||
qWarning() << "Unable to create GlobalShortcut with empty name.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto* manager = GlobalShortcutManager::instance();
|
|
||||||
if (!manager->isActive()) {
|
|
||||||
qWarning() << "The active compositor does not support hyprland_global_shortcuts_v1.";
|
|
||||||
qWarning() << "GlobalShortcut will not work.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->shortcut = manager->registerShortcut(
|
|
||||||
this->mAppid,
|
|
||||||
this->mName,
|
|
||||||
this->mDescription,
|
|
||||||
this->mTriggerDescription
|
|
||||||
);
|
|
||||||
|
|
||||||
QObject::connect(this->shortcut, &ShortcutImpl::pressed, this, &GlobalShortcut::onPressed);
|
|
||||||
QObject::connect(this->shortcut, &ShortcutImpl::released, this, &GlobalShortcut::onReleased);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool GlobalShortcut::isPressed() const { return this->mPressed; }
|
|
||||||
|
|
||||||
QString GlobalShortcut::appid() const { return this->mAppid; }
|
|
||||||
|
|
||||||
void GlobalShortcut::setAppid(QString appid) {
|
|
||||||
if (this->shortcut != nullptr) {
|
|
||||||
qWarning() << "GlobalShortcut cannot be modified after creation.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (appid == this->mAppid) return;
|
|
||||||
|
|
||||||
this->mAppid = std::move(appid);
|
|
||||||
emit this->appidChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString GlobalShortcut::name() const { return this->mName; }
|
|
||||||
|
|
||||||
void GlobalShortcut::setName(QString name) {
|
|
||||||
if (this->shortcut != nullptr) {
|
|
||||||
qWarning() << "GlobalShortcut cannot be modified after creation.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name == this->mName) return;
|
|
||||||
|
|
||||||
this->mName = std::move(name);
|
|
||||||
emit this->nameChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString GlobalShortcut::description() const { return this->mDescription; }
|
|
||||||
|
|
||||||
void GlobalShortcut::setDescription(QString description) {
|
|
||||||
if (this->shortcut != nullptr) {
|
|
||||||
qWarning() << "GlobalShortcut cannot be modified after creation.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description == this->mDescription) return;
|
|
||||||
|
|
||||||
this->mDescription = std::move(description);
|
|
||||||
emit this->descriptionChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
QString GlobalShortcut::triggerDescription() const { return this->mTriggerDescription; }
|
|
||||||
|
|
||||||
void GlobalShortcut::setTriggerDescription(QString triggerDescription) {
|
|
||||||
if (this->shortcut != nullptr) {
|
|
||||||
qWarning() << "GlobalShortcut cannot be modified after creation.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (triggerDescription == this->mTriggerDescription) return;
|
|
||||||
|
|
||||||
this->mTriggerDescription = std::move(triggerDescription);
|
|
||||||
emit this->triggerDescriptionChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcut::onPressed() {
|
|
||||||
this->mPressed = true;
|
|
||||||
emit this->pressed();
|
|
||||||
emit this->pressedChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcut::onReleased() {
|
|
||||||
this->mPressed = false;
|
|
||||||
emit this->released();
|
|
||||||
emit this->pressedChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts
|
|
|
@ -1,108 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qqmlintegration.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
|
|
||||||
#include "../../../core/reload.hpp"
|
|
||||||
#include "shortcut.hpp"
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts {
|
|
||||||
|
|
||||||
///! Hyprland global shortcut.
|
|
||||||
/// Global shortcut implemented with [hyprland_global_shortcuts_v1].
|
|
||||||
///
|
|
||||||
/// You can use this within hyprland as a global shortcut:
|
|
||||||
/// ```
|
|
||||||
/// bind = <modifiers>, <key>, global, <appid>:<name>
|
|
||||||
/// ```
|
|
||||||
/// See [the wiki] for details.
|
|
||||||
///
|
|
||||||
/// > [!WARNING] The shortcuts protocol does not allow duplicate appid + name pairs.
|
|
||||||
/// > Within a single instance of quickshell this is handled internally, and both
|
|
||||||
/// > users will be notified, but multiple instances of quickshell or XDPH may collide.
|
|
||||||
/// >
|
|
||||||
/// > If that happens, whichever client that tries to register the shortcuts last will crash.
|
|
||||||
///
|
|
||||||
/// > [!INFO] This type does *not* use the xdg-desktop-portal global shortcuts protocol,
|
|
||||||
/// > as it is not fully functional without flatpak and would cause a considerably worse
|
|
||||||
/// > user experience from other limitations. It will only work with Hyprland.
|
|
||||||
/// > Note that, as this type bypasses xdg-desktop-portal, XDPH is not required.
|
|
||||||
///
|
|
||||||
/// [hyprland_global_shortcuts_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
|
|
||||||
/// [the wiki]: https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts
|
|
||||||
class GlobalShortcut
|
|
||||||
: public QObject
|
|
||||||
, public PostReloadHook {
|
|
||||||
using ShortcutImpl = impl::GlobalShortcut;
|
|
||||||
|
|
||||||
Q_OBJECT;
|
|
||||||
// clang-format off
|
|
||||||
/// If the keybind is currently pressed.
|
|
||||||
Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged);
|
|
||||||
/// The appid of the shortcut. Defaults to `quickshell`.
|
|
||||||
/// You cannot change this at runtime.
|
|
||||||
///
|
|
||||||
/// If you have more than one shortcut we recommend subclassing
|
|
||||||
/// GlobalShortcut to set this.
|
|
||||||
Q_PROPERTY(QString appid READ appid WRITE setAppid NOTIFY appidChanged);
|
|
||||||
/// The name of the shortcut.
|
|
||||||
/// You cannot change this at runtime.
|
|
||||||
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged);
|
|
||||||
/// The description of the shortcut that appears in `hyprctl globalshortcuts`.
|
|
||||||
/// You cannot change this at runtime.
|
|
||||||
Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged);
|
|
||||||
/// Have not seen this used ever, but included for completeness. Safe to ignore.
|
|
||||||
Q_PROPERTY(QString triggerDescription READ triggerDescription WRITE setTriggerDescription NOTIFY triggerDescriptionChanged);
|
|
||||||
// clang-format on
|
|
||||||
QML_ELEMENT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit GlobalShortcut(QObject* parent = nullptr): QObject(parent) {}
|
|
||||||
~GlobalShortcut() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(GlobalShortcut);
|
|
||||||
|
|
||||||
void onPostReload() override;
|
|
||||||
|
|
||||||
[[nodiscard]] bool isPressed() const;
|
|
||||||
|
|
||||||
[[nodiscard]] QString appid() const;
|
|
||||||
void setAppid(QString appid);
|
|
||||||
|
|
||||||
[[nodiscard]] QString name() const;
|
|
||||||
void setName(QString name);
|
|
||||||
|
|
||||||
[[nodiscard]] QString description() const;
|
|
||||||
void setDescription(QString description);
|
|
||||||
|
|
||||||
[[nodiscard]] QString triggerDescription() const;
|
|
||||||
void setTriggerDescription(QString triggerDescription);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
/// Fired when the keybind is pressed.
|
|
||||||
void pressed();
|
|
||||||
/// Fired when the keybind is released.
|
|
||||||
void released();
|
|
||||||
|
|
||||||
void pressedChanged();
|
|
||||||
void appidChanged();
|
|
||||||
void nameChanged();
|
|
||||||
void descriptionChanged();
|
|
||||||
void triggerDescriptionChanged();
|
|
||||||
|
|
||||||
private slots:
|
|
||||||
void onPressed();
|
|
||||||
void onReleased();
|
|
||||||
|
|
||||||
private:
|
|
||||||
impl::GlobalShortcut* shortcut = nullptr;
|
|
||||||
|
|
||||||
bool mPressed = false;
|
|
||||||
QString mAppid = "quickshell";
|
|
||||||
QString mName;
|
|
||||||
QString mDescription;
|
|
||||||
QString mTriggerDescription;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts
|
|
|
@ -1,33 +0,0 @@
|
||||||
#include "shortcut.hpp"
|
|
||||||
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <wayland-hyprland-global-shortcuts-v1-client-protocol.h>
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts::impl {
|
|
||||||
|
|
||||||
GlobalShortcut::GlobalShortcut(::hyprland_global_shortcut_v1* shortcut) { this->init(shortcut); }
|
|
||||||
|
|
||||||
GlobalShortcut::~GlobalShortcut() {
|
|
||||||
if (this->isInitialized()) {
|
|
||||||
this->destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcut::hyprland_global_shortcut_v1_pressed(
|
|
||||||
quint32 /*unused*/,
|
|
||||||
quint32 /*unused*/,
|
|
||||||
quint32 /*unused*/
|
|
||||||
) {
|
|
||||||
emit this->pressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
void GlobalShortcut::hyprland_global_shortcut_v1_released(
|
|
||||||
quint32 /*unused*/,
|
|
||||||
quint32 /*unused*/,
|
|
||||||
quint32 /*unused*/
|
|
||||||
) {
|
|
||||||
emit this->released();
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts::impl
|
|
|
@ -1,32 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include <qobject.h>
|
|
||||||
#include <qtclasshelpermacros.h>
|
|
||||||
#include <qtmetamacros.h>
|
|
||||||
#include <qtypes.h>
|
|
||||||
#include <qwayland-hyprland-global-shortcuts-v1.h>
|
|
||||||
|
|
||||||
namespace qs::hyprland::global_shortcuts::impl {
|
|
||||||
|
|
||||||
class GlobalShortcut
|
|
||||||
: public QObject
|
|
||||||
, public QtWayland::hyprland_global_shortcut_v1 {
|
|
||||||
Q_OBJECT;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit GlobalShortcut(::hyprland_global_shortcut_v1* shortcut);
|
|
||||||
~GlobalShortcut() override;
|
|
||||||
Q_DISABLE_COPY_MOVE(GlobalShortcut);
|
|
||||||
|
|
||||||
signals:
|
|
||||||
void pressed();
|
|
||||||
void released();
|
|
||||||
|
|
||||||
private:
|
|
||||||
// clang-format off
|
|
||||||
void hyprland_global_shortcut_v1_pressed(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override;
|
|
||||||
void hyprland_global_shortcut_v1_released(quint32 tvSecHi, quint32 tvSecLo, quint32 tvNsec) override;
|
|
||||||
// clang-format on
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace qs::hyprland::global_shortcuts::impl
|
|
|
@ -1,7 +1,6 @@
|
||||||
name = "Quickshell.Hyprland"
|
name = "Quickshell.Hyprland"
|
||||||
description = "Hyprland specific Quickshell types"
|
description = "Hyprland specific Quickshell types"
|
||||||
headers = [
|
headers = [
|
||||||
"focus_grab/qml.hpp",
|
"focus_grab/qml.hpp"
|
||||||
"global_shortcuts/qml.hpp",
|
|
||||||
]
|
]
|
||||||
-----
|
-----
|
||||||
|
|
Loading…
Reference in a new issue