Files
gKeyMapper/src/popovers.cpp
2026-10-18 20:26:22 +02:00

145 lines
5.8 KiB
C++

// 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 <vector>
namespace gkeymapper {
namespace {
void collect_popovers(GtkWidget* widget, std::vector<GtkWidget*>* 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<GtkWidget*>* 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<GtkWidget*> 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<GtkWidget*> 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<GtkWidget*> 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