diff --git a/metadata/panel.xml b/metadata/panel.xml index 2fbb3598..49dc0572 100644 --- a/metadata/panel.xml +++ b/metadata/panel.xml @@ -128,6 +128,11 @@ <_short>Battery Font default + <_short>Network @@ -231,6 +236,11 @@ <_short>Switch User Button Command dm-tool switch-to-greeter + <_short>Volume @@ -247,6 +257,11 @@ <_short>Volume Scroll Sensitivity 0.05 + <_short>Notifications diff --git a/src/panel/widgets/battery.cpp b/src/panel/widgets/battery.cpp index 090e1f23..acbae793 100644 --- a/src/panel/widgets/battery.cpp +++ b/src/panel/widgets/battery.cpp @@ -2,7 +2,11 @@ #include #include #include +#include +#include "../../util/gtk-utils.hpp" +#define POWER_PROFILE_PATH "/org/freedesktop/UPower/PowerProfiles" +#define POWER_PROFILE_NAME "org.freedesktop.UPower.PowerProfiles" #define UPOWER_NAME "org.freedesktop.UPower" #define DISPLAY_DEVICE "/org/freedesktop/UPower/devices/DisplayDevice" @@ -14,6 +18,10 @@ #define TIMETOEMPTY "TimeToEmpty" #define SHOULD_DISPLAY "IsPresent" +#define DEGRADED "PerformanceDegraded" +#define PROFILES "Profiles" +#define ACTIVE_PROFILE "ActiveProfile" + static std::string get_device_type_description(uint32_t type) { if (type == 2) @@ -165,6 +173,33 @@ void WayfireBatteryInfo::update_state() "\n\tWayfireBatteryInfo::update_state()" << std::endl; } +void WayfireBatteryInfo::setup_profiles(std::vector> profiles) +{ + profiles_menu->remove_all(); + for (auto profile : profiles) + { + if (profile.count("Profile") == 1) + { + Glib::VariantBase value = profile.at("Profile"); + if (value.is_of_type(Glib::VariantType("s"))){ + auto value_string = Glib::VariantBase::cast_dynamic>(value).get(); + auto item = Gio::MenuItem::create(value_string, "noactionyet"); + + item->set_action_and_target("actions.set_profile", Glib::Variant::create(value_string)); + profiles_menu->append_item(item); + } + } + } +} + +/* + Change the selected radio button. + */ +void WayfireBatteryInfo::set_current_profile(Glib::ustring profile) +{ + state_action->set_state(Glib::Variant::create(profile)); +} + bool WayfireBatteryInfo::setup_dbus() { auto cancellable = Gio::Cancellable::create(); @@ -175,6 +210,30 @@ bool WayfireBatteryInfo::setup_dbus() return false; } + powerprofile_proxy = Gio::DBus::Proxy::create_sync(connection, POWER_PROFILE_NAME, + POWER_PROFILE_PATH, + POWER_PROFILE_NAME); + if (!powerprofile_proxy) + { + std::cout << "Unable to conect to Power Profiles. Continuing" << std::endl; + } else + { + powerprofile_proxy->signal_properties_changed().connect( + sigc::mem_fun(*this, &WayfireBatteryInfo::on_upower_properties_changed) ); + Glib::Variant current_profile; + Glib::Variant>> profiles; + powerprofile_proxy->get_cached_property(current_profile, ACTIVE_PROFILE); + powerprofile_proxy->get_cached_property(profiles, PROFILES); + + if (profiles && current_profile){ + setup_profiles(profiles.get()); + set_current_profile(current_profile.get()); + } else + { + std::cout << "Unable to conect to Power Profiles. Continuing" << std::endl; + } + } + upower_proxy = Gio::DBus::Proxy::create_sync(connection, UPOWER_NAME, "/org/freedesktop/UPower", "org.freedesktop.UPower"); @@ -206,10 +265,36 @@ bool WayfireBatteryInfo::setup_dbus() return false; } +void WayfireBatteryInfo::on_upower_properties_changed( + const Gio::DBus::Proxy::MapChangedProperties& properties, + const std::vector& invalidated) +{ + for (auto& prop : properties) + { + if (prop.first == ACTIVE_PROFILE) + { + if (prop.second.is_of_type(Glib::VariantType("s"))){ + auto value_string = Glib::VariantBase::cast_dynamic>(prop.second).get(); + set_current_profile(value_string); + } + } else if (prop.first == PROFILES) + { + // I've been unable to find a way to change possible profiles on the fly, so cannot confirm this works at all. + auto value = Glib::VariantBase::cast_dynamic>>> (prop.second); + setup_profiles(value.get()); + } + // TODO Consider watching for "Performance Degraded" events too, but we currently have no way to output this additional information + } +} + // TODO: simplify config loading void WayfireBatteryInfo::init(Gtk::Box *container) { + profiles_menu = Gio::Menu::create(); + state_action = Gio::SimpleAction::create_radio_string("set_profile", ""); + settings_action = Gio::SimpleAction::create("settings"); + if (!setup_dbus()) { return; @@ -231,4 +316,76 @@ void WayfireBatteryInfo::init(Gtk::Box *container) button.set_child(button_box); button.property_scale_factor().signal_changed() .connect(sigc::mem_fun(*this, &WayfireBatteryInfo::update_icon)); + + menu = Gio::Menu::create(); + auto item = Gio::MenuItem::create("Settings", "actions.settings"); + menu->append_item(item); + + menu->append_section("Profiles", profiles_menu); + + auto actions = Gio::SimpleActionGroup::create(); + + state_action->signal_activate().connect([=] (Glib::VariantBase vb) + { + // User has requested a change of state. Don't change the UI choice, let the dbus roundtrip happen to be sure. + if (vb.is_of_type(Glib::VariantType("s"))) + { + // Couldn't seem to make proxy send property back, so this will have to do + Glib::VariantContainerBase params = Glib::Variant>::create({POWER_PROFILE_NAME, ACTIVE_PROFILE, vb}); + + connection->call_sync( + POWER_PROFILE_PATH, + "org.freedesktop.DBus.Properties", + "Set", + params, + NULL, + POWER_PROFILE_NAME, + -1, + Gio::DBus::CallFlags::NONE, + {} + ); + } + }); + + settings_action->signal_activate().connect([=] (Glib::VariantBase vb) + { + std::vector battery_programs = {"mate-power-statistics", "xfce4-power-manager"}; + + Glib::RefPtr app_info; + std::string value = battery_settings; + if (value == "") + { + // Auto guess + for(auto program : battery_programs) + { + if(executable_exists(program)) + { + auto keyfile = Glib::KeyFile::create(); + keyfile->set_string("Desktop Entry", "Type", "Application"); + keyfile->set_string("Desktop Entry", "Exec", "/bin/sh -c \"" + program + "\""); + app_info = Gio::DesktopAppInfo::create_from_keyfile(keyfile); + break; + } + } + + } else + { + auto keyfile = Glib::KeyFile::create(); + keyfile->set_string("Desktop Entry", "Type", "Application"); + keyfile->set_string("Desktop Entry", "Exec", "/bin/sh -c \"" + value + "\""); + app_info = Gio::DesktopAppInfo::create_from_keyfile(keyfile); + } + + if (app_info) + { + auto ctx = Gdk::Display::get_default()->get_app_launch_context(); + app_info->launch(std::vector>(), ctx); + } + }); + actions->add_action(state_action); + actions->add_action(settings_action); + + button.insert_action_group("actions", actions); + + button.set_menu_model(menu); } diff --git a/src/panel/widgets/battery.hpp b/src/panel/widgets/battery.hpp index a3366e19..6fb5bdf3 100644 --- a/src/panel/widgets/battery.hpp +++ b/src/panel/widgets/battery.hpp @@ -1,13 +1,20 @@ #ifndef WIDGETS_BATTERY_HPP #define WIDGETS_BATTERY_HPP -#include +#include #include #include #include +#include + #include #include +#include +#include +#include +#include +#include #include "../widget.hpp" @@ -22,15 +29,23 @@ class wayfire_config; class WayfireBatteryInfo : public WayfireWidget { WfOption status_opt{"panel/battery_status"}; + WfOption battery_settings{"panel/battery_settings"}; - Gtk::Button button; + Gtk::MenuButton button; Gtk::Label label; Gtk::Box button_box; Gtk::Image icon; + Gtk::PopoverMenu popover; + + std::shared_ptr menu; + std::shared_ptr profiles_menu; + + std::shared_ptr state_action, settings_action; + DBusConnection connection; - DBusProxy upower_proxy, display_device; + DBusProxy upower_proxy, powerprofile_proxy, display_device; bool setup_dbus(); @@ -42,6 +57,14 @@ class WayfireBatteryInfo : public WayfireWidget const Gio::DBus::Proxy::MapChangedProperties& properties, const std::vector& invalidated); + void on_upower_properties_changed( + const Gio::DBus::Proxy::MapChangedProperties& properties, + const std::vector& invalidated); + + void set_current_profile(Glib::ustring profile); + + void setup_profiles(std::vector> profiles); + public: virtual void init(Gtk::Box *container); virtual ~WayfireBatteryInfo() = default; diff --git a/src/panel/widgets/menu.cpp b/src/panel/widgets/menu.cpp index 705cf91e..8e3c7abd 100644 --- a/src/panel/widgets/menu.cpp +++ b/src/panel/widgets/menu.cpp @@ -838,6 +838,26 @@ void WayfireMenu::init(Gtk::Box *container) return; } + auto click_gesture = Gtk::GestureClick::create(); + click_gesture->set_button(3); // Only use right-click for this callback + click_gesture->signal_pressed().connect([=] (int count, double x, double y) + { + std::string value = menu_alternative_launch; + if (value != "") + { + auto keyfile = Glib::KeyFile::create(); + keyfile->set_string("Desktop Entry", "Type", "Application"); + keyfile->set_string("Desktop Entry", "Exec", "/bin/sh -c \"" + value + "\""); + AppInfo app_info = Gio::DesktopAppInfo::create_from_keyfile(keyfile); + if (app_info) + { + auto ctx = Gdk::Display::get_default()->get_app_launch_context(); + app_info->launch(std::vector>(), ctx); + } + } + }); + button->add_controller(click_gesture); + button->property_scale_factor().signal_changed().connect( [=] () {update_icon(); }); diff --git a/src/panel/widgets/menu.hpp b/src/panel/widgets/menu.hpp index 035a445f..ed78c148 100644 --- a/src/panel/widgets/menu.hpp +++ b/src/panel/widgets/menu.hpp @@ -172,6 +172,7 @@ class WayfireMenu : public WayfireWidget WfOption menu_min_category_width{"panel/menu_min_category_width"}; WfOption menu_min_content_height{"panel/menu_min_content_height"}; WfOption menu_show_categories{"panel/menu_show_categories"}; + WfOption menu_alternative_launch{"panel/menu_right_click"}; void update_popover_layout(); void update_category_width(); void update_content_height(); diff --git a/src/panel/widgets/volume.cpp b/src/panel/widgets/volume.cpp index c6d4f243..82c364d4 100644 --- a/src/panel/widgets/volume.cpp +++ b/src/panel/widgets/volume.cpp @@ -4,6 +4,7 @@ #include "volume.hpp" #include "launchers.hpp" #include "gtk-utils.hpp" +#include "../../util/gtk-utils.hpp" WayfireVolumeScale::WayfireVolumeScale() { @@ -72,6 +73,14 @@ static VolumeLevel get_volume_level(pa_volume_t volume, pa_volume_t max) return VOLUME_LEVEL_OOR; } +void WayfireVolume::update_menu() +{ + bool muted_mic = (gvc_stream_mic && gvc_mixer_stream_get_is_muted(gvc_stream_mic)); + bool muted_speaker = (gvc_stream && gvc_mixer_stream_get_is_muted(gvc_stream)); + microphone_action->set_state(Glib::Variant::create(muted_mic)); + speaker_action->set_state(Glib::Variant::create(muted_speaker)); +} + void WayfireVolume::update_icon() { VolumeLevel current = @@ -154,6 +163,14 @@ static void notify_is_muted(GvcMixerControl *gvc_control, { WayfireVolume *wf_volume = (WayfireVolume*)user_data; wf_volume->update_icon(); + wf_volume->update_menu(); +} + +static void notify_is_mic_muted(GvcMixerControl *gvc_control, + guint id, gpointer user_data) +{ + WayfireVolume *wf_volume = (WayfireVolume*)user_data; + wf_volume->update_menu(); } void WayfireVolume::disconnect_gvc_stream_signals() @@ -173,6 +190,15 @@ void WayfireVolume::disconnect_gvc_stream_signals() notify_is_muted_signal = 0; } +void WayfireVolume::disconnect_gvc_stream_signals_mic() +{ + if (notify_is_mic_muted_signal) + { + g_signal_handler_disconnect(gvc_stream_mic, notify_is_mic_muted_signal); + } + notify_is_mic_muted_signal = 0; +} + void WayfireVolume::on_default_sink_changed() { gvc_stream = gvc_mixer_control_get_default_sink(gvc_control); @@ -198,6 +224,23 @@ void WayfireVolume::on_default_sink_changed() /* Finally, update the displayed volume. However, do not show the * popup */ set_volume(gvc_mixer_stream_get_volume(gvc_stream), VOLUME_FLAG_NO_ACTION); + update_menu(); +} + +void WayfireVolume::on_default_source_changed() +{ + gvc_stream_mic = gvc_mixer_control_get_default_source(gvc_control); + if (!gvc_stream_mic) + { + printf("GVC: Failed to get default source\n"); + return; + } + + disconnect_gvc_stream_signals_mic(); + notify_is_mic_muted_signal = g_signal_connect(gvc_stream_mic, "notify::is-muted", + G_CALLBACK(notify_is_mic_muted), this); + + update_menu(); } static void default_sink_changed(GvcMixerControl *gvc_control, @@ -207,6 +250,13 @@ static void default_sink_changed(GvcMixerControl *gvc_control, wf_volume->on_default_sink_changed(); } +static void default_source_changed(GvcMixerControl *gvc_control, + guint id, gpointer user_data) +{ + WayfireVolume *wf_volume = (WayfireVolume*)user_data; + wf_volume->on_default_source_changed(); +} + void WayfireVolume::on_volume_value_changed() { /* User manually changed volume */ @@ -231,6 +281,7 @@ void WayfireVolume::init(Gtk::Box *container) style->add_class("flat"); gtk_widget_set_parent(GTK_WIDGET(popover.gobj()), GTK_WIDGET(button.gobj())); + gtk_widget_set_parent(GTK_WIDGET(second_popover.gobj()), GTK_WIDGET(button.gobj())); auto scroll_gesture = Gtk::EventControllerScroll::create(); scroll_gesture->signal_scroll().connect([=] (double dx, double dy) @@ -251,7 +302,22 @@ void WayfireVolume::init(Gtk::Box *container) return true; }, true); scroll_gesture->set_flags(Gtk::EventControllerScroll::Flags::VERTICAL); + + auto click_gesture = Gtk::GestureClick::create(); + click_gesture->set_button(3); // Only use right-click for this callback + click_gesture->signal_pressed().connect([=] (int count, double x, double y) + { + if (!second_popover.is_visible()) + { + second_popover.popup(); + } else + { + second_popover.popdown(); + } + }); + button.add_controller(scroll_gesture); + button.add_controller(click_gesture); /* Setup popover */ popover.set_autohide(false); @@ -271,8 +337,93 @@ void WayfireVolume::init(Gtk::Box *container) gvc_control = gvc_mixer_control_new("Wayfire Volume Control"); notify_default_sink_changed = g_signal_connect(gvc_control, "default-sink-changed", G_CALLBACK(default_sink_changed), this); + notify_default_source_changed = g_signal_connect(gvc_control, + "default-source-changed", G_CALLBACK(default_source_changed), this); gvc_mixer_control_open(gvc_control); + /* Setup Menu */ + auto menu = Gio::Menu::create(); + auto menu_item_settings = Gio::MenuItem::create("Settings", "action.settings"); + auto menu_item_toggle_volume = Gio::MenuItem::create("Mute Speaker", "action.togglespeaker"); + auto menu_item_toggle_mic = Gio::MenuItem::create("Mute Microphone", "action.togglemicrophone"); + + menu->append_item(menu_item_toggle_volume); + menu->append_item(menu_item_toggle_mic); + menu->append_item(menu_item_settings); + + second_popover.set_menu_model(menu); + + /* Menu Actions*/ + actions = Gio::SimpleActionGroup::create(); + + auto settings_action = Gio::SimpleAction::create("settings"); + speaker_action = Gio::SimpleAction::create_bool("togglespeaker", false); + microphone_action = Gio::SimpleAction::create_bool("togglemicrophone", false); + + settings_action->signal_activate().connect([=] (Glib::VariantBase vb) + { + std::vector volume_programs = {"pavucontrol", "mate-volume-control"}; + + Glib::RefPtr app_info; + std::string value = volume_settings; + if (value == "") + { + // Auto guess + for(auto program : volume_programs) + { + if(executable_exists(program)) + { + auto keyfile = Glib::KeyFile::create(); + keyfile->set_string("Desktop Entry", "Type", "Application"); + keyfile->set_string("Desktop Entry", "Exec", "/bin/sh -c \"" + program + "\""); + app_info = Gio::DesktopAppInfo::create_from_keyfile(keyfile); + break; + } + } + + } else + { + auto keyfile = Glib::KeyFile::create(); + keyfile->set_string("Desktop Entry", "Type", "Application"); + keyfile->set_string("Desktop Entry", "Exec", "/bin/sh -c \"" + value + "\""); + app_info = Gio::DesktopAppInfo::create_from_keyfile(keyfile); + } + + if (app_info) + { + auto ctx = Gdk::Display::get_default()->get_app_launch_context(); + app_info->launch(std::vector>(), ctx); + } + }); + + speaker_action->signal_activate().connect([=] (Glib::VariantBase vb) + { + if (gvc_stream) + { + bool muted = !gvc_mixer_stream_get_is_muted(gvc_stream); + gvc_mixer_stream_change_is_muted(gvc_stream, muted); + gvc_mixer_stream_push_volume(gvc_stream); + + } + }); + + microphone_action->signal_activate().connect([=] (Glib::VariantBase vb) + { + if (gvc_stream_mic) + { + bool muted = !(gvc_stream_mic && gvc_mixer_stream_get_is_muted(gvc_stream_mic)); + gvc_mixer_stream_change_is_muted(gvc_stream_mic, muted); + gvc_mixer_stream_push_volume(gvc_stream_mic); + + } + }); + + actions->add_action(speaker_action); + actions->add_action(microphone_action); + actions->add_action(settings_action); + + button.insert_action_group("action", actions); + /* Setup layout */ container->append(button); button.set_child(main_image); @@ -287,6 +438,10 @@ WayfireVolume::~WayfireVolume() { g_signal_handler_disconnect(gvc_control, notify_default_sink_changed); } + if (notify_default_source_changed) + { + g_signal_handler_disconnect(gvc_control, notify_default_source_changed); + } gvc_mixer_control_close(gvc_control); g_object_unref(gvc_control); diff --git a/src/panel/widgets/volume.hpp b/src/panel/widgets/volume.hpp index 39d3dd25..5f001365 100644 --- a/src/panel/widgets/volume.hpp +++ b/src/panel/widgets/volume.hpp @@ -38,9 +38,14 @@ class WayfireVolume : public WayfireWidget WayfireVolumeScale volume_scale; Gtk::Button button; Gtk::Popover popover; + Gtk::PopoverMenu second_popover; WfOption timeout{"panel/volume_display_timeout"}; WfOption scroll_sensitivity{"panel/volume_scroll_sensitivity"}; + WfOption volume_settings{"panel/volume_settings"}; + + std::shared_ptr actions; + std::shared_ptr speaker_action, microphone_action; // void on_volume_scroll(GdkEventScroll *event); // void on_volume_button_press(GdkEventButton *event); @@ -49,14 +54,18 @@ class WayfireVolume : public WayfireWidget GvcMixerControl *gvc_control; GvcMixerStream *gvc_stream = NULL; + GvcMixerStream *gvc_stream_mic = NULL; gdouble max_norm; // maximal volume for current stream gulong notify_volume_signal = 0; gulong notify_is_muted_signal = 0; + gulong notify_is_mic_muted_signal = 0; gulong notify_default_sink_changed = 0; + gulong notify_default_source_changed = 0; sigc::connection popover_timeout; sigc::connection volume_changed_signal; void disconnect_gvc_stream_signals(); + void disconnect_gvc_stream_signals_mic(); enum set_volume_flags_t { @@ -86,12 +95,14 @@ class WayfireVolume : public WayfireWidget /** Update the icon based on volume and muted state */ void update_icon(); + void update_menu(); /** Called when the volume changed from outside of the widget */ void on_volume_changed_external(); /** Called when the default sink changes */ void on_default_sink_changed(); + void on_default_source_changed(); /** * Check whether the popover should be auto-hidden, and if yes, start diff --git a/src/util/gtk-utils.cpp b/src/util/gtk-utils.cpp index 80cec7b5..48ad87ca 100644 --- a/src/util/gtk-utils.cpp +++ b/src/util/gtk-utils.cpp @@ -72,3 +72,27 @@ void image_set_icon(Gtk::Image *image, std::string path) image->set_from_icon_name(path); } } + + +/* Check if an executable of this name exists on path */ +bool executable_exists(std::string name) +{ + struct stat sb; + std::string delimiter = ":"; + std::string path = std::string(getenv("PATH")); + size_t start_pos = 0, end_pos = 0; + + while ((end_pos = path.find(':', start_pos)) != std::string::npos) + { + std::string current_path = + path.substr(start_pos, end_pos - start_pos) + "/"+name; + + if ((stat(current_path.c_str(), &sb) == 0) && (sb.st_mode & S_IXOTH)) + { + return true; + } + + start_pos = end_pos + 1; + } + return false; +} \ No newline at end of file diff --git a/src/util/gtk-utils.hpp b/src/util/gtk-utils.hpp index 0272f2bf..62344245 100644 --- a/src/util/gtk-utils.hpp +++ b/src/util/gtk-utils.hpp @@ -22,4 +22,6 @@ void invert_pixbuf(Glib::RefPtr& pbuff); void image_set_icon(Gtk::Image *image, std::string path); +bool executable_exists(std::string name); + #endif /* end of include guard: WF_GTK_UTILS */