dwlb

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

commit ea62b131108e433a5642e5054b91f8dd3b7e55d3
parent 55cebeb4261c59a9819b93290da089e2eb80d8d1
Author: awy <awy@awy.one>
Date:   Sat, 15 Nov 2025 14:20:28 +0300

Merge branch 'systray'

Diffstat:
M.gitignore | 2++
MMakefile | 10++++++++--
MREADME.md | 5++++-
Mdwlb.c | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Asystray/.gitignore | 6++++++
Asystray/Makefile | 35+++++++++++++++++++++++++++++++++++
Asystray/dwlbtray.c | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/sndbusmenu.c | 669+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/sndbusmenu.h | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/snhost.c | 351+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/snhost.h | 21+++++++++++++++++++++
Asystray/snitem.c | 1160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/snitem.h | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/snwatcher.c | 416+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asystray/snwatcher.h | 39+++++++++++++++++++++++++++++++++++++++
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)", &param); + + 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 */