X-Git-Url: https://git.sur5r.net/?a=blobdiff_plain;f=src%2Fhandlers.c;h=1e6b634c6cfd33de2f9367525deaa8b91b5e7f04;hb=a2a32ab1ee04c0d29217b6e8b51897140e09239c;hp=312372a7f08bea0af817eb92d203b19efa130cea;hpb=2314f107784196d8fc7ee500645dbdf548f91386;p=i3%2Fi3 diff --git a/src/handlers.c b/src/handlers.c index 312372a7..1e6b634c 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -21,6 +21,8 @@ #include int randr_base = -1; +int xkb_base = -1; +int xkb_current_group; /* After mapping/unmapping windows, a notify event is generated. However, we don’t want it, since it’d trigger an infinite loop of switching between the different windows when @@ -58,7 +60,8 @@ bool event_is_ignored(const int sequence, const int response_type) { event = SLIST_NEXT(event, ignore_events); SLIST_REMOVE(&ignore_events, save, Ignore_Event, ignore_events); free(save); - } else event = SLIST_NEXT(event, ignore_events); + } else + event = SLIST_NEXT(event, ignore_events); } SLIST_FOREACH(event, &ignore_events, ignore_events) { @@ -163,11 +166,11 @@ static void handle_enter_notify(xcb_enter_notify_event_t *event) { if (layout == L_DEFAULT) { Con *child; TAILQ_FOREACH(child, &(con->nodes_head), nodes) - if (rect_contains(child->deco_rect, event->event_x, event->event_y)) { - LOG("using child %p / %s instead!\n", child, child->name); - con = child; - break; - } + if (rect_contains(child->deco_rect, event->event_x, event->event_y)) { + LOG("using child %p / %s instead!\n", child, child->name); + con = child; + break; + } } #if 0 @@ -183,6 +186,10 @@ static void handle_enter_notify(xcb_enter_notify_event_t *event) { if (config.disable_focus_follows_mouse) return; + /* if this container is already focused, there is nothing to do. */ + if (con == focused) + return; + /* Get the currently focused workspace to check if the focus change also * involves changing workspaces. If so, we need to call workspace_show() to * correctly update state and send the IPC event. */ @@ -204,7 +211,6 @@ static void handle_enter_notify(xcb_enter_notify_event_t *event) { * */ static void handle_motion_notify(xcb_motion_notify_event_t *event) { - last_timestamp = event->time; /* Skip events where the pointer was over a child window, we are only @@ -278,7 +284,6 @@ static void handle_map_request(xcb_map_request_event_t *event) { add_ignore_event(event->sequence, -1); manage_window(event->window, cookie, false); - x_push_changes(croot); return; } @@ -294,7 +299,7 @@ static void handle_configure_request(xcb_configure_request_event_t *event) { Con *con; DLOG("window 0x%08x wants to be at %dx%d with %dx%d\n", - event->window, event->x, event->y, event->width, event->height); + event->window, event->x, event->y, event->width, event->height); /* For unmanaged windows, we just execute the configure request. As soon as * it gets mapped, we will take over anyways. */ @@ -304,12 +309,13 @@ static void handle_configure_request(xcb_configure_request_event_t *event) { uint32_t mask = 0; uint32_t values[7]; int c = 0; -#define COPY_MASK_MEMBER(mask_member, event_member) do { \ - if (event->value_mask & mask_member) { \ - mask |= mask_member; \ - values[c++] = event->event_member; \ - } \ -} while (0) +#define COPY_MASK_MEMBER(mask_member, event_member) \ + do { \ + if (event->value_mask & mask_member) { \ + mask |= mask_member; \ + values[c++] = event->event_member; \ + } \ + } while (0) COPY_MASK_MEMBER(XCB_CONFIG_WINDOW_X, x); COPY_MASK_MEMBER(XCB_CONFIG_WINDOW_Y, y); @@ -482,7 +488,6 @@ static void handle_unmap_notify_event(xcb_unmap_notify_event_t *event) { tree_close(con, DONT_KILL_WINDOW, false, false); tree_render(); - x_push_changes(croot); ignore_end: /* If the client (as opposed to i3) destroyed or unmapped a window, an @@ -528,20 +533,38 @@ static void handle_destroy_notify_event(xcb_destroy_notify_event_t *event) { handle_unmap_notify_event(&unmap); } +static bool window_name_changed(i3Window *window, char *old_name) { + if ((old_name == NULL) && (window->name == NULL)) + return false; + + /* Either the old or the new one is NULL, but not both. */ + if ((old_name == NULL) ^ (window->name == NULL)) + return true; + + return (strcmp(old_name, i3string_as_utf8(window->name)) != 0); +} + /* * Called when a window changes its title * */ static bool handle_windowname_change(void *data, xcb_connection_t *conn, uint8_t state, - xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) { + xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) { Con *con; if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return false; + char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL); + window_update_name(con->window, prop, false); x_push_changes(croot); + if (window_name_changed(con->window, old_name)) + ipc_send_window_event("title", con); + + FREE(old_name); + return true; } @@ -551,15 +574,22 @@ static bool handle_windowname_change(void *data, xcb_connection_t *conn, uint8_t * */ static bool handle_windowname_change_legacy(void *data, xcb_connection_t *conn, uint8_t state, - xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) { + xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *prop) { Con *con; if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return false; + char *old_name = (con->window->name != NULL ? sstrdup(i3string_as_utf8(con->window->name)) : NULL); + window_update_name_legacy(con->window, prop, false); x_push_changes(croot); + if (window_name_changed(con->window, old_name)) + ipc_send_window_event("title", con); + + FREE(old_name); + return true; } @@ -621,6 +651,19 @@ static void handle_expose_event(xcb_expose_event_t *event) { return; } +#define _NET_WM_MOVERESIZE_SIZE_TOPLEFT 0 +#define _NET_WM_MOVERESIZE_SIZE_TOP 1 +#define _NET_WM_MOVERESIZE_SIZE_TOPRIGHT 2 +#define _NET_WM_MOVERESIZE_SIZE_RIGHT 3 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT 4 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOM 5 +#define _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT 6 +#define _NET_WM_MOVERESIZE_SIZE_LEFT 7 +#define _NET_WM_MOVERESIZE_MOVE 8 /* movement only */ +#define _NET_WM_MOVERESIZE_SIZE_KEYBOARD 9 /* size via keyboard */ +#define _NET_WM_MOVERESIZE_MOVE_KEYBOARD 10 /* move via keyboard */ +#define _NET_WM_MOVERESIZE_CANCEL 11 /* cancel operation */ + /* * Handle client messages (EWMH) * @@ -628,7 +671,7 @@ static void handle_expose_event(xcb_expose_event_t *event) { static void handle_client_message(xcb_client_message_event_t *event) { /* If this is a startup notification ClientMessage, the library will handle * it and call our monitor_event() callback. */ - if (sn_xcb_display_process_event(sndisplay, (xcb_generic_event_t*)event)) + if (sn_xcb_display_process_event(sndisplay, (xcb_generic_event_t *)event)) return; LOG("ClientMessage for window 0x%08x\n", event->window); @@ -669,7 +712,11 @@ static void handle_client_message(xcb_client_message_event_t *event) { tree_render(); } else if (event->type == A__NET_ACTIVE_WINDOW) { + if (event->format != 32) + return; + DLOG("_NET_ACTIVE_WINDOW: Window 0x%08x should be activated\n", event->window); + Con *con = con_by_window_id(event->window); if (con == NULL) { DLOG("Could not get window for client message\n"); @@ -677,8 +724,9 @@ static void handle_client_message(xcb_client_message_event_t *event) { } Con *ws = con_get_workspace(con); - if (!workspace_is_visible(ws)) { - DLOG("Workspace not visible, ignoring _NET_ACTIVE_WINDOW\n"); + + if (ws == NULL) { + DLOG("Window is not being managed, ignoring _NET_ACTIVE_WINDOW\n"); return; } @@ -687,10 +735,26 @@ static void handle_client_message(xcb_client_message_event_t *event) { return; } - if (ws != con_get_workspace(focused)) + /* data32[0] indicates the source of the request (application or pager) */ + if (event->data.data32[0] == 2) { + /* Always focus the con if it is from a pager, because this is most + * likely from some user action */ + DLOG("This request came from a pager. Focusing con = %p\n", con); workspace_show(ws); + con_focus(con); + } else { + /* If the request is from an application, only focus if the + * workspace is visible. Otherwise set the urgency hint. */ + if (workspace_is_visible(ws)) { + DLOG("Request to focus con on a visible workspace. Focusing con = %p\n", con); + workspace_show(ws); + con_focus(con); + } else { + DLOG("Request to focus con on a hidden workspace. Setting urgent con = %p\n", con); + con_set_urgency(con, true); + } + } - con_focus(con); tree_render(); } else if (event->type == A_I3_SYNC) { xcb_window_t window = event->data.data32[0]; @@ -707,7 +771,7 @@ static void handle_client_message(xcb_client_message_event_t *event) { ev->data.data32[0] = window; ev->data.data32[1] = rnd; - xcb_send_event(conn, false, window, XCB_EVENT_MASK_NO_EVENT, (char*)ev); + xcb_send_event(conn, false, window, XCB_EVENT_MASK_NO_EVENT, (char *)ev); xcb_flush(conn); free(reply); } else if (event->type == A__NET_REQUEST_FRAME_EXTENTS) { @@ -729,17 +793,94 @@ static void handle_client_message(xcb_client_message_event_t *event) { Rect r = { config.default_border_width, /* left */ config.default_border_width, /* right */ - config.font.height + 5, /* top */ - config.default_border_width /* bottom */ + config.font.height + 5, /* top */ + config.default_border_width /* bottom */ }; xcb_change_property( - conn, - XCB_PROP_MODE_REPLACE, - event->window, - A__NET_FRAME_EXTENTS, - XCB_ATOM_CARDINAL, 32, 4, - &r); + conn, + XCB_PROP_MODE_REPLACE, + event->window, + A__NET_FRAME_EXTENTS, + XCB_ATOM_CARDINAL, 32, 4, + &r); xcb_flush(conn); + } else if (event->type == A__NET_CURRENT_DESKTOP) { + /* This request is used by pagers and bars to change the current + * desktop likely as a result of some user action. We interpret this as + * a request to focus the given workspace. See + * http://standards.freedesktop.org/wm-spec/latest/ar01s03.html#idm140251368135008 + * */ + Con *output; + uint32_t idx = 0; + DLOG("Request to change current desktop to index %d\n", event->data.data32[0]); + + TAILQ_FOREACH(output, &(croot->nodes_head), nodes) { + Con *ws; + TAILQ_FOREACH(ws, &(output_get_content(output)->nodes_head), nodes) { + if (STARTS_WITH(ws->name, "__")) + continue; + + if (idx == event->data.data32[0]) { + /* data32[1] is a timestamp used to prevent focus race conditions */ + if (event->data.data32[1]) + last_timestamp = event->data.data32[1]; + + DLOG("Handling request to focus workspace %s\n", ws->name); + + workspace_show(ws); + tree_render(); + + return; + } + + ++idx; + } + } + } else if (event->type == A__NET_CLOSE_WINDOW) { + /* + * Pagers wanting to close a window MUST send a _NET_CLOSE_WINDOW + * client message request to the root window. + * http://standards.freedesktop.org/wm-spec/wm-spec-latest.html#idm140200472668896 + */ + Con *con = con_by_window_id(event->window); + if (con) { + DLOG("Handling _NET_CLOSE_WINDOW request (con = %p)\n", con); + + if (event->data.data32[0]) + last_timestamp = event->data.data32[0]; + + tree_close(con, KILL_WINDOW, false, false); + tree_render(); + } else { + DLOG("Couldn't find con for _NET_CLOSE_WINDOW request. (window = %d)\n", event->window); + } + } else if (event->type == A__NET_WM_MOVERESIZE) { + /* + * Client-side decorated Gtk3 windows emit this signal when being + * dragged by their GtkHeaderBar + */ + Con *con = con_by_window_id(event->window); + if (!con || !con_is_floating(con)) { + DLOG("Couldn't find con for _NET_WM_MOVERESIZE request, or con not floating (window = %d)\n", event->window); + return; + } + DLOG("Handling _NET_WM_MOVERESIZE request (con = %p)\n", con); + uint32_t direction = event->data.data32[2]; + uint32_t x_root = event->data.data32[0]; + uint32_t y_root = event->data.data32[1]; + /* construct fake xcb_button_press_event_t */ + xcb_button_press_event_t fake = { + .root_x = x_root, + .root_y = y_root, + .event_x = x_root - (con->rect.x), + .event_y = y_root - (con->rect.y)}; + if (direction == _NET_WM_MOVERESIZE_MOVE) { + floating_drag_window(con->parent, &fake); + } else if (direction >= _NET_WM_MOVERESIZE_SIZE_TOPLEFT && direction <= _NET_WM_MOVERESIZE_SIZE_LEFT) { + floating_resize_window(con->parent, FALSE, &fake); + } else { + DLOG("_NET_WM_MOVERESIZE direction %d not implemented\n", direction); + } } else { DLOG("unhandled clientmessage\n"); return; @@ -764,7 +905,7 @@ int handle_window_type(void *data, xcb_connection_t *conn, uint8_t state, xcb_wi * */ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t state, xcb_window_t window, - xcb_atom_t name, xcb_get_property_reply_t *reply) { + xcb_atom_t name, xcb_get_property_reply_t *reply) { Con *con = con_by_window_id(window); if (con == NULL) { DLOG("Received WM_NORMAL_HINTS for unknown client\n"); @@ -773,7 +914,7 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat xcb_size_hints_t size_hints; - //CLIENT_LOG(client); + //CLIENT_LOG(client); /* If the hints were already in this event, use them, if not, request them */ if (reply != NULL) @@ -853,7 +994,8 @@ static bool handle_normal_hints(void *data, xcb_connection_t *conn, uint8_t stat aspect_ratio = min_aspect; } else if ((width / height) > max_aspect) { aspect_ratio = max_aspect; - } else goto render_and_return; + } else + goto render_and_return; if (fabs(con->aspect_ratio - aspect_ratio) > DBL_EPSILON) { con->aspect_ratio = aspect_ratio; @@ -872,7 +1014,7 @@ render_and_return: * */ static bool handle_hints(void *data, xcb_connection_t *conn, uint8_t state, xcb_window_t window, - xcb_atom_t name, xcb_get_property_reply_t *reply) { + xcb_atom_t name, xcb_get_property_reply_t *reply) { Con *con = con_by_window_id(window); if (con == NULL) { DLOG("Received WM_HINTS for unknown client\n"); @@ -897,7 +1039,7 @@ static bool handle_hints(void *data, xcb_connection_t *conn, uint8_t state, xcb_ * */ static bool handle_transient_for(void *data, xcb_connection_t *conn, uint8_t state, xcb_window_t window, - xcb_atom_t name, xcb_get_property_reply_t *prop) { + xcb_atom_t name, xcb_get_property_reply_t *prop) { Con *con; if ((con = con_by_window_id(window)) == NULL || con->window == NULL) { @@ -907,7 +1049,8 @@ static bool handle_transient_for(void *data, xcb_connection_t *conn, uint8_t sta if (prop == NULL) { prop = xcb_get_property_reply(conn, xcb_get_property_unchecked(conn, - false, window, XCB_ATOM_WM_TRANSIENT_FOR, XCB_ATOM_WINDOW, 0, 32), NULL); + false, window, XCB_ATOM_WM_TRANSIENT_FOR, XCB_ATOM_WINDOW, 0, 32), + NULL); if (prop == NULL) return false; } @@ -923,14 +1066,15 @@ static bool handle_transient_for(void *data, xcb_connection_t *conn, uint8_t sta * */ static bool handle_clientleader_change(void *data, xcb_connection_t *conn, uint8_t state, xcb_window_t window, - xcb_atom_t name, xcb_get_property_reply_t *prop) { + xcb_atom_t name, xcb_get_property_reply_t *prop) { Con *con; if ((con = con_by_window_id(window)) == NULL || con->window == NULL) return false; if (prop == NULL) { prop = xcb_get_property_reply(conn, xcb_get_property_unchecked(conn, - false, window, A_WM_CLIENT_LEADER, XCB_ATOM_WINDOW, 0, 32), NULL); + false, window, A_WM_CLIENT_LEADER, XCB_ATOM_WINDOW, 0, 32), + NULL); if (prop == NULL) return false; } @@ -991,6 +1135,30 @@ static void handle_focus_in(xcb_focus_in_event_t *event) { return; } +/* + * Handles the WM_CLASS property for assignments and criteria selection. + * + */ +static bool handle_class_change(void *data, xcb_connection_t *conn, uint8_t state, xcb_window_t window, + xcb_atom_t name, xcb_get_property_reply_t *prop) { + Con *con; + if ((con = con_by_window_id(window)) == NULL || con->window == NULL) + return false; + + if (prop == NULL) { + prop = xcb_get_property_reply(conn, xcb_get_property_unchecked(conn, + false, window, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 0, 32), + NULL); + + if (prop == NULL) + return false; + } + + window_update_class(con->window, prop, false); + + return true; +} + /* Returns false if the event could not be processed (e.g. the window could not * be found), true otherwise */ typedef bool (*cb_property_handler_t)(void *data, xcb_connection_t *c, uint8_t state, xcb_window_t window, xcb_atom_t atom, xcb_get_property_reply_t *property); @@ -1002,14 +1170,14 @@ struct property_handler_t { }; static struct property_handler_t property_handlers[] = { - { 0, 128, handle_windowname_change }, - { 0, UINT_MAX, handle_hints }, - { 0, 128, handle_windowname_change_legacy }, - { 0, UINT_MAX, handle_normal_hints }, - { 0, UINT_MAX, handle_clientleader_change }, - { 0, UINT_MAX, handle_transient_for }, - { 0, 128, handle_windowrole_change } -}; + {0, 128, handle_windowname_change}, + {0, UINT_MAX, handle_hints}, + {0, 128, handle_windowname_change_legacy}, + {0, UINT_MAX, handle_normal_hints}, + {0, UINT_MAX, handle_clientleader_change}, + {0, UINT_MAX, handle_transient_for}, + {0, 128, handle_windowrole_change}, + {0, 128, handle_class_change}}; #define NUM_HANDLERS (sizeof(property_handlers) / sizeof(struct property_handler_t)) /* @@ -1018,7 +1186,6 @@ static struct property_handler_t property_handlers[] = { * */ void property_handlers_init(void) { - sn_monitor_context_new(sndisplay, conn_screen, startup_monitor_event, NULL, NULL); property_handlers[0].atom = A__NET_WM_NAME; @@ -1028,6 +1195,7 @@ void property_handlers_init(void) { property_handlers[4].atom = A_WM_CLIENT_LEADER; property_handlers[5].atom = XCB_ATOM_WM_TRANSIENT_FOR; property_handlers[6].atom = A_WM_WINDOW_ROLE; + property_handlers[7].atom = XCB_ATOM_WM_CLASS; } static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) { @@ -1063,70 +1231,118 @@ static void property_notify(uint8_t state, xcb_window_t window, xcb_atom_t atom) * */ void handle_event(int type, xcb_generic_event_t *event) { + DLOG("event type %d, xkb_base %d\n", type, xkb_base); if (randr_base > -1 && type == randr_base + XCB_RANDR_SCREEN_CHANGE_NOTIFY) { handle_screen_change(event); return; } + if (xkb_base > -1 && type == xkb_base) { + DLOG("xkb event, need to handle it.\n"); + + xcb_xkb_state_notify_event_t *state = (xcb_xkb_state_notify_event_t *)event; + if (state->xkbType == XCB_XKB_NEW_KEYBOARD_NOTIFY) { + DLOG("xkb new keyboard notify, sequence %d, time %d\n", state->sequence, state->time); + xcb_key_symbols_free(keysyms); + keysyms = xcb_key_symbols_alloc(conn); + ungrab_all_keys(conn); + translate_keysyms(); + grab_all_keys(conn, false); + } else if (state->xkbType == XCB_XKB_MAP_NOTIFY) { + if (event_is_ignored(event->sequence, type)) { + DLOG("Ignoring map notify event for sequence %d.\n", state->sequence); + } else { + DLOG("xkb map notify, sequence %d, time %d\n", state->sequence, state->time); + add_ignore_event(event->sequence, type); + xcb_key_symbols_free(keysyms); + keysyms = xcb_key_symbols_alloc(conn); + ungrab_all_keys(conn); + translate_keysyms(); + grab_all_keys(conn, false); + } + } else if (state->xkbType == XCB_XKB_STATE_NOTIFY) { + DLOG("xkb state group = %d\n", state->group); + + /* See The XKB Extension: Library Specification, section 14.1 */ + /* We check if the current group (each group contains + * two levels) has been changed. Mode_switch activates + * group XCB_XKB_GROUP_2 */ + if (xkb_current_group == state->group) + return; + xkb_current_group = state->group; + if (state->group == XCB_XKB_GROUP_1) { + DLOG("Mode_switch disabled\n"); + ungrab_all_keys(conn); + grab_all_keys(conn, false); + } else { + DLOG("Mode_switch enabled\n"); + grab_all_keys(conn, true); + } + } + + return; + } + switch (type) { case XCB_KEY_PRESS: case XCB_KEY_RELEASE: - handle_key_press((xcb_key_press_event_t*)event); + handle_key_press((xcb_key_press_event_t *)event); break; case XCB_BUTTON_PRESS: - handle_button_press((xcb_button_press_event_t*)event); + case XCB_BUTTON_RELEASE: + handle_button_press((xcb_button_press_event_t *)event); break; case XCB_MAP_REQUEST: - handle_map_request((xcb_map_request_event_t*)event); + handle_map_request((xcb_map_request_event_t *)event); break; case XCB_UNMAP_NOTIFY: - handle_unmap_notify_event((xcb_unmap_notify_event_t*)event); + handle_unmap_notify_event((xcb_unmap_notify_event_t *)event); break; case XCB_DESTROY_NOTIFY: - handle_destroy_notify_event((xcb_destroy_notify_event_t*)event); + handle_destroy_notify_event((xcb_destroy_notify_event_t *)event); break; case XCB_EXPOSE: - handle_expose_event((xcb_expose_event_t*)event); + handle_expose_event((xcb_expose_event_t *)event); break; case XCB_MOTION_NOTIFY: - handle_motion_notify((xcb_motion_notify_event_t*)event); + handle_motion_notify((xcb_motion_notify_event_t *)event); break; /* Enter window = user moved his mouse over the window */ case XCB_ENTER_NOTIFY: - handle_enter_notify((xcb_enter_notify_event_t*)event); + handle_enter_notify((xcb_enter_notify_event_t *)event); break; /* Client message are sent to the root window. The only interesting * client message for us is _NET_WM_STATE, we honour * _NET_WM_STATE_FULLSCREEN and _NET_WM_STATE_DEMANDS_ATTENTION */ case XCB_CLIENT_MESSAGE: - handle_client_message((xcb_client_message_event_t*)event); + handle_client_message((xcb_client_message_event_t *)event); break; /* Configure request = window tried to change size on its own */ case XCB_CONFIGURE_REQUEST: - handle_configure_request((xcb_configure_request_event_t*)event); + handle_configure_request((xcb_configure_request_event_t *)event); break; /* Mapping notify = keyboard mapping changed (Xmodmap), re-grab bindings */ case XCB_MAPPING_NOTIFY: - handle_mapping_notify((xcb_mapping_notify_event_t*)event); + handle_mapping_notify((xcb_mapping_notify_event_t *)event); break; case XCB_FOCUS_IN: - handle_focus_in((xcb_focus_in_event_t*)event); + handle_focus_in((xcb_focus_in_event_t *)event); break; case XCB_PROPERTY_NOTIFY: { - xcb_property_notify_event_t *e = (xcb_property_notify_event_t*)event; + xcb_property_notify_event_t *e = (xcb_property_notify_event_t *)event; last_timestamp = e->time; property_notify(e->state, e->window, e->atom); break;