#include "modules/sni/item.hpp" #include #include #include #include #include #include template <> struct fmt::formatter : formatter { template auto format(const Glib::ustring& value, FormatContext& ctx) { return formatter::format(value, ctx); } }; template <> struct fmt::formatter : formatter { bool is_printable(const Glib::VariantBase& value) { auto type = value.get_type_string(); /* Print only primitive (single character excluding 'v') and short complex types */ return (type.length() == 1 && islower(type[0]) && type[0] != 'v') || value.get_size() <= 32; } template auto format(const Glib::VariantBase& value, FormatContext& ctx) { if (is_printable(value)) { return formatter::format(value.print(), ctx); } else { return formatter::format(value.get_type_string(), ctx); } } }; namespace waybar::modules::SNI { static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name; static const unsigned UPDATE_DEBOUNCE_TIME = 10; Item::Item(const std::string& bn, const std::string& op, const Json::Value& config) : bus_name(bn), object_path(op), icon_size(16), effective_icon_size(0), 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(); } event_box.add(image); 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)); cancellable_ = Gio::Cancellable::create(); auto interface = Glib::wrap(sn_item_interface_info(), true); Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SESSION, bus_name, object_path, SNI_INTERFACE_NAME, sigc::mem_fun(*this, &Item::proxyReady), cancellable_, interface); } void Item::proxyReady(Glib::RefPtr& result) { try { this->proxy_ = Gio::DBus::Proxy::create_for_bus_finish(result); /* Properties are already cached during object creation */ auto cached_properties = this->proxy_->get_cached_property_names(); for (const auto& name : cached_properties) { Glib::VariantBase value; this->proxy_->get_cached_property(value, name); setProperty(name, value); } this->proxy_->signal_signal().connect(sigc::mem_fun(*this, &Item::onSignal)); if (this->id.empty() || this->category.empty() || this->status.empty()) { spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path); return; } this->updateImage(); } catch (const Glib::Error& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); } catch (const std::exception& err) { spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what()); } } template 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); if (name == "Category") { category = get_variant(value); } else if (name == "Id") { 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 == "IconName") { icon_name = get_variant(value); } else if (name == "IconPixmap") { icon_pixmap = this->extractPixBuf(value.gobj()); } else if (name == "OverlayIconName") { overlay_icon_name = get_variant(value); } else if (name == "OverlayIconPixmap") { // TODO: overlay_icon_pixmap } else if (name == "AttentionIconName") { attention_icon_name = get_variant(value); } else if (name == "AttentionIconPixmap") { // TODO: attention_icon_pixmap } else if (name == "AttentionMovieName") { attention_movie_name = get_variant(value); } else if (name == "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()) { icon_theme->set_search_path({icon_theme_path}); } } else if (name == "Menu") { menu = get_variant(value); makeMenu(); } else if (name == "ItemIsMenu") { item_is_menu = get_variant(value); } } catch (const Glib::Error& err) { spdlog::warn("Failed to set tray item property: {}.{}, value = {}, err = {}", id.empty() ? bus_name : id, name, value, err.what()); } catch (const std::exception& err) { spdlog::warn("Failed to set tray item property: {}.{}, value = {}, err = {}", id.empty() ? bus_name : id, name, value, err.what()); } } void Item::getUpdatedProperties() { auto params = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(SNI_INTERFACE_NAME)}); proxy_->call("org.freedesktop.DBus.Properties.GetAll", sigc::mem_fun(*this, &Item::processUpdatedProperties), params); }; void Item::processUpdatedProperties(Glib::RefPtr& _result) { try { auto result = proxy_->call_finish(_result); // extract "a{sv}" from VariantContainerBase Glib::Variant> properties_variant; result.get_child(properties_variant); auto properties = properties_variant.get(); for (const auto& [name, value] : properties) { if (update_pending_.count(name.raw())) { setProperty(name, const_cast(value)); } } this->updateImage(); } 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); 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()); } } static void pixbuf_data_deleter(const guint8* data) { g_free((void*)data); } Glib::RefPtr Item::extractPixBuf(GVariant* variant) { GVariantIter* it; g_variant_get(variant, "a(iiay)", &it); if (it == nullptr) { return Glib::RefPtr{}; } GVariant* val; gint lwidth = 0; gint lheight = 0; gint width; gint height; guchar* array = nullptr; while (g_variant_iter_loop(it, "(ii@ay)", &width, &height, &val)) { if (width > 0 && height > 0 && val != nullptr && width * height > lwidth * lheight) { auto size = g_variant_get_size(val); /* Sanity check */ if (size == 4U * width * height) { /* Find the largest image */ gconstpointer data = g_variant_get_data(val); if (data != nullptr) { if (array != nullptr) { g_free(array); } array = static_cast(g_memdup(data, size)); lwidth = width; lheight = height; } } } } g_variant_iter_free(it); if (array != nullptr) { /* argb to rgba */ for (uint32_t i = 0; i < 4U * lwidth * lheight; i += 4) { guchar alpha = array[i]; array[i] = array[i + 1]; array[i + 1] = array[i + 2]; array[i + 2] = array[i + 3]; array[i + 3] = alpha; } return Gdk::Pixbuf::create_from_data(array, Gdk::Colorspace::COLORSPACE_RGB, true, 8, lwidth, lheight, 4 * lwidth, &pixbuf_data_deleter); } return Glib::RefPtr{}; } 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(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 scaled_icon_size // If people have non square icons, assume they want it to grow in width not height int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height(); 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 { 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, 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); } } Glib::RefPtr Item::getIconByName(const std::string& name, int request_size) { int tmp_size = 0; icon_theme->rescan_if_needed(); auto sizes = icon_theme->get_icon_sizes(name.c_str()); for (auto const& size : sizes) { // -1 == scalable if (size == request_size || size == -1) { tmp_size = request_size; break; } else if (size < request_size) { tmp_size = size; } else if (size > tmp_size && tmp_size > 0) { tmp_size = request_size; break; } } if (tmp_size == 0) { tmp_size = request_size; } if (!icon_theme_path.empty() && icon_theme->lookup_icon( name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) { return icon_theme->load_icon( name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } Glib::RefPtr default_theme = Gtk::IconTheme::get_default(); default_theme->rescan_if_needed(); return default_theme->load_icon( name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE); } void Item::onMenuDestroyed(Item* self, GObject* old_menu_pointer) { if (old_menu_pointer == reinterpret_cast(self->dbus_menu)) { self->gtk_menu = nullptr; self->dbus_menu = nullptr; } } void Item::makeMenu() { if (gtk_menu == nullptr && !menu.empty()) { dbus_menu = dbusmenu_gtkmenu_new(bus_name.data(), menu.data()); if (dbus_menu != nullptr) { g_object_ref_sink(G_OBJECT(dbus_menu)); g_object_weak_ref(G_OBJECT(dbus_menu), (GWeakNotify)onMenuDestroyed, this); gtk_menu = Glib::wrap(GTK_MENU(dbus_menu)); gtk_menu->attach_to_widget(event_box); } } } bool Item::handleClick(GdkEventButton* const& ev) { auto parameters = Glib::VariantContainerBase::create_tuple( {Glib::Variant::create(ev->x), Glib::Variant::create(ev->y)}); if ((ev->button == 1 && item_is_menu) || ev->button == 3) { makeMenu(); if (gtk_menu != nullptr) { #if GTK_CHECK_VERSION(3, 22, 0) gtk_menu->popup_at_pointer(reinterpret_cast(ev)); #else gtk_menu->popup(ev->button, ev->time); #endif return true; } else { proxy_->call("ContextMenu", parameters); return true; } } else if (ev->button == 1) { proxy_->call("Activate", parameters); return true; } else if (ev->button == 2) { proxy_->call("SecondaryActivate", parameters); return true; } 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