From 843ad38b3c427adb0bf319e9613d9813c8d9246c Mon Sep 17 00:00:00 2001 From: Calvin Lee Date: Wed, 7 Jun 2017 16:45:28 -0700 Subject: Implement Tray Icons This commit implements the StatusNotifierItem protocol, and enables swaybar to show tray icons. It also uses `xembedsniproxy` in order to communicate with xembed applications. The tray is completely optional, and can be disabled on compile time with the `enable-tray` option. Or on runtime with the bar config option `tray_output none`. Overview of changes: In swaybar very little is changed outside the tray subfolder except that all events are now polled in `event_loop.c`, this creates no functional difference. Six bar configuration options were added, these are detailed in sway-bar(5) The tray subfolder is where all protocol implementation takes place and is organised as follows: tray/sni_watcher.c: This file contains the StatusNotifierWatcher. It keeps track of items and hosts and reports when they come or go. tray/tray.c This file contains the StatusNotifierHost. It keeps track of sway's version of the items and represents the tray itself. tray/sni.c This file contains the StatusNotifierItem struct and all communication with individual items. tray/icon.c This file implements the icon theme protocol. It allows for finding icons by name, rather than by pixmap. tray/dbus.c This file allows for asynchronous DBus communication. See #986 #343 --- swaybar/tray/sni.c | 463 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 swaybar/tray/sni.c (limited to 'swaybar/tray/sni.c') diff --git a/swaybar/tray/sni.c b/swaybar/tray/sni.c new file mode 100644 index 00000000..f0638dca --- /dev/null +++ b/swaybar/tray/sni.c @@ -0,0 +1,463 @@ +#define _XOPEN_SOURCE 500 +#include +#include +#include +#include +#include +#include +#include +#include +#include "swaybar/tray/dbus.h" +#include "swaybar/tray/sni.h" +#include "swaybar/tray/icon.h" +#include "swaybar/bar.h" +#include "client/cairo.h" +#include "log.h" + +// Not sure what this is but cairo needs it. +static const cairo_user_data_key_t cairo_user_data_key; + +struct sni_icon_ref *sni_icon_ref_create(struct StatusNotifierItem *item, + int height) { + struct sni_icon_ref *sni_ref = malloc(sizeof(struct sni_icon_ref)); + if (!sni_ref) { + return NULL; + } + sni_ref->icon = cairo_image_surface_scale(item->image, height, height); + sni_ref->ref = item; + + return sni_ref; +} + +void sni_icon_ref_free(struct sni_icon_ref *sni_ref) { + if (!sni_ref) { + return; + } + cairo_surface_destroy(sni_ref->icon); + free(sni_ref); +} + +/* Gets the pixmap of an icon */ +static void reply_icon(DBusPendingCall *pending, void *_data) { + struct StatusNotifierItem *item = _data; + + DBusMessage *reply = dbus_pending_call_steal_reply(pending); + + if (!reply) { + sway_log(L_ERROR, "Did not get reply"); + goto bail; + } + + int message_type = dbus_message_get_type(reply); + + if (message_type == DBUS_MESSAGE_TYPE_ERROR) { + char *msg; + + dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &msg, + DBUS_TYPE_INVALID); + + sway_log(L_ERROR, "Message is error: %s", msg); + goto bail; + } + + DBusMessageIter iter; + DBusMessageIter variant; /* v[a(iiay)] */ + DBusMessageIter array; /* a(iiay) */ + DBusMessageIter d_struct; /* (iiay) */ + DBusMessageIter icon; /* ay */ + + dbus_message_iter_init(reply, &iter); + + // Each if here checks the types above before recursing + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + sway_log(L_ERROR, "Relpy type incorrect"); + sway_log(L_ERROR, "Should be \"v\", is \"%s\"", + dbus_message_iter_get_signature(&iter)); + goto bail; + } + dbus_message_iter_recurse(&iter, &variant); + + if (strcmp("a(iiay)", dbus_message_iter_get_signature(&variant)) != 0) { + sway_log(L_ERROR, "Relpy type incorrect"); + sway_log(L_ERROR, "Should be \"a(iiay)\", is \"%s\"", + dbus_message_iter_get_signature(&variant)); + goto bail; + } + + if (dbus_message_iter_get_element_count(&variant) == 0) { + // Can't recurse if there are no items + sway_log(L_INFO, "Item has no icon"); + goto bail; + } + dbus_message_iter_recurse(&variant, &array); + + dbus_message_iter_recurse(&array, &d_struct); + + int width; + dbus_message_iter_get_basic(&d_struct, &width); + dbus_message_iter_next(&d_struct); + + int height; + dbus_message_iter_get_basic(&d_struct, &height); + dbus_message_iter_next(&d_struct); + + int len = dbus_message_iter_get_element_count(&d_struct); + + if (!len) { + sway_log(L_ERROR, "No icon data"); + goto bail; + } + + // Also implies len % 4 == 0, useful below + if (len != width * height * 4) { + sway_log(L_ERROR, "Incorrect array size passed"); + goto bail; + } + + dbus_message_iter_recurse(&d_struct, &icon); + + int stride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, width); + // FIXME support a variable stride + // (works on my machine though for all tested widths) + if (!sway_assert(stride == width * 4, "Stride must be equal to byte length")) { + goto bail; + } + + // Data is by reference, no need to free + uint8_t *message_data; + dbus_message_iter_get_fixed_array(&icon, &message_data, &len); + + uint8_t *image_data = malloc(stride * height); + if (!image_data) { + sway_log(L_ERROR, "Could not allocate memory for icon"); + goto bail; + } + + // Transform from network byte order to host byte order + // Assumptions are safe because the equality above + uint32_t *network = (uint32_t *) message_data; + uint32_t *host = (uint32_t *)image_data; + for (int i = 0; i < width * height; ++i) { + host[i] = ntohl(network[i]); + } + + cairo_surface_t *image = cairo_image_surface_create_for_data( + image_data, CAIRO_FORMAT_ARGB32, + width, height, stride); + + if (image) { + if (item->image) { + cairo_surface_destroy(item->image); + } + item->image = image; + // Free the image data on surface destruction + cairo_surface_set_user_data(image, + &cairo_user_data_key, + image_data, + free); + item->dirty = true; + dirty = true; + + dbus_message_unref(reply); + return; + } else { + sway_log(L_ERROR, "Could not create image surface"); + free(image_data); + } + +bail: + if (reply) { + dbus_message_unref(reply); + } + sway_log(L_ERROR, "Could not get icon from item"); + return; +} +static void send_icon_msg(struct StatusNotifierItem *item) { + DBusPendingCall *pending; + DBusMessage *message = dbus_message_new_method_call( + item->name, + "/StatusNotifierItem", + "org.freedesktop.DBus.Properties", + "Get"); + const char *iface; + if (item->kde_special_snowflake) { + iface = "org.kde.StatusNotifierItem"; + } else { + iface = "org.freedesktop.StatusNotifierItem"; + } + const char *prop = "IconPixmap"; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID); + + bool status = + dbus_connection_send_with_reply(conn, message, &pending, -1); + + dbus_message_unref(message); + + if (!(pending || status)) { + sway_log(L_ERROR, "Could not get item icon"); + return; + } + + dbus_pending_call_set_notify(pending, reply_icon, item, NULL); +} + +/* Get an icon by its name */ +static void reply_icon_name(DBusPendingCall *pending, void *_data) { + struct StatusNotifierItem *item = _data; + + DBusMessage *reply = dbus_pending_call_steal_reply(pending); + + if (!reply) { + sway_log(L_INFO, "Got no icon name reply from item"); + goto bail; + } + + int message_type = dbus_message_get_type(reply); + + if (message_type == DBUS_MESSAGE_TYPE_ERROR) { + char *msg; + + dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &msg, + DBUS_TYPE_INVALID); + + sway_log(L_INFO, "Could not get icon name: %s", msg); + goto bail; + } + + DBusMessageIter iter; /* v[s] */ + DBusMessageIter variant; /* s */ + + dbus_message_iter_init(reply, &iter); + if (dbus_message_iter_get_arg_type(&iter) != DBUS_TYPE_VARIANT) { + sway_log(L_ERROR, "Relpy type incorrect"); + sway_log(L_ERROR, "Should be \"v\", is \"%s\"", + dbus_message_iter_get_signature(&iter)); + goto bail; + } + dbus_message_iter_recurse(&iter, &variant); + + + if (dbus_message_iter_get_arg_type(&variant) != DBUS_TYPE_STRING) { + sway_log(L_ERROR, "Relpy type incorrect"); + sway_log(L_ERROR, "Should be \"s\", is \"%s\"", + dbus_message_iter_get_signature(&iter)); + goto bail; + } + + char *icon_name; + dbus_message_iter_get_basic(&variant, &icon_name); + + cairo_surface_t *image = find_icon(icon_name, 256); + + if (image) { + sway_log(L_DEBUG, "Icon for %s found with size %d", icon_name, + cairo_image_surface_get_width(image)); + if (item->image) { + cairo_surface_destroy(item->image); + } + item->image = image; + item->dirty = true; + dirty = true; + + dbus_message_unref(reply); + return; + } + +bail: + if (reply) { + dbus_message_unref(reply); + } + // Now try the pixmap + send_icon_msg(item); + return; +} +static void send_icon_name_msg(struct StatusNotifierItem *item) { + DBusPendingCall *pending; + DBusMessage *message = dbus_message_new_method_call( + item->name, + "/StatusNotifierItem", + "org.freedesktop.DBus.Properties", + "Get"); + const char *iface; + if (item->kde_special_snowflake) { + iface = "org.kde.StatusNotifierItem"; + } else { + iface = "org.freedesktop.StatusNotifierItem"; + } + const char *prop = "IconName"; + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID); + + bool status = + dbus_connection_send_with_reply(conn, message, &pending, -1); + + dbus_message_unref(message); + + if (!(pending || status)) { + sway_log(L_ERROR, "Could not get item icon name"); + return; + } + + dbus_pending_call_set_notify(pending, reply_icon_name, item, NULL); +} + +void get_icon(struct StatusNotifierItem *item) { + send_icon_name_msg(item); +} + +void sni_activate(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + DBusMessage *message = dbus_message_new_method_call( + item->name, + "/StatusNotifierItem", + iface, + "Activate"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} + +void sni_context_menu(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + DBusMessage *message = dbus_message_new_method_call( + item->name, + "/StatusNotifierItem", + iface, + "ContextMenu"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} +void sni_secondary(struct StatusNotifierItem *item, uint32_t x, uint32_t y) { + const char *iface = + (item->kde_special_snowflake ? "org.kde.StatusNotifierItem" + : "org.freedesktop.StatusNotifierItem"); + DBusMessage *message = dbus_message_new_method_call( + item->name, + "/StatusNotifierItem", + iface, + "SecondaryActivate"); + + dbus_message_append_args(message, + DBUS_TYPE_INT32, &x, + DBUS_TYPE_INT32, &y, + DBUS_TYPE_INVALID); + + dbus_connection_send(conn, message, NULL); + + dbus_message_unref(message); +} + +static void get_unique_name(struct StatusNotifierItem *item) { + // I think that we're fine being sync here becaues the message is + // directly to the message bus. Could be async though. + DBusMessage *message = dbus_message_new_method_call( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetNameOwner"); + + dbus_message_append_args(message, + DBUS_TYPE_STRING, &item->name, + DBUS_TYPE_INVALID); + + DBusMessage *reply = dbus_connection_send_with_reply_and_block( + conn, message, -1, NULL); + + dbus_message_unref(message); + + if (!reply) { + sway_log(L_ERROR, "Could not get unique name for item: %s", + item->name); + return; + } + + if (!dbus_message_get_args(reply, NULL, + DBUS_TYPE_STRING, &item->unique_name, + DBUS_TYPE_INVALID)) { + item->unique_name = NULL; + sway_log(L_ERROR, "Error parsing method args"); + } + + dbus_message_unref(reply); +} + +struct StatusNotifierItem *sni_create(const char *name) { + struct StatusNotifierItem *item = malloc(sizeof(struct StatusNotifierItem)); + item->name = strdup(name); + item->unique_name = NULL; + item->image = NULL; + item->dirty = false; + + // If it doesn't use this name then assume that it uses the KDE spec + // This is because xembed-sni-proxy uses neither "org.freedesktop" nor + // "org.kde" and just gives us the items "unique name" + // + // We could use this to our advantage and fill out the "unique name" + // field with the given name if it is neither freedesktop or kde, but + // that's makes us rely on KDE hackyness which is bad practice + const char freedesktop_name[] = "org.freedesktop"; + if (strncmp(name, freedesktop_name, sizeof(freedesktop_name) - 1) != 0) { + item->kde_special_snowflake = true; + } else { + item->kde_special_snowflake = false; + } + + get_icon(item); + + get_unique_name(item); + + return item; +} +/* Return true if `item` has a name of `str` */ +int sni_str_cmp(const void *_item, const void *_str) { + const struct StatusNotifierItem *item = _item; + const char *str = _str; + + return strcmp(item->name, str); +} +/* Returns true if `item` has a unique name of `str` */ +int sni_uniq_cmp(const void *_item, const void *_str) { + const struct StatusNotifierItem *item = _item; + const char *str = _str; + + if (!item->unique_name) { + return false; + } + return strcmp(item->unique_name, str); +} +void sni_free(struct StatusNotifierItem *item) { + if (!item) { + return; + } + free(item->name); + if (item->image) { + cairo_surface_destroy(item->image); + } + free(item); +} -- cgit v1.2.3-54-g00ecf