diff --git a/Dockerfiles/debian b/Dockerfiles/debian index 7c9e0b0..32d7a6f 100644 --- a/Dockerfiles/debian +++ b/Dockerfiles/debian @@ -3,5 +3,5 @@ FROM debian:sid RUN apt-get update && \ - apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection libgirepository1.0-dev libxkbcommon-dev && \ + apt-get install -y build-essential meson ninja-build git pkg-config libinput10 libpugixml-dev libinput-dev wayland-protocols libwayland-client0 libwayland-cursor0 libwayland-dev libegl1-mesa-dev libgles2-mesa-dev libgbm-dev libxkbcommon-dev libudev-dev libpixman-1-dev libgtkmm-3.0-dev libjsoncpp-dev scdoc libdbusmenu-gtk3-dev libnl-3-dev libnl-genl-3-dev libpulse-dev libmpdclient-dev gobject-introspection libgirepository1.0-dev libxkbcommon-dev libxkbregistry0 && \ apt-get clean diff --git a/include/modules/sni/item.hpp b/include/modules/sni/item.hpp index 3cbd0b7..1f1503e 100644 --- a/include/modules/sni/item.hpp +++ b/include/modules/sni/item.hpp @@ -11,8 +11,16 @@ #include #include +#include +#include + namespace waybar::modules::SNI { +struct ToolTip { + Glib::ustring icon_name; + Glib::ustring text; +}; + class Item : public sigc::trackable { public: Item(const std::string&, const std::string&, const Json::Value&); @@ -27,10 +35,8 @@ class Item : public sigc::trackable { Gtk::EventBox event_box; std::string category; std::string id; - std::string status; std::string title; - int32_t window_id; std::string icon_name; Glib::RefPtr icon_pixmap; Glib::RefPtr icon_theme; @@ -39,6 +45,7 @@ class Item : public sigc::trackable { std::string attention_movie_name; std::string icon_theme_path; std::string menu; + ToolTip tooltip; DbusmenuGtkMenu* dbus_menu = nullptr; Gtk::Menu* gtk_menu = nullptr; /** @@ -51,6 +58,7 @@ class Item : public sigc::trackable { private: void proxyReady(Glib::RefPtr& result); void setProperty(const Glib::ustring& name, Glib::VariantBase& value); + void setStatus(const Glib::ustring& value); void getUpdatedProperties(); void processUpdatedProperties(Glib::RefPtr& result); void onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, @@ -62,10 +70,18 @@ class Item : public sigc::trackable { static void onMenuDestroyed(Item* self, GObject* old_menu_pointer); void makeMenu(); bool handleClick(GdkEventButton* const& /*ev*/); + bool handleScroll(GdkEventScroll* const&); + + // smooth scrolling threshold + gdouble scroll_threshold_ = 0; + gdouble distance_scrolled_x_ = 0; + gdouble distance_scrolled_y_ = 0; + // visibility of items with Status == Passive + bool show_passive_ = false; Glib::RefPtr proxy_; Glib::RefPtr cancellable_; - bool update_pending_; + std::set update_pending_; }; } // namespace waybar::modules::SNI diff --git a/man/waybar-tray.5.scd b/man/waybar-tray.5.scd index cd0e93f..c664594 100644 --- a/man/waybar-tray.5.scd +++ b/man/waybar-tray.5.scd @@ -16,6 +16,15 @@ Addressed by *tray* typeof: integer ++ Defines the size of the tray icons. +*show-passive-items*: ++ + typeof: bool ++ + default: false ++ + Defines visibility of the tray icons with *Passive* status. + +*smooth-scrolling-threshold*: ++ + typeof: double ++ + Threshold to be used when scrolling. + *spacing*: ++ typeof: integer ++ Defines the spacing between the tray icons. @@ -37,3 +46,6 @@ Addressed by *tray* # STYLE - *#tray* +- *#tray > .passive* +- *#tray > .active* +- *#tray > .needs-attention* diff --git a/resources/style.css b/resources/style.css index 63ea871..32dce42 100644 --- a/resources/style.css +++ b/resources/style.css @@ -195,6 +195,15 @@ label:focus { background-color: #2980b9; } +#tray > .passive { + -gtk-icon-effect: dim; +} + +#tray > .needs-attention { + -gtk-icon-effect: highlight; + background-color: #eb4d4b; +} + #idle_inhibitor { background-color: #2d3436; } diff --git a/src/modules/sni/item.cpp b/src/modules/sni/item.cpp index abf9556..267cf63 100644 --- a/src/modules/sni/item.cpp +++ b/src/modules/sni/item.cpp @@ -1,7 +1,12 @@ #include "modules/sni/item.hpp" + +#include #include +#include #include + #include +#include template <> struct fmt::formatter : formatter { @@ -39,14 +44,22 @@ Item::Item(const std::string& bn, const std::string& op, const Json::Value& conf object_path(op), icon_size(16), effective_icon_size(0), - icon_theme(Gtk::IconTheme::create()), - update_pending_(false) { + icon_theme(Gtk::IconTheme::create()) { if (config["icon-size"].isUInt()) { icon_size = config["icon-size"].asUInt(); } + if (config["smooth-scrolling-threshold"].isNumeric()) { + scroll_threshold_ = config["smooth-scrolling-threshold"].asDouble(); + } + if (config["show-passive-items"].isBool()) { + show_passive_ = config["show-passive-items"].asBool(); + } event_box.add(image); - event_box.add_events(Gdk::BUTTON_PRESS_MASK); + event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK); event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick)); + event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll)); + // initial visibility + event_box.set_visible(show_passive_); cancellable_ = Gio::Cancellable::create(); @@ -73,12 +86,11 @@ void Item::proxyReady(Glib::RefPtr& result) { this->proxy_->signal_signal().connect(sigc::mem_fun(*this, &Item::onSignal)); - if (this->id.empty() || this->category.empty() || this->status.empty()) { + if (this->id.empty() || this->category.empty()) { spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path); return; } this->updateImage(); - // this->event_box.set_tooltip_text(this->title); } catch (const Glib::Error& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); @@ -88,10 +100,24 @@ void Item::proxyReady(Glib::RefPtr& result) { } template -T get_variant(Glib::VariantBase& value) { +T get_variant(const Glib::VariantBase& value) { return Glib::VariantBase::cast_dynamic>(value).get(); } +template <> +ToolTip get_variant(const Glib::VariantBase& value) { + ToolTip result; + // Unwrap (sa(iiay)ss) + auto container = value.cast_dynamic(value); + result.icon_name = get_variant(container.get_child(0)); + result.text = get_variant(container.get_child(2)); + auto description = get_variant(container.get_child(3)); + if (!description.empty()) { + result.text = fmt::format("{}\n{}", result.text, description); + } + return result; +} + void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { try { spdlog::trace("Set tray item property: {}.{} = {}", id.empty() ? bus_name : id, name, value); @@ -102,10 +128,11 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { id = get_variant(value); } else if (name == "Title") { title = get_variant(value); + if (tooltip.text.empty()) { + event_box.set_tooltip_markup(title); + } } else if (name == "Status") { - status = get_variant(value); - } else if (name == "WindowId") { - window_id = get_variant(value); + setStatus(get_variant(value)); } else if (name == "IconName") { icon_name = get_variant(value); } else if (name == "IconPixmap") { @@ -121,7 +148,10 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } else if (name == "AttentionMovieName") { attention_movie_name = get_variant(value); } else if (name == "ToolTip") { - // TODO: tooltip + tooltip = get_variant(value); + if (!tooltip.text.empty()) { + event_box.set_tooltip_markup(tooltip.text); + } } else if (name == "IconThemePath") { icon_theme_path = get_variant(value); if (!icon_theme_path.empty()) { @@ -148,9 +178,22 @@ void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) { } } -void Item::getUpdatedProperties() { - update_pending_ = false; +void Item::setStatus(const Glib::ustring& value) { + Glib::ustring lower = value.lowercase(); + event_box.set_visible(show_passive_ || lower.compare("passive") != 0); + auto style = event_box.get_style_context(); + for (const auto& class_name : style->list_classes()) { + style->remove_class(class_name); + } + if (lower.compare("needsattention") == 0) { + // convert status to dash-case for CSS + lower = "needs-attention"; + } + style->add_class(lower); +} + +void Item::getUpdatedProperties() { auto params = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(SNI_INTERFACE_NAME)}); proxy_->call("org.freedesktop.DBus.Properties.GetAll", @@ -167,33 +210,48 @@ void Item::processUpdatedProperties(Glib::RefPtr& _result) { auto properties = properties_variant.get(); for (const auto& [name, value] : properties) { - Glib::VariantBase old_value; - proxy_->get_cached_property(old_value, name); - if (!old_value || !value.equal(old_value)) { - proxy_->set_cached_property(name, value); + if (update_pending_.count(name.raw())) { setProperty(name, const_cast(value)); } } this->updateImage(); - // this->event_box.set_tooltip_text(this->title); } catch (const Glib::Error& err) { spdlog::warn("Failed to update properties: {}", err.what()); } catch (const std::exception& err) { spdlog::warn("Failed to update properties: {}", err.what()); } + update_pending_.clear(); } +/** + * Mapping from a signal name to a set of possibly changed properties. + * Commented signals are not handled by the tray module at the moment. + */ +static const std::map> signal2props = { + {"NewTitle", {"Title"}}, + {"NewIcon", {"IconName", "IconPixmap"}}, + // {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}}, + // {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}}, + {"NewIconThemePath", {"IconThemePath"}}, + {"NewToolTip", {"ToolTip"}}, + {"NewStatus", {"Status"}}, + // {"XAyatanaNewLabel", {"XAyatanaLabel"}}, +}; + void Item::onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name, const Glib::VariantContainerBase& arguments) { spdlog::trace("Tray item '{}' got signal {}", id, signal_name); - if (!update_pending_ && signal_name.compare(0, 3, "New") == 0) { - /* Debounce signals and schedule update of all properties. - * Based on behavior of Plasma dataengine for StatusNotifierItem. - */ - update_pending_ = true; - Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &Item::getUpdatedProperties), - UPDATE_DEBOUNCE_TIME); + auto changed = signal2props.find(signal_name.raw()); + if (changed != signal2props.end()) { + if (update_pending_.empty()) { + /* Debounce signals and schedule update of all properties. + * Based on behavior of Plasma dataengine for StatusNotifierItem. + */ + Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &Item::getUpdatedProperties), + UPDATE_DEBOUNCE_TIME); + } + update_pending_.insert(changed->second.begin(), changed->second.end()); } } @@ -252,33 +310,42 @@ Glib::RefPtr Item::extractPixBuf(GVariant* variant) { } void Item::updateImage() { + auto scale_factor = image.get_scale_factor(); + auto scaled_icon_size = icon_size * scale_factor; + image.set_from_icon_name("image-missing", Gtk::ICON_SIZE_MENU); - image.set_pixel_size(icon_size); + image.set_pixel_size(scaled_icon_size); if (!icon_name.empty()) { try { // Try to find icons specified by path and filename std::ifstream temp(icon_name); if (temp.is_open()) { auto pixbuf = Gdk::Pixbuf::create_from_file(icon_name); + if (pixbuf->gobj() != nullptr) { // An icon specified by path and filename may be the wrong size for // the tray - // Keep the aspect ratio and scale to make the height equal to icon_size + // Keep the aspect ratio and scale to make the height equal to scaled_icon_size // If people have non square icons, assume they want it to grow in width not height - int width = icon_size * pixbuf->get_width() / pixbuf->get_height(); + int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); - pixbuf = pixbuf->scale_simple(width, icon_size, Gdk::InterpType::INTERP_BILINEAR); - image.set(pixbuf); + pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); + + auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, 0, image.get_window()); + image.set(surface); } } else { - image.set(getIconByName(icon_name, icon_size)); + auto icon_by_name = getIconByName(icon_name, scaled_icon_size); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(icon_by_name, 0, image.get_window()); + image.set(surface); } } catch (Glib::Error& e) { spdlog::error("Item '{}': {}", id, static_cast(e.what())); } } else if (icon_pixmap) { // An icon extracted may be the wrong size for the tray - icon_pixmap = icon_pixmap->scale_simple(icon_size, icon_size, Gdk::InterpType::INTERP_BILINEAR); + icon_pixmap = icon_pixmap->scale_simple(icon_size, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR); + auto surface = Gdk::Cairo::create_surface_from_pixbuf(icon_pixmap, 0, image.get_window()); image.set(icon_pixmap); } } @@ -360,4 +427,52 @@ bool Item::handleClick(GdkEventButton* const& ev) { return false; } +bool Item::handleScroll(GdkEventScroll* const& ev) { + int dx = 0, dy = 0; + switch (ev->direction) { + case GDK_SCROLL_UP: + dy = -1; + break; + case GDK_SCROLL_DOWN: + dy = 1; + break; + case GDK_SCROLL_LEFT: + dx = -1; + break; + case GDK_SCROLL_RIGHT: + dx = 1; + break; + case GDK_SCROLL_SMOOTH: + distance_scrolled_x_ += ev->delta_x; + distance_scrolled_y_ += ev->delta_y; + // check against the configured threshold and ensure that the absolute value >= 1 + if (distance_scrolled_x_ > scroll_threshold_) { + dx = (int)lround(std::max(distance_scrolled_x_, 1.0)); + distance_scrolled_x_ = 0; + } else if (distance_scrolled_x_ < -scroll_threshold_) { + dx = (int)lround(std::min(distance_scrolled_x_, -1.0)); + distance_scrolled_x_ = 0; + } + if (distance_scrolled_y_ > scroll_threshold_) { + dy = (int)lround(std::max(distance_scrolled_y_, 1.0)); + distance_scrolled_y_ = 0; + } else if (distance_scrolled_y_ < -scroll_threshold_) { + dy = (int)lround(std::min(distance_scrolled_y_, -1.0)); + distance_scrolled_y_ = 0; + } + break; + } + if (dx != 0) { + auto parameters = Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(dx), Glib::Variant::create("horizontal")}); + proxy_->call("Scroll", parameters); + } + if (dy != 0) { + auto parameters = Glib::VariantContainerBase::create_tuple( + {Glib::Variant::create(dy), Glib::Variant::create("vertical")}); + proxy_->call("Scroll", parameters); + } + return true; +} + } // namespace waybar::modules::SNI