diff --git a/src/display-x11.cc b/src/display-x11.cc index d5111d32d..30f8b3f5c 100644 --- a/src/display-x11.cc +++ b/src/display-x11.cc @@ -53,7 +53,9 @@ #include #include +#include #include +#include #include #include @@ -442,72 +444,156 @@ bool handle_event(conky::display_output_x11 *surface, Display *display, } #ifdef OWN_WINDOW -#ifdef BUILD_XINPUT template <> -bool handle_event( +bool handle_event( conky::display_output_x11 *surface, Display *display, XEvent &ev, bool *consumed, void **cookie) { - if (ev.type != GenericEvent || ev.xcookie.extension != window.xi_opcode) +#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; if (!XGetEventData(display, &ev.xcookie)) { - NORM_ERR("unable to get XInput event data"); - return false; + // already consumed + return true; } + xi_event_type event_type = ev.xcookie.evtype; - 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 (event_type == XI_HierarchyChanged) { + auto device_change = reinterpret_cast(ev.xcookie.data); + handle_xi_device_change(device_change); + XFreeEventData(display, &ev.xcookie); + return true; + } + + auto *data = xi_event_data::read_cookie(display, ev.xcookie.data); + XFreeEventData(display, &ev.xcookie); + if (data == nullptr) { + // we ate the cookie, Xi event not handled + return true; + } + *cookie = data; + + Window event_window = + query_x11_window_at_pos(display, data->root_x, data->root_y); + // query_result is not window.window in some cases. + modifier_state_t mods = x11_modifier_state(data->mods.effective); + + bool same_window = query_x11_top_parent(display, event_window) == + query_x11_top_parent(display, window.window); + bool cursor_over_conky = same_window && 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 (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, + if (already_handled) { + *consumed = true; + return true; + } + + if (data->evtype == XI_Motion) { + // TODO: Make valuator_index names configurable? + + bool is_move = data->test_valuator(valuator_t::MOVE_X) || + data->test_valuator(valuator_t::MOVE_Y); + bool is_scroll = data->test_valuator(valuator_t::SCROLL_X) || + data->test_valuator(valuator_t::SCROLL_Y); + + 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; } - 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; + + // 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) { + scroll_direction_t scroll_direction; + auto vertical = data->valuator_relative_value(valuator_t::SCROLL_Y); + double vertical_value = vertical.value_or(0.0); + + if (vertical_value != 0.0) { + scroll_direction = vertical_value < 0.0 + ? scroll_direction_t::SCROLL_UP + : scroll_direction_t::SCROLL_DOWN; + } else { + auto horizontal = data->valuator_relative_value(valuator_t::SCROLL_X); + double horizontal_value = horizontal.value_or(0.0); + if (horizontal_value != 0.0) { + scroll_direction = horizontal_value < 0.0 + ? scroll_direction_t::SCROLL_LEFT + : scroll_direction_t::SCROLL_RIGHT; + } + } + + if (scroll_direction != scroll_direction_t::SCROLL_UNKNOWN) { + *consumed = llua_mouse_hook( + mouse_scroll_event(data->event_x, data->event_y, data->root_x, + data->root_y, scroll_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)); } - XFreeEventData(display, &ev.xcookie); - return true; -} -#endif /* BUILD_XINPUT */ -template <> -bool handle_event( - conky::display_output_x11 *surface, Display *display, XEvent &ev, - bool *consumed, void **cookie) { +#else /* BUILD_XINPUT */ if (ev.type != ButtonPress && ev.type != ButtonRelease && ev.type != MotionNotify) return false; @@ -551,7 +637,7 @@ bool handle_event( // 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: @@ -751,9 +837,9 @@ void process_surface_events(conky::display_output_x11 *surface, 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. + 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; diff --git a/src/mouse-events.cc b/src/mouse-events.cc index c7cfccc89..41b3e233e 100644 --- a/src/mouse-events.cc +++ b/src/mouse-events.cc @@ -27,8 +27,20 @@ #include "logging.h" +#ifdef BUILD_XINPUT +#include +#endif + extern "C" { #include + +#ifdef BUILD_XINPUT +#include +#endif + +#include +#include +#include } namespace conky { @@ -206,4 +218,416 @@ void mouse_button_event::push_lua_data(lua_State *L) const { push_mods(L, this->mods); } +#ifdef BUILD_XINPUT +/// Last global device id. +size_t last_device_id = 0; + +static std::map device_info_cache{}; +static std::map xi_id_mapping{}; + +device_info *device_info::from_xi_id(xi_device_id device_id, Display *display) { + if (xi_id_mapping.count(device_id) > 0) { + return &device_info_cache[xi_id_mapping[device_id]]; + } + if (display == nullptr) return nullptr; + + int num_devices; + XIDeviceInfo *device = XIQueryDevice(display, device_id, &num_devices); + if (num_devices == 0) return nullptr; + + device_info info = + device_info{.id = device_id, .name = std::string(device->name)}; + + size_t id = last_device_id++; + info.init_xi_device(display, device); + XIFreeDeviceInfo(device); + + device_info_cache[id] = info; + xi_id_mapping[device_id] = id; + + return &device_info_cache[id]; +} + +void handle_xi_device_change(const XIHierarchyEvent *event) { + if ((event->flags & XISlaveRemoved) != 0) { + for (int i = 0; i < event->num_info; i++) { + auto info = event->info[i]; + if ((info.flags & XISlaveRemoved) == 0) continue; + if (xi_id_mapping.count(info.deviceid) == 0) continue; + + size_t id = xi_id_mapping[info.deviceid]; + xi_id_mapping.erase(info.deviceid); + device_info_cache.erase(id); + } + } + + if ((event->flags & XISlaveAdded) != 0) { + for (int i = 0; i < event->num_info; i++) { + auto info = event->info[i]; + if ((info.flags & XISlaveAdded) == 0) continue; + if (info.use == IsXPointer || info.use == IsXExtensionPointer) + conky::device_info::from_xi_id(info.deviceid, event->display); + } + } +} + +/// Allows override of valuator indices in `xorg.conf` in case they're wrong for +/// some device (unlikely). +size_t fixed_valuator_index(Display *display, XIDeviceInfo *device, + valuator_t valuator) { + const std::array atom_names = { + "ConkyValuatorMoveX", "ConkyValuatorMoveY", "ConkyValuatorScrollX", + "ConkyValuatorScrollY"}; + Atom override_atom = XInternAtom(display, atom_names[valuator], False); + unsigned char *value; + Atom type_return; + int format_return; + unsigned long num_items; + unsigned long bytes_after; + do { + if (XIGetProperty(display, device->deviceid, override_atom, 0, 1, False, + XA_INTEGER, &type_return, &format_return, &num_items, + &bytes_after, + reinterpret_cast(&value)) == Success) { + if (num_items == 0) break; + if (type_return != XA_INTEGER) { + NORM_ERR( + "invalid '%s' option value, expected a single integer; value will " + "be ignored", + atom_names[valuator]); + XFree(value); + break; + } + uint32_t result = *reinterpret_cast(value); + XFree(value); + return static_cast(result); + } + } while (true); + return valuator; +} + +/// Allows override of valuator value type in `xorg.conf` in case they're wrong +/// for some device (happens with VMs and some devices/setups). +bool fixed_valuator_relative(Display *display, XIDeviceInfo *device, + valuator_t valuator, + XIValuatorClassInfo *class_info) { + const std::array atom_names = { + "ConkyValuatorMoveMode", + "ConkyValuatorScrollMode", + }; + + Atom override_atom = XInternAtom(display, atom_names[valuator >> 1], True); + unsigned char *value_return; + Atom type_return; + int format_return; + unsigned long num_items; + unsigned long bytes_after; + + do { + if (XIGetProperty( + display, device->deviceid, override_atom, 0, 9, False, XA_ATOM, + &type_return, &format_return, &num_items, &bytes_after, + reinterpret_cast(&value_return)) == Success) { + if (num_items == 0) break; + if (type_return != XA_ATOM) { + NORM_ERR( + "invalid '%s' option value, expected an atom (string); value will " + "be ignored", + atom_names[valuator >> 1]); + XFree(value_return); + break; + } + Atom return_atom = *reinterpret_cast(value_return); + XFree(value_return); + char *value = XGetAtomName(display, return_atom); + + // lowercase value + for (auto c = value; *c; ++c) *c = tolower(*c); + + bool relative = false; + if (strcmp(reinterpret_cast(value), "relative") == 0) { + relative = true; + } else if (strcmp(reinterpret_cast(value), "absolute") != 0) { + NORM_ERR( + "unknown '%s' option value: '%s', expected 'absolute' or " + "'relative'; " + "value will be ignored", + atom_names[valuator >> 1]); + XFree(value); + break; + } + XFree(value); + return relative; + } + } while (true); + return class_info->mode == XIModeRelative; +} + +void device_info::init_xi_device( + Display *display, std::variant source) { + XIDeviceInfo *device = nullptr; + if (std::holds_alternative(source)) { + device = std::get(source); + } else if (std::holds_alternative(source)) { + int num_devices; + device = + XIQueryDevice(display, std::get(source), &num_devices); + if (num_devices == 0) return; + } + if (device == nullptr) return; + + std::array valuator_indices; + for (size_t i = 0; i < valuator_t::VALUATOR_COUNT; i++) { + valuator_indices[i] = + fixed_valuator_index(display, device, static_cast(i)); + } + + // class order is undefined! + for (int i = 0; i < device->num_classes; i++) { + if (device->classes[i]->type != XIValuatorClass) continue; + XIValuatorClassInfo *class_info = (XIValuatorClassInfo *)device->classes[i]; + + // check if one of used (mapped) valuators + valuator_t valuator = valuator_t::VALUATOR_COUNT; + for (size_t i = 0; i < valuator_t::VALUATOR_COUNT; i++) { + if (valuator_indices[i] == class_info->number) { + valuator = static_cast(i); + break; + } + } + if (valuator == valuator_t::VALUATOR_COUNT) { continue; } + + auto info = conky_valuator_info{ + .index = static_cast(class_info->number), + .min = class_info->min, + .max = class_info->max, + .value = class_info->value, + .relative = + fixed_valuator_relative(display, device, valuator, class_info), + }; + + this->valuators[valuator] = info; + } + + if (std::holds_alternative(source)) { + XIFreeDeviceInfo(device); + } +} +conky_valuator_info &device_info::valuator(valuator_t valuator) { + return this->valuators[valuator]; +} + +xi_event_data *xi_event_data::read_cookie(Display *display, const void *data) { + const XIDeviceEvent *source = reinterpret_cast(data); + xi_event_type event_type = source->evtype; + if (!(event_type == XI_Motion || event_type == XI_ButtonPress || + event_type == XI_ButtonRelease)) { + return nullptr; + } + + std::bitset<32> buttons; + for (size_t bi = 1; bi < source->buttons.mask_len * 8; bi++) { + if (XIMaskIsSet(source->buttons.mask, bi)) buttons[bi] = true; + } + + std::map valuators{}; + double *values = source->valuators.values; + for (size_t vi = 0; vi < source->valuators.mask_len * 8; vi++) { + if (XIMaskIsSet(source->valuators.mask, vi)) valuators[vi] = *values++; + } + + auto device = device_info::from_xi_id(source->deviceid, source->display); + if (device == nullptr) return nullptr; // shouldn't happen + + 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, + .device = device, + .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 = buttons, + .valuators = valuators, + .mods = source->mods, + .group = source->group, + .valuators_relative = {0.0, 0.0, 0.0, 0.0}, + }; + + for (size_t v = 0; v < valuator_t::VALUATOR_COUNT; v++) { + valuator_t valuator = static_cast(v); + auto &valuator_info = device->valuator(valuator); + + if (result->valuators.count(valuator_info.index) == 0) { continue; } + auto current = result->valuators[valuator_info.index]; + + if (valuator_info.relative) { + result->valuators_relative[v] = current; + } else { + // XXX these doubles come from int values and might wrap around though + // it's hard to tell what int type is the source as it depends on the + // device/driver. + result->valuators_relative[v] = current - valuator_info.value; + } + valuator_info.value = current; + } + + return result; +} + +bool xi_event_data::test_valuator(valuator_t valuator) const { + return this->valuators.count(this->device->valuator(valuator).index) > 0; +} +conky_valuator_info *xi_event_data::valuator_info(valuator_t valuator) const { + return &this->device->valuator(valuator); +} +std::optional xi_event_data::valuator_value(valuator_t valuator) const { + auto info = this->valuator_info(valuator); + if (info == nullptr) return std::nullopt; + size_t index = info->index; + if (this->valuators.count(index) == 0) return std::nullopt; + return this->valuators.at(index); +} + +std::optional xi_event_data::valuator_relative_value( + valuator_t valuator) const { + return this->valuators_relative.at(valuator); +} + +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 = this->device; + + bool is_move = this->test_valuator(valuator_t::MOVE_X) || + this->test_valuator(valuator_t::MOVE_Y); + bool is_scroll = this->test_valuator(valuator_t::SCROLL_X) || + this->test_valuator(valuator_t::SCROLL_Y); + + 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_relative_value(valuator_t::SCROLL_Y); + double vertical_value = vertical.value_or(0.0); + + if (vertical_value != 0.0) { + scroll_direction = vertical_value < 0.0 ? Button4 : Button5; + } else { + auto horizontal = this->valuator_relative_value(valuator_t::SCROLL_X); + double horizontal_value = horizontal.value_or(0.0); + if (horizontal_value != 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..cc25dc8c3 100644 --- a/src/mouse-events.h +++ b/src/mouse-events.h @@ -28,9 +28,25 @@ #include "config.h" #include "logging.h" +#ifdef BUILD_XINPUT +#include +#include +#include +#include +#include +#include +#endif /* BUILD_XINPUT */ + extern "C" { #ifdef BUILD_X11 #include + +#ifdef BUILD_XINPUT +#include +#include +#undef COUNT // define from X11/extendsions/Xi.h + +#endif /* BUILD_XINPUT */ #endif /* BUILD_X11 */ #include @@ -41,7 +57,7 @@ extern "C" { #include #elif __DragonFly__ #include -#else +#else /* platform */ // Probably incorrect for some platforms, feel free to add your platform to the // above list if it has other event codes or a standard file containing them. @@ -56,7 +72,7 @@ extern "C" { #define BTN_BACK 0x116 // Forward mouse button event code #define BTN_FORWARD 0x115 -#endif +#endif /* platform */ } namespace conky { @@ -227,6 +243,84 @@ 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; + +enum valuator_t : size_t { MOVE_X, MOVE_Y, SCROLL_X, SCROLL_Y, VALUATOR_COUNT }; + +struct conky_valuator_info { + size_t index; + double min; + double max; + double value; + bool relative; +}; + +struct device_info { + /// @brief Device name. + xi_device_id id; + std::string name; + std::array valuators{}; + + static device_info *from_xi_id(xi_device_id id, Display *display = nullptr); + + conky_valuator_info &valuator(valuator_t valuator); + + private: + void init_xi_device(Display *display, + std::variant device); +}; + +void handle_xi_device_change(const XIHierarchyEvent *event); + +/// 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; + device_info *device; + int sourceid; + 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; + + // Extra data + + /// Precomputed relative values + std::array valuators_relative; + + static xi_event_data *read_cookie(Display *display, const void *data); + + bool test_valuator(valuator_t id) const; + conky_valuator_info *valuator_info(valuator_t id) const; + std::optional valuator_value(valuator_t id) const; + std::optional valuator_relative_value(valuator_t valuator) 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 ff37cb6f0..1784f83be 100644 --- a/src/x11.cc +++ b/src/x11.cc @@ -37,6 +37,12 @@ #include "gui.h" #include "logging.h" +#ifdef BUILD_XINPUT +#include "mouse-events.h" + +#include +#endif + #include #include #include @@ -44,6 +50,7 @@ #include #include +extern "C" { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wvariadic-macros" #pragma GCC diagnostic ignored "-Wregister" @@ -73,13 +80,14 @@ #include #endif /* BUILD_XFIXES */ #ifdef BUILD_XINPUT +#include #include -#include #endif /* BUILD_XINPUT */ #ifdef HAVE_XCB_ERRORS #include #include #endif +} /* some basic X11 stuff */ Display *display = nullptr; @@ -307,6 +315,52 @@ __attribute__((noreturn)) static int x11_ioerror_handler(Display *d) { CRIT_ERR("X IO Error: Display %lx\n", reinterpret_cast(d)); } +/// @brief Function to get virtual root windows of screen. +/// +/// Some WMs (swm, tvtwm, amiwm, enlightenment, etc.) use virtual roots to +/// manage workspaces. These are direct descendants of root and WMs reparent all +/// children to them. +/// +/// @param screen screen to get the (current) virtual root of @return the +/// virtual root window of the screen +static Window VRootWindowOfScreen(Screen *screen) { + Window root = screen->root; + Display *dpy = screen->display; + + Window rootReturn, parentReturn, *children; + unsigned int numChildren; + Atom actual_type; + int actual_format; + unsigned long nitems, bytesafter; + + /* go look for a virtual root */ + Atom __SWM_VROOT = ATOM(__SWM_VROOT); + if (XQueryTree(dpy, root, &rootReturn, &parentReturn, &children, + &numChildren)) { + for (int i = 0; i < numChildren; i++) { + Window *newRoot = None; + + if (XGetWindowProperty( + dpy, children[i], __SWM_VROOT, 0, 1, False, XA_WINDOW, + &actual_type, &actual_format, &nitems, &bytesafter, + reinterpret_cast(&newRoot)) == Success && + newRoot != None) { + root = *newRoot; + break; + } + } + if (children) XFree((char *)children); + } + + return root; +} +inline Window VRootWindow(Display *display, int screen) { + return VRootWindowOfScreen(ScreenOfDisplay(display, screen)); +} +inline Window DefaultVRootWindow(Display *display) { + return VRootWindowOfScreen(DefaultScreenOfDisplay(display)); +} + /* X11 initializer */ static void init_x11() { DBGP("enter init_x11()"); @@ -416,75 +470,21 @@ static void update_workarea() { * Return desktop window on success, * and set root and desktop byref return values. * Return 0 on failure. */ -static Window find_desktop_window(Window *p_root, Window *p_desktop) { - Atom type; - int format, i; - unsigned long nitems, bytes; - unsigned int n; - if (!display) return 0; - Window root = RootWindow(display, screen); - Window win; - Window troot, parent, *children; - unsigned char *buf = nullptr; - - if ((p_root == nullptr) || (p_desktop == nullptr)) { return 0; } - - /* some window managers set __SWM_VROOT to some child of root window */ - - XQueryTree(display, root, &troot, &parent, &children, &n); - for (i = 0; i < static_cast(n); i++) { - if (XGetWindowProperty(display, children[i], ATOM(__SWM_VROOT), 0, 1, False, - XA_WINDOW, &type, &format, &nitems, &bytes, - &buf) == Success && - type == XA_WINDOW) { - win = *reinterpret_cast(buf); - XFree(buf); - XFree(children); - fprintf(stderr, - PACKAGE_NAME - ": desktop window (%lx) found from __SWM_VROOT property\n", - win); - fflush(stderr); - *p_root = win; - *p_desktop = win; - return win; - } - - if (buf != nullptr) { - XFree(buf); - buf = nullptr; - } - } - XFree(children); +static Window find_desktop_window(Window root) { + Window desktop = root; /* get subwindows from root */ - win = find_subwindow(root, -1, -1); - + desktop = find_subwindow(root, -1, -1); update_workarea(); + desktop = find_subwindow(desktop, workarea[2], workarea[3]); - win = find_subwindow(win, workarea[2], workarea[3]); - - if (buf != nullptr) { - XFree(buf); - buf = nullptr; - } - - if (win != root) { - fprintf(stderr, - PACKAGE_NAME - ": desktop window (%lx) is subwindow of root window (%lx)\n", - win, root); + if (desktop != root) { + DBGP2("desktop window (0x%lx) is subwindow of root window (0x%lx)", desktop, + root); } else { - fprintf(stderr, PACKAGE_NAME ": desktop window (%lx) is root window\n", - win); + DBGP2("desktop window (0x%lx) is root window", desktop); } - - fflush(stderr); - - *p_root = root; - *p_desktop = win; - - return win; + return desktop; } #ifdef OWN_WINDOW @@ -579,18 +579,21 @@ void x11_init_window(lua::state &l, bool own) { // own is unused if OWN_WINDOW is not defined (void)own; + window.root = VRootWindow(display, screen); + if (window.root == None) { + DBGP2("no desktop window found"); + return; + } + window.desktop = find_desktop_window(window.root); + + window.visual = DefaultVisual(display, screen); + window.colourmap = DefaultColormap(display, screen); + #ifdef OWN_WINDOW if (own) { int depth = 0, flags = CWOverrideRedirect | CWBackingStore; Visual *visual = nullptr; - if (find_desktop_window(&window.root, &window.desktop) == 0U) { - DBGP2("no desktop window found"); - return; - } - - window.visual = DefaultVisual(display, screen); - window.colourmap = DefaultColormap(display, screen); depth = CopyFromParent; visual = CopyFromParent; #ifdef BUILD_ARGB @@ -909,16 +912,7 @@ void x11_init_window(lua::state &l, bool own) { { XWindowAttributes attrs; - if (window.window == 0u) { - window.window = find_desktop_window(&window.root, &window.desktop); - } - if (window.window == 0u) { - DBGP2("no root window found"); - return; - } - - window.visual = DefaultVisual(display, screen); - window.colourmap = DefaultColormap(display, screen); + if (window.window == None) { window.window = window.desktop; } if (XGetWindowAttributes(display, window.window, &attrs) != 0) { window.width = attrs.width; @@ -939,14 +933,10 @@ void x11_init_window(lua::state &l, bool own) { input_mask |= StructureNotifyMask | ButtonPressMask | ButtonReleaseMask; } #ifdef BUILD_MOUSE_EVENTS - /* it's not recommended to add event masks to special windows in X; causes a - * crash */ - if (own && own_window_type.get(l) != TYPE_DESKTOP) { - input_mask |= PointerMotionMask | ButtonPressMask | ButtonReleaseMask; - } 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 +945,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; @@ -964,18 +954,49 @@ 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_HierarchyChanged); 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); + } + + // setup cache + int num_devices; + XDeviceInfo *info = XListInputDevices(display, &num_devices); + for (int i = 0; i < num_devices; i++) { + if (info[i].use == IsXPointer || info[i].use == IsXExtensionPointer) { + conky::device_info::from_xi_id(info[i].id, display); + } + } + XFreeDeviceList(info); + xinput_ok = true; } while (false); #endif /* BUILD_XINPUT */ + // Fallback to basic X11 enter/leave events if xinput fails to init. + // It's not recommended to add event masks to special windows in X; causes a + // crash (thus own_window_type != TYPE_DESKTOP) if (!xinput_ok && own && own_window_type.get(l) != TYPE_DESKTOP) { - input_mask |= EnterWindowMask | LeaveWindowMask; + input_mask |= PointerMotionMask | EnterWindowMask | LeaveWindowMask; } #endif /* BUILD_MOUSE_EVENTS */ #endif /* OWN_WINDOW */ @@ -1342,20 +1363,10 @@ void print_mouse_speed(struct text_object *obj, char *p, snprintf(p, p_max_size, "%d%%", (110 - threshold)); } -InputEvent *xev_as_input_event(XEvent &ev) { - if (ev.type == KeyPress || ev.type == KeyRelease || ev.type == ButtonPress || - ev.type == ButtonRelease || ev.type == MotionNotify || - ev.type == EnterNotify || ev.type == LeaveNotify) { - return reinterpret_cast(&ev); - } else { - return nullptr; - } -} - /// @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 +1375,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,138 +1400,182 @@ int ev_to_mask(int event_type) { } } +#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.desktop, 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; - InputEvent *i_ev = xev_as_input_event(ev); - if (i_ev == nullptr) { + // 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 + + if (!(ev.type == KeyPress || ev.type == KeyRelease || + ev.type == ButtonPress || ev.type == ButtonRelease || + ev.type == MotionNotify || ev.type == EnterNotify || + ev.type == LeaveNotify)) { // Not a known input event; blindly propagating them causes loops and all // sorts of other evil. return; } + // Note that using ev.xbutton is the same as using any of the above events. + // It's only important we don't access fields that are not common to all of + // them. - i_ev->common.window = window.desktop; - i_ev->common.x = i_ev->common.x_root; - i_ev->common.y = i_ev->common.y_root; - i_ev->common.time = CurrentTime; + ev.xbutton.window = window.desktop; + ev.xbutton.x = ev.xbutton.x_root; + ev.xbutton.y = ev.xbutton.y_root; + ev.xbutton.time = CurrentTime; /* forward the event to the window below conky (e.g. caja) or desktop */ { std::vector below = query_x11_windows_at_pos( - display, i_ev->common.x_root, i_ev->common.y_root, + display, ev.xbutton.x_root, ev.xbutton.y_root, [](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()) { - i_ev->common.window = below.back(); + ev.xbutton.window = below.back(); Window _ignore; // Update event x and y coordinates to be target window relative - XTranslateCoordinates(display, window.root, i_ev->common.window, - i_ev->common.x_root, i_ev->common.y_root, - &i_ev->common.x, &i_ev->common.y, &_ignore); + XTranslateCoordinates(display, window.root, ev.xbutton.window, + ev.xbutton.x_root, ev.xbutton.y_root, &ev.xbutton.x, + &ev.xbutton.y, &_ignore); } // drop below vector } + int mask = + ev_to_mask(ev.type, ev.type == ButtonRelease ? ev.xbutton.button : 0); XUngrabPointer(display, CurrentTime); - XSendEvent(display, i_ev->common.window, True, ev_to_mask(i_ev->type), &ev); + XSendEvent(display, ev.xbutton.window, True, mask, &ev); if (focus) { - XSetInputFocus(display, i_ev->common.window, RevertToParent, CurrentTime); + XSetInputFocus(display, ev.xbutton.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; +Window query_x11_top_parent(Display *display, Window child) { + Window root = DefaultVRootWindow(display); - Window current = parent; + if (child == None || child == root) return child; - while (XQueryTree(display, current, &_ignored, &_ignored, &children, - &count) == Success && - count != 0) { - current = children[count - 1]; - XFree(children); - } + Window ret_root, parent, *children; + std::uint32_t child_count; + + Window current = child; + int i; + do { + if (XQueryTree(display, current, &ret_root, &parent, &children, + &child_count) == 0) { + break; + } + if (child_count != 0) XFree(children); + if (parent == root) break; + current = parent; + } while (true); 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, (~0L), 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, bytes_after / 4, - False, XA_WINDOW, &actual_type, &actual_format, - &nitems, &bytes_after, &data) == Success) { + if (actual_format == XA_WINDOW && nitems > 0) { Window *wdata = reinterpret_cast(data); std::vector result(wdata, wdata + nitems); - free(data); + XFree(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; + return std::vector{}; +} - if (XGetWindowProperty(display, root, clients_atom, 0, count, 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; - } - } +std::vector query_x11_windows(Display *display) { + Window root = DefaultRootWindow(display); - // slowest method that also returns inaccurate results: + Atom clients_atom = ATOM(_NET_CLIENT_LIST_STACKING); + std::vector result = + x11_atom_window_list(display, root, clients_atom); + if (result.empty()) { return result; } - // TODO: How do we remove window decorations and other unwanted WM/DE junk - // from this? + clients_atom = ATOM(_NET_CLIENT_LIST); + result = x11_atom_window_list(display, root, clients_atom); + if (result.empty()) { return result; } - std::vector result; - std::vector queue = {root}; + // slowest method + + std::vector queue = {DefaultVRootWindow(display)}; Window _ignored, *children; std::uint32_t count; + const auto has_wm_hints = [&](Window window) { + auto hints = XGetWMHints(display, window); + bool result = hints != NULL; + if (result) XFree(hints); + return result; + }; + while (!queue.empty()) { Window current = queue.back(); queue.pop_back(); - if (XQueryTree(display, current, &_ignored, &_ignored, &children, &count) == - Success && - count != 0) { - for (size_t i = 0; i < count; i++) { - queue.push_back(children[i]); - result.push_back(current); - } - XFree(children); + if (XQueryTree(display, current, &_ignored, &_ignored, &children, &count)) { + for (size_t i = 0; i < count; i++) queue.push_back(children[i]); + if (has_wm_hints(current)) result.push_back(current); + if (count > 0) XFree(children); } } @@ -1515,7 +1583,7 @@ std::vector query_x11_windows(Display *display) { } Window query_x11_window_at_pos(Display *display, int x, int y) { - Window root = DefaultRootWindow(display); + Window root = DefaultVRootWindow(display); // these values are ignored but NULL can't be passed to XQueryPointer. Window root_return; @@ -1535,7 +1603,7 @@ std::vector query_x11_windows_at_pos( std::function predicate) { std::vector result; - Window root = DefaultRootWindow(display); + Window root = DefaultVRootWindow(display); XWindowAttributes attr; for (Window current : query_x11_windows(display)) { diff --git a/src/x11.h b/src/x11.h index 62314e3a6..70f858871 100644 --- a/src/x11.h +++ b/src/x11.h @@ -24,6 +24,7 @@ #pragma once +#include "config.h" #include "setting.hh" #include @@ -122,42 +123,22 @@ void set_struts(int); void x11_init_window(lua::state &l, bool own); void deinit_x11(); -// Fields common to all X11 input events -struct InputEventCommon { - int type; /* event type */ - uint64_t serial; /* # of last request processed by server */ - Bool send_event; /* true if this came from a SendEvent request */ - Display *display; /* Display the event was read from */ - Window window; /* "event" window reported relative to */ - Window root; /* root window that the event occurred on */ - Window subwindow; /* child window */ - Time time; /* milliseconds */ - int32_t x, y; /* pointer x, y coordinates in event window */ - int32_t x_root, y_root; /* coordinates relative to root */ - uint32_t state; /* key or button mask */ -}; - -union InputEvent { - int type; // event type - - InputEventCommon common; - - // Discrete interfaces - XAnyEvent xany; // common event interface - XKeyEvent xkey; // KeyPress & KeyRelease events - XButtonEvent xbutton; // ButtonPress & ButtonRelease events - XMotionEvent xmotion; // MotionNotify event - XCrossingEvent xcrossing; // EnterNotify & LeaveNotify events - - // Ensures InputEvent matches memory layout of XEvent. - // Accessing base variant is as code smell. - XEvent base; -}; - -// 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, const void *cookie); +/// @brief Forwards argument event to the top-most window at event positon that +/// isn't conky. +/// +/// Calling this function is time sensitive as it will query window at event +/// position **at invocation time**. +/// @param event event to forward +/// @param cookie optional cookie data +void propagate_x11_event(XEvent &event, const void *cookie = nullptr); + +/// @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. /// @@ -174,15 +155,15 @@ void propagate_x11_event(XEvent &ev, const void *cookie); /// list of windows std::vector query_x11_windows(Display *display); -/// @brief Finds the last descendant of a window (leaf) on the graph. +/// @brief Finds the last ascendant of a window (trunk) before root. /// -/// 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. +/// If provided `child` is root or has no windows between root and itself, the +/// `child` is returned. /// /// @param display display of parent -/// @return a descendant window -Window query_x11_last_descendant(Display *display, Window parent); +/// @param child window whose parents to query +/// @return the top level ascendant window +Window query_x11_top_parent(Display *display, Window child); /// @brief Returns the top-most window overlapping provided screen coordinates. ///