From 197503332d5c1c8e94afa57df73002787ffc175a Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 20:39:15 -0300 Subject: [PATCH 1/3] gtk4-prep: fix color picker shortcut activation for standalone picker buttons The gtk4-prep migration replaced the picker buttons' GObject button-press-event handler with a CAPTURE-phase GtkGestureMultiPress that claims the event sequence (the button's own internal bubble-phase gesture would otherwise toggle it a second time). Shortcut activation however still goes through dt_action_def_toggle, whose generic processor (_action_process_toggle) synthesizes GObject button-press-event signals: those only reach the widget class handler (bubble-phase controllers) and carry no device, so they can never trigger the CAPTURE-phase gesture -- picker shortcuts bound to standalone toggle buttons (e.g. AgX 'auto tune levels') silently stopped working. Give standalone picker buttons their own action definition (like bauhaus widgets have dt_action_def_bauhaus) whose process calls the same shared entry as real clicks, so shortcuts reach the picker directly without event synthesis. Update the five modules that define standalone picker buttons (AgX, channel mixer RGB, negadoctor, tone curve) to use it. Real clicks still go through the CAPTURE gesture, which now also passes the clicked button through so a secondary click still switches to area mode. --- src/gui/color_picker_proxy.c | 125 ++++++++++++++++++++++++++++++++++- src/gui/color_picker_proxy.h | 6 ++ src/iop/agx.c | 2 +- src/iop/channelmixerrgb.c | 2 +- src/iop/negadoctor.c | 6 +- src/iop/tonecurve.c | 2 +- 6 files changed, 135 insertions(+), 8 deletions(-) diff --git a/src/gui/color_picker_proxy.c b/src/gui/color_picker_proxy.c index 831f5d8c52e..3bc0ede8e31 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" /* @@ -269,9 +270,104 @@ 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) + GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); + event->button.button = gtk_gesture_single_get_current_button(gesture); + _color_picker_callback_button_press(button, &event->button, self); + gdk_event_free(event); +} + +/* + * 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)); + + 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; + _color_picker_callback_button_press(target, &event->button, self); + gdk_event_free(event); + + 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/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/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/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/tonecurve.c b/src/iop/tonecurve.c index c7b92043841..e51b8264f3f 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))); From 716ccf3d9e524dd4b265a4318e094a77746d5957 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Wed, 5 Aug 2026 20:43:44 -0300 Subject: [PATCH 2/3] gtk4-prep: sweep all standalone picker buttons to the picker action definition The earlier fix updated only the single-line dt_action_define_iop calls; multi-line call sites had the same latent shortcut bug. Borders (border and frame line color), watermark, the RGB levels pickers (macro) and the primary colorpicker lib button all used dt_action_def_toggle with a standalone toggle button, so picker shortcuts could not reach their CAPTURE-phase gesture either. Switch them all to dt_action_def_color_picker and drop the synthetic GdkEventButton construction in the shared callback: the picker logic only cares about ctrl/right-click, so pass those as explicit flags from the three activation paths (bauhaus quad, gesture, shortcut). --- src/gui/color_picker_proxy.c | 36 ++++++++++++++++++------------------ src/iop/borders.c | 4 ++-- src/iop/rgblevels.c | 2 +- src/iop/watermark.c | 2 +- src/libs/colorpicker.c | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/gui/color_picker_proxy.c b/src/gui/color_picker_proxy.c index 3bc0ede8e31..11bfa2365c1 100644 --- a/src/gui/color_picker_proxy.c +++ b/src/gui/color_picker_proxy.c @@ -156,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 @@ -175,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 @@ -260,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, @@ -272,10 +274,11 @@ static void _color_picker_clicked(GtkGestureSingle *gesture, GtkWidget *button = dt_gui_get_widget(gesture); // pass the clicked button through to the callback: a secondary click // switches the picker to area mode (as before the gtk4-prep migration) - GdkEvent *event = gdk_event_new(GDK_BUTTON_PRESS); - event->button.button = gtk_gesture_single_get_current_button(gesture); - _color_picker_callback_button_press(button, &event->button, self); - gdk_event_free(event); + _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); } /* @@ -315,15 +318,12 @@ static float _color_picker_widget_toggle(GtkWidget *target, 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)); - 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; - _color_picker_callback_button_press(target, &event->button, self); - gdk_event_free(event); + _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)) 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/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/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/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(); From a426666f8600c2d664b76f114f43110b7aafff14 Mon Sep 17 00:00:00 2001 From: Arecsu Date: Thu, 6 Aug 2026 13:27:15 -0300 Subject: [PATCH 3/3] gtk4-prep: don't let hold shortcuts swallow double/triple presses A key with a hold shortcut (e.g. the default 'a' = force pan/zoom/rotate with mouse) could never trigger a double/triple-press shortcut bound to the same key: every press re-engaged the hold and returned early, so the double/triple press detection never ran. Now the hold press is recorded in the shortcut state, and a fast consecutive press of the same key - while a double/triple press shortcut exists for it (checked via _shortcut_has_double_triple_press()) - falls through to the normal handling and is detected as a double/triple press instead of re-engaging the hold. --- src/gui/accelerators.c | 56 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/src/gui/accelerators.c b/src/gui/accelerators.c index 68f3779a9e7..e6962c0f81e 100644 --- a/src/gui/accelerators.c +++ b/src/gui/accelerators.c @@ -4372,6 +4372,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) @@ -4428,16 +4449,37 @@ 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); + // 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 + { + definition->process(NULL, s->element, DT_ACTION_EFFECT_ON, 1); + + this_key.hold_def = definition; + this_key.hold_element = s->element; - 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); - 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; + return; + } } }