diff --git a/.gitmodules b/.gitmodules index 03ef7c62..cf4f912a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "subprojects/wayland-logout"] path = subprojects/wayland-logout url = https://github.com/soreau/wayland-logout +[submodule "subprojects/wf-json"] + path = subprojects/wf-json + url = https://github.com/WayfireWM/wf-json diff --git a/meson.build b/meson.build index 147c4e35..83492f5e 100644 --- a/meson.build +++ b/meson.build @@ -14,6 +14,7 @@ project( ) wayfire = dependency('wayfire') +json = subproject('wf-json').get_variable('wfjson') wayland_client = dependency('wayland-client') wayland_protos = dependency('wayland-protocols') gtkmm = dependency('gtkmm-3.0', version: '>=3.24') @@ -22,6 +23,7 @@ gtklayershell = dependency('gtk-layer-shell-0', version: '>= 0.6', fallback: [' libpulse = dependency('libpulse', required : get_option('pulse')) dbusmenu_gtk = dependency('dbusmenu-gtk3-0.4') libgvc = subproject('gvc', default_options: ['static=true'], required : get_option('pulse')) +xkbregistry = dependency('xkbregistry') if get_option('wayland-logout') == true wayland_logout = subproject('wayland-logout') diff --git a/metadata/dock.xml b/metadata/dock.xml index 1c74e047..040ecd89 100644 --- a/metadata/dock.xml +++ b/metadata/dock.xml @@ -15,6 +15,16 @@ <_short>Autohide duration 300 + + + + + @@ -128,6 +153,10 @@ <_short>Battery Font default + <_short>Network @@ -247,6 +276,10 @@ <_short>Volume Scroll Sensitivity 0.05 + <_short>Notifications @@ -254,6 +287,18 @@ <_short>Notifications Display Timeout <_long>Set to negative value to disable. 2.5 + + + + <_short>Tray diff --git a/src/panel/meson.build b/src/panel/meson.build index 0a511c76..a9386a2c 100644 --- a/src/panel/meson.build +++ b/src/panel/meson.build @@ -6,6 +6,7 @@ widget_sources = ['widgets/battery.cpp', 'widgets/network.cpp', 'widgets/spacing.cpp', 'widgets/separator.cpp', + 'widgets/language.cpp', 'widgets/window-list/window-list.cpp', 'widgets/window-list/toplevel.cpp', 'widgets/notifications/daemon.cpp', @@ -17,7 +18,7 @@ widget_sources = ['widgets/battery.cpp', 'widgets/tray/item.cpp', 'widgets/tray/host.cpp'] -deps = [gtkmm, wayland_client, libutil, wf_protos, wfconfig, gtklayershell, dbusmenu_gtk] +deps = [gtkmm, wayland_client, libutil, wf_protos, wfconfig, gtklayershell, xkbregistry, json, dbusmenu_gtk] if libpulse.found() widget_sources += 'widgets/volume.cpp' diff --git a/src/panel/panel.cpp b/src/panel/panel.cpp index 566dd517..75914786 100644 --- a/src/panel/panel.cpp +++ b/src/panel/panel.cpp @@ -18,6 +18,7 @@ #include "widgets/battery.hpp" #include "widgets/command-output.hpp" +#include "widgets/language.hpp" #include "widgets/menu.hpp" #include "widgets/clock.hpp" #include "widgets/launchers.hpp" @@ -78,7 +79,7 @@ class WayfirePanel::impl window->override_background_color(rgba); }; - + WfOption panel_layer{"panel/layer"}; std::function set_panel_layer = [=] () { @@ -119,10 +120,7 @@ class WayfirePanel::impl bg_color.set_callback(on_window_color_updated); on_window_color_updated(); // set initial color - window->show_all(); - init_widgets(); - init_layout(); - + window->signal_delete_event().connect( sigc::mem_fun(this, &WayfirePanel::impl::on_delete)); } @@ -134,22 +132,7 @@ class WayfirePanel::impl return true; } - void init_layout() - { - left_box.get_style_context()->add_class("left"); - center_box.get_style_context()->add_class("center"); - right_box.get_style_context()->add_class("right"); - content_box.pack_start(left_box, false, false); - content_box.pack_end(right_box, false, false); - if (!center_box.get_children().empty()) - { - content_box.set_center_widget(center_box); - } - center_box.show_all(); - window->add(content_box); - window->show_all(); - } std::optional widget_with_value(std::string value, std::string prefix) { @@ -225,6 +208,19 @@ class WayfirePanel::impl { return Widget(new WfCommandOutputButtons()); } + + if (name == "language") + { + if (get_ipc_server_instance()->connected) + { + return Widget(new WayfireLanguage()); + } else + { + std::cerr << "Wayfire IPC not connected, which is required to load language widget." << + std::endl; + return nullptr; + } + } if (auto pixel = widget_with_value(name, "spacing")) { @@ -285,34 +281,10 @@ class WayfirePanel::impl WfOption left_widgets_opt{"panel/widgets_left"}; WfOption right_widgets_opt{"panel/widgets_right"}; WfOption center_widgets_opt{"panel/widgets_center"}; - void init_widgets() - { - left_widgets_opt.set_callback([=] () - { - reload_widgets((std::string)left_widgets_opt, left_widgets, left_box); - }); - right_widgets_opt.set_callback([=] () - { - reload_widgets((std::string)right_widgets_opt, right_widgets, right_box); - }); - center_widgets_opt.set_callback([=] () - { - reload_widgets((std::string)center_widgets_opt, center_widgets, center_box); - if (center_box.get_children().empty()) - { - content_box.unset_center_widget(); - } else - { - content_box.set_center_widget(center_box); - } - }); - reload_widgets((std::string)left_widgets_opt, left_widgets, left_box); - reload_widgets((std::string)right_widgets_opt, right_widgets, right_box); - reload_widgets((std::string)center_widgets_opt, center_widgets, center_box); - } public: + impl(WayfireOutput *output) : output(output) { create_window(); @@ -345,6 +317,67 @@ class WayfirePanel::impl w->handle_config_reload(); } } + + WayfirePanelApp *panel_app; + void set_panel_app(WayfirePanelApp *panel_app) + { + this->panel_app = panel_app; + } + + void init_widgets() + { + + left_widgets_opt.set_callback([=] () + { + reload_widgets((std::string)left_widgets_opt, left_widgets, left_box); + }); + right_widgets_opt.set_callback([=] () + { + reload_widgets((std::string)right_widgets_opt, right_widgets, right_box); + }); + center_widgets_opt.set_callback([=] () + { + reload_widgets((std::string)center_widgets_opt, center_widgets, center_box); + if (center_box.get_children().empty()) + { + content_box.unset_center_widget(); + } else + { + content_box.set_center_widget(center_box); + } + }); + + reload_widgets((std::string)left_widgets_opt, left_widgets, left_box); + reload_widgets((std::string)right_widgets_opt, right_widgets, right_box); + reload_widgets((std::string)center_widgets_opt, center_widgets, center_box); + } + + void init_layout() + { + left_box.get_style_context()->add_class("left"); + center_box.get_style_context()->add_class("center"); + right_box.get_style_context()->add_class("right"); + content_box.pack_start(left_box, false, false); + content_box.pack_end(right_box, false, false); + if (!center_box.get_children().empty()) + { + content_box.set_center_widget(center_box); + } + + center_box.show_all(); + window->add(content_box); + window->show_all(); + } + void unhide_now() + { + + window->m_show_uncertain(); + + } + std::shared_ptr get_ipc_server_instance() + { + return panel_app->get_ipc_server_instance(); + } }; WayfirePanel::WayfirePanel(WayfireOutput *output) : pimpl(new impl(output)) @@ -364,6 +397,27 @@ void WayfirePanel::handle_config_reload() return pimpl->handle_config_reload(); } +void WayfirePanel::init_widgets() +{ + pimpl->init_widgets(); +} + +void WayfirePanel::init_layout() +{ + pimpl->init_layout(); +} + +void WayfirePanel::unhide_now() +{ + pimpl->unhide_now(); +} + +void WayfirePanel::set_panel_app(WayfirePanelApp *panel_app) +{ + pimpl->set_panel_app(panel_app); +} + + class WayfirePanelApp::impl { public: @@ -436,8 +490,18 @@ void WayfirePanelApp::add_css_file(std::string file, int priority) void WayfirePanelApp::handle_new_output(WayfireOutput *output) { + if (!ipc_server) + { + ipc_server = WayfireIPC::get_instance(); + } + priv->panels[output] = std::unique_ptr( new WayfirePanel(output)); + priv->panels[output]->handle_config_reload(); + priv->panels[output]->set_panel_app(this); + priv->panels[output]->init_widgets(); + priv->panels[output]->init_layout(); + } WayfirePanel*WayfirePanelApp::panel_for_wl_output(wl_output *output) @@ -468,6 +532,11 @@ WayfirePanelApp& WayfirePanelApp::get() return dynamic_cast(*instance.get()); } +std::shared_ptr WayfirePanelApp::get_ipc_server_instance() +{ + return ipc_server; +} + void WayfirePanelApp::create(int argc, char **argv) { if (instance) @@ -479,6 +548,14 @@ void WayfirePanelApp::create(int argc, char **argv) instance->run(); } +void WayfirePanelApp::unhide_now() +{ + for (auto& p : priv->panels) + { + p.second->unhide_now(); + } +} + WayfirePanelApp::~WayfirePanelApp() = default; WayfirePanelApp::WayfirePanelApp(int argc, char **argv) : WayfireShellApp(argc, argv), priv(new impl()) diff --git a/src/panel/panel.hpp b/src/panel/panel.hpp index c20f94b9..fc5bfdc7 100644 --- a/src/panel/panel.hpp +++ b/src/panel/panel.hpp @@ -7,7 +7,9 @@ #include #include "wf-shell-app.hpp" +#include "wf-ipc.hpp" +class WayfirePanelApp; class WayfirePanel { public: @@ -16,6 +18,11 @@ class WayfirePanel wl_surface *get_wl_surface(); Gtk::Window& get_window(); void handle_config_reload(); + void init_widgets(); + void init_layout(); + void unhide_now(); + void set_panel_app(WayfirePanelApp *panel_app); + std::shared_ptr get_ipc_server_instance(); private: class impl; @@ -32,11 +39,14 @@ class WayfirePanelApp : public WayfireShellApp * call to create() */ static void create(int argc, char **argv); ~WayfirePanelApp(); - + void handle_new_output(WayfireOutput *output) override; void handle_output_removed(WayfireOutput *output) override; void on_config_reload() override; void on_css_reload() override; + void unhide_now(); + std::shared_ptr get_ipc_server_instance(); + std::shared_ptr ipc_server = nullptr; private: WayfirePanelApp(int argc, char **argv); diff --git a/src/panel/widgets/battery.cpp b/src/panel/widgets/battery.cpp index 2491044d..943dd0b4 100644 --- a/src/panel/widgets/battery.cpp +++ b/src/panel/widgets/battery.cpp @@ -174,6 +174,17 @@ void WayfireBatteryInfo::update_details() } } +void WayfireBatteryInfo::button_press_cb(GdkEventButton *event) +{ + + if ((event->button == 3) && (event->type == GDK_BUTTON_PRESS)) + { + if (g_spawn_command_line_async (std::string(battery_left_click_command).c_str(), NULL) == FALSE) + g_warning ("Couldn't execute command: %s", std::string(battery_left_click_command).c_str()); + } +} + + void WayfireBatteryInfo::update_state() { std::cout << "unimplemented reached, in battery.cpp: " @@ -234,6 +245,8 @@ void WayfireBatteryInfo::init(Gtk::HBox *container) button_box.add(icon); button.get_style_context()->add_class("battery"); button.get_style_context()->add_class("flat"); + button.signal_button_press_event().connect_notify( + sigc::mem_fun(this, &WayfireBatteryInfo::button_press_cb)); status_opt.set_callback([=] () { update_details(); }); font_opt.set_callback([=] () { update_font(); }); diff --git a/src/panel/widgets/battery.hpp b/src/panel/widgets/battery.hpp index b2a5d986..817a5761 100644 --- a/src/panel/widgets/battery.hpp +++ b/src/panel/widgets/battery.hpp @@ -25,6 +25,7 @@ class WayfireBatteryInfo : public WayfireWidget WfOption font_opt{"panel/battery_font"}; WfOption size_opt{"panel/battery_icon_size"}; WfOption invert_opt{"panel/battery_icon_invert"}; + WfOption battery_left_click_command{"panel/battery_left_click_command"}; Gtk::Button button; Gtk::Label label; @@ -41,6 +42,7 @@ class WayfireBatteryInfo : public WayfireWidget void update_icon(); void update_details(); void update_state(); + void button_press_cb(GdkEventButton *event); void on_properties_changed( const Gio::DBus::Proxy::MapChangedProperties& properties, diff --git a/src/panel/widgets/language.cpp b/src/panel/widgets/language.cpp new file mode 100644 index 00000000..66ceff66 --- /dev/null +++ b/src/panel/widgets/language.cpp @@ -0,0 +1,156 @@ +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include "../widget.hpp" +#include "wf-popover.hpp" + +#include "language.hpp" +#include "wf-ipc.hpp" +#include "panel.hpp" + +void WayfireLanguage::init(Gtk::HBox *container) +{ + + ipc_client = WayfirePanelApp::get().get_ipc_server_instance()->create_client(); + + if (!ipc_client) + { + std::cout << "Failed to connect to ipc. (are ipc and ipc-rules plugins loaded?)"; + + } + + auto style = button.get_style_context(); + style->add_class("flat"); + style->add_class("language"); + style->remove_class("activated"); + btn_sig = button.signal_clicked().connect(sigc::mem_fun(*this, &WayfireLanguage::next_layout)); + + container->pack_start(button, Gtk::PACK_SHRINK); + + button.show_all(); + ipc_client->subscribe(this, {"keyboard-modifier-state-changed"}); + ipc_client->send("{\"method\":\"wayfire/get-keyboard-state\"}", [=] (wf::json_t data) + { + if (data.serialize().find( + "error") != std::string::npos) + { + std::cerr << "Error getting keyboard state for language widget. Is wayfire ipc-rules plugin enabled?" << std::endl; + return; + } + + set_available(data["possible-layouts"]); + set_current(data["layout-index"]); + }); +} + +void WayfireLanguage::on_event(wf::json_t data) +{ + std::cout <<"on event/n"; + if (data["event"].as_string() == "keyboard-modifier-state-changed") + { + if (available_layouts.size() == 0) + { + set_available(data["state"]["possible-layouts"]); + } + + auto state_layout = data["state"]["layout-index"].as_uint(); + if (state_layout != current_layout) + { + current_layout = state_layout; + std::cerr << "Unhiding panel" << std::endl; + WayfirePanelApp::get().unhide_now(); + set_current(state_layout); + } + } +} + +bool WayfireLanguage::update_label() +{ + std::cout << "update label /n"; + if (current_layout >= available_layouts.size()) + { + return false; + } + + button.set_label(available_layouts[current_layout].ID); + return true; +} + +void WayfireLanguage::set_current(uint32_t index) +{ + std::cout << "set_current /n"; + current_layout = index; + update_label(); +} + +void WayfireLanguage::set_available(wf::json_t layouts) +{ + std::cout << "set_available/n" ; + std::vector layouts_available; + std::map names; + + for (size_t i = 0; i < layouts.size(); i++) + { + auto elem = layouts[i]; + names[elem] = i; + layouts_available.push_back(Layout{ + .Name = (std::string)elem, + .ID = "", + }); + } + + auto context = rxkb_context_new(RXKB_CONTEXT_NO_FLAGS); + rxkb_context_parse_default_ruleset(context); + auto rlayout = rxkb_layout_first(context); + for (; rlayout != NULL; rlayout = rxkb_layout_next(rlayout)) + { + auto descr = rxkb_layout_get_description(rlayout); + auto name = names.find(descr); + if (name != names.end()) + { + layouts_available[name->second].ID = rxkb_layout_get_brief(rlayout); + } + } + + available_layouts = layouts_available; + update_label(); +} + +void WayfireLanguage::next_layout() +{ + std::cout << "next layout /n"; + uint32_t next = current_layout + 1; + if (next >= available_layouts.size()) + { + next = 0; + } + + wf::json_t message; + message["method"] = "wayfire/set-keyboard-state"; + message["data"] = wf::json_t(); + message["data"]["layout-index"] = next; + ipc_client->send(message.serialize()); +} + +WayfireLanguage::WayfireLanguage() +{} + +WayfireLanguage::~WayfireLanguage() +{ + if (ipc_client) + { + ipc_client->unsubscribe(this); + } + + btn_sig.disconnect(); +} diff --git a/src/panel/widgets/language.hpp b/src/panel/widgets/language.hpp new file mode 100644 index 00000000..e8542705 --- /dev/null +++ b/src/panel/widgets/language.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../widget.hpp" +#include "wf-ipc.hpp" + +struct Layout +{ + std::string Name; + std::string ID; +}; + +class WayfireLanguage : public WayfireWidget , public IIPCSubscriber +{ + Gtk::Label label; + Gtk::Button button; + sigc::connection btn_sig; + + std::shared_ptr ipc_client; + uint32_t current_layout; + std::vector available_layouts; + + public: + void init(Gtk::HBox *container) override; + void on_event(wf::json_t data) override; + bool update_label(); + void set_current(uint32_t index); + void set_available(wf::json_t layouts); + void next_layout(); + WayfireLanguage(); + ~WayfireLanguage(); +}; diff --git a/src/panel/widgets/menu.cpp b/src/panel/widgets/menu.cpp index 00759611..eb762541 100644 --- a/src/panel/widgets/menu.cpp +++ b/src/panel/widgets/menu.cpp @@ -15,7 +15,7 @@ #include "launchers.hpp" #include "wf-autohide-window.hpp" -#define MAX_LAUNCHER_NAME_LENGTH 11 +#define MAX_LAUNCHER_NAME_LENGTH 24 const std::string default_icon = "wayfire"; WfMenuCategory::WfMenuCategory(std::string _name, std::string _icon_name) : diff --git a/src/panel/widgets/notifications/notification-center.cpp b/src/panel/widgets/notifications/notification-center.cpp index d91bdd1f..1613f233 100644 --- a/src/panel/widgets/notifications/notification-center.cpp +++ b/src/panel/widgets/notifications/notification-center.cpp @@ -17,7 +17,7 @@ void WayfireNotificationCenter::init(Gtk::HBox *container) button->show_all(); auto *popover = button->get_popover(); - popover->set_size_request(WIDTH, HEIGHT); + popover->set_size_request(notification_width, notification_height); popover->get_style_context()->add_class("notification-popover"); vbox.set_valign(Gtk::ALIGN_START); @@ -27,7 +27,7 @@ void WayfireNotificationCenter::init(Gtk::HBox *container) button->set_tooltip_text("Middle click to toggle DND mode."); button->signal_button_press_event().connect_notify([=] (GdkEventButton *ev) - { + { if (ev->button == 2) { dnd_enabled = !dnd_enabled; @@ -62,9 +62,10 @@ void WayfireNotificationCenter::newNotification(Notification::id_type id, bool s widget->set_reveal_child(); if (show_popup && !dnd_enabled || (show_critical_in_dnd && (notification.hints.urgency == 2))) { - auto *popover = button->get_popover(); + auto *popover = button->get_popover(); + popover->set_size_request(notification_width, notification_height); if ((timeout > 0) && (!popover_timeout.empty() || !popover->is_visible())) - { + { popover_timeout.disconnect(); popover_timeout = Glib::signal_timeout().connect( [=] @@ -76,7 +77,6 @@ void WayfireNotificationCenter::newNotification(Notification::id_type id, bool s }, timeout * 1000); } - button->set_keyboard_interactive(false); popover->popup(); } diff --git a/src/panel/widgets/notifications/notification-center.hpp b/src/panel/widgets/notifications/notification-center.hpp index d8106539..aa003129 100644 --- a/src/panel/widgets/notifications/notification-center.hpp +++ b/src/panel/widgets/notifications/notification-center.hpp @@ -11,7 +11,6 @@ class WayfireNotificationCenter : public WayfireWidget { private: - static const int WIDTH = 300, HEIGHT = 400; const std::shared_ptr daemon = Daemon::Launch(); sigc::connection notification_new_conn; @@ -34,6 +33,8 @@ class WayfireNotificationCenter : public WayfireWidget WfOption timeout{"panel/notifications_autohide_timeout"}; WfOption show_critical_in_dnd{"panel/notifications_critical_in_dnd"}; WfOption icon_size{"panel/notifications_icon_size"}; + WfOption notification_width{"panel/notification_width"}; + WfOption notification_height{"panel/notification_height"}; bool dnd_enabled = false; public: diff --git a/src/panel/widgets/volume.cpp b/src/panel/widgets/volume.cpp index 4535cfa2..68bce2fb 100644 --- a/src/panel/widgets/volume.cpp +++ b/src/panel/widgets/volume.cpp @@ -3,96 +3,22 @@ #include "volume.hpp" #include "launchers.hpp" #include "gtk-utils.hpp" +#include "panel.hpp" +#include "icon-select.hpp" -WayfireVolumeScale::WayfireVolumeScale() -{ - this->signal_draw().connect_notify( - [=] (const Cairo::RefPtr& ctx) - { - if (this->current_volume.running()) - { - value_changed.block(); - this->set_value(this->current_volume); - value_changed.unblock(); - } - }, true); - - value_changed = this->signal_value_changed().connect_notify([=] () - { - this->current_volume.animate(this->get_value(), this->get_value()); - if (this->user_changed_callback) - { - this->user_changed_callback(); - } - }); -} - -void WayfireVolumeScale::set_target_value(double value) -{ - this->current_volume.animate(value); - this->queue_draw(); -} - -double WayfireVolumeScale::get_target_value() const -{ - return this->current_volume.end; -} - -void WayfireVolumeScale::set_user_changed_callback( - std::function callback) -{ - this->user_changed_callback = callback; -} - -enum VolumeLevel -{ - VOLUME_LEVEL_MUTE = 0, - VOLUME_LEVEL_LOW, - VOLUME_LEVEL_MED, - VOLUME_LEVEL_HIGH, - VOLUME_LEVEL_OOR, /* Out of range */ -}; - -static VolumeLevel get_volume_level(pa_volume_t volume, pa_volume_t max) -{ - auto third = max / 3; - if (volume == 0) - { - return VOLUME_LEVEL_MUTE; - } else if ((volume > 0) && (volume <= third)) - { - return VOLUME_LEVEL_LOW; - } else if ((volume > third) && (volume <= (third * 2))) - { - return VOLUME_LEVEL_MED; - } else if ((volume > (third * 2)) && (volume <= max)) - { - return VOLUME_LEVEL_HIGH; - } - - return VOLUME_LEVEL_OOR; -} +#define ICON(volume) icon_from_range(volume_icons, volume) void WayfireVolume::update_icon() { - VolumeLevel current = - get_volume_level(volume_scale.get_target_value(), max_norm); - if (gvc_stream && gvc_mixer_stream_get_is_muted(gvc_stream)) { - set_image_icon(main_image, "audio-volume-muted", icon_size); + set_image_icon(main_image, ICON(0), icon_size); + WayfirePanelApp::get().unhide_now(); return; } - std::map icon_name_from_state = { - {VOLUME_LEVEL_MUTE, "audio-volume-muted"}, - {VOLUME_LEVEL_LOW, "audio-volume-low"}, - {VOLUME_LEVEL_MED, "audio-volume-medium"}, - {VOLUME_LEVEL_HIGH, "audio-volume-high"}, - {VOLUME_LEVEL_OOR, "audio-volume-muted"}, - }; - - set_image_icon(main_image, icon_name_from_state.at(current), icon_size); + set_image_icon(main_image, ICON(volume_scale.get_target_value()/(double)max_norm), icon_size); + WayfirePanelApp::get().unhide_now(); } bool WayfireVolume::on_popover_timeout(int timer) @@ -155,6 +81,12 @@ void WayfireVolume::on_volume_button_press(GdkEventButton *event) gvc_mixer_stream_set_is_muted(gvc_stream, true); } } + if ((event->button == 3) && (event->type == GDK_BUTTON_PRESS)) + { + if (g_spawn_command_line_async (std::string(volume_left_click_command).c_str(), NULL) == FALSE) + g_warning ("Couldn't execute command: %s", std::string(volume_left_click_command).c_str()); + } + } void WayfireVolume::on_volume_changed_external() @@ -184,7 +116,7 @@ static void notify_is_muted(GvcMixerControl *gvc_control, guint id, gpointer user_data) { WayfireVolume *wf_volume = (WayfireVolume*)user_data; - wf_volume->update_icon(); + wf_volume->update_icon(); } void WayfireVolume::disconnect_gvc_stream_signals() @@ -241,7 +173,6 @@ static void default_sink_changed(GvcMixerControl *gvc_control, void WayfireVolume::on_volume_value_changed() { /* User manually changed volume */ - button->grab_focus(); set_volume(volume_scale.get_target_value()); } diff --git a/src/panel/widgets/volume.hpp b/src/panel/widgets/volume.hpp index 69f1d9c1..7426ae15 100644 --- a/src/panel/widgets/volume.hpp +++ b/src/panel/widgets/volume.hpp @@ -8,37 +8,19 @@ #include #include "gvc-mixer-control.h" #include +#include -/** - * A custom scale which animates transitions when its value is - * changed programatically. - */ -class WayfireVolumeScale : public Gtk::Scale -{ - wf::animation::simple_animation_t current_volume{wf::create_option(200)}; - sigc::connection value_changed; - std::function user_changed_callback; - - public: - WayfireVolumeScale(); - - /* Gets the current target value */ - double get_target_value() const; - /* Set a target value to animate towards */ - void set_target_value(double value); - /** Set the callback when the user changes the scale value */ - void set_user_changed_callback(std::function callback); -}; class WayfireVolume : public WayfireWidget { Gtk::Image main_image; - WayfireVolumeScale volume_scale; + WayfireAnimatedScale volume_scale; std::unique_ptr button; WfOption icon_size{"panel/volume_icon_size"}; WfOption timeout{"panel/volume_display_timeout"}; WfOption scroll_sensitivity{"panel/volume_scroll_sensitivity"}; + WfOption volume_left_click_command{"panel/volume_left_click_command"}; void on_volume_scroll(GdkEventScroll *event); void on_volume_button_press(GdkEventButton *event); diff --git a/src/util/animated-scale.cpp b/src/util/animated-scale.cpp new file mode 100644 index 00000000..7abeea81 --- /dev/null +++ b/src/util/animated-scale.cpp @@ -0,0 +1,48 @@ +#include +#include "animated-scale.hpp" + +WayfireAnimatedScale::WayfireAnimatedScale() +{ + this->signal_draw().connect_notify( + [=] (const Cairo::RefPtr& ctx) + { + if (this->current_volume.running()) + { + value_changed.block(); + this->set_value(this->current_volume); + value_changed.unblock(); + } + }, true); + + value_changed = this->signal_value_changed().connect_notify([=] () + { + this->current_volume.animate(this->get_value(), this->get_value()); + if (this->user_changed_callback) + { + this->user_changed_callback(); + } + }); +} + +WayfireAnimatedScale::~WayfireAnimatedScale() +{ + value_changed.disconnect(); +} + +void WayfireAnimatedScale::set_target_value(double value) +{ + this->current_volume.animate(value); + this->queue_draw(); +} + + +double WayfireAnimatedScale::get_target_value() const +{ + return this->current_volume.end; +} + +void WayfireAnimatedScale::set_user_changed_callback( + std::function callback) +{ + this->user_changed_callback = callback; +} diff --git a/src/util/animated-scale.hpp b/src/util/animated-scale.hpp new file mode 100644 index 00000000..1e81dea8 --- /dev/null +++ b/src/util/animated-scale.hpp @@ -0,0 +1,31 @@ +#ifndef ANIMATED_SCALE_HPP +#define ANIMATED_SCALE_HPP + +#include "wf-popover.hpp" +#include +#include +#include + +/** + * A custom scale which animates transitions when its value is changed programatically. + */ +class WayfireAnimatedScale : public Gtk::Scale +{ + wf::animation::simple_animation_t current_volume{wf::create_option(200)}; + sigc::connection value_changed; + std::function user_changed_callback; + + public: + WayfireAnimatedScale(); + ~WayfireAnimatedScale(); + + /* Gets the current target value */ + double get_target_value() const; + /* Set a target value to animate towards */ + void set_target_value(double value); + /** Set the callback when the user changes the scale value */ + void set_user_changed_callback(std::function callback); + +}; + +#endif // ANIMATED_SCALE_HPP diff --git a/src/util/icon-select.cpp b/src/util/icon-select.cpp new file mode 100644 index 00000000..d58ea56c --- /dev/null +++ b/src/util/icon-select.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "icon-select.hpp" + +std::string icon_from_range(std::map> icons, double value) +{ + for (auto pair : icons) + { + if (value <= pair.first) + { + auto theme = Gtk::IconTheme::get_for_screen(Gdk::Display::get_default()->get_default_screen()); + if (!theme) + { + // let’s stick to standard icons in this case + return *pair.second.end(); + } + + for (auto name : pair.second) + { + if (theme->has_icon(name)) + { + return name; + } + } + } + } + + return ""; +} diff --git a/src/util/icon-select.hpp b/src/util/icon-select.hpp new file mode 100644 index 00000000..107c40d2 --- /dev/null +++ b/src/util/icon-select.hpp @@ -0,0 +1,29 @@ +#include +#include +#include +#include + +std::string icon_from_range(std::map> icons, double value); + +// the number in the first term is the maximal value at which this icon will be shown +// selection values of the tables are expected to be ordered from least to greatest +// the vector of icons is the different possibilites of icons to show, +// with the prefered one as the first. This is to be able to use non-standard icons +// if they are present in the current theme, and fall back to a standard one if not. + +const std::map> volume_icons = { + {std::numeric_limits::min(), {"emblem-unreadable"}}, + {0.0, {"audio-volume-muted"}}, + {0.33, {"audio-volume-low"}}, + {0.66, {"audio-volume-medium"}}, + {1.0, {"audio-volume-high"}}, + {std::numeric_limits::max(), {"audio-volume-high-danger", "dialog-warning"}} +}; + +const std::map> brightness_display_icons = { + {std::numeric_limits::min(), {"display-brightness-invalid", "emblem-unreadable"}}, + {0, {"display-brightness-low-symbolic", "display-brightness-low"}}, + {0.5, {"display-brightness-medium-symbolic", "display-medium-low"}}, + {1, {"display-brightness-high-symbolic", "display-brightness-high"}}, + {std::numeric_limits::max(), {"display-brightness-invalid", "emblem-unreadable"}}, +}; diff --git a/src/util/meson.build b/src/util/meson.build index 056f79db..1b1a7933 100644 --- a/src/util/meson.build +++ b/src/util/meson.build @@ -1,5 +1,26 @@ -util = static_library('util', ['gtk-utils.cpp', 'wf-shell-app.cpp', 'wf-autohide-window.cpp', 'wf-popover.cpp'], - dependencies: [wf_protos, wayland_client, gtkmm, wfconfig, libinotify, gtklayershell]) +util = static_library( + 'util', + [ + 'gtk-utils.cpp', + 'wf-shell-app.cpp', + 'animated-scale.cpp', + 'wf-ipc.cpp', + 'wf-autohide-window.cpp', + 'wf-popover.cpp', + 'icon-select.cpp' + ], + + dependencies: + [ + wf_protos, + wayland_client, + gtkmm, + wfconfig, + libinotify, + json, + gtklayershell + ] +) util_includes = include_directories('.') libutil = declare_dependency( diff --git a/src/util/wf-autohide-window.cpp b/src/util/wf-autohide-window.cpp index 6e785ee1..21b7f057 100644 --- a/src/util/wf-autohide-window.cpp +++ b/src/util/wf-autohide-window.cpp @@ -9,15 +9,14 @@ #include #include -#define AUTOHIDE_SHOW_DELAY 300 -#define AUTOHIDE_HIDE_DELAY 500 - WayfireAutohidingWindow::WayfireAutohidingWindow(WayfireOutput *output, const std::string& section) : position{section + "/position"}, y_position{WfOption{section + "/autohide_duration"}}, edge_offset{section + "/edge_offset"}, - autohide_opt{section + "/autohide"} + autohide_opt{section + "/autohide"}, + autohide_show_delay{section + "/autohide_show_delay"}, + autohide_hide_delay{section + "/autohide_hide_delay"} { this->output = output; this->set_decorated(false); @@ -135,7 +134,7 @@ void WayfireAutohidingWindow::m_show_uncertain() { schedule_hide(0); return false; - }, AUTOHIDE_HIDE_DELAY); + }, autohide_hide_delay); } } @@ -218,7 +217,7 @@ void WayfireAutohidingWindow::setup_hotspot() ZWF_OUTPUT_V2_HOTSPOT_EDGE_TOP : ZWF_OUTPUT_V2_HOTSPOT_EDGE_BOTTOM; this->edge_hotspot = zwf_output_v2_create_hotspot(output->output, - edge, edge_offset, AUTOHIDE_SHOW_DELAY); + edge, edge_offset, autohide_show_delay); this->panel_hotspot = zwf_output_v2_create_hotspot(output->output, edge, this->get_allocated_height(), 0); // immediate @@ -254,7 +253,7 @@ void WayfireAutohidingWindow::setup_hotspot() this->input_inside_panel = false; if (this->should_autohide()) { - this->schedule_hide(AUTOHIDE_HIDE_DELAY); + this->schedule_hide(autohide_hide_delay); } }; @@ -437,7 +436,7 @@ void WayfireAutohidingWindow::unset_active_popover(WayfireMenuButton& button) if (should_autohide()) { - schedule_hide(AUTOHIDE_HIDE_DELAY); + schedule_hide(autohide_hide_delay); } } diff --git a/src/util/wf-autohide-window.hpp b/src/util/wf-autohide-window.hpp index b96d14ae..fe357360 100644 --- a/src/util/wf-autohide-window.hpp +++ b/src/util/wf-autohide-window.hpp @@ -30,6 +30,9 @@ class WayfireAutohidingWindow : public Gtk::Window * 2. section/autohide_duration * 3. section/edge_offset * 4. section/autohide + * 5. section/autohide_duration + * 6. section/autohide_show_delay + * 7. section/autohide_hide_delay */ WayfireAutohidingWindow(WayfireOutput *output, const std::string& section); WayfireAutohidingWindow(WayfireAutohidingWindow&&) = delete; @@ -51,6 +54,7 @@ class WayfireAutohidingWindow : public Gtk::Window * in the meantime */ void schedule_hide(int delay); void schedule_show(int delay); + void m_show_uncertain(); /** When auto exclusive zone is set, the window will adjust its exclusive * zone based on the window size. @@ -92,6 +96,9 @@ class WayfireAutohidingWindow : public Gtk::Window bool last_autohide_value = autohide_opt; void setup_autohide(); void update_autohide(); + + WfOption autohide_show_delay; + WfOption autohide_hide_delay; bool auto_exclusive_zone = !autohide_opt; int auto_exclusive_zone_size = 0; @@ -103,8 +110,6 @@ class WayfireAutohidingWindow : public Gtk::Window bool m_do_hide(); int autohide_counter = static_cast(autohide_opt); - /** Show the window but hide if no pointer input */ - void m_show_uncertain(); int32_t last_hotspot_height = -1; bool input_inside_panel = false; diff --git a/src/util/wf-ipc.cpp b/src/util/wf-ipc.cpp new file mode 100644 index 00000000..bae91f77 --- /dev/null +++ b/src/util/wf-ipc.cpp @@ -0,0 +1,398 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "wf-ipc.hpp" + +WayfireIPC::WayfireIPC() +{ + + std::cerr << "WAYFIRE_IPC CREATING" << std::endl; + if (connect()) + { + sig_connection = Glib::signal_io().connect( + sigc::mem_fun(*this, &WayfireIPC::receive), + connection->get_socket()->get_fd(), + Glib::IOCondition::IO_IN); + std::cerr << "WAYFIRE_IPC CONNECTED" << std::endl; + connected = true; + } else + { + std::cerr << "Failed to connect to WAYFIRE_SOCKET. Is wayfire ipc plugin enabled?" << std::endl; + } +} + +WayfireIPC::~WayfireIPC() +{ + if (connected) + { + disconnect(); + } +} + +bool WayfireIPC::connect() +{ + const char *socket_path = getenv("WAYFIRE_SOCKET"); + if (!socket_path || std::string(socket_path).empty()) + { + std::cerr << "Wayfire socket not found" << std::endl; + return false; + } + + try { + auto client = Gio::SocketClient::create(); + auto address = Gio::UnixSocketAddress::create(socket_path); + connection = client->connect(address); + connection->get_socket()->set_blocking(false); + output = connection->get_output_stream(); + input = connection->get_input_stream(); + cancel = Gio::Cancellable::create(); + + return true; + } catch (const Glib::Error& ex) + { + std::cerr << "Error connecting to WAYFIRE_SOCKET at path \"" << socket_path << "\": " << ex.what(); + return false; + } + + return false; +} + +void WayfireIPC::disconnect() +{ + cancel->cancel(); + sig_connection.disconnect(); + connection->close(); +} + +void WayfireIPC::send(const std::string& message) +{ + send_message(message); + response_handlers.push(0); +} + +void WayfireIPC::send(const std::string& message, int response_handler) +{ + if (!connected) + { + return; + } + + send_message(message); + response_handlers.push(response_handler); +} + +void WayfireIPC::send_message(const std::string& message) +{ + if (output->has_pending() || writing) + { + write_queue.push(message); + write_next(); + return; + } + + // Shortcut: stream is not busy, no queue needed + write_stream(message); +} + +void WayfireIPC::write_next() +{ + if (writing || cancel->is_cancelled()) + { + return; + } + + writing = true; + sig_connection = Glib::signal_io().connect( + sigc::mem_fun(*this, &WayfireIPC::send_queue), + connection->get_socket()->get_fd(), + Glib::IOCondition::IO_OUT); +} + +void WayfireIPC::write_stream(const std::string& message) +{ + try { + writing = true; + uint32_t length = message.size(); + // Pointer to data must be valid until completely wrote and + // slot is called, as documented for write_all_async. + // So we pin it with a shared pointer, destroyed *after* slot is called. + auto all_data = std::make_shared((char*)&length, 4); + *all_data += message; + output->write_all_async(all_data->data(), all_data->size(), + [this, all_data] (Glib::RefPtr& result) + { + try { + gsize written; + auto success = output->write_all_finish(result, written); + if (!success) + { + LOGE("IPC error: write failed. Bytes written: ", written); + } + + this->writing = false; + if (!cancel->is_cancelled()) + { + write_next(); + } + } catch (const Glib::Error& e) + { + this->writing = false; + if (e.code() == G_IO_ERROR_CANCELLED) + { + // Intended behavior + return; + } else + { + LOGE("IPC error: write failed: ", e.what()); + } + } + }, cancel); + } catch (const Gio::Error& e) + { + LOGE("IPC error: ", e.what()); + } +} + +bool WayfireIPC::send_queue(Glib::IOCondition cond) +{ + if (write_queue.empty()) + { + writing = false; + return false; + } + + auto message = write_queue.front(); + write_queue.pop(); + + write_stream(message); + return false; +} + +bool WayfireIPC::receive(Glib::IOCondition cond) +{ + try { + ssize_t received = 0; + uint32_t length; + + // TODO: Input buffer can(?) contain incomplete message + while (connection->get_socket()->get_available_bytes() > 0) + { + received = input->read(&length, sizeof(length)); + if (received == -1) + { + LOGE("IPC error: Receive message length failed"); + return false; + } + + if (received == 0) + { + LOGE("IPC error: Disconnected"); + return false; + } + + if (received != sizeof(length)) + { + LOGE("IPC error: failed to read message. Expected (bytes): ", + sizeof(length), + ", was read (bytes)", + received); + return false; + } + + std::string buf(length, 0); + received = input->read(&buf[0], length); + if (received == -1) + { + LOGE("IPC error: receive message body failed"); + return false; + } + + if (received == 0) + { + LOGE("IPC error: Disconnected"); + return false; + } + + if (received != length) + { + LOGE("IPC error: failed to read message. Expected (bytes): ", + length, + ", was read (bytes)", + received); + return false; + } + + wf::json_t message; + auto err = wf::json_t::parse_string(buf, message); + if (err.has_value()) + { + LOGE("IPC error: JSON parse: ", err.value(), " message: ", buf, " length: ", buf.length()); + return false; + } + + if (message.has_member("event")) + { + for (auto subscriber : subscribers) + { + subscriber->on_event(message); + } + + if (subscriptions.find(message["event"]) != subscriptions.end()) + { + for (auto sub : subscriptions[message["event"]]) + { + sub->on_event(message); + } + } + } else + { + auto handler = response_handlers.front(); + response_handlers.pop(); + auto client = clients.find(handler); + if (client != clients.end()) + { + client->second->handle_response(message); + } + } + } + } catch (const Gio::Error& e) + { + LOGE("IPC error: ", e.what()); + return false; + } + + return true; +} + +void WayfireIPC::subscribe_all(IIPCSubscriber *subscriber) +{ + subscribers.insert(subscriber); + + wf::json_t new_subs; + new_subs["method"] = "window-rules/events/watch"; + send(new_subs.serialize()); +} + +void WayfireIPC::subscribe(IIPCSubscriber *subscriber, const std::vector& events) +{ + wf::json_t new_subs; + new_subs["method"] = "window-rules/events/watch"; + new_subs["events"] = wf::json_t::array(); + + for (auto event : events) + { + if (subscriptions.find(event) == subscriptions.end()) + { + new_subs["events"].append(event); + subscriptions[event] = std::set(); + } + + subscriptions[event].insert(subscriber); + } + + if (new_subs["events"].size() > 0) + { + send(new_subs.serialize()); + } +} + +void WayfireIPC::unsubscribe(IIPCSubscriber *subscriber) +{ + subscribers.erase(subscriber); + + for (auto& [_, subs] : subscriptions) + { + subs.erase(subscriber); + } +} + +std::shared_ptr WayfireIPC::create_client() +{ + + std::cerr << "FAILED TO CREATE IPC CLIENT" << std::endl; + if (!connected) + { + return std::shared_ptr(new IPCClient(0, shared_from_this())); + } + + auto client = new IPCClient(next_client_id, shared_from_this()); + clients[next_client_id++] = client; + + // Zero is reserved for NO CLIENT id, so just in case :) + if (next_client_id == 0) + { + next_client_id++; + } + + return std::shared_ptr(client); +} + +void WayfireIPC::client_destroyed(int id) +{ + clients.erase(id); +} + +std::shared_ptr WayfireIPC::get_instance() +{ + static std::weak_ptr ipc; +std::cerr << "CALLED GET INSTANCE \n"; + auto instance = ipc.lock(); + if (!instance) + { + instance = std::shared_ptr(new WayfireIPC()); + ipc = instance; + } + + return instance; +} + +// IPCClient +IPCClient::~IPCClient() +{ + ipc->client_destroyed(id); +} + +void IPCClient::send(const std::string& message) +{ + ipc->send(message); +} + +void IPCClient::send(const std::string& message, response_handler cb) +{ + response_handlers.push(cb); + ipc->send(message, id); +} + +void IPCClient::handle_response(wf::json_t response) +{ + auto handler = response_handlers.front(); + response_handlers.pop(); + handler(response); +} + +void IPCClient::subscribe(IIPCSubscriber *subscriber, const std::vector& events) +{ + ipc->subscribe(subscriber, events); +} + +void IPCClient::subscribe_all(IIPCSubscriber *subscriber) +{ + ipc->subscribe_all(subscriber); +} + +void IPCClient::unsubscribe(IIPCSubscriber *subscriber) +{ + ipc->unsubscribe(subscriber); +} diff --git a/src/util/wf-ipc.hpp b/src/util/wf-ipc.hpp new file mode 100644 index 00000000..d9496a14 --- /dev/null +++ b/src/util/wf-ipc.hpp @@ -0,0 +1,82 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class IIPCSubscriber +{ + public: + virtual void on_event(wf::json_t) = 0; +}; + +using response_handler = std::function; + +class WayfireIPC; +class IPCClient +{ + private: + int id; + std::shared_ptr ipc; + std::queue response_handlers; + + public: + IPCClient(int id, std::shared_ptr ipc) : id(id), ipc(ipc) + {} + ~IPCClient(); + void handle_response(wf::json_t response); + void send(const std::string& message); + void send(const std::string& message, response_handler cb); + void subscribe(IIPCSubscriber *subscriber, const std::vector& events); + void subscribe_all(IIPCSubscriber *subscriber); + void unsubscribe(IIPCSubscriber *subscriber); +}; + +class WayfireIPC : public std::enable_shared_from_this +{ + private: + std::queue response_handlers; + std::set subscribers; + std::unordered_map> subscriptions; + int next_client_id{1}; + std::unordered_map clients; + sigc::connection sig_connection; + Glib::RefPtr connection; + Glib::RefPtr input; + Glib::RefPtr output; + Glib::RefPtr cancel; + std::queue write_queue; + bool writing = false; + + bool connect(); + void disconnect(); + void send_message(const std::string& message); + bool send_queue(Glib::IOCondition cond); + bool receive(Glib::IOCondition cond); + void write_stream(const std::string& message); + void write_next(); + + public: + void send(const std::string& message); + void send(const std::string& message, int response_handler); + void subscribe(IIPCSubscriber *subscriber, const std::vector& events); + void subscribe_all(IIPCSubscriber *subscriber); + void unsubscribe(IIPCSubscriber *subscriber); + std::shared_ptr create_client(); + void client_destroyed(int id); + + static std::shared_ptr get_instance(); + bool connected = false; + WayfireIPC(); + ~WayfireIPC(); +}; diff --git a/subprojects/wf-json b/subprojects/wf-json new file mode 160000 index 00000000..70039e13 --- /dev/null +++ b/subprojects/wf-json @@ -0,0 +1 @@ +Subproject commit 70039e13cdeaebd8ec498ed30bf5ab91c2e313ec