dwlb

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit f19f1105ed8220e81fa89884fcba1a841175242f
parent 78a53029bdaf30f92de695c0a7bf66be7d606361
Author: Janne Veteläinen <janne.vetelainen@elisanet.fi>
Date:   Fri, 22 Mar 2024 00:29:11 +0200

Add statusnotifieritem based systray

Diffstat:
M.gitignore | 4++++
MMakefile | 15+++++++++++++--
MREADME.md | 9+++++++++
AResources/DBusMenu.xml | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AResources/StatusNotifierItem.xml | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AResources/StatusNotifierWatcher.xml | 24++++++++++++++++++++++++
AResources/boxbg.css | 3+++
Adbusmenu.c | 307+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdwlb.c | 51++++++++++++++++++++++++++++++++++++++++++++++++++-
Adwlbtray.c | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adwlbtray.h | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatusnotifierhost.c | 435+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatusnotifieritem.c | 405+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 1581 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -9,3 +9,7 @@ xdg-output-unstable-v1-protocol.c xdg-output-unstable-v1-protocol.h dwl-ipc-unstable-v2-protocol.c dwl-ipc-unstable-v2-protocol.h +dwlbtray +valgrind* +.cache +compile_commands.json diff --git a/Makefile b/Makefile @@ -1,8 +1,10 @@ -BINS = dwlb +BINS = dwlb dwlbtray MANS = dwlb.1 +RESOURCES = Resources/DBusMenu.xml Resources/StatusNotifierItem.xml Resources/StatusNotifierWatcher.xml Resources/boxbg.css PREFIX ?= /usr/local -CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-format-truncation -g +CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-format-truncation -g \ + -DBUILD_DIR=\"$(shell pwd)/\" all: $(BINS) @@ -15,6 +17,7 @@ clean: install: all install -D -t $(PREFIX)/bin $(BINS) install -D -m0644 -t $(PREFIX)/share/man/man1 $(MANS) + install -D -m0644 -t $(PREFIX)/share/dwlb $(RESOURCES) WAYLAND_PROTOCOLS=$(shell pkg-config --variable=pkgdatadir wayland-protocols) WAYLAND_SCANNER=$(shell pkg-config --variable=wayland_scanner wayland-scanner) @@ -48,8 +51,16 @@ dwlb.o: utf8.h config.h xdg-shell-protocol.h xdg-output-unstable-v1-protocol.h w # Protocol dependencies dwlb: xdg-shell-protocol.o xdg-output-unstable-v1-protocol.o wlr-layer-shell-unstable-v1-protocol.o dwl-ipc-unstable-v2-protocol.o +statusnotifierhost.o: dwlbtray.h +statusnotifieritem.o: dwlbtray.h +dbusmenu.o: dwlbtray.h +dwlbtray.o: dwlbtray.h +dwlbtray: dwlbtray.o statusnotifierhost.o statusnotifieritem.o dbusmenu.o + # Library dependencies dwlb: CFLAGS+=$(shell pkg-config --cflags wayland-client wayland-cursor fcft pixman-1) dwlb: LDLIBS+=$(shell pkg-config --libs wayland-client wayland-cursor fcft pixman-1) -lrt +dwlbtray: CFLAGS+=$(shell pkg-config --cflags glib-2.0 gtk4 gtk4-layer-shell-0) +dwlbtray: LDLIBS+=$(shell pkg-config --libs glib-2.0 gtk4 gtk4-layer-shell-0) -lrt .PHONY: all clean install diff --git a/README.md b/README.md @@ -3,6 +3,8 @@ A fast, feature-complete bar for [dwl](https://github.com/djpohly/dwl). +This fork adds the systemtray feature implementing KDE's KStatusNotifierItem spec + ![screenshot 1](/screenshot1.png "screenshot 1") ![screenshot 2](/screenshot2.png "screenshot 2") </div> @@ -12,6 +14,8 @@ A fast, feature-complete bar for [dwl](https://github.com/djpohly/dwl). * libwayland-cursor * pixman * fcft +* gtk4 +* [gtk4-layer-shell](https://github.com/wmww/gtk4-layer-shell) ## Installation ```bash @@ -27,6 +31,11 @@ Pass `dwlb` as an argument to dwl's `-s` flag. This will populate each connected dwl -s 'dwlb -font "monospace:size=16"' ``` +To specify on which monitor the systray will appear, append option `-traymon [OUTPUT]` to the command line: +```bash +dwl -s 'dwlb -font "monospace:size=16" -traymon DP-1 +``` + ## Ipc If dwl is [patched](https://github.com/djpohly/dwl/wiki/ipc) appropriately, dwlb is capable of communicating directly with dwl. When ipc is enabled with `-ipc`, dwlb does not read from stdin, and clicking tags functions as you would expect. Ipc can be disabled with `-no-ipc`. diff --git a/Resources/DBusMenu.xml b/Resources/DBusMenu.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<node> + <interface name="com.canonical.dbusmenu"> + + <!-- methods --> + <method name="GetLayout"> + <arg type="i" name="parentId" direction="in"/> + <arg type="i" name="recursionDepth" direction="in"/> + <arg type="as" name="propertyNames" direction="in"/> + <arg type="u" name="revision" direction="out"/> + <arg type="(ia{sv}av)" name="layout" direction="out"/> + </method> + <method name="Event"> + <arg type="i" name="id" direction="in"/> + <arg type="s" name="eventId" direction="in"/> + <arg type="v" name="data" direction="in"/> + <arg type="u" name="timestamp" direction="in"/> + </method> + <method name="AboutToShow"> + <arg type="i" name="id" direction="in"/> + <arg type="b" name="needUpdate" direction="out"/> + </method> + <!-- + <method name="AboutToShowGroup"> + <arg type="ai" name="ids" direction="in"/> + <arg type="ai" name="updatesNeeded" direction="out"/> + <arg type="ai" name="idErrors" direction="out"/> + </method> + <method name="GetGroupProperties"> + <arg type="ai" name="ids" direction="in"/> + <arg type="as" name="propertyNames" direction="in"/> + <arg type="a(ia{sv})" name="properties" direction="out"/> + </method> + <method name="GetProperty"> + <arg type="i" name="id" direction="in"/> + <arg type="s" name="name" direction="in"/> + <arg type="v" name="value" direction="out"/> + </method> + <method name="EventGroup"> + <arg type="a(isvu)" name="events" direction="in"/> + <arg type="ai" name="idErrors" direction="out"/> + </method> + --> + + <!-- properties --> + <!-- + <property name="Version" type="u" access="read"/> + <property name="TextDirection" type="s" access="read"/> + <property name="Status" type="s" access="read"/> + <property name="IconThemePath" type="as" access="read"/> + --> + + <!-- Signals --> + <signal name="ItemsPropertiesUpdated"> + <arg type="a(ia{sv})" name="updatedProps" direction="out"/> + <arg type="a(ias)" name="removedProps" direction="out"/> + </signal> + <signal name="LayoutUpdated"> + <arg type="u" name="revision" direction="out"/> + <arg type="i" name="parent" direction="out"/> + </signal> + <!-- + <signal name="ItemActivationRequested"> + <arg type="i" name="id" direction="out"/> + <arg type="u" name="timestamp" direction="out"/> + </signal> + --> + + </interface> +</node> diff --git a/Resources/StatusNotifierItem.xml b/Resources/StatusNotifierItem.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<node> + <interface name="org.kde.StatusNotifierItem"> + + <!-- methods --> + <method name="Activate"> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + <!-- + <method name="Scroll"> + <arg name="delta" type="i" direction="in"/> + <arg name="orientation" type="s" direction="in"/> + </method> + <method name="ContextMenu"> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + <method name="SecondaryActivate"> + <arg name="x" type="i" direction="in"/> + <arg name="y" type="i" direction="in"/> + </method> + --> + + <!-- properties --> + <property name="Menu" type="o" access="read"/> + <property name="IconName" type="s" access="read"/> + <property name="IconPixmap" type="a(iiay)" access="read"/> + <property name="IconThemePath" type="s" access="read"/> + <!-- + <property name="OverlayIconName" type="s" access="read"/> + <property name="OverlayIconPixmap" type="a(iiay)" access="read"/> + <property name="AttentionIconName" type="s" access="read"/> + <property name="AttentionIconPixmap" type="a(iiay)" access="read"/> + <property name="Category" type="s" access="read"/> + <property name="Id" type="s" access="read"/> + <property name="Title" type="s" access="read"/> + <property name="Status" type="s" access="read"/> + <property name="WindowId" type="i" access="read"/> + <property name="ItemIsMenu" type="b" access="read"/> + <property name="AttentionMovieName" type="s" access="read"/> + <property name="ToolTip" type="(sa(iiay)ss)" access="read"/> + --> + + <!-- signals --> + <signal name="NewIcon"/> + <!-- + <signal name="NewAttentionIcon"/> + <signal name="NewOverlayIcon"/> + <signal name="NewTitle"/> + <signal name="NewToolTip"/> + <signal name="NewStatus"> + <arg name="status" type="s"/> + </signal> + --> + + </interface> +</node> diff --git a/Resources/StatusNotifierWatcher.xml b/Resources/StatusNotifierWatcher.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<node> + <interface name="org.kde.StatusNotifierWatcher"> + <!-- methods --> + <method name="RegisterStatusNotifierItem"> + <arg name="service" type="s" direction="in" /> + </method> + + <!-- properties --> + <property name="RegisteredStatusNotifierItems" type="as" access="read" /> + <property name="IsStatusNotifierHostRegistered" type="b" access="read" /> + <property name="ProtocolVersion" type="i" access="read" /> + + <!-- signals --> + <signal name="StatusNotifierItemRegistered"> + <arg type="s"/> + </signal> + <signal name="StatusNotifierItemUnregistered"> + <arg type="s"/> + </signal> + <signal name="StatusNotifierHostRegistered"> + </signal> + </interface> +</node> diff --git a/Resources/boxbg.css b/Resources/boxbg.css @@ -0,0 +1,3 @@ +window { + background-color: #222222; +} diff --git a/dbusmenu.c b/dbusmenu.c @@ -0,0 +1,307 @@ +#include <time.h> + +#include <glib.h> +#include <gio/gio.h> + +#include "dwlbtray.h" + +static GMenu* create_menumodel(GVariant *data, StatusNotifierItem *snitem); + + +static void +action_activated_cb(GSimpleAction *action, GVariant* param, ActionCallbackData *data) +{ + // GError *err = NULL; + + g_dbus_proxy_call(data->proxy, + "Event", + g_variant_new("(isvu)", + data->id, + "clicked", + g_variant_new_string(""), + time(NULL)), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + NULL); + /* + GVariant *retval = g_dbus_proxy_call_sync(data->proxy, + "Event", + g_variant_new("(isvu)", + data->id, + "clicked", + g_variant_new_string(""), + time(NULL)), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + &err); + + if (err) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + } else { + g_variant_unref(retval); + } + */ +} + + +static GSimpleAction* +create_action(uint32_t id, StatusNotifierItem *snitem) +{ + char *action_name = g_strdup_printf("%u", id); + GSimpleAction *action = g_simple_action_new(action_name, NULL); + + ActionCallbackData *data = g_malloc(sizeof(ActionCallbackData)); + data->id = id; + data->proxy = snitem->menuproxy; + snitem->action_cb_data_slist = g_slist_prepend(snitem->action_cb_data_slist, data); + + g_signal_connect(action, "activate", G_CALLBACK(action_activated_cb), data); + + g_free(action_name); + + return action; +} + + +static GMenuItem* +create_menuitem(GVariant *data, StatusNotifierItem *snitem) +{ + // (ia{sv}av) + // GVariant *data + GMenuItem *menuitem = NULL; + + int32_t id; + // a{sv] + GVariant *menu_data; + + g_variant_get_child(data, 0, "i", &id); + menu_data = g_variant_get_child_value(data, 1); + + const char *label = NULL; + const char *type = NULL; + gboolean isenabled = TRUE; + gboolean isvisible = TRUE; + gboolean has_submenu = FALSE; + + /* TODO: dynamic menu updates + * gboolean ischeckmark = FALSE; + * gboolean isradio = FALSE; + * int32_t toggle_state = 99; + * const char *toggle_type = NULL; + */ + + const char *has_submenu_s = NULL; + GVariantDict dict; + g_variant_dict_init(&dict, menu_data); + g_variant_dict_lookup(&dict, "label", "&s", &label); + g_variant_dict_lookup(&dict, "type", "&s", &type); + g_variant_dict_lookup(&dict, "enabled", "b", &isenabled); + g_variant_dict_lookup(&dict, "visible", "b", &isvisible); + g_variant_dict_lookup(&dict, "children-display", "&s", &has_submenu_s); + /* TODO: dynamic menu updates + * g_variant_dict_lookup(&dict, "toggle-type", "&s", &toggle_type); + * g_variant_dict_lookup(&dict, "toggle-state", "i", &toggle_state); + */ + g_variant_dict_clear(&dict); + + if (has_submenu_s && strcmp(has_submenu_s, "submenu") == 0) + has_submenu = TRUE; + + /* TODO: dynamic menu updates + * if (toggle_type && strcmp(toggle_type, "checkmark") == 0) + * ischeckmark = TRUE; + * else if (toggle_type && strcmp(toggle_type, "radio") == 0) + * isradio = TRUE; + */ + + if ((label && isvisible && isenabled) && !(type && strcmp(type, "separator") == 0)) { + GSimpleAction *action = create_action(id, snitem); + char *action_name = g_strdup_printf("%s.%u", "menuitem", id); + g_action_map_add_action(G_ACTION_MAP(snitem->actiongroup), + G_ACTION(action)); + menuitem = g_menu_item_new(label, action_name); + + g_free(action_name); + g_object_unref(action); + } else if ((label && (!isvisible || !isenabled)) && !(type && strcmp(type, "separator") == 0)) { + if (!isvisible) + g_debug("menuitem %i, label %s should be invisible\n", id, label); + GSimpleAction *action = create_action(id, snitem); + g_simple_action_set_enabled(action, FALSE); + char *action_name = g_strdup_printf("%s.%u", "menuitem", id); + g_action_map_add_action(G_ACTION_MAP(snitem->actiongroup), + G_ACTION(action)); + menuitem = g_menu_item_new(label, action_name); + + g_free(action_name); + g_object_unref(action); + } + + if (has_submenu) { + GVariant *submenu_data = g_variant_get_child_value(data, 2); + GMenu *submenu = create_menumodel(submenu_data, snitem); + g_menu_item_set_submenu(menuitem, G_MENU_MODEL(submenu)); + g_object_unref(submenu); + g_variant_unref(submenu_data); + } + + g_variant_unref(menu_data); + + return menuitem; +} + + +static GMenu* +create_menumodel(GVariant *data, StatusNotifierItem *snitem) +{ + GMenu *ret = NULL; + + GVariantIter iter; + GVariant *menuitem_data; + gboolean has_sections = FALSE; + GMenu *menu = g_menu_new(); + GMenu *section = g_menu_new(); + g_variant_iter_init(&iter, data); + + while ((g_variant_iter_next(&iter, "v", &menuitem_data))) { + GMenuItem *menuitem = create_menuitem(menuitem_data, snitem); + if (menuitem) { + g_menu_append_item(section, menuitem); + g_object_unref(menuitem); + } + else { + g_menu_append_section(menu, NULL, G_MENU_MODEL(section)); + g_object_unref(section); + has_sections = TRUE; + section = g_menu_new(); + } + + g_variant_unref(menuitem_data); + } + + if (has_sections) { + g_menu_append_section(menu, NULL, G_MENU_MODEL(section)); + ret = menu; + g_object_unref(section); + } else { + ret = section; + g_object_unref(menu); + } + + return ret; +} + + +static void +on_menulayout_ready(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + GVariant *data = g_dbus_proxy_call_finish(proxy, res, &err); + // (u(ia{sv}av)) + + if (err) { + // Sometimes apps send update messages just as they are closing + // So the dbus object might not exist in this case + g_debug("%s\n", err->message); + g_error_free(err); + + return; + } + + + uint32_t revision; + GVariant *layout; + // (ia{sv}av) + // uint32_t GVariant * GVariant * + // 0, {"children-display": "submenu"}, {GVariant *menuitem, GVariant *menuitem ...} + GVariant *menuitems; + // (ia{sv}av) + // uint32_t GVariant * GVariant * + // 1, {"label": "foobar", ...}, {GVariant *menuitem, GVariant *menuitem ...} + + g_variant_get_child(data, 0, "u", &revision); + if (snitem->menurevision != UINT32_MAX && revision <= snitem->menurevision) { + g_variant_unref(data); + return; + } else { + snitem->menurevision = revision; + } + + if (snitem->menu && snitem->popovermenu) { + gtk_widget_unparent(snitem->popovermenu); + g_menu_remove_all(snitem->menu); + } + layout = g_variant_get_child_value(data, 1); + + menuitems = g_variant_get_child_value(layout, 2); + + GMenu *menu = create_menumodel(menuitems, snitem); + snitem->menu = menu; + + snitem->popovermenu = gtk_popover_menu_new_from_model(G_MENU_MODEL(snitem->menu)); + gtk_popover_set_has_arrow(GTK_POPOVER(snitem->popovermenu), FALSE); + gtk_popover_menu_set_menu_model(GTK_POPOVER_MENU(snitem->popovermenu), + G_MENU_MODEL(snitem->menu)); + gtk_widget_set_parent(snitem->popovermenu, snitem->icon); + + g_variant_unref(menuitems); + g_variant_unref(layout); + g_variant_unref(data); +} + + +static void +on_menuproxy_signal(GDBusProxy *proxy, + const char *sender, + const char *signal, + GVariant *params, + StatusNotifierItem *snitem) +{ + if (strcmp(signal, "LayoutUpdated") == 0) { + g_debug("%s's menu got LayoutUpdated\n", snitem->busname); + g_dbus_proxy_call(snitem->menuproxy, + "GetLayout", + g_variant_new("(iias)", 0, -1, NULL), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback)on_menulayout_ready, + snitem); + // TODO: dynamic menu updates + } else if (strcmp(signal, "ItemsPropertiesUpdated") == 0) { + g_debug("%s's menu got LayoutUpdated\n", snitem->busname); + } +} + + +void +create_menu(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + snitem->menuproxy = g_dbus_proxy_new_for_bus_finish(res, &err); + snitem->menurevision = UINT32_MAX; + + if (err) { + g_debug("%s\n", err->message); + g_error_free(err); + return; + } + + g_dbus_proxy_call(snitem->menuproxy, + "GetLayout", + g_variant_new ("(iias)", 0, -1, NULL), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback)on_menulayout_ready, + snitem); + + g_signal_connect(proxy, + "g-signal", + G_CALLBACK (on_menuproxy_signal), + snitem); +} diff --git a/dwlb.c b/dwlb.c @@ -109,6 +109,7 @@ " -set-top [OUTPUT] draw bar at the top\n" \ " -set-bottom [OUTPUT] draw bar at the bottom\n" \ " -toggle-location [OUTPUT] toggle bar location\n" \ + " -traymon [OUTPUT] set monitor name where systray will appear\n" \ "Other\n" \ " -v get version information\n" \ " -h view this help text\n" @@ -150,6 +151,7 @@ typedef struct { bool configured; uint32_t width, height; + uint32_t width_orig; uint32_t textpadding; uint32_t stride, bufsize; @@ -497,10 +499,11 @@ layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, Bar *bar = (Bar *)data; - if (bar->configured && w == bar->width && h == bar->height) + if (bar->configured && w == bar->width_orig && h == bar->height) return; bar->width = w; + bar->width_orig = w; bar->height = h; bar->stride = bar->width * 4; bar->bufsize = bar->stride * bar->height; @@ -1427,6 +1430,17 @@ copy_customtext(CustomText *from, CustomText *to) } static void +request_resize(Bar *bar, char *data) +{ + uint32_t traywidth = (uint32_t)atoi(data); + + bar->width = bar->width_orig - traywidth; + bar->stride = bar->width * 4; + bar->bufsize = bar->stride * bar->height; + bar->redraw = true; +} + +static void read_socket(void) { int cli_fd; @@ -1571,6 +1585,8 @@ read_socket(void) else set_bottom(bar); } + } else if (!strcmp(wordbeg, "resize")) { + request_resize(bar, wordend); } } @@ -1669,6 +1685,7 @@ main(int argc, char **argv) struct sockaddr_un sock_address; Bar *bar, *bar2; Seat *seat, *seat2; + char *traymon = NULL; /* Establish socket directory */ if (!(xdgruntimedir = getenv("XDG_RUNTIME_DIR"))) @@ -1848,6 +1865,10 @@ main(int argc, char **argv) if (++i >= argc) DIE("Option -scale requires an argument"); buffer_scale = strtoul(argv[i], &argv[i] + strlen(argv[i]), 10); + } else if (!strcmp(argv[i], "-traymon")) { + if (++i >= argc) + DIE("Option -traymon requires an argument"); + traymon = argv[i]; } else if (!strcmp(argv[i], "-v")) { fprintf(stderr, PROGRAM " " VERSION "\n"); return 0; @@ -1940,6 +1961,34 @@ main(int argc, char **argv) signal(SIGTERM, sig_handler); signal(SIGCHLD, SIG_IGN); + /* Start tray program */ + char progname[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", progname, sizeof(progname)); + + if (len != -1) + progname[len] = '\0'; + else + exit(-1); + + char tray_exe_path[PATH_MAX]; + if (strncmp(progname, BUILD_DIR, strlen(BUILD_DIR)) == 0) { + strcpy(tray_exe_path, BUILD_DIR); + strcat(tray_exe_path, "dwlbtray"); + } else { + strcpy(tray_exe_path, "dwlbtray"); + } + int child_pid = fork(); + if (child_pid == 0) { + char height_param[64]; + char traymon_param[64]; + snprintf(height_param, sizeof(height_param), "--height=%u", height); + snprintf(traymon_param, sizeof(traymon_param), "--traymon=%s", traymon); + char *args[] = { tray_exe_path, height_param, traymon_param, NULL}; + if (!traymon) + args[2] = NULL; + execvp(args[0], args); + } + /* Run */ run_display = true; event_loop(); diff --git a/dwlbtray.c b/dwlbtray.c @@ -0,0 +1,140 @@ +#include <stdlib.h> +#include <unistd.h> +// #include <limits.h> + +#include <glib.h> +#include <gtk4-layer-shell.h> +#include <gtk/gtk.h> +// #include "glib-unix.h" + +#include "dwlbtray.h" + + +const char *RESOURCE_PATH; + + +static void +activate(GtkApplication* app, StatusNotifierHost *snhost) +{ + GtkWindow *window = GTK_WINDOW(gtk_application_window_new(app)); + snhost->window = window; + + gtk_layer_init_for_window(window); + gtk_layer_set_layer(window, GTK_LAYER_SHELL_LAYER_BOTTOM); + gtk_layer_set_exclusive_zone(window, -1); + static const gboolean anchors[] = {FALSE, TRUE, TRUE, FALSE}; + for (int i = 0; i < GTK_LAYER_SHELL_EDGE_ENTRY_NUMBER; i++) { + gtk_layer_set_anchor(window, i, anchors[i]); + } + + if (snhost->traymon) { + GListModel *mons = gdk_display_get_monitors(gdk_display_get_default()); + for (uint i = 0; i < g_list_model_get_n_items(mons); i++) { + GdkMonitor *mon = g_list_model_get_item(mons, i); + const char *conn = gdk_monitor_get_connector(mon); + if (strcmp(conn, snhost->traymon) == 0) { + gtk_layer_set_monitor(window, mon); + } + } + } + + GtkCssProvider *css = gtk_css_provider_new(); + char *css_path = g_strdup_printf("%s%s", RESOURCE_PATH, "/boxbg.css"); + gtk_css_provider_load_from_path(css, css_path); + gtk_style_context_add_provider_for_display(gdk_display_get_default(), + GTK_STYLE_PROVIDER(css), + GTK_STYLE_PROVIDER_PRIORITY_USER); + g_free(css_path); + + GtkWidget *box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); + + gtk_widget_set_vexpand(box, TRUE); + gtk_widget_set_hexpand(box, TRUE); + gtk_widget_set_margin_start(box, snhost->margin); + gtk_widget_set_margin_end(box, snhost->margin); + + gtk_window_set_default_size(GTK_WINDOW(window), 22, snhost->height); + dwlb_request_resize(snhost); + + snhost->box = box; + + gtk_window_set_child(window, box); + + gtk_window_present(window); + +} + + +/* + * gboolean + * terminate_app(StatusNotifierHost *snhost) + * { + * terminate_statusnotifierhost(snhost); + * + * GApplication *app = g_application_get_default(); + * g_application_release(app); + * + * return G_SOURCE_REMOVE; + * } + */ + +int +main(int argc, char *argv[]) +{ + char progname[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", progname, sizeof(progname)); + + if (len != -1) { + progname[len] = '\0'; + } + else { + fprintf(stderr, "bad progname, exiting\n"); + exit(-1); + } + + if (strncmp(progname, BUILD_DIR, strlen(BUILD_DIR)) == 0) { + RESOURCE_PATH = g_strdup_printf("%s%s", BUILD_DIR, "Resources"); + } else { + const char * const *datadirs = g_get_system_data_dirs(); + int i; + + for (i = 0; datadirs[i]; i++) { + char *test = g_build_filename(datadirs[i], "dwlb", "boxbg.css", NULL); + if (g_file_test(test, G_FILE_TEST_EXISTS)) + RESOURCE_PATH = g_path_get_dirname(test); + } + } + + if (!RESOURCE_PATH) + RESOURCE_PATH = "/usr/local/dwlb"; + + StatusNotifierHost *snhost = start_statusnotifierhost(); + + int i = 1; + for (; i < argc; i++) { + + char **strings = g_strsplit(argv[i], "=", 0); + if (strcmp(strings[0], "--height") == 0) { + snhost->height = atoi(strings[1]); + } else if (strcmp(strings[0], "--traymon") == 0) { + snhost->traymon = g_strdup(strings[1]); + } + g_strfreev(strings); + } + + GtkApplication *app = gtk_application_new("com.vetu104.Gtktray", + G_APPLICATION_DEFAULT_FLAGS); + + g_signal_connect(app, "activate", G_CALLBACK(activate), snhost); + + /* + * g_unix_signal_add(SIGINT, (GSourceFunc)terminate_app, snhost); + * g_unix_signal_add(SIGTERM, (GSourceFunc)terminate_app, snhost); + */ + + char *argv_inner[] = { progname, NULL }; + int status = g_application_run(G_APPLICATION(app), 1, argv_inner); + + g_object_unref(app); + return status; +} diff --git a/dwlbtray.h b/dwlbtray.h @@ -0,0 +1,63 @@ +#ifndef GTKTRAY_H +#define GTKTRAY_H + +#include <glib.h> +#include <gio/gio.h> +#include <gtk/gtk.h> + +extern const char *RESOURCE_PATH; + +typedef struct { + uint32_t id; + GDBusProxy* proxy; +} ActionCallbackData; + + +typedef struct StatusNotifierHost { + GDBusConnection *conn; + GDBusNodeInfo *nodeinfo; + GDBusProxy *watcherproxy; + GSList *trayitems; + GtkWidget *box; + GtkWindow *window; + int bus_obj_reg_id; + int cursize; + int height; + int margin; + int noitems; + int owner_id; + uint nameowner_sig_sub_id; + uint watcher_id; + char *traymon; +} StatusNotifierHost; + +typedef struct StatusNotifierItem { + GDBusNodeInfo *menunodeinfo; + GDBusNodeInfo *nodeinfo; + GDBusProxy *menuproxy; + GDBusProxy *proxy; + GMenu *menu; + GSList *action_cb_data_slist; + GSimpleActionGroup *actiongroup; + GVariant *iconpixmap_v; + GdkPaintable *paintable; + GtkWidget *icon; + GtkWidget *popovermenu; + StatusNotifierHost *host; + char *busname; + char *busobj; + char *iconname; + char *menuobj; + uint32_t menurevision; + unsigned char *icon_data; +} StatusNotifierItem; + + +void create_trayitem(GDBusConnection *conn, GAsyncResult *res, StatusNotifierItem *snitem); +void create_menu(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem); +StatusNotifierHost* start_statusnotifierhost(); +// void terminate_statusnotifierhost(StatusNotifierHost *snhost); +GDBusNodeInfo* get_interface_info(const char *xmlpath); +void dwlb_request_resize(StatusNotifierHost *snhost); + +#endif /* STATUSNOTIFIERHOST_H */ diff --git a/statusnotifierhost.c b/statusnotifierhost.c @@ -0,0 +1,435 @@ +#include <sys/types.h> +#include <sys/socket.h> +#include <sys/un.h> + +#include <glib.h> +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <gtk4-layer-shell.h> + +#include "dwlbtray.h" + + +static void handle_method_call(GDBusConnection* conn, const char* sender, + const char* object_path, const char* iface, const char* method, + GVariant* parameters, GDBusMethodInvocation* invocation, + StatusNotifierHost* snhost +); + + +static GVariant* handle_get_prop(GDBusConnection* conn, const char* sender, + const char* obj_path, const char* iface_name, + const char* prop, GError** error, StatusNotifierHost* snhost +); + + +static GDBusInterfaceVTable interface_vtable = { + (GDBusInterfaceMethodCallFunc)handle_method_call, + (GDBusInterfaceGetPropertyFunc)handle_get_prop, + NULL +}; + + +void +dwlb_request_resize(StatusNotifierHost *snhost) +{ + if (snhost->noitems <= 1) + snhost->cursize = 22; + else + snhost->cursize = 22 * snhost->noitems - 6; // dunno why substract 6 to make it align, just trial and error until it worked + + struct sockaddr_un sockaddr; + sockaddr.sun_family = AF_UNIX; + char *socketpath = g_strdup_printf("%s/dwlb/dwlb-0", g_get_user_runtime_dir()); + snprintf(sockaddr.sun_path, sizeof(sockaddr.sun_path), "%s", socketpath); + char *sockbuf = NULL; + if (snhost->traymon) { + sockbuf = g_strdup_printf("%s %s %i", snhost->traymon, "resize", snhost->cursize); + } + else { + sockbuf = g_strdup_printf("%s %s %i", "selected", "resize", snhost->cursize); + } + + size_t len = strlen(sockbuf); + int sock_fd = socket(AF_UNIX, SOCK_STREAM, 1); + + int connstatus = connect(sock_fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr)); + if (connstatus != 0) { + g_error("Error connecting to dwlb socket"); + } + + if (send(sock_fd, sockbuf, len, 0) == -1) + fprintf(stderr, "Could not send size update to %s\n", sockaddr.sun_path); + close(sock_fd); + + g_free(sockbuf); + g_free(socketpath); +} + +GDBusNodeInfo* +get_interface_info(const char *xmlpath) +{ + char *contents; + GError *err = NULL; + + g_file_get_contents(xmlpath, &contents, NULL, &err); + + if (err) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + return NULL; + } + + GDBusNodeInfo *node = g_dbus_node_info_new_for_xml(contents, &err); + g_free(contents); + + if (err) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + return NULL; + } + + return node; +} + + + +static void +register_statusnotifieritem(const char *busname, + const char *busobj, + StatusNotifierHost *snhost) +{ + StatusNotifierItem *snitem; + snitem = g_malloc(sizeof(StatusNotifierItem)); + + snitem->busname = g_strdup(busname); + snitem->busobj = g_strdup(busobj); + + snitem->host = snhost; + + char *xml_path = g_strdup_printf("%s%s", RESOURCE_PATH, "/StatusNotifierItem.xml"); + snitem->nodeinfo = get_interface_info(xml_path); + g_free(xml_path); + + snitem->action_cb_data_slist = NULL; + snitem->actiongroup = NULL; + snitem->icon = NULL; + snitem->iconname = NULL; + snitem->iconpixmap_v = NULL; + snitem->menu = NULL; + snitem->menunodeinfo = NULL; + snitem->menuobj = NULL; + snitem->menuproxy = NULL; + snitem->paintable = NULL; + snitem->popovermenu = NULL; + + snhost->noitems = snhost->noitems + 1; + + dwlb_request_resize(snhost); + + snhost->trayitems = g_slist_prepend(snhost->trayitems, snitem); + + g_dbus_proxy_new(snhost->conn, + G_DBUS_PROXY_FLAGS_NONE, + snitem->nodeinfo->interfaces[0], + snitem->busname, + snitem->busobj, + "org.kde.StatusNotifierItem", + NULL, + (GAsyncReadyCallback)create_trayitem, + snitem); + + GError *err = NULL; + g_dbus_connection_emit_signal(snhost->conn, + NULL, + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierItemRegistered", + g_variant_new("(s)", snitem->busname), + &err); + if (err) { + g_debug("%s\n", err->message); + g_error_free(err); + } +} + + +static int +find_snitem(StatusNotifierItem *snitem, char *busname_match) +{ + if (strcmp(snitem->busname, busname_match) == 0) + return 0; + else + return -1; +} + + +static void +unregister_statusnotifieritem(StatusNotifierItem *snitem, StatusNotifierHost *snhost) +{ + g_debug("item %s is closing\n", snitem->busname); + if (snitem->popovermenu) + gtk_widget_unparent(snitem->popovermenu); + + if (snitem->icon) + gtk_box_remove(GTK_BOX(snhost->box), snitem->icon); + + GError *err = NULL; + g_dbus_connection_emit_signal(snhost->conn, + NULL, + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierItemUnregistered", + g_variant_new("(s)", snitem->busname), + &err); + if (err) { + g_debug("%s\n", err->message); + g_error_free(err); + } + + if (snitem->menu) { + g_object_unref(snitem->menuproxy); + g_slist_free_full(snitem->action_cb_data_slist, g_free); + g_object_unref(snitem->menu); + } + + g_object_unref(snitem->paintable); + if (snitem->iconpixmap_v) + g_variant_unref(snitem->iconpixmap_v); + + g_object_unref(snitem->proxy); + g_object_unref(snitem->actiongroup); + g_dbus_node_info_unref(snitem->menunodeinfo); + g_dbus_node_info_unref(snitem->nodeinfo); + g_free(snitem->busname); + g_free(snitem->busobj); + g_free(snitem->menuobj); + snhost->trayitems = g_slist_remove(snhost->trayitems, snitem); + g_free(snitem); + snitem = NULL; + + snhost->noitems = snhost->noitems - 1; + dwlb_request_resize(snhost); +} + + +/* + * static void + * unregister_all(StatusNotifierHost *snhost) + * { + * g_slist_foreach(snhost->trayitems, (GFunc)unregister_statusnotifieritem, snhost); + * } +*/ + + +static void +handle_method_call(GDBusConnection *conn, + const char *sender, + const char *object_path, + const char *interface_name, + const char *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + StatusNotifierHost *snhost) +{ + if (strcmp(method_name, "RegisterStatusNotifierItem") == 0) { + char *param; + const char *busobj; + + g_variant_get(parameters, "(s)", &param); + + if (g_str_has_prefix(param, "/")) { + busobj = param; + } else { + busobj = "/StatusNotifierItem"; + } + + register_statusnotifieritem(sender, busobj, snhost); + g_dbus_method_invocation_return_value(invocation, NULL); + g_free(param); + } else { + g_dbus_method_invocation_return_dbus_error(invocation, + "org.freedesktop.DBus.Error.UnknownMethod", + "Unknown method"); + } +} + + +static void +add_trayitem_name_to_builder(StatusNotifierItem *snitem, GVariantBuilder *builder) +{ + g_variant_builder_add_value(builder, g_variant_new_string(snitem->busname)); +} + +static GVariant* +handle_get_prop(GDBusConnection* conn, + const char* sender, + const char* object_path, + const char* interface_name, + const char* property_name, + GError** err, + StatusNotifierHost *snhost) +{ + if (strcmp(property_name, "ProtocolVersion") == 0) { + return g_variant_new("i", 0); + } else if (strcmp(property_name, "IsStatusNotifierHostRegistered") == 0) { + return g_variant_new("b", TRUE); + } else if (strcmp(property_name, "RegisteredStatusNotifierItems") == 0) { + if (!snhost->trayitems) + return g_variant_new("as", NULL); + + GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY); + g_slist_foreach(snhost->trayitems, (GFunc)add_trayitem_name_to_builder, builder); + GVariant *as = g_variant_builder_end(builder); + + g_variant_builder_unref(builder); + + return as; + } else { + g_set_error(err, + G_DBUS_ERROR, + G_DBUS_ERROR_UNKNOWN_PROPERTY, + "Unknown property '%s'.", + property_name); + g_error_free(*err); + return NULL; + } +} + +/* + * void + * terminate_statusnotifierhost(StatusNotifierHost *snhost) + * { + * g_dbus_connection_signal_unsubscribe(snhost->conn, snhost->nameowner_sig_sub_id); + * g_bus_unown_name(snhost->owner_id); + * unregister_all(snhost); + * g_slist_free(snhost->trayitems); + * g_dbus_node_info_unref(snhost->nodeinfo); + * g_free(snhost); + * } + */ + +// Finds trayitems which dropped from the bus and untracks them +static void +monitor_bus(GDBusConnection* conn, + const char* sender, + const char* objpath, + const char* iface_name, + const char* signame, + GVariant *params, + StatusNotifierHost *snhost) +{ + if (strcmp(signame, "NameOwnerChanged") == 0) { + if (!snhost->trayitems) + return; + + char *name; + char *old_owner; + char *new_owner; + + g_variant_get(params, "(sss)", &name, &old_owner, &new_owner); + + if (strcmp(new_owner, "") == 0) { + GSList *pmatch = g_slist_find_custom(snhost->trayitems, name, (GCompareFunc)find_snitem); + + + if (!pmatch) { + g_free(name); + g_free(old_owner); + g_free(new_owner); + return; + } + + StatusNotifierItem *snitem = pmatch->data; + unregister_statusnotifieritem(snitem, snhost); + } + g_free(name); + g_free(old_owner); + g_free(new_owner); + } +} + + +static void +bus_acquired_handler(GDBusConnection *conn, const char *busname, StatusNotifierHost *snhost) +{ + snhost->conn = conn; + GError *err = NULL; + snhost->bus_obj_reg_id = g_dbus_connection_register_object(conn, + "/StatusNotifierWatcher", + snhost->nodeinfo->interfaces[0], + &interface_vtable, + snhost, // udata + NULL, // udata_free_func + &err); + + if (err) { + g_error("%s\n", err->message); + g_error_free(err); + exit(-1); + } + + snhost->nameowner_sig_sub_id = g_dbus_connection_signal_subscribe(conn, + NULL, // Listen to all senders); + "org.freedesktop.DBus", + "NameOwnerChanged", + NULL, // Match all obj paths + NULL, // Match all arg0s + G_DBUS_SIGNAL_FLAGS_NONE, + (GDBusSignalCallback)monitor_bus, + snhost, // udata + NULL); // udata free func +} + + +static void +name_acquired_handler(GDBusConnection *conn, const char *busname, StatusNotifierHost *snhost) +{ + GError *err = NULL; + + g_dbus_connection_emit_signal(conn, + NULL, + "/StatusNotifierWatcher", + "org.kde.StatusNotifierWatcher", + "StatusNotifierHostRegistered", + NULL, + &err); + + if (err) { + g_debug("%s\n", err->message); + g_error_free(err); + } +} + + +static void +name_lost_handler(GDBusConnection *conn, const char *busname, StatusNotifierHost *snhost) +{ + g_error("Could not acquire %s\n", busname); + exit(-1); +} + + +StatusNotifierHost* +start_statusnotifierhost() +{ + StatusNotifierHost *snhost = g_malloc0(sizeof(StatusNotifierHost)); + char *xml_path = g_strdup_printf("%s%s", RESOURCE_PATH, "/StatusNotifierWatcher.xml"); + snhost->nodeinfo = get_interface_info(xml_path); + g_free(xml_path); + + snhost->height = 22; + snhost->margin = 4; + snhost->noitems = 0; + + snhost->owner_id = g_bus_own_name(G_BUS_TYPE_SESSION, + "org.kde.StatusNotifierWatcher", + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusAcquiredCallback)bus_acquired_handler, + (GBusNameAcquiredCallback)name_acquired_handler, + (GBusNameLostCallback)name_lost_handler, + snhost, + NULL); // (GDestroyNotify)snhost_finalize); + + return snhost; +} diff --git a/statusnotifieritem.c b/statusnotifieritem.c @@ -0,0 +1,405 @@ +#include <glib.h> +#include <gio/gio.h> +#include <gtk/gtk.h> +#include <gtk4-layer-shell.h> + +#include "dwlbtray.h" + + +static void +argb_to_rgba(int32_t width, int32_t height, unsigned char *icon_data) +{ + // Icon data is ARGB, gdk textures are RGBA. Flip the channels + // Shamelessly copied from Waybar + for (int32_t i = 0; i < 4 * width * height; i += 4) { + unsigned char alpha = icon_data[i]; + icon_data[i] = icon_data[i + 1]; + icon_data[i + 1] = icon_data[i + 2]; + icon_data[i + 2] = icon_data[i + 3]; + icon_data[i + 3] = alpha; + } +} + + +static void +about_to_show_cb(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + GVariant *retval = g_dbus_proxy_call_finish(proxy, res, &err); + + if (err) { + // Some apps require "AboutToShow" to be called before activating a menuitem + // Others do not implement this method + // In case of the latter we log a debug warning and continue + g_debug("%s\n", err->message); + g_error_free(err); + } else { + g_variant_unref(retval); + } + + // Widget may be about to be destroyed + if (GTK_IS_WIDGET(snitem->popovermenu)) + gtk_popover_popup(GTK_POPOVER(snitem->popovermenu)); +} + + +static void +on_leftclick_cb(GtkGestureClick *click, + int n_press, + double x, + double y, + StatusNotifierItem *snitem) +{ + g_dbus_proxy_call(snitem->proxy, + "Activate", + g_variant_new("(ii)", 0, 0), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + NULL, + snitem); +} + + + + +static void +on_rightclick_cb(GtkGestureClick *click, + int n_press, + double x, + double y, + StatusNotifierItem *snitem) +{ + g_dbus_proxy_call(snitem->menuproxy, + "AboutToShow", + g_variant_new("(i)", 0), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback)about_to_show_cb, + snitem); +} + + +static void +pb_destroy(unsigned char *pixeld, void *data) +{ + g_free(pixeld); +} + + +static GVariant* +select_icon_by_size(GVariant *icondata_v) +{ + // Apps broadcast icons as variant a(iiay) + // Meaning array of tuples, tuple representing an icon + // first 2 members ii in each tuple are width and height + // We iterate the array and pick the icon size closest to + // the target based on its width and save the index + GVariantIter iter; + int selected_index = 0; + int current_index = 0; + int32_t target_icon_size = 22; + int32_t diff = INT32_MAX; + GVariant *child; + g_variant_iter_init(&iter, icondata_v); + while ((child = g_variant_iter_next_value(&iter))) { + int32_t curwidth; + g_variant_get_child(child, 0, "i", &curwidth); + int32_t curdiff; + if (curwidth > target_icon_size) + curdiff = curwidth - target_icon_size; + else + curdiff = target_icon_size - curwidth; + + if (curdiff < diff) + selected_index = current_index; + + current_index = current_index + 1; + g_variant_unref(child); + } + + GVariant *iconpixmap_v = g_variant_get_child_value(icondata_v, + (size_t)selected_index); + + return iconpixmap_v; +} + + +static GdkPaintable* +get_paintable_from_data(GVariant *icondata_v) +{ + GdkPaintable *paintable; + GVariantIter iter; + GVariant *iconpixmap_v = select_icon_by_size(icondata_v); + + int32_t width; + int32_t height; + GVariant *icon_data_v; + + g_variant_iter_init(&iter, iconpixmap_v); + + g_variant_iter_next(&iter, "i", &width); + g_variant_iter_next(&iter, "i", &height); + icon_data_v = g_variant_iter_next_value(&iter); + + size_t size = g_variant_get_size(icon_data_v); + const void *icon_data_dup = g_variant_get_data(icon_data_v); + + unsigned char *icon_data = g_memdup2(icon_data_dup, size); + argb_to_rgba(width, height, icon_data); + + int32_t padding = size / height - 4 * width; + int32_t rowstride = 4 * width + padding; + + GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(icon_data, + GDK_COLORSPACE_RGB, + TRUE, + 8, + width, + height, + rowstride, + (GdkPixbufDestroyNotify)pb_destroy, + NULL); + + GdkTexture *texture = gdk_texture_new_for_pixbuf(pixbuf); + paintable = GDK_PAINTABLE(texture); + + g_object_unref(pixbuf); + g_variant_unref(icon_data_v); + g_variant_unref(iconpixmap_v); + + return paintable; +} + + +static GdkPaintable* +get_paintable_from_name(const char *iconname) +{ + GdkPaintable *paintable = NULL; + GtkIconPaintable *icon; + + GtkIconTheme *theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); + icon = gtk_icon_theme_lookup_icon(theme, + iconname, + NULL, // const char **fallbacks + 22, + 1, + GTK_TEXT_DIR_LTR, + 0); // GtkIconLookupFlags + paintable = GDK_PAINTABLE(icon); + + return paintable; +} + + +static void +new_iconname_handler(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + GVariant *data = g_dbus_proxy_call_finish(proxy, res, &err); + // (s) + + if (err) { + g_debug("%s\n", err->message); + g_error_free(err); + return; + } + + const char *iconname = NULL; + g_variant_get(data, "(&s)", &iconname); + + if (strcmp(iconname, snitem->iconname) == 0) { + g_debug ("%s\n", "pixmap didnt change, nothing to"); + g_variant_unref(data); + return; + } + + g_object_unref(snitem->paintable); + snitem->iconname = iconname; + snitem->paintable = get_paintable_from_name(snitem->iconname); + gtk_image_set_from_paintable(GTK_IMAGE(snitem->icon), snitem->paintable); + g_variant_unref(data); +} + + +static void +new_iconpixmap_handler(GDBusProxy *proxy, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + GVariant *data = g_dbus_proxy_call_finish(proxy, res, &err); + // (v) + + if (err) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + return; + } + + GVariant *newpixmap_v; + g_variant_get(data, "(v)", &newpixmap_v); + g_variant_unref(data); + + if (g_variant_equal(newpixmap_v, snitem->iconpixmap_v)) { + g_debug ("%s\n", "pixmap didnt change, nothing to"); + g_variant_unref(newpixmap_v); + return; + } + + g_object_unref(snitem->paintable); + g_variant_unref(snitem->iconpixmap_v); + snitem->iconpixmap_v = newpixmap_v; + snitem->paintable = get_paintable_from_data(snitem->iconpixmap_v); + gtk_image_set_from_paintable(GTK_IMAGE (snitem->icon), snitem->paintable); +} + + +static void +trayitem_signal_handler(GDBusProxy *proxy, + const char *sender, + const char *signal, + GVariant *data_v, + StatusNotifierItem *snitem) +{ + // TODO: this can fire many times in a short amount of time + if (strcmp(signal, "NewIcon") == 0) { + if (snitem->iconpixmap_v) + g_dbus_proxy_call(proxy, + "org.freedesktop.DBus.Properties.Get", + g_variant_new("(ss)", "org.kde.StatusNotifierItem", "IconPixmap"), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback)new_iconpixmap_handler, + snitem); + + if (snitem->iconname) + g_dbus_proxy_call(proxy, + "org.freedesktop.DBus.Properties.Get", + g_variant_new("(ss)", "org.kde.StatusNotifierItem", "IconName"), + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, + (GAsyncReadyCallback) new_iconname_handler, + snitem); + } +} + + +static GtkWidget* +create_icon(GDBusProxy *proxy, StatusNotifierItem *snitem) +{ + + GtkWidget *image = NULL; + + const char *iconname = NULL; + GdkPaintable *paintable = NULL; + GVariant *iconname_v = g_dbus_proxy_get_cached_property(proxy, "IconName"); + + if (iconname_v) { + iconname = g_variant_get_string(iconname_v, NULL); + g_variant_unref(iconname_v); + } + + if (iconname && strcmp(iconname, "") != 0) { + paintable = get_paintable_from_name(iconname); + + snitem->iconname = iconname; + + } else { + GVariant *iconpixmap_v = g_dbus_proxy_get_cached_property(proxy, "IconPixmap"); + if (!iconpixmap_v) + return NULL; + paintable = get_paintable_from_data(iconpixmap_v); + + snitem->iconpixmap_v = iconpixmap_v; + } + + image = gtk_image_new_from_paintable(paintable); + snitem->paintable = paintable; + + return image; +} + + +void +create_trayitem(GDBusConnection *conn, GAsyncResult *res, StatusNotifierItem *snitem) +{ + GError *err = NULL; + snitem->proxy = g_dbus_proxy_new_finish(res, &err); + + if (err) { + fprintf(stderr, "%s\n", err->message); + g_error_free(err); + return; + } + + GtkIconTheme *theme = gtk_icon_theme_get_for_display(gdk_display_get_default ()); + GVariant *iconthemepath_v = g_dbus_proxy_get_cached_property(snitem->proxy, "IconThemePath"); + + if (iconthemepath_v) { + const char *path = g_variant_get_string(iconthemepath_v, NULL); + gtk_icon_theme_add_search_path(theme, path); + g_variant_unref(iconthemepath_v); + } + + GtkGesture *leftclick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(leftclick), 1); + g_signal_connect(leftclick, "pressed", G_CALLBACK(on_leftclick_cb), snitem); + + GtkGesture *rightclick = gtk_gesture_click_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(rightclick), 3); + g_signal_connect(rightclick, "pressed", G_CALLBACK(on_rightclick_cb), snitem); + + const char *valid_menuobjects[] = { + "/MenuBar", + "/com/canonical/dbusmenu", + "/org/ayatana/NotificationItem", + NULL + }; + + GVariant *menuobj_v = g_dbus_proxy_get_cached_property(snitem->proxy, "Menu"); + if (menuobj_v) { + snitem->menuobj = g_strdup(g_variant_get_string(menuobj_v, NULL)); + g_variant_unref(menuobj_v); + } else { + snitem->menuobj = g_strdup("Invalid_menuobj"); + } + snitem->actiongroup = g_simple_action_group_new(); + + char *xml_path = g_strdup_printf("%s%s", RESOURCE_PATH, "/DBusMenu.xml"); + snitem->menunodeinfo = get_interface_info(xml_path); + g_free(xml_path); + + // gtk_popover_menu_set_menu_model(GTK_POPOVER_MENU(snitem->popovermenu), + // G_MENU_MODEL(snitem->menu)); + + g_signal_connect(snitem->proxy, "g-signal", G_CALLBACK(trayitem_signal_handler), snitem); + + for (int i = 0; valid_menuobjects[i] != NULL; i++) { + if (g_strrstr(snitem->menuobj, valid_menuobjects[i]) != NULL) { + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + snitem->menunodeinfo->interfaces[0], + snitem->busname, + snitem->menuobj, + "com.canonical.dbusmenu", + NULL, + (GAsyncReadyCallback)create_menu, + snitem); + } + } + + GtkWidget *image = create_icon(snitem->proxy, snitem); + if (!image) + return; + + snitem->icon = image; + gtk_box_append(GTK_BOX(snitem->host->box), snitem->icon); + + gtk_widget_add_controller(snitem->icon, GTK_EVENT_CONTROLLER(leftclick)); + gtk_widget_add_controller(snitem->icon, GTK_EVENT_CONTROLLER(rightclick)); + gtk_widget_insert_action_group(snitem->icon, + "menuitem", + G_ACTION_GROUP (snitem->actiongroup)); +}