From 74655f835aa9fe0e976473d443f62d253602696c Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Fri, 7 Dec 2018 12:33:45 +0000 Subject: swaybar: add StatusNotifierItem to tray --- swaybar/tray/item.c | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 swaybar/tray/item.c (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c new file mode 100644 index 00000000..561a3425 --- /dev/null +++ b/swaybar/tray/item.c @@ -0,0 +1,236 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include "swaybar/tray/host.h" +#include "swaybar/tray/item.h" +#include "swaybar/tray/tray.h" +#include "list.h" +#include "log.h" + +// TODO menu + +static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, + const char *prop, list_t **dest) { + int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)"); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); + return ret; + } + + if (sd_bus_message_at_end(msg, 0)) { + return ret; + } + + list_t *pixmaps = create_list(); + if (!pixmaps) { + return -12; // -ENOMEM + } + + while (!sd_bus_message_at_end(msg, 0)) { + ret = sd_bus_message_enter_container(msg, 'r', "iiay"); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); + goto error; + } + + int size; + ret = sd_bus_message_read(msg, "ii", NULL, &size); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); + goto error; + } + + const void *pixels; + size_t npixels; + ret = sd_bus_message_read_array(msg, 'y', &pixels, &npixels); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); + goto error; + } + + struct swaybar_pixmap *pixmap = + malloc(sizeof(struct swaybar_pixmap) + npixels); + pixmap->size = size; + memcpy(pixmap->pixels, pixels, npixels); + list_add(pixmaps, pixmap); + + sd_bus_message_exit_container(msg); + } + *dest = pixmaps; + + return ret; +error: + list_free_items_and_destroy(pixmaps); + return ret; +} + +struct get_property_data { + struct swaybar_sni *sni; + const char *prop; + const char *type; + void *dest; +}; + +static int get_property_callback(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct get_property_data *d = data; + struct swaybar_sni *sni = d->sni; + const char *prop = d->prop; + const char *type = d->type; + void *dest = d->dest; + + int ret; + if (sd_bus_message_is_method_error(msg, NULL)) { + sd_bus_error err = *sd_bus_message_get_error(msg); + wlr_log(WLR_DEBUG, "Failed to get property %s: %s", prop, err.message); + ret = -sd_bus_error_get_errno(&err); + goto cleanup; + } + + ret = sd_bus_message_enter_container(msg, 'v', type); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); + goto cleanup; + } + + if (!type) { + ret = read_pixmap(msg, sni, prop, dest); + if (ret < 0) { + goto cleanup; + } + } else { + ret = sd_bus_message_read(msg, type, dest); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, + strerror(-ret)); + goto cleanup; + } else if (*type == 's' || *type == 'o') { + char **str = dest; + *str = strdup(*str); + } + } +cleanup: + free(data); + return ret; +} + +static void sni_get_property_async(struct swaybar_sni *sni, const char *prop, + const char *type, void *dest) { + struct get_property_data *data = malloc(sizeof(struct get_property_data)); + data->sni = sni; + data->prop = prop; + data->type = type; + data->dest = dest; + int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, + sni->path, "org.freedesktop.DBus.Properties", "Get", + get_property_callback, data, "ss", sni->interface, prop); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to get property %s: %s", prop, strerror(-ret)); + } +} + +static int handle_new_icon(sd_bus_message *msg, void *data, sd_bus_error *error) { + struct swaybar_sni *sni = data; + wlr_log(WLR_DEBUG, "%s has new IconName", sni->watcher_id); + + free(sni->icon_name); + sni->icon_name = NULL; + sni_get_property_async(sni, "IconName", "s", &sni->icon_name); + + list_free_items_and_destroy(sni->icon_pixmap); + sni->icon_pixmap = NULL; + sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); + + return 0; +} + +static int handle_new_attention_icon(sd_bus_message *msg, void *data, + sd_bus_error *error) { + struct swaybar_sni *sni = data; + wlr_log(WLR_DEBUG, "%s has new AttentionIconName", sni->watcher_id); + + free(sni->attention_icon_name); + sni->attention_icon_name = NULL; + sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); + + list_free_items_and_destroy(sni->attention_icon_pixmap); + sni->attention_icon_pixmap = NULL; + sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); + + return 0; +} + +static int handle_new_status(sd_bus_message *msg, void *data, sd_bus_error *error) { + char *status; + int ret = sd_bus_message_read(msg, "s", &status); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to read new status message: %s", strerror(-ret)); + } else { + struct swaybar_sni *sni = data; + free(sni->status); + sni->status = strdup(status); + wlr_log(WLR_DEBUG, "%s has new Status '%s'", sni->watcher_id, status); + } + return ret; +} + +static void sni_match_signal(struct swaybar_sni *sni, char *signal, + sd_bus_message_handler_t callback) { + int ret = sd_bus_match_signal(sni->tray->bus, NULL, sni->service, sni->path, + sni->interface, signal, callback, sni); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to subscribe to signal %s: %s", signal, + strerror(-ret)); + } +} + +struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) { + struct swaybar_sni *sni = calloc(1, sizeof(struct swaybar_sni)); + if (!sni) { + return NULL; + } + sni->tray = tray; + sni->watcher_id = strdup(id); + char *path_ptr = strchr(id, '/'); + if (!path_ptr) { + sni->service = strdup(id); + sni->path = strdup("/StatusNotifierItem"); + sni->interface = "org.freedesktop.StatusNotifierItem"; + } else { + sni->service = strndup(id, path_ptr - id); + sni->path = strdup(path_ptr); + sni->interface = "org.kde.StatusNotifierItem"; + } + + // Ignored: Category, Id, Title, WindowId, OverlayIconName, + // OverlayIconPixmap, AttentionMovieName, ToolTip + sni_get_property_async(sni, "Status", "s", &sni->status); + sni_get_property_async(sni, "IconName", "s", &sni->icon_name); + sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); + sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); + sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); + sni_get_property_async(sni, "ItemIsMenu", "b", &sni->item_is_menu); + sni_get_property_async(sni, "Menu", "o", &sni->menu); + + sni_match_signal(sni, "NewIcon", handle_new_icon); + sni_match_signal(sni, "NewAttentionIcon", handle_new_attention_icon); + sni_match_signal(sni, "NewStatus", handle_new_status); + + return sni; +} + +void destroy_sni(struct swaybar_sni *sni) { + if (!sni) { + return; + } + + free(sni->watcher_id); + free(sni->service); + free(sni->path); + free(sni->status); + free(sni->icon_name); + free(sni->icon_pixmap); + free(sni->attention_icon_name); + free(sni->menu); + free(sni); +} -- cgit v1.2.3 From fa2c5282c1d09eced82a9c15a4ee26e7b00a37c4 Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Fri, 7 Dec 2018 12:37:33 +0000 Subject: swaybar: implement tray rendering --- swaybar/tray/item.c | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index 561a3425..019e8ed0 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -1,14 +1,27 @@ #define _POSIX_C_SOURCE 200809L +#include +#include #include #include +#include "swaybar/bar.h" +#include "swaybar/config.h" #include "swaybar/tray/host.h" +#include "swaybar/tray/icon.h" #include "swaybar/tray/item.h" #include "swaybar/tray/tray.h" +#include "background-image.h" +#include "cairo.h" #include "list.h" #include "log.h" // TODO menu +static bool sni_ready(struct swaybar_sni *sni) { + return sni->status && (sni->status[0] == 'N' ? + sni->attention_icon_name || sni->attention_icon_pixmap : + sni->icon_name || sni->icon_pixmap); +} + static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, const char *prop, list_t **dest) { int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)"); @@ -234,3 +247,74 @@ void destroy_sni(struct swaybar_sni *sni) { free(sni->menu); free(sni); } + +uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, + struct swaybar_sni *sni) { + uint32_t height = output->height * output->scale; + int padding = output->bar->config->tray_padding; + int ideal_size = height - 2*padding; + if ((ideal_size < sni->min_size || ideal_size > sni->max_size) && sni_ready(sni)) { + bool icon_found = false; + char *icon_name = sni->status[0] == 'N' ? + sni->attention_icon_name : sni->icon_name; + if (icon_name) { + char *icon_path = find_icon(sni->tray->themes, sni->tray->basedirs, + icon_name, ideal_size, output->bar->config->icon_theme, + &sni->min_size, &sni->max_size); + if (icon_path) { + cairo_surface_destroy(sni->icon); + sni->icon = load_background_image(icon_path); + free(icon_path); + icon_found = true; + } + } + if (!icon_found) { + list_t *pixmaps = sni->status[0] == 'N' ? + sni->attention_icon_pixmap : sni->icon_pixmap; + if (pixmaps) { + int idx = -1; + unsigned smallest_error = -1; // UINT_MAX + for (int i = 0; i < pixmaps->length; ++i) { + struct swaybar_pixmap *pixmap = pixmaps->items[i]; + unsigned error = (ideal_size - pixmap->size) * + (ideal_size < pixmap->size ? -1 : 1); + if (error < smallest_error) { + smallest_error = error; + idx = i; + } + } + struct swaybar_pixmap *pixmap = pixmaps->items[idx]; + cairo_surface_destroy(sni->icon); + sni->icon = cairo_image_surface_create_for_data(pixmap->pixels, + CAIRO_FORMAT_ARGB32, pixmap->size, pixmap->size, + cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixmap->size)); + } + } + } + + if (!sni->icon) { + // TODO fallback + return 0; + } + + cairo_surface_t *icon; + int actual_size = cairo_image_surface_get_height(sni->icon); + int icon_size = actual_size < ideal_size ? + actual_size * (ideal_size / actual_size) : ideal_size; + icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size); + + int padded_size = icon_size + 2*padding; + *x -= padded_size; + int y = floor((height - padded_size) / 2.0); + + cairo_operator_t op = cairo_get_operator(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); + cairo_set_source_surface(cairo, icon, *x + padding, y + padding); + cairo_rectangle(cairo, *x, y, padded_size, padded_size); + cairo_fill(cairo); + cairo_set_operator(cairo, op); + + cairo_surface_destroy(icon); + + return output->height; +} -- cgit v1.2.3 From 6becde024680503100c94702ed7d1fbf4d01576e Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Fri, 7 Dec 2018 12:39:35 +0000 Subject: swaybar: implement mouse events for tray --- swaybar/tray/item.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index 019e8ed0..450b6882 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -5,6 +5,7 @@ #include #include "swaybar/bar.h" #include "swaybar/config.h" +#include "swaybar/input.h" #include "swaybar/tray/host.h" #include "swaybar/tray/icon.h" #include "swaybar/tray/item.h" @@ -13,6 +14,7 @@ #include "cairo.h" #include "list.h" #include "log.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" // TODO menu @@ -248,6 +250,83 @@ void destroy_sni(struct swaybar_sni *sni) { free(sni); } +static void handle_click(struct swaybar_sni *sni, int x, int y, + enum x11_button button, int delta) { + const char *method = sni->tray->bar->config->tray_bindings[button]; + if (!method) { + static const char *default_bindings[10] = { + "nop", + "Activate", + "SecondaryActivate", + "ContextMenu", + "ScrollUp", + "ScrollDown", + "ScrollLeft", + "ScrollRight", + "nop", + "nop" + }; + method = default_bindings[button]; + } + if (strcmp(method, "nop") == 0) { + return; + } + if (sni->item_is_menu && strcmp(method, "Activate") == 0) { + method = "ContextMenu"; + } + + if (strncmp(method, "Scroll", strlen("Scroll")) == 0) { + char dir = method[strlen("Scroll")]; + char *orientation = (dir = 'U' || dir == 'D') ? "vertical" : "horizontal"; + int sign = (dir == 'U' || dir == 'L') ? -1 : 1; + + int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, + sni->path, sni->interface, "Scroll", NULL, NULL, "is", + delta*sign, orientation); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to scroll on SNI: %s", strerror(-ret)); + } + } else { + int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, + sni->path, sni->interface, method, NULL, NULL, "ii", x, y); + if (ret < 0) { + wlr_log(WLR_DEBUG, "Failed to click on SNI: %s", strerror(-ret)); + } + } +} + +static int cmp_sni_id(const void *item, const void *cmp_to) { + const struct swaybar_sni *sni = item; + return strcmp(sni->watcher_id, cmp_to); +} + +static enum hotspot_event_handling icon_hotspot_callback( + struct swaybar_output *output, struct swaybar_hotspot *hotspot, + int x, int y, enum x11_button button, void *data) { + wlr_log(WLR_DEBUG, "Clicked on Status Notifier Item '%s'", (char *)data); + + struct swaybar_tray *tray = output->bar->tray; + int idx = list_seq_find(tray->items, cmp_sni_id, data); + + if (idx != -1) { + struct swaybar_sni *sni = tray->items->items[idx]; + // guess global position since wayland doesn't expose it + struct swaybar_config *config = tray->bar->config; + int global_x = output->output_x + config->gaps.left + x; + bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + int global_y = output->output_y + (top_bar ? config->gaps.top + y: + (int) output->output_height - config->gaps.bottom - y); + + wlr_log(WLR_DEBUG, "Guessing click at (%d, %d)", global_x, global_y); + handle_click(sni, global_x, global_y, button, 1); // TODO get delta from event + return HOTSPOT_IGNORE; + } else { + wlr_log(WLR_DEBUG, "but it doesn't exist"); + } + + return HOTSPOT_PROCESS; +} + uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, struct swaybar_sni *sni) { uint32_t height = output->height * output->scale; @@ -316,5 +395,15 @@ uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, cairo_surface_destroy(icon); + struct swaybar_hotspot *hotspot = calloc(1, sizeof(struct swaybar_hotspot)); + hotspot->x = *x; + hotspot->y = 0; + hotspot->width = height; + hotspot->height = height; + hotspot->callback = icon_hotspot_callback; + hotspot->destroy = free; + hotspot->data = strdup(sni->watcher_id); + wl_list_insert(&output->hotspots, &hotspot->link); + return output->height; } -- cgit v1.2.3 From 2fd41fe9c85a866173fc2770ed7669871258bced Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Fri, 7 Dec 2018 12:40:45 +0000 Subject: swaybar: set bar dirty on SNI event --- swaybar/tray/item.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index 450b6882..98d3dc1f 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -24,6 +24,13 @@ static bool sni_ready(struct swaybar_sni *sni) { sni->icon_name || sni->icon_pixmap); } +static void set_sni_dirty(struct swaybar_sni *sni) { + if (sni_ready(sni)) { + sni->min_size = sni->max_size = 0; // invalidate previous icon + set_bar_dirty(sni->tray->bar); + } +} + static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, const char *prop, list_t **dest) { int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)"); @@ -124,6 +131,11 @@ static int get_property_callback(sd_bus_message *msg, void *data, *str = strdup(*str); } } + + if (strcmp(prop, "Status") == 0 || (sni->status && (sni->status[0] == 'N' ? + prop[0] == 'A' : strncmp(prop, "Icon", 4) == 0))) { + set_sni_dirty(sni); + } cleanup: free(data); return ret; @@ -185,6 +197,7 @@ static int handle_new_status(sd_bus_message *msg, void *data, sd_bus_error *erro free(sni->status); sni->status = strdup(status); wlr_log(WLR_DEBUG, "%s has new Status '%s'", sni->watcher_id, status); + set_sni_dirty(sni); } return ret; } -- cgit v1.2.3 From 8ffb7f05293d306959d709063f430182dfbffb7a Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Sun, 16 Dec 2018 18:43:40 +0000 Subject: swaybar: draw a sad face if SNI has no icon --- swaybar/tray/item.c | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index 98d3dc1f..d00339e2 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -384,16 +384,32 @@ uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, } } - if (!sni->icon) { - // TODO fallback - return 0; - } - + int icon_size; cairo_surface_t *icon; - int actual_size = cairo_image_surface_get_height(sni->icon); - int icon_size = actual_size < ideal_size ? - actual_size * (ideal_size / actual_size) : ideal_size; - icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size); + if (sni->icon) { + int actual_size = cairo_image_surface_get_height(sni->icon); + icon_size = actual_size < ideal_size ? + actual_size*(ideal_size/actual_size) : ideal_size; + icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size); + } else { // draw a sad face + icon_size = ideal_size*0.8; + icon = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, icon_size, icon_size); + cairo_t *cairo_icon = cairo_create(icon); + cairo_set_source_u32(cairo_icon, 0xFF0000FF); + cairo_translate(cairo_icon, icon_size/2, icon_size/2); + cairo_scale(cairo_icon, icon_size/2, icon_size/2); + cairo_arc(cairo_icon, 0, 0, 1, 0, 7); + cairo_fill(cairo_icon); + cairo_set_operator(cairo_icon, CAIRO_OPERATOR_CLEAR); + cairo_arc(cairo_icon, 0.35, -0.3, 0.1, 0, 7); + cairo_fill(cairo_icon); + cairo_arc(cairo_icon, -0.35, -0.3, 0.1, 0, 7); + cairo_fill(cairo_icon); + cairo_arc(cairo_icon, 0, 0.75, 0.5, 3.71238898038469, 5.71238898038469); + cairo_set_line_width(cairo_icon, 0.1); + cairo_stroke(cairo_icon); + cairo_destroy(cairo_icon); + } int padded_size = icon_size + 2*padding; *x -= padded_size; -- cgit v1.2.3 From 9e31f5d79ce9de8f1c73013b792aa94f355a1983 Mon Sep 17 00:00:00 2001 From: Ian Fan Date: Mon, 17 Dec 2018 23:52:15 +0000 Subject: swaybar: use KDE's SNI IconThemePath property --- swaybar/tray/item.c | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'swaybar/tray/item.c') diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c index d00339e2..41cacd16 100644 --- a/swaybar/tray/item.c +++ b/swaybar/tray/item.c @@ -228,6 +228,7 @@ struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) { sni->service = strndup(id, path_ptr - id); sni->path = strdup(path_ptr); sni->interface = "org.kde.StatusNotifierItem"; + sni_get_property_async(sni, "IconThemePath", "s", &sni->icon_theme_path); } // Ignored: Category, Id, Title, WindowId, OverlayIconName, @@ -353,6 +354,10 @@ uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, char *icon_path = find_icon(sni->tray->themes, sni->tray->basedirs, icon_name, ideal_size, output->bar->config->icon_theme, &sni->min_size, &sni->max_size); + if (!icon_path && sni->icon_theme_path) { + icon_path = find_icon_in_dir(icon_name, sni->icon_theme_path, + &sni->min_size, &sni->max_size); + } if (icon_path) { cairo_surface_destroy(sni->icon); sni->icon = load_background_image(icon_path); -- cgit v1.2.3