diff --git a/src/display-x11.cc b/src/display-x11.cc index 4cff779db..04714a7ad 100644 --- a/src/display-x11.cc +++ b/src/display-x11.cc @@ -53,7 +53,9 @@ #include #include +#include #include +#include #include #include @@ -234,6 +236,9 @@ bool display_output_x11::shutdown() { return true; } +void process_surface_events(conky::display_output_x11 *surface, + Display *display); + bool display_output_x11::main_loop_wait(double t) { /* wait for X event or timeout */ if (!display || !window.gc) return true; @@ -374,266 +379,7 @@ bool display_output_x11::main_loop_wait(double t) { } } - DBGP2("Processing %d X11 events...", XPending(display)); - /* handle X events */ - while (XPending(display) != 0) { - XEvent ev; - /* indicates whether processed event was consumed */ - bool consumed = false; - - XNextEvent(display, &ev); - -#if defined(OWN_WINDOW) && defined(BUILD_MOUSE_EVENTS) && defined(BUILD_XINPUT) - // no need to check whether these events have been consumed because - // they're global and shouldn't be propagated - if (ev.type == GenericEvent && ev.xcookie.extension == window.xi_opcode) { - if (!XGetEventData(display, &ev.xcookie)) { - NORM_ERR("unable to get XInput event data"); - continue; - } - - auto *data = reinterpret_cast(ev.xcookie.data); - - // the only way to differentiate between a scroll and move event is - // though valuators - move has first 2 set, other axis movements have - // other. - bool is_cursor_move = - data->valuators.mask_len >= 1 && - (data->valuators.mask[0] & 3) == data->valuators.mask[0]; - for (std::size_t i = 1; i < data->valuators.mask_len; i++) { - if (data->valuators.mask[i] != 0) { - is_cursor_move = false; - break; - } - } - - if (data->evtype == XI_Motion && is_cursor_move) { - Window query_result = - query_x11_window_at_pos(display, data->root_x, data->root_y); - // query_result is not window.window in some cases. - query_result = query_x11_last_descendant(display, query_result); - - static bool cursor_inside = false; - - // - over conky window - // - conky has now window, over desktop and within conky region - bool cursor_over_conky = query_result == window.window && - (window.window != 0u || - (data->root_x >= window.x && - data->root_x < (window.x + window.width) && - data->root_y >= window.y && - data->root_y < (window.y + window.height))); - if (cursor_over_conky) { - if (!cursor_inside) { - llua_mouse_hook(mouse_crossing_event( - mouse_event_t::AREA_ENTER, data->root_x - window.x, - data->root_y - window.x, data->root_x, data->root_y)); - } - cursor_inside = true; - } else if (cursor_inside) { - llua_mouse_hook(mouse_crossing_event( - mouse_event_t::AREA_LEAVE, data->root_x - window.x, - data->root_y - window.x, data->root_x, data->root_y)); - cursor_inside = false; - } - } - XFreeEventData(display, &ev.xcookie); - continue; - } -#endif /* BUILD_MOUSE_EVENTS && BUILD_XINPUT */ - - // Any of the remaining events apply to conky window - if (ev.xany.window != window.window && ev.type != PropertyNotify) continue; - switch (ev.type) { - case Expose: { - XRectangle r; - r.x = ev.xexpose.x; - r.y = ev.xexpose.y; - r.width = ev.xexpose.width; - r.height = ev.xexpose.height; - XUnionRectWithRegion(&r, x11_stuff.region, x11_stuff.region); - XSync(display, False); - - continue; - } - - case PropertyNotify: { - if (ev.xproperty.state == PropertyNewValue) { - get_x11_desktop_info(ev.xproperty.display, ev.xproperty.atom); - } -#ifdef USE_ARGB - if (!have_argb_visual) { -#endif - if (ev.xproperty.atom == ATOM(_XROOTPMAP_ID) || - ev.xproperty.atom == ATOM(_XROOTMAP_ID)) { - if (forced_redraw.get(*state)) { - draw_stuff(); - next_update_time = get_time(); - need_to_update = 1; - } - } -#ifdef USE_ARGB - } -#endif - continue; - } - -#ifdef OWN_WINDOW - case ReparentNotify: - /* make background transparent */ - if (own_window.get(*state)) { - set_transparent_background(window.window); - } - continue; - - case ConfigureNotify: - if (own_window.get(*state)) { - /* if window size isn't what expected, set fixed size */ - if (ev.xconfigure.width != window.width || - ev.xconfigure.height != window.height) { - if (window.width != 0 && window.height != 0) { fixed_size = 1; } - - /* clear old stuff before screwing up - * size and pos */ - clear_text(1); - - { - XWindowAttributes attrs; - if (XGetWindowAttributes(display, window.window, &attrs) != 0) { - window.width = attrs.width; - window.height = attrs.height; - } - } - - int border_total = get_border_total(); - - text_width = window.width - 2 * border_total; - text_height = window.height - 2 * border_total; - int mw = this->dpi_scale(maximum_width.get(*state)); - if (text_width > mw && mw > 0) { text_width = mw; } - } - - /* if position isn't what expected, set fixed pos - * total_updates avoids setting fixed_pos when window - * is set to weird locations when started */ - /* // this is broken - if (total_updates >= 2 && !fixed_pos - && (window.x != ev.xconfigure.x - || window.y != ev.xconfigure.y) - && (ev.xconfigure.x != 0 - || ev.xconfigure.y != 0)) { - fixed_pos = 1; - } */ - } - continue; - - case ButtonPress: -#ifdef BUILD_MOUSE_EVENTS - { - modifier_state_t mods = x11_modifier_state(ev.xbutton.state); - if (4 <= ev.xbutton.button && ev.xbutton.button <= 7) { - scroll_direction_t direction = - x11_scroll_direction(ev.xbutton.button); - consumed = llua_mouse_hook( - mouse_scroll_event(ev.xbutton.x, ev.xbutton.y, ev.xbutton.x_root, - ev.xbutton.y_root, direction, mods)); - } else { - mouse_button_t button = x11_mouse_button_code(ev.xbutton.button); - consumed = llua_mouse_hook(mouse_button_event( - mouse_event_t::MOUSE_PRESS, ev.xbutton.x, ev.xbutton.y, - ev.xbutton.x_root, ev.xbutton.y_root, button, mods)); - } - } -#endif /* BUILD_MOUSE_EVENTS */ - if (own_window.get(*state)) { - /* if an ordinary window with decorations */ - if ((own_window_type.get(*state) == TYPE_NORMAL && - !TEST_HINT(own_window_hints.get(*state), HINT_UNDECORATED)) || - own_window_type.get(*state) == TYPE_DESKTOP) { - /* allow conky to hold input focus. */ - break; - } - } - break; - - case ButtonRelease: -#ifdef BUILD_MOUSE_EVENTS - /* don't report scroll release events */ - if (4 > ev.xbutton.button || ev.xbutton.button > 7) { - modifier_state_t mods = x11_modifier_state(ev.xbutton.state); - mouse_button_t button = x11_mouse_button_code(ev.xbutton.button); - consumed = llua_mouse_hook(mouse_button_event( - mouse_event_t::MOUSE_RELEASE, ev.xbutton.x, ev.xbutton.y, - ev.xbutton.x_root, ev.xbutton.y_root, button, mods)); - } -#endif /* BUILD_MOUSE_EVENTS */ - if (own_window.get(*state)) { - /* if an ordinary window with decorations */ - if ((own_window_type.get(*state) == TYPE_NORMAL) && - !TEST_HINT(own_window_hints.get(*state), HINT_UNDECORATED)) { - /* allow conky to hold input focus. */ - break; - } - } - break; -#ifdef BUILD_MOUSE_EVENTS - /* - windows below are notified for the following events as well; - can't forward the event without filtering XQueryTree output. - */ - case MotionNotify: { - modifier_state_t mods = x11_modifier_state(ev.xmotion.state); - consumed = llua_mouse_hook(mouse_move_event(ev.xmotion.x, ev.xmotion.y, - ev.xmotion.x_root, - ev.xmotion.y_root, mods)); - } break; - case LeaveNotify: - case EnterNotify: - if (window.xi_opcode == 0) { - bool not_over_conky = - ev.xcrossing.x_root <= window.x || - ev.xcrossing.y_root <= window.y || - ev.xcrossing.x_root >= window.x + window.width || - ev.xcrossing.y_root >= window.y + window.height; - - if ((not_over_conky && ev.xcrossing.type == LeaveNotify) || - (!not_over_conky && ev.xcrossing.type == EnterNotify)) { - llua_mouse_hook(mouse_crossing_event( - ev.xcrossing.type == EnterNotify ? mouse_event_t::AREA_ENTER - : mouse_event_t::AREA_LEAVE, - ev.xcrossing.x, ev.xcrossing.y, ev.xcrossing.x_root, - ev.xcrossing.y_root)); - } - } - // can't propagate these events in a way that makes sense for desktop - continue; -#endif /* BUILD_MOUSE_EVENTS */ -#endif /* OWN_WINDOW */ - default: -#ifdef BUILD_XDAMAGE - if (ev.type == x11_stuff.event_base + XDamageNotify) { - auto *dev = reinterpret_cast(&ev); - - XFixesSetRegion(display, x11_stuff.part, &dev->area, 1); - XFixesUnionRegion(display, x11_stuff.region2, x11_stuff.region2, - x11_stuff.part); - continue; // TODO: Propagate damage - } -#endif /* BUILD_XDAMAGE */ - break; - } - - if (!consumed) { - propagate_x11_event(ev); - } else { - InputEvent *i_ev = xev_as_input_event(ev); - if (i_ev != nullptr) { - XSetInputFocus(display, window.window, RevertToParent, - i_ev->common.time); - } - } - } - DBGP2("Done with events!"); + process_surface_events(this, display); #ifdef BUILD_XDAMAGE if (x11_stuff.damage) { @@ -679,6 +425,441 @@ bool display_output_x11::main_loop_wait(double t) { return true; } +enum x_event_handler { + XINPUT_MOTION, + MOUSE_INPUT, + PROPERTY_NOTIFY, + + EXPOSE, + REPARENT, + CONFIGURE, + BORDER_CROSSING, + DAMAGE, +}; + +template +bool handle_event(conky::display_output_x11 *surface, Display *display, + XEvent &ev, bool *consumed, void **cookie) { + return false; +} + +#ifdef OWN_WINDOW +template <> +bool handle_event( + conky::display_output_x11 *surface, Display *display, XEvent &ev, + bool *consumed, void **cookie) { +#ifdef BUILD_XINPUT + if (ev.type == ButtonPress || ev.type == ButtonRelease || + ev.type == MotionNotify) { + // destroy basic X11 events; and manufacture them later when trying to + // propagate XInput ones - this is required because there's no (simple) way + // of making sure the lua hook controls both when it only handles XInput + // ones. + *consumed = true; + return true; + } + + if (ev.type != GenericEvent || ev.xgeneric.extension != window.xi_opcode) + return false; + + auto *data = xi_event_data::read_cookie(display, &ev.xcookie); + if (data == nullptr) { + NORM_ERR("unable to get XInput event data"); + return false; + } + *cookie = data; + + if (data->evtype == XI_DeviceChanged) { + int device_id = data->sourceid; + + // update cached device info + if (xi_device_info_cache.count(device_id)) { + xi_device_info_cache.erase(device_id); + conky_device_info::from_xi_id(display, device_id); + } + return true; + } + + Window event_window; + modifier_state_t mods; + if (data->evtype == XI_Motion || data->evtype == XI_ButtonPress || + data->evtype == XI_ButtonRelease) { + event_window = query_x11_window_at_pos(display, data->root_x, data->root_y); + // query_result is not window.window in some cases. + event_window = query_x11_last_descendant(display, event_window); + mods = x11_modifier_state(data->mods.effective); + } + + bool cursor_over_conky = + (event_window == window.window || + window.window == 0L && + (event_window == window.root || event_window == window.desktop)) && + (data->root_x >= window.x && data->root_x < (window.x + window.width) && + data->root_y >= window.y && data->root_y < (window.y + window.height)); + + // XInput reports events twice on some hardware (even by 'xinput --test-xi2') + auto hash = std::make_tuple(data->serial, data->evtype, data->event); + typedef std::map MouseEventDebounceMap; + static MouseEventDebounceMap debounce{}; + + Time now = data->time; + bool already_handled = debounce.count(hash) > 0; + debounce[hash] = now; + + // clear stale entries + for (auto iter = debounce.begin(); iter != debounce.end();) { + if (data->time - iter->second > 1000) { + iter = debounce.erase(iter); + } else { + ++iter; + } + } + + if (already_handled) { + *consumed = true; + return true; + } + + if (data->evtype == XI_Motion) { + auto device_info = conky_device_info::from_xi_id(display, data->deviceid); + // TODO: Make valuator_index names configurable? + + // Note that these are absolute (not relative) values in some cases + int hor_move_v = device_info->valuators["Rel X"].index; // Almost always 0 + int vert_move_v = device_info->valuators["Rel Y"].index; // Almost always 1 + int hor_scroll_v = + device_info->valuators["Rel Horiz Scroll"].index; // Almost always 2 + int vert_scroll_v = + device_info->valuators["Rel Vert Scroll"].index; // Almost always 3 + + bool is_move = + data->test_valuator(hor_move_v) || data->test_valuator(vert_move_v); + bool is_scroll = + data->test_valuator(hor_scroll_v) || data->test_valuator(vert_scroll_v); + + if (is_move) { + static bool cursor_inside = false; + + // generate crossing events + if (cursor_over_conky) { + if (!cursor_inside) { + *consumed = llua_mouse_hook(mouse_crossing_event( + mouse_event_t::AREA_ENTER, data->root_x - window.x, + data->root_y - window.x, data->root_x, data->root_y)); + } + cursor_inside = true; + } else if (cursor_inside) { + *consumed = llua_mouse_hook(mouse_crossing_event( + mouse_event_t::AREA_LEAVE, data->root_x - window.x, + data->root_y - window.x, data->root_x, data->root_y)); + cursor_inside = false; + } + + // generate movement events + if (cursor_over_conky) { + *consumed = llua_mouse_hook(mouse_move_event( + data->event_x, data->event_y, data->root_x, data->root_y, mods)); + } + } + if (is_scroll && cursor_over_conky) { + // FIXME: Turn into relative values so direction works + auto horizontal = data->valuator_value(hor_scroll_v); + if (horizontal.value_or(0.0) != 0.0) { + scroll_direction_t direction = horizontal.value() > 0.0 + ? scroll_direction_t::SCROLL_LEFT + : scroll_direction_t::SCROLL_RIGHT; + *consumed = llua_mouse_hook( + mouse_scroll_event(data->event_x, data->event_y, data->root_x, + data->root_y, direction, mods)); + } + auto vertical = data->valuator_value(vert_scroll_v); + if (vertical.value_or(0.0) != 0.0) { + scroll_direction_t direction = vertical.value() > 0.0 + ? scroll_direction_t::SCROLL_DOWN + : scroll_direction_t::SCROLL_UP; + *consumed = llua_mouse_hook( + mouse_scroll_event(data->event_x, data->event_y, data->root_x, + data->root_y, direction, mods)); + } + } + } else if (cursor_over_conky && (data->evtype == XI_ButtonPress || + data->evtype == XI_ButtonRelease)) { + if (data->detail >= 4 && data->detail <= 7) { + // Handled via motion event valuators, ignoring "backward compatibility" + // ones. + return true; + } + + mouse_event_t type = mouse_event_t::MOUSE_PRESS; + if (data->evtype == XI_ButtonRelease) { + type = mouse_event_t::MOUSE_RELEASE; + } + + mouse_button_t button = x11_mouse_button_code(data->detail); + *consumed = llua_mouse_hook(mouse_button_event(type, data->event_x, + data->event_y, data->root_x, + data->root_y, button, mods)); + } +#else /* BUILD_XINPUT */ + if (ev.type != ButtonPress && ev.type != ButtonRelease && + ev.type != MotionNotify) + return false; + if (ev.xany.window != window.window) return true; // Skip other windows + +#ifdef BUILD_MOUSE_EVENTS + switch (ev.type) { + case ButtonPress: { + modifier_state_t mods = x11_modifier_state(ev.xbutton.state); + if (ev.xbutton.button >= 4 && + ev.xbutton.button <= 7) { // scroll "buttons" + scroll_direction_t direction = x11_scroll_direction(ev.xbutton.button); + *consumed = llua_mouse_hook( + mouse_scroll_event(ev.xbutton.x, ev.xbutton.y, ev.xbutton.x_root, + ev.xbutton.y_root, direction, mods)); + } else { + mouse_button_t button = x11_mouse_button_code(ev.xbutton.button); + *consumed = llua_mouse_hook(mouse_button_event( + mouse_event_t::MOUSE_PRESS, ev.xbutton.x, ev.xbutton.y, + ev.xbutton.x_root, ev.xbutton.y_root, button, mods)); + } + } + case ButtonRelease: { + /* don't report scroll release events */ + if (ev.xbutton.button >= 4 && ev.xbutton.button <= 7) return true; + + modifier_state_t mods = x11_modifier_state(ev.xbutton.state); + mouse_button_t button = x11_mouse_button_code(ev.xbutton.button); + *consumed = llua_mouse_hook(mouse_button_event( + mouse_event_t::MOUSE_RELEASE, ev.xbutton.x, ev.xbutton.y, + ev.xbutton.x_root, ev.xbutton.y_root, button, mods)); + } + case MotionNotify: { + modifier_state_t mods = x11_modifier_state(ev.xmotion.state); + *consumed = llua_mouse_hook(mouse_move_event(ev.xmotion.x, ev.xmotion.y, + ev.xmotion.x_root, + ev.xmotion.y_root, mods)); + } + } +#else /* BUILD_MOUSE_EVENTS */ + // always propagate mouse input if not handling mouse events + *consumed = false; +#endif /* BUILD_MOUSE_EVENTS */ +#endif /* BUILD_XINPUT */ + if (!own_window.get(*state)) return true; + switch (own_window_type.get(*state)) { + case window_type::TYPE_NORMAL: + case window_type::TYPE_UTILITY: + // decorated normal windows always consume events + if (!TEST_HINT(own_window_hints.get(*state), HINT_UNDECORATED)) { + *consumed = true; + } + break; + case window_type::TYPE_DESKTOP: + // assume conky is always on bottom; nothing to propagate events to + *consumed = true; + default: + break; + } + + return true; +} + +template <> +bool handle_event(conky::display_output_x11 *surface, + Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != ReparentNotify) return false; + + if (own_window.get(*state)) { set_transparent_background(window.window); } + return true; +} + +template <> +bool handle_event( + conky::display_output_x11 *surface, Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != ConfigureNotify) return false; + + if (own_window.get(*state)) { + /* if window size isn't what expected, set fixed size */ + if (ev.xconfigure.width != window.width || + ev.xconfigure.height != window.height) { + if (window.width != 0 && window.height != 0) { fixed_size = 1; } + + /* clear old stuff before screwing up + * size and pos */ + surface->clear_text(1); + + { + XWindowAttributes attrs; + if (XGetWindowAttributes(display, window.window, &attrs) != 0) { + window.width = attrs.width; + window.height = attrs.height; + } + } + + int border_total = get_border_total(); + + text_width = window.width - 2 * border_total; + text_height = window.height - 2 * border_total; + int mw = surface->dpi_scale(maximum_width.get(*state)); + if (text_width > mw && mw > 0) { text_width = mw; } + } + + /* if position isn't what expected, set fixed pos + * total_updates avoids setting fixed_pos when window + * is set to weird locations when started */ + /* // this is broken + if (total_updates >= 2 && !fixed_pos + && (window.x != ev.xconfigure.x + || window.y != ev.xconfigure.y) + && (ev.xconfigure.x != 0 + || ev.xconfigure.y != 0)) { + fixed_pos = 1; + } */ + } + + return true; +} +#endif /* OWN_WINDOW */ + +template <> +bool handle_event( + conky::display_output_x11 *surface, Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != PropertyNotify) return false; + + if (ev.xproperty.state == PropertyNewValue) { + get_x11_desktop_info(ev.xproperty.display, ev.xproperty.atom); + } + +#ifdef USE_ARGB + if (have_argb_visual) return true; +#endif + + if (ev.xproperty.atom == ATOM(_XROOTPMAP_ID) || + ev.xproperty.atom == ATOM(_XROOTMAP_ID)) { + if (forced_redraw.get(*state)) { + draw_stuff(); + next_update_time = get_time(); + need_to_update = 1; + } + } + return true; +} + +template <> +bool handle_event(conky::display_output_x11 *surface, + Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != Expose) return false; + + XRectangle r; + r.x = ev.xexpose.x; + r.y = ev.xexpose.y; + r.width = ev.xexpose.width; + r.height = ev.xexpose.height; + XUnionRectWithRegion(&r, x11_stuff.region, x11_stuff.region); + XSync(display, False); + return true; +} + +template <> +bool handle_event( + conky::display_output_x11 *surface, Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != EnterNotify && ev.type != LeaveNotify) return false; + if (window.xi_opcode != 0) return true; // handled by mouse_input already + + bool not_over_conky = ev.xcrossing.x_root <= window.x || + ev.xcrossing.y_root <= window.y || + ev.xcrossing.x_root >= window.x + window.width || + ev.xcrossing.y_root >= window.y + window.height; + + if ((not_over_conky && ev.xcrossing.type == LeaveNotify) || + (!not_over_conky && ev.xcrossing.type == EnterNotify)) { + llua_mouse_hook(mouse_crossing_event( + ev.xcrossing.type == EnterNotify ? mouse_event_t::AREA_ENTER + : mouse_event_t::AREA_LEAVE, + ev.xcrossing.x, ev.xcrossing.y, ev.xcrossing.x_root, + ev.xcrossing.y_root)); + } + return true; +} + +#ifdef BUILD_XDAMAGE +template <> +bool handle_event(conky::display_output_x11 *surface, + Display *display, XEvent &ev, + bool *consumed, void **cookie) { + if (ev.type != x11_stuff.event_base + XDamageNotify) return false; + + auto *dev = reinterpret_cast(&ev); + + XFixesSetRegion(display, x11_stuff.part, &dev->area, 1); + XFixesUnionRegion(display, x11_stuff.region2, x11_stuff.region2, + x11_stuff.part); + return true; +} +#endif /* BUILD_XDAMAGE */ + +/// Handles all events conky can receive. +/// +/// @return true if event should move input focus to conky +bool process_event(conky::display_output_x11 *surface, Display *display, + XEvent ev, bool *consumed, void **cookie) { +#define HANDLE_EV(event) \ + if (handle_event(surface, display, ev, consumed, \ + cookie)) { \ + return true; \ + } + + HANDLE_EV(XINPUT_MOTION) + HANDLE_EV(MOUSE_INPUT) + HANDLE_EV(PROPERTY_NOTIFY) + + // only accept remaining events if they're sent to Conky. + if (ev.xany.window != window.window) return false; + + HANDLE_EV(EXPOSE) + HANDLE_EV(REPARENT) + HANDLE_EV(CONFIGURE) + HANDLE_EV(BORDER_CROSSING) + HANDLE_EV(DAMAGE) + + // event not handled + return false; +} + +void process_surface_events(conky::display_output_x11 *surface, + Display *display) { + int pending = XPending(display); + if (pending == 0) return; + + DBGP2("Processing %d X11 events...", pending); + + /* handle X events */ + while (XPending(display) != 0) { + XEvent ev; + XNextEvent(display, &ev); + + /* + indicates whether processed event was consumed; true by default so we + don't propagate handled events unless they explicitly state they haven't + been consumed. + */ + bool consumed = true; + void *cookie = nullptr; + bool handled = process_event(surface, display, ev, &consumed, &cookie); + + if (!consumed) { propagate_x11_event(ev, cookie); } + + if (cookie != nullptr) { free(cookie); } + } + + DBGP2("Done processing %d events.", pending); +} + void display_output_x11::sigterm_cleanup() { XDestroyRegion(x11_stuff.region); x11_stuff.region = nullptr; diff --git a/src/mouse-events.cc b/src/mouse-events.cc index c7cfccc89..7ebb43eb1 100644 --- a/src/mouse-events.cc +++ b/src/mouse-events.cc @@ -27,8 +27,19 @@ #include "logging.h" +#ifdef BUILD_XINPUT +#include +#endif + extern "C" { #include + +#ifdef BUILD_XINPUT +#include +#endif + +#include +#include } namespace conky { @@ -206,4 +217,237 @@ void mouse_button_event::push_lua_data(lua_State *L) const { push_mods(L, this->mods); } +#ifdef BUILD_XINPUT +XIDeviceInfoMap xi_device_info_cache{}; + +conky_device_info *conky_device_info::from_xi_id(Display *display, + int device_id) { + if (xi_device_info_cache.count(device_id)) { + return &xi_device_info_cache[device_id]; + } + + int num_devices; + XIDeviceInfo *device = XIQueryDevice(display, device_id, &num_devices); + if (num_devices == 0) { return nullptr; } + + std::map valuators; + for (int i = 0; i < device->num_classes; i++) { + if (device->classes[i]->type != XIValuatorClass) continue; + + XIValuatorClassInfo *class_info = (XIValuatorClassInfo *)device->classes[i]; + char *label = XGetAtomName(display, class_info->label); + if (label == nullptr) { + XFree(label); + continue; + } + + valuators[std::string(label)] = + xi_valuator_info{.index = static_cast(class_info->number), + .min = class_info->min, + .max = class_info->max}; + DBGP2("%s - %f %f %f", label, class_info->value, class_info->min, + class_info->max); + XFree(label); + } + + xi_device_info_cache[device_id] = + conky_device_info{.device_id = device_id, + .name = std::string(device->name), + .valuators = valuators}; + XIFreeDeviceInfo(device); + + return &xi_device_info_cache[device_id]; +} + +bool xi_event_data::test_valuator(size_t index) const { + return this->valuators.count(index) > 0; +} + +std::optional xi_event_data::valuator_value(size_t index) const { + if (this->valuators.count(index) == 0) return std::nullopt; + return std::optional(this->valuators.at(index)); +} + +xi_event_data *xi_event_data::read_cookie(Display *display, + XGenericEventCookie *cookie) { + if (!XGetEventData(display, cookie)) { + // already consumed + return nullptr; + } + auto *source = reinterpret_cast(cookie->data); + + uint32_t buttons = 0; + for (size_t bi = 1; bi <= source->buttons.mask_len; bi++) { + buttons |= source->buttons.mask[bi] << (source->buttons.mask_len - bi) * 8; + } + + std::map valuators{}; + size_t valuator_index = 0; + for (size_t vi = 0; vi < source->valuators.mask_len * 8; vi++) { + if (XIMaskIsSet(source->valuators.mask, vi)) { + valuators[vi] = source->valuators.values[valuator_index++]; + } + } + + auto result = new xi_event_data{ + .evtype = static_cast(source->evtype), + .serial = source->serial, + .send_event = source->send_event, + .display = source->display, + .extension = source->extension, + .time = source->time, + .deviceid = source->deviceid, + .sourceid = source->sourceid, + .detail = source->detail, + .root = source->root, + .event = source->event, + .child = source->child, + .root_x = source->root_x, + .root_y = source->root_y, + .event_x = source->event_x, + .event_y = source->event_y, + .flags = source->flags, + .buttons = std::bitset<32>(buttons), + .valuators = valuators, + .mods = source->mods, + .group = source->group, + }; + XFreeEventData(display, cookie); + + return result; +} + +std::vector> xi_event_data::generate_events( + Window target, Window child, double target_x, double target_y) const { + std::vector> result{}; + + if (this->evtype == XI_Motion) { + auto device_info = conky_device_info::from_xi_id(display, this->deviceid); + + // Note that these are absolute (not relative) values in some cases + int hor_move_v = device_info->valuators["Rel X"].index; // Almost always 0 + int vert_move_v = device_info->valuators["Rel Y"].index; // Almost always 1 + int hor_scroll_v = + device_info->valuators["Rel Horiz Scroll"].index; // Almost always 2 + int vert_scroll_v = + device_info->valuators["Rel Vert Scroll"].index; // Almost always 3 + + bool is_move = + this->test_valuator(hor_move_v) || this->test_valuator(vert_move_v); + bool is_scroll = + this->test_valuator(hor_scroll_v) || this->test_valuator(vert_scroll_v); + + if (is_move) { + XEvent *produced = new XEvent; + std::memset(produced, 0, sizeof(XEvent)); + + XMotionEvent *e = &produced->xmotion; + e->type = MotionNotify; + e->display = this->display; + e->root = this->root; + e->window = target; + e->subwindow = child; + e->time = CurrentTime; + e->x = static_cast(target_x); + e->y = static_cast(target_y); + e->x_root = static_cast(this->root_x); + e->y_root = static_cast(this->root_y); + e->state = this->mods.effective; + e->is_hint = NotifyNormal; + e->same_screen = True; + result.emplace_back(std::make_tuple(PointerMotionMask, produced)); + } + if (is_scroll) { + XEvent *produced = new XEvent; + std::memset(produced, 0, sizeof(XEvent)); + + uint scroll_direction = 4; + auto vertical = this->valuator_value(vert_scroll_v); + + // FIXME: Turn into relative values so direction works + if (vertical.value_or(0.0) != 0.0) { + scroll_direction = vertical.value() < 0.0 ? Button4 : Button5; + } else { + auto horizontal = this->valuator_value(hor_scroll_v); + if (horizontal.value_or(0.0) != 0.0) { + scroll_direction = horizontal.value() < 0.0 ? 6 : 7; + } + } + + XButtonEvent *e = &produced->xbutton; + e->display = display; + e->root = this->root; + e->window = target; + e->subwindow = child; + e->time = CurrentTime; + e->x = static_cast(target_x); + e->y = static_cast(target_y); + e->x_root = static_cast(this->root_x); + e->y_root = static_cast(this->root_y); + e->state = this->mods.effective; + e->button = scroll_direction; + e->same_screen = True; + + XEvent *press = new XEvent; + e->type = ButtonPress; + std::memcpy(press, produced, sizeof(XEvent)); + result.emplace_back(std::make_tuple(ButtonPressMask, press)); + + e->type = ButtonRelease; + result.emplace_back(std::make_tuple(ButtonReleaseMask, produced)); + } + } else { + XEvent *produced = new XEvent; + std::memset(produced, 0, sizeof(XEvent)); + + XButtonEvent *e = &produced->xbutton; + e->display = display; + e->root = this->root; + e->window = target; + e->subwindow = child; + e->time = CurrentTime; + e->x = static_cast(target_x); + e->y = static_cast(target_y); + e->x_root = static_cast(this->root_x); + e->y_root = static_cast(this->root_y); + e->state = this->mods.effective; + e->button = this->detail; + e->same_screen = True; + + long event_mask = NoEventMask; + switch (this->evtype) { + case XI_ButtonPress: + e->type = ButtonPress; + event_mask = ButtonPressMask; + break; + case XI_ButtonRelease: + e->type = ButtonRelease; + event_mask = ButtonReleaseMask; + switch (this->detail) { + case 1: + event_mask |= Button1MotionMask; + break; + case 2: + event_mask |= Button2MotionMask; + break; + case 3: + event_mask |= Button3MotionMask; + break; + case 4: + event_mask |= Button4MotionMask; + break; + case 5: + event_mask |= Button5MotionMask; + break; + } + break; + } + + result.emplace_back(std::make_tuple(event_mask, produced)); + } + + return result; +} +#endif /* BUILD_XINPUT */ + } // namespace conky \ No newline at end of file diff --git a/src/mouse-events.h b/src/mouse-events.h index ac95d26aa..0bb2f7dd6 100644 --- a/src/mouse-events.h +++ b/src/mouse-events.h @@ -28,9 +28,20 @@ #include "config.h" #include "logging.h" +#ifdef BUILD_XINPUT +#include +#include +#include +#include +#endif /* BUILD_XINPUT */ + extern "C" { #ifdef BUILD_X11 #include + +#ifdef BUILD_XINPUT +#include +#endif /* BUILD_XINPUT */ #endif /* BUILD_X11 */ #include @@ -227,6 +238,74 @@ struct mouse_crossing_event : public mouse_positioned_event { : mouse_positioned_event{type, x, y, x_abs, y_abs} {}; }; +#ifdef BUILD_XINPUT + +typedef int xi_device_id; +typedef int xi_event_type; + +struct xi_valuator_info { + size_t index; + double min; + double max; +}; + +struct conky_device_info { + xi_device_id device_id; + std::string name; + std::map valuators; + + static conky_device_info *from_xi_id(Display *display, xi_device_id id); +}; + +typedef std::map XIDeviceInfoMap; +extern XIDeviceInfoMap xi_device_info_cache; + +int xi_valuator_index(Display *display, xi_device_id device_id, + const char *valuator); + +/// Almost an exact copy of `XIDeviceEvent`, except it owns all data. +struct xi_event_data { + xi_event_type evtype; + unsigned long serial; + Bool send_event; + Display *display; + /// XI extension offset + // TODO: Check whether this is consistent between different clients by + // printing. + int extension; + Time time; + xi_device_id deviceid; + int sourceid; + /// Primary event detail. Meaning depends on `evtype` value: + /// XI_ButtonPress - Mouse button + int detail; + Window root; + Window event; + Window child; + double root_x; + double root_y; + double event_x; + double event_y; + int flags; + /// pressed button mask + std::bitset<32> buttons; + std::map valuators; + XIModifierState mods; + XIGroupState group; + + static xi_event_data *read_cookie(Display *display, + XGenericEventCookie *cookie); + + bool test_valuator(size_t index) const; + std::optional valuator_value(size_t index) const; + + std::vector> generate_events(Window target, + Window child, + double target_x, + double target_y) const; +}; + +#endif /* BUILD_XINPUT */ } // namespace conky #endif /* MOUSE_EVENTS_H */ diff --git a/src/x11.cc b/src/x11.cc index cabab9b88..987bbe8b5 100644 --- a/src/x11.cc +++ b/src/x11.cc @@ -37,6 +37,10 @@ #include "gui.h" #include "logging.h" +#ifdef BUILD_XINPUT +#include "mouse-events.h" +#endif + #include #include #include @@ -946,7 +950,8 @@ void x11_init_window(lua::state &l, bool own) { } bool xinput_ok = false; #ifdef BUILD_XINPUT - do { // not loop + // not a loop; substitutes goto with break - if checks fail + do { int _ignored; // segfault if NULL if (!XQueryExtension(display, "XInputExtension", &window.xi_opcode, &_ignored, &_ignored)) { @@ -955,8 +960,8 @@ void x11_init_window(lua::state &l, bool own) { break; } - int32_t major = 2, minor = 0; - uint32_t retval = XIQueryVersion(display, &major, &minor); + int major = 2, minor = 0; + int retval = XIQueryVersion(display, &major, &minor); if (retval != Success) { NORM_ERR("Error: XInput 2.0 is not supported!"); break; @@ -965,15 +970,33 @@ void x11_init_window(lua::state &l, bool own) { const std::size_t mask_size = (XI_LASTEVENT + 7) / 8; unsigned char mask_bytes[mask_size] = {0}; /* must be zeroed! */ XISetMask(mask_bytes, XI_Motion); + // Capture click events for "override" window type + if (!own) { + XISetMask(mask_bytes, XI_ButtonPress); + XISetMask(mask_bytes, XI_ButtonRelease); + } XIEventMask ev_masks[1]; ev_masks[0].deviceid = XIAllDevices; ev_masks[0].mask_len = sizeof(mask_bytes); ev_masks[0].mask = mask_bytes; XISelectEvents(display, window.root, ev_masks, 1); + + if (own) { + XIClearMask(mask_bytes, XI_Motion); + XISetMask(mask_bytes, XI_ButtonPress); + XISetMask(mask_bytes, XI_ButtonRelease); + + ev_masks[0].deviceid = XIAllDevices; + ev_masks[0].mask_len = sizeof(mask_bytes); + ev_masks[0].mask = mask_bytes; + XISelectEvents(display, window.window, ev_masks, 1); + } + xinput_ok = true; } while (false); #endif /* BUILD_XINPUT */ + // fallback to basic X11 enter/leave events if xinput fails to init if (!xinput_ok && own && own_window_type.get(l) != TYPE_DESKTOP) { input_mask |= EnterWindowMask | LeaveWindowMask; } @@ -1355,7 +1378,7 @@ InputEvent *xev_as_input_event(XEvent &ev) { /// @brief Returns a mask for the event_type /// @param event_type Xlib event type /// @return Xlib event mask -int ev_to_mask(int event_type) { +int ev_to_mask(int event_type, int button) { switch (event_type) { case KeyPress: return KeyPressMask; @@ -1364,7 +1387,20 @@ int ev_to_mask(int event_type) { case ButtonPress: return ButtonPressMask; case ButtonRelease: - return ButtonReleaseMask; + switch (button) { + case 1: + return ButtonReleaseMask | Button1MotionMask; + case 2: + return ButtonReleaseMask | Button2MotionMask; + case 3: + return ButtonReleaseMask | Button3MotionMask; + case 4: + return ButtonReleaseMask | Button4MotionMask; + case 5: + return ButtonReleaseMask | Button5MotionMask; + default: + return ButtonReleaseMask; + } case EnterNotify: return EnterWindowMask; case LeaveNotify: @@ -1376,7 +1412,58 @@ int ev_to_mask(int event_type) { } } -void propagate_x11_event(XEvent &ev) { +#ifdef BUILD_XINPUT +void propagate_xinput_event(const conky::xi_event_data *ev) { + if (ev->evtype != XI_Motion && ev->evtype != XI_ButtonPress && + ev->evtype != XI_ButtonRelease) { + return; + } + + Window target = window.root; + Window child = None; + int target_x = ev->event_x; + int target_y = ev->event_y; + { + std::vector below = query_x11_windows_at_pos( + display, ev->root_x, ev->root_y, + [](XWindowAttributes &a) { return a.map_state == IsViewable; }); + auto it = std::remove_if(below.begin(), below.end(), + [](Window w) { return w == window.window; }); + below.erase(it, below.end()); + if (!below.empty()) { + target = below.back(); + + // Update event x and y coordinates to be target window relative + XTranslateCoordinates(display, window.root, ev->event, ev->root_x, + ev->root_y, &target_x, &target_y, &child); + } + } + + auto events = ev->generate_events(target, child, target_x, target_y); + + XUngrabPointer(display, CurrentTime); + for (auto it : events) { + auto ev = std::get<1>(it); + XSendEvent(display, target, True, std::get<0>(it), ev); + free(ev); + } + + XFlush(display); +} +#endif + +void propagate_x11_event(XEvent &ev, const void *cookie) { + bool focus = ev.type == ButtonPress; + + // cookie must be allocated before propagation, and freed after +#ifdef BUILD_XINPUT + if (ev.type == GenericEvent && ev.xgeneric.extension == window.xi_opcode) { + if (cookie == nullptr) { return; } + return propagate_xinput_event( + reinterpret_cast(cookie)); + } +#endif + InputEvent *i_ev = xev_as_input_event(ev); if (i_ev == nullptr) { // Not a known input event; blindly propagating them causes loops and all @@ -1402,7 +1489,7 @@ void propagate_x11_event(XEvent &ev) { Window _ignore; // Update event x and y coordinates to be target window relative - XTranslateCoordinates(display, window.root, i_ev->common.window, + XTranslateCoordinates(display, window.desktop, i_ev->common.window, i_ev->common.x_root, i_ev->common.y_root, &i_ev->common.x, &i_ev->common.y, &_ignore); } @@ -1410,15 +1497,15 @@ void propagate_x11_event(XEvent &ev) { } XUngrabPointer(display, CurrentTime); - XSendEvent(display, i_ev->common.window, True, ev_to_mask(i_ev->type), &ev); + XSendEvent(display, i_ev->common.window, True, + ev_to_mask(i_ev->type, + ev.type == ButtonRelease ? i_ev->xbutton.button : 0), + &ev); + if (focus) { + XSetInputFocus(display, i_ev->common.window, RevertToParent, CurrentTime); + } } -/// @brief This function returns the last descendant of a window (leaf) on the -/// graph. -/// -/// This function assumes the window stack below `parent` is linear. If it -/// isn't, it's only guaranteed that _some_ descendant of `parent` will be -/// returned. If provided `parent` has no descendants, the `parent` is returned. Window query_x11_last_descendant(Display *display, Window parent) { Window _ignored, *children; std::uint32_t count; @@ -1435,58 +1522,50 @@ Window query_x11_last_descendant(Display *display, Window parent) { return current; } -std::vector query_x11_windows(Display *display) { - // _NET_CLIENT_LIST_STACKING - Window root = DefaultRootWindow(display); - - Atom clients_atom = XInternAtom(display, "_NET_CLIENT_LIST_STACKING", 0); - +std::vector x11_atom_window_list(Display *display, Window window, + Atom atom) { Atom actual_type; int actual_format; unsigned long nitems; unsigned long bytes_after; unsigned char *data = nullptr; - // try retrieving ordered windows first: - if (XGetWindowProperty(display, root, clients_atom, 0, 0, False, XA_WINDOW, + if (XGetWindowProperty(display, window, atom, 0, 0, False, XA_WINDOW, &actual_type, &actual_format, &nitems, &bytes_after, &data) == Success) { - free(data); + XFree(data); size_t count = bytes_after / 4; - if (XGetWindowProperty(display, root, clients_atom, 0, bytes_after / 4, - False, XA_WINDOW, &actual_type, &actual_format, - &nitems, &bytes_after, &data) == Success) { - Window *wdata = reinterpret_cast(data); - std::vector result(wdata, wdata + nitems); - free(data); - return result; - } - } - - clients_atom = XInternAtom(display, "_NET_CLIENT_LIST", 0); - if (XGetWindowProperty(display, root, clients_atom, 0, 0, False, XA_WINDOW, - &actual_type, &actual_format, &nitems, &bytes_after, - &data) == Success) { - free(data); - size_t count = bytes_after / 4; - - if (XGetWindowProperty(display, root, clients_atom, 0, count, False, + if (XGetWindowProperty(display, window, atom, 0, bytes_after / 4, False, XA_WINDOW, &actual_type, &actual_format, &nitems, &bytes_after, &data) == Success) { Window *wdata = reinterpret_cast(data); std::vector result(wdata, wdata + nitems); - free(data); + XFree(data); return result; } } + return std::vector{}; +} + +std::vector query_x11_windows(Display *display) { + Window root = DefaultRootWindow(display); + + Atom clients_atom = XInternAtom(display, "_NET_CLIENT_LIST_STACKING", 0); + std::vector result = + x11_atom_window_list(display, root, clients_atom); + if (result.empty()) { return result; } + + clients_atom = XInternAtom(display, "_NET_CLIENT_LIST", 0); + result = x11_atom_window_list(display, root, clients_atom); + if (result.empty()) { return result; } + // slowest method that also returns inaccurate results: // TODO: How do we remove window decorations and other unwanted WM/DE junk // from this? - std::vector result; std::vector queue = {root}; Window _ignored, *children; diff --git a/src/x11.h b/src/x11.h index 48ccad425..d9347d816 100644 --- a/src/x11.h +++ b/src/x11.h @@ -41,6 +41,7 @@ #include #include +#include #include #ifdef BUILD_ARGB @@ -157,7 +158,15 @@ union InputEvent { // Returns InputEvent pointer to provided XEvent is an input event; nullptr // otherwise. InputEvent *xev_as_input_event(XEvent &ev); -void propagate_x11_event(XEvent &ev); +void propagate_x11_event(XEvent &ev, const void *cookie); + +/// @brief Returns a list of window values for the given atom. +/// @param display display with which the atom is associated +/// @param window window to query for the atom value +/// @param atom atom to query for +/// @return a list of window values for the given atom +std::vector x11_atom_window_list(Display *display, Window window, + Atom atom); /// @brief Tries getting a list of windows ordered from bottom to top. ///