From f7224d84594627e66e3e900e4636e72ac9aac05c Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:08:57 +0200 Subject: [PATCH 01/13] Initial implementation --- include/factory.hpp | 5 +- include/modules/privacy/privacy.hpp | 44 ++++++ include/modules/privacy/privacy_item.hpp | 46 ++++++ include/util/pipewire/pipewire_backend.hpp | 40 +++++ include/util/pipewire/privacy_node_info.hpp | 36 +++++ meson.build | 15 +- meson_options.txt | 1 + resources/icons/meson.build | 6 + .../waybar-privacy-audio-input-symbolic.svg | 4 + .../waybar-privacy-audio-output-symbolic.svg | 2 + .../waybar-privacy-screen-share-symbolic.svg | 7 + resources/icons/waybar_icons.gresource.xml | 8 + src/bar.cpp | 2 +- src/client.cpp | 6 + src/factory.cpp | 8 +- src/modules/privacy/privacy.cpp | 138 ++++++++++++++++ src/modules/privacy/privacy_item.cpp | 140 ++++++++++++++++ src/util/pipewire_backend.cpp | 149 ++++++++++++++++++ 18 files changed, 653 insertions(+), 4 deletions(-) create mode 100644 include/modules/privacy/privacy.hpp create mode 100644 include/modules/privacy/privacy_item.hpp create mode 100644 include/util/pipewire/pipewire_backend.hpp create mode 100644 include/util/pipewire/privacy_node_info.hpp create mode 100644 resources/icons/meson.build create mode 100644 resources/icons/waybar-privacy-audio-input-symbolic.svg create mode 100644 resources/icons/waybar-privacy-audio-output-symbolic.svg create mode 100644 resources/icons/waybar-privacy-screen-share-symbolic.svg create mode 100644 resources/icons/waybar_icons.gresource.xml create mode 100644 src/modules/privacy/privacy.cpp create mode 100644 src/modules/privacy/privacy_item.cpp create mode 100644 src/util/pipewire_backend.cpp diff --git a/include/factory.hpp b/include/factory.hpp index cb25078d..fea5ba99 100644 --- a/include/factory.hpp +++ b/include/factory.hpp @@ -68,6 +68,9 @@ #ifdef HAVE_UPOWER #include "modules/upower/upower.hpp" #endif +#ifdef HAVE_PIPEWIRE +#include "modules/privacy/privacy.hpp" +#endif #ifdef HAVE_LIBPULSE #include "modules/pulseaudio.hpp" #endif @@ -101,7 +104,7 @@ namespace waybar { class Factory { public: Factory(const Bar& bar, const Json::Value& config); - AModule* makeModule(const std::string& name) const; + AModule* makeModule(const std::string& name, const std::string& pos) const; private: const Bar& bar_; diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp new file mode 100644 index 00000000..3ec767a6 --- /dev/null +++ b/include/modules/privacy/privacy.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +#include "ALabel.hpp" +#include "gtkmm/box.h" +#include "modules/privacy/privacy_item.hpp" +#include "util/pipewire/pipewire_backend.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +using waybar::util::PipewireBackend::PrivacyNodeInfo; + +namespace waybar::modules::privacy { + +class Privacy : public AModule { + public: + Privacy(const std::string &, const Json::Value &, const std::string &pos); + auto update() -> void override; + + void onPrivacyNodesChanged(); + + private: + std::list nodes_screenshare; // Screen is being shared + std::list nodes_audio_in; // Application is using the microphone + std::list nodes_audio_out; // Application is outputting audio + + PrivacyItem privacy_item_screenshare; + PrivacyItem privacy_item_audio_input; + PrivacyItem privacy_item_audio_output; + + sigc::connection visibility_conn; + + // Config + Gtk::Box box_; + uint iconSpacing = 4; + uint iconSize = 20; + uint transition_duration = 500; + + std::shared_ptr backend = nullptr; +}; + +} // namespace waybar::modules::privacy diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp new file mode 100644 index 00000000..56d17acf --- /dev/null +++ b/include/modules/privacy/privacy_item.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include + +#include +#include +#include +#include + +#include "gtkmm/box.h" +#include "gtkmm/image.h" +#include "gtkmm/revealer.h" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +class PrivacyItem : public Gtk::Revealer { + public: + PrivacyItem(const Json::Value&, enum util::PipewireBackend::PrivacyNodeType privacy_type_, + const std::string& pos); + + bool is_enabled(); + + void set_in_use(bool in_use); + + void set_icon_size(uint size); + + private: + enum util::PipewireBackend::PrivacyNodeType privacy_type; + + std::mutex mutex_; + sigc::connection signal_conn; + + bool init = false; + bool in_use = false; + std::string lastStatus; + + // Config + bool enabled = true; + std::string iconName = "image-missing-symbolic"; + + Gtk::Box box_; + Gtk::Image icon_; +}; + +} // namespace waybar::modules::privacy diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp new file mode 100644 index 00000000..fc622567 --- /dev/null +++ b/include/util/pipewire/pipewire_backend.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include "util/backend_common.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::util::PipewireBackend { + +class PipewireBackend { + private: + pw_thread_loop* mainloop_; + pw_context* context_; + pw_core* core_; + + spa_hook registry_listener; + + /* Hack to keep constructor inaccessible but still public. + * This is required to be able to use std::make_shared. + * It is important to keep this class only accessible via a reference-counted + * pointer because the destructor will manually free memory, and this could be + * a problem with C++20's copy and move semantics. + */ + struct private_constructor_tag {}; + + public: + std::mutex mutex_; + + pw_registry* registry; + + sigc::signal privacy_nodes_changed_signal_event; + + std::unordered_map privacy_nodes; + + static std::shared_ptr getInstance(); + + PipewireBackend(private_constructor_tag tag); + ~PipewireBackend(); +}; +} // namespace waybar::util::pipewire::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp new file mode 100644 index 00000000..c645ade5 --- /dev/null +++ b/include/util/pipewire/privacy_node_info.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +namespace waybar::util::PipewireBackend { + +enum PrivacyNodeType { + PRIVACY_NODE_TYPE_NONE, + PRIVACY_NODE_TYPE_VIDEO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_INPUT, + PRIVACY_NODE_TYPE_AUDIO_OUTPUT +}; + +class PrivacyNodeInfo { + public: + PrivacyNodeType type = PRIVACY_NODE_TYPE_NONE; + uint32_t id; + uint32_t client_id; + enum pw_node_state state = PW_NODE_STATE_IDLE; + std::string media_class; + std::string media_name; + std::string node_name; + + struct spa_hook node_listener; + + bool changed = false; + + void* data; + + PrivacyNodeInfo(uint32_t id_, void* data_) : id(id_), data(data_) {} + + ~PrivacyNodeInfo() { spa_hook_remove(&node_listener); } +}; +} // namespace waybar::util::pipewire::PipewireBackend diff --git a/meson.build b/meson.build index bb9abdeb..8bea0ce3 100644 --- a/meson.build +++ b/meson.build @@ -98,6 +98,7 @@ libinput = dependency('libinput', required: get_option('libinput')) libnl = dependency('libnl-3.0', required: get_option('libnl')) libnlgen = dependency('libnl-genl-3.0', required: get_option('libnl')) upower_glib = dependency('upower-glib', required: get_option('upower_glib')) +pipewire = dependency('libpipewire-0.3', required: get_option('pipewire')) playerctl = dependency('playerctl', version : ['>=2.0.0'], required: get_option('mpris')) libpulse = dependency('libpulse', required: get_option('pulseaudio')) libudev = dependency('libudev', required: get_option('libudev')) @@ -273,6 +274,14 @@ if (upower_glib.found() and giounix.found() and not get_option('logind').disable src_files += 'src/modules/upower/upower_tooltip.cpp' endif + +if (pipewire.found()) + add_project_arguments('-DHAVE_PIPEWIRE', language: 'cpp') + src_files += 'src/modules/privacy/privacy.cpp' + src_files += 'src/modules/privacy/privacy_item.cpp' + src_files += 'src/util/pipewire_backend.cpp' +endif + if (playerctl.found() and giounix.found() and not get_option('logind').disabled()) add_project_arguments('-DHAVE_MPRIS', language: 'cpp') src_files += 'src/modules/mpris/mpris.cpp' @@ -373,9 +382,12 @@ endif subdir('protocol') +app_resources = [] +subdir('resources/icons') + executable( 'waybar', - src_files, + [src_files, app_resources], dependencies: [ thread_dep, client_protos, @@ -392,6 +404,7 @@ executable( libnl, libnlgen, upower_glib, + pipewire, playerctl, libpulse, libjack, diff --git a/meson_options.txt b/meson_options.txt index 7dacf087..827f9ac1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -5,6 +5,7 @@ option('libudev', type: 'feature', value: 'auto', description: 'Enable libudev s option('libevdev', type: 'feature', value: 'auto', description: 'Enable libevdev support for evdev related features') option('pulseaudio', type: 'feature', value: 'auto', description: 'Enable support for pulseaudio') option('upower_glib', type: 'feature', value: 'auto', description: 'Enable support for upower') +option('pipewire', type: 'feature', value: 'auto', description: 'Enable support for pipewire') option('mpris', type: 'feature', value: 'auto', description: 'Enable support for mpris') option('systemd', type: 'feature', value: 'auto', description: 'Install systemd user service unit') option('dbusmenu-gtk', type: 'feature', value: 'auto', description: 'Enable support for tray') diff --git a/resources/icons/meson.build b/resources/icons/meson.build new file mode 100644 index 00000000..05532d3d --- /dev/null +++ b/resources/icons/meson.build @@ -0,0 +1,6 @@ +gnome = import('gnome') + +app_resources += gnome.compile_resources('icon-resources', + 'waybar_icons.gresource.xml', + c_name: 'waybar_icons' +) diff --git a/resources/icons/waybar-privacy-audio-input-symbolic.svg b/resources/icons/waybar-privacy-audio-input-symbolic.svg new file mode 100644 index 00000000..61356891 --- /dev/null +++ b/resources/icons/waybar-privacy-audio-input-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/icons/waybar-privacy-audio-output-symbolic.svg b/resources/icons/waybar-privacy-audio-output-symbolic.svg new file mode 100644 index 00000000..10ad4f9d --- /dev/null +++ b/resources/icons/waybar-privacy-audio-output-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/icons/waybar-privacy-screen-share-symbolic.svg b/resources/icons/waybar-privacy-screen-share-symbolic.svg new file mode 100644 index 00000000..9738c571 --- /dev/null +++ b/resources/icons/waybar-privacy-screen-share-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/icons/waybar_icons.gresource.xml b/resources/icons/waybar_icons.gresource.xml new file mode 100644 index 00000000..077049bf --- /dev/null +++ b/resources/icons/waybar_icons.gresource.xml @@ -0,0 +1,8 @@ + + + + waybar-privacy-audio-input-symbolic.svg + waybar-privacy-audio-output-symbolic.svg + waybar-privacy-screen-share-symbolic.svg + + diff --git a/src/bar.cpp b/src/bar.cpp index d0a187c6..1ffe2ef6 100644 --- a/src/bar.cpp +++ b/src/bar.cpp @@ -760,7 +760,7 @@ void waybar::Bar::getModules(const Factory& factory, const std::string& pos, getModules(factory, ref, group_module); module = group_module; } else { - module = factory.makeModule(ref); + module = factory.makeModule(ref, pos); } std::shared_ptr module_sp(module); diff --git a/src/client.cpp b/src/client.cpp index cd0fa55b..066247e7 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -4,6 +4,7 @@ #include +#include "gtkmm/icontheme.h" #include "idle-inhibit-unstable-v1-client-protocol.h" #include "util/clara.hpp" #include "util/format.hpp" @@ -244,6 +245,11 @@ int waybar::Client::main(int argc, char *argv[]) { } gtk_app = Gtk::Application::create(argc, argv, "fr.arouillard.waybar", Gio::APPLICATION_HANDLES_COMMAND_LINE); + + // Initialize Waybars GTK resources with our custom icons + auto theme = Gtk::IconTheme::get_default(); + theme->add_resource_path("/fr/arouillard/waybar/icons"); + gdk_display = Gdk::Display::get_default(); if (!gdk_display) { throw std::runtime_error("Can't find display"); diff --git a/src/factory.cpp b/src/factory.cpp index aaf46036..91a882b0 100644 --- a/src/factory.cpp +++ b/src/factory.cpp @@ -10,7 +10,8 @@ waybar::Factory::Factory(const Bar& bar, const Json::Value& config) : bar_(bar), config_(config) {} -waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { +waybar::AModule* waybar::Factory::makeModule(const std::string& name, + const std::string& pos) const { try { auto hash_pos = name.find('#'); auto ref = name.substr(0, hash_pos); @@ -30,6 +31,11 @@ waybar::AModule* waybar::Factory::makeModule(const std::string& name) const { return new waybar::modules::upower::UPower(id, config_[name]); } #endif +#ifdef HAVE_PIPEWIRE + if (ref == "privacy") { + return new waybar::modules::privacy::Privacy(id, config_[name], pos); + } +#endif #ifdef HAVE_MPRIS if (ref == "mpris") { return new waybar::modules::mpris::Mpris(id, config_[name]); diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp new file mode 100644 index 00000000..ea97b352 --- /dev/null +++ b/src/modules/privacy/privacy.cpp @@ -0,0 +1,138 @@ +#include "modules/privacy/privacy.hpp" + +#include +#include +#include + +#include +#include +#include + +#include "AModule.hpp" +#include "gtkmm/image.h" + +namespace waybar::modules::privacy { + +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT; +using util::PipewireBackend::PRIVACY_NODE_TYPE_NONE; +using util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT; + +Privacy::Privacy(const std::string& id, const Json::Value& config, const std::string& pos) + : AModule(config, "privacy", id), + nodes_screenshare(), + nodes_audio_in(), + nodes_audio_out(), + privacy_item_screenshare(config["screenshare"], PRIVACY_NODE_TYPE_VIDEO_INPUT, pos), + privacy_item_audio_input(config["audio-in"], PRIVACY_NODE_TYPE_AUDIO_INPUT, pos), + privacy_item_audio_output(config["audio-out"], PRIVACY_NODE_TYPE_AUDIO_OUTPUT, pos), + visibility_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0) { + box_.set_name(name_); + box_.add(privacy_item_screenshare); + box_.add(privacy_item_audio_output); + box_.add(privacy_item_audio_input); + + event_box_.add(box_); + + // Icon Spacing + if (config_["icon-spacing"].isUInt()) { + iconSpacing = config_["icon-spacing"].asUInt(); + } + box_.set_spacing(iconSpacing); + + // Icon Size + if (config_["icon-size"].isUInt()) { + iconSize = config_["icon-size"].asUInt(); + } + privacy_item_screenshare.set_icon_size(iconSize); + privacy_item_audio_output.set_icon_size(iconSize); + privacy_item_audio_input.set_icon_size(iconSize); + + // Transition Duration + if (config_["transition-duration"].isUInt()) { + transition_duration = config_["transition-duration"].asUInt(); + } + privacy_item_screenshare.set_transition_duration(transition_duration); + privacy_item_audio_output.set_transition_duration(transition_duration); + privacy_item_audio_input.set_transition_duration(transition_duration); + + if (!privacy_item_screenshare.is_enabled() && !privacy_item_audio_input.is_enabled() && + !privacy_item_audio_output.is_enabled()) { + throw std::runtime_error("No privacy modules enabled"); + } + backend = util::PipewireBackend::PipewireBackend::getInstance(); + backend->privacy_nodes_changed_signal_event.connect( + sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); +} + +void Privacy::onPrivacyNodesChanged() { + nodes_audio_out.clear(); + nodes_audio_in.clear(); + nodes_screenshare.clear(); + + bool screenshare = false; + bool audio_in = false; + bool audio_out = false; + for (auto& node : backend->privacy_nodes) { + if (screenshare && audio_in && audio_out) break; + switch (node.second->state) { + case PW_NODE_STATE_RUNNING: + switch (node.second->type) { + case PRIVACY_NODE_TYPE_VIDEO_INPUT: + screenshare = true; + nodes_screenshare.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_INPUT: + audio_in = true; + nodes_audio_in.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + audio_out = true; + nodes_audio_out.push_back(node.second); + break; + case PRIVACY_NODE_TYPE_NONE: + continue; + } + break; + default: + break; + } + } + + dp.emit(); +} + +auto Privacy::update() -> void { + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + + privacy_item_screenshare.set_in_use(screenshare); + privacy_item_audio_input.set_in_use(audio_in); + privacy_item_audio_output.set_in_use(audio_out); + + // Hide the whole widget if none are in use + bool is_visible = screenshare || audio_in || audio_out; + if (is_visible != event_box_.get_visible()) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + visibility_conn.disconnect(); + if (is_visible) { + event_box_.set_visible(true); + } else { + visibility_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + event_box_.set_visible(false); + return false; + }, + *this), + transition_duration); + } + } + + // Call parent update + AModule::update(); +} + +} // namespace waybar::modules::privacy diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp new file mode 100644 index 00000000..943dfdbf --- /dev/null +++ b/src/modules/privacy/privacy_item.cpp @@ -0,0 +1,140 @@ +#include "modules/privacy/privacy_item.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "AModule.hpp" +#include "glibmm/main.h" +#include "glibmm/priorities.h" +#include "gtkmm/enums.h" +#include "gtkmm/label.h" +#include "gtkmm/revealer.h" +#include "gtkmm/tooltip.h" +#include "sigc++/adaptors/bind.h" +#include "util/gtk_icon.hpp" +#include "util/pipewire/privacy_node_info.hpp" + +namespace waybar::modules::privacy { + +PrivacyItem::PrivacyItem(const Json::Value& config_, + enum util::PipewireBackend::PrivacyNodeType privacy_type_, + const std::string& pos) + : Gtk::Revealer(), + privacy_type(privacy_type_), + mutex_(), + signal_conn(), + box_(Gtk::ORIENTATION_HORIZONTAL, 0), + icon_() { + switch (privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + get_style_context()->add_class("audio-in"); + iconName = "waybar-privacy-audio-input-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + get_style_context()->add_class("audio-out"); + iconName = "waybar-privacy-audio-output-symbolic"; + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + get_style_context()->add_class("screenshare"); + iconName = "waybar-privacy-screen-share-symbolic"; + break; + default: + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + enabled = false; + return; + } + + // Set the reveal transition to not look weird when sliding in + if (pos == "modules-left") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_RIGHT); + } else if (pos == "modules-center") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_CROSSFADE); + } else if (pos == "modules-right") { + set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT); + } + + box_.set_name("privacy-item"); + box_.add(icon_); + add(box_); + + // Icon Name + if (config_["enabled"].isBool()) { + enabled = config_["enabled"].asBool(); + } + + // Icon Name + if (config_["icon-name"].isString()) { + iconName = config_["icon-name"].asString(); + } + icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + + // Don't show by default + set_reveal_child(true); + set_visible(false); +} + +bool PrivacyItem::is_enabled() { return enabled; } + +void PrivacyItem::set_in_use(bool in_use) { + mutex_.lock(); + if (this->in_use == in_use && init) { + mutex_.unlock(); + return; + } + + if (init) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + signal_conn.disconnect(); + + this->in_use = in_use; + if (this->in_use) { + set_visible(true); + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + set_reveal_child(true); + return false; + }, + *this), + 0); + } else { + set_reveal_child(false); + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + set_visible(false); + return false; + }, + *this), + get_transition_duration()); + } + } else { + set_visible(false); + set_reveal_child(false); + } + this->init = true; + + // CSS status class + const std::string status = this->in_use ? "in-use" : ""; + // Remove last status if it exists + if (!lastStatus.empty() && get_style_context()->has_class(lastStatus)) { + get_style_context()->remove_class(lastStatus); + } + // Add the new status class to the Box + if (!status.empty() && !get_style_context()->has_class(status)) { + get_style_context()->add_class(status); + } + lastStatus = status; + + mutex_.unlock(); +} + +void PrivacyItem::set_icon_size(uint size) { icon_.set_pixel_size(size); } + +} // namespace waybar::modules::privacy diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp new file mode 100644 index 00000000..a2ac64a1 --- /dev/null +++ b/src/util/pipewire_backend.cpp @@ -0,0 +1,149 @@ +#include "util/pipewire/pipewire_backend.hpp" + +namespace waybar::util::PipewireBackend { + +// TODO: Refresh on suspend wake +static void get_node_info(void *data_, const struct pw_node_info *info) { + PrivacyNodeInfo *p_node_info = static_cast(data_); + PipewireBackend *backend = (PipewireBackend *)p_node_info->data; + + p_node_info->state = info->state; + + const struct spa_dict_item *item; + spa_dict_for_each(item, info->props) { + if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { + p_node_info->client_id = strtoul(item->value, NULL, 10); + } else if (strcmp(item->key, PW_KEY_MEDIA_CLASS) == 0) { + p_node_info->media_class = item->value; + if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Video") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Audio") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Output/Audio") == 0) { + p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } + } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { + p_node_info->media_name = item->value; + } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { + p_node_info->node_name = item->value; + } + } + + if (p_node_info->type != PRIVACY_NODE_TYPE_NONE) { + backend->mutex_.lock(); + p_node_info->changed = true; + backend->privacy_nodes.insert_or_assign(info->id, p_node_info); + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); + } else { + if (p_node_info->changed) { + backend->mutex_.lock(); + PrivacyNodeInfo *node = backend->privacy_nodes.at(info->id); + delete node; + backend->privacy_nodes.erase(info->id); + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); + } + } +} + +static const struct pw_node_events node_events = { + .version = PW_VERSION_NODE_EVENTS, + .info = get_node_info, +}; + +static void registry_event_global(void *_data, uint32_t id, uint32_t permissions, const char *type, + uint32_t version, const struct spa_dict *props) { + if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + + PipewireBackend *backend = static_cast(_data); + struct pw_proxy *proxy = (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, 0); + if (proxy) { + PrivacyNodeInfo *p_node_info; + backend->mutex_.lock(); + if (backend->privacy_nodes.contains(id)) { + p_node_info = backend->privacy_nodes.at(id); + } else { + p_node_info = new PrivacyNodeInfo(id, backend); + } + backend->mutex_.unlock(); + pw_proxy_add_object_listener(proxy, &p_node_info->node_listener, &node_events, p_node_info); + } +} + +static void registry_event_global_remove(void *_data, uint32_t id) { + auto backend = static_cast(_data); + + backend->mutex_.lock(); + if (backend->privacy_nodes.contains(id)) { + PrivacyNodeInfo *node_info = backend->privacy_nodes.at(id); + delete node_info; + backend->privacy_nodes.erase(id); + } + backend->mutex_.unlock(); + + backend->privacy_nodes_changed_signal_event.emit(); +} + +static const struct pw_registry_events registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +PipewireBackend::PipewireBackend(private_constructor_tag tag) + : mainloop_(nullptr), context_(nullptr), core_(nullptr) { + pw_init(nullptr, nullptr); + mainloop_ = pw_thread_loop_new("waybar", nullptr); + if (mainloop_ == nullptr) { + throw std::runtime_error("pw_thread_loop_new() failed."); + } + context_ = pw_context_new(pw_thread_loop_get_loop(mainloop_), nullptr, 0); + if (context_ == nullptr) { + throw std::runtime_error("pa_context_new() failed."); + } + core_ = pw_context_connect(context_, nullptr, 0); + if (core_ == nullptr) { + throw std::runtime_error("pw_context_connect() failed"); + } + registry = pw_core_get_registry(core_, PW_VERSION_REGISTRY, 0); + + spa_zero(registry_listener); + pw_registry_add_listener(registry, ®istry_listener, ®istry_events, this); + if (pw_thread_loop_start(mainloop_) < 0) { + throw std::runtime_error("pw_thread_loop_start() failed."); + } +} + +PipewireBackend::~PipewireBackend() { + for (auto &node : privacy_nodes) { + delete node.second; + } + + if (registry != nullptr) { + pw_proxy_destroy((struct pw_proxy *)registry); + } + + spa_zero(registry_listener); + + if (core_ != nullptr) { + pw_core_disconnect(core_); + } + + if (context_ != nullptr) { + pw_context_destroy(context_); + } + + if (mainloop_ != nullptr) { + pw_thread_loop_stop(mainloop_); + pw_thread_loop_destroy(mainloop_); + } +} + +std::shared_ptr PipewireBackend::getInstance() { + private_constructor_tag tag; + return std::make_shared(tag); +} +} // namespace waybar::util::PipewireBackend From e73ea8d608e54dca4c99616444343a7c24340e61 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:37:10 +0200 Subject: [PATCH 02/13] Fixed cases where the module would be hidden when it should be visible --- include/modules/privacy/privacy.hpp | 1 + src/modules/privacy/privacy.cpp | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index 3ec767a6..b14cf452 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -30,6 +30,7 @@ class Privacy : public AModule { PrivacyItem privacy_item_audio_input; PrivacyItem privacy_item_audio_output; + std::mutex mutex_; sigc::connection visibility_conn; // Config diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index ea97b352..56fd6d88 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -67,6 +67,7 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st } void Privacy::onPrivacyNodesChanged() { + mutex_.lock(); nodes_audio_out.clear(); nodes_audio_in.clear(); nodes_screenshare.clear(); @@ -100,6 +101,7 @@ void Privacy::onPrivacyNodesChanged() { } } + mutex_.unlock(); dp.emit(); } @@ -121,13 +123,17 @@ auto Privacy::update() -> void { if (is_visible) { event_box_.set_visible(true); } else { - visibility_conn = Glib::signal_timeout().connect(sigc::track_obj( - [this] { - event_box_.set_visible(false); - return false; - }, - *this), - transition_duration); + visibility_conn = Glib::signal_timeout().connect( + sigc::track_obj( + [this] { + bool screenshare = !nodes_screenshare.empty(); + bool audio_in = !nodes_audio_in.empty(); + bool audio_out = !nodes_audio_out.empty(); + event_box_.set_visible(screenshare || audio_in || audio_out); + return false; + }, + *this), + transition_duration); } } From ace319b5158a8e07306b7fb4374357df28bb4411 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Thu, 26 Oct 2023 23:44:04 +0200 Subject: [PATCH 03/13] Updated default CSS to include the privacy module --- resources/style.css | 25 +++++++++++++++++++++++++ src/modules/privacy/privacy_item.cpp | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/resources/style.css b/resources/style.css index cf5c5fb0..78cdf604 100644 --- a/resources/style.css +++ b/resources/style.css @@ -278,3 +278,28 @@ label:focus { #scratchpad.empty { background-color: transparent; } + +#privacy { + padding: 0; +} + +#privacy > box { + padding: 0; +} + +#privacy-item { + padding: 0 5px; + color: white; +} + +#privacy-item.screenshare { + background-color: #cf5700; +} + +#privacy-item.audio-in { + background-color: #1ca000; +} + +#privacy-item.audio-out { + background-color: #0069d4; +} diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 943dfdbf..c859d7e1 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -34,15 +34,15 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, icon_() { switch (privacy_type) { case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: - get_style_context()->add_class("audio-in"); + box_.get_style_context()->add_class("audio-in"); iconName = "waybar-privacy-audio-input-symbolic"; break; case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: - get_style_context()->add_class("audio-out"); + box_.get_style_context()->add_class("audio-out"); iconName = "waybar-privacy-audio-output-symbolic"; break; case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: - get_style_context()->add_class("screenshare"); + box_.get_style_context()->add_class("screenshare"); iconName = "waybar-privacy-screen-share-symbolic"; break; default: From 4a4c888d7d8ee4f86e2ba0fa3115e60fa161a7e4 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Fri, 27 Oct 2023 00:01:40 +0200 Subject: [PATCH 04/13] Fixed linter complaining --- include/util/pipewire/pipewire_backend.hpp | 2 +- include/util/pipewire/privacy_node_info.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp index fc622567..4e23b282 100644 --- a/include/util/pipewire/pipewire_backend.hpp +++ b/include/util/pipewire/pipewire_backend.hpp @@ -37,4 +37,4 @@ class PipewireBackend { PipewireBackend(private_constructor_tag tag); ~PipewireBackend(); }; -} // namespace waybar::util::pipewire::PipewireBackend +} // namespace waybar::util::PipewireBackend diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index c645ade5..b370cbb2 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -33,4 +33,4 @@ class PrivacyNodeInfo { ~PrivacyNodeInfo() { spa_hook_remove(&node_listener); } }; -} // namespace waybar::util::pipewire::PipewireBackend +} // namespace waybar::util::PipewireBackend From 86491e151247f3584624c2e64c9774de509cc114 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 28 Oct 2023 16:47:06 +0200 Subject: [PATCH 05/13] Call module emit in privacy module contructor --- src/modules/privacy/privacy.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 56fd6d88..f1f92838 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -64,6 +64,8 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st backend = util::PipewireBackend::PipewireBackend::getInstance(); backend->privacy_nodes_changed_signal_event.connect( sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); + + dp.emit(); } void Privacy::onPrivacyNodesChanged() { From 46e36c0e688071142935d7b8356f1dbce32e4985 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 28 Oct 2023 18:30:50 +0200 Subject: [PATCH 06/13] Simplified the privacy_item hiding/showing logic --- include/modules/privacy/privacy_item.hpp | 4 ++- src/modules/privacy/privacy.cpp | 2 ++ src/modules/privacy/privacy_item.cpp | 43 +++++++++++++----------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 56d17acf..321cae5c 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -29,7 +29,6 @@ class PrivacyItem : public Gtk::Revealer { enum util::PipewireBackend::PrivacyNodeType privacy_type; std::mutex mutex_; - sigc::connection signal_conn; bool init = false; bool in_use = false; @@ -41,6 +40,9 @@ class PrivacyItem : public Gtk::Revealer { Gtk::Box box_; Gtk::Image icon_; + + void on_child_revealed_changed(); + void on_map_changed(); }; } // namespace waybar::modules::privacy diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index f1f92838..06fb8259 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -125,6 +125,8 @@ auto Privacy::update() -> void { if (is_visible) { event_box_.set_visible(true); } else { + // Hides the widget when all of the privacy_item revealers animations + // have finished animating visibility_conn = Glib::signal_timeout().connect( sigc::track_obj( [this] { diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index c859d7e1..7ed8efe1 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -29,7 +29,6 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, : Gtk::Revealer(), privacy_type(privacy_type_), mutex_(), - signal_conn(), box_(Gtk::ORIENTATION_HORIZONTAL, 0), icon_() { switch (privacy_type) { @@ -75,6 +74,10 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, } icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + property_child_revealed().signal_changed().connect( + sigc::mem_fun(*this, &PrivacyItem::on_child_revealed_changed)); + signal_map().connect(sigc::mem_fun(*this, &PrivacyItem::on_map_changed)); + // Don't show by default set_reveal_child(true); set_visible(false); @@ -82,6 +85,18 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, bool PrivacyItem::is_enabled() { return enabled; } +void PrivacyItem::on_child_revealed_changed() { + if (!this->get_child_revealed()) { + set_visible(false); + } +} + +void PrivacyItem::on_map_changed() { + if (this->get_visible()) { + set_reveal_child(true); + } +} + void PrivacyItem::set_in_use(bool in_use) { mutex_.lock(); if (this->in_use == in_use && init) { @@ -90,29 +105,19 @@ void PrivacyItem::set_in_use(bool in_use) { } if (init) { - // Disconnect any previous connection so that it doesn't get activated in - // the future, hiding the module when it should be visible - signal_conn.disconnect(); - this->in_use = in_use; if (this->in_use) { set_visible(true); - signal_conn = Glib::signal_timeout().connect(sigc::track_obj( - [this] { - set_reveal_child(true); - return false; - }, - *this), - 0); + // The `on_map_changed` callback will call `set_reveal_child(true)` + // when the widget is realized so we don't need to call that here. + // This fixes a bug where the revealer wouldn't start the animation + // due to us changing the visibility at the same time. } else { set_reveal_child(false); - signal_conn = Glib::signal_timeout().connect(sigc::track_obj( - [this] { - set_visible(false); - return false; - }, - *this), - get_transition_duration()); + // The `on_child_revealed_changed` callback will call `set_visible(false)` + // when the animation has finished so we don't need to call that here. + // We do this so that the widget gets hidden after the revealer hide animation + // has finished. } } else { set_visible(false); From d32da917e497546e63270637bb42cb9c211588d7 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:17:53 +0200 Subject: [PATCH 07/13] Added tooltips --- include/modules/privacy/privacy_item.hpp | 15 +++-- include/util/pipewire/pipewire_backend.hpp | 2 +- include/util/pipewire/privacy_node_info.hpp | 35 +++++++++++- src/modules/privacy/privacy.cpp | 30 +++++----- src/modules/privacy/privacy_item.cpp | 63 +++++++++++++++++---- src/util/pipewire_backend.cpp | 18 +++--- 6 files changed, 121 insertions(+), 42 deletions(-) diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 321cae5c..802ca08e 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -12,12 +12,15 @@ #include "gtkmm/revealer.h" #include "util/pipewire/privacy_node_info.hpp" +using waybar::util::PipewireBackend::PrivacyNodeInfo; +using waybar::util::PipewireBackend::PrivacyNodeType; + namespace waybar::modules::privacy { class PrivacyItem : public Gtk::Revealer { public: - PrivacyItem(const Json::Value&, enum util::PipewireBackend::PrivacyNodeType privacy_type_, - const std::string& pos); + PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes, const std::string &pos); bool is_enabled(); @@ -26,9 +29,10 @@ class PrivacyItem : public Gtk::Revealer { void set_icon_size(uint size); private: - enum util::PipewireBackend::PrivacyNodeType privacy_type; + enum PrivacyNodeType privacy_type; + std::list *nodes; - std::mutex mutex_; + Gtk::Box tooltip_window; bool init = false; bool in_use = false; @@ -37,12 +41,15 @@ class PrivacyItem : public Gtk::Revealer { // Config bool enabled = true; std::string iconName = "image-missing-symbolic"; + bool tooltip = true; + uint tooltipIconSize = 24; Gtk::Box box_; Gtk::Image icon_; void on_child_revealed_changed(); void on_map_changed(); + void update_tooltip(); }; } // namespace waybar::modules::privacy diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp index 4e23b282..8eb0184a 100644 --- a/include/util/pipewire/pipewire_backend.hpp +++ b/include/util/pipewire/pipewire_backend.hpp @@ -30,7 +30,7 @@ class PipewireBackend { sigc::signal privacy_nodes_changed_signal_event; - std::unordered_map privacy_nodes; + std::unordered_map privacy_nodes; static std::shared_ptr getInstance(); diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index b370cbb2..1c523f97 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -4,6 +4,8 @@ #include +#include "util/gtk_icon.hpp" + namespace waybar::util::PipewireBackend { enum PrivacyNodeType { @@ -22,15 +24,44 @@ class PrivacyNodeInfo { std::string media_class; std::string media_name; std::string node_name; + std::string application_name; + + std::string pipewire_access_portal_app_id; + std::string application_icon_name; struct spa_hook node_listener; bool changed = false; - void* data; + void *data; - PrivacyNodeInfo(uint32_t id_, void* data_) : id(id_), data(data_) {} + PrivacyNodeInfo(uint32_t id_, void *data_) : id(id_), data(data_) {} ~PrivacyNodeInfo() { spa_hook_remove(&node_listener); } + + std::string get_name() { + const std::vector names{&application_name, &node_name}; + std::string name = "Unknown Application"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0) { + name = *name_; + name[0] = toupper(name[0]); + break; + } + } + return name; + } + + std::string get_icon_name() { + const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, + &application_name, &node_name}; + std::string name = "application-x-executable-symbolic"; + for (auto &name_ : names) { + if (name_ != nullptr && name_->length() > 0 && DefaultGtkIconThemeWrapper::has_icon(*name_)) { + return *name_; + } + } + return name; + } }; } // namespace waybar::util::PipewireBackend diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 06fb8259..2f7e4235 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -23,9 +23,12 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st nodes_screenshare(), nodes_audio_in(), nodes_audio_out(), - privacy_item_screenshare(config["screenshare"], PRIVACY_NODE_TYPE_VIDEO_INPUT, pos), - privacy_item_audio_input(config["audio-in"], PRIVACY_NODE_TYPE_AUDIO_INPUT, pos), - privacy_item_audio_output(config["audio-out"], PRIVACY_NODE_TYPE_AUDIO_OUTPUT, pos), + privacy_item_screenshare(config["screenshare"], PRIVACY_NODE_TYPE_VIDEO_INPUT, + &nodes_screenshare, pos), + privacy_item_audio_input(config["audio-in"], PRIVACY_NODE_TYPE_AUDIO_INPUT, &nodes_audio_in, + pos), + privacy_item_audio_output(config["audio-out"], PRIVACY_NODE_TYPE_AUDIO_OUTPUT, + &nodes_audio_out, pos), visibility_conn(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { box_.set_name(name_); @@ -74,25 +77,18 @@ void Privacy::onPrivacyNodesChanged() { nodes_audio_in.clear(); nodes_screenshare.clear(); - bool screenshare = false; - bool audio_in = false; - bool audio_out = false; for (auto& node : backend->privacy_nodes) { - if (screenshare && audio_in && audio_out) break; - switch (node.second->state) { + switch (node.second.state) { case PW_NODE_STATE_RUNNING: - switch (node.second->type) { + switch (node.second.type) { case PRIVACY_NODE_TYPE_VIDEO_INPUT: - screenshare = true; - nodes_screenshare.push_back(node.second); + nodes_screenshare.push_back(&node.second); break; case PRIVACY_NODE_TYPE_AUDIO_INPUT: - audio_in = true; - nodes_audio_in.push_back(node.second); + nodes_audio_in.push_back(&node.second); break; case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: - audio_out = true; - nodes_audio_out.push_back(node.second); + nodes_audio_out.push_back(&node.second); break; case PRIVACY_NODE_TYPE_NONE: continue; @@ -108,6 +104,7 @@ void Privacy::onPrivacyNodesChanged() { } auto Privacy::update() -> void { + mutex_.lock(); bool screenshare = !nodes_screenshare.empty(); bool audio_in = !nodes_audio_in.empty(); bool audio_out = !nodes_audio_out.empty(); @@ -115,6 +112,7 @@ auto Privacy::update() -> void { privacy_item_screenshare.set_in_use(screenshare); privacy_item_audio_input.set_in_use(audio_in); privacy_item_audio_output.set_in_use(audio_out); + mutex_.unlock(); // Hide the whole widget if none are in use bool is_visible = screenshare || audio_in || audio_out; @@ -130,9 +128,11 @@ auto Privacy::update() -> void { visibility_conn = Glib::signal_timeout().connect( sigc::track_obj( [this] { + mutex_.lock(); bool screenshare = !nodes_screenshare.empty(); bool audio_in = !nodes_audio_in.empty(); bool audio_out = !nodes_audio_out.empty(); + mutex_.unlock(); event_box_.set_visible(screenshare || audio_in || audio_out); return false; }, diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 7ed8efe1..f4c04ede 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -23,12 +23,12 @@ namespace waybar::modules::privacy { -PrivacyItem::PrivacyItem(const Json::Value& config_, - enum util::PipewireBackend::PrivacyNodeType privacy_type_, - const std::string& pos) +PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, + std::list *nodes_, const std::string &pos) : Gtk::Revealer(), privacy_type(privacy_type_), - mutex_(), + nodes(nodes_), + tooltip_window(Gtk::ORIENTATION_VERTICAL, 0), box_(Gtk::ORIENTATION_HORIZONTAL, 0), icon_() { switch (privacy_type) { @@ -74,6 +74,26 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, } icon_.set_from_icon_name(iconName, Gtk::ICON_SIZE_INVALID); + // Tooltip Icon Size + if (config_["tooltip-icon-size"].isUInt()) { + tooltipIconSize = config_["tooltip-icon-size"].asUInt(); + } + // Tooltip + if (config_["tooltip"].isString()) { + tooltip = config_["tooltip"].asBool(); + } + set_has_tooltip(tooltip); + if (tooltip) { + // Sets the window to use when showing the tooltip + update_tooltip(); + this->signal_query_tooltip().connect(sigc::track_obj( + [this](int x, int y, bool keyboard_tooltip, const Glib::RefPtr &tooltip) { + tooltip->set_custom(tooltip_window); + return true; + }, + *this)); + } + property_child_revealed().signal_changed().connect( sigc::mem_fun(*this, &PrivacyItem::on_child_revealed_changed)); signal_map().connect(sigc::mem_fun(*this, &PrivacyItem::on_map_changed)); @@ -83,6 +103,31 @@ PrivacyItem::PrivacyItem(const Json::Value& config_, set_visible(false); } +void PrivacyItem::update_tooltip() { + // Removes all old nodes + for (auto child : tooltip_window.get_children()) { + delete child; + } + + for (auto *node : *nodes) { + Gtk::Box *box = new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4); + + // Set device icon + Gtk::Image *node_icon = new Gtk::Image(); + node_icon->set_pixel_size(tooltipIconSize); + node_icon->set_from_icon_name(node->get_icon_name(), Gtk::ICON_SIZE_INVALID); + box->add(*node_icon); + + // Set model + Gtk::Label *node_name = new Gtk::Label(node->get_name()); + box->add(*node_name); + + tooltip_window.add(*box); + } + + tooltip_window.show_all(); +} + bool PrivacyItem::is_enabled() { return enabled; } void PrivacyItem::on_child_revealed_changed() { @@ -98,12 +143,12 @@ void PrivacyItem::on_map_changed() { } void PrivacyItem::set_in_use(bool in_use) { - mutex_.lock(); - if (this->in_use == in_use && init) { - mutex_.unlock(); - return; + if (in_use) { + update_tooltip(); } + if (this->in_use == in_use && init) return; + if (init) { this->in_use = in_use; if (this->in_use) { @@ -136,8 +181,6 @@ void PrivacyItem::set_in_use(bool in_use) { get_style_context()->add_class(status); } lastStatus = status; - - mutex_.unlock(); } void PrivacyItem::set_icon_size(uint size) { icon_.set_pixel_size(size); } diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp index a2ac64a1..47f4dc4f 100644 --- a/src/util/pipewire_backend.cpp +++ b/src/util/pipewire_backend.cpp @@ -26,21 +26,25 @@ static void get_node_info(void *data_, const struct pw_node_info *info) { p_node_info->media_name = item->value; } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { p_node_info->node_name = item->value; + } else if (strcmp(item->key, PW_KEY_APP_NAME) == 0) { + p_node_info->application_name = item->value; + } else if (strcmp(item->key, "pipewire.access.portal.app_id") == 0) { + p_node_info->pipewire_access_portal_app_id = item->value; + } else if (strcmp(item->key, PW_KEY_APP_ICON_NAME) == 0) { + p_node_info->application_icon_name = item->value; } } if (p_node_info->type != PRIVACY_NODE_TYPE_NONE) { backend->mutex_.lock(); p_node_info->changed = true; - backend->privacy_nodes.insert_or_assign(info->id, p_node_info); + backend->privacy_nodes.insert_or_assign(info->id, *p_node_info); backend->mutex_.unlock(); backend->privacy_nodes_changed_signal_event.emit(); } else { if (p_node_info->changed) { backend->mutex_.lock(); - PrivacyNodeInfo *node = backend->privacy_nodes.at(info->id); - delete node; backend->privacy_nodes.erase(info->id); backend->mutex_.unlock(); @@ -64,7 +68,7 @@ static void registry_event_global(void *_data, uint32_t id, uint32_t permissions PrivacyNodeInfo *p_node_info; backend->mutex_.lock(); if (backend->privacy_nodes.contains(id)) { - p_node_info = backend->privacy_nodes.at(id); + p_node_info = &backend->privacy_nodes.at(id); } else { p_node_info = new PrivacyNodeInfo(id, backend); } @@ -78,8 +82,6 @@ static void registry_event_global_remove(void *_data, uint32_t id) { backend->mutex_.lock(); if (backend->privacy_nodes.contains(id)) { - PrivacyNodeInfo *node_info = backend->privacy_nodes.at(id); - delete node_info; backend->privacy_nodes.erase(id); } backend->mutex_.unlock(); @@ -118,10 +120,6 @@ PipewireBackend::PipewireBackend(private_constructor_tag tag) } PipewireBackend::~PipewireBackend() { - for (auto &node : privacy_nodes) { - delete node.second; - } - if (registry != nullptr) { pw_proxy_destroy((struct pw_proxy *)registry); } From c4226f3745b26a2439017e307c2febde48e59ece Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:01:47 +0100 Subject: [PATCH 08/13] Readded signal_timeout instead of map to fix indicator being stuck --- include/modules/privacy/privacy_item.hpp | 4 +-- src/modules/privacy/privacy_item.cpp | 44 ++++++++++-------------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 802ca08e..ac94d168 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -32,6 +32,8 @@ class PrivacyItem : public Gtk::Revealer { enum PrivacyNodeType privacy_type; std::list *nodes; + sigc::connection signal_conn; + Gtk::Box tooltip_window; bool init = false; @@ -47,8 +49,6 @@ class PrivacyItem : public Gtk::Revealer { Gtk::Box box_; Gtk::Image icon_; - void on_child_revealed_changed(); - void on_map_changed(); void update_tooltip(); }; diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index f4c04ede..9f1c0819 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -6,7 +6,6 @@ #include #include -#include #include #include @@ -28,6 +27,7 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac : Gtk::Revealer(), privacy_type(privacy_type_), nodes(nodes_), + signal_conn(), tooltip_window(Gtk::ORIENTATION_VERTICAL, 0), box_(Gtk::ORIENTATION_HORIZONTAL, 0), icon_() { @@ -94,10 +94,6 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac *this)); } - property_child_revealed().signal_changed().connect( - sigc::mem_fun(*this, &PrivacyItem::on_child_revealed_changed)); - signal_map().connect(sigc::mem_fun(*this, &PrivacyItem::on_map_changed)); - // Don't show by default set_reveal_child(true); set_visible(false); @@ -130,18 +126,6 @@ void PrivacyItem::update_tooltip() { bool PrivacyItem::is_enabled() { return enabled; } -void PrivacyItem::on_child_revealed_changed() { - if (!this->get_child_revealed()) { - set_visible(false); - } -} - -void PrivacyItem::on_map_changed() { - if (this->get_visible()) { - set_reveal_child(true); - } -} - void PrivacyItem::set_in_use(bool in_use) { if (in_use) { update_tooltip(); @@ -150,20 +134,30 @@ void PrivacyItem::set_in_use(bool in_use) { if (this->in_use == in_use && init) return; if (init) { + // Disconnect any previous connection so that it doesn't get activated in + // the future, hiding the module when it should be visible + signal_conn.disconnect(); + this->in_use = in_use; + guint duration = 0; if (this->in_use) { set_visible(true); - // The `on_map_changed` callback will call `set_reveal_child(true)` - // when the widget is realized so we don't need to call that here. - // This fixes a bug where the revealer wouldn't start the animation - // due to us changing the visibility at the same time. } else { set_reveal_child(false); - // The `on_child_revealed_changed` callback will call `set_visible(false)` - // when the animation has finished so we don't need to call that here. - // We do this so that the widget gets hidden after the revealer hide animation - // has finished. + duration = get_transition_duration(); } + + signal_conn = Glib::signal_timeout().connect(sigc::track_obj( + [this] { + if (this->in_use) { + set_reveal_child(true); + } else { + set_visible(false); + } + return false; + }, + *this), + duration); } else { set_visible(false); set_reveal_child(false); From c60a8e9836d72de69c9a02ddb4dd832912a8d706 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Tue, 31 Oct 2023 08:46:21 +0100 Subject: [PATCH 09/13] free pipewire listeners on proxy destruction --- include/util/pipewire/privacy_node_info.hpp | 8 ++++++-- src/util/pipewire_backend.cpp | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index 1c523f97..cd50e3a0 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -29,7 +29,8 @@ class PrivacyNodeInfo { std::string pipewire_access_portal_app_id; std::string application_icon_name; - struct spa_hook node_listener; + struct spa_hook object_listener; + struct spa_hook proxy_listener; bool changed = false; @@ -37,7 +38,10 @@ class PrivacyNodeInfo { PrivacyNodeInfo(uint32_t id_, void *data_) : id(id_), data(data_) {} - ~PrivacyNodeInfo() { spa_hook_remove(&node_listener); } + ~PrivacyNodeInfo() { + spa_hook_remove(&object_listener); + spa_hook_remove(&proxy_listener); + } std::string get_name() { const std::vector names{&application_name, &node_name}; diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp index 47f4dc4f..627d62f9 100644 --- a/src/util/pipewire_backend.cpp +++ b/src/util/pipewire_backend.cpp @@ -1,5 +1,7 @@ #include "util/pipewire/pipewire_backend.hpp" +#include "util/pipewire/privacy_node_info.hpp" + namespace waybar::util::PipewireBackend { // TODO: Refresh on suspend wake @@ -58,9 +60,21 @@ static const struct pw_node_events node_events = { .info = get_node_info, }; +static void proxy_destroy(void *data) { + PrivacyNodeInfo *node = (PrivacyNodeInfo *)data; + + spa_hook_remove(&node->proxy_listener); + spa_hook_remove(&node->object_listener); +} + +static const struct pw_proxy_events proxy_events = { + .version = PW_VERSION_PROXY_EVENTS, + .destroy = proxy_destroy, +}; + static void registry_event_global(void *_data, uint32_t id, uint32_t permissions, const char *type, uint32_t version, const struct spa_dict *props) { - if (strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; + if (!props || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; PipewireBackend *backend = static_cast(_data); struct pw_proxy *proxy = (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, 0); @@ -73,7 +87,8 @@ static void registry_event_global(void *_data, uint32_t id, uint32_t permissions p_node_info = new PrivacyNodeInfo(id, backend); } backend->mutex_.unlock(); - pw_proxy_add_object_listener(proxy, &p_node_info->node_listener, &node_events, p_node_info); + pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); + pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); } } From 49caa4bf31658555e69e9b44f9b0a6dfc25aca12 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Tue, 31 Oct 2023 11:37:28 +0100 Subject: [PATCH 10/13] Add the PrivacyNodeInfo object as pw_proxy data --- include/util/pipewire/pipewire_backend.hpp | 2 +- include/util/pipewire/privacy_node_info.hpp | 11 +--- src/modules/privacy/privacy.cpp | 10 +-- src/util/pipewire_backend.cpp | 73 ++++++++++----------- 4 files changed, 40 insertions(+), 56 deletions(-) diff --git a/include/util/pipewire/pipewire_backend.hpp b/include/util/pipewire/pipewire_backend.hpp index 8eb0184a..4e23b282 100644 --- a/include/util/pipewire/pipewire_backend.hpp +++ b/include/util/pipewire/pipewire_backend.hpp @@ -30,7 +30,7 @@ class PipewireBackend { sigc::signal privacy_nodes_changed_signal_event; - std::unordered_map privacy_nodes; + std::unordered_map privacy_nodes; static std::shared_ptr getInstance(); diff --git a/include/util/pipewire/privacy_node_info.hpp b/include/util/pipewire/privacy_node_info.hpp index cd50e3a0..3b7f446d 100644 --- a/include/util/pipewire/privacy_node_info.hpp +++ b/include/util/pipewire/privacy_node_info.hpp @@ -32,17 +32,8 @@ class PrivacyNodeInfo { struct spa_hook object_listener; struct spa_hook proxy_listener; - bool changed = false; - void *data; - PrivacyNodeInfo(uint32_t id_, void *data_) : id(id_), data(data_) {} - - ~PrivacyNodeInfo() { - spa_hook_remove(&object_listener); - spa_hook_remove(&proxy_listener); - } - std::string get_name() { const std::vector names{&application_name, &node_name}; std::string name = "Unknown Application"; @@ -59,7 +50,7 @@ class PrivacyNodeInfo { std::string get_icon_name() { const std::vector names{&application_icon_name, &pipewire_access_portal_app_id, &application_name, &node_name}; - std::string name = "application-x-executable-symbolic"; + const std::string name = "application-x-executable-symbolic"; for (auto &name_ : names) { if (name_ != nullptr && name_->length() > 0 && DefaultGtkIconThemeWrapper::has_icon(*name_)) { return *name_; diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 2f7e4235..72b7928b 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -78,17 +78,17 @@ void Privacy::onPrivacyNodesChanged() { nodes_screenshare.clear(); for (auto& node : backend->privacy_nodes) { - switch (node.second.state) { + switch (node.second->state) { case PW_NODE_STATE_RUNNING: - switch (node.second.type) { + switch (node.second->type) { case PRIVACY_NODE_TYPE_VIDEO_INPUT: - nodes_screenshare.push_back(&node.second); + nodes_screenshare.push_back(node.second); break; case PRIVACY_NODE_TYPE_AUDIO_INPUT: - nodes_audio_in.push_back(&node.second); + nodes_audio_in.push_back(node.second); break; case PRIVACY_NODE_TYPE_AUDIO_OUTPUT: - nodes_audio_out.push_back(&node.second); + nodes_audio_out.push_back(node.second); break; case PRIVACY_NODE_TYPE_NONE: continue; diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp index 627d62f9..5449cddc 100644 --- a/src/util/pipewire_backend.cpp +++ b/src/util/pipewire_backend.cpp @@ -4,7 +4,6 @@ namespace waybar::util::PipewireBackend { -// TODO: Refresh on suspend wake static void get_node_info(void *data_, const struct pw_node_info *info) { PrivacyNodeInfo *p_node_info = static_cast(data_); PipewireBackend *backend = (PipewireBackend *)p_node_info->data; @@ -15,15 +14,6 @@ static void get_node_info(void *data_, const struct pw_node_info *info) { spa_dict_for_each(item, info->props) { if (strcmp(item->key, PW_KEY_CLIENT_ID) == 0) { p_node_info->client_id = strtoul(item->value, NULL, 10); - } else if (strcmp(item->key, PW_KEY_MEDIA_CLASS) == 0) { - p_node_info->media_class = item->value; - if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Video") == 0) { - p_node_info->type = PRIVACY_NODE_TYPE_VIDEO_INPUT; - } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Input/Audio") == 0) { - p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_INPUT; - } else if (strcmp(p_node_info->media_class.c_str(), "Stream/Output/Audio") == 0) { - p_node_info->type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; - } } else if (strcmp(item->key, PW_KEY_MEDIA_NAME) == 0) { p_node_info->media_name = item->value; } else if (strcmp(item->key, PW_KEY_NODE_NAME) == 0) { @@ -37,22 +27,7 @@ static void get_node_info(void *data_, const struct pw_node_info *info) { } } - if (p_node_info->type != PRIVACY_NODE_TYPE_NONE) { - backend->mutex_.lock(); - p_node_info->changed = true; - backend->privacy_nodes.insert_or_assign(info->id, *p_node_info); - backend->mutex_.unlock(); - - backend->privacy_nodes_changed_signal_event.emit(); - } else { - if (p_node_info->changed) { - backend->mutex_.lock(); - backend->privacy_nodes.erase(info->id); - backend->mutex_.unlock(); - - backend->privacy_nodes_changed_signal_event.emit(); - } - } + backend->privacy_nodes_changed_signal_event.emit(); } static const struct pw_node_events node_events = { @@ -76,27 +51,45 @@ static void registry_event_global(void *_data, uint32_t id, uint32_t permissions uint32_t version, const struct spa_dict *props) { if (!props || strcmp(type, PW_TYPE_INTERFACE_Node) != 0) return; - PipewireBackend *backend = static_cast(_data); - struct pw_proxy *proxy = (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, 0); - if (proxy) { - PrivacyNodeInfo *p_node_info; - backend->mutex_.lock(); - if (backend->privacy_nodes.contains(id)) { - p_node_info = &backend->privacy_nodes.at(id); - } else { - p_node_info = new PrivacyNodeInfo(id, backend); - } - backend->mutex_.unlock(); - pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); - pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); + const char *lookup_str = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS); + if (!lookup_str) return; + std::string media_class = lookup_str; + enum PrivacyNodeType media_type = PRIVACY_NODE_TYPE_NONE; + if (media_class == "Stream/Input/Video") { + media_type = PRIVACY_NODE_TYPE_VIDEO_INPUT; + } else if (media_class == "Stream/Input/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_INPUT; + } else if (media_class == "Stream/Output/Audio") { + media_type = PRIVACY_NODE_TYPE_AUDIO_OUTPUT; + } else { + return; } + + PipewireBackend *backend = static_cast(_data); + struct pw_proxy *proxy = + (pw_proxy *)pw_registry_bind(backend->registry, id, type, version, sizeof(PrivacyNodeInfo)); + + if (!proxy) return; + + PrivacyNodeInfo *p_node_info = (PrivacyNodeInfo *)pw_proxy_get_user_data(proxy); + p_node_info->id = id; + p_node_info->data = backend; + p_node_info->type = media_type; + p_node_info->media_class = media_class; + + pw_proxy_add_listener(proxy, &p_node_info->proxy_listener, &proxy_events, p_node_info); + + pw_proxy_add_object_listener(proxy, &p_node_info->object_listener, &node_events, p_node_info); + + backend->privacy_nodes.insert_or_assign(id, p_node_info); } static void registry_event_global_remove(void *_data, uint32_t id) { auto backend = static_cast(_data); backend->mutex_.lock(); - if (backend->privacy_nodes.contains(id)) { + auto iter = backend->privacy_nodes.find(id); + if(iter != backend->privacy_nodes.end()) { backend->privacy_nodes.erase(id); } backend->mutex_.unlock(); From ca7c9a68f12b05173249ee61a9162f82e18c8648 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 4 Nov 2023 13:18:52 +0100 Subject: [PATCH 11/13] Made creation of privacy modules more modular --- include/modules/privacy/privacy.hpp | 4 -- include/modules/privacy/privacy_item.hpp | 9 +-- src/modules/privacy/privacy.cpp | 72 +++++++++++++++++------- src/modules/privacy/privacy_item.cpp | 15 ++--- 4 files changed, 58 insertions(+), 42 deletions(-) diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index b14cf452..c6b09b2c 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -26,10 +26,6 @@ class Privacy : public AModule { std::list nodes_audio_in; // Application is using the microphone std::list nodes_audio_out; // Application is outputting audio - PrivacyItem privacy_item_screenshare; - PrivacyItem privacy_item_audio_input; - PrivacyItem privacy_item_audio_output; - std::mutex mutex_; sigc::connection visibility_conn; diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index ac94d168..40ed40c5 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -20,16 +20,14 @@ namespace waybar::modules::privacy { class PrivacyItem : public Gtk::Revealer { public: PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, - std::list *nodes, const std::string &pos); + std::list *nodes, const std::string &pos, const uint icon_size, + const uint transition_duration); - bool is_enabled(); + enum PrivacyNodeType privacy_type; void set_in_use(bool in_use); - void set_icon_size(uint size); - private: - enum PrivacyNodeType privacy_type; std::list *nodes; sigc::connection signal_conn; @@ -41,7 +39,6 @@ class PrivacyItem : public Gtk::Revealer { std::string lastStatus; // Config - bool enabled = true; std::string iconName = "image-missing-symbolic"; bool tooltip = true; uint tooltipIconSize = 24; diff --git a/src/modules/privacy/privacy.cpp b/src/modules/privacy/privacy.cpp index 72b7928b..e96f14fa 100644 --- a/src/modules/privacy/privacy.cpp +++ b/src/modules/privacy/privacy.cpp @@ -1,6 +1,7 @@ #include "modules/privacy/privacy.hpp" #include +#include #include #include @@ -10,6 +11,7 @@ #include "AModule.hpp" #include "gtkmm/image.h" +#include "modules/privacy/privacy_item.hpp" namespace waybar::modules::privacy { @@ -23,18 +25,9 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st nodes_screenshare(), nodes_audio_in(), nodes_audio_out(), - privacy_item_screenshare(config["screenshare"], PRIVACY_NODE_TYPE_VIDEO_INPUT, - &nodes_screenshare, pos), - privacy_item_audio_input(config["audio-in"], PRIVACY_NODE_TYPE_AUDIO_INPUT, &nodes_audio_in, - pos), - privacy_item_audio_output(config["audio-out"], PRIVACY_NODE_TYPE_AUDIO_OUTPUT, - &nodes_audio_out, pos), visibility_conn(), box_(Gtk::ORIENTATION_HORIZONTAL, 0) { box_.set_name(name_); - box_.add(privacy_item_screenshare); - box_.add(privacy_item_audio_output); - box_.add(privacy_item_audio_input); event_box_.add(box_); @@ -48,22 +41,45 @@ Privacy::Privacy(const std::string& id, const Json::Value& config, const std::st if (config_["icon-size"].isUInt()) { iconSize = config_["icon-size"].asUInt(); } - privacy_item_screenshare.set_icon_size(iconSize); - privacy_item_audio_output.set_icon_size(iconSize); - privacy_item_audio_input.set_icon_size(iconSize); // Transition Duration if (config_["transition-duration"].isUInt()) { transition_duration = config_["transition-duration"].asUInt(); } - privacy_item_screenshare.set_transition_duration(transition_duration); - privacy_item_audio_output.set_transition_duration(transition_duration); - privacy_item_audio_input.set_transition_duration(transition_duration); - if (!privacy_item_screenshare.is_enabled() && !privacy_item_audio_input.is_enabled() && - !privacy_item_audio_output.is_enabled()) { - throw std::runtime_error("No privacy modules enabled"); + // Initialize each privacy module + Json::Value modules = config_["modules"]; + // Add Screenshare and Mic usage as default modules if none are specified + if (!modules.isArray() || modules.size() == 0) { + modules = Json::Value(Json::arrayValue); + for (auto& type : {"screenshare", "audio-in"}) { + Json::Value obj = Json::Value(Json::objectValue); + obj["type"] = type; + modules.append(obj); + } } + for (uint i = 0; i < modules.size(); i++) { + const Json::Value& module_config = modules[i]; + if (!module_config.isObject() || !module_config["type"].isString()) continue; + const std::string type = module_config["type"].asString(); + if (type == "screenshare") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_VIDEO_INPUT, + &nodes_screenshare, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-in") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_INPUT, + &nodes_audio_in, pos, iconSize, transition_duration); + box_.add(*item); + } else if (type == "audio-out") { + auto item = + Gtk::make_managed(module_config, PRIVACY_NODE_TYPE_AUDIO_OUTPUT, + &nodes_audio_out, pos, iconSize, transition_duration); + box_.add(*item); + } + } + backend = util::PipewireBackend::PipewireBackend::getInstance(); backend->privacy_nodes_changed_signal_event.connect( sigc::mem_fun(*this, &Privacy::onPrivacyNodesChanged)); @@ -109,9 +125,23 @@ auto Privacy::update() -> void { bool audio_in = !nodes_audio_in.empty(); bool audio_out = !nodes_audio_out.empty(); - privacy_item_screenshare.set_in_use(screenshare); - privacy_item_audio_input.set_in_use(audio_in); - privacy_item_audio_output.set_in_use(audio_out); + for (Gtk::Widget* widget : box_.get_children()) { + PrivacyItem* module = dynamic_cast(widget); + if (!module) continue; + switch (module->privacy_type) { + case util::PipewireBackend::PRIVACY_NODE_TYPE_VIDEO_INPUT: + module->set_in_use(screenshare); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_INPUT: + module->set_in_use(audio_in); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_AUDIO_OUTPUT: + module->set_in_use(audio_out); + break; + case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: + break; + } + } mutex_.unlock(); // Hide the whole widget if none are in use diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 9f1c0819..1d4c8a0e 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -23,7 +23,8 @@ namespace waybar::modules::privacy { PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privacy_type_, - std::list *nodes_, const std::string &pos) + std::list *nodes_, const std::string &pos, + const uint icon_size, const uint transition_duration) : Gtk::Revealer(), privacy_type(privacy_type_), nodes(nodes_), @@ -46,7 +47,6 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac break; default: case util::PipewireBackend::PRIVACY_NODE_TYPE_NONE: - enabled = false; return; } @@ -58,16 +58,13 @@ PrivacyItem::PrivacyItem(const Json::Value &config_, enum PrivacyNodeType privac } else if (pos == "modules-right") { set_transition_type(Gtk::REVEALER_TRANSITION_TYPE_SLIDE_LEFT); } + set_transition_duration(transition_duration); box_.set_name("privacy-item"); box_.add(icon_); + icon_.set_pixel_size(icon_size); add(box_); - // Icon Name - if (config_["enabled"].isBool()) { - enabled = config_["enabled"].asBool(); - } - // Icon Name if (config_["icon-name"].isString()) { iconName = config_["icon-name"].asString(); @@ -124,8 +121,6 @@ void PrivacyItem::update_tooltip() { tooltip_window.show_all(); } -bool PrivacyItem::is_enabled() { return enabled; } - void PrivacyItem::set_in_use(bool in_use) { if (in_use) { update_tooltip(); @@ -177,6 +172,4 @@ void PrivacyItem::set_in_use(bool in_use) { lastStatus = status; } -void PrivacyItem::set_icon_size(uint size) { icon_.set_pixel_size(size); } - } // namespace waybar::modules::privacy From 6050fa3a43a0d34ee9addf7d499a80dc4f341ae9 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:12:45 +0100 Subject: [PATCH 12/13] Added documentation --- README.md | 1 + include/modules/privacy/privacy.hpp | 2 +- include/modules/privacy/privacy_item.hpp | 1 - man/waybar-privacy.5.scd | 85 ++++++++++++++++++++++++ meson.build | 1 + resources/style.css | 4 -- src/modules/privacy/privacy_item.cpp | 12 ---- 7 files changed, 88 insertions(+), 18 deletions(-) create mode 100644 man/waybar-privacy.5.scd diff --git a/README.md b/README.md index ac9718b5..6009d91a 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - Network - Bluetooth - Pulseaudio +- Privacy Info - Wireplumber - Disk - Memory diff --git a/include/modules/privacy/privacy.hpp b/include/modules/privacy/privacy.hpp index c6b09b2c..b8e76768 100644 --- a/include/modules/privacy/privacy.hpp +++ b/include/modules/privacy/privacy.hpp @@ -33,7 +33,7 @@ class Privacy : public AModule { Gtk::Box box_; uint iconSpacing = 4; uint iconSize = 20; - uint transition_duration = 500; + uint transition_duration = 250; std::shared_ptr backend = nullptr; }; diff --git a/include/modules/privacy/privacy_item.hpp b/include/modules/privacy/privacy_item.hpp index 40ed40c5..a0e3038b 100644 --- a/include/modules/privacy/privacy_item.hpp +++ b/include/modules/privacy/privacy_item.hpp @@ -36,7 +36,6 @@ class PrivacyItem : public Gtk::Revealer { bool init = false; bool in_use = false; - std::string lastStatus; // Config std::string iconName = "image-missing-symbolic"; diff --git a/man/waybar-privacy.5.scd b/man/waybar-privacy.5.scd new file mode 100644 index 00000000..d13d8ed3 --- /dev/null +++ b/man/waybar-privacy.5.scd @@ -0,0 +1,85 @@ +waybar-privacy(5) + +# NAME + +waybar - privacy module + +# DESCRIPTION + +The *privacy* module displays if any application is capturing audio, sharing ++ +the screen or playing audio. + +# CONFIGURATION + +*icon-spacing*: ++ + typeof: integer ++ + default: 4 ++ + The spacing between each privacy icon. + +*icon-size*: ++ + typeof: integer ++ + default: 20 ++ + The size of each privacy icon. + +*transition-duration*: ++ + typeof: integer ++ + default: 250 ++ + Option to disable tooltip on hover. + +*modules* ++ + typeof: array of objects ++ + default: [{"type": "screenshare"}, {"type": "audio-in"}] ++ + Which privacy modules to monitor. See *MODULES CONFIGURATION* for++ + more information. + +# MODULES CONFIGURATION + +*type*: ++ + typeof: string ++ + values: "screenshare", "audio-in", "audio-out" ++ + Specifies which module to use and configure. + +*tooltip*: ++ + typeof: bool ++ + default: true ++ + Option to disable tooltip on hover. + +*tooltip-icon-size*: ++ + typeof: integer ++ + default: 24 ++ + The size of each icon in the tooltip. + +# EXAMPLES + +``` +"privacy": { + "icon-spacing": 4, + "icon-size": 18, + "transition-duration": 250, + "modules": [ + { + "type": "screenshare", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-out", + "tooltip": true, + "tooltip-icon-size": 24 + }, + { + "type": "audio-in", + "tooltip": true, + "tooltip-icon-size": 24 + } + ] +}, +``` + +# STYLE + +- *#privacy* +- *#privacy-item* +- *#privacy-item.screenshare* +- *#privacy-item.audio-in* +- *#privacy-item.audio-out* diff --git a/meson.build b/meson.build index 8bea0ce3..cac29747 100644 --- a/meson.build +++ b/meson.build @@ -467,6 +467,7 @@ if scdoc.found() 'waybar-network.5.scd', 'waybar-pulseaudio.5.scd', 'waybar-pulseaudio-slider.5.scd', + 'waybar-privacy.5.scd', 'waybar-river-mode.5.scd', 'waybar-river-tags.5.scd', 'waybar-river-window.5.scd', diff --git a/resources/style.css b/resources/style.css index 78cdf604..e6017fdb 100644 --- a/resources/style.css +++ b/resources/style.css @@ -283,10 +283,6 @@ label:focus { padding: 0; } -#privacy > box { - padding: 0; -} - #privacy-item { padding: 0 5px; color: white; diff --git a/src/modules/privacy/privacy_item.cpp b/src/modules/privacy/privacy_item.cpp index 1d4c8a0e..a0a2da57 100644 --- a/src/modules/privacy/privacy_item.cpp +++ b/src/modules/privacy/privacy_item.cpp @@ -158,18 +158,6 @@ void PrivacyItem::set_in_use(bool in_use) { set_reveal_child(false); } this->init = true; - - // CSS status class - const std::string status = this->in_use ? "in-use" : ""; - // Remove last status if it exists - if (!lastStatus.empty() && get_style_context()->has_class(lastStatus)) { - get_style_context()->remove_class(lastStatus); - } - // Add the new status class to the Box - if (!status.empty() && !get_style_context()->has_class(status)) { - get_style_context()->add_class(status); - } - lastStatus = status; } } // namespace waybar::modules::privacy From f21b1dfa4d052f37a0b9fa3c2cbf558a148a76b7 Mon Sep 17 00:00:00 2001 From: Erik Reider <35975961+ErikReider@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:14:37 +0100 Subject: [PATCH 13/13] fixed linter issues --- src/util/pipewire_backend.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/pipewire_backend.cpp b/src/util/pipewire_backend.cpp index 5449cddc..5fe3ba62 100644 --- a/src/util/pipewire_backend.cpp +++ b/src/util/pipewire_backend.cpp @@ -89,7 +89,7 @@ static void registry_event_global_remove(void *_data, uint32_t id) { backend->mutex_.lock(); auto iter = backend->privacy_nodes.find(id); - if(iter != backend->privacy_nodes.end()) { + if (iter != backend->privacy_nodes.end()) { backend->privacy_nodes.erase(id); } backend->mutex_.unlock();