Event propagation order and children

Hello,

I’m sure this is a common issue but I’m having trouble finding relevant discussions or explanations.

I did find this document about event propagation, but not much more.

I’m having this issue now the second time, in a totally different application than the first time around, so I’m sure this must be a common issue.

Let’s say that I have window and a small button in the middle of the window. I want to capture events… let’s say mouse wheel events (could also be keyboard events)… but I want to do something if the mouse was over the window itself, and something else if the mouse was over the button.

The problem for me is that if both the window and the button register for the events, the window will get the event first, and only later will the button get the event. In this case I’d rather have the opposite: the button would get the event, return true (the event was handled) and the event is not propagated to the window, the end. But that’s now how it works.

And so I’m not sure what to do. The mouse wheel handler on the window could apply its action, and the mouse wheel action on the button could reverse the action from the window handler (knowing it has just executed), then apply its own action, but that’s really ugly.

What is the idiomatic way to handle this scenario in GTK? Thank you!

Emmanuel

1 Like

In GTK3, capturing events from the parent container before it reaches its descendants is not possible.

In GTK4, capturing is a phase of a GtkEventController.

Thank you! Ok, for now i’m only interested in gtk3. So if I understood you correctly, in this case both widgets are going to get notified of the event and that cannot be prevented.

So how to prevent the parent from the running the action then? (I want only the child to run the action)

I had some idea that the parent widget could use idle_add() to run its action, and the child when executing its action, would somehow set a marker for which the callback would check, or maybe the child could cancel the scheduled callback. That would be relying on the fact that the idle_add() callback would be executed after the child callback, which I’m not sure is true, and it also feels very complicated.

I also thought of the parent calling gtk_window_get_focus() to find out whether the child has the focus, but that doesn’t work for mouse wheel events where the child may not have the focus, even though the mouse is on top of it.

So right now I don’t have a solution :frowning:

GTK3’s input handling model is similar to GTK4’s, except it only has the “bubble” phase: event handling will always start at the innermost descendant, and “bubble” up the hierarchy through its ancestors, until it reaches the top level container (a GtkWindow).

If you wish to consume an event, you must return GDK_EVENT_STOP from the signal handler.

“Focus” means “key focus”, not whether the pointer is hovering inside a widget.

Thank you. The child being notified first is what I would expect (and what I understood from the link I sent, except for keyboard events), but what I see is the parent (the window) callback being called first, and the child second.

It crosses my mind now that it may be relevant that there are EventBoxes involved too.

This is not my application (I’m trying to add a feature to the ‘waybar’ application), but I put the inspector diagram at the bottom of this post. The callbacks are registered through:

// window
    auto &window = const_cast<Bar &>(bar_).window;
    window.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
    window.signal_scroll_event().connect(sigc::mem_fun(*this, &Workspaces::handleScroll));

// pulseaudio child
  event_box_.add_events(Gdk::SCROLL_MASK | Gdk::SMOOTH_SCROLL_MASK);
  event_box_.signal_scroll_event().connect(sigc::mem_fun(*this, &Pulseaudio::handleScroll));

Printing lines in the callbacks, I see that the window callback is invoked before the pulseaudio (child) one. If the child was invoked first I’d have no problem, but that’s not the case right now.

also in that code both handlers return true which I understand in gtkmm mean that propagation would stop, yet both handlers are still called :confused:

Well if I need to do something like that, probably I’ll try to think it first how should the program flow works.
Now I am not sure that I Understand what are you trying to achieve there, but from what I understand this is what you try to do:

Code:

#include <gtk/gtk.h>

#define EVENTS_MASK ( GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_SCROLL_MASK )

gulong handler_window_scroll;
gulong handler_button_scroll;

void scroll_up_func   ( const gchar *const msg );
void scroll_down_func ( const gchar *const msg );

void enter_surface_func ( const gchar *const msg );
void leave_surface_func ( const gchar *const msg );

gboolean show_enter_event  ( GtkWidget *widget, GdkEvent *event, gpointer data );
gboolean show_leave_event  ( GtkWidget *widget, GdkEvent *event, gpointer data );
gboolean show_scroll_event ( GtkWidget *widget, GdkEvent *event, gpointer data );

int main ( int argc, char **argv )
{
    GtkWidget *window;
    GtkWidget *button;

    /// ***
    gtk_init ( &argc, &argv );

    window = gtk_window_new ( GTK_WINDOW_TOPLEVEL );
    gtk_window_set_default_size ( GTK_WINDOW ( window ), 200, 200 );
    gtk_widget_show ( window );

    /// ***
    button = gtk_button_new_with_label ( "Click me" );
    g_object_set ( button, "margin", 50, NULL );
    gtk_container_add ( GTK_CONTAINER ( window ), button );

    /// ******************************************************************************************************************************
    gint window_events;
    window_events = gtk_widget_get_events ( window ) | EVENTS_MASK;
    gtk_widget_add_events ( window,  window_events );

    g_signal_connect ( window, "enter-notify-event", G_CALLBACK ( show_enter_event ), button );
    g_signal_connect ( window, "leave-notify-event", G_CALLBACK ( show_leave_event ), button );
    handler_window_scroll = g_signal_connect ( window, "scroll-event", G_CALLBACK ( show_scroll_event ), button );

    /// ******************************************************************************************************************************
    gint button_events;
    button_events = gtk_widget_get_events ( button ) | EVENTS_MASK;
    gtk_widget_add_events ( button, button_events );

    g_signal_connect ( button, "enter-notify-event", G_CALLBACK ( show_enter_event ), window );
    g_signal_connect ( button, "leave-notify-event", G_CALLBACK ( show_leave_event ), window );
    handler_button_scroll = g_signal_connect ( button, "scroll-event",       G_CALLBACK ( show_scroll_event ), window );

    /// ******************************************************************************************************************************
    g_signal_handler_block ( window, handler_window_scroll );
    g_signal_handler_block ( button, handler_button_scroll );

    /// ***
    gtk_widget_show_all ( window );
    gtk_main();
}

void enter_surface_func ( const gchar *const msg )
{
    g_print ( "The mouse enter the %s area.\n", msg );
}

void leave_surface_func ( const gchar *const msg )
{
    g_print ( "\tThe mouse is leaving the %s area.\n", msg );
}

void scroll_up_func ( const gchar *const msg )
{
    g_print ( "%s Scroll-UP Detected\n", msg );
}

void scroll_down_func ( const gchar *const msg )
{
    g_print ( "%s Scroll-Down Detected\n", msg );
}

gboolean show_enter_event  ( GtkWidget *widget, GdkEvent *event, gpointer data )
{
    static gboolean flag = TRUE;
    if ( event->type == GDK_ENTER_NOTIFY )
    {
        if ( GTK_IS_BUTTON ( widget ) )
        {
            enter_surface_func ( "'Button'" );
            if ( flag == FALSE )
            {
                g_signal_handler_block ( data, handler_window_scroll );
                flag = TRUE;
                return GDK_EVENT_PROPAGATE;
            }
        }

        if ( GTK_IS_WINDOW ( widget ) )
        {
            enter_surface_func ( "'Window'" );
            if ( flag == TRUE )
            {
                g_signal_handler_unblock ( widget, handler_window_scroll );
                flag = FALSE;
                return GDK_EVENT_PROPAGATE;
            }
        }
    }

    return GDK_EVENT_STOP;
}

gboolean show_leave_event  ( GtkWidget *widget, GdkEvent *event, gpointer data )
{
    static gboolean flag = TRUE;
    if ( event->type == GDK_LEAVE_NOTIFY )
    {
        if ( GTK_IS_BUTTON ( widget ) )
        {
            leave_surface_func ( "'Button'" );
            if ( flag == FALSE )
            {
                g_signal_handler_block ( widget, handler_button_scroll );
                flag = TRUE;
                return GDK_EVENT_PROPAGATE;
            }
        }

        if ( GTK_IS_WINDOW ( widget ) )
        {
            leave_surface_func ( "'Window'" );
            if ( flag == TRUE )
            {
                g_signal_handler_unblock ( data, handler_button_scroll );
                flag = FALSE;
                return GDK_EVENT_PROPAGATE;
            }
        }
    }

    return GDK_EVENT_STOP;
}

gboolean show_scroll_event ( GtkWidget *widget, GdkEvent *event, gpointer data )
{
    if ( event->type == GDK_SCROLL )
    {
        if ( event->scroll.direction == GDK_SCROLL_DOWN )
        {
            if ( GTK_IS_WINDOW ( widget ) )
            {
                scroll_down_func ( "'Window'" );
            }
            else
            {
                scroll_down_func ( "'Button'" );
            }
        }

        if ( event->scroll.direction == GDK_SCROLL_UP )
        {
            if ( GTK_IS_BUTTON ( data ) )
            {
                scroll_up_func ( "'Window'" );
            }
            else
            {
                scroll_up_func ( "'Button'" );
            }
        }
    }
    return GDK_EVENT_STOP;
}

And the program flow is like this:

The mouse enter the 'Window' area.
'Window' Scroll-Down Detected
'Window' Scroll-UP Detected
    The mouse is leaving the 'Window' area.
The mouse enter the 'Button' area.
'Button' Scroll-Down Detected
'Button' Scroll-UP Detected
    The mouse is leaving the 'Button' area.
The mouse enter the 'Window' area.
    The mouse is leaving the 'Window' area.

It is not much to explain here, but more important is, that I am monitorin enter-notify-event and leave-notify-event events and based on which widget has focus I am blocking and unblocking their scroll-event.

Now of course, the program does what you need, but there is no certitude that works at all.
More over because I did not test it enough.

Have fun with GTK.

@MichiB thank you for the very clear answer and the effort. What I did for now though, was to remove the event handler on the child completely, and in the event handler of the parent, check the coordinates for the event and decide on the action based on whether the coordinates match the child or not. But yeah, your approach is good too. None of those is as simple as what I had in mind though. Thank you again!

1 Like