diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index 8ba15ae249c..2b3440a969c 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -534,17 +534,20 @@ static void _popup_scroll_cb(GtkEventControllerScroll *controller, if(w->type == DT_BAUHAUS_COMBOBOX) { // match keyboard: right & down -> next - int delta_x = 0, delta_y = 0; - dt_gui_get_scroll_unit_deltas_fallback(dx, dy, &delta_x, &delta_y); - const int delta = abs(delta_x) > abs(delta_y) ? delta_x : delta_y; + // the DISCRETE scroll proxy already accumulated smooth deltas into unit + // steps, so dx/dy are integer steps here. + const int delta = fabs(dx) > fabs(dy) ? (int)dx : (int)dy; if(delta != 0) _combobox_next_sensitive(w, delta, 0, w->combobox.mute_scrolling); } else { - int delta = 0; - dt_gui_get_scroll_unit_delta_fallback(dy, &delta); - _slider_zoom_range(w, delta); + // only zoom the range on a real scroll step: delta == 0 means no unit + // was accumulated yet and must NOT trigger the "reset zoom range" + // branch of _slider_zoom_range() (the middle-click action). + const int delta = (int)dy; + if(delta != 0) + _slider_zoom_range(w, delta); } } diff --git a/src/develop/blend_gui.c b/src/develop/blend_gui.c index c6449d2788b..cde3d520b52 100644 --- a/src/develop/blend_gui.c +++ b/src/develop/blend_gui.c @@ -1358,7 +1358,7 @@ static void _blendop_blendif_showmask_clicked(GtkGestureSingle *gesture, { DT_GUARD_GUI_UPDATE(); - if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; + if(dt_gui_current_button(gesture) != GDK_BUTTON_PRIMARY) return; GtkWidget *button = dt_gui_get_widget(gesture); @@ -1372,8 +1372,7 @@ static void _blendop_blendif_showmask_clicked(GtkGestureSingle *gesture, | DT_DEV_PIXELPIPE_DISPLAY_CHANNEL | DT_DEV_PIXELPIPE_DISPLAY_ANY); - GdkModifierType state; - gtk_get_current_event_state(&state); + GdkModifierType state = dt_gui_current_state(gesture); if(dt_modifier_is(state, GDK_CONTROL_MASK | GDK_SHIFT_MASK)) module->request_mask_display |= @@ -1416,7 +1415,7 @@ static void _blendop_masks_modes_none_clicked(GtkGestureSingle *gesture, GtkWidget *button = dt_gui_get_widget(gesture); dt_iop_gui_blend_data_t *data = module->blend_data; - if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY && data->selected_mask_mode != button) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->selected_mask_mode), @@ -1637,8 +1636,7 @@ static void _blendop_masks_add_shape(GtkGestureSingle *gesture, dt_iop_gui_blend_data_t *bd = self->blend_data; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = dt_gui_current_state(gesture); const gboolean continuous = dt_modifier_is(state, GDK_CONTROL_MASK); // find out who we are @@ -1705,8 +1703,7 @@ static void _blendop_masks_show_and_edit(GtkGestureSingle *gesture, dt_iop_color_picker_reset(self, FALSE); - GdkModifierType state; - gtk_get_current_event_state(&state); + GdkModifierType state = dt_gui_current_state(gesture); dt_masks_form_t *grp = dt_masks_get_from_id(darktable.develop, self->blend_params->mask_id); @@ -2611,7 +2608,7 @@ void dt_iop_gui_init_blendif(GtkWidget *blendw, dt_iop_module_t *module) _("pick GUI color from image\n" "ctrl+click or right-click to select an area")); gtk_widget_set_name(bd->colorpicker, "keep-active"); - dt_action_define_iop(module, "blend`pickers", N_("show color"), bd->colorpicker, &dt_action_def_toggle); + dt_action_define_iop(module, "blend`pickers", N_("show color"), bd->colorpicker, &dt_action_def_color_picker); bd->colorpicker_set_values = dt_color_picker_new(module, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_IO, header); @@ -2623,7 +2620,7 @@ void dt_iop_gui_init_blendif(GtkWidget *blendw, dt_iop_module_t *module) _("set the range based on an area from the image\n" "drag to use the input image\n" "ctrl+drag to use the output image")); - dt_action_define_iop(module, "blend`pickers", N_("set range"), bd->colorpicker_set_values, &dt_action_def_toggle); + dt_action_define_iop(module, "blend`pickers", N_("set range"), bd->colorpicker_set_values, &dt_action_def_color_picker); GtkWidget *btn = dt_iop_togglebutton_new(module, "blend`tools", N_("invert all channel's polarities"), NULL, diff --git a/src/develop/imageop.c b/src/develop/imageop.c index db15cba2be7..5e579a87060 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -2551,8 +2551,6 @@ static void _gui_reset_clicked(GtkGestureSingle *gesture, dt_iop_gui_update(module); dt_dev_add_history_item(module->dev, module, TRUE); - - dt_dev_add_history_item(module->dev, module, TRUE); } // rebuild the accelerators @@ -2591,6 +2589,22 @@ static void _presets_popup_clicked(GtkGestureSingle *gesture, button, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); } +/* per-presets-button hysteresis state: a continuous trackpad gesture is a + * series of swipes and each swipe ends with a short opposite-sign stream + * (fingers lifting / rebounding), which the discrete scroll proxy turns + * into a step back. Mid-list that oscillates between two presets; at the + * first/last preset it flashes between the boundary preset and its + * neighbour and spams the "(first)"/"(last)" toast. The state is stored + * per button (not process-global) so that a step on one module's presets + * cannot dampen another module's, and the dampener only applies to smooth + * (trackpad) scrolls: reversing a clicky wheel is a deliberate direction + * change and must apply immediately. */ +typedef struct dt_presets_scroll_t +{ + gint64 last_step_time; + int last_dir; +} dt_presets_scroll_t; + static void _presets_scrolled(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, @@ -2599,9 +2613,37 @@ static void _presets_scrolled(GtkEventControllerScroll *controller, if(dy == 0.0 && dx == 0.0) return; // preset cycling: right==down==next - int delta_x = 0, delta_y = 0; - dt_gui_get_scroll_unit_deltas_fallback(dx, dy, &delta_x, &delta_y); - const int delta = abs(delta_x) > abs(delta_y) ? delta_x : delta_y; + // the DISCRETE scroll proxy already accumulates smooth deltas into unit + // steps, so dx/dy are integer steps here; only apply a preset when one + // was actually emitted (a zero delta would query "adjacent to nothing" + // and show a misleading "(last)" toast). + const int delta = fabs(dx) > fabs(dy) ? (int)dx : (int)dy; + if(delta == 0) return; + + GdkEvent *event = gtk_get_current_event(); + const gboolean smooth = event + && dt_gdk_event_get_scroll_direction(event) == GDK_SCROLL_SMOOTH; + if(event) gdk_event_free(event); + + GtkWidget *widget = dt_gui_get_widget(controller); + dt_presets_scroll_t *state + = g_object_get_data(G_OBJECT(widget), "dt_presets_scroll_state"); + if(!state) + { + state = g_malloc0(sizeof(dt_presets_scroll_t)); + g_object_set_data_full(G_OBJECT(widget), "dt_presets_scroll_state", + state, g_free); + } + + const gint64 now = g_get_monotonic_time(); + if(smooth + && state->last_dir != 0 + && (delta > 0) != (state->last_dir > 0) + && now - state->last_step_time < 250000) + return; + state->last_dir = delta > 0 ? 1 : -1; + state->last_step_time = now; + dt_gui_presets_apply_adjacent_preset(module, delta); } diff --git a/src/develop/imageop_gui.c b/src/develop/imageop_gui.c index 009e3966ac1..48727b9477e 100644 --- a/src/develop/imageop_gui.c +++ b/src/develop/imageop_gui.c @@ -270,6 +270,9 @@ GtkWidget *dt_iop_togglebutton_new(dt_iop_module_t *self, const char *section, c g_signal_connect_data(gesture, "pressed", callback, self, NULL, 0); g_signal_connect(gesture, "begin", G_CALLBACK(_gesture_begin_claim), NULL); gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); + /* shortcut activation routes through this gesture (DT_ACTION_GESTURE_KEY, + * see _action_process_toggle in accelerators.c) */ + g_object_set_data(G_OBJECT(w), DT_ACTION_GESTURE_KEY, gesture); } if(!ctrl_label) diff --git a/src/dtgtk/button.c b/src/dtgtk/button.c index 500479f586b..ea36837e403 100644 --- a/src/dtgtk/button.c +++ b/src/dtgtk/button.c @@ -27,6 +27,38 @@ static void dtgtk_button_init(GtkDarktableButton *button) { } +/* GTK3 synthesizes a fake enter crossing (GDK_CROSSING_GTK_UNGRAB) on every + * widget a grab was shadowing when that grab ends -- menus, popovers, modal + * dialogs, ... -- so the button's internal enter handler marks the pointer as + * inside and the button keeps its hover state even after the pointer has + * left. The stale highlight then only clears on the next genuine crossing, + * which users see as a "sticky" hover after closing a menu or dialog. + * When a grab that shadowed the button ends, drop the stale hover/pressed + * flags unless the pointer really is over the button; genuine crossings + * re-add them as appropriate. + * GTK4 migration: delete this handler together with the rest of this file + * (GtkButton has no event window, gtk_container_add() is gone, ...); GTK4 + * removed grabs, so no synthetic crossings exist and the bug disappears. */ +static void _dtgtk_button_grab_notify(GtkWidget *widget, + gboolean was_grabbed, + gpointer user_data) +{ + if(!was_grabbed || !gtk_widget_get_realized(widget)) return; + + gint x, y; + gdk_window_get_device_position(gtk_widget_get_window(widget), + gdk_seat_get_pointer( + gdk_display_get_default_seat(gdk_display_get_default())), + &x, &y, NULL); + + GtkAllocation allocation; + gtk_widget_get_allocation(widget, &allocation); + if(x < allocation.x || y < allocation.y + || x >= allocation.x + allocation.width + || y >= allocation.y + allocation.height) + gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE); +} + static gboolean _button_draw(GtkWidget *widget, cairo_t *cr) { g_return_val_if_fail(widget != NULL, FALSE); @@ -124,9 +156,16 @@ GtkWidget *dtgtk_button_new(DTGTKCairoPaintIconFunc paint, gtk_container_add(GTK_CONTAINER(button), button->canvas); dt_gui_add_class(GTK_WIDGET(button), "dt_module_btn"); gtk_widget_set_name(GTK_WIDGET(button->canvas), "button-canvas"); + dtgtk_button_connect_stale_hover_cleanup(GTK_WIDGET(button)); return (GtkWidget *)button; } +void dtgtk_button_connect_stale_hover_cleanup(GtkWidget *widget) +{ + g_signal_connect(G_OBJECT(widget), "grab-notify", + G_CALLBACK(_dtgtk_button_grab_notify), NULL); +} + void dtgtk_button_set_paint(GtkDarktableButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, diff --git a/src/dtgtk/button.h b/src/dtgtk/button.h index b0994cb1eb3..eac71481765 100644 --- a/src/dtgtk/button.h +++ b/src/dtgtk/button.h @@ -45,6 +45,10 @@ struct _GtkDarktableButton GtkWidget *dtgtk_button_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata); /** set the paint function for a button */ void dtgtk_button_set_paint(GtkDarktableButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata); +/** clear the stale hover/pressed state GTK3 leaves on buttons after a grab + * (menu, popover, modal dialog) that shadowed them has ended; connect on any + * button that pops up menus or opens dialogs */ +void dtgtk_button_connect_stale_hover_cleanup(GtkWidget *widget); /** set the active state of the button icon */ void dtgtk_button_set_active(GtkDarktableButton *button, gboolean active); diff --git a/src/dtgtk/culling.c b/src/dtgtk/culling.c index 44114711288..8f384fee79c 100644 --- a/src/dtgtk/culling.c +++ b/src/dtgtk/culling.c @@ -700,12 +700,14 @@ static void _event_scroll(GtkEventControllerScroll *controller, if(direction == GDK_SCROLL_SMOOTH && !is_stop && dt_modifiers_include(state, GDK_CONTROL_MASK)) { - if(dx != 0.0 || dy != 0.0) + // raw platform deltas, not the attenuated controller deltas, so that + // one full unit of scroll (delta_y == 1.0) still matches the 0.5 + // zoom_delta of a discrete mouse-wheel click. right==up==zoom-in + gdouble ddx = 0.0, ddy = 0.0; + if(dt_gui_get_scroll_deltas((const GdkEventScroll *)event, &ddx, &ddy) + && (ddx != 0.0 || ddy != 0.0)) { - // controller dx/dy gives the raw fractional platform delta. - // Scale so that one full unit of scroll (delta_y == 1.0) matches the - // 0.5 zoom_delta of a discrete mouse-wheel click. right==up==zoom-in - const gdouble delta = fabs(dx) > fabs(dy) ? -dx : dy; + const gdouble delta = fabs(ddx) > fabs(ddy) ? -ddx : ddy; const float zoom_delta = (float)(-delta * 0.5); // convert screen to culling coordinates int ox = 0, oy = 0; @@ -742,18 +744,20 @@ static void _event_scroll(GtkEventControllerScroll *controller, fz, fz > 1.0f ? "pan path" : "navigate path"); if(fz > 1.0f) { - if(dx != 0.0 || dy != 0.0) + gdouble ddx = 0.0, ddy = 0.0; + if(dt_gui_get_scroll_deltas((const GdkEventScroll *)event, &ddx, &ddy) + && (ddx != 0.0 || ddy != 0.0)) { - // controller dx/dy is platform-normalised fractional units; - // scale to pixel-scale (matches the factor used by the center-widget pan path). + // raw platform deltas; scale to pixel-scale (matches the factor + // used by the center-widget pan path). dt_print(DT_DEBUG_INPUT, "[culling scroll] panning dx=%.3f dy=%.3f (scaled: dx=%.1f dy=%.1f)", - dx, dy, dx * 50.0, dy * 50.0); - dt_culling_pan_move(table, (float)(-dx * 50.0), (float)(-dy * 50.0), state); + ddx, ddy, ddx * 50.0, ddy * 50.0); + dt_culling_pan_move(table, (float)(-ddx * 50.0), (float)(-ddy * 50.0), state); } else { - dt_print(DT_DEBUG_INPUT, "[culling scroll] smooth pan: no delta from controller"); + dt_print(DT_DEBUG_INPUT, "[culling scroll] smooth pan: no delta"); } gdk_event_free(event); return; @@ -830,11 +834,10 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, return; } - table->mouse_inside = FALSE; - - /* Don't clear the mouse-over when leaving to a child widget (thumbnail), - * or while the pointer is grabbed: the shortcut machinery's synthetic - * crossings must not lose the hovered image (see #21729). + /* Don't clear the mouse-over nor the inside-table state when leaving to a + * child widget (thumbnail), or while the pointer is grabbed: the shortcut + * machinery's synthetic crossings must not lose the hovered image nor + * mouse_inside (see #21729, #21745). * GTK4 migration: drop the pointer-grab check (see * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ GdkEvent *event = gtk_get_current_event(); @@ -844,7 +847,10 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, && event->crossing.mode != GDK_CROSSING_GTK_GRAB && event->crossing.mode != GDK_CROSSING_GRAB && !dt_gui_pointer_is_grabbed()) + { + table->mouse_inside = FALSE; dt_control_set_mouse_over_id(NO_IMGID); + } gdk_event_free(event); } } @@ -875,6 +881,8 @@ static void _event_enter_cb(GtkEventControllerMotion *controller, * crossing detail is required: redraws under the pointer make GDK report * VIRTUAL/NONLINEAR details instead of INFERIOR, which would otherwise * leave a stale hovered image (see #21729). */ + table->mouse_inside = TRUE; + GdkEvent *event = gtk_get_current_event(); if(event) { @@ -892,20 +900,7 @@ static void _event_button_press_cb(GtkGestureSingle *gesture, gdouble y, dt_culling_t *table) { - /* - * GTK3 bridge for GDK_2BUTTON_PRESS (see dt_gui_connect_double_click): - * GtkGestureMultiPress doesn't process multi-press events, so when a - * double/triple-click is detected by the classic "button-press-event" - * signal handler, it calls this callback with gesture=NULL. In that - * case the button is always GDK_BUTTON_PRIMARY (GDK_2BUTTON_PRESS only - * occurs for primary button). - * - * GTK4 migration: remove the ternary and just call - * gtk_gesture_single_get_current_button(gesture) directly — GtkGestureClick - * will provide a valid gesture pointer for all n_press values. */ - const guint button = gesture - ? gtk_gesture_single_get_current_button(gesture) - : GDK_BUTTON_PRIMARY; + const guint button = gtk_gesture_single_get_current_button(gesture); if(button == GDK_BUTTON_PRIMARY && n_press == 1) { @@ -946,9 +941,7 @@ static void _event_button_press_cb(GtkGestureSingle *gesture, } // start panning — need root coordinates for pan tracking - const GdkEvent *current = gtk_get_current_event(); - if(current) - gdk_event_get_root_coords(current, &table->pan_x, &table->pan_y); + dt_gui_get_current_root_coords(&table->pan_x, &table->pan_y); table->panning = TRUE; } @@ -958,9 +951,8 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, dt_culling_t *table) { // get root coordinates for pan tracking - const GdkEvent *current = gtk_get_current_event(); gdouble root_x = 0, root_y = 0; - if(current) gdk_event_get_root_coords(current, &root_x, &root_y); + dt_gui_get_current_root_coords(&root_x, &root_y); table->mouse_inside = TRUE; @@ -1070,7 +1062,8 @@ static void _event_button_release_cb(GtkGestureSingle *gesture, // if the act_on algorithm need a specific culling "selection", // we use a very simple culling-specific selection if(dt_act_on_use_culling_selection() - && dt_is_valid_imgid(overid)) + && dt_is_valid_imgid(overid) + && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { const dt_imgid_t old_sel = table->selection; if(table->selection == overid) @@ -1297,22 +1290,12 @@ dt_culling_t *dt_culling_new(const dt_culling_mode_t mode) g_signal_connect(G_OBJECT(table->widget), "event", G_CALLBACK(_event_gesture), table); - dt_gui_connect_scroll(table->widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES - | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, + dt_gui_connect_scroll(table->widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, _event_scroll, table); g_signal_connect(G_OBJECT(table->widget), "draw", G_CALLBACK(_event_draw), table); dt_gui_connect_motion(table->widget, _event_motion_notify_cb, _event_enter_cb, _event_leave_cb, table); dt_gui_connect_click_all(table->widget, _event_button_press_cb, _event_button_release_cb, table); - /* GTK3 bridge: GtkGestureMultiPress does not process GDK_2BUTTON_PRESS. - * dt_gui_connect_double_click forwards double/triple clicks via a - * "button-press-event" signal handler. The callback checks for NULL - * 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_current_button() - * safely on the real gesture pointer. */ - dt_gui_connect_double_click(table->widget, _event_button_press_cb, table); // we register globals signals DT_CONTROL_SIGNAL_CONNECT(DT_SIGNAL_MOUSE_OVER_IMAGE_CHANGE, diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index e01e9808619..6e5bbf82105 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -1136,14 +1136,20 @@ static void _event_scroll(GtkEventControllerScroll *controller, { gdouble deltaf = 0.f; gboolean did_scroll; - if(dt_conf_get_bool("thumbtable_fractional_scrolling")) - { - // use controller dx/dy directly for fractional scrolling - did_scroll = (dx != 0.0 || dy != 0.0); + if(dt_conf_get_bool("thumbtable_fractional_scrolling") + && dt_gdk_event_get_scroll_direction(e) == GDK_SCROLL_SMOOTH) + { + // pixel-precise scrolling for precision touch pads: use the raw + // platform deltas (scaled back up in _event_scroll_compressed), not + // the attenuated controller deltas, so movement tracks the finger + // 1:1 like the native scrollbars. clicky wheels keep the + // row-by-row path below. + gdouble deltaf_x, deltaf_y; + did_scroll = dt_gui_get_scroll_deltas(e, &deltaf_x, &deltaf_y); if(did_scroll) { - // file manager scroll: tilt right (dx > 0) or scroll down (dy > 0) -> down - deltaf = fabs(dx) > fabs(dy) ? dx : dy; + // file manager scroll: tilt right (delta_x > 0) or scroll down (delta_y > 0) -> down + deltaf = fabs(deltaf_x) > fabs(deltaf_y) ? deltaf_x : deltaf_y; } } else @@ -1391,11 +1397,10 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, return; } - table->mouse_inside = FALSE; - - /* Don't clear the mouse-over when leaving to a child widget (thumbnail), - * or while the pointer is grabbed: the shortcut machinery's synthetic - * crossings must not lose the hovered image (see #21729). + /* Don't clear the mouse-over nor the inside-table state when leaving to a + * child widget (thumbnail), or while the pointer is grabbed: the shortcut + * machinery's synthetic crossings must not lose the hovered image nor + * mouse_inside (see #21729, #21745). * GTK4 migration: drop the pointer-grab check (see * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ GdkEvent *event = gtk_get_current_event(); @@ -1405,7 +1410,10 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, && event->crossing.mode != GDK_CROSSING_GTK_GRAB && event->crossing.mode != GDK_CROSSING_GRAB && !dt_gui_pointer_is_grabbed()) + { + table->mouse_inside = FALSE; dt_control_set_mouse_over_id(NO_IMGID); + } gdk_event_free(event); } } @@ -1417,6 +1425,8 @@ static void _event_enter_cb(GtkEventControllerMotion *controller, { dt_set_backthumb_time(0.0); + table->mouse_inside = TRUE; + /* The pointer entered the thumbtable area (from a thumbnail, a panel, or * after a redraw of the table). Clear the mouse-over only if the pointer * landed on the table itself; if it landed on a thumbnail, the thumbnail's @@ -1457,20 +1467,7 @@ static void _event_button_press_cb(GtkGestureSingle *gesture, { dt_set_backthumb_time(0.0); - /* - * GTK3 bridge for GDK_2BUTTON_PRESS (see dt_gui_connect_double_click): - * GtkGestureMultiPress doesn't process multi-press events, so when a - * double/triple-click is detected by the classic "button-press-event" - * signal handler, it calls this callback with gesture=NULL. In that - * case the button is always GDK_BUTTON_PRIMARY (GDK_2BUTTON_PRESS only - * occurs for primary button). - * - * GTK4 migration: remove the ternary and just call - * gtk_gesture_single_get_current_button(gesture) directly — GtkGestureClick - * will provide a valid gesture pointer for all n_press values. */ - const guint button = gesture - ? gtk_gesture_single_get_current_button(gesture) - : GDK_BUTTON_PRIMARY; + const guint button = gtk_gesture_single_get_current_button(gesture); const dt_imgid_t id = dt_control_get_mouse_over_id(); if(button == GDK_BUTTON_PRIMARY && n_press == 2) @@ -1581,9 +1578,8 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, dt_control_set_mouse_over_id(NO_IMGID); // get root coordinates for drag tracking - const GdkEvent *current = gtk_get_current_event(); gdouble root_x = 0, root_y = 0; - if(current) gdk_event_get_root_coords(current, &root_x, &root_y); + dt_gui_get_current_root_coords(&root_x, &root_y); if(table->dragging && table->mode == DT_THUMBTABLE_MODE_ZOOM) { @@ -1626,7 +1622,8 @@ static void _event_button_release_cb(GtkGestureSingle *gesture, GdkModifierType state; gtk_get_current_event_state(&state); - if(dt_is_valid_imgid(id)) + if(dt_is_valid_imgid(id) + && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) { /* the keyboard cursor continues from the image we clicked on */ table->key_pos = id; @@ -2663,22 +2660,12 @@ dt_thumbtable_t *dt_thumbtable_new() g_signal_connect(table->widget, "drag-data-received", G_CALLBACK(dt_thumbtable_event_dnd_received), table); - dt_gui_connect_scroll(table->widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES - | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, + dt_gui_connect_scroll(table->widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, _event_scroll, table); g_signal_connect(G_OBJECT(table->widget), "draw", G_CALLBACK(_event_draw), table); dt_gui_connect_motion(table->widget, _event_motion_notify_cb, _event_enter_cb, _event_leave_cb, table); dt_gui_connect_click_all(table->widget, _event_button_press_cb, _event_button_release_cb, table); - /* GTK3 bridge: GtkGestureMultiPress does not process GDK_2BUTTON_PRESS. - * dt_gui_connect_double_click forwards double/triple clicks via a - * "button-press-event" signal handler. The callback checks for NULL - * 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_current_button() - * safely on the real gesture pointer. */ - dt_gui_connect_double_click(table->widget, _event_button_press_cb, table); // we register globals signals DT_CONTROL_SIGNAL_CONNECT(DT_SIGNAL_COLLECTION_CHANGED, diff --git a/src/dtgtk/togglebutton.c b/src/dtgtk/togglebutton.c index 5a978d2b307..f28d01ee387 100644 --- a/src/dtgtk/togglebutton.c +++ b/src/dtgtk/togglebutton.c @@ -142,6 +142,7 @@ GtkWidget *dtgtk_togglebutton_new(DTGTKCairoPaintIconFunc paint, gint paintflags dt_gui_add_class(GTK_WIDGET(button), "dt_module_btn"); gtk_widget_set_name(GTK_WIDGET(button->canvas), "button-canvas"); g_signal_connect(G_OBJECT(button), "toggled", G_CALLBACK(gtk_widget_queue_draw), NULL); + dtgtk_button_connect_stale_hover_cleanup(GTK_WIDGET(button)); return (GtkWidget *)button; } diff --git a/src/gui/accelerators.c b/src/gui/accelerators.c index 734a503cd85..fc9daefc7e3 100644 --- a/src/gui/accelerators.c +++ b/src/gui/accelerators.c @@ -209,6 +209,40 @@ const dt_action_element_def_t _action_elements_entry[] const dt_action_element_def_t _action_elements_value_fallback[] = { { NULL, dt_action_effect_value } }; +/* encode an action effect as (state << 8) | button for the synthetic gesture + * press used by shortcut activation (decoded by dt_gui_current_button() / + * dt_gui_current_state() in gtk.h); plain effects become a plain primary + * click. + * + * Effect values alias across action definitions (DT_ACTION_EFFECT_ON == + * DT_ACTION_EFFECT_ACTIVATE_CTRL == 1 and DT_ACTION_EFFECT_OFF == + * DT_ACTION_EFFECT_ACTIVATE_RIGHT == 2, see action.h), so the caller's + * definition selects the meaning: a toggle's plain on/off is a plain + * primary click (matching the synthetic branch below), while a button's + * ctrl/right variants carry the modifier / button. A correction confined + * to this switch could not serve both callers. */ +static inline gint _action_effect_button_state(const dt_action_effect_t effect, + const gboolean is_toggle) +{ + switch(effect) + { + case DT_ACTION_EFFECT_TOGGLE_CTRL: + case DT_ACTION_EFFECT_ON_CTRL: + return (GDK_CONTROL_MASK << 8) | GDK_BUTTON_PRIMARY; + case DT_ACTION_EFFECT_TOGGLE_RIGHT: + case DT_ACTION_EFFECT_ON_RIGHT: + return (0 << 8) | GDK_BUTTON_SECONDARY; + case DT_ACTION_EFFECT_ACTIVATE_CTRL: /* == DT_ACTION_EFFECT_ON */ + return is_toggle ? (0 << 8) | GDK_BUTTON_PRIMARY + : (GDK_CONTROL_MASK << 8) | GDK_BUTTON_PRIMARY; + case DT_ACTION_EFFECT_ACTIVATE_RIGHT: /* == DT_ACTION_EFFECT_OFF */ + return is_toggle ? (0 << 8) | GDK_BUTTON_PRIMARY + : (0 << 8) | GDK_BUTTON_SECONDARY; + default: + return (0 << 8) | GDK_BUTTON_PRIMARY; + } +} + static float _action_process_toggle(gpointer target, dt_action_element_t element, dt_action_effect_t effect, @@ -219,27 +253,44 @@ static float _action_process_toggle(gpointer target, if(DT_ACTION_TOGGLE_NEEDED(effect, move_size, value) && gtk_widget_get_ancestor(target, GTK_TYPE_WINDOW)) { - GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); - event->button.state = (effect == DT_ACTION_EFFECT_TOGGLE_CTRL - || effect == DT_ACTION_EFFECT_ON_CTRL) - ? GDK_CONTROL_MASK : 0; - event->button.button = (effect == DT_ACTION_EFFECT_TOGGLE_RIGHT - || effect == DT_ACTION_EFFECT_ON_RIGHT) - ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY; + GtkGestureSingle *const gesture = g_object_get_data(G_OBJECT(target), DT_ACTION_GESTURE_KEY); + if(gesture) + { + /* the widget's activation lives in a gesture controller, which + * synthetic GdkEvents never reach: invoke it through the same + * "pressed" signal a real click produces, carrying the action + * effect as button/state so ctrl-/right-variants keep working + * (the callback manages the button states itself, as for a real + * click) */ + g_object_set_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY, + GINT_TO_POINTER(_action_effect_button_state(effect, TRUE))); + g_signal_emit_by_name(gesture, "pressed", 1, 0.0, 0.0); + g_object_set_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY, NULL); + } + else + { + GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); + event->button.state = (effect == DT_ACTION_EFFECT_TOGGLE_CTRL + || effect == DT_ACTION_EFFECT_ON_CTRL) + ? GDK_CONTROL_MASK : 0; + event->button.button = (effect == DT_ACTION_EFFECT_TOGGLE_RIGHT + || effect == DT_ACTION_EFFECT_ON_RIGHT) + ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY; - if(!gtk_widget_get_realized(target)) gtk_widget_realize(target); - event->button.window = gtk_widget_get_window(target); - g_object_ref(event->button.window); + if(!gtk_widget_get_realized(target)) gtk_widget_realize(target); + event->button.window = gtk_widget_get_window(target); + g_object_ref(event->button.window); - // some togglebuttons connect to the clicked signal, others to toggled or button-press-event - // gtk_widget_event does not work when widgets are hidden in event boxes or some other conditions - gboolean handled; - g_signal_emit_by_name(G_OBJECT(target), "button-press-event", event, &handled); - if(!handled) gtk_button_clicked(GTK_BUTTON(target)); - event->type = GDK_BUTTON_RELEASE; - g_signal_emit_by_name(G_OBJECT(target), "button-release-event", event, &handled); + // some togglebuttons connect to the clicked signal, others to toggled or button-press-event + // gtk_widget_event does not work when widgets are hidden in event boxes or some other conditions + gboolean handled; + g_signal_emit_by_name(G_OBJECT(target), "button-press-event", event, &handled); + if(!handled) gtk_button_clicked(GTK_BUTTON(target)); + event->type = GDK_BUTTON_RELEASE; + g_signal_emit_by_name(G_OBJECT(target), "button-release-event", event, &handled); - gdk_event_free(event); + gdk_event_free(event); + } value = gtk_toggle_button_get_active(target); @@ -261,28 +312,42 @@ static float _action_process_button(gpointer target, && gtk_widget_is_sensitive(target) && gtk_widget_get_ancestor(target, GTK_TYPE_WINDOW)) { - if(!gtk_widget_get_realized(target)) gtk_widget_realize(target); - - if(effect != DT_ACTION_EFFECT_ACTIVATE - || !g_signal_handler_find(target, G_SIGNAL_MATCH_ID, - g_signal_lookup("clicked", gtk_button_get_type()), - 0, NULL, NULL, NULL) - || !gtk_widget_activate(GTK_WIDGET(target))) + GtkGestureSingle *const gesture = g_object_get_data(G_OBJECT(target), DT_ACTION_GESTURE_KEY); + if(gesture) { - GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); - event->button.state = effect == DT_ACTION_EFFECT_ACTIVATE_CTRL - ? GDK_CONTROL_MASK : 0; - event->button.button = effect == DT_ACTION_EFFECT_ACTIVATE_RIGHT - ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY; + /* same as _action_process_toggle: the widget's activation lives in a + * gesture controller, which synthetic GdkEvents never reach; carry the + * action effect as button/state (see _action_effect_button_state) */ + g_object_set_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY, + GINT_TO_POINTER(_action_effect_button_state(effect, FALSE))); + g_signal_emit_by_name(gesture, "pressed", 1, 0.0, 0.0); + g_object_set_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY, NULL); + } + else + { + if(!gtk_widget_get_realized(target)) gtk_widget_realize(target); - event->button.window = gtk_widget_get_window(target); - g_object_ref(event->button.window); + if(effect != DT_ACTION_EFFECT_ACTIVATE + || !g_signal_handler_find(target, G_SIGNAL_MATCH_ID, + g_signal_lookup("clicked", gtk_button_get_type()), + 0, NULL, NULL, NULL) + || !gtk_widget_activate(GTK_WIDGET(target))) + { + GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); + event->button.state = effect == DT_ACTION_EFFECT_ACTIVATE_CTRL + ? GDK_CONTROL_MASK : 0; + event->button.button = effect == DT_ACTION_EFFECT_ACTIVATE_RIGHT + ? GDK_BUTTON_SECONDARY : GDK_BUTTON_PRIMARY; - gtk_widget_event(target, event); - event->type = GDK_BUTTON_RELEASE; - gtk_widget_event(target, event); + event->button.window = gtk_widget_get_window(target); + g_object_ref(event->button.window); - gdk_event_free(event); + gtk_widget_event(target, event); + event->type = GDK_BUTTON_RELEASE; + gtk_widget_event(target, event); + + gdk_event_free(event); + } } } @@ -2066,7 +2131,11 @@ static gboolean _view_key_pressed_cb(GtkEventControllerKey *controller, } } - return dt_gui_search_start(widget, (GdkEventKey *)gtk_get_current_event(), GTK_SEARCH_ENTRY(search_entry)); + GdkEvent *event = gtk_get_current_event(); + const gboolean handled = + dt_gui_search_start(widget, (GdkEventKey *)event, GTK_SEARCH_ENTRY(search_entry)); + gdk_event_free(event); + return handled; } static void _add_shortcuts_to_tree() @@ -3653,6 +3722,12 @@ static gboolean _pointer_grabbed = FALSE; // seat grab held by the shortcut mach // report it, see dt_gui_pointer_is_grabbed) #endif static guint _last_time = 0; // time of key or button press + +/* a hold key's delayed release is pending (see dt_shortcut_key_release): + * the hold action was already dispatched (ON at engage, OFF at release), + * so the delayed release must not re-run the shortcut pass, it only has + * to clear _sc once the double/triple-press window elapses */ +static gboolean _hold_release_pending = FALSE; // used to determine if release should trigger action // set to 0 by any intermediate move (so no action on release) static guint _last_mapping_time = 0; @@ -4256,6 +4331,16 @@ static inline void _interrupt_delayed_release(gboolean trigger) g_source_remove(_timeout_source); _timeout_source = 0; + if(_hold_release_pending) + { + /* the cancelled delayed release belonged to a hold key whose action + * was already dispatched: drop the stale key so the reentrant pass + * below (and any later shortcut lookup) cannot re-dispatch it */ + _hold_release_pending = FALSE; + _sc.key_device = 0; + _sc.key = 0; + } + if(trigger) dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1); @@ -4379,17 +4464,35 @@ static gboolean _key_release_delayed(gpointer timed_out) * events between the ungrab and the action, which clear the * mouse-over id and make 'prioritize hovered image' fall back to the * selection (see #21729). This also matches the double/triple-press - * path, which processes the action while the grab is held. */ - if(!timed_out) + * path, which processes the action while the grab is held. + * + * A released hold key's action was already dispatched by the hold + * machinery (DT_ACTION_EFFECT_ON at engage, OFF at release), so for it + * this pass only has to clear _sc, not re-run the shortcut. */ + const gboolean hold_release = _hold_release_pending; + _hold_release_pending = FALSE; + + if(!timed_out && !hold_release) dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, 0, DT_SHORTCUT_MOVE_NONE, 1); if(!_pressed_keys) _ungrab_grab_widget(); - if(!_pressed_keys) + if(!_pressed_keys && !_hold_keys) _sc = (dt_shortcut_t) { 0 }; else + { + if(!_pressed_keys) + { + /* another hold key is still engaged: keep its identity in _sc, or + * move/scroll routing stops matching while it is held (see + * _shortcut_closest_match, which requires c->key == s->key) */ + const dt_device_key_t *held = _hold_keys->data; + _sc.key_device = held->key_device; + _sc.key = held->key; + } _sc.press &= ~DT_SHORTCUT_LONG; + } return G_SOURCE_REMOVE; } @@ -4407,6 +4510,27 @@ static gboolean _button_release_delayed(gpointer timed_out) return G_SOURCE_REMOVE; } +// returns TRUE if a double or triple press shortcut is registered for this key +static gboolean _shortcut_has_double_triple_press(const dt_input_device_t id, + const guint key, + const guint mods) +{ + dt_shortcut_t s = { .key_device = id, .key = key, .mods = mods, + .press = DT_SHORTCUT_DOUBLE, + .views = dt_view_get_current() }; + + GSequenceIter *multi = _shortcut_search(&s, GINT_TO_POINTER(s.views)); + for(int checks = 2; checks--; multi = g_sequence_iter_prev(multi)) + { + if(g_sequence_iter_is_end(multi)) continue; + + const dt_shortcut_t *m = g_sequence_get(multi); + if(m->key_device == id && m->key == key && m->press >= DT_SHORTCUT_DOUBLE) + return TRUE; + } + return FALSE; +} + void dt_shortcut_key_press(const dt_input_device_t id, const guint time, const guint key) @@ -4463,16 +4587,50 @@ void dt_shortcut_key_press(const dt_input_device_t id, _shortcut_description(s), _action_description(s, 2)); } - definition->process(NULL, s->element, DT_ACTION_EFFECT_ON, 1); - - this_key.hold_def = definition; - this_key.hold_element = s->element; - - dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t)); - *new_key = this_key; - _hold_keys = g_slist_prepend(_hold_keys, new_key); - - return; + // a hold action must not swallow a fast consecutive press of the same + // key: if the key was held and released within the double-click time + // window and a double/triple press shortcut exists for it, fall + // through to the normal handling below which turns this press into a + // double/triple press instead of re-engaging the hold + if(key == _sc.key + && _sc.key_device == id + && !dt_gui_long_click(time, _last_time) + && _shortcut_has_double_triple_press(id, key, _sc.mods)) + { + /* fall through to the double/triple press detection below */ + } + else + { + /* a still-armed delayed release (a previous tap or click whose + * double/triple-press window has not elapsed) must be flushed + * against the state that armed it before the hold takes over + * _sc: the pending pass requires c->key == s->key, so left armed + * it would resolve against the hold key and re-run the hold + * action, silently disengaging the hold while the key is still + * held. A same-key re-engage inside the window is that pending + * pass's own double-press check: just cancel it -- the hold + * action already ran OFF at the previous release. */ + const gboolean same_key_reengage + = key == _sc.key && _sc.key_device == id && !dt_gui_long_click(time, _last_time); + _interrupt_delayed_release(!same_key_reengage); + + definition->process(NULL, s->element, DT_ACTION_EFFECT_ON, 1); + + this_key.hold_def = definition; + this_key.hold_element = s->element; + + dt_device_key_t *new_key = calloc(1, sizeof(dt_device_key_t)); + *new_key = this_key; + _hold_keys = g_slist_prepend(_hold_keys, new_key); + + // remember the press so a fast consecutive press of this key + // (after release) can be detected as a double/triple press below + _last_time = time; + _sc.key_device = id; + _sc.key = key; + + return; + } } } @@ -4563,6 +4721,16 @@ static void _delay_for_double_triple(guint time, guint is_key) { _ungrab_grab_widget(); dt_control_log(_("short key press resets stuck keys")); + /* this early return skips the _key_release_delayed() cleanup a hold + * release scheduled: drop its flag and the stale hold key now, or the + * next ordinary key release would silently skip its own dispatch and + * every later move/scroll would resolve as "that key + move" */ + if(_hold_release_pending) + { + _hold_release_pending = FALSE; + _sc.key_device = 0; + _sc.key = 0; + } return; } else if((is_key ? _sc.press : _sc.click) & DT_SHORTCUT_TRIPLE) @@ -4624,6 +4792,26 @@ void dt_shortcut_key_release(const dt_input_device_t id, held_data->hold_def->process(NULL, held_data->hold_element, DT_ACTION_EFFECT_OFF, 1); g_free(held_data); _hold_keys = g_slist_delete_link(_hold_keys, held_key); + + /* schedule the same delayed cleanup a normal key release gets: the + * double/triple-press window needs _sc.key to stay valid for a fast + * consecutive press of this key, but once the window elapses _sc must + * be cleared -- otherwise the stale hold key makes every later + * move/scroll shortcut resolve as "that key + move" (see + * _shortcut_closest_match, which requires c->key == s->key) and wheel + * scrolling stops working. The HOLD_OFF dispatch above replaces the + * shortcut pass the delayed release would otherwise run, so flag it. + * Also cancel a still-armed delayed release before scheduling the + * cleanup, mirroring the ordinary key release below: otherwise the + * g_timeout_add() in _delay_for_double_triple() overwrites the live + * source handle and the orphaned pass later runs an extra cleanup. + * Never trigger it here (TRUE): it would resolve against the hold key + * still in _sc and re-run the hold action; the OFF dispatch above + * already replaced that pass. */ + _interrupt_delayed_release(FALSE); + + _hold_release_pending = TRUE; + _delay_for_double_triple(time, -1); return; } diff --git a/src/gui/accelerators.h b/src/gui/accelerators.h index 58d8a4a271b..78f43c5045b 100644 --- a/src/gui/accelerators.h +++ b/src/gui/accelerators.h @@ -225,6 +225,20 @@ void dt_action_cleanup_instance_iop(dt_iop_module_t *module); // UX miscellaneous functions void dt_action_widget_toast(dt_action_t *action, GtkWidget *widget, const gchar *msg, ...); +/* Widgets whose activation lives in a gesture controller (a GtkGesture* + * "pressed"/"released" handler instead of widget class vfuncs or "clicked"/ + * "toggled" signals) register their gesture here so the shortcut machinery + * can invoke the same code path as a real click: synthetic GdkEvents never + * reach gesture controllers, so without this a shortcut would flip the + * widget's visual state without activating it. See _action_process_toggle + * and _action_process_button in accelerators.c. + * + * GTK4: this stored-gesture path survives the switch -- "button-press-event" + * does not exist in GTK4 and GdkEvent is opaque + * (https://docs.gtk.org/gtk4/migrating-3to4.html, "Event controllers and + * gestures replace event signals"). */ +#define DT_ACTION_GESTURE_KEY "_dt_action_gesture" + // check if widget intentionally hidden (to disable it) gboolean dt_action_widget_invisible(GtkWidget *w); diff --git a/src/gui/color_picker_proxy.c b/src/gui/color_picker_proxy.c index 831f5d8c52e..11bfa2365c1 100644 --- a/src/gui/color_picker_proxy.c +++ b/src/gui/color_picker_proxy.c @@ -22,6 +22,7 @@ #include "libs/lib.h" #include "control/control.h" #include "gui/gtk.h" +#include "gui/accelerators.h" #include "develop/blend.h" /* @@ -155,7 +156,8 @@ static void _init_picker(dt_iop_color_picker_t *picker, } static gboolean _color_picker_callback_button_press(GtkWidget *button, - GdkEventButton *e, + const gboolean ctrl, + const gboolean right, dt_iop_color_picker_t *self) { // module is NULL if primary colorpicker @@ -174,9 +176,7 @@ static gboolean _color_picker_callback_button_press(GtkWidget *button, if(module && module->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(module->off), TRUE); - const GdkModifierType state = e != NULL ? e->state : dt_key_modifier_state(); - const gboolean to_area_mode = - dt_modifier_is(state, GDK_CONTROL_MASK) || (e != NULL && e->button == GDK_BUTTON_SECONDARY); + const gboolean to_area_mode = ctrl || right; dt_iop_color_picker_flags_t flags = self->flags; // setup if a new picker or switching between point/area mode @@ -259,7 +259,10 @@ static gboolean _color_picker_callback_button_press(GtkWidget *button, static void _color_picker_callback(GtkWidget *button, dt_iop_color_picker_t *self) { - _color_picker_callback_button_press(button, NULL, self); + _color_picker_callback_button_press(button, + dt_modifier_is(dt_key_modifier_state(), GDK_CONTROL_MASK), + FALSE, + self); } static void _color_picker_clicked(GtkGestureSingle *gesture, @@ -269,9 +272,102 @@ static void _color_picker_clicked(GtkGestureSingle *gesture, dt_iop_color_picker_t *self) { GtkWidget *button = dt_gui_get_widget(gesture); - _color_picker_callback_button_press(button, NULL, self); + // pass the clicked button through to the callback: a secondary click + // switches the picker to area mode (as before the gtk4-prep migration) + _color_picker_callback_button_press(button, + dt_modifier_is(dt_key_modifier_state(), GDK_CONTROL_MASK), + gtk_gesture_single_get_current_button(gesture) + == GDK_BUTTON_SECONDARY, + self); } +/* + * Claim the event sequence in CAPTURE phase so the toggle button's own + * internal GtkGestureMultiPress (GTK_PHASE_BUBBLE, which emits "clicked" + * and toggles the button on release) never processes the event; the + * picker callback fully controls the button state instead. Same pattern + * as dt_iop_togglebutton_new. + * + * GTK4 migration: the pattern is the same, just rename + * GtkGestureMultiPress to GtkGestureClick. */ +static void _gesture_begin_claim(GtkGesture *gesture, + GdkEventSequence *sequence, + gpointer user_data) +{ + gtk_gesture_set_sequence_state(gesture, sequence, GTK_EVENT_SEQUENCE_CLAIMED); +} + +/* + * Shared activation entry for standalone picker toggle buttons (created + * with a NULL container, e.g. AgX "auto tune levels"). Real clicks go + * through the CAPTURE-phase gesture above, shortcuts through the action + * definition below (dt_action_def_color_picker); both end up here, + * mirroring how bauhaus pickers route clicks and shortcuts through + * dt_bauhaus_widget_press_quad(). + */ +static float _color_picker_widget_toggle(GtkWidget *target, + const dt_action_effect_t effect, + const float move_size) +{ + if(!DT_PERFORM_ACTION(move_size) || !target) + return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(target)); + + dt_iop_color_picker_t *self = g_object_get_data(G_OBJECT(target), DT_COLOR_PICKER_INSTANCE_KEY); + if(!self) return 0.f; + + if(!DT_ACTION_TOGGLE_NEEDED(effect, move_size, gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(target)))) + return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(target)); + + _color_picker_callback_button_press(target, + effect == DT_ACTION_EFFECT_TOGGLE_CTRL + || effect == DT_ACTION_EFFECT_ON_CTRL, + effect == DT_ACTION_EFFECT_TOGGLE_RIGHT + || effect == DT_ACTION_EFFECT_ON_RIGHT, + self); + + const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(target)); + if(!gtk_widget_is_visible(target)) + dt_action_widget_toast(NULL, target, active ? _("on") : _("off")); + + return active; +} + +/* + * Action definition for standalone picker toggle buttons. + * + * The generic toggle definition (dt_action_def_toggle in accelerators.c) + * activates widgets by synthesizing GObject "button-press-event" signals. + * Those only reach the widget class handler (which dispatches + * BUBBLE-phase controllers) and carry no device, so they can never + * trigger the CAPTURE-phase gesture that picker buttons need in order to + * suppress the button's own internal gesture -- which is why picker + * shortcuts stopped working after the gtk4-prep migration. Like bauhaus + * widgets, picker buttons therefore use their own definition whose + * process calls the same shared entry as real clicks. + */ +static const dt_action_element_def_t _color_picker_elements[] + = { { NULL, dt_action_effect_toggle } }; + +static const dt_shortcut_fallback_t _color_picker_fallbacks[] + = { { .mods = GDK_CONTROL_MASK , .effect = DT_ACTION_EFFECT_TOGGLE_CTRL }, + { .button = DT_SHORTCUT_RIGHT , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT }, + { .press = DT_SHORTCUT_LONG , .effect = DT_ACTION_EFFECT_TOGGLE_RIGHT }, + { } }; + +static float _color_picker_process(gpointer target, + const dt_action_element_t element, + const dt_action_effect_t effect, + const float move_size) +{ + return _color_picker_widget_toggle(target, effect, move_size); +} + +const dt_action_def_t dt_action_def_color_picker + = { N_("toggle"), + _color_picker_process, + _color_picker_elements, + _color_picker_fallbacks }; + void dt_iop_color_picker_set_cst(dt_iop_module_t *module, const dt_iop_colorspace_type_t picker_cst) { @@ -390,6 +486,10 @@ static void _color_picker_destroy(dt_iop_color_picker_t *picker) // before freeing the struct to prevent use-after-free in dt_iop_color_picker_reset. if(darktable.lib && darktable.lib->proxy.colorpicker.picker_proxy == picker) darktable.lib->proxy.colorpicker.picker_proxy = NULL; + if(picker->colorpick) + { + g_object_set_data(G_OBJECT(picker->colorpick), DT_COLOR_PICKER_INSTANCE_KEY, NULL); + } g_free(picker); } @@ -411,7 +511,28 @@ static GtkWidget *_color_picker_new(dt_iop_module_t *module, color_picker->picker_cst = cst; color_picker->fixed_cst = TRUE; } - dt_gui_connect_click(button, _color_picker_clicked, NULL, color_picker); + // The button is a GtkToggleButton, which owns its own + // GtkGestureMultiPress (bubble phase) that emits "clicked" and toggles + // the button on release. Use a CAPTURE-phase gesture that claims the + // event sequence (same pattern as dt_iop_togglebutton_new) so that + // internal gesture never runs and the picker callback fully controls + // the button state on real clicks. + // + // Shortcuts (dt_action_def_color_picker, see above) call + // _color_picker_widget_toggle() directly instead of synthesizing + // GObject "button-press-event" signals, which cannot reach this + // CAPTURE-phase gesture (the widget class handler only dispatches + // BUBBLE-phase controllers and synthetic events carry no device). + GtkGesture *gesture = gtk_gesture_multi_press_new(button); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture), + GTK_PHASE_CAPTURE); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); + dt_gui_add_controller(button, gesture); + g_signal_connect_data(gesture, "pressed", + G_CALLBACK(_color_picker_clicked), + color_picker, (GClosureNotify)_color_picker_destroy, 0); + g_signal_connect(gesture, "begin", G_CALLBACK(_gesture_begin_claim), NULL); + g_object_set_data(G_OBJECT(button), DT_COLOR_PICKER_INSTANCE_KEY, color_picker); if(w) gtk_box_pack_start(GTK_BOX(w), button, FALSE, FALSE, 0); return button; diff --git a/src/gui/color_picker_proxy.h b/src/gui/color_picker_proxy.h index 59cf7182546..d01a5c2faa0 100644 --- a/src/gui/color_picker_proxy.h +++ b/src/gui/color_picker_proxy.h @@ -72,6 +72,12 @@ typedef struct dt_iop_color_picker_t gboolean dt_iop_color_picker_is_visible(const dt_develop_t *dev); +/* g_object data key on the picker widget: the owning dt_iop_color_picker_t */ +#define DT_COLOR_PICKER_INSTANCE_KEY "dt-color-picker-instance" + +/* action definition for standalone picker toggle buttons (see color_picker_proxy.c) */ +extern const struct dt_action_def_t dt_action_def_color_picker; + //* reset current color picker if not keep-active or not keep */ void dt_iop_color_picker_reset(dt_iop_module_t *module, const gboolean keep); diff --git a/src/gui/gtk.c b/src/gui/gtk.c index 303e28fc7ff..475e40b45b9 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -656,36 +656,6 @@ gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, return FALSE; } -gboolean dt_gui_get_scroll_unit_deltas_fallback(const gdouble dx, - const gdouble dy, - int *delta_x, - int *delta_y) -{ - GdkEvent *event = gtk_get_current_event(); - if(event) - { - const gboolean ok = dt_gui_get_scroll_unit_deltas((const GdkEventScroll *)event, delta_x, delta_y); - gdk_event_free(event); - return ok; - } - // fallback: use raw values if no current event available - *delta_x = (int)dx; - *delta_y = (int)dy; - return TRUE; -} - -gboolean dt_gui_get_scroll_unit_delta_fallback(const gdouble dy, - int *delta) -{ - int delta_x, delta_y; - if(dt_gui_get_scroll_unit_deltas_fallback(0, dy, &delta_x, &delta_y)) - { - *delta = abs(delta_x) > abs(delta_y) ? -delta_x : delta_y; - return TRUE; - } - return FALSE; -} - static gboolean _draw_borders(GtkWidget *widget, cairo_t *crf, const gpointer user_data) @@ -4818,6 +4788,28 @@ void dt_gui_add_controller(GtkWidget *widget, #endif } +/* + * Root (screen-absolute) coordinates of the current event, or FALSE if there + * is no current event. gtk_get_current_event() returns an owned copy on + * GTK3 (transfer full), so the copy is released here -- the whole + * get-event/use/free dance is confined to this one helper. + * + * GTK4 migration: gtk_get_current_event() disappears; gesture/controller + * callbacks get the event from gtk_gesture_get_last_event() or + * gtk_event_controller_get_current_event() (borrowed, no free). Callers + * of this helper should then switch to those and read the coordinates + * directly, or drop the helper and use + * gtk_gesture_get_last_event(gesture, NULL). + */ +gboolean dt_gui_get_current_root_coords(gdouble *x, gdouble *y) +{ + GdkEvent *event = gtk_get_current_event(); + if(!event) return FALSE; + const gboolean ok = gdk_event_get_root_coords(event, x, y); + gdk_event_free(event); + return ok; +} + static void _gesture_cancel(GtkGestureSingle *gesture, GdkEventSequence *sequence, GtkWidget *widget) @@ -4851,70 +4843,6 @@ GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, return (GtkGestureSingle *)gesture; } -/* - * GTK3 bridge for double/triple-click detection. - * - * GtkGestureMultiPress in GTK3 does not process GDK_2BUTTON_PRESS or - * GDK_3BUTTON_PRESS events at all: - * - gtk_gesture_handle_event() only dispatches GDK_BUTTON_PRESS (not 2/3) - * - gtk_gesture_multi_press_begin() checks for GDK_BUTTON_PRESS only - * - GDK always delivers GDK_2BUTTON_PRESS for the second press of a - * double-click (never a second GDK_BUTTON_PRESS) - * - * As a result, GtkGestureMultiPress's "pressed" signal with n_press=2 or 3 - * is NEVER emitted in GTK3. This helper works around it by connecting a - * classic "button-press-event" signal handler that catches GDK_2BUTTON_PRESS - * and GDK_3BUTTON_PRESS and forwards them to the same pressed callback with - * the correct n_press value. - * - * GTK4 migration: DELETE this entire helper (struct, free, handler, and both - * public functions). GtkGestureClick (the GTK4 replacement for - * GtkGestureMultiPress) uses an internal timer to detect multi-press patterns - * and correctly emits "pressed" with n_press=2/3 without needing GDK_2BUTTON_PRESS. - * The callers using dt_gui_connect_double_click() should simply remove those - * calls — the gesture alone will suffice. - */ -typedef struct _DblClkData -{ - GCallback cb; - gpointer data; -} DblClkData; - -static void _dbl_clk_free(gpointer p, GClosure *cl) -{ - g_free(p); -} - -static gboolean _dbl_clk_handler(GtkWidget *w, GdkEventButton *ev, gpointer user) -{ - DblClkData *d = user; - int n; - if(ev->type == GDK_2BUTTON_PRESS) n = 2; - else if(ev->type == GDK_3BUTTON_PRESS) n = 3; - else return GDK_EVENT_PROPAGATE; - ((void (*)(GtkGestureSingle *, int, double, double, gpointer))d->cb) - (NULL, n, ev->x, ev->y, d->data); - return GDK_EVENT_STOP; -} - -unsigned long -(dt_gui_connect_double_click)(GtkWidget *widget, - GCallback pressed, - gpointer data) -{ - DblClkData *d = g_malloc(sizeof(DblClkData)); - d->cb = pressed; - d->data = data; - return g_signal_connect_data(widget, "button-press-event", - G_CALLBACK(_dbl_clk_handler), - d, _dbl_clk_free, 0); -} - -void (dt_gui_disconnect_double_click)(GtkWidget *widget, unsigned long id) -{ - g_signal_handler_disconnect(widget, id); -} - GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, GCallback drag_begin, GCallback drag_end, @@ -5083,7 +5011,12 @@ GtkEventController *(dt_gui_connect_scroll)(GtkWidget *widget, flags &= ~GTK_EVENT_CONTROLLER_SCROLL_DISCRETE; GtkEventController *const controller = gtk_event_controller_scroll_new(widget, flags); - gtk_event_controller_set_propagation_phase(controller, GTK_PHASE_TARGET); + /* BUBBLE phase matches the bubbling behavior of the replaced + * "scroll-event" signal: the controller fires whenever the event target + * is the widget or any of its descendants (e.g. child widgets such as + * thumbnails or star icons placed on a GtkLayout). GTK_PHASE_TARGET + * would only fire when the widget is the target itself. */ + gtk_event_controller_set_propagation_phase(controller, GTK_PHASE_BUBBLE); dt_gui_add_controller(widget, controller); // GTK4 gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(controller)); g_signal_connect(controller, "scroll", G_CALLBACK(proxy), data); diff --git a/src/gui/gtk.h b/src/gui/gtk.h index a1bc24bae4b..50602a946cd 100644 --- a/src/gui/gtk.h +++ b/src/gui/gtk.h @@ -253,11 +253,6 @@ gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, gdouble *delta); * Effectively makes smooth scroll events act like old-style unit * scroll events. */ gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, int *delta); -/* Same as above but for use inside scroll-event-controller callbacks where the - * raw dx/dy are available but no GdkEventScroll *. Tries gtk_get_current_event() - * first for smooth-scroll accumulation, falls back to casting the raw values. */ -gboolean dt_gui_get_scroll_unit_deltas_fallback(gdouble dx, gdouble dy, int *delta_x, int *delta_y); -gboolean dt_gui_get_scroll_unit_delta_fallback(gdouble dy, int *delta); /* * new ui api @@ -571,6 +566,17 @@ gboolean dt_gui_long_click(const guint second, void dt_gui_add_controller(GtkWidget *widget, gpointer controller); +/* + * Root (screen-absolute) coordinates of the current event, or FALSE when + * there is no current event. Owns and releases the gtk_get_current_event() + * copy internally (GTK3 returns transfer-full). + * + * GTK4 migration: gtk_get_current_event() disappears; use + * gtk_gesture_get_last_event()/gtk_event_controller_get_current_event() + * (borrowed) and drop this helper. + */ +gboolean dt_gui_get_current_root_coords(gdouble *x, gdouble *y); + GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, GCallback pressed, GCallback released, @@ -579,29 +585,11 @@ GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, ASSERT_FUNC_TYPE(pressed, void(*)(GtkGestureSingle *, int, double, double, __typeof__(data))), \ ASSERT_FUNC_TYPE(released, void(*)(GtkGestureSingle *, int, double, double, __typeof__(data))), \ dt_gui_connect_click(GTK_WIDGET(widget), G_CALLBACK(pressed), G_CALLBACK(released), (data))) +/* dt_gui_connect_click() already listens to any button (button=0): callbacks + * decide via gtk_gesture_single_get_current_button(), as the old + * button-press-event handlers did. This alias only documents that intent. */ #define dt_gui_connect_click_all(widget, pressed, released, data) \ - gtk_gesture_single_set_button(dt_gui_connect_click(widget, pressed, released, data), 0) - -/* - * GTK3 bridge: GtkGestureMultiPress in GTK3 does not process - * GDK_2BUTTON_PRESS / GDK_3BUTTON_PRESS. See implementation in gtk.c - * for the full explanation and GTK4 migration instructions. - * - * Connection: call alongside dt_gui_connect_click() with the same - * pressed callback and data. The callback receives (NULL, n_press, x, y, data) - * — the NULL gesture indicates the call came from this bridge, so the callback - * must handle that gracefully (see culling.c/thumbtable.c for the pattern). - * - * GTK4 migration: remove all calls to dt_gui_connect_double_click() and - * delete this declaration. GtkGestureClick handles n_press natively. - */ -unsigned long dt_gui_connect_double_click(GtkWidget *widget, - GCallback pressed, - gpointer data); -#define dt_gui_connect_double_click(widget, pressed, data) ( \ - ASSERT_FUNC_TYPE(pressed, void(*)(GtkGestureSingle *, int, double, double, __typeof__(data))), \ - dt_gui_connect_double_click(GTK_WIDGET(widget), G_CALLBACK(pressed), (data))) -void dt_gui_disconnect_double_click(GtkWidget *widget, unsigned long id); + dt_gui_connect_click(widget, pressed, released, data) GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, GCallback drag_begin, @@ -643,6 +631,37 @@ GtkEventController *(dt_gui_connect_key)(GtkWidget *widget, #define dt_gui_get_widget(controller) \ gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller)) +/* object-data key on the gesture carrying a shortcut-activated press' + * button+state, encoded as (state << 8) | button (see + * _action_process_toggle/_action_process_button). Set right before the + * synthetic "pressed" emit and cleared after it, so a NULL means the press + * came from a real event. */ +#define DT_ACTION_GESTURE_SYNTH_KEY "_dt_action_gesture_synth" + +/* button of the current gesture press; for a shortcut-activated press the + * effect-determined button, else the real press' button (GtkGestureSingle + * resets current-button to 0 on release, so a 0 is never a real press and + * stands for a primary click) */ +static inline guint dt_gui_current_button(GtkGestureSingle *gesture) +{ + const gpointer synth = g_object_get_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY); + if(synth) return GPOINTER_TO_INT(synth) & 0xff; + const guint button = gtk_gesture_single_get_current_button(gesture); + return button ? button : GDK_BUTTON_PRIMARY; +} + +/* modifiers of the current gesture press: for a shortcut-activated press the + * effect-determined modifiers (the action effect wins over whatever key + * produced the shortcut), else the current event's */ +static inline GdkModifierType dt_gui_current_state(GtkGestureSingle *gesture) +{ + const gpointer synth = g_object_get_data(G_OBJECT(gesture), DT_ACTION_GESTURE_SYNTH_KEY); + if(synth) return (GdkModifierType)(GPOINTER_TO_INT(synth) >> 8); + GdkModifierType state; + gtk_get_current_event_state(&state); + return state; +} + #define dt_gui_claim(gesture) \ gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED) #define dt_gui_deny(gesture) \ diff --git a/src/iop/agx.c b/src/iop/agx.c index cca6b43cf76..b86baa6d486 100644 --- a/src/iop/agx.c +++ b/src/iop/agx.c @@ -2071,7 +2071,7 @@ static void _add_exposure_box(dt_iop_module_t *self, dt_iop_agx_gui_data_t *g, d GtkWidget *auto_tune_label = dt_ui_label_new(_("auto tune levels")); g->range_exposure_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_DENOISE, NULL); gtk_widget_set_tooltip_text(g->range_exposure_picker, _("set black and white relative exposure using the selected area")); - dt_action_define_iop(real_self, N_("exposure range"), N_("auto tune levels"), g->range_exposure_picker, &dt_action_def_toggle); + dt_action_define_iop(real_self, N_("exposure range"), N_("auto tune levels"), g->range_exposure_picker, &dt_action_def_color_picker); dt_gui_box_add(auto_tune_box, dt_gui_expand(auto_tune_label), g->range_exposure_picker); dt_gui_box_add(g->range_exposure_picker_group, auto_tune_box); diff --git a/src/iop/ashift.c b/src/iop/ashift.c index 862f0838a6f..8e40cf22f85 100644 --- a/src/iop/ashift.c +++ b/src/iop/ashift.c @@ -5326,13 +5326,14 @@ static void _event_fit_v_button_clicked(GtkGestureSingle *gesture, { DT_GUARD_GUI_UPDATE(); - if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) { dt_iop_ashift_params_t *p = self->params; dt_iop_ashift_gui_data_t *g = self->gui_data; - const int control = dt_modifiers_include(dt_key_modifier_state(), GDK_CONTROL_MASK); - const int shift = dt_modifiers_include(dt_key_modifier_state(), GDK_SHIFT_MASK); + const GdkModifierType state = dt_gui_current_state(gesture); + const int control = dt_modifiers_include(state, GDK_CONTROL_MASK); + const int shift = dt_modifiers_include(state, GDK_SHIFT_MASK); dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; @@ -5375,13 +5376,14 @@ static void _event_fit_h_button_clicked(GtkGestureSingle *gesture, { DT_GUARD_GUI_UPDATE(); - if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) { dt_iop_ashift_params_t *p = self->params; dt_iop_ashift_gui_data_t *g = self->gui_data; - const int control = dt_modifiers_include(dt_key_modifier_state(), GDK_CONTROL_MASK); - const int shift = dt_modifiers_include(dt_key_modifier_state(), GDK_SHIFT_MASK); + const GdkModifierType state = dt_gui_current_state(gesture); + const int control = dt_modifiers_include(state, GDK_CONTROL_MASK); + const int shift = dt_modifiers_include(state, GDK_SHIFT_MASK); dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; @@ -5424,13 +5426,14 @@ static void _event_fit_both_button_clicked(GtkGestureSingle *gesture, { DT_GUARD_GUI_UPDATE(); - if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) { dt_iop_ashift_params_t *p = self->params; dt_iop_ashift_gui_data_t *g = self->gui_data; - const int control = dt_modifiers_include(dt_key_modifier_state(), GDK_CONTROL_MASK); - const int shift = dt_modifiers_include(dt_key_modifier_state(), GDK_SHIFT_MASK); + const GdkModifierType state = dt_gui_current_state(gesture); + const int control = dt_modifiers_include(state, GDK_CONTROL_MASK); + const int shift = dt_modifiers_include(state, GDK_SHIFT_MASK); dt_iop_ashift_fitaxis_t fitaxis = ASHIFT_FIT_NONE; @@ -5476,15 +5479,16 @@ static void _event_structure_auto_clicked(GtkGestureSingle *gesture, GtkWidget *widget = dt_gui_get_widget(gesture); DT_GUARD_GUI_UPDATE(); - if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) { dt_iop_ashift_params_t *p = self->params; dt_iop_ashift_gui_data_t *g = self->gui_data; _do_clean_structure(self, p, TRUE); - const int control = dt_modifiers_include(dt_key_modifier_state(), GDK_CONTROL_MASK); - const int shift = dt_modifiers_include(dt_key_modifier_state(), GDK_SHIFT_MASK); + const GdkModifierType state = dt_gui_current_state(gesture); + const int control = dt_modifiers_include(state, GDK_CONTROL_MASK); + const int shift = dt_modifiers_include(state, GDK_SHIFT_MASK); dt_iop_ashift_enhance_t enhance; @@ -6117,12 +6121,18 @@ void gui_init(dt_iop_module_t *self) (g->structure_quad, _("manually define perspective rectangle")); gtk_widget_set_tooltip_text(g->structure_lines, _("manually draw structure lines")); - dt_gui_connect_click(g->fit_v, _event_fit_v_button_clicked, NULL, self); - dt_gui_connect_click(g->fit_h, _event_fit_h_button_clicked, NULL, self); - dt_gui_connect_click(g->fit_both, _event_fit_both_button_clicked, NULL, self); - dt_gui_connect_click(g->structure_quad, _event_structure_quad_clicked, NULL, self); - dt_gui_connect_click(g->structure_lines, _event_structure_lines_clicked, NULL, self); - dt_gui_connect_click(g->structure_auto, _event_structure_auto_clicked, NULL, self); + g_object_set_data(G_OBJECT(g->fit_v), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->fit_v, _event_fit_v_button_clicked, NULL, self)); + g_object_set_data(G_OBJECT(g->fit_h), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->fit_h, _event_fit_h_button_clicked, NULL, self)); + g_object_set_data(G_OBJECT(g->fit_both), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->fit_both, _event_fit_both_button_clicked, NULL, self)); + g_object_set_data(G_OBJECT(g->structure_quad), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->structure_quad, _event_structure_quad_clicked, NULL, self)); + g_object_set_data(G_OBJECT(g->structure_lines), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->structure_lines, _event_structure_lines_clicked, NULL, self)); + g_object_set_data(G_OBJECT(g->structure_auto), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(g->structure_auto, _event_structure_auto_clicked, NULL, self)); g_signal_connect(G_OBJECT(self->widget), "draw", G_CALLBACK(_event_draw), self); dt_action_define_iop(self, N_("fit"), diff --git a/src/iop/basecurve.c b/src/iop/basecurve.c index ef972eac2dc..9815d7e3c0e 100644 --- a/src/iop/basecurve.c +++ b/src/iop/basecurve.c @@ -1936,12 +1936,17 @@ static void dt_iop_basecurve_button_press(GtkGestureSingle *gesture, } else if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY && g->selected >= 0) { + // consume the event so it does not bubble to the module body's + // right-click handler (which opens the presets menu) + dt_gui_claim(gesture); + if(g->selected == 0 || g->selected == nodes - 1) { float reset_value = g->selected == 0 ? 0 : 1; basecurve[g->selected].y = basecurve[g->selected].x = reset_value; gtk_widget_queue_draw(GTK_WIDGET(g->area)); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget); + return; // an endpoint reset must not fall through into node removal } for(int k = g->selected; k < nodes - 1; k++) diff --git a/src/iop/borders.c b/src/iop/borders.c index d01d67f7647..3cd4cda89a7 100644 --- a/src/iop/borders.c +++ b/src/iop/borders.c @@ -1017,7 +1017,7 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_tooltip_text(GTK_WIDGET(g->border_picker), _("pick border color from image")); dt_action_define_iop(self, N_("pickers"), N_("border color"), - g->border_picker, &dt_action_def_toggle); + g->border_picker, &dt_action_def_color_picker); dt_gui_box_add(self->widget, dt_gui_hbox(dt_gui_expand(label), g->colorpick, g->border_picker)); label = dtgtk_reset_label_new(_("frame line color"), self, &p->frame_color, 3 * sizeof(float)); @@ -1031,7 +1031,7 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_tooltip_text(GTK_WIDGET(g->frame_picker), _("pick frame line color from image")); dt_action_define_iop(self, N_("pickers"), N_("frame line color"), - g->frame_picker, &dt_action_def_toggle); + g->frame_picker, &dt_action_def_color_picker); dt_gui_box_add(self->widget, dt_gui_hbox(dt_gui_expand(label), g->frame_colorpick, g->frame_picker)); } diff --git a/src/iop/channelmixerrgb.c b/src/iop/channelmixerrgb.c index 0dc94dce451..79fb7fcc17e 100644 --- a/src/iop/channelmixerrgb.c +++ b/src/iop/channelmixerrgb.c @@ -4482,7 +4482,7 @@ void gui_init(dt_iop_module_t *self) G_CALLBACK(_illuminant_color_draw), self); g->color_picker = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, NULL); - dt_action_define_iop(self, NULL, N_("picker"), g->color_picker, &dt_action_def_toggle); + dt_action_define_iop(self, NULL, N_("picker"), g->color_picker, &dt_action_def_color_picker); gtk_widget_set_tooltip_text(g->color_picker, _("set white balance to detected from area")); diff --git a/src/iop/colorzones.c b/src/iop/colorzones.c index 87dd0fa77df..41cfed05bfb 100644 --- a/src/iop/colorzones.c +++ b/src/iop/colorzones.c @@ -2185,6 +2185,10 @@ static void _area_button_press_callback(GtkGestureSingle *gesture, } else if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY && g->selected >= 0) { + // consume the event so it does not bubble to the module body's + // right-click handler (which opens the presets menu) + dt_gui_claim(gesture); + if((g->selected == 0 || g->selected == nodes - 1) && p->splines_version == DT_IOP_COLORZONES_SPLINES_V1) { @@ -2205,6 +2209,7 @@ static void _area_button_press_callback(GtkGestureSingle *gesture, dt_iop_color_picker_reset(self, TRUE); gtk_widget_queue_draw(GTK_WIDGET(g->area)); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); + return; // an endpoint reset must not fall through into node removal } // right click deletes the node, ctrl+right click reset the node to y-zero @@ -2627,7 +2632,7 @@ void gui_init(dt_iop_module_t *self) _("pick GUI color from image\nctrl+click or right-click to select an area")); gtk_widget_set_name(g->colorpicker, "keep-active"); dt_action_define_iop(self, N_("pickers"), N_("show color"), - g->colorpicker, &dt_action_def_toggle); + g->colorpicker, &dt_action_def_color_picker); g->colorpicker_set_values = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_AREA, NULL, IOP_CS_LCH); @@ -2642,7 +2647,7 @@ void gui_init(dt_iop_module_t *self) "ctrl+drag to create a positive curve\n" "shift+drag to create a negative curve")); dt_action_define_iop(self, N_("pickers"), N_("create curve"), - g->colorpicker_set_values, &dt_action_def_toggle); + g->colorpicker_set_values, &dt_action_def_color_picker); // the nice graph g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap(NULL, diff --git a/src/iop/liquify.c b/src/iop/liquify.c index 4ae571472be..6a7e1fae775 100644 --- a/src/iop/liquify.c +++ b/src/iop/liquify.c @@ -3620,13 +3620,13 @@ static void btn_make_radio_callback(GtkGestureSingle *gesture, double y, dt_iop_module_t *self) { - GdkEvent *event = gtk_get_current_event(); - gboolean ctrl_pressed = FALSE; - if(event) - { - ctrl_pressed = dt_modifier_is(dt_gdk_event_get_state(event), GDK_CONTROL_MASK); - gdk_event_free(event); - } + /* read the modifier state from the gesture: for a real click this is + * the live event state, for a shortcut-activated press it is the + * declared effect carried by _action_effect_button_state() (see + * DT_ACTION_GESTURE_SYNTH_KEY in gtk.h); gtk_get_current_event() would + * return the key event during shortcut dispatch and lose the declared + * ctrl variant */ + const gboolean ctrl_pressed = dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK); GtkWidget *btn = dt_gui_get_widget(gesture); diff --git a/src/iop/negadoctor.c b/src/iop/negadoctor.c index bbee8da05ca..f5943d19b8a 100644 --- a/src/iop/negadoctor.c +++ b/src/iop/negadoctor.c @@ -847,7 +847,7 @@ void gui_init(dt_iop_module_t *self) g->Dmin_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, NULL); gtk_widget_set_tooltip_text(g->Dmin_sampler , _("pick color of film material from image")); - dt_action_define_iop(self, N_("pickers"), N_("film material"), g->Dmin_sampler, &dt_action_def_toggle); + dt_action_define_iop(self, N_("pickers"), N_("film material"), g->Dmin_sampler, &dt_action_def_color_picker); dt_gui_box_add(page1, dt_ui_section_label_new(C_("section", "color of the film base")), dt_gui_hbox(dt_gui_expand(g->Dmin_picker), g->Dmin_sampler)); @@ -909,7 +909,7 @@ void gui_init(dt_iop_module_t *self) g->WB_low_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, NULL); gtk_widget_set_tooltip_text(g->WB_low_sampler, _("pick shadows color from image")); - dt_action_define_iop(self, N_("pickers"), N_("shadows"), g->WB_low_sampler, &dt_action_def_toggle); + dt_action_define_iop(self, N_("pickers"), N_("shadows"), g->WB_low_sampler, &dt_action_def_color_picker); dt_gui_box_add(page2, dt_ui_section_label_new(C_("section", "shadows color cast")), dt_gui_hbox(dt_gui_expand(g->WB_low_picker), g->WB_low_sampler)); @@ -943,7 +943,7 @@ void gui_init(dt_iop_module_t *self) g->WB_high_sampler = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, NULL); gtk_widget_set_tooltip_text(g->WB_high_sampler , _("pick illuminant color from image")); - dt_action_define_iop(self, N_("pickers"), N_("illuminant"), g->WB_high_sampler, &dt_action_def_toggle); + dt_action_define_iop(self, N_("pickers"), N_("illuminant"), g->WB_high_sampler, &dt_action_def_color_picker); dt_gui_box_add(page2, dt_ui_section_label_new(C_("section", "highlights white balance")), dt_gui_hbox(dt_gui_expand(g->WB_high_picker), g->WB_high_sampler)); diff --git a/src/iop/retouch.c b/src/iop/retouch.c index 902cf02ba93..033667ef849 100644 --- a/src/iop/retouch.c +++ b/src/iop/retouch.c @@ -1817,9 +1817,10 @@ static void rt_edit_masks_callback(GtkGestureSingle *gesture, return; } - GdkEvent *event = gtk_get_current_event(); - const guint button = event ? dt_gdk_event_get_button(event) : 0; - const GdkModifierType state = event ? dt_gdk_event_get_state(event) : 0; + /* for a shortcut-activated press dt_gui_current_button/state return the + * action effect's button/state, for a real click the event's */ + const guint button = dt_gui_current_button(gesture); + const GdkModifierType state = dt_gui_current_state(gesture); dt_iop_gui_blend_data_t *bd = self->blend_data; dt_iop_retouch_gui_data_t *g = self->gui_data; @@ -1886,8 +1887,6 @@ static void rt_edit_masks_callback(GtkGestureSingle *gesture, DT_LEAVE_GUI_UPDATE(); } - - if(event) gdk_event_free(event); } static void rt_add_shape_callback(GtkGestureSingle *gesture, @@ -1904,13 +1903,8 @@ static void rt_add_shape_callback(GtkGestureSingle *gesture, dt_iop_color_picker_reset(self, TRUE); - gboolean creation_continuous = FALSE; - GdkEvent *event = gtk_get_current_event(); - if(event) - { - creation_continuous = dt_modifier_is(dt_gdk_event_get_state(event), GDK_CONTROL_MASK); - gdk_event_free(event); - } + const gboolean creation_continuous = + dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK); rt_add_shape(widget, creation_continuous, self); @@ -1951,8 +1945,7 @@ static void rt_select_algorithm_callback(GtkGestureSingle *gesture, // check if we have to do something gboolean accept = TRUE; - GdkEvent *event = gtk_get_current_event(); - const GdkModifierType state = event ? dt_gdk_event_get_state(event) : 0; + const GdkModifierType state = dt_gui_current_state(gesture); const int index = rt_get_selected_shape_index(p); if(index >= 0 && dt_modifier_is(state, GDK_CONTROL_MASK)) @@ -1989,10 +1982,7 @@ static void rt_select_algorithm_callback(GtkGestureSingle *gesture, rt_show_hide_controls(self); if(!accept) - { - if(event) gdk_event_free(event); return; - } if(index >= 0 && dt_modifier_is(state, GDK_CONTROL_MASK)) { @@ -2027,8 +2017,6 @@ static void rt_select_algorithm_callback(GtkGestureSingle *gesture, dt_control_queue_redraw_center(); } - if(event) gdk_event_free(event); - dt_dev_add_history_item(darktable.develop, self, TRUE); // if we have the shift key pressed, we set it as default @@ -2656,7 +2644,7 @@ void gui_init(dt_iop_module_t *self) NULL); gtk_widget_set_tooltip_text(g->colorpicker, _("pick fill color from image")); dt_action_define_iop(self, NULL, N_("pick fill color"), - g->colorpicker, &dt_action_def_toggle); + g->colorpicker, &dt_action_def_color_picker); g->hbox_color_pick = dt_gui_hbox(dt_ui_label_new(_("fill color: ")), dt_gui_expand(g->colorpick), g->colorpicker); diff --git a/src/iop/rgbcurve.c b/src/iop/rgbcurve.c index 148d5e85457..dbe165a7265 100644 --- a/src/iop/rgbcurve.c +++ b/src/iop/rgbcurve.c @@ -1399,6 +1399,10 @@ static void _area_button_press_callback(GtkGestureSingle *gesture, } else if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY && g->selected >= 0) { + // consume the event so it does not bubble to the module body's + // right-click handler (which opens the presets menu) + dt_gui_claim(gesture); + if(g->selected == 0 || g->selected == nodes - 1) { const float reset_value = g->selected == 0 ? 0.f : 1.f; @@ -1406,6 +1410,7 @@ static void _area_button_press_callback(GtkGestureSingle *gesture, dt_iop_color_picker_reset(self, TRUE); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); gtk_widget_queue_draw(GTK_WIDGET(g->area)); + return; // an endpoint reset must not fall through into node removal } for(int k = g->selected; k < nodes - 1; k++) @@ -1492,7 +1497,7 @@ void gui_init(dt_iop_module_t *self) "ctrl+click or right-click to select an area")); gtk_widget_set_name(g->colorpicker, "keep-active"); dt_action_define_iop(self, N_("pickers"), N_("show color"), - g->colorpicker, &dt_action_def_toggle); + g->colorpicker, &dt_action_def_color_picker); g->colorpicker_set_values = dt_color_picker_new(self, DT_COLOR_PICKER_AREA | DT_COLOR_PICKER_IO, NULL); @@ -1507,7 +1512,7 @@ void gui_init(dt_iop_module_t *self) "ctrl+drag to create a positive curve\n" "shift+drag to create a negative curve")); dt_action_define_iop(self, N_("pickers"), N_("create curve"), - g->colorpicker_set_values, &dt_action_def_toggle); + g->colorpicker_set_values, &dt_action_def_color_picker); g->area = GTK_DRAWING_AREA(dtgtk_drawing_area_new_with_height(0)); g_object_set_data(G_OBJECT(g->area), "iop-instance", self); diff --git a/src/iop/rgblevels.c b/src/iop/rgblevels.c index 8ea698fa777..3291917bed4 100644 --- a/src/iop/rgblevels.c +++ b/src/iop/rgblevels.c @@ -1054,7 +1054,7 @@ void gui_init(dt_iop_module_t *self) #define PICKER_SETUP(color, name, tooltip) \ g->color##pick = dt_color_picker_new(self, DT_COLOR_PICKER_POINT, NULL); \ dt_action_define_iop(self, N_("pickers"), name, g->color##pick, \ - &dt_action_def_toggle); \ + &dt_action_def_color_picker); \ gtk_widget_set_tooltip_text(g->color##pick, tooltip); \ gtk_widget_set_name(GTK_WIDGET(g->color##pick), "picker-"#color); \ g_signal_connect(dt_gui_expand(g->color##pick), "toggled", \ diff --git a/src/iop/spots.c b/src/iop/spots.c index d498bde6756..b548054e589 100644 --- a/src/iop/spots.c +++ b/src/iop/spots.c @@ -340,13 +340,8 @@ static void _add_shape_callback(GtkGestureSingle *gesture, GtkWidget *widget = dt_gui_get_widget(gesture); const dt_iop_spots_gui_data_t *g = self->gui_data; - gboolean creation_continuous = FALSE; - GdkEvent *event = gtk_get_current_event(); - if(event) - { - creation_continuous = dt_modifier_is(dt_gdk_event_get_state(event), GDK_CONTROL_MASK); - gdk_event_free(event); - } + const gboolean creation_continuous = + dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK); _add_shape(widget, creation_continuous, self); diff --git a/src/iop/temperature.c b/src/iop/temperature.c index a0448e2df33..7231018cfc9 100644 --- a/src/iop/temperature.c +++ b/src/iop/temperature.c @@ -2076,7 +2076,7 @@ void gui_init(dt_iop_module_t *self) g->colorpicker = dt_color_picker_new_with_cst(self, DT_COLOR_PICKER_AREA, NULL, IOP_CS_NONE); dt_action_define_iop(self, N_("settings"), N_("from image area"), - g->colorpicker, &dt_action_def_toggle); + g->colorpicker, &dt_action_def_color_picker); dtgtk_togglebutton_set_paint(DTGTK_TOGGLEBUTTON(g->colorpicker), dtgtk_cairo_paint_colorpicker, 0, NULL); dt_gui_add_class(g->colorpicker, "dt_transparent_background"); diff --git a/src/iop/tonecurve.c b/src/iop/tonecurve.c index c7b92043841..2b9be6d6fdf 100644 --- a/src/iop/tonecurve.c +++ b/src/iop/tonecurve.c @@ -1279,7 +1279,7 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_tooltip_text (g->colorpicker, _("pick GUI color from image\nctrl+click or right-click to select an area")); - dt_action_define_iop(self, NULL, N_("pick color"), g->colorpicker, &dt_action_def_toggle); + dt_action_define_iop(self, NULL, N_("pick color"), g->colorpicker, &dt_action_def_color_picker); dt_gui_box_add(self->widget, dt_gui_hbox(dt_gui_expand(g->channel_tabs), dt_gui_align_right(g->colorpicker))); @@ -1944,12 +1944,18 @@ static void dt_iop_tonecurve_button_press(GtkGestureSingle *gesture, } else if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY && g->selected >= 0) { + // consume the event so it does not bubble to the module body's + // right-click handler (which opens the presets menu); the pre-migration + // button-press handler returned TRUE here + dt_gui_claim(gesture); + if(g->selected == 0 || g->selected == nodes - 1) { float reset_value = g->selected == 0 ? 0 : 1; tonecurve[g->selected].y = tonecurve[g->selected].x = reset_value; gtk_widget_queue_draw(GTK_WIDGET(g->area)); dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + ch); + return; // an endpoint reset must not fall through into node removal } for(int k = g->selected; k < nodes - 1; k++) diff --git a/src/iop/watermark.c b/src/iop/watermark.c index a3afabf29eb..411e50a4540 100644 --- a/src/iop/watermark.c +++ b/src/iop/watermark.c @@ -1461,7 +1461,7 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_tooltip_text(GTK_WIDGET(g->color_picker_button), _("pick color from image")); dt_action_define_iop(self, NULL, N_("pick color"), - g->color_picker_button, &dt_action_def_toggle); + g->color_picker_button, &dt_action_def_color_picker); gtk_grid_attach(grid, label, 0, line++, 1, 1); gtk_grid_attach_next_to(grid, g->colorpick, label, GTK_POS_RIGHT, 1, 1); diff --git a/src/libs/collect.c b/src/libs/collect.c index ec92d7fcb01..eaef0f5d918 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -632,6 +632,84 @@ static void view_popup_menu(GtkWidget *treeview, gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)event); } +/* Claim the press only when the collections handler fully owns it, so a + * plain click still reaches the treeview's internal bubble-phase gesture: + * single-click expander toggles, focus grab and cursor placement keep + * working (the pre-migration button-press-event handler returned FALSE for + * those presses). The presses that must claim are the ones the handler + * resolves itself and that the internal gesture would fight: modifier + * presses (ctrl/shift selection, shift-range, ctrl+shift view switch), + * single-click mode, the MONTH rule's single-press activation, and the + * folder/filmroll context menu. Same selective pattern as in + * gui/accelerators.c. */ +static void _gesture_begin_claim(GtkGesture *gesture, + GdkEventSequence *sequence, + gpointer user_data) +{ + const dt_lib_collect_t *d = user_data; + + GdkModifierType state; + gtk_get_current_event_state(&state); + + const gboolean modifier = dt_modifier_is(state, GDK_SHIFT_MASK) + || dt_modifier_is(state, GDK_CONTROL_MASK) + || dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK); + const guint button = gtk_gesture_single_get_current_button(GTK_GESTURE_SINGLE(gesture)); + const gboolean folder_menu + = (d->view_rule == DT_COLLECTION_PROP_FOLDERS + || d->view_rule == DT_COLLECTION_PROP_FILMROLL) + && button == GDK_BUTTON_SECONDARY && !modifier; + + if(modifier || d->singleclick || d->view_rule == DT_COLLECTION_PROP_MONTH || folder_menu) + gtk_gesture_set_sequence_state(gesture, sequence, GTK_EVENT_SEQUENCE_CLAIMED); +} + +/* Replicates GTK's private coords_are_over_arrow(): TRUE when the bin-window + * coordinates hit the expander arrow of a parent row. Needed to tell apart + * a double-click on the arrow (the treeview's internal gesture toggles it on + * every primary release, so ours must claim the second press and not add a + * third toggle) from a double-click on the row body (the internal gesture + * does not toggle there, ours must). */ +static gboolean _coords_are_over_arrow(GtkTreeView *view, gint bin_x, gint bin_y) +{ + GtkTreePath *path = NULL; + GtkTreeViewColumn *column = NULL; + gint cell_x, cell_y; + + if(!gtk_tree_view_get_path_at_pos(view, bin_x, bin_y, &path, &column, &cell_x, &cell_y)) + return FALSE; + + GtkTreeIter iter; + GtkTreeModel *model = gtk_tree_view_get_model(view); + const gboolean over_parent = gtk_tree_model_get_iter(model, &iter, path) + && gtk_tree_model_iter_has_child(model, &iter); + const gboolean in_expander_column = column == gtk_tree_view_get_expander_column(view); + if(!over_parent || !in_expander_column) + { + gtk_tree_path_free(path); + return FALSE; + } + + gboolean indent_expanders = TRUE; + gint expander_size = 12, horizontal_separator = 0; + gtk_widget_style_get(GTK_WIDGET(view), + "indent-expanders", &indent_expanders, + "horizontal-separator", &horizontal_separator, + "expander-size", &expander_size, NULL); + + /* GTK draws the arrow one expander width in per tree level below the + * top level when indent-expanders is set: gtk_tree_view_get_arrow_xrange + * offsets by expander_size * (rbtree_depth - 1) (the root rbtree has + * depth 0), and the rbtree depth is path depth - 1. */ + const gint depth = gtk_tree_path_get_depth(path); + const gint x = indent_expanders ? expander_size * (depth - 1) : 0; + const gint x1 = x + horizontal_separator / 2; + const gint x2 = x + expander_size; + gtk_tree_path_free(path); + + return cell_x >= x1 && cell_x < x2; +} + static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, double x, double y, dt_lib_collect_t *d) @@ -655,8 +733,32 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, GdkModifierType mod_state; gtk_get_current_event_state(&mod_state); + const guint button = gtk_gesture_single_get_current_button(gesture); + const gboolean modifier = dt_modifier_is(mod_state, GDK_SHIFT_MASK) + || dt_modifier_is(mod_state, GDK_CONTROL_MASK) + || dt_modifier_is(mod_state, GDK_SHIFT_MASK | GDK_CONTROL_MASK); + + const gboolean plain_primary + = button == GDK_BUTTON_PRIMARY && !modifier && !d->singleclick + && d->view_rule != DT_COLLECTION_PROP_MONTH; + const gboolean over_arrow = path && _coords_are_over_arrow(GTK_TREE_VIEW(treeview), bin_x, bin_y); + + /* the treeview's internal gesture toggles the expander on every primary + * release over the arrow, so it owns all arrow toggles; ours must not add + * a toggle for any n_press >= 2 primary press on the arrow (a double + * click would otherwise get release1 + press2 + release2 = 3 toggles). + * Only the second press of a fast pair is claimed, so the internal + * gesture never sees it: a double-click then toggles exactly once (on the + * first release) with no flicker, while every later press of a longer + * burst is still handled by the internal gesture and keeps responding. */ + const gboolean arrow_second_press = n_press == 2 && plain_primary && over_arrow; + if(arrow_second_press) + { + GdkEventSequence *sequence = gtk_gesture_single_get_current_sequence(GTK_GESTURE_SINGLE(gesture)); + gtk_gesture_set_sequence_state(GTK_GESTURE(gesture), sequence, GTK_EVENT_SEQUENCE_CLAIMED); + } - if((n_press >= 2 || d->singleclick) && path) + if((n_press >= 2 || d->singleclick) && path && !(n_press >= 2 && plain_primary && over_arrow)) { if(mod_state == last_mod_state) { @@ -695,6 +797,7 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, row_activated_with_event(GTK_TREE_VIEW(treeview), path, NULL, (GdkEventButton *)event, d); gtk_tree_path_free(path); + gdk_event_free(event); return; } @@ -704,8 +807,6 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, gtk_tree_selection_select_path(selection, path); } - const guint button = gtk_gesture_single_get_current_button(gesture); - // case of a context-menu (folder/filmroll) if(((d->view_rule == DT_COLLECTION_PROP_FOLDERS) || (d->view_rule == DT_COLLECTION_PROP_FILMROLL)) @@ -717,25 +818,26 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, view_popup_menu(treeview, (GdkEventButton *)event, d); if(path) gtk_tree_path_free(path); + gdk_event_free(event); return; } // case of a activation if((!d->singleclick && n_press >= 2 && button == GDK_BUTTON_PRIMARY) || (d->singleclick && n_press == 1 && button == GDK_BUTTON_PRIMARY) - || (!d->singleclick && n_press == 1 && button == GDK_BUTTON_PRIMARY - && (dt_modifier_is(mod_state, GDK_SHIFT_MASK) - || dt_modifier_is(mod_state, GDK_CONTROL_MASK))) + || (!d->singleclick && n_press == 1 && button == GDK_BUTTON_PRIMARY && modifier) || (d->view_rule == DT_COLLECTION_PROP_MONTH && n_press == 1 && button == GDK_BUTTON_PRIMARY)) { row_activated_with_event(GTK_TREE_VIEW(treeview), path, NULL, (GdkEventButton *)event, d); if(path) gtk_tree_path_free(path); + gdk_event_free(event); return; } if(path) gtk_tree_path_free(path); + gdk_event_free(event); } static gboolean view_onPopupMenu(GtkWidget *treeview, dt_lib_collect_t *d) @@ -3674,7 +3776,9 @@ static void popup_button_callback_cb(GtkGestureSingle *gesture, int n_press, gtk_widget_show_all(GTK_WIDGET(menu)); - gtk_menu_popup_at_pointer(GTK_MENU(menu), gtk_get_current_event()); + GdkEvent *event = gtk_get_current_event(); + gtk_menu_popup_at_pointer(GTK_MENU(menu), event); + gdk_event_free(event); return; } @@ -4074,7 +4178,17 @@ void gui_init(dt_lib_module_t *self) d->view_rule = -1; d->view = view; gtk_tree_view_set_headers_visible(view, FALSE); - dt_gui_connect_click_all(view, view_onButtonPressed_cb, NULL, d); + /* the treeview owns an internal bubble-phase GtkGestureMultiPress that + * would fight our own selection handling: use a CAPTURE-phase gesture + * that claims the sequence, replicating the event consumption of the + * pre-migration button-press-event handler */ + GtkGesture *gesture = gtk_gesture_multi_press_new(GTK_WIDGET(view)); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture), + GTK_PHASE_CAPTURE); + dt_gui_add_controller(GTK_WIDGET(view), gesture); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); + g_signal_connect(gesture, "pressed", G_CALLBACK(view_onButtonPressed_cb), d); + g_signal_connect(gesture, "begin", G_CALLBACK(_gesture_begin_claim), d); g_signal_connect(G_OBJECT(view), "popup-menu", G_CALLBACK(view_onPopupMenu), d); GtkTreeViewColumn *col = gtk_tree_view_column_new(); diff --git a/src/libs/colorpicker.c b/src/libs/colorpicker.c index 39dc733fe61..6eedeb4d6b6 100644 --- a/src/libs/colorpicker.c +++ b/src/libs/colorpicker.c @@ -768,7 +768,7 @@ void gui_init(dt_lib_module_t *self) g_signal_connect(G_OBJECT(data->picker_button), "toggled", G_CALLBACK(_picker_button_toggled), data); dt_action_define(DT_ACTION(self), NULL, N_("pick color"), - data->picker_button, &dt_action_def_toggle); + data->picker_button, &dt_action_def_color_picker); // The small sample, label and add button GtkWidget *sample_row_events = gtk_event_box_new(); diff --git a/src/libs/lib.c b/src/libs/lib.c index a3f15255648..925469d3d26 100644 --- a/src/libs/lib.c +++ b/src/libs/lib.c @@ -970,10 +970,31 @@ static void _lib_gui_reset_callback(GtkGestureSingle *gesture, gdouble y, dt_lib_module_t *module) { + /* the reset action, shared by the module header reset button (wired through + * its "clicked" signal in _lib_init_header) and the action/shortcut + * fallback (_action_process, which passes a NULL gesture) */ module->gui_reset(module); if(module->has_preset_label(module)) gtk_label_set_text(GTK_LABEL(module->preset_label), ""); - dt_gui_claim(gesture); + if(gesture) + dt_gui_claim(gesture); +} + +static void _lib_gui_reset_button_clicked_callback(GtkWidget *widget, + gpointer user_data) +{ + /* the reset runs on the button's own "clicked" signal, not on a custom + * gesture's press/release: gui_reset() may open a modal dialog, and a + * dialog opened from inside a press handler would interrupt the press + * dispatch -- the button's gesture only processes the press after the + * dialog closes and its release was consumed by the dialog's grab, leaving + * the button stuck in its pressed state. "clicked" fires once the press + * and release have both completed, so the dialog cannot split them. + * (A custom gesture's "released" is equally unusable: the dialog's grab + * cancels the gesture and dt_gui_connect_click() forwards that + * cancellation to the released handler, firing the reset twice.) */ + dt_lib_module_t *module = user_data; + _lib_gui_reset_callback(NULL, 1, 0.0, 0.0, module); } static void _presets_popup_callback(GtkWidget *button, @@ -1395,7 +1416,9 @@ GtkWidget *dt_lib_gui_get_expander(dt_lib_module_t *module) /* add reset button if module has implementation */ module->reset_button = dtgtk_button_new(dtgtk_cairo_paint_reset, 0, NULL); - dt_gui_connect_click(module->reset_button, _lib_gui_reset_callback, NULL, module); + /* "clicked" rather than a custom gesture: see _lib_gui_reset_button_clicked_callback */ + g_signal_connect(G_OBJECT(module->reset_button), "clicked", + G_CALLBACK(_lib_gui_reset_button_clicked_callback), module); dt_gui_connect_motion(module->reset_button, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(DT_ACTION_ELEMENT_RESET)); if(!module->gui_reset) gtk_widget_set_sensitive(module->reset_button, FALSE); dt_action_define(&module->actions, NULL, NULL, module->reset_button, NULL); diff --git a/src/libs/masks.c b/src/libs/masks.c index f298d12bfa1..188c93faf08 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_current_button(gesture) == GDK_BUTTON_PRIMARY) + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) { #ifdef HAVE_AI if(GPOINTER_TO_INT(shape) == DT_MASKS_OBJECT && !dt_masks_object_available()) @@ -678,9 +678,7 @@ static void _bt_add_shape_cb(GtkGestureSingle *gesture, int n_press, double x, d #endif _tree_add_shape(NULL, shape); - GdkModifierType state; - gtk_get_current_event_state(&state); - if(dt_modifier_is(state, GDK_CONTROL_MASK)) + if(dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK)) { darktable.develop->form_gui->creation_continuous = TRUE; darktable.develop->form_gui->creation_continuous_module = @@ -1518,7 +1516,9 @@ static void _tree_button_pressed_cb(GtkGestureSingle *gesture, int n_press, doub gtk_widget_show_all(GTK_WIDGET(menu)); - gtk_menu_popup_at_pointer(GTK_MENU(menu), gtk_get_current_event()); + GdkEvent *event = gtk_get_current_event(); + gtk_menu_popup_at_pointer(GTK_MENU(menu), event); + gdk_event_free(event); } } @@ -2274,35 +2274,45 @@ void gui_init(dt_lib_module_t *self) d->bt_gradient = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_gradient, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add gradient"), d->bt_gradient, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_gradient, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_GRADIENT)); + g_object_set_data(G_OBJECT(d->bt_gradient), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_gradient, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_GRADIENT))); gtk_widget_set_tooltip_text(d->bt_gradient, _("add gradient")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_gradient), FALSE); d->bt_path = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_path, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add path"), d->bt_path, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_path, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_PATH)); + g_object_set_data(G_OBJECT(d->bt_path), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_path, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_PATH))); gtk_widget_set_tooltip_text(d->bt_path, _("add path")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_path), FALSE); d->bt_ellipse = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_ellipse, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add ellipse"), d->bt_ellipse, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_ellipse, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_ELLIPSE)); + g_object_set_data(G_OBJECT(d->bt_ellipse), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_ellipse, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_ELLIPSE))); gtk_widget_set_tooltip_text(d->bt_ellipse, _("add ellipse")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_ellipse), FALSE); d->bt_circle = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_circle, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add circle"), d->bt_circle, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_circle, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_CIRCLE)); + g_object_set_data(G_OBJECT(d->bt_circle), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_circle, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_CIRCLE))); gtk_widget_set_tooltip_text(d->bt_circle, _("add circle")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_circle), FALSE); d->bt_brush = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_brush, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add brush"), d->bt_brush, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_brush, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_BRUSH)); + g_object_set_data(G_OBJECT(d->bt_brush), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_brush, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_BRUSH))); gtk_widget_set_tooltip_text(d->bt_brush, _("add brush")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_brush), FALSE); @@ -2310,7 +2320,9 @@ void gui_init(dt_lib_module_t *self) d->bt_object = dtgtk_togglebutton_new(dtgtk_cairo_paint_masks_object, 0, NULL); dt_action_define(DT_ACTION(self), N_("shapes"), N_("add object"), d->bt_object, &dt_action_def_toggle); - dt_gui_connect_click(d->bt_object, _bt_add_shape_cb, NULL, GINT_TO_POINTER(DT_MASKS_OBJECT)); + g_object_set_data(G_OBJECT(d->bt_object), DT_ACTION_GESTURE_KEY, + dt_gui_connect_click(d->bt_object, _bt_add_shape_cb, NULL, + GINT_TO_POINTER(DT_MASKS_OBJECT))); gtk_widget_set_tooltip_text(d->bt_object, _("add AI object")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d->bt_object), FALSE); #endif diff --git a/src/libs/metadata.c b/src/libs/metadata.c index d976a41fcfb..48f1828f08e 100644 --- a/src/libs/metadata.c +++ b/src/libs/metadata.c @@ -448,7 +448,11 @@ static gboolean _key_pressed_cb(GtkEventControllerKey *controller, break; } - return gtk_text_view_im_context_filter_keypress(GTK_TEXT_VIEW(textview), (GdkEventKey *)gtk_get_current_event()); + GdkEvent *event = gtk_get_current_event(); + const gboolean handled = + gtk_text_view_im_context_filter_keypress(GTK_TEXT_VIEW(textview), (GdkEventKey *)event); + gdk_event_free(event); + return handled; } static gboolean _textview_focus(GtkWidget *widget, diff --git a/src/libs/tools/colorlabels.c b/src/libs/tools/colorlabels.c index a6e66f686c2..fd92d29e9a4 100644 --- a/src/libs/tools/colorlabels.c +++ b/src/libs/tools/colorlabels.c @@ -282,13 +282,9 @@ static void _lib_colorlabels_button_press_callback(GtkGestureSingle *gesture, in && colorlabel != 5) // The button to reset colorlabels needs no description { d->colorlabel = colorlabel; - GdkEvent *ev = gtk_get_current_event(); - if(ev) - { - gdouble root_x, root_y; - gdk_event_get_root_coords(ev, &root_x, &root_y); + gdouble root_x, root_y; + if(dt_gui_get_current_root_coords(&root_x, &root_y)) _lib_colorlabels_edit(self, root_x, root_y); - } } else { diff --git a/src/views/darkroom.c b/src/views/darkroom.c index 5426401c597..ed3b1621574 100644 --- a/src/views/darkroom.c +++ b/src/views/darkroom.c @@ -5040,7 +5040,7 @@ static void _second_window_scrolled_callback(GtkEventControllerScroll *controlle dt_dev_viewport_t *port = pinned_dev ? &pinned_dev->preview2 : &dev->preview2; - const GdkEvent *current = gtk_get_current_event(); + GdkEvent *current = gtk_get_current_event(); if(!current) return; GdkModifierType state; @@ -5054,18 +5054,27 @@ static void _second_window_scrolled_callback(GtkEventControllerScroll *controlle { const GdkEventScroll *scroll = (const GdkEventScroll *)current; gdouble pan_dx = 0.0, pan_dy = 0.0; - if(!dt_gui_get_scroll_deltas(scroll, &pan_dx, &pan_dy)) return; + if(!dt_gui_get_scroll_deltas(scroll, &pan_dx, &pan_dy)) + { + gdk_event_free(current); + return; + } pan_dx *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; pan_dy *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; dt_print(DT_DEBUG_INPUT, "[darkroom second window] pan dx=%.3f dy=%.3f", pan_dx, pan_dy); if(pan_dx != 0.0 || pan_dy != 0.0) dt_dev_zoom_move(port, DT_ZOOM_MOVE, 1.0f, 0, pan_dx, pan_dy, TRUE); + gdk_event_free(current); return; } int delta_y; - if(!dt_gui_get_scroll_unit_delta((const GdkEventScroll *)current, &delta_y)) return; + if(!dt_gui_get_scroll_unit_delta((const GdkEventScroll *)current, &delta_y)) + { + gdk_event_free(current); + return; + } const gboolean constrained = dev->constrain_zoom && !dt_modifier_is(state, GDK_CONTROL_MASK); gdouble x = 0.0, y = 0.0; @@ -5074,6 +5083,7 @@ static void _second_window_scrolled_callback(GtkEventControllerScroll *controlle "[darkroom second window] scroll zoom delta_y=%d", delta_y); dt_dev_zoom_move(port, DT_ZOOM_SCROLL, 0.0f, delta_y < 0 ? 1 : 0, x, y, constrained); + gdk_event_free(current); } static gboolean _second_window_pinch_callback(GtkWidget *widget,