145 lines
5.8 KiB
C++
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
|