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