// gKeyMapper - popup handling that GTK gets wrong in this version. // // GTK 4.6's popovers - the list a dropdown opens and the header bar menu - grab // the pointer while they are open. An X11 grab swallows every click of the // other clients, and this GTK does not close the popup on a click outside the // application either: with a dropdown open, nothing on the desktop can be // clicked any more, neither another window nor the desktop itself, until Escape // is pressed. GTK 3 replayed such a click to the window under the pointer, GTK 4 // dropped that. A popup that is left open is not the problem; a desktop that is // frozen is. // // A popover only grabs while its "autohide" is on, so switching that off hands // the clicks back to the rest of the desktop. The popups are then closed here, // which is the part that autohide would have done: // - a click in the window, outside the popup: on the press, so the click still // reaches the widget under the pointer (a previous popup must not eat it), // - a click on the widget that opened it: left alone, its own toggle closes it, // - a click in the popup: left alone, it picks the entry and closes the popup, // - a click anywhere else (another program, the desktop): the window loses the // focus, and that is what closes it here - the click itself goes to whatever // was clicked, which is the whole point, // - Escape. #include "popovers.h" #include namespace gkeymapper { namespace { void collect_popovers(GtkWidget* widget, std::vector* popovers) { for (GtkWidget* child = gtk_widget_get_first_child(widget); child != nullptr; child = gtk_widget_get_next_sibling(child)) { if (GTK_IS_POPOVER(child)) { popovers->push_back(child); } collect_popovers(child, popovers); } } void popovers_of(GtkWindow* window, std::vector* popovers) { collect_popovers(GTK_WIDGET(window), popovers); } // A press in the popup itself is a choice and must reach the entry under the // pointer. GTK hands such a press to the popup's own surface, so this controller // normally does not see it at all - the check is a guard for the case where it // does, and it has to be by surface and not by position, because the coordinates // of that press are the popup's and not the window's. bool press_on_popup(GdkEvent* event, GtkWidget* popover) { GdkSurface* popup = gtk_native_get_surface(GTK_NATIVE(popover)); return popup != nullptr && gdk_event_get_surface(event) == popup; } bool press_inside(GtkWidget* widget, GtkWindow* window, double x, double y) { graphene_rect_t rect; if (widget == nullptr || !gtk_widget_compute_bounds(widget, GTK_WIDGET(window), &rect)) { return false; } return x >= rect.origin.x && x <= rect.origin.x + rect.size.width && y >= rect.origin.y && y <= rect.origin.y + rect.size.height; } void close_visible_popovers(GtkWindow* window) { std::vector popovers; popovers_of(window, &popovers); for (GtkWidget* popover : popovers) { if (gtk_widget_get_visible(popover)) { gtk_popover_popdown(GTK_POPOVER(popover)); } } } gboolean on_event(GtkEventControllerLegacy* /*controller*/, GdkEvent* event, gpointer userData) { GtkWindow* window = GTK_WINDOW(userData); const GdkEventType type = gdk_event_get_event_type(event); if (type != GDK_BUTTON_PRESS && type != GDK_KEY_PRESS) { return GDK_EVENT_PROPAGATE; } std::vector popovers; popovers_of(window, &popovers); for (GtkWidget* popover : popovers) { // A popover GTK made after this was installed must not grab either; the // press that opens it is seen here before the widget acts on it, and // switching a visible one off drops the grab right away. gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE); } if (type == GDK_KEY_PRESS) { if (g_strcmp0(gdk_keyval_name(gdk_key_event_get_keyval(event)), "Escape") == 0) { for (GtkWidget* popover : popovers) { if (gtk_widget_get_visible(popover)) { gtk_popover_popdown(GTK_POPOVER(popover)); } } } return GDK_EVENT_PROPAGATE; } double x = 0; double y = 0; gdk_event_get_position(event, &x, &y); for (GtkWidget* popover : popovers) { if (!gtk_widget_get_visible(popover) || press_on_popup(event, popover)) { continue; } // The widget that opened the popup sits in the window, so the position is // in the window's coordinates here. if (press_inside(gtk_widget_get_parent(popover), window, x, y)) { continue; } gtk_popover_popdown(GTK_POPOVER(popover)); } return GDK_EVENT_PROPAGATE; } void on_focus_leave(GtkEventControllerFocus* /*controller*/, gpointer userData) { close_visible_popovers(GTK_WINDOW(userData)); } } // namespace void make_popovers_release_input(GtkWindow* window) { std::vector popovers; popovers_of(window, &popovers); for (GtkWidget* popover : popovers) { gtk_popover_set_autohide(GTK_POPOVER(popover), FALSE); } // A legacy controller does not claim the events it sees, so the widgets keep // behaving as usual; the capture phase is what makes it see a press before // the widget under the pointer does anything with it. GtkEventController* events = gtk_event_controller_legacy_new(); gtk_event_controller_set_propagation_phase(events, GTK_PHASE_CAPTURE); g_signal_connect(events, "event", G_CALLBACK(on_event), window); gtk_widget_add_controller(GTK_WIDGET(window), events); GtkEventController* focus = gtk_event_controller_focus_new(); g_signal_connect(focus, "leave", G_CALLBACK(on_focus_leave), window); gtk_widget_add_controller(GTK_WIDGET(window), focus); } } // namespace gkeymapper