diff --git a/src/backends/x11/meta-backend-x11.c b/src/backends/x11/meta-backend-x11.c index d6819dfd6..4e8a0a73e 100644 --- a/src/backends/x11/meta-backend-x11.c +++ b/src/backends/x11/meta-backend-x11.c @@ -53,6 +53,7 @@ #include "clutter/x11/clutter-x11.h" #include "compositor/compositor-private.h" #include "core/display-private.h" +#include "core/keybindings-private.h" #include "meta/meta-cursor-tracker.h" #include "meta/util.h" @@ -425,6 +426,12 @@ handle_host_xevent (MetaBackend *backend, meta_backend_notify_keymap_layout_group_changed (backend, layout_group); } + if (display && + (xkb_ev->state.changed & (XkbModifierStateMask | + XkbModifierBaseMask))) + meta_display_process_zoom_modifier_state (display, + xkb_ev->state.mods, + xkb_ev->state.time); break; default: break; @@ -580,6 +587,14 @@ meta_backend_x11_post_init (MetaBackend *backend) meta_fatal ("X server doesn't have the XKB extension, version %d.%d or newer\n", XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION); + /* Make sure modifier state changes are among the selected + * XkbStateNotify details; the zoom modifier pointer grab depends on + * them (see meta_display_process_zoom_modifier_state). Only the + * listed detail bits are affected, existing selections remain. */ + XkbSelectEventDetails (priv->xdisplay, XkbUseCoreKbd, XkbStateNotify, + XkbModifierStateMask | XkbModifierBaseMask, + XkbModifierStateMask | XkbModifierBaseMask); + META_BACKEND_CLASS (meta_backend_x11_parent_class)->post_init (backend); monitor_manager = meta_backend_get_monitor_manager (backend); diff --git a/src/core/events.c b/src/core/events.c index 1ae1e933e..d704e3806 100644 --- a/src/core/events.c +++ b/src/core/events.c @@ -291,11 +291,24 @@ meta_display_handle_event (MetaDisplay *display, meta_display_a11y_zoom (display, FALSE); } + /* Don't let the zoom modifier's release trigger a + * modifier-only keybinding (e.g. Super opening the menu) */ + meta_keybindings_cancel_modifier_only (display); + + /* On X11, hold the pointer for the duration of the scroll burst + * so smooth-scroll events can't leak to the client under the + * pointer. */ + meta_display_zoom_scroll_grab_notify (display, + clutter_event_get_time (event)); + bypass_wayland = bypass_clutter = TRUE; goto out; } } + if (event->type == CLUTTER_BUTTON_PRESS) + meta_display_zoom_grab_break (display, clutter_event_get_time (event)); + if (event->type != CLUTTER_DEVICE_ADDED && event->type != CLUTTER_DEVICE_REMOVED) { diff --git a/src/core/keybindings-private.h b/src/core/keybindings-private.h index 0e3586869..1dfbda162 100644 --- a/src/core/keybindings-private.h +++ b/src/core/keybindings-private.h @@ -126,6 +126,10 @@ typedef struct /* Alt+click button grabs */ ClutterModifierType window_grab_modifiers; ClutterModifierType mouse_zoom_modifiers; + + /* Active pointer grab held during zoom scroll bursts (X11) */ + gboolean zoom_pointer_grabbed; + guint zoom_grab_timeout_id; } MetaKeyBindingManager; void meta_display_init_keys (MetaDisplay *display); @@ -144,6 +148,15 @@ int meta_keybindings_get_mouse_zoom_modifiers (MetaDisplay *display); ClutterModifierType meta_display_get_window_grab_modifiers (MetaDisplay *display); uint meta_keybindings_get_ignored_modifier_mask (MetaDisplay *display); +void meta_keybindings_cancel_modifier_only (MetaDisplay *display); + +void meta_display_process_zoom_modifier_state (MetaDisplay *display, + unsigned int mods, + guint32 timestamp); +void meta_display_zoom_scroll_grab_notify (MetaDisplay *display, + guint32 timestamp); +void meta_display_zoom_grab_break (MetaDisplay *display, + guint32 timestamp); gboolean meta_prefs_add_keybinding (const char *name, GSettings *settings, diff --git a/src/core/keybindings.c b/src/core/keybindings.c index 4a8a9a495..9b8ab3981 100644 --- a/src/core/keybindings.c +++ b/src/core/keybindings.c @@ -1503,6 +1503,12 @@ meta_display_shutdown_keys (MetaDisplay *display) g_hash_table_destroy (keys->key_bindings_index); g_hash_table_destroy (keys->key_bindings); + if (keys->zoom_grab_timeout_id != 0) + { + g_source_remove (keys->zoom_grab_timeout_id); + keys->zoom_grab_timeout_id = 0; + } + clear_active_keyboard_layouts (keys); } @@ -2474,6 +2480,7 @@ meta_keybindings_process_event (MetaDisplay *display, case CLUTTER_BUTTON_RELEASE: case CLUTTER_TOUCH_BEGIN: case CLUTTER_TOUCH_END: + case CLUTTER_SCROLL: modifier_key_only_pressed = FALSE; return FALSE; @@ -4207,6 +4214,110 @@ meta_keybindings_get_ignored_modifier_mask (MetaDisplay *display) return keys->ignored_modifier_mask; } +void +meta_keybindings_cancel_modifier_only (MetaDisplay *display) +{ + modifier_key_only_pressed = FALSE; +} + +#define ZOOM_SCROLL_GRAB_TIMEOUT_MS 1000 + +/* While zoom scrolling is in progress on X11 we hold an active grab on + * the pointer. The passive button 4/5 grabs only intercept the emulated + * legacy scroll button events; XInput2-aware clients scroll with the + * smooth-scroll XI_Motion events, which are delivered before any passive + * button grab can activate. Only an active device grab keeps them from + * reaching the client under the pointer. + * + * The grab is only held for the duration of a scroll burst (released on + * a short idle timeout, on a button press, or when the zoom modifier is + * released) so that modifier+click interactions keep working. A click + * swallowed by the grab cannot be re-injected: the physical press has + * already registered in the master device's button state, so a synthetic + * press is discarded as a duplicate. + */ + +void +meta_display_zoom_grab_break (MetaDisplay *display, + guint32 timestamp) +{ + MetaKeyBindingManager *keys = &display->key_binding_manager; + + if (keys->zoom_grab_timeout_id != 0) + { + g_source_remove (keys->zoom_grab_timeout_id); + keys->zoom_grab_timeout_id = 0; + } + + if (!keys->zoom_pointer_grabbed) + return; + + /* If a compositor or window grab took over in the meantime, our + * grab was already replaced; ungrabbing would break theirs. */ + if (display->event_route == META_EVENT_ROUTE_NORMAL) + meta_backend_ungrab_device (keys->backend, + META_VIRTUAL_CORE_POINTER_ID, + timestamp); + keys->zoom_pointer_grabbed = FALSE; +} + +static gboolean +zoom_scroll_grab_timeout (gpointer data) +{ + MetaDisplay *display = data; + MetaKeyBindingManager *keys = &display->key_binding_manager; + + keys->zoom_grab_timeout_id = 0; + meta_display_zoom_grab_break (display, META_CURRENT_TIME); + return G_SOURCE_REMOVE; +} + +void +meta_display_zoom_scroll_grab_notify (MetaDisplay *display, + guint32 timestamp) +{ + MetaKeyBindingManager *keys = &display->key_binding_manager; + + if (meta_is_wayland_compositor ()) + return; + + if (!keys->zoom_pointer_grabbed && + display->event_route == META_EVENT_ROUTE_NORMAL && + display->grab_op == META_GRAB_OP_NONE) + { + keys->zoom_pointer_grabbed = + meta_backend_grab_device (keys->backend, + META_VIRTUAL_CORE_POINTER_ID, + timestamp); + } + + if (keys->zoom_pointer_grabbed) + { + if (keys->zoom_grab_timeout_id != 0) + g_source_remove (keys->zoom_grab_timeout_id); + keys->zoom_grab_timeout_id = g_timeout_add (ZOOM_SCROLL_GRAB_TIMEOUT_MS, + zoom_scroll_grab_timeout, + display); + } +} + +/* Called on XkbStateNotify: release the scroll grab as soon as the zoom + * modifier is no longer held. */ +void +meta_display_process_zoom_modifier_state (MetaDisplay *display, + unsigned int mods, + guint32 timestamp) +{ + MetaKeyBindingManager *keys = &display->key_binding_manager; + + if (!keys->zoom_pointer_grabbed) + return; + + if ((mods & ~keys->ignored_modifier_mask) != keys->mouse_zoom_modifiers) + meta_display_zoom_grab_break (display, timestamp); +} + + static void init_builtin_key_bindings (MetaDisplay *display) {