#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, const Bar& bar) : 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(); } if (config["show-passive-items"].isBool()) { show_passive_ = config["show-passive-items"].asBool(); } auto &window = const_cast(bar).window; window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Item::onConfigure)); 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)); // initial visibility event_box.set_visible(show_passive_); 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::onConfigure(GdkEventConfigure* ev) { this->updateImage(); } 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()) { 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") { setStatus(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::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", 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 pixbuf = getIconPixbuf(); auto scaled_icon_size = getScaledIconSize(); if (!pixbuf) { pixbuf = getIconByName("image-missing", getScaledIconSize()); } // If the loaded icon is not square, assume that the icon height should match the // requested icon size, but the width is allowed to be different. As such, if the // height of the image does not match the requested icon size, resize the icon such that // the aspect ratio is maintained, but the height matches the requested icon size. if (pixbuf->get_height() != scaled_icon_size) { 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); } Glib::RefPtr Item::getIconPixbuf() { try { if (!icon_name.empty()) { std::ifstream temp(icon_name); if (temp.is_open()) { return Gdk::Pixbuf::create_from_file(icon_name); } return getIconByName(icon_name, getScaledIconSize()); } else if (icon_pixmap) { return icon_pixmap; } } catch (Glib::Error& e) { spdlog::error("Item '{}': {}", id, static_cast(e.what())); } return getIconByName("image-missing", getScaledIconSize()); } 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); } double Item::getScaledIconSize() { // apply the scale factor from the Gtk window to the requested icon size return icon_size * image.get_scale_factor(); } 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