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; + } } } 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/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/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/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/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/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))); 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();