Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
06e85df
gtk4-prep: fix color picker shortcut activation for standalone picker…
Arecsu Aug 5, 2026
4b07774
gtk4-prep: sweep all standalone picker buttons to the picker action d…
Arecsu Aug 5, 2026
c19d2ea
gtk4-prep: don't let hold shortcuts swallow double/triple presses
Arecsu Aug 6, 2026
7c68170
gtk4-prep: use bubble phase for scroll event controllers
Arecsu Aug 6, 2026
bfe16da
gtk4-prep: restore raw scroll deltas in thumbtable and culling
Arecsu Aug 6, 2026
8f08a23
gtk4-prep: route widget shortcut activation through gesture controllers
Arecsu Aug 6, 2026
f49c824
gtk4-prep: register gesture-driven masks and ashift buttons for short…
Arecsu Aug 6, 2026
b8ff325
gtk4-prep: restore early return after curve endpoint reset on right-c…
Arecsu Aug 7, 2026
067b0f4
gtk4-prep: remove duplicate history item in module reset
Arecsu Aug 7, 2026
c66e0fb
gtk4-prep: fix smooth-scroll unit handling in preset and popup scrollers
Arecsu Aug 7, 2026
4f85bee
gtk4-prep: drop the GTK3 double-click bridge
Arecsu Aug 7, 2026
dd7b820
gtk4-prep: fix GdkEvent leaks in event controller callbacks
Arecsu Aug 7, 2026
4f71d69
gtk4-prep: restore primary-button guard in thumbtable and culling rel…
Arecsu Aug 7, 2026
ed8a194
gtk4-prep: carry shortcut action effects into synthetic gesture presses
Arecsu Aug 7, 2026
3af780f
gtk4-prep: damp preset-scroll direction reversal at the preset list b…
Arecsu Aug 7, 2026
967628c
gtk4-prep: don't open the module presets menu when right-clicking a c…
Arecsu Aug 7, 2026
324ca14
gtk4-prep: encode toggle on/off effects as a plain click in synthetic…
Arecsu Aug 7, 2026
c31b53a
gtk4-prep: clear the stale hold key from the shortcut state after the…
Arecsu Aug 7, 2026
6e86732
gtk4-prep: register the remaining standalone color pickers with their…
Arecsu Aug 7, 2026
b36a4f0
gtk4-prep: read gesture state in the liquify tool shortcut handler
Arecsu Aug 7, 2026
9f853f0
gtk4-prep: scope the preset-scroll dampener per button and fix its si…
Arecsu Aug 7, 2026
e6c6d2f
gtk4-prep: claim collection treeview presses to suppress the internal…
Arecsu Aug 7, 2026
eb0fdb0
gtk4-prep: fix hold shortcut delayed-release handling
Arecsu Aug 7, 2026
eaef795
gtk4-prep: fix collection treeview press claiming and activation
Arecsu Aug 7, 2026
7aef482
gtk4-prep: fix sticky hover state on buttons after menus and dialogs …
Arecsu Aug 7, 2026
ce0a863
gtk4-prep: keep mouse_inside across shortcut grab crossings
Arecsu Aug 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/bauhaus/bauhaus.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
17 changes: 7 additions & 10 deletions src/develop/blend_gui.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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 |=
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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,
Expand Down
52 changes: 47 additions & 5 deletions src/develop/imageop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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);
}

Expand Down
3 changes: 3 additions & 0 deletions src/develop/imageop_gui.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
39 changes: 39 additions & 0 deletions src/dtgtk/button.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/dtgtk/button.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading
Loading