1 #include "modules/sni/item.hpp"
2 
3 #include <gdkmm/general.h>
4 #include <glibmm/main.h>
5 #include <gtkmm/tooltip.h>
6 #include <spdlog/spdlog.h>
7 
8 #include <fstream>
9 #include <map>
10 
11 #include "util/format.hpp"
12 
13 template <>
14 struct fmt::formatter<Glib::VariantBase> : formatter<std::string> {
is_printablefmt::formatter15   bool is_printable(const Glib::VariantBase& value) {
16     auto type = value.get_type_string();
17     /* Print only primitive (single character excluding 'v') and short complex types */
18     return (type.length() == 1 && islower(type[0]) && type[0] != 'v') || value.get_size() <= 32;
19   }
20 
21   template <typename FormatContext>
formatfmt::formatter22   auto format(const Glib::VariantBase& value, FormatContext& ctx) {
23     if (is_printable(value)) {
24       return formatter<std::string>::format(value.print(), ctx);
25     } else {
26       return formatter<std::string>::format(value.get_type_string(), ctx);
27     }
28   }
29 };
30 
31 namespace waybar::modules::SNI {
32 
33 static const Glib::ustring SNI_INTERFACE_NAME = sn_item_interface_info()->name;
34 static const unsigned      UPDATE_DEBOUNCE_TIME = 10;
35 
Item(const std::string & bn,const std::string & op,const Json::Value & config,const Bar & bar)36 Item::Item(const std::string& bn, const std::string& op, const Json::Value& config, const Bar& bar)
37     : bus_name(bn),
38       object_path(op),
39       icon_size(16),
40       effective_icon_size(0),
41       icon_theme(Gtk::IconTheme::create()) {
42   if (config["icon-size"].isUInt()) {
43     icon_size = config["icon-size"].asUInt();
44   }
45   if (config["smooth-scrolling-threshold"].isNumeric()) {
46     scroll_threshold_ = config["smooth-scrolling-threshold"].asDouble();
47   }
48   if (config["show-passive-items"].isBool()) {
49     show_passive_ = config["show-passive-items"].asBool();
50   }
51 
52   auto &window = const_cast<Bar &>(bar).window;
53   window.signal_configure_event().connect_notify(sigc::mem_fun(*this, &Item::onConfigure));
54   event_box.add(image);
55   event_box.add_events(Gdk::BUTTON_PRESS_MASK | Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
56   event_box.signal_button_press_event().connect(sigc::mem_fun(*this, &Item::handleClick));
57   event_box.signal_scroll_event().connect(sigc::mem_fun(*this, &Item::handleScroll));
58   // initial visibility
59   event_box.show_all();
60   event_box.set_visible(show_passive_);
61 
62   cancellable_ = Gio::Cancellable::create();
63 
64   auto interface = Glib::wrap(sn_item_interface_info(), true);
65   Gio::DBus::Proxy::create_for_bus(Gio::DBus::BusType::BUS_TYPE_SESSION,
66                                    bus_name,
67                                    object_path,
68                                    SNI_INTERFACE_NAME,
69                                    sigc::mem_fun(*this, &Item::proxyReady),
70                                    cancellable_,
71                                    interface);
72 }
73 
onConfigure(GdkEventConfigure * ev)74 void Item::onConfigure(GdkEventConfigure* ev) {
75   this->updateImage();
76 }
77 
proxyReady(Glib::RefPtr<Gio::AsyncResult> & result)78 void Item::proxyReady(Glib::RefPtr<Gio::AsyncResult>& result) {
79   try {
80     this->proxy_ = Gio::DBus::Proxy::create_for_bus_finish(result);
81     /* Properties are already cached during object creation */
82     auto cached_properties = this->proxy_->get_cached_property_names();
83     for (const auto& name : cached_properties) {
84       Glib::VariantBase value;
85       this->proxy_->get_cached_property(value, name);
86       setProperty(name, value);
87     }
88 
89     this->proxy_->signal_signal().connect(sigc::mem_fun(*this, &Item::onSignal));
90 
91     if (this->id.empty() || this->category.empty()) {
92       spdlog::error("Invalid Status Notifier Item: {}, {}", bus_name, object_path);
93       return;
94     }
95     this->updateImage();
96 
97   } catch (const Glib::Error& err) {
98     spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what());
99   } catch (const std::exception& err) {
100     spdlog::error("Failed to create DBus Proxy for {} {}: {}", bus_name, object_path, err.what());
101   }
102 }
103 
104 template <typename T>
get_variant(const Glib::VariantBase & value)105 T get_variant(const Glib::VariantBase& value) {
106   return Glib::VariantBase::cast_dynamic<Glib::Variant<T>>(value).get();
107 }
108 
109 template <>
get_variant(const Glib::VariantBase & value)110 ToolTip get_variant<ToolTip>(const Glib::VariantBase& value) {
111   ToolTip result;
112   // Unwrap (sa(iiay)ss)
113   auto container = value.cast_dynamic<Glib::VariantContainerBase>(value);
114   result.icon_name = get_variant<Glib::ustring>(container.get_child(0));
115   result.text = get_variant<Glib::ustring>(container.get_child(2));
116   auto description = get_variant<Glib::ustring>(container.get_child(3));
117   if (!description.empty()) {
118     result.text = fmt::format("<b>{}</b>\n{}", result.text, description);
119   }
120   return result;
121 }
122 
setProperty(const Glib::ustring & name,Glib::VariantBase & value)123 void Item::setProperty(const Glib::ustring& name, Glib::VariantBase& value) {
124   try {
125     spdlog::trace("Set tray item property: {}.{} = {}", id.empty() ? bus_name : id, name, value);
126 
127     if (name == "Category") {
128       category = get_variant<std::string>(value);
129     } else if (name == "Id") {
130       id = get_variant<std::string>(value);
131     } else if (name == "Title") {
132       title = get_variant<std::string>(value);
133       if (tooltip.text.empty()) {
134         event_box.set_tooltip_markup(title);
135       }
136     } else if (name == "Status") {
137       setStatus(get_variant<Glib::ustring>(value));
138     } else if (name == "IconName") {
139       icon_name = get_variant<std::string>(value);
140     } else if (name == "IconPixmap") {
141       icon_pixmap = this->extractPixBuf(value.gobj());
142     } else if (name == "OverlayIconName") {
143       overlay_icon_name = get_variant<std::string>(value);
144     } else if (name == "OverlayIconPixmap") {
145       // TODO: overlay_icon_pixmap
146     } else if (name == "AttentionIconName") {
147       attention_icon_name = get_variant<std::string>(value);
148     } else if (name == "AttentionIconPixmap") {
149       // TODO: attention_icon_pixmap
150     } else if (name == "AttentionMovieName") {
151       attention_movie_name = get_variant<std::string>(value);
152     } else if (name == "ToolTip") {
153       tooltip = get_variant<ToolTip>(value);
154       if (!tooltip.text.empty()) {
155         event_box.set_tooltip_markup(tooltip.text);
156       }
157     } else if (name == "IconThemePath") {
158       icon_theme_path = get_variant<std::string>(value);
159       if (!icon_theme_path.empty()) {
160         icon_theme->set_search_path({icon_theme_path});
161       }
162     } else if (name == "Menu") {
163       menu = get_variant<std::string>(value);
164       makeMenu();
165     } else if (name == "ItemIsMenu") {
166       item_is_menu = get_variant<bool>(value);
167     }
168   } catch (const Glib::Error& err) {
169     spdlog::warn("Failed to set tray item property: {}.{}, value = {}, err = {}",
170                  id.empty() ? bus_name : id,
171                  name,
172                  value,
173                  err.what());
174   } catch (const std::exception& err) {
175     spdlog::warn("Failed to set tray item property: {}.{}, value = {}, err = {}",
176                  id.empty() ? bus_name : id,
177                  name,
178                  value,
179                  err.what());
180   }
181 }
182 
setStatus(const Glib::ustring & value)183 void Item::setStatus(const Glib::ustring& value) {
184   Glib::ustring lower = value.lowercase();
185   event_box.set_visible(show_passive_ || lower.compare("passive") != 0);
186 
187   auto style = event_box.get_style_context();
188   for (const auto& class_name : style->list_classes()) {
189     style->remove_class(class_name);
190   }
191   if (lower.compare("needsattention") == 0) {
192     // convert status to dash-case for CSS
193     lower = "needs-attention";
194   }
195   style->add_class(lower);
196 }
197 
getUpdatedProperties()198 void Item::getUpdatedProperties() {
199   auto params = Glib::VariantContainerBase::create_tuple(
200       {Glib::Variant<Glib::ustring>::create(SNI_INTERFACE_NAME)});
201   proxy_->call("org.freedesktop.DBus.Properties.GetAll",
202                sigc::mem_fun(*this, &Item::processUpdatedProperties),
203                params);
204 };
205 
processUpdatedProperties(Glib::RefPtr<Gio::AsyncResult> & _result)206 void Item::processUpdatedProperties(Glib::RefPtr<Gio::AsyncResult>& _result) {
207   try {
208     auto result = proxy_->call_finish(_result);
209     // extract "a{sv}" from VariantContainerBase
210     Glib::Variant<std::map<Glib::ustring, Glib::VariantBase>> properties_variant;
211     result.get_child(properties_variant);
212     auto properties = properties_variant.get();
213 
214     for (const auto& [name, value] : properties) {
215       if (update_pending_.count(name.raw())) {
216         setProperty(name, const_cast<Glib::VariantBase&>(value));
217       }
218     }
219 
220     this->updateImage();
221   } catch (const Glib::Error& err) {
222     spdlog::warn("Failed to update properties: {}", err.what());
223   } catch (const std::exception& err) {
224     spdlog::warn("Failed to update properties: {}", err.what());
225   }
226   update_pending_.clear();
227 }
228 
229 /**
230  * Mapping from a signal name to a set of possibly changed properties.
231  * Commented signals are not handled by the tray module at the moment.
232  */
233 static const std::map<std::string_view, std::set<std::string_view>> signal2props = {
234     {"NewTitle", {"Title"}},
235     {"NewIcon", {"IconName", "IconPixmap"}},
236     // {"NewAttentionIcon", {"AttentionIconName", "AttentionIconPixmap", "AttentionMovieName"}},
237     // {"NewOverlayIcon", {"OverlayIconName", "OverlayIconPixmap"}},
238     {"NewIconThemePath", {"IconThemePath"}},
239     {"NewToolTip", {"ToolTip"}},
240     {"NewStatus", {"Status"}},
241     // {"XAyatanaNewLabel", {"XAyatanaLabel"}},
242 };
243 
onSignal(const Glib::ustring & sender_name,const Glib::ustring & signal_name,const Glib::VariantContainerBase & arguments)244 void Item::onSignal(const Glib::ustring& sender_name, const Glib::ustring& signal_name,
245                     const Glib::VariantContainerBase& arguments) {
246   spdlog::trace("Tray item '{}' got signal {}", id, signal_name);
247   auto changed = signal2props.find(signal_name.raw());
248   if (changed != signal2props.end()) {
249     if (update_pending_.empty()) {
250       /* Debounce signals and schedule update of all properties.
251        * Based on behavior of Plasma dataengine for StatusNotifierItem.
252        */
253       Glib::signal_timeout().connect_once(sigc::mem_fun(*this, &Item::getUpdatedProperties),
254                                           UPDATE_DEBOUNCE_TIME);
255     }
256     update_pending_.insert(changed->second.begin(), changed->second.end());
257   }
258 }
259 
pixbuf_data_deleter(const guint8 * data)260 static void pixbuf_data_deleter(const guint8* data) { g_free((void*)data); }
261 
extractPixBuf(GVariant * variant)262 Glib::RefPtr<Gdk::Pixbuf> Item::extractPixBuf(GVariant* variant) {
263   GVariantIter* it;
264   g_variant_get(variant, "a(iiay)", &it);
265   if (it == nullptr) {
266     return Glib::RefPtr<Gdk::Pixbuf>{};
267   }
268   GVariant* val;
269   gint      lwidth = 0;
270   gint      lheight = 0;
271   gint      width;
272   gint      height;
273   guchar*   array = nullptr;
274   while (g_variant_iter_loop(it, "(ii@ay)", &width, &height, &val)) {
275     if (width > 0 && height > 0 && val != nullptr && width * height > lwidth * lheight) {
276       auto size = g_variant_get_size(val);
277       /* Sanity check */
278       if (size == 4U * width * height) {
279         /* Find the largest image */
280         gconstpointer data = g_variant_get_data(val);
281         if (data != nullptr) {
282           if (array != nullptr) {
283             g_free(array);
284           }
285 #if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 68
286           array = static_cast<guchar*>(g_memdup2(data, size));
287 #else
288           array = static_cast<guchar*>(g_memdup(data, size));
289 #endif
290           lwidth = width;
291           lheight = height;
292         }
293       }
294     }
295   }
296   g_variant_iter_free(it);
297   if (array != nullptr) {
298     /* argb to rgba */
299     for (uint32_t i = 0; i < 4U * lwidth * lheight; i += 4) {
300       guchar alpha = array[i];
301       array[i] = array[i + 1];
302       array[i + 1] = array[i + 2];
303       array[i + 2] = array[i + 3];
304       array[i + 3] = alpha;
305     }
306     return Gdk::Pixbuf::create_from_data(array,
307                                          Gdk::Colorspace::COLORSPACE_RGB,
308                                          true,
309                                          8,
310                                          lwidth,
311                                          lheight,
312                                          4 * lwidth,
313                                          &pixbuf_data_deleter);
314   }
315   return Glib::RefPtr<Gdk::Pixbuf>{};
316 }
317 
updateImage()318 void Item::updateImage() {
319   auto pixbuf = getIconPixbuf();
320   auto scaled_icon_size = getScaledIconSize();
321 
322   if (!pixbuf) {
323     pixbuf = getIconByName("image-missing", getScaledIconSize());
324   }
325 
326   // If the loaded icon is not square, assume that the icon height should match the
327   // requested icon size, but the width is allowed to be different. As such, if the
328   // height of the image does not match the requested icon size, resize the icon such that
329   // the aspect ratio is maintained, but the height matches the requested icon size.
330   if (pixbuf->get_height() != scaled_icon_size) {
331     int width = scaled_icon_size * pixbuf->get_width() / pixbuf->get_height();
332     pixbuf = pixbuf->scale_simple(width, scaled_icon_size, Gdk::InterpType::INTERP_BILINEAR);
333   }
334 
335   auto surface = Gdk::Cairo::create_surface_from_pixbuf(pixbuf, 0, image.get_window());
336   image.set(surface);
337 }
338 
getIconPixbuf()339 Glib::RefPtr<Gdk::Pixbuf> Item::getIconPixbuf() {
340   try {
341     if (!icon_name.empty()) {
342       std::ifstream temp(icon_name);
343       if (temp.is_open()) {
344         return Gdk::Pixbuf::create_from_file(icon_name);
345       }
346       return getIconByName(icon_name, getScaledIconSize());
347     } else if (icon_pixmap) {
348       return icon_pixmap;
349     }
350   } catch (Glib::Error& e) {
351     spdlog::error("Item '{}': {}", id, static_cast<std::string>(e.what()));
352   }
353   return getIconByName("image-missing", getScaledIconSize());
354 }
355 
getIconByName(const std::string & name,int request_size)356 Glib::RefPtr<Gdk::Pixbuf> Item::getIconByName(const std::string& name, int request_size) {
357   int tmp_size = 0;
358   icon_theme->rescan_if_needed();
359   auto sizes = icon_theme->get_icon_sizes(name.c_str());
360 
361   for (auto const& size : sizes) {
362     // -1 == scalable
363     if (size == request_size || size == -1) {
364       tmp_size = request_size;
365       break;
366     } else if (size < request_size) {
367       tmp_size = size;
368     } else if (size > tmp_size && tmp_size > 0) {
369       tmp_size = request_size;
370       break;
371     }
372   }
373   if (tmp_size == 0) {
374     tmp_size = request_size;
375   }
376   if (!icon_theme_path.empty() &&
377       icon_theme->lookup_icon(
378           name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE)) {
379     return icon_theme->load_icon(
380         name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
381   }
382   Glib::RefPtr<Gtk::IconTheme> default_theme = Gtk::IconTheme::get_default();
383   default_theme->rescan_if_needed();
384   return default_theme->load_icon(
385       name.c_str(), tmp_size, Gtk::IconLookupFlags::ICON_LOOKUP_FORCE_SIZE);
386 }
387 
getScaledIconSize()388 double Item::getScaledIconSize() {
389   // apply the scale factor from the Gtk window to the requested icon size
390   return icon_size * image.get_scale_factor();
391 }
392 
onMenuDestroyed(Item * self,GObject * old_menu_pointer)393 void Item::onMenuDestroyed(Item* self, GObject* old_menu_pointer) {
394   if (old_menu_pointer == reinterpret_cast<GObject*>(self->dbus_menu)) {
395     self->gtk_menu = nullptr;
396     self->dbus_menu = nullptr;
397   }
398 }
399 
makeMenu()400 void Item::makeMenu() {
401   if (gtk_menu == nullptr && !menu.empty()) {
402     dbus_menu = dbusmenu_gtkmenu_new(bus_name.data(), menu.data());
403     if (dbus_menu != nullptr) {
404       g_object_ref_sink(G_OBJECT(dbus_menu));
405       g_object_weak_ref(G_OBJECT(dbus_menu), (GWeakNotify)onMenuDestroyed, this);
406       gtk_menu = Glib::wrap(GTK_MENU(dbus_menu));
407       gtk_menu->attach_to_widget(event_box);
408     }
409   }
410 }
411 
handleClick(GdkEventButton * const & ev)412 bool Item::handleClick(GdkEventButton* const& ev) {
413   auto parameters = Glib::VariantContainerBase::create_tuple(
414       {Glib::Variant<int>::create(ev->x), Glib::Variant<int>::create(ev->y)});
415   if ((ev->button == 1 && item_is_menu) || ev->button == 3) {
416     makeMenu();
417     if (gtk_menu != nullptr) {
418 #if GTK_CHECK_VERSION(3, 22, 0)
419       gtk_menu->popup_at_pointer(reinterpret_cast<GdkEvent*>(ev));
420 #else
421       gtk_menu->popup(ev->button, ev->time);
422 #endif
423       return true;
424     } else {
425       proxy_->call("ContextMenu", parameters);
426       return true;
427     }
428   } else if (ev->button == 1) {
429     proxy_->call("Activate", parameters);
430     return true;
431   } else if (ev->button == 2) {
432     proxy_->call("SecondaryActivate", parameters);
433     return true;
434   }
435   return false;
436 }
437 
handleScroll(GdkEventScroll * const & ev)438 bool Item::handleScroll(GdkEventScroll* const& ev) {
439   int dx = 0, dy = 0;
440   switch (ev->direction) {
441     case GDK_SCROLL_UP:
442       dy = -1;
443       break;
444     case GDK_SCROLL_DOWN:
445       dy = 1;
446       break;
447     case GDK_SCROLL_LEFT:
448       dx = -1;
449       break;
450     case GDK_SCROLL_RIGHT:
451       dx = 1;
452       break;
453     case GDK_SCROLL_SMOOTH:
454       distance_scrolled_x_ += ev->delta_x;
455       distance_scrolled_y_ += ev->delta_y;
456       // check against the configured threshold and ensure that the absolute value >= 1
457       if (distance_scrolled_x_ > scroll_threshold_) {
458         dx = (int)lround(std::max(distance_scrolled_x_, 1.0));
459         distance_scrolled_x_ = 0;
460       } else if (distance_scrolled_x_ < -scroll_threshold_) {
461         dx = (int)lround(std::min(distance_scrolled_x_, -1.0));
462         distance_scrolled_x_ = 0;
463       }
464       if (distance_scrolled_y_ > scroll_threshold_) {
465         dy = (int)lround(std::max(distance_scrolled_y_, 1.0));
466         distance_scrolled_y_ = 0;
467       } else if (distance_scrolled_y_ < -scroll_threshold_) {
468         dy = (int)lround(std::min(distance_scrolled_y_, -1.0));
469         distance_scrolled_y_ = 0;
470       }
471       break;
472   }
473   if (dx != 0) {
474     auto parameters = Glib::VariantContainerBase::create_tuple(
475         {Glib::Variant<int>::create(dx), Glib::Variant<Glib::ustring>::create("horizontal")});
476     proxy_->call("Scroll", parameters);
477   }
478   if (dy != 0) {
479     auto parameters = Glib::VariantContainerBase::create_tuple(
480         {Glib::Variant<int>::create(dy), Glib::Variant<Glib::ustring>::create("vertical")});
481     proxy_->call("Scroll", parameters);
482   }
483   return true;
484 }
485 
486 }  // namespace waybar::modules::SNI
487