diff --git a/packages/ghaf-audio-control/default.nix b/packages/ghaf-audio-control/default.nix index afd0c8f..5221db1 100644 --- a/packages/ghaf-audio-control/default.nix +++ b/packages/ghaf-audio-control/default.nix @@ -5,6 +5,7 @@ stdenv, cmake, gtkmm3, + libayatana-appindicator, libpulseaudio, ninja, pkg-config, @@ -19,12 +20,14 @@ stdenv.mkDerivation rec { nativeBuildInputs = [ cmake + gtkmm3 + libayatana-appindicator ninja pkg-config - gtkmm3 ]; buildInputs = [ gtkmm3 + libayatana-appindicator libpulseaudio ]; diff --git a/packages/ghaf-audio-control/src/app/CMakeLists.txt b/packages/ghaf-audio-control/src/app/CMakeLists.txt index 7af8c7f..40b16b1 100644 --- a/packages/ghaf-audio-control/src/app/CMakeLists.txt +++ b/packages/ghaf-audio-control/src/app/CMakeLists.txt @@ -5,8 +5,14 @@ cmake_minimum_required(VERSION 3.5) project(GhafAudioControlStandalone LANGUAGES CXX) +find_package(PkgConfig REQUIRED) +pkg_check_modules(AYATANA REQUIRED ayatana-appindicator3-0.1) + +include_directories(${AYATANA_INCLUDE_DIRS}) +link_directories(${AYATANA_INCLUDE_DIRS}) + add_executable(GhafAudioControlStandalone main.cpp) -target_link_libraries(GhafAudioControlStandalone GhafAudioControl) +target_link_libraries(GhafAudioControlStandalone GhafAudioControl ${AYATANA_LIBRARIES}) include(GNUInstallDirs) install(TARGETS ${PROJECT_NAME}) diff --git a/packages/ghaf-audio-control/src/app/main.cpp b/packages/ghaf-audio-control/src/app/main.cpp index 0b7e516..e5d25db 100644 --- a/packages/ghaf-audio-control/src/app/main.cpp +++ b/packages/ghaf-audio-control/src/app/main.cpp @@ -8,9 +8,16 @@ #include #include +#include +#include +#include +#include +#include + #include #include -#include + +#include #include @@ -19,6 +26,15 @@ using namespace ghaf::AudioControl; namespace { +constexpr auto AppId = "Ghaf Audio Control"; + +struct AppArgs +{ + Glib::ustring pulseServerAddress; + Glib::ustring indicatorIconPath; + Glib::ustring appVms; +}; + std::vector GetAppVmsList(const std::string& appVms) { std::vector result; @@ -32,59 +48,166 @@ std::vector GetAppVmsList(const std::string& appVms) return result; } -int GtkClient(const std::string& pulseAudioServerAddress, const std::string& appVms) +class MyApp : public Gtk::Application { - auto app = Gtk::Application::create(); - Gtk::ApplicationWindow window; - window.set_title("Ghaf Audio Control"); +private: + class AppMenu : public Gtk::Menu + { + public: + AppMenu(MyApp& app) + : m_app(app) + , m_openItem("Open/Hide Audio Control") + , m_quitItem("Quit") + { + add(m_openItem); + add(m_quitItem); + + const auto onOpen = [this]() + { + auto& window = *m_app.m_window; + + Logger::debug(std::format("Indicator has been activated. window.is_visible: {}", window.is_visible())); + + if (window.is_visible()) + window.hide(); + else + { + window.show_all(); + window.present(); + } + }; + + m_connections = {m_openItem.signal_activate().connect(onOpen), m_quitItem.signal_activate().connect([this]() { m_app.onQuit(); })}; + + show_all(); + } + + ~AppMenu() override = default; + + private: + MyApp& m_app; + + Gtk::MenuItem m_openItem; + Gtk::MenuItem m_quitItem; + + ConnectionContainer m_connections; + }; + +public: + MyApp(int argc, char** argv) + : m_menu(*this) + , m_indicator(createAppIndicator()) + { + AppArgs appArgs; - AudioControl audioControl{std::make_unique(pulseAudioServerAddress), GetAppVmsList(appVms)}; + Glib::OptionEntry pulseServerOption; + pulseServerOption.set_long_name("pulseaudio_server"); + pulseServerOption.set_description("PulseAudio server address"); - window.add(audioControl); - window.show_all(); + Glib::OptionEntry indicatorIconPathOption; + indicatorIconPathOption.set_long_name("indicator_icon_path"); + indicatorIconPathOption.set_description("Tray's icon indicator path"); - return app->run(window); -} + Glib::OptionEntry appVmsOption; + appVmsOption.set_long_name("app_vms"); + appVmsOption.set_description("AppVMs list"); -} // namespace + Glib::OptionGroup options("Main", "Main"); + options.add_entry(pulseServerOption, appArgs.pulseServerAddress); + options.add_entry(indicatorIconPathOption, appArgs.indicatorIconPath); + options.add_entry(appVmsOption, appArgs.appVms); -int main(int argc, char** argv) -{ - pthread_setname_np(pthread_self(), "main"); + Glib::OptionContext context("Application Options"); + context.set_main_group(options); - Glib::ustring pulseServerAddress; - Glib::OptionEntry pulseServerOption; - pulseServerOption.set_long_name("pulseaudio_server"); - pulseServerOption.set_description("PulseAudio server address"); + if (!context.parse(argc, argv)) + { + Logger::info(context.get_help().c_str()); + throw std::runtime_error{"Couldn't parse the command line arguments"}; + } - Glib::ustring appVms; - Glib::OptionEntry appVmsOption; - appVmsOption.set_long_name("app_vms"); - appVmsOption.set_description("AppVMs list"); + app_indicator_set_icon(m_indicator.get(), appArgs.indicatorIconPath.c_str()); - Glib::OptionGroup options("Main", "Main"); - options.add_entry(pulseServerOption, pulseServerAddress); - options.add_entry(appVmsOption, appVms); + m_audioControl = std::make_unique(std::make_unique(appArgs.pulseServerAddress), + GetAppVmsList(appArgs.appVms)); + } - Glib::OptionContext context("Application Options"); - context.set_main_group(options); + int start(int argc, char** argv) + { + Logger::debug(__PRETTY_FUNCTION__); + return run(argc, argv); + } - try + [[nodiscard]] RaiiWrap createAppIndicator() { - if (!context.parse(argc, argv)) - throw std::runtime_error{"Couldn't parse command line arguments"}; + const auto contructor = [this](AppIndicator*& indicator) + { + indicator = app_indicator_new(AppId, "", APP_INDICATOR_CATEGORY_APPLICATION_STATUS); + app_indicator_set_status(indicator, APP_INDICATOR_STATUS_ACTIVE); + app_indicator_set_label(indicator, AppId, AppId); + app_indicator_set_title(indicator, AppId); + app_indicator_set_menu(indicator, GTK_MENU(m_menu.gobj())); + }; + + return {contructor, {}}; } - catch (const Glib::Error& ex) + +protected: + bool onWindowDelete([[maybe_unused]] GdkEventAny* event) { - Logger::error(std::format("Error: {}", ex.what().c_str())); - Logger::info(context.get_help().c_str()); + Logger::debug(__PRETTY_FUNCTION__); - return 1; + m_window->hide(); + return true; + } + + void onQuit() + { + Logger::debug(__PRETTY_FUNCTION__); + + release(); + quit(); + } + + void on_activate() override + { + Logger::debug(__PRETTY_FUNCTION__); + + m_window = std::make_unique(); + m_window->set_title(AppId); + m_window->add(*m_audioControl); + + m_window->signal_delete_event().connect(sigc::mem_fun(*this, &MyApp::onWindowDelete)); + + hold(); + add_window(*m_window); + + m_window->show_all(); } +private: + std::unique_ptr m_audioControl; + std::unique_ptr m_window; + + AppMenu m_menu; + RaiiWrap m_indicator; +}; + +} // namespace + +int main(int argc, char** argv) +{ + pthread_setname_np(pthread_self(), "main"); + try { - return GtkClient(pulseServerAddress, appVms); + MyApp app(argc, argv); + return app.start(argc, argv); + } + catch (const Glib::Error& ex) + { + Logger::error(std::format("Error: {}", ex.what().c_str())); + return 1; } catch (const std::exception& e) { diff --git a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp index 6833d7c..2646628 100644 --- a/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp +++ b/packages/ghaf-audio-control/src/lib/src/Backends/PulseAudio/AudioControlBackend.cpp @@ -335,7 +335,7 @@ void AudioControlBackend::cardInfoCallback(pa_context* context, const pa_card_in if (info == nullptr) return; - Logger::debug("\n###############################################"); + Logger::debug("###############################################"); Logger::debug(std::format("Card. index: {}, name: {}", info->index, info->name)); for (size_t i = 0; i < info->n_ports; ++i) @@ -345,25 +345,21 @@ void AudioControlBackend::cardInfoCallback(pa_context* context, const pa_card_in auto* self = static_cast(data); - auto sinkPredicate = [&info](const ISink& sink) -> bool + const auto sinkPredicate = [&info](const ISink& sink) { return dynamic_cast(sink).getCardIndex() == info->index; }; for (auto it : self->m_sinks.findByPredicate(sinkPredicate)) - { self->m_sinks.update(it, [&info](ISink& sink) { dynamic_cast(sink).update(*info); }); - } - auto sourcePredicate = [&info](const ISource& source) + const auto sourcePredicate = [&info](const ISource& source) { return dynamic_cast(source).getCardIndex() == info->index; }; for (auto it : self->m_sources.findByPredicate(sourcePredicate)) - { self->m_sources.update(it, [&info](ISource& source) { dynamic_cast(source).update(*info); }); - } } } // namespace ghaf::AudioControl::Backend::PulseAudio