commit ea62b131108e433a5642e5054b91f8dd3b7e55d3
parent 55cebeb4261c59a9819b93290da089e2eb80d8d1
Author: awy <awy@awy.one>
Date: Sat, 15 Nov 2025 14:20:28 +0300
Merge branch 'systray'
Diffstat:
15 files changed, 3161 insertions(+), 5 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -9,3 +9,5 @@ xdg-output-unstable-v1-protocol.c
xdg-output-unstable-v1-protocol.h
dwl-ipc-unstable-v2-protocol.c
dwl-ipc-unstable-v2-protocol.h
+.cache
+compile_commands.json
diff --git a/Makefile b/Makefile
@@ -4,17 +4,19 @@ MANS = dwlb.1
PREFIX ?= /usr/local
CFLAGS += -Wall -Wextra -Wno-unused-parameter -Wno-format-truncation -g
-all: $(BINS)
+all: $(BINS) systray
config.h:
cp config.def.h $@
clean:
$(RM) $(BINS) $(addsuffix .o,$(BINS))
+ $(MAKE) clean -C systray
install: all
install -D -t $(PREFIX)/bin $(BINS)
install -D -m0644 -t $(PREFIX)/share/man/man1 $(MANS)
+ $(MAKE) install -C systray
WAYLAND_PROTOCOLS=$(shell pkg-config --variable=pkgdatadir wayland-protocols)
WAYLAND_SCANNER=$(shell pkg-config --variable=wayland_scanner wayland-scanner)
@@ -52,4 +54,8 @@ dwlb: xdg-shell-protocol.o xdg-output-unstable-v1-protocol.o wlr-layer-shell-uns
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
-.PHONY: all clean install
+systray:
+ $(MAKE) -C systray
+
+
+.PHONY: all systray clean install
diff --git a/README.md b/README.md
@@ -1,5 +1,6 @@
<div align="center">
<h1>dwlb</h1>
+<h2>This fork adds the systemtray feature implementing KDE's KStatusNotifierItem spec</h2>
A fast, feature-complete bar for [dwl](https://github.com/djpohly/dwl).
@@ -12,10 +13,12 @@ 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
-git clone https://github.com/kolunmi/dwlb
+git clone https://github.com/vetu104/dwlb
cd dwlb
make
make install
diff --git a/dwlb.c b/dwlb.c
@@ -11,6 +11,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <libgen.h>
#include <sys/mman.h>
#include <sys/select.h>
#include <sys/socket.h>
@@ -111,6 +112,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" \
+ " -no-systray do not launch the systray program\n" \
"Other\n" \
" -v get version information\n" \
" -h view this help text\n"
@@ -152,6 +154,7 @@ typedef struct {
bool configured;
uint32_t width, height;
+ uint32_t width_orig;
uint32_t textpadding;
uint32_t stride, bufsize;
@@ -515,11 +518,12 @@ layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface,
zwlr_layer_surface_v1_ack_configure(surface, serial);
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;
@@ -1447,6 +1451,20 @@ copy_customtext(CustomText *from, CustomText *to)
}
static void
+request_resize(Bar *bar, char *data)
+{
+ if (!bar)
+ return;
+
+ uint32_t traywidth = (uint32_t)atoi(data);
+
+ bar->width = bar->width_orig - buffer_scale * traywidth;
+ bar->stride = bar->width * 4;
+ bar->bufsize = bar->stride * bar->height;
+ bar->redraw = true;
+}
+
+static void
read_socket(void)
{
int cli_fd;
@@ -1591,6 +1609,13 @@ read_socket(void)
else
set_bottom(bar);
}
+ } else if (!strcmp(wordbeg, "resize")) {
+ if (all) {
+ wl_list_for_each(bar, &bar_list, link)
+ request_resize(bar, wordend);
+ } else {
+ request_resize(bar, wordend);
+ }
}
}
@@ -1682,6 +1707,92 @@ sig_handler(int sig)
run_display = false;
}
+#define MAX_ARGS 16
+#define MAX_ARG_LEN 16
+
+static void
+construct_tray_path(char *path_buf, const char *parent_progname, size_t size)
+{
+ const char tray_bin_name[] = "dwlbtray";
+ char progname_buf[PATH_MAX];
+ char traypath_maybe[PATH_MAX];
+
+ snprintf(progname_buf, sizeof(progname_buf), "%s", parent_progname);
+
+ char *dirpath = dirname(progname_buf);
+ if (dirpath) {
+ snprintf(traypath_maybe,
+ sizeof(traypath_maybe),
+ "%s/systray/%s",
+ dirpath,
+ tray_bin_name);
+ } else {
+ traypath_maybe[0] = '\0';
+ }
+
+ if (access(traypath_maybe, X_OK) == 0)
+ snprintf(path_buf, size, "%s", traypath_maybe);
+ else
+ snprintf(path_buf, size, "%s", tray_bin_name);
+}
+
+static void
+construct_traybg_arg(char *traybg_arg, size_t size)
+{
+ pixman_color_t *traybg_clr = &inactive_bg_color;
+ snprintf(traybg_arg,
+ size,
+ "#%02x%02x%02x",
+ (traybg_clr->red / 0x101),
+ (traybg_clr->green / 0x101),
+ (traybg_clr->blue) / 0x101);
+}
+
+static void
+construct_trayheight_arg(char *height_arg, size_t size)
+{
+ snprintf(height_arg, size, "%u", height);
+}
+
+static void
+start_systray(const char *parent_progname, bool bottom)
+{
+ char *args[MAX_ARGS];
+
+ char argv0[PATH_MAX];
+ char traybg_opt[] = "-c";
+ char trayheight_opt[] = "-s";
+ char bottom_opt[] = "-b";
+ char traybg_arg[MAX_ARG_LEN];
+ char trayheight_arg[MAX_ARG_LEN];
+
+ construct_tray_path(argv0, parent_progname, sizeof(argv0));
+ construct_traybg_arg(traybg_arg, sizeof(traybg_arg));
+ construct_trayheight_arg(trayheight_arg, sizeof(trayheight_arg));
+
+ int curarg = 0;
+ args[curarg++] = argv0;
+ args[curarg++] = traybg_opt;
+ args[curarg++] = traybg_arg;
+ args[curarg++] = trayheight_opt;
+ args[curarg++] = trayheight_arg;
+ if (bottom)
+ args[curarg++] = bottom_opt;
+ args[curarg] = NULL;
+
+ // Example result:
+ // char *args[16] = { "/home/user/git/dwlb/systray/dwlbtray", "-c", "#FFFFFF", "-s", "99", "-b", "-t", "DP-1", NULL, *garbage*, ... };
+
+ int child_pid = fork();
+ if (child_pid == -1) {
+ DIE("Fork failed");
+ } else if (child_pid == 0) {
+ if (execvp(args[0], args) == -1) {
+ DIE("Could not start systray program");
+ };
+ }
+}
+
int
main(int argc, char **argv)
{
@@ -1689,6 +1800,7 @@ main(int argc, char **argv)
struct sockaddr_un sock_address;
Bar *bar, *bar2;
Seat *seat, *seat2;
+ bool systray_enabled = true;
/* Establish socket directory */
if (!(xdgruntimedir = getenv("XDG_RUNTIME_DIR")))
@@ -1872,6 +1984,8 @@ 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], "-no-systray")) {
+ systray_enabled = false;;
} else if (!strcmp(argv[i], "-v")) {
fprintf(stderr, PROGRAM " " VERSION "\n");
return 0;
@@ -1964,6 +2078,10 @@ main(int argc, char **argv)
signal(SIGTERM, sig_handler);
signal(SIGCHLD, SIG_IGN);
+ /* Start tray program */
+ if (systray_enabled)
+ start_systray(argv[0], bottom);
+
/* Run */
run_display = true;
event_loop();
diff --git a/systray/.gitignore b/systray/.gitignore
@@ -0,0 +1,6 @@
+*.o
+dwlbtray
+valgrind*
+pango.supp
+.cache
+compile_commands.json
diff --git a/systray/Makefile b/systray/Makefile
@@ -0,0 +1,35 @@
+.SUFFIXES:
+
+PROGNAME = dwlbtray
+CC = cc
+RM = rm -f
+PKG_CONFIG ?= pkg-config
+PREFIX ?= /usr/local
+CFLAGS ?= -O2 -Wall -Wextra -g
+SUPPRESS = -Wno-missing-field-initializers -Wno-unused-parameter
+DEPINCLUDES = `$(PKG_CONFIG) --cflags glib-2.0 gobject-2.0 gio-2.0 \
+ gdk-pixbuf-2.0 gtk4 gtk4-layer-shell-0`
+DEPLIBS = `$(PKG_CONFIG) --libs glib-2.0 gobject-2.0 gio-2.0 \
+ gdk-pixbuf-2.0 gtk4 gtk4-layer-shell-0`
+OBJS = dwlbtray.o snwatcher.o snhost.o snitem.o sndbusmenu.o
+
+all: $(PROGNAME)
+
+clean:
+ $(RM) $(PROGNAME) $(OBJS)
+
+install: all
+ install -Dm755 $(PROGNAME) $(PREFIX)/bin/$(PROGNAME)
+
+$(PROGNAME): $(OBJS)
+ $(CC) $(DEPLIBS) $(LDFLAGS) -o $(PROGNAME) $(OBJS)
+
+.SUFFIXES: .c .o
+.c.o:
+ $(CC) -c $(DEPINCLUDES) $(CFLAGS) $(SUPPRESS) $<
+
+dwlbtray.o: dwlbtray.c snhost.h
+snwatcher.o: snwatcher.c snwatcher.h
+snhost.o: snhost.c snhost.h snwatcher.h snitem.h
+snitem.o: snitem.c snitem.h sndbusmenu.h
+sndbusmenu.o: sndbusmenu.c sndbusmenu.h snitem.h
diff --git a/systray/dwlbtray.c b/systray/dwlbtray.c
@@ -0,0 +1,159 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <glib-unix.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include <gtk4-layer-shell.h>
+
+#include "snhost.h"
+
+typedef struct args_parsed {
+ char cssdata[64];
+ int barheight;
+ int position;
+} args_parsed;
+
+static const int margin = 4;
+static const int spacing = 4;
+
+enum {
+ DWLB_POSITION_TOP,
+ DWLB_POSITION_BOTTOM
+};
+
+static void
+activate(GtkApplication* app, void *data)
+{
+ args_parsed *args = (args_parsed*)data;
+
+ GdkDisplay *display = gdk_display_get_default();
+
+ int iconsize, win_default_width, win_default_height;
+
+ iconsize = args->barheight - 2 * margin;
+ win_default_width = args->barheight;
+ win_default_height = args->barheight;
+
+ GtkCssProvider *css = gtk_css_provider_new();
+ gtk_css_provider_load_from_string(css, args->cssdata);
+ gtk_style_context_add_provider_for_display(display,
+ GTK_STYLE_PROVIDER(css),
+ GTK_STYLE_PROVIDER_PRIORITY_USER);
+ g_object_unref(css);
+
+ gboolean anchors[4];
+
+ switch (args->position) {
+ case DWLB_POSITION_TOP:
+ anchors[GTK_LAYER_SHELL_EDGE_LEFT] = FALSE;
+ anchors[GTK_LAYER_SHELL_EDGE_RIGHT] = TRUE;
+ anchors[GTK_LAYER_SHELL_EDGE_TOP] = TRUE;
+ anchors[GTK_LAYER_SHELL_EDGE_BOTTOM] = FALSE;
+ break;
+ case DWLB_POSITION_BOTTOM:
+ anchors[GTK_LAYER_SHELL_EDGE_LEFT] = FALSE;
+ anchors[GTK_LAYER_SHELL_EDGE_RIGHT] = TRUE;
+ anchors[GTK_LAYER_SHELL_EDGE_TOP] = FALSE;
+ anchors[GTK_LAYER_SHELL_EDGE_BOTTOM] = TRUE;
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+
+ GListModel *mons = gdk_display_get_monitors(display);
+
+ // Create tray for each monitor
+ unsigned int i;
+ for (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);
+
+ SnHost *host = sn_host_new(win_default_width,
+ win_default_height,
+ iconsize,
+ margin,
+ spacing,
+ conn);
+
+ GtkWindow *window = GTK_WINDOW(host);
+
+ gtk_window_set_application(window, app);
+
+ gtk_layer_init_for_window(window);
+ gtk_layer_set_layer(window, GTK_LAYER_SHELL_LAYER_BOTTOM);
+ gtk_layer_set_exclusive_zone(window, -1);
+
+ gtk_layer_set_monitor(window, mon);
+
+ for (int j = 0; j < GTK_LAYER_SHELL_EDGE_ENTRY_NUMBER; j++) {
+ gtk_layer_set_anchor(window, j, anchors[j]);
+ }
+
+ gtk_window_present(window);
+ }
+}
+
+static void
+terminate_app_helper(void *data, void *udata)
+{
+ GtkWindow *window = GTK_WINDOW(data);
+
+ gtk_window_close(window);
+}
+
+static gboolean
+terminate_app(GtkApplication *app)
+{
+ GList *windows = gtk_application_get_windows(app);
+ g_list_foreach(windows, terminate_app_helper, NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+int
+main(int argc, char *argv[])
+{
+ const char cssskele[] = "window{background-color:%s;}";
+
+ args_parsed args;
+ args.barheight = 22;
+ args.position = DWLB_POSITION_TOP;
+ snprintf(args.cssdata, sizeof(args.cssdata), cssskele, "#222222");
+
+ int option;
+ while ((option = getopt(argc, argv, "bc:s:")) != -1) {
+ switch (option) {
+ case 'b': // "bottom"
+ args.position = DWLB_POSITION_BOTTOM;
+ break;
+ case 'c': // "color"
+ snprintf(args.cssdata, sizeof(args.cssdata), cssskele, optarg);
+ break;
+ case 's': // "size"
+ args.barheight = strtol(optarg, NULL, 10);
+ break;
+ }
+ }
+
+ GtkApplication *app = gtk_application_new("org.dwlb.dwlbtray",
+ G_APPLICATION_DEFAULT_FLAGS);
+
+ g_signal_connect(app, "activate", G_CALLBACK(activate), &args);
+
+ g_unix_signal_add(SIGINT, (GSourceFunc)terminate_app, app);
+ g_unix_signal_add(SIGTERM, (GSourceFunc)terminate_app, app);
+
+ char *argv_inner[] = { argv[0], NULL };
+ int status = g_application_run(G_APPLICATION(app), 1, argv_inner);
+
+ g_object_unref(app);
+
+ return status;
+}
diff --git a/systray/sndbusmenu.c b/systray/sndbusmenu.c
@@ -0,0 +1,669 @@
+#include "sndbusmenu.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <time.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "snitem.h"
+
+
+struct _SnDbusmenu {
+ GObject parent_instance;
+
+ char* busname;
+ char* busobj;
+ SnItem* snitem;
+
+ GMenu* menu;
+ GSimpleActionGroup* actiongroup;
+ GDBusProxy* proxy;
+
+ uint32_t revision;
+ gboolean update_pending;
+ gboolean reschedule;
+};
+
+G_DEFINE_FINAL_TYPE(SnDbusmenu, sn_dbusmenu, G_TYPE_OBJECT)
+
+enum
+{
+ PROP_BUSNAME=1,
+ PROP_BUSOBJ,
+ PROP_SNITEM,
+ PROP_PROXY,
+ N_PROPERTIES
+};
+
+enum
+{
+ ABOUT_TO_SHOW_HANDLED,
+ LAST_SIGNAL
+};
+
+#define ACTION_NAME_MAX_LEN 32
+
+static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
+static unsigned int signals[LAST_SIGNAL];
+static const char actiongroup_pfx[] = "menuitem";
+static const int layout_update_freq = 80;
+
+typedef struct {
+ uint32_t id;
+ GDBusProxy* proxy;
+} ActionCallbackData;
+
+static void sn_dbusmenu_constructed (GObject *object);
+static void sn_dbusmenu_dispose (GObject *object);
+static void sn_dbusmenu_finalize (GObject *object);
+
+static void sn_dbusmenu_get_property (GObject *object,
+ unsigned int property_id,
+ GValue *value,
+ GParamSpec *pspec);
+
+static void sn_dbusmenu_set_property (GObject *object,
+ unsigned int property_id,
+ const GValue *value,
+ GParamSpec *pspec);
+
+static GMenu* create_menumodel (GVariant *data, SnDbusmenu *self);
+
+static GMenuItem* create_menuitem (int32_t id, GVariant *menu_data,
+ GVariant *submenu_data,
+ SnDbusmenu *self);
+
+
+static void
+action_activated_handler(GSimpleAction *action, GVariant* param, ActionCallbackData *data)
+{
+ 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);
+}
+
+static void
+action_free(void *data, GClosure *closure)
+{
+ ActionCallbackData *acbd = (ActionCallbackData *)data;
+ g_free(acbd);
+}
+
+static GSimpleAction*
+create_action(uint32_t id, gboolean ischeckmark, SnDbusmenu *self)
+{
+ GSimpleAction *action;
+ char name[ACTION_NAME_MAX_LEN];
+ sprintf(name, "%u", id);
+
+ if (ischeckmark)
+ action = g_simple_action_new_stateful(name, NULL, g_variant_new("b", TRUE));
+ else
+ action = g_simple_action_new(name, NULL);
+
+ ActionCallbackData *data = g_malloc(sizeof(ActionCallbackData));
+ data->id = id;
+ data->proxy = self->proxy;
+
+ g_signal_connect_data(action,
+ "activate",
+ G_CALLBACK(action_activated_handler),
+ data,
+ action_free,
+ G_CONNECT_DEFAULT);
+
+ return action;
+}
+
+static GMenuItem*
+create_menuitem(int32_t id, GVariant *menuitem_data, GVariant *submenuitem_data, SnDbusmenu *self)
+{
+ GActionMap *actionmap = G_ACTION_MAP(self->actiongroup);
+
+ char detailed_name[ACTION_NAME_MAX_LEN];
+ const char *label = NULL;
+ const char *type_s = NULL;
+ const char *toggle_type_s = NULL;
+ const char *has_submenu_s = NULL;
+
+ gboolean isenabled = TRUE;
+ gboolean isvisible = TRUE;
+ gboolean isseparator = FALSE;
+ gboolean ischeckmark = FALSE;
+ gboolean has_submenu = FALSE;
+ // gboolean isradio = FALSE;
+
+ gboolean checkmark_toggle_state = TRUE;
+
+ GVariantDict dict;
+ g_variant_dict_init(&dict, menuitem_data);
+ g_variant_dict_lookup(&dict, "label", "&s", &label);
+ g_variant_dict_lookup(&dict, "type", "&s", &type_s);
+ 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);
+ g_variant_dict_lookup(&dict, "toggle-type", "&s", &toggle_type_s);
+ g_variant_dict_lookup(&dict, "toggle-state", "i", &checkmark_toggle_state);
+ g_variant_dict_clear(&dict);
+
+ if (has_submenu_s && strcmp(has_submenu_s, "submenu") == 0)
+ has_submenu = TRUE;
+
+ if (type_s && strcmp(type_s, "separator") == 0)
+ isseparator = TRUE;
+ else if (toggle_type_s && strcmp(toggle_type_s, "checkmark") == 0)
+ ischeckmark = TRUE;
+ /*
+ else if (toggle_type_s && strcmp(toggle_type_s, "radio") == 0)
+ isradio = TRUE;
+ */
+
+ if (!isvisible || isseparator)
+ return NULL;
+
+ GSimpleAction *action = create_action(id, ischeckmark, self);
+ sprintf(detailed_name, "%s.%u", actiongroup_pfx, id);
+
+ GMenuItem *menuitem = g_menu_item_new(label, detailed_name);
+
+ if (!isenabled)
+ g_simple_action_set_enabled(action, FALSE);
+
+ if (ischeckmark)
+ g_simple_action_set_state(action,
+ g_variant_new("b",
+ checkmark_toggle_state));
+
+ if (has_submenu) {
+ GMenu *submenu = create_menumodel(submenuitem_data, self);
+ g_menu_item_set_submenu(menuitem, G_MENU_MODEL(submenu));
+ g_object_unref(submenu);
+ }
+
+ g_action_map_add_action(actionmap, G_ACTION(action));
+ g_object_unref(action);
+
+ return menuitem;
+}
+
+static GMenu*
+create_menumodel(GVariant *data, SnDbusmenu *self)
+{
+ GMenu *ret = g_menu_new();
+ GVariantIter iter;
+
+ // (ia{sv}av)
+ GVariant *menuitem_data_packed;
+ GVariant *menuitem_data;
+ int32_t id;
+
+ g_variant_iter_init(&iter, data);
+ while ((g_variant_iter_next(&iter, "v", &menuitem_data_packed))) {
+ g_variant_get_child(menuitem_data_packed, 0, "i", &id);
+ menuitem_data = g_variant_get_child_value(menuitem_data_packed, 1);
+ GVariant *submenu_data = g_variant_get_child_value(menuitem_data_packed, 2);
+ GMenuItem *menuitem = create_menuitem(id, menuitem_data, submenu_data, self);
+ if (menuitem) {
+ g_menu_append_item(ret, menuitem);
+ g_object_unref(menuitem);
+ }
+ g_variant_unref(submenu_data);
+ g_variant_unref(menuitem_data);
+ g_variant_unref(menuitem_data_packed);
+ }
+
+ return ret;
+}
+
+static void
+layout_update_finish(GObject *obj, GAsyncResult *res, void *udata)
+{
+ SnDbusmenu *self = SN_DBUSMENU(udata);
+ GError *err = NULL;
+
+ GVariant *data = g_dbus_proxy_call_finish(self->proxy, res, &err);
+
+ if (err) {
+ g_debug("Error in layout_update %s", err->message);
+ g_error_free(err);
+
+ g_object_unref(self->snitem);
+ g_object_unref(self);
+ return;
+ }
+
+ GVariant *layout;
+ GVariant *menuitems;
+
+ layout = g_variant_get_child_value(data, 1);
+ menuitems = g_variant_get_child_value(layout, 2);
+
+ gboolean isvisible = sn_item_get_popover_visible(self->snitem);
+ if (isvisible) {
+ self->reschedule = TRUE;
+ g_debug("Popover was visible, couldn't update menu %s", self->busname);
+ } else {
+ GSimpleActionGroup *newag = g_simple_action_group_new();
+ sn_item_set_actiongroup(self->snitem, actiongroup_pfx, newag);
+ g_object_unref(self->actiongroup);
+ self->actiongroup = newag;
+
+ GMenu *newmenu = create_menumodel(menuitems, self);
+ sn_item_set_menu_model(self->snitem, newmenu);
+ g_object_unref(self->menu);
+ self->menu = newmenu;
+ }
+
+ g_variant_unref(menuitems);
+ g_variant_unref(layout);
+
+ g_variant_unref(data);
+
+ g_object_unref(self->snitem);
+ g_object_unref(self);
+}
+
+static void
+layout_update(SnDbusmenu *self)
+{
+ g_debug("%s running menu layout update", self->busname);
+ self->update_pending = FALSE;
+
+ g_dbus_proxy_call(self->proxy,
+ "GetLayout",
+ g_variant_new("(iias)", 0, -1, NULL),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ layout_update_finish,
+ self);
+}
+
+static void
+reschedule_update(SnItem *snitem, GParamSpec *pspec, void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ g_return_if_fail(SN_IS_ITEM(self->snitem));
+
+ gboolean popover_visible = sn_item_get_popover_visible(snitem);
+ if (popover_visible || !self->reschedule)
+ return;
+
+ g_debug("%s rescheduling layout update", self->busname);
+
+ self->reschedule = FALSE;
+
+ g_object_ref(self);
+ g_object_ref(self->snitem);
+ layout_update(self);
+}
+
+// Update signals are often received multiple times in row,
+// we throttle update frequency to *layout_update_freq*
+static void
+proxy_signal_handler(GDBusProxy *proxy,
+ const char *sender,
+ const char *signal,
+ GVariant *params,
+ void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ g_return_if_fail(SN_IS_ITEM(self->snitem));
+
+ if (strcmp(signal, "LayoutUpdated") == 0) {
+ uint32_t revision;
+ int32_t parentid;
+ g_variant_get(params, "(ui)", &revision, &parentid);
+ g_debug("%s got LayoutUpdated, revision %i, parentid %i", self->busname, revision, parentid);
+
+ if (self->revision == UINT32_MAX || self->revision < revision) {
+ self->revision = revision;
+ }
+
+ if (!self->update_pending) {
+ self->update_pending = TRUE;
+ g_object_ref(self->snitem);
+ g_timeout_add_once(layout_update_freq,
+ (GSourceOnceFunc)layout_update,
+ g_object_ref(self));
+ } else {
+ g_debug("skipping update");
+ }
+
+ } else if (strcmp(signal, "ItemsPropertiesUpdated") == 0) {
+ g_debug("%s got ItemsPropertiesUpdated", self->busname);
+
+ if (!self->update_pending) {
+ self->update_pending = TRUE;
+ g_object_ref(self->snitem);
+ g_timeout_add_once(layout_update_freq,
+ (GSourceOnceFunc)layout_update,
+ g_object_ref(self));
+ } else {
+ g_debug("skipping update");
+ }
+ }
+}
+
+static void
+menulayout_ready_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ GError *err = NULL;
+ GVariant *retvariant = g_dbus_proxy_call_finish(self->proxy, res, &err);
+ // (u(ia{sv}av))
+
+ // "No such object path '/NO_DBUSMENU'"
+ // generated by QBittorrent when it sends a broken trayitem on startup
+ // and replaces it later
+ if (err && g_error_matches(err, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_OBJECT)) {
+ g_error_free(err);
+ g_object_unref(self);
+ return;
+ } else if (err) {
+ g_warning("%s\n", err->message);
+ g_error_free(err);
+ g_object_unref(self);
+ return;
+ }
+
+ uint32_t revision = 0;
+ GVariant *layout;
+ GVariant *menuitems;
+
+ g_variant_get_child(retvariant, 0, "u", &revision);
+
+ layout = g_variant_get_child_value(retvariant, 1);
+ menuitems = g_variant_get_child_value(layout, 2);
+
+ self->menu = create_menumodel(menuitems, self);
+ sn_item_set_menu_model(self->snitem, self->menu);
+
+ g_variant_unref(menuitems);
+ g_variant_unref(layout);
+ g_variant_unref(retvariant);
+ g_object_unref(self);
+}
+
+static void
+about_to_show_timeout_handler(void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ g_signal_emit(self, signals[ABOUT_TO_SHOW_HANDLED], 0);
+ g_object_unref(self);
+}
+
+static void
+about_to_show_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ GError *err = NULL;
+ GVariant *val = g_dbus_proxy_call_finish(self->proxy, res, &err);
+
+ // I give up trying to get nm-applet working properly. Wait 2 seconds until popping the menu
+ // to let it finish its business.
+ int timeout;
+ if (strcmp(self->busobj, "/org/ayatana/NotificationItem/nm_applet/Menu" ) == 0) {
+ timeout = 2000;
+ } else {
+ timeout = 100;
+ }
+
+ // Discord generates the following error here:
+ // 'G_DBUS_ERROR' 'G_DBUS_ERROR_FAILED' 'error occurred in AboutToShow'
+ // We ignore it.
+ if (err && !g_error_matches(err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED) &&
+ g_strrstr(err->message, "error occured in AboutToShow") != 0) {
+ g_warning("%s\n", err->message);
+
+ } else {
+ // This dbusmenu call might have triggered a menu update,
+ g_timeout_add_once(timeout, about_to_show_timeout_handler, g_object_ref(self));
+ }
+
+ err ? g_error_free(err) : g_variant_unref(val);
+ g_object_unref(self);
+}
+
+
+static void
+rightclick_handler(GObject *obj, void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ g_assert(SN_IS_DBUSMENU(self));
+ g_dbus_proxy_call(self->proxy,
+ "AboutToShow",
+ g_variant_new("(i)", 0),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ about_to_show_handler,
+ g_object_ref(self));
+}
+
+static void
+proxy_ready_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnDbusmenu *self = SN_DBUSMENU(data);
+
+ GError *err = NULL;
+ GDBusProxy *proxy = g_dbus_proxy_new_for_bus_finish(res, &err);
+
+ if (err) {
+ g_warning("Failed to construct gdbusproxy for menu: %s\n", err->message);
+ g_error_free(err);
+ g_object_unref(self);
+ return;
+ }
+
+ g_debug("Created gdbusproxy for menu %s %s",
+ g_dbus_proxy_get_name(proxy),
+ g_dbus_proxy_get_object_path(proxy));
+
+ g_object_set(self, "proxy", proxy, NULL);
+
+ g_dbus_proxy_call(self->proxy,
+ "GetLayout",
+ g_variant_new ("(iias)", 0, -1, NULL),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ menulayout_ready_handler,
+ g_object_ref(self));
+
+ g_signal_connect(self->proxy, "g-signal", G_CALLBACK(proxy_signal_handler), self);
+ g_object_unref(self);
+}
+
+static void
+sn_dbusmenu_get_property(GObject *object,
+ unsigned int property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+}
+
+static void
+sn_dbusmenu_set_property(GObject *object,
+ unsigned int property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnDbusmenu *self = SN_DBUSMENU(object);
+
+ switch (property_id) {
+ case PROP_BUSNAME:
+ self->busname = g_strdup(g_value_get_string(value));
+ break;
+ case PROP_BUSOBJ:
+ self->busobj = g_strdup(g_value_get_string(value));
+ break;
+ case PROP_SNITEM:
+ self->snitem = g_value_get_object(value);
+ break;
+ case PROP_PROXY:
+ self->proxy = g_value_get_object(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_dbusmenu_class_init(SnDbusmenuClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ object_class->set_property = sn_dbusmenu_set_property;
+ object_class->get_property = sn_dbusmenu_get_property;
+ object_class->constructed = sn_dbusmenu_constructed;
+ object_class->dispose = sn_dbusmenu_dispose;
+ object_class->finalize = sn_dbusmenu_finalize;
+
+ obj_properties[PROP_BUSNAME] =
+ g_param_spec_string("busname", NULL, NULL,
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+ obj_properties[PROP_BUSOBJ] =
+ g_param_spec_string("busobj", NULL, NULL,
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_SNITEM] =
+ g_param_spec_object("snitem", NULL, NULL,
+ SN_TYPE_ITEM,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_PROXY] =
+ g_param_spec_object("proxy", NULL, NULL,
+ G_TYPE_DBUS_PROXY,
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties);
+
+ signals[ABOUT_TO_SHOW_HANDLED] = g_signal_new("abouttoshowhandled",
+ SN_TYPE_DBUSMENU,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+sn_dbusmenu_init(SnDbusmenu *self)
+{
+ // When reschedule is TRUE, menu will be updated next time it is closed.
+ self->reschedule = FALSE;
+ self->update_pending = FALSE;
+
+ self->actiongroup = g_simple_action_group_new();
+}
+
+static void
+sn_dbusmenu_constructed(GObject *obj)
+{
+ SnDbusmenu *self = SN_DBUSMENU(obj);
+
+ sn_item_set_actiongroup(self->snitem, actiongroup_pfx, self->actiongroup);
+
+ GDBusNodeInfo *nodeinfo = g_dbus_node_info_new_for_xml(DBUSMENU_XML, NULL);
+ g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ nodeinfo->interfaces[0],
+ self->busname,
+ self->busobj,
+ "com.canonical.dbusmenu",
+ NULL,
+ (GAsyncReadyCallback)proxy_ready_handler,
+ g_object_ref(self));
+ g_dbus_node_info_unref(nodeinfo);
+
+ g_signal_connect(self->snitem, "notify::menuvisible", G_CALLBACK(reschedule_update), self);
+
+ g_signal_connect(self->snitem, "rightclick", G_CALLBACK(rightclick_handler), self);
+
+ G_OBJECT_CLASS(sn_dbusmenu_parent_class)->constructed(obj);
+}
+
+static void
+sn_dbusmenu_dispose(GObject *obj)
+{
+ SnDbusmenu *self = SN_DBUSMENU(obj);
+
+ g_debug("Disposing sndbusmenu %s %s", self->busname, self->busobj);
+
+ if (self->proxy) {
+ g_object_unref(self->proxy);
+ self->proxy = NULL;
+ }
+
+ if (self->actiongroup) {
+ sn_item_clear_actiongroup(self->snitem, actiongroup_pfx);
+ g_object_unref(self->actiongroup);
+ self->actiongroup = NULL;
+ }
+
+ if (self->menu) {
+ sn_item_clear_menu_model(self->snitem);
+ g_object_unref(self->menu);
+ self->menu = NULL;
+ }
+
+ if (self->snitem) {
+ g_object_unref(self->snitem);
+ self->snitem = NULL;
+ }
+
+ G_OBJECT_CLASS(sn_dbusmenu_parent_class)->dispose(obj);
+}
+
+static void
+sn_dbusmenu_finalize(GObject *obj)
+{
+ SnDbusmenu *self = SN_DBUSMENU(obj);
+ g_free(self->busname);
+ g_free(self->busobj);
+ G_OBJECT_CLASS(sn_dbusmenu_parent_class)->finalize(obj);
+}
+
+SnDbusmenu*
+sn_dbusmenu_new(const char *busname, const char *busobj, SnItem *snitem)
+{
+ return g_object_new(SN_TYPE_DBUSMENU,
+ "busname", busname,
+ "busobj", busobj,
+ "snitem", snitem,
+ NULL);
+}
diff --git a/systray/sndbusmenu.h b/systray/sndbusmenu.h
@@ -0,0 +1,87 @@
+#ifndef SNDBUSMENU_H
+#define SNDBUSMENU_H
+
+#include <glib-object.h>
+
+#include "snitem.h"
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_DBUSMENU sn_dbusmenu_get_type()
+G_DECLARE_FINAL_TYPE(SnDbusmenu, sn_dbusmenu, SN, DBUSMENU, GObject);
+
+SnDbusmenu* sn_dbusmenu_new (const char *busname,
+ const char *busobj,
+ SnItem *snitem);
+
+G_END_DECLS
+
+#define DBUSMENU_XML \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
+ "<node>\n" \
+ " <interface name=\"com.canonical.dbusmenu\">\n" \
+ " <!-- methods -->\n" \
+ " <method name=\"GetLayout\">\n" \
+ " <arg type=\"i\" name=\"parentId\" direction=\"in\"/>\n" \
+ " <arg type=\"i\" name=\"recursionDepth\" direction=\"in\"/>\n" \
+ " <arg type=\"as\" name=\"propertyNames\" direction=\"in\"/>\n" \
+ " <arg type=\"u\" name=\"revision\" direction=\"out\"/>\n" \
+ " <arg type=\"(ia{sv}av)\" name=\"layout\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " <method name=\"Event\">\n" \
+ " <arg type=\"i\" name=\"id\" direction=\"in\"/>\n" \
+ " <arg type=\"s\" name=\"eventId\" direction=\"in\"/>\n" \
+ " <arg type=\"v\" name=\"data\" direction=\"in\"/>\n" \
+ " <arg type=\"u\" name=\"timestamp\" direction=\"in\"/>\n" \
+ " </method>\n" \
+ " <method name=\"AboutToShow\">\n" \
+ " <arg type=\"i\" name=\"id\" direction=\"in\"/>\n" \
+ " <arg type=\"b\" name=\"needUpdate\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " <!--\n" \
+ " <method name=\"AboutToShowGroup\">\n" \
+ " <arg type=\"ai\" name=\"ids\" direction=\"in\"/>\n" \
+ " <arg type=\"ai\" name=\"updatesNeeded\" direction=\"out\"/>\n" \
+ " <arg type=\"ai\" name=\"idErrors\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " <method name=\"GetGroupProperties\">\n" \
+ " <arg type=\"ai\" name=\"ids\" direction=\"in\"/>\n" \
+ " <arg type=\"as\" name=\"propertyNames\" direction=\"in\"/>\n" \
+ " <arg type=\"a(ia{sv})\" name=\"properties\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " <method name=\"GetProperty\">\n" \
+ " <arg type=\"i\" name=\"id\" direction=\"in\"/>\n" \
+ " <arg type=\"s\" name=\"name\" direction=\"in\"/>\n" \
+ " <arg type=\"v\" name=\"value\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " <method name=\"EventGroup\">\n" \
+ " <arg type=\"a(isvu)\" name=\"events\" direction=\"in\"/>\n" \
+ " <arg type=\"ai\" name=\"idErrors\" direction=\"out\"/>\n" \
+ " </method>\n" \
+ " -->\n" \
+ " <!-- properties -->\n" \
+ " <!--\n" \
+ " <property name=\"Version\" type=\"u\" access=\"read\"/>\n" \
+ " <property name=\"TextDirection\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"Status\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"IconThemePath\" type=\"as\" access=\"read\"/>\n" \
+ " -->\n" \
+ " <!-- Signals -->\n" \
+ " <signal name=\"ItemsPropertiesUpdated\">\n" \
+ " <arg type=\"a(ia{sv})\" name=\"updatedProps\" direction=\"out\"/>\n" \
+ " <arg type=\"a(ias)\" name=\"removedProps\" direction=\"out\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"LayoutUpdated\">\n" \
+ " <arg type=\"u\" name=\"revision\" direction=\"out\"/>\n" \
+ " <arg type=\"i\" name=\"parent\" direction=\"out\"/>\n" \
+ " </signal>\n" \
+ " <!--\n" \
+ " <signal name=\"ItemActivationRequested\">\n" \
+ " <arg type=\"i\" name=\"id\" direction=\"out\"/>\n" \
+ " <arg type=\"u\" name=\"timestamp\" direction=\"out\"/>\n" \
+ " </signal>\n" \
+ " -->\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#endif /* SNDBUSMENU_H */
diff --git a/systray/snhost.c b/systray/snhost.c
@@ -0,0 +1,351 @@
+#include "snhost.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <limits.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+#include "snwatcher.h"
+#include "snitem.h"
+
+
+struct _SnHost
+{
+ GtkWidget parent_instance;
+
+ GtkWidget* box;
+ SnWatcher* watcher;
+ GHashTable* snitems;
+ char* mon;
+
+ unsigned long reg_sub_id;
+ unsigned long unreg_sub_id;
+
+ int defaultwidth;
+ int defaultheight;
+ int iconsize;
+ int margins;
+ int spacing;
+
+ int nitems;
+ int curwidth;
+ gboolean exiting;
+};
+
+G_DEFINE_FINAL_TYPE(SnHost, sn_host, GTK_TYPE_WINDOW)
+
+enum
+{
+ PROP_MON = 1,
+ PROP_DEFAULTWIDTH,
+ PROP_DEFAULTHEIGHT,
+ PROP_ICONSIZE,
+ PROP_MARGINS,
+ PROP_SPACING,
+ N_PROPERTIES
+};
+
+static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
+
+
+static void sn_host_constructed (GObject *obj);
+static void sn_host_dispose (GObject *obj);
+static void sn_host_finalize (GObject *obj);
+
+
+static void
+dwlb_request_resize(SnHost *self)
+{
+ if (self->exiting) {
+ // Restore original size on exit.
+ self->curwidth = 0;
+ } else if (self->nitems <= 1) {
+ // Width of 1 icon even when there are none.
+ self->curwidth = self->iconsize + 2 * self->margins;
+ } else {
+ self->curwidth =
+ // Icons themselves.
+ self->nitems * self->iconsize +
+
+ // Spacing between icons.
+ (self->nitems - 1) * self->spacing +
+
+ // Margins before first icon and after last icon.
+ 2 * self->margins;
+ }
+
+ struct sockaddr_un sockaddr;
+
+ sockaddr.sun_family = AF_UNIX;
+
+ snprintf(sockaddr.sun_path,
+ sizeof(sockaddr.sun_path),
+ "%s/dwlb/dwlb-0",
+ g_get_user_runtime_dir());
+
+ char sockbuf[64] = {0};
+ snprintf(sockbuf, sizeof(sockbuf), "%s %s %i", self->mon, "resize", self->curwidth);
+ 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");
+
+ size_t sendstatus =
+ send(sock_fd, sockbuf, len, 0);
+
+ if (sendstatus == (size_t)-1)
+ g_error("Could not send size update to %s", sockaddr.sun_path);
+
+ close(sock_fd);
+}
+
+static void
+sn_host_register_item(SnWatcher *watcher,
+ const char *busname,
+ const char *busobj,
+ SnHost *self)
+{
+ g_debug("Adding %s to snhost %s", busname, self->mon);
+
+ SnItem *snitem = sn_item_new(busname, busobj, self->iconsize);
+ gtk_box_append(GTK_BOX(self->box), GTK_WIDGET(snitem));
+ g_hash_table_insert(self->snitems, g_strdup(busname), snitem);
+
+ self->nitems = self->nitems + 1;
+ dwlb_request_resize(self);
+}
+
+static void
+sn_host_unregister_item(SnWatcher *watcher, const char *busname, SnHost *self)
+{
+ g_debug("Removing %s from snhost %s", busname, self->mon);
+
+ GtkBox *box = GTK_BOX(self->box);
+ void *match = g_hash_table_lookup(self->snitems, busname);
+ GtkWidget *snitem = GTK_WIDGET(match);
+
+ gtk_box_remove(box, snitem);
+ g_hash_table_remove(self->snitems, busname);
+
+ self->nitems = self->nitems - 1;
+ dwlb_request_resize(self);
+}
+
+static void
+sn_host_set_property(GObject *object,
+ unsigned int property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnHost *self = SN_HOST(object);
+
+ switch (property_id) {
+ case PROP_MON:
+ self->mon = g_value_dup_string(value);
+ break;
+ case PROP_DEFAULTWIDTH:
+ self->defaultwidth = g_value_get_int(value);
+ break;
+ case PROP_DEFAULTHEIGHT:
+ self->defaultheight = g_value_get_int(value);
+ break;
+ case PROP_ICONSIZE:
+ self->iconsize = g_value_get_int(value);
+ break;
+ case PROP_MARGINS:
+ self->margins = g_value_get_int(value);
+ break;
+ case PROP_SPACING:
+ self->spacing = g_value_get_int(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_host_class_init(SnHostClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ object_class->set_property = sn_host_set_property;
+ object_class->constructed = sn_host_constructed;
+ object_class->dispose = sn_host_dispose;
+ object_class->finalize = sn_host_finalize;
+
+ obj_properties[PROP_MON] =
+ g_param_spec_string("mon", NULL, NULL,
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_DEFAULTWIDTH] =
+ g_param_spec_int("defaultwidth", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 22,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_DEFAULTHEIGHT] =
+ g_param_spec_int("defaultheight", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 22,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_ICONSIZE] =
+ g_param_spec_int("iconsize", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 22,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_MARGINS] =
+ g_param_spec_int("margins", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 4,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_SPACING] =
+ g_param_spec_int("spacing", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 4,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties);
+}
+
+static void
+sn_host_init(SnHost *self)
+{
+ self->snitems = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+
+ self->exiting = FALSE;
+ self->nitems = 0;
+
+ self->watcher = sn_watcher_new();
+
+ self->reg_sub_id = g_signal_connect(self->watcher,
+ "trayitem-registered",
+ G_CALLBACK(sn_host_register_item),
+ self);
+
+ self->unreg_sub_id = g_signal_connect(self->watcher,
+ "trayitem-unregistered",
+ G_CALLBACK(sn_host_unregister_item),
+ self);
+}
+
+static void
+sn_host_constructed(GObject *obj)
+{
+ SnHost *self = SN_HOST(obj);
+
+ GtkWindow *window = GTK_WINDOW(self);
+ gtk_window_set_decorated(window, FALSE);
+ gtk_window_set_default_size(window, self->defaultwidth, self->defaultheight);
+
+ self->box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, self->spacing);
+ gtk_box_set_homogeneous(GTK_BOX(self->box), TRUE);
+ gtk_box_set_spacing(GTK_BOX(self->box), self->margins);
+
+ GtkWidget *widget = GTK_WIDGET(self->box);
+ gtk_widget_set_vexpand(widget, TRUE);
+ gtk_widget_set_hexpand(widget, TRUE);
+ gtk_widget_set_margin_start(widget, self->margins);
+ gtk_widget_set_margin_end(widget, self->margins);
+ gtk_widget_set_margin_top(widget, self->margins);
+ gtk_widget_set_margin_bottom(widget, self->margins);
+
+ gtk_window_set_child(GTK_WINDOW(self), self->box);
+
+ dwlb_request_resize(self);
+
+ g_debug("Created snhost for monitor %s", self->mon);
+
+ G_OBJECT_CLASS(sn_host_parent_class)->constructed(obj);
+}
+
+static void
+sn_host_dispose(GObject *obj)
+{
+ SnHost *self = SN_HOST(obj);
+
+ g_debug("Disposing snhost of monitor %s", self->mon);
+ self->exiting = TRUE;
+
+ if (self->reg_sub_id > 0) {
+ g_signal_handler_disconnect(self->watcher, self->reg_sub_id);
+ self->reg_sub_id = 0;
+ }
+
+ if (self->unreg_sub_id > 0) {
+ g_signal_handler_disconnect(self->watcher, self->unreg_sub_id);
+ self->reg_sub_id = 0;
+ }
+
+ if (self->watcher) {
+ g_object_unref(self->watcher);
+ self->watcher = NULL;
+ }
+
+ dwlb_request_resize(self);
+
+ G_OBJECT_CLASS(sn_host_parent_class)->dispose(obj);
+}
+
+static void
+sn_host_finalize(GObject *obj)
+{
+ SnHost *self = SN_HOST(obj);
+
+ g_hash_table_destroy(self->snitems);
+ g_free(self->mon);
+
+ G_OBJECT_CLASS(sn_host_parent_class)->finalize(obj);
+}
+
+SnHost*
+sn_host_new(int defaultwidth,
+ int defaultheight,
+ int iconsize,
+ int margins,
+ int spacing,
+ const char *conn)
+{
+ return g_object_new(SN_TYPE_HOST,
+ "defaultwidth", defaultwidth,
+ "defaultheight", defaultheight,
+ "iconsize", iconsize,
+ "margins", margins,
+ "spacing", spacing,
+ "mon", conn,
+ NULL);
+}
diff --git a/systray/snhost.h b/systray/snhost.h
@@ -0,0 +1,21 @@
+#ifndef SNHOST_H
+#define SNHOST_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_HOST sn_host_get_type()
+G_DECLARE_FINAL_TYPE(SnHost, sn_host, SN, HOST, GtkWindow)
+
+SnHost *sn_host_new (int defaultwidth,
+ int defaultheight,
+ int iconsize,
+ int margins,
+ int spacing,
+ const char *conn);
+
+G_END_DECLS
+
+#endif /* SNHOST_H */
diff --git a/systray/snitem.c b/systray/snitem.c
@@ -0,0 +1,1160 @@
+#include "snitem.h"
+
+#include "sndbusmenu.h"
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gdk/gdk.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+struct _SnItem
+{
+ GtkWidget parent_instance;
+
+ char* busname;
+ char* busobj;
+
+ GMenu* init_menu;
+ GtkGesture* lclick;
+ GtkGesture* rclick;
+ GtkWidget* image;
+ GtkWidget* popovermenu;
+
+ GDBusProxy* proxy;
+ GSList* cachedicons;
+ GVariant* iconpixmap;
+ SnDbusmenu* dbusmenu;
+ char *iconpath;
+ char* iconname;
+
+ unsigned long lclick_id;
+ unsigned long popup_id;
+ unsigned long proxy_id;
+ unsigned long rclick_id;
+
+ int icon_source;
+ int iconsize;
+ int status;
+ gboolean in_destruction;
+ gboolean menu_visible;
+};
+
+G_DEFINE_FINAL_TYPE(SnItem, sn_item, GTK_TYPE_WIDGET)
+
+enum
+{
+ PROP_BUSNAME = 1,
+ PROP_BUSOBJ,
+ PROP_ICONSIZE,
+ PROP_DBUSMENU,
+ PROP_MENUVISIBLE,
+ N_PROPERTIES
+};
+
+enum
+{
+ RIGHTCLICK,
+ LAST_SIGNAL
+};
+
+enum icon_sources
+{
+ ICON_SOURCE_UNKNOWN,
+ ICON_SOURCE_NAME,
+ ICON_SOURCE_PATH,
+ ICON_SOURCE_PIXMAP,
+};
+
+static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };
+static unsigned int signals[LAST_SIGNAL];
+
+typedef struct {
+ char* iconname;
+ char *iconpath;
+ GVariant* iconpixmap;
+ GdkPaintable* icondata;
+} cached_icon_t;
+
+static void sn_item_constructed (GObject *obj);
+static void sn_item_dispose (GObject *obj);
+static void sn_item_finalize (GObject *obj);
+
+static void sn_item_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline);
+
+static void sn_item_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline);
+
+static void request_newicon_name(SnItem *self);
+static void request_newicon_pixmap(SnItem *self);
+static void request_newicon_path(SnItem *self);
+
+static gboolean
+validate_pixdata(GVariant *icondata)
+{
+ int32_t width, height;
+ GVariant *bytearr;
+ size_t size;
+
+ g_variant_get_child(icondata, 0, "i", &width);
+ g_variant_get_child(icondata, 1, "i", &height);
+ bytearr = g_variant_get_child_value(icondata, 2);
+ size = g_variant_get_size(bytearr);
+
+ g_variant_unref(bytearr);
+
+ if (width == 0 || height == 0 || size == 0)
+ return false;
+ else
+ return true;
+}
+
+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
+ 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 int
+find_cached_icon_path(cached_icon_t *cicon,
+ const char *path)
+{
+ if (cicon->iconpath == NULL || path == NULL)
+ return -1;
+
+ return strcmp(cicon->iconname, path);
+}
+
+static int
+find_cached_icon_name(cached_icon_t *cicon,
+ const char *name)
+{
+ if (cicon->iconname == NULL || name == NULL)
+ return -1;
+
+ return strcmp(cicon->iconname, name);
+}
+
+static int
+find_cached_icon_pixmap(cached_icon_t *cicon, GVariant *pixmap)
+{
+ if (cicon->iconpixmap == NULL || pixmap == NULL)
+ return -1;
+
+ if (g_variant_equal(cicon->iconpixmap, pixmap))
+ return 0;
+ else
+ return 1;
+}
+
+static void
+cachedicons_free(void *data)
+{
+ cached_icon_t *cicon = (cached_icon_t*)data;
+ g_free(cicon->iconname);
+ g_free(cicon->iconpath);
+ if (cicon->iconpixmap != NULL)
+ g_variant_unref(cicon->iconpixmap);
+ if (cicon->icondata != NULL)
+ g_object_unref(cicon->icondata);
+ g_free(cicon);
+}
+
+static void
+pixbuf_destroy(unsigned char *pixeld, void *data)
+{
+ g_free(pixeld);
+}
+
+static GVariant*
+select_icon_by_size(GVariant *vicondata, int32_t target_icon_size)
+{
+ // 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 diff = INT32_MAX;
+ GVariant *child;
+ g_variant_iter_init(&iter, vicondata);
+ 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 *selected = g_variant_get_child_value(vicondata,
+ (size_t)selected_index);
+
+ // Discard if the array is empty
+ if (validate_pixdata(selected)) {
+ return selected;
+ } else {
+ g_variant_unref(selected);
+ return NULL;
+ }
+}
+
+static GdkPaintable*
+get_paintable_from_name(const char *iconname, int32_t iconsize)
+{
+ 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
+ iconsize,
+ 1,
+ GTK_TEXT_DIR_LTR,
+ 0); // GtkIconLookupFlags
+ paintable = GDK_PAINTABLE(icon);
+
+ return paintable;
+}
+
+static GdkPaintable*
+get_paintable_from_path(const char *path, int32_t iconsize)
+{
+ GdkPaintable *paintable = NULL;
+
+ GdkPixbuf* pixbuf = gdk_pixbuf_new_from_file_at_size(path,
+ iconsize,
+ iconsize,
+ NULL);
+
+ GdkTexture* texture = gdk_texture_new_for_pixbuf(pixbuf);
+ paintable = GDK_PAINTABLE(texture);
+
+ g_object_unref(pixbuf);
+
+ return paintable;
+}
+
+static GdkPaintable*
+get_paintable_from_data(GVariant *iconpixmap_v, int32_t iconsize)
+{
+ GdkPaintable *paintable;
+ GVariantIter iter;
+
+ int32_t width;
+ int32_t height;
+ GVariant *vicondata;
+
+ g_variant_iter_init(&iter, iconpixmap_v);
+
+ g_variant_iter_next(&iter, "i", &width);
+ g_variant_iter_next(&iter, "i", &height);
+ vicondata = g_variant_iter_next_value(&iter);
+
+ size_t size = g_variant_get_size(vicondata);
+ const void *icon_data_dup = g_variant_get_data(vicondata);
+
+ unsigned char *icon_data = g_memdup2(icon_data_dup, size);
+ argb_to_rgba(width, height, icon_data);
+
+ if (height == 0) {
+ g_variant_unref(vicondata);
+ return NULL;
+ }
+ 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)pixbuf_destroy,
+ NULL);
+
+ GdkTexture *texture = gdk_texture_new_for_pixbuf(pixbuf);
+ paintable = GDK_PAINTABLE(texture);
+
+ g_object_unref(pixbuf);
+ g_variant_unref(vicondata);
+
+ return paintable;
+}
+
+static void
+new_iconname_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnItem *self = SN_ITEM(data);
+ GDBusProxy *proxy = G_DBUS_PROXY(obj);
+
+ GError *err = NULL;
+ GVariant *retvariant = g_dbus_proxy_call_finish(proxy, res, &err);
+ // (v)
+
+ if (err != NULL) {
+ switch (err->code) {
+ case G_DBUS_ERROR_UNKNOWN_OBJECT:
+ // Remote object went away while call was underway
+ break;
+ case G_DBUS_ERROR_UNKNOWN_PROPERTY:
+ // Expected when ICON_SOURCE_UNKNOWN
+ break;
+ default:
+ g_warning("%s\n", err->message);
+ break;
+ }
+ g_error_free(err);
+ request_newicon_pixmap(self);
+ g_object_unref(self);
+ return;
+ }
+
+ GVariant *iconname_v;
+ const char *iconname = NULL;
+ g_variant_get(retvariant, "(v)", &iconname_v);
+ g_variant_get(iconname_v, "&s", &iconname);
+ g_variant_unref(retvariant);
+
+ // New iconname invalid
+ if (iconname == NULL || strcmp(iconname, "") == 0) {
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ g_variant_unref(iconname_v);
+ g_object_unref(self);
+ request_newicon_pixmap(self);
+ return;
+ // New iconname is a path
+ } else if (access(iconname, R_OK) == 0) {
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ g_variant_unref(iconname_v);
+ g_object_unref(self);
+ request_newicon_path(self);
+ return;
+ }
+
+ // Icon didn't change
+ if (strcmp(iconname, self->iconname) == 0) {
+ g_variant_unref(iconname_v);
+ g_object_unref(self);
+ return;
+ }
+
+ GSList *elem = g_slist_find_custom(self->cachedicons,
+ iconname,
+ (GCompareFunc)find_cached_icon_name);
+
+ // Cache hit
+ if (elem != NULL) {
+ cached_icon_t *cicon = (cached_icon_t*)elem->data;
+ self->iconname = cicon->iconname;
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ g_debug("%s: Icon cache hit - iconname", self->busname);
+ // Cache miss -> cache new icon
+ } else {
+ cached_icon_t *cicon = g_malloc0(sizeof(cached_icon_t));
+ self->iconname = g_strdup(iconname);
+ cicon->iconname = self->iconname;
+ cicon->icondata = get_paintable_from_name(self->iconname,
+ self->iconsize);
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ self->cachedicons = g_slist_prepend(self->cachedicons, cicon);
+ self->icon_source = ICON_SOURCE_NAME;
+ }
+
+ g_variant_unref(iconname_v);
+ g_object_unref(self);
+}
+
+static void
+new_iconpath_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnItem *self = SN_ITEM(data);
+ GDBusProxy *proxy = G_DBUS_PROXY(obj);
+
+ GError *err = NULL;
+ GVariant *retvariant = g_dbus_proxy_call_finish(proxy, res, &err);
+ // (v)
+
+ if (err != NULL) {
+ switch (err->code) {
+ case G_DBUS_ERROR_UNKNOWN_OBJECT:
+ // Remote object went away while call was underway
+ break;
+ case G_DBUS_ERROR_UNKNOWN_PROPERTY:
+ // Expected when ICON_SOURCE_UNKNOWN
+ break;
+ default:
+ g_warning("%s\n", err->message);
+ break;
+ }
+ g_error_free(err);
+ request_newicon_pixmap(self);
+ g_object_unref(self);
+ return;
+ }
+
+ GVariant *viconpath;
+ const char *iconpath = NULL;
+ g_variant_get(retvariant, "(v)", &viconpath);
+ g_variant_get(viconpath, "&s", &iconpath);
+ g_variant_unref(retvariant);
+
+ // New iconpath is invalid
+ if (iconpath == NULL || strcmp(iconpath, "") == 0 || access(iconpath, R_OK) == 0) {
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ g_variant_unref(viconpath);
+ g_object_unref(self);
+ request_newicon_pixmap(self);
+ return;
+ // New iconpath is not a path but possibly an iconname
+ } else if (iconpath != NULL && strcmp(iconpath, "") != 0) {
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ g_variant_unref(viconpath);
+ g_object_unref(self);
+ request_newicon_name(self);
+ return;
+ }
+
+ // Icon didn't change
+ if (strcmp(iconpath, self->iconpath) == 0) {
+ g_variant_unref(viconpath);
+ request_newicon_pixmap(self);
+ g_object_unref(self);
+ return;
+ }
+
+ GSList *elem = g_slist_find_custom(self->cachedicons,
+ iconpath,
+ (GCompareFunc)find_cached_icon_path);
+
+ // Cache hit
+ if (elem != NULL) {
+ cached_icon_t *cicon = (cached_icon_t*)elem->data;
+ self->iconpath = cicon->iconpath;
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ g_debug("%s: Icon cache hit - iconpath", self->busname);
+ // Cache miss -> cache new icon
+ } else {
+ cached_icon_t *cicon = g_malloc0(sizeof(cached_icon_t));
+ self->iconpath = g_strdup(iconpath);
+ cicon->iconpath = self->iconpath;
+ cicon->icondata = get_paintable_from_name(self->iconpath,
+ self->iconsize);
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ self->cachedicons = g_slist_prepend(self->cachedicons, cicon);
+ self->icon_source = ICON_SOURCE_PATH;
+ }
+
+ g_variant_unref(viconpath);
+ g_object_unref(self);
+}
+
+static void
+new_pixmaps_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnItem *self = SN_ITEM(data);
+ GDBusProxy *proxy = G_DBUS_PROXY(obj);
+
+ GError *err = NULL;
+ GVariant *retvariant = g_dbus_proxy_call_finish(proxy, res, &err);
+ // (v)
+
+ if (err != NULL) {
+ switch (err->code) {
+ case G_DBUS_ERROR_UNKNOWN_OBJECT:
+ // Remote object went away while call was underway
+ break;
+ case G_DBUS_ERROR_UNKNOWN_PROPERTY:
+ // Expected when ICON_SOURCE_UNKNOWN
+ break;
+ default:
+ g_warning("%s\n", err->message);
+ }
+ g_error_free(err);
+ g_object_unref(self);
+ return;
+ }
+
+ GVariant *newpixmaps;
+ g_variant_get(retvariant, "(v)", &newpixmaps);
+
+ GVariant *pixmap = select_icon_by_size(newpixmaps, self->iconsize);
+
+ // No valid icon in data
+ if (pixmap == NULL) {
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ g_variant_unref(newpixmaps);
+ g_variant_unref(retvariant);
+ g_object_unref(self);
+ return;
+ }
+
+ // Icon didn't change
+ if (self->iconpixmap && g_variant_equal(pixmap, self->iconpixmap)) {
+ g_variant_unref(pixmap);
+ g_variant_unref(newpixmaps);
+ g_variant_unref(retvariant);
+ g_object_unref(self);
+ return;
+ }
+
+ GSList *elem = g_slist_find_custom(self->cachedicons,
+ pixmap,
+ (GCompareFunc)find_cached_icon_pixmap);
+
+ // Cache hit
+ if (elem != NULL) {
+ cached_icon_t *cicon = (cached_icon_t*)elem->data;
+ self->iconpixmap = cicon->iconpixmap;
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ g_debug("%s: Icon cache hit - pixmap", self->busname);
+ // Cache miss -> cache new icon
+ } else {
+ cached_icon_t *cicon = g_malloc0(sizeof(cached_icon_t));
+ self->iconpixmap = g_variant_ref(pixmap);
+ cicon->iconpixmap = self->iconpixmap;
+ cicon->icondata = get_paintable_from_data(self->iconpixmap,
+ self->iconsize);
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), cicon->icondata);
+ self->cachedicons = g_slist_prepend(self->cachedicons, cicon);
+ self->icon_source = ICON_SOURCE_PIXMAP;
+ }
+
+ g_variant_unref(pixmap);
+ g_variant_unref(newpixmaps);
+ g_variant_unref(retvariant);
+ g_object_unref(self);
+}
+
+static void
+request_newicon_name(SnItem *self)
+{
+ g_dbus_proxy_call(self->proxy,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new("(ss)",
+ "org.kde.StatusNotifierItem",
+ "IconName"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ new_iconname_handler,
+ g_object_ref(self));
+}
+
+static void
+request_newicon_path(SnItem *self)
+{
+ g_dbus_proxy_call(self->proxy,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new("(ss)",
+ "org.kde.StatusNotifierItem",
+ "IconName"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ new_iconpath_handler,
+ g_object_ref(self));
+}
+
+static void
+request_newicon_pixmap(SnItem *self)
+{
+ g_dbus_proxy_call(self->proxy,
+ "org.freedesktop.DBus.Properties.Get",
+ g_variant_new("(ss)",
+ "org.kde.StatusNotifierItem",
+ "IconPixmap"),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ new_pixmaps_handler,
+ g_object_ref(self));
+}
+
+static void
+proxy_signal_handler(GDBusProxy *proxy,
+ const char *sender,
+ const char *signal,
+ GVariant *data_v,
+ void *data)
+{
+ SnItem *self = SN_ITEM(data);
+
+ if (strcmp(signal, "NewIcon") == 0) {
+ switch (self->icon_source) {
+ case ICON_SOURCE_NAME:
+ request_newicon_name(self);
+ break;
+ case ICON_SOURCE_PATH:
+ request_newicon_path(self);
+ break;
+ case ICON_SOURCE_PIXMAP:
+ request_newicon_pixmap(self);
+ break;
+ default:
+ request_newicon_name(self);
+ break;
+ }
+ }
+}
+
+static void
+popup_popover(SnDbusmenu *dbusmenu, SnItem *self)
+{
+ if (self->in_destruction)
+ return;
+
+ g_object_set(self, "menuvisible", true, NULL);
+ gtk_popover_popup(GTK_POPOVER(self->popovermenu));
+}
+
+static void
+leftclick_handler(GtkGestureClick *click,
+ int n_press,
+ double x,
+ double y,
+ void *data)
+{
+ SnItem *self = SN_ITEM(data);
+
+ g_dbus_proxy_call(self->proxy,
+ "Activate",
+ g_variant_new("(ii)", 0, 0),
+ G_DBUS_CALL_FLAGS_NONE,
+ -1,
+ NULL,
+ NULL,
+ NULL);
+}
+
+static void
+rightclick_handler(GtkGestureClick *click,
+ int n_press,
+ double x,
+ double y,
+ void *data)
+{
+ SnItem *self = SN_ITEM(data);
+ if (self->in_destruction)
+ return;
+
+ g_signal_emit(self, signals[RIGHTCLICK], 0);
+}
+
+static void
+connect_to_menu(SnItem *self)
+{
+ if (self->in_destruction)
+ return;
+
+ const char *menu_buspath = NULL;
+ GVariant *vmenupath = g_dbus_proxy_get_cached_property(self->proxy, "Menu");
+
+ if (vmenupath != NULL) {
+ g_variant_get(vmenupath, "&o", &menu_buspath);
+ if (strcmp(menu_buspath, "") != 0) {
+ self->dbusmenu = sn_dbusmenu_new(self->busname, menu_buspath, self);
+
+ self->rclick_id = g_signal_connect(self->rclick,
+ "pressed",
+ G_CALLBACK(rightclick_handler),
+ self);
+
+ self->popup_id = g_signal_connect(self->dbusmenu,
+ "abouttoshowhandled",
+ G_CALLBACK(popup_popover),
+ self);
+ }
+ g_variant_unref(vmenupath);
+ }
+}
+
+static void
+select_icon_source(SnItem *self)
+{
+ char *iconname_or_path = NULL;
+ GVariant *vname = g_dbus_proxy_get_cached_property(self->proxy, "IconName");
+ GVariant *vpixmaps = g_dbus_proxy_get_cached_property(self->proxy, "IconPixmap");
+
+ if (vname != NULL) {
+ g_variant_get(vname, "s", &iconname_or_path);
+ if (strcmp(iconname_or_path, "") == 0) {
+ g_free(iconname_or_path);
+ iconname_or_path = NULL;
+ }
+ }
+
+ if (iconname_or_path != NULL && access(iconname_or_path, R_OK) == 0) {
+ self->iconpath = iconname_or_path;
+ self->icon_source = ICON_SOURCE_PATH;
+ } else if (iconname_or_path != NULL) {
+ self->iconname = iconname_or_path;
+ self->icon_source = ICON_SOURCE_NAME;
+ } else if (vpixmaps != NULL) {
+ GVariant *pixmap = select_icon_by_size(vpixmaps, self->iconsize);
+ if (pixmap != NULL) {
+ self->iconpixmap = pixmap;
+ self->icon_source = ICON_SOURCE_PIXMAP;
+ }
+ } else {
+ self->iconname = g_strdup("missing-icon");
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+ }
+
+ if (vname != NULL)
+ g_variant_unref(vname);
+ if (vpixmaps != NULL)
+ g_variant_unref(vpixmaps);
+}
+
+static void
+add_icontheme_path(GDBusProxy *proxy)
+{
+ const char *iconthemepath;
+ GVariant *viconthemepath;
+ GtkIconTheme *theme;
+
+ viconthemepath = g_dbus_proxy_get_cached_property(proxy, "IconThemePath");
+ theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
+
+ if (viconthemepath != NULL) {
+ g_variant_get(viconthemepath, "&s", &iconthemepath);
+ gtk_icon_theme_add_search_path(theme, iconthemepath);
+ g_variant_unref(viconthemepath);
+ }
+}
+
+static void
+proxy_ready_handler(GObject *obj, GAsyncResult *res, void *data)
+{
+ SnItem *self = SN_ITEM(data);
+
+ GError *err = NULL;
+ GDBusProxy *proxy = g_dbus_proxy_new_for_bus_finish(res, &err);
+
+ if (err != NULL) {
+ g_warning("Failed to construct gdbusproxy for snitem: %s\n", err->message);
+ g_error_free(err);
+ g_object_unref(self);
+ return;
+ }
+ self->proxy = proxy;
+ self->proxy_id = g_signal_connect(self->proxy,
+ "g-signal",
+ G_CALLBACK(proxy_signal_handler),
+ self);
+
+ add_icontheme_path(proxy);
+ select_icon_source(self);
+
+ GdkPaintable *paintable;
+ cached_icon_t *cicon = g_malloc0(sizeof(cached_icon_t));
+
+ switch (self->icon_source) {
+ case ICON_SOURCE_NAME:
+ paintable = get_paintable_from_name(self->iconname, self->iconsize);
+ cicon->iconname = self->iconname;
+ cicon->icondata = paintable;
+ break;
+ case ICON_SOURCE_PIXMAP:
+ paintable = get_paintable_from_data(self->iconpixmap, self->iconsize);
+ cicon->iconpixmap = self->iconpixmap;
+ cicon->icondata = paintable;
+ break;
+ case ICON_SOURCE_PATH:
+ paintable = get_paintable_from_path(self->iconpath, self->iconsize);
+ cicon->iconpath = self->iconpath;
+ cicon->icondata = paintable;
+ break;
+ case ICON_SOURCE_UNKNOWN:
+ paintable = get_paintable_from_name(self->iconname, self->iconsize);
+ cicon->iconname = self->iconname;
+ cicon->icondata = paintable;
+ break;
+ default:
+ g_assert_not_reached();
+ break;
+ }
+
+ self->cachedicons = g_slist_prepend(self->cachedicons, cicon);
+ gtk_image_set_from_paintable(GTK_IMAGE(self->image), paintable);
+
+ self->lclick_id = g_signal_connect(self->lclick,
+ "pressed",
+ G_CALLBACK(leftclick_handler),
+ self);
+ connect_to_menu(self);
+
+ g_object_unref(self);
+}
+
+static void
+sn_item_notify_closed(GtkPopover *popover, void *data)
+{
+ SnItem *self = SN_ITEM(data);
+ g_object_set(self, "menuvisible", false, NULL);
+}
+
+static void
+sn_item_measure(GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ SnItem *self = SN_ITEM(widget);
+
+ switch (orientation) {
+ case GTK_ORIENTATION_HORIZONTAL:
+ *natural = self->iconsize;
+ *minimum = self->iconsize;
+ *minimum_baseline = -1;
+ *natural_baseline = -1;
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ *natural = self->iconsize;
+ *minimum = self->iconsize;
+ *minimum_baseline = -1;
+ *natural_baseline = -1;
+ break;
+ }
+}
+
+static void
+sn_item_size_allocate(GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ SnItem *self = SN_ITEM(widget);
+ gtk_widget_size_allocate(self->image, &(GtkAllocation) {0, 0, width, height}, -1);
+ gtk_popover_present(GTK_POPOVER(self->popovermenu));
+}
+
+static void
+sn_item_set_property(GObject *object,
+ unsigned int property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SnItem *self = SN_ITEM(object);
+
+ switch (property_id) {
+ case PROP_BUSNAME:
+ self->busname = g_strdup(g_value_get_string(value));
+ break;
+ case PROP_BUSOBJ:
+ self->busobj = g_strdup(g_value_get_string(value));
+ break;
+ case PROP_ICONSIZE:
+ self->iconsize = g_value_get_int(value);
+ break;
+ case PROP_DBUSMENU:
+ self->dbusmenu = g_value_get_object(value);
+ break;
+ case PROP_MENUVISIBLE:
+ self->menu_visible = g_value_get_boolean(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_item_get_property(GObject *object, unsigned int property_id, GValue *value, GParamSpec *pspec)
+{
+ SnItem *self = SN_ITEM(object);
+
+ switch (property_id) {
+ case PROP_BUSNAME:
+ g_value_set_string(value, self->busname);
+ break;
+ case PROP_BUSOBJ:
+ g_value_set_string(value, self->busobj);
+ break;
+ case PROP_ICONSIZE:
+ g_value_set_int(value, self->iconsize);
+ break;
+ case PROP_DBUSMENU:
+ g_value_set_object(value, self->dbusmenu);
+ break;
+ case PROP_MENUVISIBLE:
+ g_value_set_boolean(value, self->menu_visible);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+sn_item_class_init(SnItemClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
+
+ widget_class->measure = sn_item_measure;
+ widget_class->size_allocate = sn_item_size_allocate;
+
+ object_class->set_property = sn_item_set_property;
+ object_class->get_property = sn_item_get_property;
+
+ object_class->constructed = sn_item_constructed;
+ object_class->dispose = sn_item_dispose;
+ object_class->finalize = sn_item_finalize;
+
+ obj_properties[PROP_BUSNAME] =
+ g_param_spec_string("busname", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_BUSOBJ] =
+ g_param_spec_string("busobj", NULL, NULL,
+ NULL,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_ICONSIZE] =
+ g_param_spec_int("iconsize", NULL, NULL,
+ INT_MIN,
+ INT_MAX,
+ 22,
+ G_PARAM_WRITABLE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_DBUSMENU] =
+ g_param_spec_object("dbusmenu", NULL, NULL,
+ SN_TYPE_DBUSMENU,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ obj_properties[PROP_MENUVISIBLE] =
+ g_param_spec_boolean("menuvisible", NULL, NULL,
+ false,
+ G_PARAM_CONSTRUCT |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties(object_class, N_PROPERTIES, obj_properties);
+
+ signals[RIGHTCLICK] = g_signal_new("rightclick",
+ SN_TYPE_ITEM,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_css_name(widget_class, "systray-item");
+}
+
+static void
+sn_item_init(SnItem *self)
+{
+ GtkWidget *widget = GTK_WIDGET(self);
+
+ self->in_destruction = false;
+ self->icon_source = ICON_SOURCE_UNKNOWN;
+
+ self->image = gtk_image_new();
+ gtk_widget_set_parent(self->image, widget);
+
+ self->init_menu = g_menu_new();
+ self->popovermenu = gtk_popover_menu_new_from_model(G_MENU_MODEL(self->init_menu));
+ gtk_popover_menu_set_flags(GTK_POPOVER_MENU(self->popovermenu), GTK_POPOVER_MENU_NESTED);
+ gtk_popover_set_has_arrow(GTK_POPOVER(self->popovermenu), false);
+ gtk_widget_set_parent(self->popovermenu, widget);
+
+ self->lclick = gtk_gesture_click_new();
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->lclick), 1);
+ gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(self->lclick));
+
+ self->rclick = gtk_gesture_click_new();
+ gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(self->rclick), 3);
+ gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(self->rclick));
+
+ g_signal_connect(self->popovermenu, "closed", G_CALLBACK(sn_item_notify_closed), self);
+}
+
+static void
+sn_item_constructed(GObject *obj)
+{
+ SnItem *self = SN_ITEM(obj);
+
+ GDBusNodeInfo *nodeinfo = g_dbus_node_info_new_for_xml(STATUSNOTIFIERITEM_XML, NULL);
+ g_dbus_proxy_new_for_bus(G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ nodeinfo->interfaces[0],
+ self->busname,
+ self->busobj,
+ "org.kde.StatusNotifierItem",
+ NULL,
+ proxy_ready_handler,
+ g_object_ref(self));
+ g_dbus_node_info_unref(nodeinfo);
+
+ G_OBJECT_CLASS(sn_item_parent_class)->constructed(obj);
+}
+
+static void
+sn_item_dispose(GObject *obj)
+{
+ SnItem *self = SN_ITEM(obj);
+
+ self->in_destruction = true;
+
+ g_clear_signal_handler(&self->lclick_id, self->lclick);
+ g_clear_signal_handler(&self->rclick_id, self->rclick);
+ g_clear_signal_handler(&self->proxy_id, self->proxy);
+ g_clear_signal_handler(&self->popup_id, self->dbusmenu);
+
+ if (self->dbusmenu != NULL) {
+ // Unref will be called from sndbusmenu dispose function
+ g_object_ref(self);
+
+ g_object_unref(self->dbusmenu);
+ self->dbusmenu = NULL;
+ }
+
+ if (self->proxy != NULL) {
+ g_object_unref(self->proxy);
+ self->proxy = NULL;
+ }
+
+ if (self->popovermenu != NULL) {
+ gtk_widget_unparent(self->popovermenu);
+ self->popovermenu = NULL;
+ g_object_unref(self->init_menu);
+ self->init_menu = NULL;
+ }
+
+ if (self->image != NULL) {
+ gtk_widget_unparent(self->image);
+ self->image = NULL;
+ }
+
+ G_OBJECT_CLASS(sn_item_parent_class)->dispose(obj);
+}
+
+static void
+sn_item_finalize(GObject *object)
+{
+ SnItem *self = SN_ITEM(object);
+
+ g_free(self->busname);
+ g_free(self->busobj);
+
+ g_slist_free_full(self->cachedicons, cachedicons_free);
+
+ G_OBJECT_CLASS(sn_item_parent_class)->finalize(object);
+}
+
+/* PUBLIC METHODS */
+void
+sn_item_set_menu_model(SnItem *self, GMenu* menu)
+{
+ g_return_if_fail(SN_IS_ITEM(self));
+ g_return_if_fail(G_IS_MENU(menu));
+
+ if (self->popovermenu == NULL)
+ return;
+
+ gtk_popover_menu_set_menu_model(GTK_POPOVER_MENU(self->popovermenu), G_MENU_MODEL(menu));
+}
+
+void
+sn_item_clear_menu_model(SnItem *self)
+{
+ g_return_if_fail(SN_IS_ITEM(self));
+
+ if (self->popovermenu == NULL)
+ return;
+
+ GtkPopoverMenu *popovermenu = GTK_POPOVER_MENU(self->popovermenu);
+ GMenuModel *menumodel = G_MENU_MODEL(self->init_menu);
+
+ gtk_popover_menu_set_menu_model(popovermenu, menumodel);
+}
+
+void
+sn_item_set_actiongroup(SnItem *self, const char *prefix, GSimpleActionGroup *group)
+{
+ g_return_if_fail(SN_IS_ITEM(self));
+ g_return_if_fail(G_IS_SIMPLE_ACTION_GROUP(group));
+
+ gtk_widget_insert_action_group(GTK_WIDGET(self),
+ prefix,
+ G_ACTION_GROUP(group));
+}
+
+void
+sn_item_clear_actiongroup(SnItem *self, const char *prefix)
+{
+ g_return_if_fail(SN_IS_ITEM(self));
+
+ gtk_widget_insert_action_group(GTK_WIDGET(self),
+ prefix,
+ NULL);
+}
+
+gboolean
+sn_item_get_popover_visible(SnItem *self)
+{
+ g_return_val_if_fail(SN_IS_ITEM(self), true);
+
+ return self->menu_visible;
+}
+
+SnItem*
+sn_item_new(const char *busname, const char *busobj, int iconsize)
+{
+ return g_object_new(SN_TYPE_ITEM,
+ "busname", busname,
+ "busobj", busobj,
+ "iconsize", iconsize,
+ NULL);
+}
+/* PUBLIC METHODS */
diff --git a/systray/snitem.h b/systray/snitem.h
@@ -0,0 +1,84 @@
+#ifndef SNITEM_H
+#define SNITEM_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_ITEM sn_item_get_type()
+G_DECLARE_FINAL_TYPE(SnItem, sn_item, SN, ITEM, GtkWidget)
+
+SnItem* sn_item_new (const char *busname,
+ const char *busobj,
+ int iconsize);
+
+char* sn_item_get_busname (SnItem *self);
+gboolean sn_item_get_popover_visible (SnItem *self);
+void sn_item_set_menu_model (SnItem *widget, GMenu *menu);
+void sn_item_set_actiongroup (SnItem *self,
+ const char *prefix,
+ GSimpleActionGroup *group);
+void sn_item_clear_actiongroup (SnItem *self, const char *prefix);
+void sn_item_clear_menu_model (SnItem *widget);
+
+G_END_DECLS
+
+#define STATUSNOTIFIERITEM_XML \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
+ "<node>\n" \
+ " <interface name=\"org.kde.StatusNotifierItem\">\n" \
+ " <!-- methods -->\n" \
+ " <method name=\"Activate\">\n" \
+ " <arg name=\"x\" type=\"i\" direction=\"in\"/>\n" \
+ " <arg name=\"y\" type=\"i\" direction=\"in\"/>\n" \
+ " </method>\n" \
+ " <!--\n" \
+ " <method name=\"Scroll\">\n" \
+ " <arg name=\"delta\" type=\"i\" direction=\"in\"/>\n" \
+ " <arg name=\"orientation\" type=\"s\" direction=\"in\"/>\n" \
+ " </method>\n" \
+ " <method name=\"ContextMenu\">\n" \
+ " <arg name=\"x\" type=\"i\" direction=\"in\"/>\n" \
+ " <arg name=\"y\" type=\"i\" direction=\"in\"/>\n" \
+ " </method>\n" \
+ " <method name=\"SecondaryActivate\">\n" \
+ " <arg name=\"x\" type=\"i\" direction=\"in\"/>\n" \
+ " <arg name=\"y\" type=\"i\" direction=\"in\"/>\n" \
+ " </method>\n" \
+ " -->\n" \
+ " <!-- properties -->\n" \
+ " <property name=\"Menu\" type=\"o\" access=\"read\"/>\n" \
+ " <property name=\"IconName\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"IconPixmap\" type=\"a(iiay)\" access=\"read\"/>\n" \
+ " <property name=\"IconThemePath\" type=\"s\" access=\"read\"/>\n" \
+ " <!--\n" \
+ " <property name=\"OverlayIconName\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"OverlayIconPixmap\" type=\"a(iiay)\" access=\"read\"/>\n" \
+ " <property name=\"AttentionIconName\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"AttentionIconPixmap\" type=\"a(iiay)\" access=\"read\"/>\n" \
+ " <property name=\"Category\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"Id\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"Title\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"Status\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"WindowId\" type=\"i\" access=\"read\"/>\n" \
+ " <property name=\"ItemIsMenu\" type=\"b\" access=\"read\"/>\n" \
+ " <property name=\"AttentionMovieName\" type=\"s\" access=\"read\"/>\n" \
+ " <property name=\"ToolTip\" type=\"(sa(iiay)ss)\" access=\"read\"/>\n" \
+ " -->\n" \
+ " <!-- signals -->\n" \
+ " <signal name=\"NewIcon\"/>\n" \
+ " <!--\n" \
+ " <signal name=\"NewAttentionIcon\"/>\n" \
+ " <signal name=\"NewOverlayIcon\"/>\n" \
+ " <signal name=\"NewTitle\"/>\n" \
+ " <signal name=\"NewToolTip\"/>\n" \
+ " <signal name=\"NewStatus\">\n" \
+ " <arg name=\"status\" type=\"s\"/>\n" \
+ " </signal>\n" \
+ " -->\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#endif /* SNITEM_H */
diff --git a/systray/snwatcher.c b/systray/snwatcher.c
@@ -0,0 +1,416 @@
+#include "snwatcher.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include <glib.h>
+#include <glib-object.h>
+#include <gio/gio.h>
+#include <gtk/gtk.h>
+
+struct _SnWatcher
+{
+ GObject parent_instance;
+
+ GDBusConnection* conn;
+ GList* tracked_items;
+
+ int owner_id;
+ int obj_reg_id;
+ int sig_sub_id;
+};
+
+G_DEFINE_FINAL_TYPE(SnWatcher, sn_watcher, G_TYPE_OBJECT)
+
+enum
+{
+ TRAYITEM_REGISTERED,
+ TRAYITEM_UNREGISTERED,
+ LAST_SIGNAL
+};
+
+static unsigned int signals[LAST_SIGNAL];
+
+static void sn_watcher_dispose (GObject *obj);
+
+static void sn_watcher_call_method_handler (GDBusConnection *conn,
+ const char *sender,
+ const char *object_path,
+ const char *interface_name,
+ const char *method_name,
+ GVariant *parameters,
+ GDBusMethodInvocation *invocation,
+ void *data);
+
+static GVariant* sn_watcher_prop_get_handler (GDBusConnection* conn,
+ const char* sender,
+ const char* object_path,
+ const char* interface_name,
+ const char* property_name,
+ GError** err,
+ void *data);
+
+static GDBusInterfaceVTable interface_vtable = {
+ sn_watcher_call_method_handler,
+ sn_watcher_prop_get_handler,
+ NULL
+};
+
+
+static void
+sn_watcher_register_item(SnWatcher *self,
+ const char *busname,
+ const char *busobj)
+{
+ // Check if we are already tracking this item
+ if (g_list_find_custom(self->tracked_items, busname,(GCompareFunc)strcmp)
+ != NULL) {
+ return;
+ }
+
+ g_debug("Registering %s", busname);
+ self->tracked_items = g_list_prepend(self->tracked_items, g_strdup(busname));
+
+ g_signal_emit(self, signals[TRAYITEM_REGISTERED], 0, busname, busobj);
+
+ // Dbus signal is emitted only to conform to the specification.
+ // We don't use this ourselves.
+ GError *err = NULL;
+ g_dbus_connection_emit_signal(self->conn,
+ NULL,
+ "/StatusNotifierWatcher",
+ "org.kde.StatusNotifierWatcher",
+ "StatusNotifierItemRegistered",
+ g_variant_new("(s)", busname),
+ &err);
+ if (err) {
+ g_warning("%s", err->message);
+ g_error_free(err);
+ }
+}
+
+static void
+sn_watcher_unregister_item(SnWatcher *self, const char *busname)
+{
+ g_debug("Unregistering %s", busname);
+
+ g_signal_emit(self, signals[TRAYITEM_UNREGISTERED], 0, busname);
+
+ // Dbus signal is emitted only to conform to the specification.
+ // We don't use this ourselves.
+ GError *err = NULL;
+ g_dbus_connection_emit_signal(self->conn,
+ NULL,
+ "/StatusNotifierWatcher",
+ "org.kde.StatusNotifierWatcher",
+ "StatusNotifierItemUnregistered",
+ g_variant_new("(s)", busname),
+ &err);
+ if (err) {
+ g_warning("%s", err->message);
+ g_error_free(err);
+ }
+}
+
+static void
+bus_get_snitems_helper(void *data, void *udata)
+{
+ char *busname = (char*)data;
+ GVariantBuilder *builder = (GVariantBuilder*)udata;
+
+ g_variant_builder_add_value(builder, g_variant_new_string(busname));
+}
+
+static GVariant*
+sn_watcher_prop_get_handler(GDBusConnection* conn,
+ const char* sender,
+ const char* object_path,
+ const char* interface_name,
+ const char* property_name,
+ GError** err,
+ void *data)
+{
+ SnWatcher *self = SN_WATCHER(data);
+
+ 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 (!self->tracked_items)
+ return g_variant_new("as", NULL);
+
+ GVariantBuilder *builder = g_variant_builder_new(G_VARIANT_TYPE_ARRAY);
+ g_list_foreach(self->tracked_items, bus_get_snitems_helper, 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);
+ return NULL;
+ }
+}
+
+
+static void
+sn_watcher_call_method_handler(GDBusConnection *conn,
+ const char *sender,
+ const char *obj_path,
+ const char *iface_name,
+ const char *method_name,
+ GVariant *params,
+ GDBusMethodInvocation *invoc,
+ void *data)
+{
+ SnWatcher *self = SN_WATCHER(data);
+
+ if (strcmp(method_name, "RegisterStatusNotifierItem") == 0) {
+ const char *param;
+ const char *busobj;
+ const char *registree_name;
+
+ g_variant_get(params, "(&s)", ¶m);
+
+ if (g_str_has_prefix(param, "/"))
+ busobj = param;
+ else
+ busobj = "/StatusNotifierItem";
+
+ if (g_str_has_prefix(param, ":"))
+ registree_name = param;
+ else
+ registree_name = sender;
+
+ sn_watcher_register_item(self, registree_name, busobj);
+ g_dbus_method_invocation_return_value(invoc, NULL);
+
+ } else {
+ g_dbus_method_invocation_return_dbus_error(invoc,
+ "org.freedesktop.DBus.Error.UnknownMethod",
+ "Unknown method");
+ }
+}
+
+static void
+sn_watcher_monitor_bus(GDBusConnection* conn,
+ const char* sender,
+ const char* objpath,
+ const char* iface_name,
+ const char* signame,
+ GVariant *params,
+ void *data)
+{
+ SnWatcher *self = SN_WATCHER(data);
+
+ if (strcmp(signame, "NameOwnerChanged") == 0) {
+ if (!self->tracked_items)
+ return;
+
+ const char *name;
+ const char *old_owner;
+ const char *new_owner;
+ g_variant_get(params, "(&s&s&s)", &name, &old_owner, &new_owner);
+ if (strcmp(new_owner, "") == 0) {
+ GList *pmatch = g_list_find_custom(self->tracked_items,
+ name,
+ (GCompareFunc)strcmp);
+ if (pmatch) {
+ sn_watcher_unregister_item(self, pmatch->data);
+ g_free(pmatch->data);
+ self->tracked_items = g_list_delete_link(self->tracked_items,
+ pmatch);
+ }
+ }
+ }
+}
+
+static void
+sn_watcher_unregister_all(SnWatcher *self)
+{
+ GList *tmp = self->tracked_items;
+
+ while (tmp) {
+ GList *next = tmp->next;
+ sn_watcher_unregister_item(self, tmp->data);
+ g_free(tmp->data);
+ self->tracked_items = g_list_delete_link(self->tracked_items, tmp);
+ tmp = next;
+ }
+}
+
+static void
+sn_watcher_bus_acquired_handler(GDBusConnection *conn, const char *busname, void *data)
+{
+ SnWatcher *self = SN_WATCHER(data);
+
+ self->conn = conn;
+
+ GError *err = NULL;
+ GDBusNodeInfo *nodeinfo = g_dbus_node_info_new_for_xml(STATUSNOTIFIERWATCHER_XML, NULL);
+
+ self->obj_reg_id =
+ g_dbus_connection_register_object(self->conn,
+ "/StatusNotifierWatcher",
+ nodeinfo->interfaces[0],
+ &interface_vtable,
+ self,
+ NULL,
+ &err);
+
+ g_dbus_node_info_unref(nodeinfo);
+
+ if (err) {
+ g_error("%s", err->message);
+ g_error_free(err);
+ exit(-1);
+ }
+
+ self->sig_sub_id =
+ g_dbus_connection_signal_subscribe(self->conn,
+ NULL, // Listen to all senders);
+ "org.freedesktop.DBus",
+ "NameOwnerChanged",
+ NULL, // Match all obj paths
+ NULL, // Match all arg0s
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ sn_watcher_monitor_bus,
+ self,
+ NULL);
+}
+
+static void
+sn_watcher_name_acquired_handler(GDBusConnection *conn, const char *busname, void *data)
+{
+ SnWatcher *self = SN_WATCHER(data);
+
+ GError *err = NULL;
+
+ g_dbus_connection_emit_signal(self->conn,
+ NULL,
+ "/StatusNotifierWatcher",
+ "org.kde.StatusNotifierWatcher",
+ "StatusNotifierHostRegistered",
+ NULL,
+ &err);
+
+ if (err) {
+ g_warning("%s", err->message);
+ g_error_free(err);
+ }
+}
+
+static void
+sn_watcher_name_lost_handler(GDBusConnection *conn, const char *busname, void *data)
+{
+ g_error("Could not acquire %s, maybe another instance is running?", busname);
+ exit(-1);
+}
+
+static GObject*
+sn_watcher_constructor(GType type,
+ unsigned int n_construct_properties,
+ GObjectConstructParam *construct_properties)
+{
+ static GObject *singleton = NULL;
+
+ if (singleton == NULL) {
+ singleton =
+ G_OBJECT_CLASS(sn_watcher_parent_class)->constructor(type,
+ n_construct_properties,
+ construct_properties);
+ g_object_add_weak_pointer(singleton, (void*)&singleton);
+
+ return singleton;
+ }
+
+ return g_object_ref(singleton);
+}
+
+
+static void
+sn_watcher_class_init(SnWatcherClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS(klass);
+
+ object_class->constructor = sn_watcher_constructor;
+ object_class->dispose = sn_watcher_dispose;
+
+ signals[TRAYITEM_REGISTERED] = g_signal_new("trayitem-registered",
+ SN_TYPE_WATCHER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_STRING,
+ G_TYPE_STRING);
+
+ signals[TRAYITEM_UNREGISTERED] = g_signal_new("trayitem-unregistered",
+ SN_TYPE_WATCHER,
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_STRING);
+}
+
+static void
+sn_watcher_init(SnWatcher *self)
+{
+ self->owner_id =
+ g_bus_own_name(G_BUS_TYPE_SESSION,
+ "org.kde.StatusNotifierWatcher",
+ G_BUS_NAME_OWNER_FLAGS_NONE,
+ sn_watcher_bus_acquired_handler,
+ sn_watcher_name_acquired_handler,
+ sn_watcher_name_lost_handler,
+ self,
+ NULL);
+
+ g_debug("Created snwatcher");
+}
+
+static void
+sn_watcher_dispose(GObject *obj)
+{
+ g_debug("Disposing snwatcher");
+ SnWatcher *self = SN_WATCHER(obj);
+
+ if (self->sig_sub_id > 0) {
+ g_dbus_connection_signal_unsubscribe(self->conn, self->sig_sub_id);
+ self->sig_sub_id = 0;
+ }
+
+ if (self->obj_reg_id > 0) {
+ g_dbus_connection_unregister_object(self->conn, self->obj_reg_id);
+ self->obj_reg_id = 0;
+ }
+
+ if (self->tracked_items) {
+ sn_watcher_unregister_all(self);
+ self->tracked_items = NULL;
+ }
+
+ if (self->owner_id > 0) {
+ g_bus_unown_name(self->owner_id);
+ self->owner_id = 0;
+ self->conn = NULL;
+ }
+
+ G_OBJECT_CLASS(sn_watcher_parent_class)->dispose(obj);
+}
+
+SnWatcher*
+sn_watcher_new(void)
+{
+ return g_object_new(SN_TYPE_WATCHER, NULL);
+}
diff --git a/systray/snwatcher.h b/systray/snwatcher.h
@@ -0,0 +1,39 @@
+#ifndef SNWATCHER_H
+#define SNWATCHER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SN_TYPE_WATCHER sn_watcher_get_type()
+G_DECLARE_FINAL_TYPE(SnWatcher, sn_watcher, SN, WATCHER, GObject)
+
+SnWatcher *sn_watcher_new (void);
+
+G_END_DECLS
+
+#define STATUSNOTIFIERWATCHER_XML \
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" \
+ "<node>\n" \
+ " <interface name=\"org.kde.StatusNotifierWatcher\">\n" \
+ " <!-- methods -->\n" \
+ " <method name=\"RegisterStatusNotifierItem\">\n" \
+ " <arg name=\"service\" type=\"s\" direction=\"in\" />\n" \
+ " </method>\n" \
+ " <!-- properties -->\n" \
+ " <property name=\"IsStatusNotifierHostRegistered\" type=\"b\" access=\"read\" />\n" \
+ " <property name=\"ProtocolVersion\" type=\"i\" access=\"read\" />\n" \
+ " <property name=\"RegisteredStatusNotifierItems\" type=\"as\" access=\"read\" />\n" \
+ " <!-- signals -->\n" \
+ " <signal name=\"StatusNotifierItemRegistered\">\n" \
+ " <arg type=\"s\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"StatusNotifierItemUnregistered\">\n" \
+ " <arg type=\"s\"/>\n" \
+ " </signal>\n" \
+ " <signal name=\"StatusNotifierHostRegistered\">\n" \
+ " </signal>\n" \
+ " </interface>\n" \
+ "</node>\n"
+
+#endif /* SNWATCHER_H */