From 7708c295740dcf04794b6a1ea3fb5501e0c5ff74 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 15:21:24 -0300 Subject: [PATCH 1/4] gtk4-prep: keep darkroom module header buttons visible only while hovering --- src/develop/imageop.c | 74 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/src/develop/imageop.c b/src/develop/imageop.c index e33c394c021..206b403ac30 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -449,6 +449,45 @@ void dt_iop_init_pipe(dt_iop_module_t *module, piece->blendop_data = calloc(1, sizeof(dt_develop_blend_params_t)); } +/* the module whose header buttons are currently shown (if any) */ +static dt_iop_module_t *_header_buttons_module = NULL; + +#if !GTK_CHECK_VERSION(4, 0, 0) +static gboolean _pointer_in_module(const dt_iop_module_t *module) +{ + GdkDevice *const device = + gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_display_get_default())); + gint x, y; + + /* gdk_window_get_device_position() returns the window under the device, not + * a within-bounds test (over the label's event box it returns that window, + * over the plain header background it returns NULL). Compare the returned + * coordinates against the window size instead. + * GTK3-only: on the GTK4 switch this becomes gdk_surface_get_device_position(). */ + GtkWidget *const header_evb = + dtgtk_expander_get_header_event_box(DTGTK_EXPANDER(module->expander)); + if(gtk_widget_get_realized(header_evb)) + { + GdkWindow *const win = gtk_widget_get_window(header_evb); + gdk_window_get_device_position(win, device, &x, &y, NULL); + if(x >= 0 && y >= 0 + && x < gdk_window_get_width(win) && y < gdk_window_get_height(win)) + return TRUE; + } + GtkWidget *const body_evb = + dtgtk_expander_get_body_event_box(DTGTK_EXPANDER(module->expander)); + if(gtk_widget_get_realized(body_evb)) + { + GdkWindow *const win = gtk_widget_get_window(body_evb); + gdk_window_get_device_position(win, device, &x, &y, NULL); + if(x >= 0 && y >= 0 + && x < gdk_window_get_width(win) && y < gdk_window_get_height(win)) + return TRUE; + } + return FALSE; +} +#endif + static void _header_enter_notify_callback(GtkEventControllerMotion *controller, gdouble x, gdouble y, @@ -463,18 +502,31 @@ static void _header_motion_notify_show_callback(GtkEventControllerMotion *contro dt_iop_module_t *module) { darktable.control->element = DT_ACTION_ELEMENT_SHOW; + + /* entering another module's header generates no usable leave for the + * previous one: the leave crossing's position is still within the old + * module's bounds, so the position check in the hide callback cannot hide + * it. Hide the previously shown buttons here instead. */ + if(_header_buttons_module && _header_buttons_module != module + && g_list_find(darktable.develop->iop, _header_buttons_module)) + dt_iop_show_hide_header_buttons(_header_buttons_module, NULL, FALSE, FALSE); + _header_buttons_module = module; + dt_iop_show_hide_header_buttons(module, NULL, TRUE, FALSE); } static void _header_motion_notify_hide_callback(GtkEventControllerMotion *controller, dt_iop_module_t *module) { - dt_iop_show_hide_header_buttons(module, NULL, FALSE, FALSE); -} - -static void _header_menu_deactivate_callback(GtkMenuShell *menushell, - dt_iop_module_t *module) -{ +#if !GTK_CHECK_VERSION(4, 0, 0) + /* GTK3-only: the motion controller fires leave for every crossing, + * including inferior ones (pointer moving to a child button) and the + * synthetic ones a menu popup/close generates. Only hide when the pointer + * has really left the module. + * GTK4: compiled out -- GtkEventControllerMotion only emits leave for real + * widget crossings there. */ + if(_pointer_in_module(module)) return; +#endif dt_iop_show_hide_header_buttons(module, NULL, FALSE, FALSE); } @@ -1047,9 +1099,6 @@ static gboolean _gui_multiinstance_callback(GtkButton *button, G_CALLBACK(_gui_rename_callback), module); gtk_menu_shell_append(menu, item); - g_signal_connect(G_OBJECT(menu), "deactivate", - G_CALLBACK(_header_menu_deactivate_callback), module); - dt_gui_menu_popup(GTK_MENU(menu), GTK_WIDGET(button), GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); @@ -2358,6 +2407,7 @@ void dt_iop_commit_params(dt_iop_module_t *module, void dt_iop_gui_cleanup_module(dt_iop_module_t *module) { + if(_header_buttons_module == module) _header_buttons_module = NULL; g_slist_free_full(module->widget_list, g_free); module->widget_list = NULL; DT_CONTROL_SIGNAL_DISCONNECT_ALL(module, module->so->op); @@ -2507,9 +2557,6 @@ static gboolean _presets_popup_callback(GtkButton *button, GtkMenu *menu = dt_gui_presets_popup_menu_show_for_module(module); - g_signal_connect(G_OBJECT(menu), "deactivate", - G_CALLBACK(_header_menu_deactivate_callback), module); - dt_gui_menu_popup(menu, GTK_WIDGET(button), GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); @@ -2528,9 +2575,6 @@ static void _presets_popup_clicked(GtkGestureSingle *gesture, GtkWidget *button = dt_gui_get_widget(gesture); GtkMenu *menu = dt_gui_presets_popup_menu_show_for_module(module); - g_signal_connect(G_OBJECT(menu), "deactivate", - G_CALLBACK(_header_menu_deactivate_callback), module); - dt_gui_menu_popup(menu, button, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); } From 9444f4d125374a42f6584b588030115110393784 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 15:21:25 -0300 Subject: [PATCH 2/4] gtk4-prep: restore right- and middle-click handling to gesture handlers --- src/develop/imageop.c | 16 +++++++++++++++- src/dtgtk/culling.c | 2 +- src/dtgtk/thumbtable.c | 2 +- src/gui/gtk.c | 6 ++++++ src/gui/preferences_ai.c | 2 +- src/libs/collect.c | 2 +- src/libs/import.c | 6 +++--- src/libs/masks.c | 2 +- src/libs/neural_restore.c | 2 +- src/libs/tools/viewswitcher.c | 2 +- src/views/map.c | 2 +- 11 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/develop/imageop.c b/src/develop/imageop.c index 206b403ac30..ecd6606548d 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -1132,7 +1132,21 @@ static void _gui_multiinstance_clicked(GtkGestureSingle *gesture, gdouble y, dt_iop_module_t *module) { - _gui_multiinstance_callback(NULL, NULL, module); + // restore the per-button behaviour of the old button-press-event handler: + // secondary-click creates a new instance, middle-click does nothing, + // anything else opens the multi-instance popup. + // gtk_gesture_single_get_current_button() is unchanged in GTK4. + const guint button = gtk_gesture_single_get_current_button(gesture); + if(button == GDK_BUTTON_SECONDARY) + { + if(!(module->flags() & IOP_FLAGS_ONE_INSTANCE)) + _gui_copy_callback(NULL, module); + return; + } + if(button == GDK_BUTTON_MIDDLE) + return; + + _gui_multiinstance_callback(GTK_BUTTON(dt_gui_get_widget(gesture)), NULL, module); } static gboolean _rename_module_key_pressed(GtkEventControllerKey *controller, diff --git a/src/dtgtk/culling.c b/src/dtgtk/culling.c index 6a91fea3232..d9b066ca9bb 100644 --- a/src/dtgtk/culling.c +++ b/src/dtgtk/culling.c @@ -1262,7 +1262,7 @@ dt_culling_t *dt_culling_new(const dt_culling_mode_t mode) * gesture (meaning it came from this bridge) and uses GDK_BUTTON_PRIMARY. * * GTK4 migration: remove this call. GtkGestureClick handles n_press - * natively and the callback can use gtk_gesture_single_get_button() + * natively and the callback can use gtk_gesture_single_get_current_button() * safely on the real gesture pointer. */ dt_gui_connect_double_click(table->widget, _event_button_press_cb, table); diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index e3873de9a6a..7bf5e3f004d 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -2634,7 +2634,7 @@ dt_thumbtable_t *dt_thumbtable_new() * gesture (meaning it came from this bridge) and uses GDK_BUTTON_PRIMARY. * * GTK4 migration: remove this call. GtkGestureClick handles n_press - * natively and the callback can use gtk_gesture_single_get_button() + * natively and the callback can use gtk_gesture_single_get_current_button() * safely on the real gesture pointer. */ dt_gui_connect_double_click(table->widget, _event_button_press_cb, table); diff --git a/src/gui/gtk.c b/src/gui/gtk.c index 807137b0c8b..ff827b3c45c 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -4802,6 +4802,12 @@ GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, // GTK4 GtkGesture *gesture = gtk_gesture_click_new(); // gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(gesture)); + // GtkGestureSingle:button defaults to GDK_BUTTON_PRIMARY on GTK3, which + // would silently drop right/middle clicks. The old button-press-event + // handlers reacted to all buttons, so listen to any button; callers + // distinguish them via gtk_gesture_single_get_current_button(). + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); + if(pressed) g_signal_connect(gesture, "pressed", G_CALLBACK(pressed), data); if(released) { diff --git a/src/gui/preferences_ai.c b/src/gui/preferences_ai.c index 00e1fa5b632..a8b2dbc0d28 100644 --- a/src/gui/preferences_ai.c +++ b/src/gui/preferences_ai.c @@ -1281,7 +1281,7 @@ static void _on_info_button_press_cb(GtkGestureSingle *gesture, int n_press, gpointer user_data) { GtkWidget *widget = dt_gui_get_widget(gesture); - if(gtk_gesture_single_get_button(gesture) != 1) return; + if(gtk_gesture_single_get_current_button(gesture) != 1) return; dt_prefs_ai_data_t *data = (dt_prefs_ai_data_t *)user_data; GtkTreeView *tv = GTK_TREE_VIEW(widget); diff --git a/src/libs/collect.c b/src/libs/collect.c index 2c37fa11fb7..eeedc2a76ba 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -3609,7 +3609,7 @@ static void popup_button_callback_cb(GtkGestureSingle *gesture, int n_press, double x, double y, dt_lib_collect_rule_t *d) { - if(gtk_gesture_single_get_button(gesture) != 1) + if(gtk_gesture_single_get_current_button(gesture) != 1) return; GtkWidget *menu = gtk_menu_new(); diff --git a/src/libs/import.c b/src/libs/import.c index 6ee7d4a03ab..b5d5572d45b 100644 --- a/src/libs/import.c +++ b/src/libs/import.c @@ -1289,7 +1289,7 @@ static void _places_button_press_cb(GtkGestureSingle *gesture, int n_press, gtk_tree_model_get(model, &iter, 0, &folder_name, 1, &folder_path, -1); // left-click: set as new root - if(gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { GtkTreeSelection *place_selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view)); gtk_tree_selection_select_path(place_selection, path); @@ -1324,7 +1324,7 @@ static void _folders_button_press_cb(GtkGestureSingle *gesture, int n_press, gtk_get_current_event_state(&state); if(n_press == 1 - && gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY + && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY && !dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK)) { GtkTreePath *path = NULL; @@ -1354,7 +1354,7 @@ static void _folders_button_press_cb(GtkGestureSingle *gesture, int n_press, } gtk_tree_path_free(path); } - else if(n_press >= 2 && gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY) + else if(n_press >= 2 && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { GtkTreePath *path = NULL; gtk_tree_view_get_path_at_pos(GTK_TREE_VIEW(view), diff --git a/src/libs/masks.c b/src/libs/masks.c index 85e1e8db1d7..f298d12bfa1 100644 --- a/src/libs/masks.c +++ b/src/libs/masks.c @@ -667,7 +667,7 @@ static void _tree_add_shape(GtkButton *button, gpointer shape) static void _bt_add_shape_cb(GtkGestureSingle *gesture, int n_press, double x, double y, gpointer shape) { - if(gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { #ifdef HAVE_AI if(GPOINTER_TO_INT(shape) == DT_MASKS_OBJECT && !dt_masks_object_available()) diff --git a/src/libs/neural_restore.c b/src/libs/neural_restore.c index d1767455d3e..df7f99bbbd5 100644 --- a/src/libs/neural_restore.c +++ b/src/libs/neural_restore.c @@ -3931,7 +3931,7 @@ static void _preview_button_press_cb(GtkGestureSingle *gesture, int n_press, GtkWidget *widget = dt_gui_get_widget(gesture); dt_lib_neural_restore_t *d = (dt_lib_neural_restore_t *)self->data; - if(gtk_gesture_single_get_button(gesture) != 1) return; + if(gtk_gesture_single_get_current_button(gesture) != 1) return; // thumbnail picking mode: click to select area and trigger preview if(d->picking_thumbnail && d->export_cairo) diff --git a/src/libs/tools/viewswitcher.c b/src/libs/tools/viewswitcher.c index 19b178eaf70..49ce5a2a807 100644 --- a/src/libs/tools/viewswitcher.c +++ b/src/libs/tools/viewswitcher.c @@ -290,7 +290,7 @@ static void _lib_viewswitcher_button_press_callback(GtkGestureSingle *gesture, i double x, double y, dt_view_t *view) { - if(gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { _switch_view(view); } diff --git a/src/views/map.c b/src/views/map.c index 2676ee17a2a..830d40f9d42 100644 --- a/src/views/map.c +++ b/src/views/map.c @@ -2168,7 +2168,7 @@ static void _view_map_click_pressed_cb(GtkGestureSingle *gesture, } } - if(gtk_gesture_single_get_button(gesture) == GDK_BUTTON_PRIMARY) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { // check if the click was in a location form - ctrl gives priority to images if(lib->loc.main.id > 0 From 6f8e623d86e433435c184e2f70f31316acbb8954 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 15:21:25 -0300 Subject: [PATCH 3/4] gtk4-prep: open module header menus on button release --- src/develop/imageop.c | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/develop/imageop.c b/src/develop/imageop.c index ecd6606548d..ec9dc888996 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -3294,14 +3294,28 @@ GtkWidget *dt_iop_gui_header_button(dt_iop_module_t *module, } dt_gui_connect_motion(button, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(element)); - dt_gui_connect_click(button, (element == DT_ACTION_ELEMENT_ENABLE) - ? _gui_off_button_clicked - : (element == DT_ACTION_ELEMENT_RESET) - ? _gui_reset_clicked - : (element == DT_ACTION_ELEMENT_PRESETS) - ? _presets_popup_clicked - : _gui_multiinstance_clicked, - NULL, module); + if(element == DT_ACTION_ELEMENT_PRESETS || element == DT_ACTION_ELEMENT_INSTANCE) + { + // open the menu on release so that the GtkButton finishes its own + // press/release cycle before the menu grabs the pointer: a grab on press + // would swallow the release and leave the button stuck in its pressed + // state. connect "released" directly, as dt_gui_connect_click() would + // also bridge "cancel" to a synthetic release and open the menu twice + GtkGestureSingle *const gesture = dt_gui_connect_click(button, NULL, NULL, module); + g_signal_connect(gesture, "released", + G_CALLBACK(element == DT_ACTION_ELEMENT_PRESETS + ? _presets_popup_clicked + : _gui_multiinstance_clicked), + module); + } + else + { + dt_gui_connect_click(button, + element == DT_ACTION_ELEMENT_ENABLE + ? _gui_off_button_clicked + : _gui_reset_clicked, + NULL, module); + } dt_action_define(&module->so->actions, NULL, NULL, button, NULL); gtk_widget_show(button); @@ -3418,7 +3432,13 @@ void dt_iop_gui_set_expander(dt_iop_module_t *module) module->header = header; /* setup the header box */ - dt_gui_connect_click(header_evb, NULL, _iop_plugin_header_released, module); + // connect the header's released handler directly: dt_gui_connect_click() + // would also bridge "cancel" to a synthetic release, which -- after the + // presets menu (opened by the real release) grabs the pointer and cancels + // the gesture -- would open the menu a second time + GtkGestureSingle *const header_gesture = dt_gui_connect_click(header_evb, NULL, NULL, module); + g_signal_connect(header_gesture, "released", + G_CALLBACK(_iop_plugin_header_released), module); dt_gui_connect_motion(header_evb, NULL, _header_motion_notify_show_callback, _header_motion_notify_hide_callback, module); From 173f41816ef826474e5e5fef80fc7e37303cfe93 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 21:35:58 -0300 Subject: [PATCH 4/4] gtk4-prep: track shown header-buttons module in dt_develop_t Replace the file-static _header_buttons_module with a dt_develop_t field so the state lives in the global darktable struct (avoids static variables, per review feedback on #21726). --- src/develop/develop.h | 1 + src/develop/imageop.c | 14 ++++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/develop/develop.h b/src/develop/develop.h index 41249769374..72b1af3eb5b 100644 --- a/src/develop/develop.h +++ b/src/develop/develop.h @@ -181,6 +181,7 @@ typedef struct dt_develop_t uint32_t timestamp; uint32_t preview_average_delay; struct dt_iop_module_t *gui_module; // this module claims gui expose/event callbacks. + struct dt_iop_module_t *header_buttons_module; // module whose header buttons are currently shown (if any) // image processing pipeline with caching struct dt_dev_pixelpipe_t *preview_pipe; diff --git a/src/develop/imageop.c b/src/develop/imageop.c index ec9dc888996..db15cba2be7 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -449,9 +449,6 @@ void dt_iop_init_pipe(dt_iop_module_t *module, piece->blendop_data = calloc(1, sizeof(dt_develop_blend_params_t)); } -/* the module whose header buttons are currently shown (if any) */ -static dt_iop_module_t *_header_buttons_module = NULL; - #if !GTK_CHECK_VERSION(4, 0, 0) static gboolean _pointer_in_module(const dt_iop_module_t *module) { @@ -507,10 +504,10 @@ static void _header_motion_notify_show_callback(GtkEventControllerMotion *contro * previous one: the leave crossing's position is still within the old * module's bounds, so the position check in the hide callback cannot hide * it. Hide the previously shown buttons here instead. */ - if(_header_buttons_module && _header_buttons_module != module - && g_list_find(darktable.develop->iop, _header_buttons_module)) - dt_iop_show_hide_header_buttons(_header_buttons_module, NULL, FALSE, FALSE); - _header_buttons_module = module; + if(darktable.develop->header_buttons_module && darktable.develop->header_buttons_module != module + && g_list_find(darktable.develop->iop, darktable.develop->header_buttons_module)) + dt_iop_show_hide_header_buttons(darktable.develop->header_buttons_module, NULL, FALSE, FALSE); + darktable.develop->header_buttons_module = module; dt_iop_show_hide_header_buttons(module, NULL, TRUE, FALSE); } @@ -2421,7 +2418,8 @@ void dt_iop_commit_params(dt_iop_module_t *module, void dt_iop_gui_cleanup_module(dt_iop_module_t *module) { - if(_header_buttons_module == module) _header_buttons_module = NULL; + if(darktable.develop->header_buttons_module == module) + darktable.develop->header_buttons_module = NULL; g_slist_free_full(module->widget_list, g_free); module->widget_list = NULL; DT_CONTROL_SIGNAL_DISCONNECT_ALL(module, module->so->op);