ui: a press anywhere in the window closes the time-window popover
The window control is a GtkMenuButton whose popover autohides -- which works for as long as
GTK holds the popover's grab, and the grab is what can go missing: a window manager moving
it, the nested popover the unit dropdown opens taking it, and every headless run, where
there is no manager to hand one out at all. After that a click anywhere else in the window
leaves the bubble up, which is the "sometimes it is awkward to close" report.
`on_window_popover_press()` states the rule outright instead of relying on the grab: any
press that reaches the window's own surface closes it. That needs no hit test beyond the
button -- a popover is its own surface, so a click *inside* it never arrives here -- and the
button is left alone, because its own click is the toggle that closes it. The press gesture
is on the window in the capture phase, so a widget under the press (the plot's own gestures
among them) cannot hide it.
[windowpop] reads both halves, with autohide turned off for the probe -- the state a user
meets when the grab goes missing:
[windowpop] opened=1 visible=1 click_gestures=2
[windowpop] after a press elsewhere: active=0 (want 0)
[windowpop] after a press on the button: active=1 (want 1)
`click_gestures` is read as well as the handler, so "the rule works" and "the rule is
connected" are two facts rather than one.
This commit is contained in:
1 parent
47d49e75ff
commit
d34b97158e
5 files changed
+126
No files matched your search
+39
@@ -4114,6 +4114,32 @@ GtkWidget* build_window_control(App& app) {
|
||||
return app.window_button;
|
||||
}
|
||||
|
||||
/// Any press in the window closes the time-window popover.
|
||||
///
|
||||
/// `GtkPopover`'s autohide does this by itself for as long as GTK holds the popover's grab -- and the grab
|
||||
/// is the thing that can go missing (a window manager moving it, a nested popover taking it for the unit
|
||||
/// menu, and every headless run, where there is no manager to hand one out at all), after which a click
|
||||
/// anywhere else in the window leaves the bubble up. That is the "sometimes it is awkward to close".
|
||||
///
|
||||
/// A press that reaches the window's own surface was *not* inside the popover: a popover is its own
|
||||
/// surface, so a click within it never arrives here. That is what makes this the rule stated outright
|
||||
/// rather than a grab away from working -- and it needs no hit test beyond leaving the button alone,
|
||||
/// whose own click is what toggles the bubble shut.
|
||||
void on_window_popover_press(GtkGestureClick*, int, double x, double y, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window_button == nullptr ||
|
||||
!gtk_menu_button_get_active(GTK_MENU_BUTTON(app->window_button))) {
|
||||
return;
|
||||
}
|
||||
// The button itself is left to its own click, which toggles the bubble shut -- closing it here would
|
||||
// only have the toggle open it again. Anything else on the window's surface closes it.
|
||||
for (GtkWidget* w = gtk_widget_pick(app->window, x, y, GTK_PICK_DEFAULT);
|
||||
w != nullptr && w != app->window; w = gtk_widget_get_parent(w)) {
|
||||
if (w == app->window_button) return;
|
||||
}
|
||||
gtk_menu_button_popdown(GTK_MENU_BUTTON(app->window_button));
|
||||
}
|
||||
|
||||
/// The one place a key turns into an action. Toggles go through
|
||||
/// gtk_toggle_button_set_active(), which emits ::toggled and so runs exactly the handler a
|
||||
/// click would; the non-toggles call their click handlers with a null button, which they
|
||||
@@ -4659,6 +4685,13 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_app_key_pressed), app);
|
||||
gtk_widget_add_controller(app->window, keys);
|
||||
|
||||
// Capture phase, unlike the keys above: a press belongs to whatever widget is under it (the plot's
|
||||
// gestures among them), and the point here is to see every one of them, not to win none of them.
|
||||
GtkGesture* press = gtk_gesture_click_new();
|
||||
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(press), GTK_PHASE_CAPTURE);
|
||||
g_signal_connect(press, "pressed", G_CALLBACK(on_window_popover_press), app);
|
||||
gtk_widget_add_controller(app->window, GTK_EVENT_CONTROLLER(press));
|
||||
|
||||
gtk_window_present(GTK_WINDOW(app->window));
|
||||
}
|
||||
|
||||
@@ -5279,6 +5312,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOWPOP_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), windowpop_probe_step, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
|
||||
}
|
||||
|
||||
@@ -1349,6 +1349,11 @@ gboolean zoomkeys_probe(gpointer user);
|
||||
void menus_probe_next(App& a, int step, guint delay_ms);
|
||||
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
||||
gboolean menus_probe_step(gpointer user);
|
||||
gboolean windowpop_probe_step(gpointer user);
|
||||
|
||||
/// Closes the time-window popover when a press lands elsewhere in the window (see ui.cpp). Declared for
|
||||
/// the probe, which drives it and presses the button to check the one case that must *not* close it.
|
||||
void on_window_popover_press(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user);
|
||||
gboolean color_probe(gpointer user);
|
||||
gboolean reseq_probe(gpointer user);
|
||||
void capture_probe_next(App& a, int step, guint delay_ms);
|
||||
|
||||
@@ -2500,6 +2500,63 @@ void menus_probe_next(App& a, int step, guint delay_ms) {
|
||||
g_timeout_add(delay_ms, menus_probe_step, &a);
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_WINDOWPOP_MS`: the time-window popover, closed by a press elsewhere in the window.
|
||||
///
|
||||
/// Autohide is turned off for the probe, exactly as the menu probe does it: a popover dismisses itself
|
||||
/// when GTK holds its *grab*, and a bare `xvfb-run` has no window manager to hand one out -- so with
|
||||
/// autohide on the bubble would not stay up to be tested. Autohide off is the state a user meets when
|
||||
/// the grab goes missing, which is the report this guards: the press has to close it anyway. The second
|
||||
/// half presses the button itself, which must *not* close it (its own click toggles).
|
||||
gboolean windowpop_probe_step(gpointer user) {
|
||||
static int step = 0;
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->window_button == nullptr) return G_SOURCE_REMOVE;
|
||||
const auto active = [a] {
|
||||
return gtk_menu_button_get_active(GTK_MENU_BUTTON(a->window_button)) ? 1 : 0;
|
||||
};
|
||||
if (step == 0) {
|
||||
GtkPopover* pop = gtk_menu_button_get_popover(GTK_MENU_BUTTON(a->window_button));
|
||||
if (pop != nullptr) gtk_popover_set_autohide(pop, FALSE);
|
||||
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
|
||||
step = 1;
|
||||
g_timeout_add(300, windowpop_probe_step, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
// How many click gestures the window carries, so that "the handler works" and "the handler is
|
||||
// connected" are read as two facts rather than one.
|
||||
int clicks = 0;
|
||||
{
|
||||
GListModel* controllers = gtk_widget_observe_controllers(a->window);
|
||||
const guint n = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0;
|
||||
for (guint i = 0; i < n; ++i) {
|
||||
GObject* item = G_OBJECT(g_list_model_get_item(controllers, i));
|
||||
if (item != nullptr) {
|
||||
if (GTK_IS_GESTURE_CLICK(item)) ++clicks;
|
||||
g_object_unref(item);
|
||||
}
|
||||
}
|
||||
if (controllers != nullptr) g_object_unref(controllers);
|
||||
}
|
||||
std::fprintf(stderr, "[windowpop] opened=%d visible=%d click_gestures=%d\n", active(),
|
||||
gtk_widget_is_visible(GTK_WIDGET(gtk_menu_button_get_popover(
|
||||
GTK_MENU_BUTTON(a->window_button))))
|
||||
? 1
|
||||
: 0,
|
||||
clicks);
|
||||
on_window_popover_press(nullptr, 0, 20.0, 200.0, a);
|
||||
std::fprintf(stderr, "[windowpop] after a press elsewhere: active=%d (want 0)\n", active());
|
||||
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
|
||||
graphene_rect_t r = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||
if (gtk_widget_compute_bounds(a->window_button, a->window, &r)) {
|
||||
on_window_popover_press(nullptr, 0, r.origin.x + r.size.width / 2.0,
|
||||
r.origin.y + r.size.height / 2.0, a);
|
||||
}
|
||||
std::fprintf(stderr, "[windowpop] after a press on the button: active=%d (want 1)\n", active());
|
||||
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean menus_probe_step(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
|
||||
Reference in new issue
Block a user