diff --git a/i3bar/include/common.h b/i3bar/include/common.h index 644b777c..74bd2152 100644 --- a/i3bar/include/common.h +++ b/i3bar/include/common.h @@ -9,8 +9,9 @@ #ifndef COMMON_H_ #define COMMON_H_ +#include + typedef struct rect_t rect; -typedef int bool; struct ev_loop* main_loop; char *statusline; @@ -29,6 +30,7 @@ struct rect_t { #include "outputs.h" #include "util.h" #include "workspaces.h" +#include "trayclients.h" #include "xcb.h" #include "ucs2_to_utf8.h" #include "config.h" diff --git a/i3bar/include/outputs.h b/i3bar/include/outputs.h index 41712ac3..c6402a5b 100644 --- a/i3bar/include/outputs.h +++ b/i3bar/include/outputs.h @@ -47,6 +47,7 @@ struct i3_output { xcb_gcontext_t bargc; /* The graphical context of the bar */ struct ws_head *workspaces; /* The workspaces on this output */ + struct tc_head *trayclients; /* The tray clients on this output */ SLIST_ENTRY(i3_output) slist; /* Pointer for the SLIST-Macro */ }; diff --git a/i3bar/include/trayclients.h b/i3bar/include/trayclients.h new file mode 100644 index 00000000..1113daeb --- /dev/null +++ b/i3bar/include/trayclients.h @@ -0,0 +1,26 @@ +/* + * i3bar - an xcb-based status- and ws-bar for i3 + * + * © 2010-2011 Axel Wagner and contributors + * + * See file LICNSE for license information + * + */ +#ifndef TRAYCLIENT_H_ +#define TRAYCLIENT_H_ + +#include "common.h" + +typedef struct trayclient trayclient; + +TAILQ_HEAD(tc_head, trayclient); + +struct trayclient { + xcb_window_t win; /* The window ID of the tray client */ + bool mapped; /* Whether this window is mapped */ + int xe_version; /* The XEMBED version supported by the client */ + + TAILQ_ENTRY(trayclient) tailq; /* Pointer for the TAILQ-Macro */ +}; + +#endif diff --git a/i3bar/include/xcb.h b/i3bar/include/xcb.h index 531fdfe9..c1b7cc14 100644 --- a/i3bar/include/xcb.h +++ b/i3bar/include/xcb.h @@ -12,6 +12,18 @@ #include //#include "outputs.h" +#ifdef XCB_COMPAT +#define XCB_ATOM_CARDINAL CARDINAL +#endif + +#define _NET_SYSTEM_TRAY_ORIENTATION_HORZ 0 +#define _NET_SYSTEM_TRAY_ORIENTATION_VERT 1 +#define SYSTEM_TRAY_REQUEST_DOCK 0 +#define SYSTEM_TRAY_BEGIN_MESSAGE 1 +#define SYSTEM_TRAY_CANCEL_MESSAGE 2 +#define XEMBED_MAPPED (1 << 0) +#define XEMBED_EMBEDDED_NOTIFY 0 + struct xcb_color_strings_t { char *bar_fg; char *bar_bg; diff --git a/i3bar/include/xcb_atoms.def b/i3bar/include/xcb_atoms.def index 5d168873..b75ceabd 100644 --- a/i3bar/include/xcb_atoms.def +++ b/i3bar/include/xcb_atoms.def @@ -2,4 +2,10 @@ ATOM_DO(_NET_WM_WINDOW_TYPE) ATOM_DO(_NET_WM_WINDOW_TYPE_DOCK) ATOM_DO(_NET_WM_STRUT_PARTIAL) ATOM_DO(I3_SOCKET_PATH) +ATOM_DO(MANAGER) +ATOM_DO(_NET_SYSTEM_TRAY_ORIENTATION) +ATOM_DO(_NET_SYSTEM_TRAY_VISUAL) +ATOM_DO(_NET_SYSTEM_TRAY_OPCODE) +ATOM_DO(_XEMBED_INFO) +ATOM_DO(_XEMBED) #undef ATOM_DO diff --git a/i3bar/src/outputs.c b/i3bar/src/outputs.c index 9daf328d..464f24a0 100644 --- a/i3bar/src/outputs.c +++ b/i3bar/src/outputs.c @@ -43,7 +43,7 @@ static int outputs_null_cb(void *params_) { * Parse a boolean value (active) * */ -static int outputs_boolean_cb(void *params_, bool val) { +static int outputs_boolean_cb(void *params_, int val) { struct outputs_json_params *params = (struct outputs_json_params*) params_; if (strcmp(params->cur_key, "active")) { @@ -161,6 +161,9 @@ static int outputs_start_map_cb(void *params_) { new_output->workspaces = malloc(sizeof(struct ws_head)); TAILQ_INIT(new_output->workspaces); + new_output->trayclients = malloc(sizeof(struct tc_head)); + TAILQ_INIT(new_output->trayclients); + params->outputs_walk = new_output; return 1; diff --git a/i3bar/src/workspaces.c b/i3bar/src/workspaces.c index eeb9ca34..a84e152b 100644 --- a/i3bar/src/workspaces.c +++ b/i3bar/src/workspaces.c @@ -29,7 +29,7 @@ struct workspaces_json_params { * Parse a boolean value (visible, focused, urgent) * */ -static int workspaces_boolean_cb(void *params_, bool val) { +static int workspaces_boolean_cb(void *params_, int val) { struct workspaces_json_params *params = (struct workspaces_json_params*) params_; if (!strcmp(params->cur_key, "visible")) { diff --git a/i3bar/src/xcb.c b/i3bar/src/xcb.c index 28ef3ea2..e25bc959 100644 --- a/i3bar/src/xcb.c +++ b/i3bar/src/xcb.c @@ -63,6 +63,7 @@ xcb_atom_t atoms[NUM_ATOMS]; /* Variables, that are the same for all functions at all times */ xcb_connection_t *xcb_connection; +int screen; xcb_screen_t *xcb_screen; xcb_window_t xcb_root; xcb_font_t xcb_font; @@ -383,6 +384,252 @@ void handle_button(xcb_button_press_event_t *event) { i3_send_msg(I3_IPC_MESSAGE_TYPE_COMMAND, buffer); } +/* + * Configures the x coordinate of all trayclients. To be called after adding a + * new tray client or removing an old one. + * + */ +static void configure_trayclients() { + trayclient *trayclient; + i3_output *output; + SLIST_FOREACH(output, outputs, slist) { + if (!output->active) + continue; + + int clients = 0; + TAILQ_FOREACH_REVERSE(trayclient, output->trayclients, tc_head, tailq) { + clients++; + + DLOG("Configuring tray window %08x to x=%d\n", + trayclient->win, output->rect.w - (clients * (font_height + 2))); + uint32_t x = output->rect.w - (clients * (font_height + 2)); + xcb_configure_window(xcb_connection, + trayclient->win, + XCB_CONFIG_WINDOW_X, + &x); + } + } +} + +/* + * Handles ClientMessages (messages sent from another client directly to us). + * + * At the moment, only the tray window will receive client messages. All + * supported client messages currently are _NET_SYSTEM_TRAY_OPCODE. + * + */ +static void handle_client_message(xcb_client_message_event_t* event) { + if (event->type == atoms[_NET_SYSTEM_TRAY_OPCODE] && + event->format == 32) { + DLOG("_NET_SYSTEM_TRAY_OPCODE received\n"); + /* event->data.data32[0] is the timestamp */ + uint32_t op = event->data.data32[1]; + uint32_t mask; + uint32_t values[2]; + if (op == SYSTEM_TRAY_REQUEST_DOCK) { + xcb_window_t client = event->data.data32[2]; + + /* Listen for PropertyNotify events to get the most recent value of + * the XEMBED_MAPPED atom, also listen for UnmapNotify events */ + mask = XCB_CW_EVENT_MASK; + values[0] = XCB_EVENT_MASK_PROPERTY_CHANGE | + XCB_EVENT_MASK_STRUCTURE_NOTIFY; + xcb_change_window_attributes(xcb_connection, + client, + mask, + values); + + /* Request the _XEMBED_INFO property. The XEMBED specification + * (which is referred by the tray specification) says this *has* to + * be set, but VLC does not set it… */ + bool map_it = true; + int xe_version = 1; + xcb_get_property_cookie_t xembedc; + xembedc = xcb_get_property_unchecked(xcb_connection, + 0, + client, + atoms[_XEMBED_INFO], + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 2 * 32); + + xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection, + xembedc, + NULL); + if (xembedr != NULL && xembedr->length != 0) { + DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length); + uint32_t *xembed = xcb_get_property_value(xembedr); + DLOG("xembed version = %d\n", xembed[0]); + DLOG("xembed flags = %d\n", xembed[1]); + map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED); + xe_version = xembed[0]; + if (xe_version > 1) + xe_version = 1; + free(xembedr); + } else { + ELOG("Window %08x violates the XEMBED protocol, _XEMBED_INFO not set\n", client); + } + + DLOG("X window %08x requested docking\n", client); + i3_output *walk, *output; + SLIST_FOREACH(walk, outputs, slist) { + if (!walk->active) + continue; + DLOG("using output %s\n", walk->name); + output = walk; + } + xcb_reparent_window(xcb_connection, + client, + output->bar, + output->rect.w - font_height - 2, + 2); + /* We reconfigure the window to use a reasonable size. The systray + * specification explicitly says: + * Tray icons may be assigned any size by the system tray, and + * should do their best to cope with any size effectively + */ + mask = XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT; + values[0] = font_height; + values[1] = font_height; + xcb_configure_window(xcb_connection, + client, + mask, + values); + + /* send the XEMBED_EMBEDDED_NOTIFY message */ + void *event = calloc(32, 1); + xcb_client_message_event_t *ev = event; + ev->response_type = XCB_CLIENT_MESSAGE; + ev->window = client; + ev->type = atoms[_XEMBED]; + ev->format = 32; + ev->data.data32[0] = XCB_CURRENT_TIME; + ev->data.data32[1] = atoms[XEMBED_EMBEDDED_NOTIFY]; + ev->data.data32[2] = output->bar; + ev->data.data32[3] = xe_version; + xcb_send_event(xcb_connection, + 0, + client, + XCB_EVENT_MASK_NO_EVENT, + (char*)ev); + free(event); + + if (map_it) { + DLOG("Mapping dock client\n"); + xcb_map_window(xcb_connection, client); + } else { + DLOG("Not mapping dock client yet\n"); + } + trayclient *tc = malloc(sizeof(trayclient)); + tc->win = client; + tc->mapped = map_it; + tc->xe_version = xe_version; + TAILQ_INSERT_TAIL(output->trayclients, tc, tailq); + + /* Trigger an update to copy the statusline text to the appropriate + * position */ + configure_trayclients(); + draw_bars(); + } + } +} + +/* + * Handles UnmapNotify events. These events happen when a tray window unmaps + * itself. We then update our data structure + * + */ +static void handle_unmap_notify(xcb_unmap_notify_event_t* event) { + DLOG("UnmapNotify for window = %08x, event = %08x\n", event->window, event->event); + + i3_output *walk; + SLIST_FOREACH(walk, outputs, slist) { + if (!walk->active) + continue; + DLOG("checking output %s\n", walk->name); + trayclient *trayclient; + TAILQ_FOREACH(trayclient, walk->trayclients, tailq) { + if (trayclient->win != event->window) + continue; + + DLOG("Removing tray client with window ID %08x\n", event->window); + TAILQ_REMOVE(walk->trayclients, trayclient, tailq); + + /* Trigger an update, we now have more space for the statusline */ + configure_trayclients(); + draw_bars(); + return; + } + } +} + +/* + * Handle PropertyNotify messages. Currently only the _XEMBED_INFO property is + * handled, which tells us whether a dock client should be mapped or unmapped. + * + */ +static void handle_property_notify(xcb_property_notify_event_t *event) { + DLOG("PropertyNotify\n"); + if (event->atom == atoms[_XEMBED_INFO] && + event->state == XCB_PROPERTY_NEW_VALUE) { + DLOG("xembed_info updated\n"); + trayclient *trayclient = NULL, *walk; + i3_output *o_walk; + SLIST_FOREACH(o_walk, outputs, slist) { + if (!o_walk->active) + continue; + + TAILQ_FOREACH(walk, o_walk->trayclients, tailq) { + if (walk->win != event->window) + continue; + trayclient = walk; + break; + } + + if (trayclient) + break; + } + if (!trayclient) { + ELOG("PropertyNotify received for unknown window %08x\n", + event->window); + return; + } + xcb_get_property_cookie_t xembedc; + xembedc = xcb_get_property_unchecked(xcb_connection, + 0, + trayclient->win, + atoms[_XEMBED_INFO], + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 2 * 32); + + xcb_get_property_reply_t *xembedr = xcb_get_property_reply(xcb_connection, + xembedc, + NULL); + if (xembedr == NULL || xembedr->length == 0) + return; + + DLOG("xembed format = %d, len = %d\n", xembedr->format, xembedr->length); + uint32_t *xembed = xcb_get_property_value(xembedr); + DLOG("xembed version = %d\n", xembed[0]); + DLOG("xembed flags = %d\n", xembed[1]); + bool map_it = ((xembed[1] & XEMBED_MAPPED) == XEMBED_MAPPED); + DLOG("map-state now %d\n", map_it); + if (trayclient->mapped && !map_it) { + /* need to unmap the window */ + xcb_unmap_window(xcb_connection, trayclient->win); + trayclient->mapped = map_it; + draw_bars(); + } else if (!trayclient->mapped && map_it) { + /* need to map the window */ + xcb_map_window(xcb_connection, trayclient->win); + trayclient->mapped = map_it; + draw_bars(); + } + free(xembedr); + } +} + /* * This function is called immediately before the main loop locks. We flush xcb * then (and only then) @@ -413,6 +660,19 @@ void xcb_chk_cb(struct ev_loop *loop, ev_check *watcher, int revents) { /* Button-press-events are mouse-buttons clicked on one of our bars */ handle_button((xcb_button_press_event_t*) event); break; + case XCB_CLIENT_MESSAGE: + /* Client messages are used for client-to-client communication, for + * example system tray widgets talk to us directly via client messages. */ + handle_client_message((xcb_client_message_event_t*) event); + break; + case XCB_UNMAP_NOTIFY: + /* UnmapNotifies are received when a tray window unmaps itself */ + handle_unmap_notify((xcb_unmap_notify_event_t*) event); + break; + case XCB_PROPERTY_NOTIFY: + /* PropertyNotify */ + handle_property_notify((xcb_property_notify_event_t*) event); + break; } FREE(event); } @@ -470,7 +730,7 @@ void xkb_io_cb(struct ev_loop *loop, ev_io *watcher, int revents) { */ char *init_xcb(char *fontname) { /* FIXME: xcb_connect leaks Memory */ - xcb_connection = xcb_connect(NULL, NULL); + xcb_connection = xcb_connect(NULL, &screen); if (xcb_connection_has_error(xcb_connection)) { ELOG("Cannot open display\n"); exit(EXIT_FAILURE); @@ -634,6 +894,95 @@ char *init_xcb(char *fontname) { return path; } +/* + * Initializes tray support by requesting the appropriate _NET_SYSTEM_TRAY atom + * for the X11 display we are running on, then acquiring the selection for this + * atom. Afterwards, tray clients will send ClientMessages to our window. + * + */ +void init_tray() { + /* request the tray manager atom for the X11 display we are running on */ + char atomname[strlen("_NET_SYSTEM_TRAY_S") + 11]; + snprintf(atomname, strlen("_NET_SYSTEM_TRAY_S") + 11, "_NET_SYSTEM_TRAY_S%d", screen); + xcb_intern_atom_cookie_t tray_cookie; + xcb_intern_atom_reply_t *tray_reply; + tray_cookie = xcb_intern_atom(xcb_connection, 0, strlen(atomname), atomname); + + /* tray support: we need a window to own the selection */ + xcb_window_t selwin = xcb_generate_id(xcb_connection); + uint32_t selmask = XCB_CW_OVERRIDE_REDIRECT; + uint32_t selval[] = { 1 }; + xcb_create_window(xcb_connection, + xcb_screen->root_depth, + selwin, + xcb_root, + -1, -1, + 1, 1, + 1, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + xcb_screen->root_visual, + selmask, + selval); + + uint32_t orientation = _NET_SYSTEM_TRAY_ORIENTATION_HORZ; + /* set the atoms */ + xcb_change_property(xcb_connection, + XCB_PROP_MODE_REPLACE, + selwin, + atoms[_NET_SYSTEM_TRAY_ORIENTATION], + XCB_ATOM_CARDINAL, + 32, + 1, + &orientation); + + if (!(tray_reply = xcb_intern_atom_reply(xcb_connection, tray_cookie, NULL))) { + ELOG("Could not get atom %s\n", atomname); + exit(EXIT_FAILURE); + } + + xcb_set_selection_owner(xcb_connection, + selwin, + tray_reply->atom, + XCB_CURRENT_TIME); + + /* Verify that we have the selection */ + xcb_get_selection_owner_cookie_t selcookie; + xcb_get_selection_owner_reply_t *selreply; + + selcookie = xcb_get_selection_owner(xcb_connection, tray_reply->atom); + if (!(selreply = xcb_get_selection_owner_reply(xcb_connection, selcookie, NULL))) { + ELOG("Could not get selection owner for %s\n", atomname); + exit(EXIT_FAILURE); + } + + if (selreply->owner != selwin) { + ELOG("Could not set the %s selection. " \ + "Maybe another tray is already running?\n", atomname); + /* NOTE that this error is not fatal. We just can’t provide tray + * functionality */ + free(selreply); + return; + } + + /* Inform clients waiting for a new _NET_SYSTEM_TRAY that we are here */ + void *event = calloc(32, 1); + xcb_client_message_event_t *ev = event; + ev->response_type = XCB_CLIENT_MESSAGE; + ev->window = xcb_root; + ev->type = atoms[MANAGER]; + ev->format = 32; + ev->data.data32[0] = XCB_CURRENT_TIME; + ev->data.data32[1] = tray_reply->atom; + ev->data.data32[2] = selwin; + xcb_send_event(xcb_connection, + 0, + xcb_root, + XCB_EVENT_MASK_STRUCTURE_NOTIFY, + (char*)ev); + free(event); + free(tray_reply); +} + /* * Cleanup the xcb-stuff. * Called once, before the program terminates. @@ -641,15 +990,27 @@ char *init_xcb(char *fontname) { */ void clean_xcb() { i3_output *o_walk; + trayclient *trayclient; free_workspaces(); SLIST_FOREACH(o_walk, outputs, slist) { + TAILQ_FOREACH(trayclient, o_walk->trayclients, tailq) { + /* Unmap, then reparent (to root) the tray client windows */ + xcb_unmap_window(xcb_connection, trayclient->win); + xcb_reparent_window(xcb_connection, + trayclient->win, + xcb_root, + 0, + 0); + } destroy_window(o_walk); + FREE(o_walk->trayclients); FREE(o_walk->workspaces); FREE(o_walk->name); } FREE_SLIST(outputs, i3_output); FREE(outputs); + xcb_flush(xcb_connection); xcb_disconnect(xcb_connection); ev_check_stop(main_loop, xcb_chk); @@ -757,6 +1118,9 @@ void reconfig_windows() { if (walk->bar == XCB_NONE) { DLOG("Creating Window for output %s\n", walk->name); + /* TODO: only call init_tray() if the tray is configured for this output */ + init_tray(); + walk->bar = xcb_generate_id(xcb_connection); walk->buffer = xcb_generate_id(xcb_connection); mask = XCB_CW_BACK_PIXEL | XCB_CW_OVERRIDE_REDIRECT | XCB_CW_EVENT_MASK; @@ -946,13 +1310,26 @@ void draw_bars() { /* Luckily we already prepared a seperate pixmap containing the rendered * statusline, we just have to copy the relevant parts to the relevant * position */ + trayclient *trayclient; + int traypx = 0; + TAILQ_FOREACH(trayclient, outputs_walk->trayclients, tailq) { + if (!trayclient->mapped) + continue; + /* We assume the tray icons are quadratic (we use the font + * *height* as *width* of the icons) because we configured them + * like this. */ + traypx += font_height; + } + /* Add 2px of padding if there are any tray icons */ + if (traypx > 0) + traypx += 2; xcb_copy_area(xcb_connection, statusline_pm, outputs_walk->buffer, outputs_walk->bargc, MAX(0, (int16_t)(statusline_width - outputs_walk->rect.w + 4)), 0, - MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - 4)), 3, - MIN(outputs_walk->rect.w - 4, statusline_width), font_height); + MAX(0, (int16_t)(outputs_walk->rect.w - statusline_width - traypx - 4)), 3, + MIN(outputs_walk->rect.w - traypx - 4, statusline_width), font_height); } if (config.disable_ws) {