diff options
Diffstat (limited to 'swaybar/tray/item.c')
-rw-r--r-- | swaybar/tray/item.c | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/swaybar/tray/item.c b/swaybar/tray/item.c new file mode 100644 index 00000000..41cacd16 --- /dev/null +++ b/swaybar/tray/item.c | |||
@@ -0,0 +1,443 @@ | |||
1 | #define _POSIX_C_SOURCE 200809L | ||
2 | #include <cairo.h> | ||
3 | #include <stdbool.h> | ||
4 | #include <stdlib.h> | ||
5 | #include <string.h> | ||
6 | #include "swaybar/bar.h" | ||
7 | #include "swaybar/config.h" | ||
8 | #include "swaybar/input.h" | ||
9 | #include "swaybar/tray/host.h" | ||
10 | #include "swaybar/tray/icon.h" | ||
11 | #include "swaybar/tray/item.h" | ||
12 | #include "swaybar/tray/tray.h" | ||
13 | #include "background-image.h" | ||
14 | #include "cairo.h" | ||
15 | #include "list.h" | ||
16 | #include "log.h" | ||
17 | #include "wlr-layer-shell-unstable-v1-client-protocol.h" | ||
18 | |||
19 | // TODO menu | ||
20 | |||
21 | static bool sni_ready(struct swaybar_sni *sni) { | ||
22 | return sni->status && (sni->status[0] == 'N' ? | ||
23 | sni->attention_icon_name || sni->attention_icon_pixmap : | ||
24 | sni->icon_name || sni->icon_pixmap); | ||
25 | } | ||
26 | |||
27 | static void set_sni_dirty(struct swaybar_sni *sni) { | ||
28 | if (sni_ready(sni)) { | ||
29 | sni->min_size = sni->max_size = 0; // invalidate previous icon | ||
30 | set_bar_dirty(sni->tray->bar); | ||
31 | } | ||
32 | } | ||
33 | |||
34 | static int read_pixmap(sd_bus_message *msg, struct swaybar_sni *sni, | ||
35 | const char *prop, list_t **dest) { | ||
36 | int ret = sd_bus_message_enter_container(msg, 'a', "(iiay)"); | ||
37 | if (ret < 0) { | ||
38 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); | ||
39 | return ret; | ||
40 | } | ||
41 | |||
42 | if (sd_bus_message_at_end(msg, 0)) { | ||
43 | return ret; | ||
44 | } | ||
45 | |||
46 | list_t *pixmaps = create_list(); | ||
47 | if (!pixmaps) { | ||
48 | return -12; // -ENOMEM | ||
49 | } | ||
50 | |||
51 | while (!sd_bus_message_at_end(msg, 0)) { | ||
52 | ret = sd_bus_message_enter_container(msg, 'r', "iiay"); | ||
53 | if (ret < 0) { | ||
54 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); | ||
55 | goto error; | ||
56 | } | ||
57 | |||
58 | int size; | ||
59 | ret = sd_bus_message_read(msg, "ii", NULL, &size); | ||
60 | if (ret < 0) { | ||
61 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); | ||
62 | goto error; | ||
63 | } | ||
64 | |||
65 | const void *pixels; | ||
66 | size_t npixels; | ||
67 | ret = sd_bus_message_read_array(msg, 'y', &pixels, &npixels); | ||
68 | if (ret < 0) { | ||
69 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); | ||
70 | goto error; | ||
71 | } | ||
72 | |||
73 | struct swaybar_pixmap *pixmap = | ||
74 | malloc(sizeof(struct swaybar_pixmap) + npixels); | ||
75 | pixmap->size = size; | ||
76 | memcpy(pixmap->pixels, pixels, npixels); | ||
77 | list_add(pixmaps, pixmap); | ||
78 | |||
79 | sd_bus_message_exit_container(msg); | ||
80 | } | ||
81 | *dest = pixmaps; | ||
82 | |||
83 | return ret; | ||
84 | error: | ||
85 | list_free_items_and_destroy(pixmaps); | ||
86 | return ret; | ||
87 | } | ||
88 | |||
89 | struct get_property_data { | ||
90 | struct swaybar_sni *sni; | ||
91 | const char *prop; | ||
92 | const char *type; | ||
93 | void *dest; | ||
94 | }; | ||
95 | |||
96 | static int get_property_callback(sd_bus_message *msg, void *data, | ||
97 | sd_bus_error *error) { | ||
98 | struct get_property_data *d = data; | ||
99 | struct swaybar_sni *sni = d->sni; | ||
100 | const char *prop = d->prop; | ||
101 | const char *type = d->type; | ||
102 | void *dest = d->dest; | ||
103 | |||
104 | int ret; | ||
105 | if (sd_bus_message_is_method_error(msg, NULL)) { | ||
106 | sd_bus_error err = *sd_bus_message_get_error(msg); | ||
107 | wlr_log(WLR_DEBUG, "Failed to get property %s: %s", prop, err.message); | ||
108 | ret = -sd_bus_error_get_errno(&err); | ||
109 | goto cleanup; | ||
110 | } | ||
111 | |||
112 | ret = sd_bus_message_enter_container(msg, 'v', type); | ||
113 | if (ret < 0) { | ||
114 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, strerror(-ret)); | ||
115 | goto cleanup; | ||
116 | } | ||
117 | |||
118 | if (!type) { | ||
119 | ret = read_pixmap(msg, sni, prop, dest); | ||
120 | if (ret < 0) { | ||
121 | goto cleanup; | ||
122 | } | ||
123 | } else { | ||
124 | ret = sd_bus_message_read(msg, type, dest); | ||
125 | if (ret < 0) { | ||
126 | wlr_log(WLR_DEBUG, "Failed to read property %s: %s", prop, | ||
127 | strerror(-ret)); | ||
128 | goto cleanup; | ||
129 | } else if (*type == 's' || *type == 'o') { | ||
130 | char **str = dest; | ||
131 | *str = strdup(*str); | ||
132 | } | ||
133 | } | ||
134 | |||
135 | if (strcmp(prop, "Status") == 0 || (sni->status && (sni->status[0] == 'N' ? | ||
136 | prop[0] == 'A' : strncmp(prop, "Icon", 4) == 0))) { | ||
137 | set_sni_dirty(sni); | ||
138 | } | ||
139 | cleanup: | ||
140 | free(data); | ||
141 | return ret; | ||
142 | } | ||
143 | |||
144 | static void sni_get_property_async(struct swaybar_sni *sni, const char *prop, | ||
145 | const char *type, void *dest) { | ||
146 | struct get_property_data *data = malloc(sizeof(struct get_property_data)); | ||
147 | data->sni = sni; | ||
148 | data->prop = prop; | ||
149 | data->type = type; | ||
150 | data->dest = dest; | ||
151 | int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, | ||
152 | sni->path, "org.freedesktop.DBus.Properties", "Get", | ||
153 | get_property_callback, data, "ss", sni->interface, prop); | ||
154 | if (ret < 0) { | ||
155 | wlr_log(WLR_DEBUG, "Failed to get property %s: %s", prop, strerror(-ret)); | ||
156 | } | ||
157 | } | ||
158 | |||
159 | static int handle_new_icon(sd_bus_message *msg, void *data, sd_bus_error *error) { | ||
160 | struct swaybar_sni *sni = data; | ||
161 | wlr_log(WLR_DEBUG, "%s has new IconName", sni->watcher_id); | ||
162 | |||
163 | free(sni->icon_name); | ||
164 | sni->icon_name = NULL; | ||
165 | sni_get_property_async(sni, "IconName", "s", &sni->icon_name); | ||
166 | |||
167 | list_free_items_and_destroy(sni->icon_pixmap); | ||
168 | sni->icon_pixmap = NULL; | ||
169 | sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); | ||
170 | |||
171 | return 0; | ||
172 | } | ||
173 | |||
174 | static int handle_new_attention_icon(sd_bus_message *msg, void *data, | ||
175 | sd_bus_error *error) { | ||
176 | struct swaybar_sni *sni = data; | ||
177 | wlr_log(WLR_DEBUG, "%s has new AttentionIconName", sni->watcher_id); | ||
178 | |||
179 | free(sni->attention_icon_name); | ||
180 | sni->attention_icon_name = NULL; | ||
181 | sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); | ||
182 | |||
183 | list_free_items_and_destroy(sni->attention_icon_pixmap); | ||
184 | sni->attention_icon_pixmap = NULL; | ||
185 | sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); | ||
186 | |||
187 | return 0; | ||
188 | } | ||
189 | |||
190 | static int handle_new_status(sd_bus_message *msg, void *data, sd_bus_error *error) { | ||
191 | char *status; | ||
192 | int ret = sd_bus_message_read(msg, "s", &status); | ||
193 | if (ret < 0) { | ||
194 | wlr_log(WLR_DEBUG, "Failed to read new status message: %s", strerror(-ret)); | ||
195 | } else { | ||
196 | struct swaybar_sni *sni = data; | ||
197 | free(sni->status); | ||
198 | sni->status = strdup(status); | ||
199 | wlr_log(WLR_DEBUG, "%s has new Status '%s'", sni->watcher_id, status); | ||
200 | set_sni_dirty(sni); | ||
201 | } | ||
202 | return ret; | ||
203 | } | ||
204 | |||
205 | static void sni_match_signal(struct swaybar_sni *sni, char *signal, | ||
206 | sd_bus_message_handler_t callback) { | ||
207 | int ret = sd_bus_match_signal(sni->tray->bus, NULL, sni->service, sni->path, | ||
208 | sni->interface, signal, callback, sni); | ||
209 | if (ret < 0) { | ||
210 | wlr_log(WLR_DEBUG, "Failed to subscribe to signal %s: %s", signal, | ||
211 | strerror(-ret)); | ||
212 | } | ||
213 | } | ||
214 | |||
215 | struct swaybar_sni *create_sni(char *id, struct swaybar_tray *tray) { | ||
216 | struct swaybar_sni *sni = calloc(1, sizeof(struct swaybar_sni)); | ||
217 | if (!sni) { | ||
218 | return NULL; | ||
219 | } | ||
220 | sni->tray = tray; | ||
221 | sni->watcher_id = strdup(id); | ||
222 | char *path_ptr = strchr(id, '/'); | ||
223 | if (!path_ptr) { | ||
224 | sni->service = strdup(id); | ||
225 | sni->path = strdup("/StatusNotifierItem"); | ||
226 | sni->interface = "org.freedesktop.StatusNotifierItem"; | ||
227 | } else { | ||
228 | sni->service = strndup(id, path_ptr - id); | ||
229 | sni->path = strdup(path_ptr); | ||
230 | sni->interface = "org.kde.StatusNotifierItem"; | ||
231 | sni_get_property_async(sni, "IconThemePath", "s", &sni->icon_theme_path); | ||
232 | } | ||
233 | |||
234 | // Ignored: Category, Id, Title, WindowId, OverlayIconName, | ||
235 | // OverlayIconPixmap, AttentionMovieName, ToolTip | ||
236 | sni_get_property_async(sni, "Status", "s", &sni->status); | ||
237 | sni_get_property_async(sni, "IconName", "s", &sni->icon_name); | ||
238 | sni_get_property_async(sni, "IconPixmap", NULL, &sni->icon_pixmap); | ||
239 | sni_get_property_async(sni, "AttentionIconName", "s", &sni->attention_icon_name); | ||
240 | sni_get_property_async(sni, "AttentionIconPixmap", NULL, &sni->attention_icon_pixmap); | ||
241 | sni_get_property_async(sni, "ItemIsMenu", "b", &sni->item_is_menu); | ||
242 | sni_get_property_async(sni, "Menu", "o", &sni->menu); | ||
243 | |||
244 | sni_match_signal(sni, "NewIcon", handle_new_icon); | ||
245 | sni_match_signal(sni, "NewAttentionIcon", handle_new_attention_icon); | ||
246 | sni_match_signal(sni, "NewStatus", handle_new_status); | ||
247 | |||
248 | return sni; | ||
249 | } | ||
250 | |||
251 | void destroy_sni(struct swaybar_sni *sni) { | ||
252 | if (!sni) { | ||
253 | return; | ||
254 | } | ||
255 | |||
256 | free(sni->watcher_id); | ||
257 | free(sni->service); | ||
258 | free(sni->path); | ||
259 | free(sni->status); | ||
260 | free(sni->icon_name); | ||
261 | free(sni->icon_pixmap); | ||
262 | free(sni->attention_icon_name); | ||
263 | free(sni->menu); | ||
264 | free(sni); | ||
265 | } | ||
266 | |||
267 | static void handle_click(struct swaybar_sni *sni, int x, int y, | ||
268 | enum x11_button button, int delta) { | ||
269 | const char *method = sni->tray->bar->config->tray_bindings[button]; | ||
270 | if (!method) { | ||
271 | static const char *default_bindings[10] = { | ||
272 | "nop", | ||
273 | "Activate", | ||
274 | "SecondaryActivate", | ||
275 | "ContextMenu", | ||
276 | "ScrollUp", | ||
277 | "ScrollDown", | ||
278 | "ScrollLeft", | ||
279 | "ScrollRight", | ||
280 | "nop", | ||
281 | "nop" | ||
282 | }; | ||
283 | method = default_bindings[button]; | ||
284 | } | ||
285 | if (strcmp(method, "nop") == 0) { | ||
286 | return; | ||
287 | } | ||
288 | if (sni->item_is_menu && strcmp(method, "Activate") == 0) { | ||
289 | method = "ContextMenu"; | ||
290 | } | ||
291 | |||
292 | if (strncmp(method, "Scroll", strlen("Scroll")) == 0) { | ||
293 | char dir = method[strlen("Scroll")]; | ||
294 | char *orientation = (dir = 'U' || dir == 'D') ? "vertical" : "horizontal"; | ||
295 | int sign = (dir == 'U' || dir == 'L') ? -1 : 1; | ||
296 | |||
297 | int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, | ||
298 | sni->path, sni->interface, "Scroll", NULL, NULL, "is", | ||
299 | delta*sign, orientation); | ||
300 | if (ret < 0) { | ||
301 | wlr_log(WLR_DEBUG, "Failed to scroll on SNI: %s", strerror(-ret)); | ||
302 | } | ||
303 | } else { | ||
304 | int ret = sd_bus_call_method_async(sni->tray->bus, NULL, sni->service, | ||
305 | sni->path, sni->interface, method, NULL, NULL, "ii", x, y); | ||
306 | if (ret < 0) { | ||
307 | wlr_log(WLR_DEBUG, "Failed to click on SNI: %s", strerror(-ret)); | ||
308 | } | ||
309 | } | ||
310 | } | ||
311 | |||
312 | static int cmp_sni_id(const void *item, const void *cmp_to) { | ||
313 | const struct swaybar_sni *sni = item; | ||
314 | return strcmp(sni->watcher_id, cmp_to); | ||
315 | } | ||
316 | |||
317 | static enum hotspot_event_handling icon_hotspot_callback( | ||
318 | struct swaybar_output *output, struct swaybar_hotspot *hotspot, | ||
319 | int x, int y, enum x11_button button, void *data) { | ||
320 | wlr_log(WLR_DEBUG, "Clicked on Status Notifier Item '%s'", (char *)data); | ||
321 | |||
322 | struct swaybar_tray *tray = output->bar->tray; | ||
323 | int idx = list_seq_find(tray->items, cmp_sni_id, data); | ||
324 | |||
325 | if (idx != -1) { | ||
326 | struct swaybar_sni *sni = tray->items->items[idx]; | ||
327 | // guess global position since wayland doesn't expose it | ||
328 | struct swaybar_config *config = tray->bar->config; | ||
329 | int global_x = output->output_x + config->gaps.left + x; | ||
330 | bool top_bar = config->position & ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; | ||
331 | int global_y = output->output_y + (top_bar ? config->gaps.top + y: | ||
332 | (int) output->output_height - config->gaps.bottom - y); | ||
333 | |||
334 | wlr_log(WLR_DEBUG, "Guessing click at (%d, %d)", global_x, global_y); | ||
335 | handle_click(sni, global_x, global_y, button, 1); // TODO get delta from event | ||
336 | return HOTSPOT_IGNORE; | ||
337 | } else { | ||
338 | wlr_log(WLR_DEBUG, "but it doesn't exist"); | ||
339 | } | ||
340 | |||
341 | return HOTSPOT_PROCESS; | ||
342 | } | ||
343 | |||
344 | uint32_t render_sni(cairo_t *cairo, struct swaybar_output *output, double *x, | ||
345 | struct swaybar_sni *sni) { | ||
346 | uint32_t height = output->height * output->scale; | ||
347 | int padding = output->bar->config->tray_padding; | ||
348 | int ideal_size = height - 2*padding; | ||
349 | if ((ideal_size < sni->min_size || ideal_size > sni->max_size) && sni_ready(sni)) { | ||
350 | bool icon_found = false; | ||
351 | char *icon_name = sni->status[0] == 'N' ? | ||
352 | sni->attention_icon_name : sni->icon_name; | ||
353 | if (icon_name) { | ||
354 | char *icon_path = find_icon(sni->tray->themes, sni->tray->basedirs, | ||
355 | icon_name, ideal_size, output->bar->config->icon_theme, | ||
356 | &sni->min_size, &sni->max_size); | ||
357 | if (!icon_path && sni->icon_theme_path) { | ||
358 | icon_path = find_icon_in_dir(icon_name, sni->icon_theme_path, | ||
359 | &sni->min_size, &sni->max_size); | ||
360 | } | ||
361 | if (icon_path) { | ||
362 | cairo_surface_destroy(sni->icon); | ||
363 | sni->icon = load_background_image(icon_path); | ||
364 | free(icon_path); | ||
365 | icon_found = true; | ||
366 | } | ||
367 | } | ||
368 | if (!icon_found) { | ||
369 | list_t *pixmaps = sni->status[0] == 'N' ? | ||
370 | sni->attention_icon_pixmap : sni->icon_pixmap; | ||
371 | if (pixmaps) { | ||
372 | int idx = -1; | ||
373 | unsigned smallest_error = -1; // UINT_MAX | ||
374 | for (int i = 0; i < pixmaps->length; ++i) { | ||
375 | struct swaybar_pixmap *pixmap = pixmaps->items[i]; | ||
376 | unsigned error = (ideal_size - pixmap->size) * | ||
377 | (ideal_size < pixmap->size ? -1 : 1); | ||
378 | if (error < smallest_error) { | ||
379 | smallest_error = error; | ||
380 | idx = i; | ||
381 | } | ||
382 | } | ||
383 | struct swaybar_pixmap *pixmap = pixmaps->items[idx]; | ||
384 | cairo_surface_destroy(sni->icon); | ||
385 | sni->icon = cairo_image_surface_create_for_data(pixmap->pixels, | ||
386 | CAIRO_FORMAT_ARGB32, pixmap->size, pixmap->size, | ||
387 | cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixmap->size)); | ||
388 | } | ||
389 | } | ||
390 | } | ||
391 | |||
392 | int icon_size; | ||
393 | cairo_surface_t *icon; | ||
394 | if (sni->icon) { | ||
395 | int actual_size = cairo_image_surface_get_height(sni->icon); | ||
396 | icon_size = actual_size < ideal_size ? | ||
397 | actual_size*(ideal_size/actual_size) : ideal_size; | ||
398 | icon = cairo_image_surface_scale(sni->icon, icon_size, icon_size); | ||
399 | } else { // draw a sad face | ||
400 | icon_size = ideal_size*0.8; | ||
401 | icon = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, icon_size, icon_size); | ||
402 | cairo_t *cairo_icon = cairo_create(icon); | ||
403 | cairo_set_source_u32(cairo_icon, 0xFF0000FF); | ||
404 | cairo_translate(cairo_icon, icon_size/2, icon_size/2); | ||
405 | cairo_scale(cairo_icon, icon_size/2, icon_size/2); | ||
406 | cairo_arc(cairo_icon, 0, 0, 1, 0, 7); | ||
407 | cairo_fill(cairo_icon); | ||
408 | cairo_set_operator(cairo_icon, CAIRO_OPERATOR_CLEAR); | ||
409 | cairo_arc(cairo_icon, 0.35, -0.3, 0.1, 0, 7); | ||
410 | cairo_fill(cairo_icon); | ||
411 | cairo_arc(cairo_icon, -0.35, -0.3, 0.1, 0, 7); | ||
412 | cairo_fill(cairo_icon); | ||
413 | cairo_arc(cairo_icon, 0, 0.75, 0.5, 3.71238898038469, 5.71238898038469); | ||
414 | cairo_set_line_width(cairo_icon, 0.1); | ||
415 | cairo_stroke(cairo_icon); | ||
416 | cairo_destroy(cairo_icon); | ||
417 | } | ||
418 | |||
419 | int padded_size = icon_size + 2*padding; | ||
420 | *x -= padded_size; | ||
421 | int y = floor((height - padded_size) / 2.0); | ||
422 | |||
423 | cairo_operator_t op = cairo_get_operator(cairo); | ||
424 | cairo_set_operator(cairo, CAIRO_OPERATOR_OVER); | ||
425 | cairo_set_source_surface(cairo, icon, *x + padding, y + padding); | ||
426 | cairo_rectangle(cairo, *x, y, padded_size, padded_size); | ||
427 | cairo_fill(cairo); | ||
428 | cairo_set_operator(cairo, op); | ||
429 | |||
430 | cairo_surface_destroy(icon); | ||
431 | |||
432 | struct swaybar_hotspot *hotspot = calloc(1, sizeof(struct swaybar_hotspot)); | ||
433 | hotspot->x = *x; | ||
434 | hotspot->y = 0; | ||
435 | hotspot->width = height; | ||
436 | hotspot->height = height; | ||
437 | hotspot->callback = icon_hotspot_callback; | ||
438 | hotspot->destroy = free; | ||
439 | hotspot->data = strdup(sni->watcher_id); | ||
440 | wl_list_insert(&output->hotspots, &hotspot->link); | ||
441 | |||
442 | return output->height; | ||
443 | } | ||