diff --git a/src/bauhaus/bauhaus.c b/src/bauhaus/bauhaus.c index 4bf0788ff3d9..7e72b6189527 100644 --- a/src/bauhaus/bauhaus.c +++ b/src/bauhaus/bauhaus.c @@ -701,27 +701,30 @@ static void _window_motion_handle(GtkWidget *widget, // handlers below are the fallback. For GTK4, the popup should be migrated to // GtkPopover or a GtkWindow with proper event controllers. -static gboolean _window_motion_handler(GtkWidget *widget, GdkEventMotion *event, - gpointer user_data) +static void _window_motion_handler(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) { - _window_motion_handle(widget, - dt_gdk_event_get_root_x(event), - dt_gdk_event_get_root_y(event), - dt_gdk_event_get_state(event)); - return TRUE; + // the popup window is a toplevel, but take root coordinates anyway to stay + // in the same coordinate space as the old GdkEvent handler (GTK4 falls + // back to surface-relative coordinates, see dt_gui_get_event_coords()) + gdouble root_x = 0.0, root_y = 0.0; + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) dt_gui_get_event_coords(event, &root_x, &root_y); + _window_motion_handle(gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller)), + root_x, root_y, + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller))); +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } -static gboolean _popup_leave_handler(GtkWidget *widget, GdkEventCrossing *event, - gpointer user_data) +static void _popup_leave_cb(GtkEventControllerMotion *controller, + gpointer user_data) { + GtkWidget *widget = dt_gui_get_widget(controller); gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_NORMAL, TRUE); - return TRUE; -} - -static gboolean _popup_key_handler(GtkWidget *widget, GdkEventKey *event, - gpointer user_data) -{ - return _popup_key_press(NULL, dt_gdk_event_get_keyval(event), 0, dt_gdk_event_get_state(event), NULL); } static void _popup_button_release_cb(GtkGestureSingle *gesture, @@ -744,18 +747,25 @@ static void _popup_button_press_cb(GtkGestureSingle *gesture, dt_bauhaus_widget_t *w = bh->current; const guint button = gtk_gesture_single_get_current_button(gesture); - // reject clicks that come from outside the popup area window - GdkEvent *cur_event = gtk_get_current_event(); - if(cur_event) + // reject clicks that come from outside the popup area window. Under GTK3 + // the popup holds a grab (gtk_grab_add() in _window_show), so foreign + // clicks still arrive at this gesture and the window comparison rejects + // them. GTK4 removed gtk_grab_add(): pointer events are only delivered + // inside the popup's own surface, so there is nothing foreign to reject -- + // and gtk_widget_get_surface() is not public API; the naive + // gtk_native_get_surface(gtk_widget_get_native()) replacement would always + // return the event's own surface and never fire. +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4: no foreign clicks to reject (see above). */ +#else + const GdkEvent *cur_event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(cur_event + && dt_gdk_event_get_window(cur_event) != gtk_widget_get_window(dt_gui_get_widget(gesture))) { - if(dt_gdk_event_get_window(cur_event) != gtk_widget_get_window(dt_gui_get_widget(gesture))) - { - gdk_event_free(cur_event); - _popup_reject(); - return; - } - gdk_event_free(cur_event); + _popup_reject(); + return; } +#endif if(button == GDK_BUTTON_PRIMARY) { @@ -767,8 +777,8 @@ static void _popup_button_press_cb(GtkGestureSingle *gesture, && !dt_gui_long_click(gdk_event_get_time(gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL)), bh->opentime)) { // counts as double click, reset: - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(!(dt_modifier_is(state, GDK_CONTROL_MASK) && w->field && dt_gui_presets_autoapply_for_module((dt_iop_module_t *)w->module, GTK_WIDGET(w)))) @@ -777,7 +787,7 @@ static void _popup_button_press_cb(GtkGestureSingle *gesture, bh->change_active = TRUE; GtkWidget *w_current = dt_gui_get_widget(gesture); - GdkEvent *current_event = gtk_get_current_event(); + const GdkEvent *current_event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); if(current_event) { GdkModifierType motion_state = dt_gdk_event_get_state(current_event); @@ -786,7 +796,6 @@ static void _popup_button_press_cb(GtkGestureSingle *gesture, dt_gdk_event_get_root_x(current_event), dt_gdk_event_get_root_y(current_event), motion_state); - gdk_event_free(current_event); } } else if(button == GDK_BUTTON_MIDDLE && w->type == DT_BAUHAUS_SLIDER) @@ -1049,9 +1058,9 @@ void dt_bauhaus_init() "moved-to-rect", G_CALLBACK(_window_moved_to_rect), NULL); g_signal_connect(window, "show", G_CALLBACK(_window_show), area); g_signal_connect(area, "draw", G_CALLBACK(_popup_draw), NULL); - g_signal_connect(window, "motion-notify-event", G_CALLBACK(_window_motion_handler), NULL); - g_signal_connect(area, "leave-notify-event", G_CALLBACK(_popup_leave_handler), NULL); - g_signal_connect(area, "key-press-event", G_CALLBACK(_popup_key_handler), NULL); + dt_gui_connect_motion(pop->window, _window_motion_handler, NULL, NULL, NULL); + dt_gui_connect_motion(area, NULL, NULL, _popup_leave_cb, NULL); + dt_gui_connect_key(area, _popup_key_press, NULL); dt_gui_connect_click_all(area, _popup_button_press_cb, _popup_button_release_cb, NULL); dt_gui_connect_scroll(area, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, @@ -3343,12 +3352,14 @@ void dt_bauhaus_widget_show_popup(GtkWidget *widget) if(w->type == DT_BAUHAUS_TOGGLE) return; +#if !GTK_CHECK_VERSION(4, 0, 0) GdkEvent *event = gtk_get_current_event(); if(event) { bh->opentime = gdk_event_get_time(event); gdk_event_free(event); } +#endif bh->mouse_x = 0; bh->mouse_y = 0; _popup_show(widget); @@ -3397,7 +3408,7 @@ static void _widget_scroll(GtkEventControllerScroll *controller, double dx, double dy, GtkWidget *widget) { - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!event || gdk_event_get_event_type(event) != GDK_SCROLL) dt_print(DT_DEBUG_ALWAYS, "[_widget_scroll] called on non-scroll event"); else if(darktable.control->mapping_widget) @@ -3446,7 +3457,9 @@ static void _widget_scroll(GtkEventControllerScroll *controller, } } } +#if !GTK_CHECK_VERSION(4, 0, 0) if(event) gdk_event_free(event); +#endif } static gboolean _widget_key_press(GtkWidget *widget, GdkEventKey *event) @@ -3881,18 +3894,41 @@ static gboolean _popup_key_press(GtkEventControllerKey *controller, break; default: { + /* GTK3 reads the key string from the event; GTK4 dropped the key-event + * string, so derive the unicode character from the keyval instead + * (single-character input only -- composed input goes through GTK4's + * IM handling, not this controller). */ + const gchar *str; +#if GTK_CHECK_VERSION(4, 0, 0) + gchar keybuf[7]; + const gunichar uc = gdk_keyval_to_unicode(keyval); + if(!uc) return FALSE; + const gint len = g_unichar_to_utf8(uc, keybuf); + keybuf[len] = '\0'; + str = keybuf; +#else GdkEvent *event = gtk_get_current_event(); if(!event) return FALSE; - const gchar *str = ((GdkEventKey *)event)->string; + str = ((GdkEventKey *)event)->string; + if(!str) + { + gdk_event_free(event); + return FALSE; + } +#endif if(!str || !g_utf8_validate(str, -1, NULL)) { +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif return FALSE; } const gunichar c = g_utf8_get_char(str); if(!g_unichar_isprint(c)) { +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif return FALSE; } const long int char_width = g_utf8_next_char(str) - str; @@ -3905,7 +3941,9 @@ static gboolean _popup_key_press(GtkEventControllerKey *controller, if(!is_combo) _start_cursor(-1); } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif break; } } @@ -3959,12 +3997,8 @@ static void _widget_button_press(GtkGestureSingle *gesture, } else if(button == GDK_BUTTON_SECONDARY || w->type == DT_BAUHAUS_COMBOBOX) { - GdkEvent *const event = gtk_get_current_event(); - if(event) - { - bh->opentime = gdk_event_get_time(event); - gdk_event_free(event); - } + const GdkEvent *const event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event) bh->opentime = gdk_event_get_time((GdkEvent *)event); bh->mouse_x = x; bh->mouse_y = y; _popup_show(widget); @@ -4057,10 +4091,13 @@ static void _widget_motion(GtkEventControllerMotion *controller, const float steps = floorf((x - bh->mouse_x) / scaled_step); // the current event may be NULL for synthetic motions; fall back to // the global keyboard state in that case - GdkEvent *const event = gtk_get_current_event(); + GdkEvent *const event = + dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); const GdkModifierType motion_state = event ? dt_gdk_event_get_state(event) : dt_key_modifier_state(); +#if !GTK_CHECK_VERSION(4, 0, 0) if(event) gdk_event_free(event); +#endif _slider_add_step(widget, copysignf(1, d->factor) * steps, motion_state, FALSE); diff --git a/src/chart/main.c b/src/chart/main.c index 59de32d50c16..6e6d310a67ac 100644 --- a/src/chart/main.c +++ b/src/chart/main.c @@ -99,7 +99,7 @@ static void get_pixel_region(const image_t *const image, const point_t *const co int *x_end, int *y_end); static void reset_bb(image_t *image); static void free_image(image_t *image); -static gboolean handle_motion(GtkWidget *widget, gdouble mx, gdouble my, dt_lut_t *self, image_t *image); +static gboolean handle_motion(GtkEventControllerMotion *controller, gdouble mx, gdouble my, dt_lut_t *self, image_t *image); static int find_closest_corner(point_t *bb, float x, float y); static void map_mouse_to_0_1(GtkWidget *widget, gdouble mx, gdouble my, image_t *image, float *x, float *y); static void update_corner(image_t *image, int which, float *x, float *y); @@ -176,8 +176,7 @@ static void _motion_cb_source(GtkEventControllerMotion *controller, gdouble y, dt_lut_t *self) { - GtkWidget *widget = dt_gui_get_widget(controller); - const gboolean res = handle_motion(widget, x, y, self, &self->source); + const gboolean res = handle_motion(controller, x, y, self, &self->source); if(res) { collect_source_patches(self); @@ -190,8 +189,7 @@ static void _motion_cb_reference(GtkEventControllerMotion *controller, gdouble y, dt_lut_t *self) { - GtkWidget *widget = dt_gui_get_widget(controller); - const gboolean res = handle_motion(widget, x, y, self, &self->reference); + const gboolean res = handle_motion(controller, x, y, self, &self->reference); if(res) { collect_reference_patches(self); @@ -199,10 +197,11 @@ static void _motion_cb_reference(GtkEventControllerMotion *controller, } } -static gboolean handle_motion(GtkWidget *widget, gdouble mx, gdouble my, dt_lut_t *self, image_t *image) +static gboolean handle_motion(GtkEventControllerMotion *controller, gdouble mx, gdouble my, dt_lut_t *self, image_t *image) { - GdkModifierType state; - gtk_get_current_event_state(&state); + GtkWidget *widget = dt_gui_get_widget(controller); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); if(!(state & GDK_BUTTON1_MASK) || !image->image) return FALSE; // mouse -> 0..1 diff --git a/src/common/gdk_event_utils.h b/src/common/gdk_event_utils.h index d2faa9796c60..9e48d50c786f 100644 --- a/src/common/gdk_event_utils.h +++ b/src/common/gdk_event_utils.h @@ -58,41 +58,75 @@ static inline guint dt_gdk_event_get_click_count(const void *e) static inline GdkModifierType dt_gdk_event_get_state(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_event_get_modifier_state((const GdkEvent *)e); +#else GdkModifierType s = 0; gdk_event_get_state((const GdkEvent *)e, &s); return s; +#endif } static inline gdouble dt_gdk_event_get_x(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble x = 0, y = 0; + gdk_event_get_position((const GdkEvent *)e, &x, &y); + (void)y; + return x; +#else gdouble x = 0, y = 0; gdk_event_get_coords((const GdkEvent *)e, &x, &y); (void)y; return x; +#endif } static inline gdouble dt_gdk_event_get_y(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble x = 0, y = 0; + gdk_event_get_position((const GdkEvent *)e, &x, &y); + (void)x; + return y; +#else gdouble x = 0, y = 0; gdk_event_get_coords((const GdkEvent *)e, &x, &y); (void)x; return y; +#endif } static inline gdouble dt_gdk_event_get_root_x(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4 has no root (screen-absolute) coordinates; fall back to the + // surface-relative position (delta-based consumers are unaffected). + gdouble x = 0, y = 0; + gdk_event_get_position((const GdkEvent *)e, &x, &y); + (void)y; + return x; +#else gdouble x = 0, y = 0; gdk_event_get_root_coords((const GdkEvent *)e, &x, &y); (void)y; return x; +#endif } static inline gdouble dt_gdk_event_get_root_y(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble x = 0, y = 0; + gdk_event_get_position((const GdkEvent *)e, &x, &y); + (void)x; + return y; +#else gdouble x = 0, y = 0; gdk_event_get_root_coords((const GdkEvent *)e, &x, &y); (void)x; return y; +#endif } static inline guint dt_gdk_event_get_keyval(const void *e) @@ -111,6 +145,9 @@ static inline guint16 dt_gdk_event_get_keycode(const void *e) static inline GdkScrollDirection dt_gdk_event_get_scroll_direction(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_scroll_event_get_direction((GdkEvent *)e); +#else GdkScrollDirection d = GDK_SCROLL_UP; if(!gdk_event_get_scroll_direction((const GdkEvent *)e, &d)) { @@ -122,22 +159,46 @@ static inline GdkScrollDirection dt_gdk_event_get_scroll_direction(const void *e d = GDK_SCROLL_SMOOTH; } return d; +#endif } static inline gdouble dt_gdk_event_get_scroll_delta_x(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble dx = 0, dy = 0; + gdk_scroll_event_get_deltas((GdkEvent *)e, &dx, &dy); + (void)dy; + return dx; +#else gdouble dx = 0, dy = 0; gdk_event_get_scroll_deltas((const GdkEvent *)e, &dx, &dy); (void)dy; return dx; +#endif } static inline gdouble dt_gdk_event_get_scroll_delta_y(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble dx = 0, dy = 0; + gdk_scroll_event_get_deltas((GdkEvent *)e, &dx, &dy); + (void)dx; + return dy; +#else gdouble dx = 0, dy = 0; gdk_event_get_scroll_deltas((const GdkEvent *)e, &dx, &dy); (void)dx; return dy; +#endif +} + +static inline gboolean dt_gdk_event_is_scroll_stop(const void *e) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_scroll_event_is_stop((GdkEvent *)e); +#else + return gdk_event_is_scroll_stop_event((const GdkEvent *)e); +#endif } static inline GdkWindow *dt_gdk_event_get_window(const void *e) @@ -152,7 +213,13 @@ static inline GdkDevice *dt_gdk_event_get_device(const void *e) static inline GdkDevice *dt_gdk_event_get_source_device(const void *e) { +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4 dropped the separate "source device": the event device is the + // source (the GdkDeviceTool distinguishes pen/eraser on top of it). + return gdk_event_get_device((const GdkEvent *)e); +#else return gdk_event_get_source_device((const GdkEvent *)e); +#endif } static inline GdkScreen *dt_gdk_event_get_screen(const void *e) @@ -177,4 +244,56 @@ static inline gboolean dt_gdk_event_get_axis(const void *e, return gdk_event_get_axis((const GdkEvent *)e, axis_use, value); } +/* --- touchpad gesture events --- + * GTK3 exposes the GdkEventTouchpadPinch/GdkEventTouchpadSwipe structs, GTK4 + * the gdk_touchpad_event_* accessors; these wrappers keep both callable. */ + +static inline GdkTouchpadGesturePhase dt_gdk_touchpad_pinch_get_phase(const void *e) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_touchpad_event_get_gesture_phase((GdkEvent *)e); +#else + return ((const GdkEvent *)e)->touchpad_pinch.phase; +#endif +} + +static inline guint dt_gdk_touchpad_pinch_get_n_fingers(const void *e) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_touchpad_event_get_n_fingers((GdkEvent *)e); +#else + return ((const GdkEvent *)e)->touchpad_pinch.n_fingers; +#endif +} + +static inline void dt_gdk_touchpad_pinch_get_deltas(const void *e, + gdouble *dx, + gdouble *dy) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_touchpad_event_get_deltas((GdkEvent *)e, dx, dy); +#else + if(dx) *dx = ((const GdkEvent *)e)->touchpad_pinch.dx; + if(dy) *dy = ((const GdkEvent *)e)->touchpad_pinch.dy; +#endif +} + +static inline gdouble dt_gdk_touchpad_pinch_get_scale(const void *e) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_touchpad_event_get_pinch_scale((GdkEvent *)e); +#else + return ((const GdkEvent *)e)->touchpad_pinch.scale; +#endif +} + +static inline GdkTouchpadGesturePhase dt_gdk_touchpad_swipe_get_phase(const void *e) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gdk_touchpad_event_get_gesture_phase((GdkEvent *)e); +#else + return ((const GdkEvent *)e)->touchpad_swipe.phase; +#endif +} + G_END_DECLS diff --git a/src/develop/blend_gui.c b/src/develop/blend_gui.c index cde3d520b52d..503ddd787dda 100644 --- a/src/develop/blend_gui.c +++ b/src/develop/blend_gui.c @@ -2223,8 +2223,8 @@ static void _blendop_blendif_enter_cb(GtkEventControllerMotion *controller, dt_dev_pixelpipe_display_mask_t mode = DT_DEV_PIXELPIPE_DISPLAY_NONE; - GdkModifierType state; - if(gtk_get_current_event_state(&state)) + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); { // depending on shift modifiers we activate channel and/or mask display if(dt_modifier_is(state, GDK_SHIFT_MASK | GDK_CONTROL_MASK)) @@ -3506,8 +3506,10 @@ void dt_iop_gui_init_blending(GtkWidget *iopw, bd->masks_modes_toggles = g_list_append(bd->masks_modes_toggles, GTK_WIDGET(but)); } - GtkWidget *presets_button = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL); - gtk_widget_set_tooltip_text(presets_button, _("blending options")); + GtkWidget *presets_button = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("blending options"), + }); if(bd->blendif_support) { g_signal_connect(G_OBJECT(presets_button), "clicked", diff --git a/src/develop/imageop.c b/src/develop/imageop.c index e48902d71d8f..51b068078c55 100644 --- a/src/develop/imageop.c +++ b/src/develop/imageop.c @@ -1034,21 +1034,9 @@ void _get_multi_show(dt_iop_module_t *module, multi_show->down = 0; } -static gboolean _gui_multiinstance_callback(GtkButton *button, - GdkEventButton *event, - dt_iop_module_t *module) +static void _gui_multiinstance_callback(GtkButton *button, + dt_iop_module_t *module) { - if(event && dt_gdk_event_get_button(event) == GDK_BUTTON_SECONDARY) - { - if(!(module->flags() & IOP_FLAGS_ONE_INSTANCE)) - _gui_copy_callback(button, module); - return TRUE; - } - else if(event && dt_gdk_event_get_button(event) == GDK_BUTTON_MIDDLE) - { - return FALSE; - } - dt_iop_gui_multi_show_t multi_show; _get_multi_show(module, &multi_show); @@ -1102,8 +1090,6 @@ static gboolean _gui_multiinstance_callback(GtkButton *button, // make sure the button is deactivated now that the menu is opened if(button) dtgtk_button_set_active(DTGTK_BUTTON(button), FALSE); - - return TRUE; } static void _gui_off_button_clicked(GtkGestureSingle *gesture, @@ -1143,7 +1129,7 @@ static void _gui_multiinstance_clicked(GtkGestureSingle *gesture, if(button == GDK_BUTTON_MIDDLE) return; - _gui_multiinstance_callback(GTK_BUTTON(dt_gui_get_widget(gesture)), NULL, module); + _gui_multiinstance_callback(GTK_BUTTON(dt_gui_get_widget(gesture)), module); } static gboolean _rename_module_key_pressed(GtkEventControllerKey *controller, @@ -2618,10 +2604,12 @@ static void _presets_scrolled(GtkEventControllerScroll *controller, const int delta = fabs(dx) > fabs(dy) ? (int)dx : (int)dy; if(delta == 0) return; - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); const gboolean smooth = event && dt_gdk_event_get_scroll_direction(event) == GDK_SCROLL_SMOOTH; +#if !GTK_CHECK_VERSION(4, 0, 0) if(event) gdk_event_free(event); +#endif GtkWidget *widget = dt_gui_get_widget(controller); dt_presets_scroll_t *state @@ -2876,17 +2864,22 @@ static void _iop_plugin_header_released(GtkGestureSingle *gesture, if(n_press >= 2) return; // ignore clicks on buttons inside the header (presets, reset, enable, multiinstance) - // GTK4: use gtk_gesture_get_last_event(gesture) instead of gtk_get_current_event() - GdkEvent *event = gtk_get_current_event(); - if(event) - { - if(GTK_IS_BUTTON(gtk_get_event_widget(event))) - { - gdk_event_free(event); - return; - } - gdk_event_free(event); - } +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4 has no gtk_get_event_widget(): find the widget under the release + * point inside the header instead, and ignore the release if it lands on + * (or inside) a button -- the gesture receives releases over the whole + * header, including its child buttons (e.g. a press started on the header + * and released over a button). The surface-compare alternative is wrong: + * gdk_event_get_surface() always returns this widget's own surface. */ + GtkWidget *const header = dt_gui_get_widget(gesture); + GtkWidget *target = gtk_widget_pick(header, x, y, GTK_PICK_DEFAULT); + for(; target && target != header; target = gtk_widget_get_parent(target)) + if(GTK_IS_BUTTON(target)) return; +#else + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event && GTK_IS_BUTTON(gtk_get_event_widget((GdkEvent *)event))) + return; +#endif const guint button = gtk_gesture_single_get_current_button(gesture); @@ -3300,18 +3293,19 @@ GtkWidget *dt_iop_gui_header_button(dt_iop_module_t *module, if(element == DT_ACTION_ELEMENT_ENABLE) { - button = dtgtk_togglebutton_new(paint, 0, module); - char tooltip[512]; gchar *module_label = dt_history_item_get_name(module); snprintf(tooltip, sizeof(tooltip), module->enabled ? _("'%s' is switched on") : _("'%s' is switched off"), module_label); g_free(module_label); - gtk_widget_set_tooltip_text(button, tooltip); + button = dtgtk_togglebutton_new_full(paint, 0, module, + &(dtgtk_button_config_t){ + .tooltip = tooltip, + .toggled_cb = G_CALLBACK(_gui_off_callback), + .toggled_data = module, + }); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), module->enabled); - - g_signal_connect(button, "toggled", G_CALLBACK(_gui_off_callback), module); gtk_box_pack_start(GTK_BOX(header), button, FALSE, FALSE, 0); } else @@ -4364,7 +4358,7 @@ static float _action_process(gpointer target, _gui_delete_callback (NULL, module); else if(effect == DT_ACTION_EFFECT_RENAME ) _gui_rename_callback (NULL, module); - else _gui_multiinstance_callback(NULL, NULL, module); + else _gui_multiinstance_callback(NULL, module); break; case DT_ACTION_ELEMENT_RESET: { diff --git a/src/develop/imageop_gui.c b/src/develop/imageop_gui.c index 48727b9477ec..9ace8e112617 100644 --- a/src/develop/imageop_gui.c +++ b/src/develop/imageop_gui.c @@ -300,8 +300,10 @@ GtkWidget *dt_iop_button_new(dt_iop_module_t *self, const gchar *label, if(paint) { - button = dtgtk_button_new(paint, paintflags, NULL); - gtk_widget_set_tooltip_text(button, Q_(label)); + button = dtgtk_button_new_full(paint, paintflags, NULL, + &(dtgtk_button_config_t){ + .tooltip = Q_(label), + }); } else { diff --git a/src/dtgtk/button.c b/src/dtgtk/button.c index ea36837e4034..47d21cb220f2 100644 --- a/src/dtgtk/button.c +++ b/src/dtgtk/button.c @@ -18,6 +18,7 @@ #include "button.h" #include "bauhaus/bauhaus.h" +#include "gui/accelerators.h" #include "gui/gtk.h" #include @@ -160,6 +161,29 @@ GtkWidget *dtgtk_button_new(DTGTKCairoPaintIconFunc paint, return (GtkWidget *)button; } +GtkWidget *dtgtk_button_new_full(DTGTKCairoPaintIconFunc paint, + gint paintflags, + void *paintdata, + const dtgtk_button_config_t *config) +{ + GtkWidget *button = dtgtk_button_new(paint, paintflags, paintdata); + if(!config) return button; + if(config->tooltip) + gtk_widget_set_tooltip_text(button, config->tooltip); + else if(config->tooltip_markup) + gtk_widget_set_tooltip_markup(button, config->tooltip_markup); + if(config->action) + dt_action_define(config->action, config->action_section, + config->action_label, button, config->action_def); + if(config->clicked_cb) + /* g_signal_connect() is a type-checking macro that token-pastes the + * handler name, so the generic factory has to go through + * g_signal_connect_data() directly. */ + g_signal_connect_data(G_OBJECT(button), "clicked", config->clicked_cb, + config->clicked_data, NULL, (GConnectFlags) 0); + return button; +} + void dtgtk_button_connect_stale_hover_cleanup(GtkWidget *widget) { g_signal_connect(G_OBJECT(widget), "grab-notify", diff --git a/src/dtgtk/button.h b/src/dtgtk/button.h index eac714817659..18341a2e1dd6 100644 --- a/src/dtgtk/button.h +++ b/src/dtgtk/button.h @@ -21,6 +21,10 @@ #include "paint.h" #include +/* forward declarations, see gui/accelerators.h for the real definitions */ +struct dt_action_t; +typedef struct dt_action_def_t dt_action_def_t; + G_BEGIN_DECLS #define DTKGTK_TYPE_BUTTON dtgtk_button_get_type() @@ -41,8 +45,51 @@ struct _GtkDarktableButton GtkWidget *canvas; }; -/** instantiate a new darktable button control passing paint function as content */ +/** instantiate a new darktable button control passing paint function as content; + * for the tooltip/action/click wiring variant see dtgtk_button_new_full(). */ GtkWidget *dtgtk_button_new(DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata); + +/** instantiate a new darktable button and wire the common extras: a tooltip, + * an action definition and a primary-click callback. paint/paintflags/ + * paintdata are mandatory; the wiring is optional and passed as a config + * struct so callers can name the fields they use (designated initializers, + * everything left out stays off). This is the convenience constructor for + * the plain button idiom (dtgtk_button_new + tooltip + dt_action_define + + * "clicked"); when a press/release gesture or a secondary click is needed, + * call dt_gui_connect_click()/dt_gui_connect_click_secondary() on the + * result instead of passing a clicked callback. */ +typedef struct dtgtk_button_config_t +{ + /** tooltip text, NULL for none */ + const gchar *tooltip; + /** tooltip markup (plain text wins if both are set), NULL for none */ + const gchar *tooltip_markup; + /** action owner for dt_action_define(), NULL for none; pass DT_ACTION(x) + * or &module->actions -- iop modules dispatch to dt_action_define_iop() */ + struct dt_action_t *action; + /** action section (submenu), usually NULL */ + const gchar *action_section; + /** action label, NULL defines an anonymous action */ + const gchar *action_label; + /** action definition, e.g. &dt_action_def_button */ + const dt_action_def_t *action_def; + /** "clicked" callback, NULL for none; connected by + * dtgtk_button_new_full(), and by dtgtk_togglebutton_new_full() for the + * toggles that use a plain click */ + GCallback clicked_cb; + /** user data passed to clicked_cb */ + gpointer clicked_data; + /** "toggled" callback, NULL for none; connected by + * dtgtk_togglebutton_new_full() */ + GCallback toggled_cb; + /** user data passed to toggled_cb */ + gpointer toggled_data; +} dtgtk_button_config_t; + +GtkWidget *dtgtk_button_new_full(DTGTKCairoPaintIconFunc paint, + gint paintflags, + void *paintdata, + const dtgtk_button_config_t *config); /** 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 diff --git a/src/dtgtk/culling.c b/src/dtgtk/culling.c index df01c3a407ef..33dd8255ebc3 100644 --- a/src/dtgtk/culling.c +++ b/src/dtgtk/culling.c @@ -629,26 +629,30 @@ static void _update_selected_thumbnail(dt_culling_t *table, } } -static gboolean _event_gesture(GtkWidget *widget, - GdkEvent *event, - gpointer user_data) +/* touchpad pinch via dt_gui_connect_pinch: the old "event" signal handler + * (GTK3-only) forwarded raw GDK_TOUCHPAD_PINCH events; the phase field + * becomes the begin / scale-changed / end signals ("end" fires on cancel + * too, so the view's END/CANCEL reset still runs). */ +static void _event_pinch(GtkGesture *gesture, + const dt_gui_pinch_event_t *e, + gpointer user_data) { - if(dt_gdk_event_get_type(event) != GDK_TOUCHPAD_PINCH) return FALSE; - const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; + (void)user_data; + GtkWidget *widget = dt_gui_get_widget(gesture); + dt_print(DT_DEBUG_INPUT, "[culling gesture] pinch phase=%d x=%.1f y=%.1f dx=%.3f dy=%.3f scale=%.6f state=0x%x", - pinch->phase, pinch->x, pinch->y, pinch->dx, pinch->dy, pinch->scale, pinch->state); - // Forward root (screen-absolute) coordinates — same convention as _event_scroll + e->phase, e->x, e->y, e->dx, e->dy, e->scale, e->state); + // Forward root (screen-absolute) coordinates -- same convention as _event_scroll // passing e->x_root, e->y_root. Using pinch->x_root avoids a manual conversion // via gdk_window_get_origin that was producing the wrong focal point. - dt_view_manager_gesture_pinch(darktable.view_manager, - pinch->x_root, pinch->y_root, - pinch->dx, pinch->dy, - pinch->phase, - pinch->scale, - pinch->state & 0xf); - gtk_widget_queue_draw(widget); - return TRUE; + if(dt_view_manager_gesture_pinch(darktable.view_manager, + e->x, e->y, + e->dx, e->dy, + e->phase, + e->scale, + e->state)) + gtk_widget_queue_draw(widget); } // zoom direction for a scroll: see dt_gui_scroll_zoom_delta() in gtk.c for @@ -659,7 +663,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, gdouble dy, dt_culling_t *table) { - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!event) return; GdkDevice *device = dt_gdk_event_get_source_device(event); @@ -695,7 +699,9 @@ static void _event_scroll(GtkEventControllerScroll *controller, // stop event at all.) if(direction == GDK_SCROLL_SMOOTH && is_stop) { + #if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif dt_culling_zoom_end(table); return; } @@ -707,10 +713,10 @@ static void _event_scroll(GtkEventControllerScroll *controller, // 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) + if(dt_gui_get_scroll_deltas(event, &ddx, &ddy) && (ddx != 0.0 || ddy != 0.0)) { - const float zoom_delta = dt_gui_scroll_zoom_delta((const GdkEventScroll *)event, ddx, ddy); + const float zoom_delta = dt_gui_scroll_zoom_delta(event, ddx, ddy); // convert screen to culling coordinates int ox = 0, oy = 0; GdkWindow *win = gtk_widget_get_window(table->widget); @@ -724,7 +730,9 @@ static void _event_scroll(GtkEventControllerScroll *controller, if(fabsf(zoom_delta) > 0.001f) _thumbs_zoom_add(table, zoom_delta, x_culling, y_culling, state, TRUE); } + #if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif return; } @@ -733,7 +741,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, // dt_gui_scroll_should_pan() restricts this to touchpad-sourced events, so // mouse wheels always navigate between images, even at 100% zoom (GTK4 can // deliver wheel scrolls as smooth events too). - if(dt_gui_scroll_should_pan((const GdkEventScroll *)event)) + if(dt_gui_scroll_should_pan(event)) { // Check if any thumbnail is zoomed in; if so, pan instead of navigate. float fz = 1.0f; @@ -748,7 +756,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, if(fz > 1.0f) { gdouble ddx = 0.0, ddy = 0.0; - if(dt_gui_get_scroll_deltas((const GdkEventScroll *)event, &ddx, &ddy) + if(dt_gui_get_scroll_deltas(event, &ddx, &ddy) && (ddx != 0.0 || ddy != 0.0)) { // raw platform deltas; scale to pixel-scale (matches the factor @@ -763,13 +771,15 @@ static void _event_scroll(GtkEventControllerScroll *controller, { dt_print(DT_DEBUG_INPUT, "[culling scroll] smooth pan: no delta"); } - gdk_event_free(event); + #if !GTK_CHECK_VERSION(4, 0, 0) + gdk_event_free(event); +#endif return; } } // discrete scroll: use unit deltas for zoom/navigation - const GdkEventScroll *scroll_event = (const GdkEventScroll *)event; + const GdkEvent *scroll_event = event; int delta_x = 0, delta_y = 0; if(dt_gui_get_scroll_unit_deltas(scroll_event, &delta_x, &delta_y)) { @@ -803,7 +813,9 @@ static void _event_scroll(GtkEventControllerScroll *controller, _thumbs_move(table, move); } } - gdk_event_free(event); + #if !GTK_CHECK_VERSION(4, 0, 0) + gdk_event_free(event); +#endif } static gboolean _event_draw(GtkWidget *widget, @@ -843,18 +855,27 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, * 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(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR + && gdk_crossing_event_get_mode(event) != GDK_CROSSING_GTK_GRAB + && gdk_crossing_event_get_mode(event) != GDK_CROSSING_GRAB + && !dt_gui_pointer_is_grabbed()) +#else if(event->crossing.detail != GDK_NOTIFY_INFERIOR && event->crossing.mode != GDK_CROSSING_GTK_GRAB && event->crossing.mode != GDK_CROSSING_GRAB && !dt_gui_pointer_is_grabbed()) +#endif { table->mouse_inside = FALSE; dt_control_set_mouse_over_id(NO_IMGID); } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } } @@ -886,15 +907,10 @@ static void _event_enter_cb(GtkEventControllerMotion *controller, * leave a stale hovered image (see #21729). */ table->mouse_inside = TRUE; - GdkEvent *event = gtk_get_current_event(); - if(event) - { - /* GTK4 migration: drop the pointer-grab check (see - * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ - if(!dt_gui_pointer_is_grabbed()) - dt_control_set_mouse_over_id(_culling_image_at_pos(table, x, y)); - gdk_event_free(event); - } + /* GTK4 migration: drop the pointer-grab check (see + * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ + if(!dt_gui_pointer_is_grabbed()) + dt_control_set_mouse_over_id(_culling_image_at_pos(table, x, y)); } static void _event_button_press_cb(GtkGestureSingle *gesture, @@ -914,8 +930,8 @@ static void _event_button_press_cb(GtkGestureSingle *gesture, if(button == GDK_BUTTON_MIDDLE) { // if shift is pressed, we work only with image hovered - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(dt_modifier_is(state, GDK_SHIFT_MASK)) _toggle_zoom_current(table, x, y); else @@ -944,7 +960,8 @@ static void _event_button_press_cb(GtkGestureSingle *gesture, } // start panning — need root coordinates for pan tracking - dt_gui_get_current_root_coords(&table->pan_x, &table->pan_y); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event) dt_gui_get_event_coords(event, &table->pan_x, &table->pan_y); table->panning = TRUE; } @@ -955,7 +972,8 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, { // get root coordinates for pan tracking gdouble root_x = 0, root_y = 0; - dt_gui_get_current_root_coords(&root_x, &root_y); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) dt_gui_get_event_coords(event, &root_x, &root_y); table->mouse_inside = TRUE; @@ -972,6 +990,9 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, { table->pan_x = root_x; table->pan_y = root_y; +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif return; } @@ -979,7 +1000,12 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, const int max_in_memory_images = _get_max_in_memory_images(); if(table->mode == DT_CULLING_MODE_CULLING && table->thumbs_count > max_in_memory_images) + { +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif return; + } float fz = 1.0f; for(GList *l = table->list; l; l = g_list_next(l)) @@ -995,8 +1021,8 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, const float valx = (root_x - table->pan_x) * scale; const float valy = (root_y - table->pan_y) * scale; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); if(dt_modifier_is(state, GDK_SHIFT_MASK)) { const dt_imgid_t mouseid = dt_control_get_mouse_over_id(); @@ -1051,6 +1077,9 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, dt_thumbnail_t *th = l->data; dt_thumbnail_image_refresh_position(th); } +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } static void _event_button_release_cb(GtkGestureSingle *gesture, @@ -1291,8 +1320,11 @@ dt_culling_t *dt_culling_new(const dt_culling_mode_t mode) dt_gui_add_class(table->widget, "dt_transparent_background"); gtk_widget_set_can_focus(table->widget, TRUE); - g_signal_connect(G_OBJECT(table->widget), "event", - G_CALLBACK(_event_gesture), table); + // touchpad pinch via dt_gui_connect_pinch (the old "event" signal handler + // is GTK3-only); the culling widget also keeps its own draw/scroll/motion/click + // controllers below + dt_gui_connect_pinch(table->widget, _event_pinch, table); + dt_gui_connect_scroll(table->widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, _event_scroll, table); g_signal_connect(G_OBJECT(table->widget), "draw", diff --git a/src/dtgtk/gradientslider.c b/src/dtgtk/gradientslider.c index 602d06f16adc..122fc2fd3c93 100644 --- a/src/dtgtk/gradientslider.c +++ b/src/dtgtk/gradientslider.c @@ -15,8 +15,6 @@ You should have received a copy of the GNU General Public License along with darktable. If not, see . */ -#include "common/gdk_event_utils.h" - #include #include #include @@ -47,18 +45,35 @@ static gboolean _gradient_slider_draw(GtkWidget *widget, cairo_t *cr); static void _gradient_slider_dispose(GObject *object); // Events -static gboolean _gradient_slider_enter_notify_event(GtkWidget *widget, - GdkEventCrossing *event); -static gboolean _gradient_slider_button_press(GtkWidget *widget, - GdkEventButton *event); -static gboolean _gradient_slider_button_release(GtkWidget *widget, - GdkEventButton *event); -static gboolean _gradient_slider_motion_notify(GtkWidget *widget, - GdkEventMotion *event); -static gboolean _gradient_slider_scroll_event(GtkWidget *widget, - GdkEventScroll *event); -static gboolean _gradient_slider_key_press_event(GtkWidget *widget, - GdkEventKey *event); +static void _gradient_slider_enter(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data); +static void _gradient_slider_leave(GtkEventControllerMotion *controller, + gpointer user_data); +static void _gradient_slider_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data); +static void _gradient_slider_button_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data); +static void _gradient_slider_motion(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data); +static void _gradient_slider_scroll(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data); +static gboolean _gradient_slider_key_pressed(GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer user_data); enum { @@ -274,23 +289,22 @@ static float _default_linear_scale_callback(GtkWidget *self, return value; } -static gboolean _gradient_slider_enter_notify_event(GtkWidget *widget, - GdkEventCrossing *event) +static void _gradient_slider_enter(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), FALSE); - + GtkWidget *widget = dt_gui_get_widget(controller); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, TRUE); gslider->is_entered = TRUE; gtk_widget_queue_draw(widget); - return FALSE; } -static gboolean _gradient_slider_leave_notify_event(GtkWidget *widget, - GdkEventCrossing *event) +static void _gradient_slider_leave(GtkEventControllerMotion *controller, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), FALSE); - + GtkWidget *widget = dt_gui_get_widget(controller); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); if(!(gslider->is_dragging)) { @@ -299,18 +313,20 @@ static gboolean _gradient_slider_leave_notify_event(GtkWidget *widget, gslider->active = -1; gtk_widget_queue_draw(widget); } - return FALSE; } -static gboolean _gradient_slider_button_press(GtkWidget *widget, - GdkEventButton *event) +static void _gradient_slider_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), FALSE); - + GtkWidget *widget = dt_gui_get_widget(gesture); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); + const guint button = gtk_gesture_single_get_current_button(gesture); - // reset slider - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY && dt_gdk_event_get_type(event) == GDK_2BUTTON_PRESS && gslider->is_resettable) + // reset slider on double click + if(button == GDK_BUTTON_PRIMARY && n_press == 2 && gslider->is_resettable) { gslider->is_dragging = FALSE; gslider->do_reset = TRUE; @@ -320,19 +336,19 @@ static gboolean _gradient_slider_button_press(GtkWidget *widget, g_signal_emit_by_name(G_OBJECT(widget), "value-changed"); g_signal_emit_by_name(G_OBJECT(widget), "value-reset"); } - else if((dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY || dt_gdk_event_get_button(event) == GDK_BUTTON_SECONDARY) && dt_gdk_event_get_type(event) == GDK_BUTTON_PRESS) + else if((button == GDK_BUTTON_PRIMARY || button == GDK_BUTTON_SECONDARY) && n_press == 1) { - const gint lselected = _get_active_marker_from_screen(widget, dt_gdk_event_get_x(event), dt_gdk_event_get_y(event)); + const gint lselected = _get_active_marker_from_screen(widget, x, y); assert(lselected >= 0); assert(lselected <= gslider->positions - 1); - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) // left mouse button : select and start dragging + if(button == GDK_BUTTON_PRIMARY) // left mouse button : select and start dragging { gslider->selected = lselected; gslider->do_reset = FALSE; - const gdouble newposition = _get_position_from_screen(widget, dt_gdk_event_get_x(event)); + const gdouble newposition = _get_position_from_screen(widget, x); const gint direction = gslider->position[gslider->selected] <= newposition ? MOVE_RIGHT : MOVE_LEFT; _slider_move(widget, gslider->selected, newposition, direction); @@ -359,22 +375,21 @@ static gboolean _gradient_slider_button_press(GtkWidget *widget, gtk_widget_queue_draw(widget); } } - - return TRUE; } -static gboolean _gradient_slider_motion_notify(GtkWidget *widget, - GdkEventMotion *event) +static void _gradient_slider_motion(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), FALSE); - + GtkWidget *widget = dt_gui_get_widget(controller); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); if(gslider->is_dragging == TRUE && gslider->selected != -1 && gslider->do_reset == FALSE) { assert(gslider->timeout_handle > 0); - const gdouble newposition = _get_position_from_screen(widget, dt_gdk_event_get_x(event)); + const gdouble newposition = _get_position_from_screen(widget, x); const gint direction = gslider->position[gslider->selected] <= newposition ? MOVE_RIGHT : MOVE_LEFT; _slider_move(widget, gslider->selected, newposition, direction); @@ -385,27 +400,28 @@ static gboolean _gradient_slider_motion_notify(GtkWidget *widget, } else { - gslider->active = _get_active_marker_from_screen(widget, dt_gdk_event_get_x(event), dt_gdk_event_get_y(event)); + gslider->active = _get_active_marker_from_screen(widget, x, y); } if(gslider->selected != -1) gtk_widget_grab_focus(widget); - - return TRUE; } -static gboolean _gradient_slider_button_release(GtkWidget *widget, - GdkEventButton *event) +static void _gradient_slider_button_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), FALSE); - + GtkWidget *widget = dt_gui_get_widget(gesture); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); const gint selected = _get_active_marker(gslider); - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY && selected != -1 && gslider->do_reset == FALSE) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY + && selected != -1 && gslider->do_reset == FALSE) { // First get some dimension info gslider->is_changed = TRUE; - const gdouble newposition = _get_position_from_screen(widget, dt_gdk_event_get_x(event)); + const gdouble newposition = _get_position_from_screen(widget, x); const gint direction = gslider->position[selected] <= newposition ? MOVE_RIGHT : MOVE_LEFT; _slider_move(widget, selected, newposition, direction); @@ -417,43 +433,41 @@ static gboolean _gradient_slider_button_release(GtkWidget *widget, gslider->timeout_handle = 0; g_signal_emit_by_name(G_OBJECT(widget), "value-changed"); } - return TRUE; } -static gboolean _gradient_slider_scroll_event(GtkWidget *widget, - GdkEventScroll *event) +static void _gradient_slider_scroll(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), TRUE); - - if(dt_gui_ignore_scroll(event)) return FALSE; - + GtkWidget *widget = dt_gui_get_widget(controller); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); const gint selected = _get_active_marker(gslider); - if(selected == -1) return TRUE; + if(selected == -1) return; gtk_widget_grab_focus(widget); - int delta_y; - if(dt_gui_get_scroll_unit_delta(event, &delta_y)) - { - gdouble delta = delta_y * -gslider->increment; - return _gradient_slider_add_delta_internal(widget, delta, - dt_gdk_event_get_state(event), selected); - } + // the DISCRETE proxy already accumulated smooth deltas into unit steps + const int delta_y = (int)dy; + if(delta_y == 0) return; - return TRUE; + _gradient_slider_add_delta_internal(widget, delta_y * -gslider->increment, + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)), + selected); } -static gboolean _gradient_slider_key_press_event(GtkWidget *widget, - GdkEventKey *event) +static gboolean _gradient_slider_key_pressed(GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer user_data) { - g_return_val_if_fail(DTGTK_IS_GRADIENT_SLIDER(widget), TRUE); - + GtkWidget *widget = dt_gui_get_widget(controller); GtkDarktableGradientSlider *gslider = DTGTK_GRADIENT_SLIDER(widget); int handled = FALSE; float delta = -gslider->increment; - switch(dt_gdk_event_get_keyval(event)) + switch(keyval) { case GDK_KEY_Up: case GDK_KEY_KP_Up: @@ -472,8 +486,7 @@ static gboolean _gradient_slider_key_press_event(GtkWidget *widget, const gint selected = _get_active_marker(gslider); if(selected == -1) return TRUE; - return _gradient_slider_add_delta_internal(widget, delta, - dt_gdk_event_get_state(event), selected); + return _gradient_slider_add_delta_internal(widget, delta, state, selected); } static void _gradient_slider_class_init(GtkDarktableGradientSliderClass *klass) @@ -486,14 +499,6 @@ static void _gradient_slider_class_init(GtkDarktableGradientSliderClass *klass) GObjectClass *object_class = (GObjectClass *)klass; object_class->dispose = _gradient_slider_dispose; - widget_class->enter_notify_event = _gradient_slider_enter_notify_event; - widget_class->leave_notify_event = _gradient_slider_leave_notify_event; - widget_class->button_press_event = _gradient_slider_button_press; - widget_class->button_release_event = _gradient_slider_button_release; - widget_class->motion_notify_event = _gradient_slider_motion_notify; - widget_class->scroll_event = _gradient_slider_scroll_event; - widget_class->key_press_event = _gradient_slider_key_press_event; - _signals[VALUE_CHANGED] = g_signal_new("value-changed", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, 0, @@ -526,6 +531,20 @@ static void _gradient_slider_init(GtkDarktableGradientSlider *gslider) gtk_widget_set_has_window(widget, TRUE); gtk_widget_set_can_focus(widget, TRUE); + + // GTK3 class handlers (button-press-event & friends) are gone in GTK4: + // route the input through controllers, which exist in both. n_press + // replaces the GDK_2BUTTON_PRESS reset check, the DISCRETE scroll proxy + // replaces the unit-delta accumulator. + dt_gui_connect_click(widget, _gradient_slider_button_pressed, + _gradient_slider_button_released, NULL); + dt_gui_connect_motion(widget, _gradient_slider_motion, + _gradient_slider_enter, _gradient_slider_leave, NULL); + dt_gui_connect_scroll(widget, + GTK_EVENT_CONTROLLER_SCROLL_VERTICAL + | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, + _gradient_slider_scroll, NULL); + dt_gui_connect_key(widget, _gradient_slider_key_pressed, NULL); } static void _gradient_slider_get_preferred_height(GtkWidget *widget, diff --git a/src/dtgtk/range.c b/src/dtgtk/range.c index e68fb129dca8..8f617eb9620a 100644 --- a/src/dtgtk/range.c +++ b/src/dtgtk/range.c @@ -1567,8 +1567,8 @@ static void _event_band_leave_cb(GtkEventControllerMotion *controller, GtkDarkta static void _event_band_press_cb(GtkGestureSingle *gesture, int n_press, double x, double y, GtkDarktableRangeSelect *range) { const guint button = gtk_gesture_single_get_current_button(gesture); - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(button == GDK_BUTTON_PRIMARY && n_press == 2) { diff --git a/src/dtgtk/stylemenu.c b/src/dtgtk/stylemenu.c index 5844056cc51c..cdf5b2cfd42a 100644 --- a/src/dtgtk/stylemenu.c +++ b/src/dtgtk/stylemenu.c @@ -22,6 +22,7 @@ #include "common/utility.h" #include "dtgtk/stylemenu.h" #include "gui/accelerators.h" +#include "gui/gtk.h" #include "gui/styles.h" static gboolean _styles_tooltip_callback(GtkWidget* self, @@ -56,6 +57,36 @@ static void _free_menu_data(dt_stylemenu_data_t *data) free(data); } +/* The shell emits "activate" on mouse release as well (for any button), so + * the activate callback must tell keyboard activation (Enter/mnemonic/accel) + * apart from mouse clicks: the press gesture marks the item and + * dt_gui_menuitem_activated_by_keyboard() skips the release-time activate. + * This replaces the gtk_get_current_event() GDK_KEY_PRESS check, which does + * not exist in GTK4. */ +typedef struct +{ + dtgtk_menuitem_button_callback_fn *callback; + dt_stylemenu_data_t *data; +} dt_stylemenu_button_conn_t; + +static void _style_menu_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) +{ + dt_stylemenu_button_conn_t *conn = user_data; + dt_gui_menuitem_mark_pressed(dt_gui_get_widget(gesture)); + conn->callback(gesture, n_press, x, y, conn->data); +} + +static void _free_button_conn(gpointer data) +{ + dt_stylemenu_button_conn_t *conn = data; + _free_menu_data(conn->data); + g_free(conn); +} + static void _build_style_submenus(GtkMenuShell *menu, const gchar *style_name, gchar **splits, @@ -125,15 +156,20 @@ static void _build_style_submenus(GtkMenuShell *menu, } if(button_callback) { - dt_stylemenu_data_t *menu_data = malloc(sizeof(dt_stylemenu_data_t)); - if(menu_data) - { - menu_data->name = g_strdup(style_name); - menu_data->user_data = user_data; - g_signal_connect_data(G_OBJECT(mi), "button-press-event", - G_CALLBACK(button_callback), - menu_data, (GClosureNotify)_free_menu_data, 0); - } + dt_stylemenu_button_conn_t *conn = g_new0(dt_stylemenu_button_conn_t, 1); + conn->callback = button_callback; + conn->data = malloc(sizeof(dt_stylemenu_data_t)); + conn->data->name = g_strdup(style_name); + conn->data->user_data = user_data; + // pressed is the direct replacement of the old button-press-event + // connection; the closure notify keeps owning the data. the wrapper + // additionally marks the item as mouse-handled (see above). + GtkGesture *gesture = gtk_gesture_multi_press_new(GTK_WIDGET(mi)); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(gesture), 0); + dt_gui_add_controller(GTK_WIDGET(mi), gesture); + g_signal_connect_data(gesture, "pressed", + G_CALLBACK(_style_menu_button_pressed), + conn, (GClosureNotify)_free_button_conn, 0); } gtk_widget_show(GTK_WIDGET(mi)); diff --git a/src/dtgtk/stylemenu.h b/src/dtgtk/stylemenu.h index fa5dc6d71d11..8c1a388e36c0 100644 --- a/src/dtgtk/stylemenu.h +++ b/src/dtgtk/stylemenu.h @@ -27,9 +27,11 @@ typedef struct { typedef void dtgtk_menuitem_activate_callback_fn(GtkMenuItem *menuitem, const dt_stylemenu_data_t *menu_data); -typedef gboolean dtgtk_menuitem_button_callback_fn(GtkMenuItem *, - GdkEventButton *event, - const dt_stylemenu_data_t *menu_data); +typedef void dtgtk_menuitem_button_callback_fn(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + const dt_stylemenu_data_t *menu_data); GtkMenuShell *dtgtk_build_style_menu_hierarchy(gboolean allow_none, dtgtk_menuitem_activate_callback_fn *activate_callback, diff --git a/src/dtgtk/thumbnail.c b/src/dtgtk/thumbnail.c index 8fe8677c1a0b..b75cd1995f74 100644 --- a/src/dtgtk/thumbnail.c +++ b/src/dtgtk/thumbnail.c @@ -1099,8 +1099,8 @@ static void _event_grouping_release_cb(GtkGestureSingle *gesture, if(!thumb->moved) { - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); //TODO: will succeed if either or *both* of Shift and Control are //pressed. Do we want this? if(dt_modifier_is(state, GDK_SHIFT_MASK) diff --git a/src/dtgtk/thumbtable.c b/src/dtgtk/thumbtable.c index 9d042cb1ebf8..f3935ee6fa95 100644 --- a/src/dtgtk/thumbtable.c +++ b/src/dtgtk/thumbtable.c @@ -1123,7 +1123,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, gdouble dy, dt_thumbtable_t *table) { - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!event) return; const GdkEventScroll *e = (const GdkEventScroll *)event; const GdkModifierType state = dt_gdk_event_get_state(event); @@ -1145,7 +1145,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, // 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); + did_scroll = dt_gui_get_scroll_deltas((const GdkEvent *)e, &deltaf_x, &deltaf_y); if(did_scroll) { // file manager scroll: tilt right (delta_x > 0) or scroll down (delta_y > 0) -> down @@ -1155,7 +1155,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, else { int delta_x, delta_y; - did_scroll = dt_gui_get_scroll_unit_deltas(e, &delta_x, &delta_y); + did_scroll = dt_gui_get_scroll_unit_deltas((const GdkEvent *)e, &delta_x, &delta_y); if(did_scroll) { deltaf = abs(delta_x) > abs(delta_y) ? delta_x : delta_y; @@ -1171,14 +1171,18 @@ static void _event_scroll(GtkEventControllerScroll *controller, } table->scroll_value += deltaf; } + // GTK3: owned copy from dt_gui_get_current_event() (see the same guard + // on the other free in this function); GTK4: borrowed, nothing to free +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif return; } // filmstrip and zoom mode always use clicky scroll: int delta_x, delta_y; - if(dt_gui_get_scroll_unit_deltas(e, &delta_x, &delta_y)) + if(dt_gui_get_scroll_unit_deltas((const GdkEvent *)e, &delta_x, &delta_y)) { // for zoomable, scroll = zoom if(table->mode == DT_THUMBTABLE_MODE_ZOOM @@ -1189,7 +1193,7 @@ static void _event_scroll(GtkEventControllerScroll *controller, // keep the accumulated magnitude for the step size const int dominant = abs(delta_x) > abs(delta_y) ? delta_x : delta_y; const int delta = - dt_gui_scroll_zoom_delta((const GdkEventScroll *)e, delta_x, delta_y) > 0.0f + dt_gui_scroll_zoom_delta((const GdkEvent *)e, delta_x, delta_y) > 0.0f ? -abs(dominant) : abs(dominant); if(table->mode == DT_THUMBTABLE_MODE_FILMSTRIP) @@ -1220,7 +1224,9 @@ static void _event_scroll(GtkEventControllerScroll *controller, dt_control_set_mouse_over_id(th->imgid); } } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } static void _line_to(cairo_t *cr, @@ -1408,18 +1414,27 @@ static void _event_leave_cb(GtkEventControllerMotion *controller, * 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(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR + && gdk_crossing_event_get_mode(event) != GDK_CROSSING_GTK_GRAB + && gdk_crossing_event_get_mode(event) != GDK_CROSSING_GRAB + && !dt_gui_pointer_is_grabbed()) +#else if(event->crossing.detail != GDK_NOTIFY_INFERIOR && event->crossing.mode != GDK_CROSSING_GTK_GRAB && event->crossing.mode != GDK_CROSSING_GRAB && !dt_gui_pointer_is_grabbed()) +#endif { table->mouse_inside = FALSE; dt_control_set_mouse_over_id(NO_IMGID); } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } } @@ -1439,16 +1454,11 @@ static void _event_enter_cb(GtkEventControllerMotion *controller, * the 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). */ - GdkEvent *event = gtk_get_current_event(); - if(event) - { - /* GTK4 migration: drop the pointer-grab check (see - * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ - if(!dt_gui_pointer_is_grabbed() - && !_thumb_get_at_pos(table, (int)x, (int)y)) - dt_control_set_mouse_over_id(NO_IMGID); - gdk_event_free(event); - } + /* GTK4 migration: drop the pointer-grab check (see + * dt_gui_pointer_is_grabbed()) -- GTK4 has no grabs. */ + if(!dt_gui_pointer_is_grabbed() + && !_thumb_get_at_pos(table, (int)x, (int)y)) + dt_control_set_mouse_over_id(NO_IMGID); } static gboolean _do_select_single(gpointer user_data) @@ -1584,7 +1594,8 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, // get root coordinates for drag tracking gdouble root_x = 0, root_y = 0; - dt_gui_get_current_root_coords(&root_x, &root_y); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) dt_gui_get_event_coords(event, &root_x, &root_y); if(table->dragging && table->mode == DT_THUMBTABLE_MODE_ZOOM) { @@ -1604,6 +1615,9 @@ static void _event_motion_notify_cb(GtkEventControllerMotion *controller, table->last_x = ceil(root_x); table->last_y = ceil(root_y); +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } static void _event_button_release_cb(GtkGestureSingle *gesture, @@ -1624,8 +1638,8 @@ static void _event_button_release_cb(GtkGestureSingle *gesture, dt_set_backthumb_time(0.0); const dt_imgid_t id = dt_control_get_mouse_over_id(); - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(dt_is_valid_imgid(id) && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) diff --git a/src/dtgtk/togglebutton.c b/src/dtgtk/togglebutton.c index f28d01ee387d..5835a5c0b25c 100644 --- a/src/dtgtk/togglebutton.c +++ b/src/dtgtk/togglebutton.c @@ -18,6 +18,7 @@ #include "togglebutton.h" #include "bauhaus/bauhaus.h" #include "button.h" +#include "gui/accelerators.h" #include "gui/gtk.h" #include @@ -146,6 +147,32 @@ GtkWidget *dtgtk_togglebutton_new(DTGTKCairoPaintIconFunc paint, gint paintflags return (GtkWidget *)button; } +GtkWidget *dtgtk_togglebutton_new_full(DTGTKCairoPaintIconFunc paint, + gint paintflags, + void *paintdata, + const dtgtk_button_config_t *config) +{ + GtkWidget *button = dtgtk_togglebutton_new(paint, paintflags, paintdata); + if(!config) return button; + if(config->tooltip) + gtk_widget_set_tooltip_text(button, config->tooltip); + else if(config->tooltip_markup) + gtk_widget_set_tooltip_markup(button, config->tooltip_markup); + if(config->action) + dt_action_define(config->action, config->action_section, + config->action_label, button, config->action_def); + /* g_signal_connect() is a type-checking macro that token-pastes the + * handler name, so the generic factory has to go through + * g_signal_connect_data() directly. */ + if(config->clicked_cb) + g_signal_connect_data(G_OBJECT(button), "clicked", config->clicked_cb, + config->clicked_data, NULL, (GConnectFlags) 0); + if(config->toggled_cb) + g_signal_connect_data(G_OBJECT(button), "toggled", config->toggled_cb, + config->toggled_data, NULL, (GConnectFlags) 0); + return button; +} + void dtgtk_togglebutton_set_paint(GtkDarktableToggleButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata) { diff --git a/src/dtgtk/togglebutton.h b/src/dtgtk/togglebutton.h index ac0db642b8d9..cf079f68b69e 100644 --- a/src/dtgtk/togglebutton.h +++ b/src/dtgtk/togglebutton.h @@ -18,6 +18,7 @@ #pragma once +#include "button.h" #include "paint.h" #include @@ -39,6 +40,16 @@ struct _GtkDarktableToggleButton /** instantiate a new darktable toggle button */ GtkWidget *dtgtk_togglebutton_new(DTGTKCairoPaintIconFunc paint, gint paintflag, void *paintdata); +/** instantiate a new darktable toggle button and wire the same optional + * extras as dtgtk_button_new_full() (tooltip/markup, action definition, + * "clicked" or "toggled" callback), see dtgtk_button_config_t there. The + * "toggled" callback is connected in addition to the internal repaint + * handler. */ +GtkWidget *dtgtk_togglebutton_new_full(DTGTKCairoPaintIconFunc paint, + gint paintflags, + void *paintdata, + const dtgtk_button_config_t *config); + /** Set the paint function and paint flags */ void dtgtk_togglebutton_set_paint(GtkDarktableToggleButton *button, DTGTKCairoPaintIconFunc paint, gint paintflags, void *paintdata); diff --git a/src/gui/accelerators.c b/src/gui/accelerators.c index 001c50b86591..5cea34a356fb 100644 --- a/src/gui/accelerators.c +++ b/src/gui/accelerators.c @@ -269,27 +269,33 @@ static float _action_process_toggle(gpointer target, } 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); - - // 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); + /* no stored gesture: a plain GTK toggle button. Its internal press + * gesture is primary-button-only and the "clicked"/"toggled" wiring + * lives in the button itself, so gtk_widget_activate() is the + * GTK4-compatible equivalent of the synthetic press+release GTK3 + * used. DT_ACTION_TOGGLE_NEEDED above already restricts the ON/OFF + * effects to the state change they demand, so a single flip via + * activate() is right for every effect: the toggle variants flip, + * the ON/OFF variants flip only when the button is in the wrong + * state, and the right variants have no distinct right-click handler + * on a plain button and flip like the primary (the old synthetic + * secondary press fell back to gtk_button_clicked() the same way; + * a widget whose right-click behavior lives in a gesture uses the + * stored-gesture branch above). */ + switch(effect) + { + case DT_ACTION_EFFECT_TOGGLE: + case DT_ACTION_EFFECT_TOGGLE_CTRL: + case DT_ACTION_EFFECT_ON: + case DT_ACTION_EFFECT_OFF: + case DT_ACTION_EFFECT_ON_CTRL: + case DT_ACTION_EFFECT_TOGGLE_RIGHT: + case DT_ACTION_EFFECT_ON_RIGHT: + gtk_widget_activate(GTK_WIDGET(target)); + break; + default: + break; + } } value = gtk_toggle_button_get_active(target); @@ -325,29 +331,17 @@ static float _action_process_button(gpointer target, } else { - 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))) - { - 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; - - event->button.window = gtk_widget_get_window(target); - g_object_ref(event->button.window); - - gtk_widget_event(target, event); - event->type = GDK_BUTTON_RELEASE; - gtk_widget_event(target, event); - - gdk_event_free(event); - } + /* no stored gesture: a plain GTK button. Its internal press gesture + * is primary-button-only, so the old synthetic press+release ran the + * "clicked" handler for the primary/ctrl variants and reached the + * then-still-connected button-press-event handlers for the right + * variant; those handlers now live in gestures (routed through the + * stored-gesture branch above), so on a plain button a right-click + * shortcut has nothing left to reach and stays a no-op. + * gtk_widget_activate() is the GTK4-compatible equivalent of the + * primary press+release. */ + if(effect == DT_ACTION_EFFECT_ACTIVATE || effect == DT_ACTION_EFFECT_ACTIVATE_CTRL) + gtk_widget_activate(GTK_WIDGET(target)); } } @@ -2131,11 +2125,21 @@ static gboolean _view_key_pressed_cb(GtkEventControllerKey *controller, } } - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4: gtk_search_entry_handle_event() is gone -- the entry handles key + * events through its internal controller. Focus the entry so typing + * lands in it; the very first keystroke is consumed (TODO: feed it via + * gdk_keyval_to_unicode for full search-as-you-type parity). */ + (void)event; + gtk_entry_grab_focus_without_selecting(GTK_ENTRY(search_entry)); + return TRUE; +#else const gboolean handled = dt_gui_search_start(widget, (GdkEventKey *)event, GTK_SEARCH_ENTRY(search_entry)); gdk_event_free(event); return handled; +#endif } static void _add_shortcuts_to_tree() @@ -2975,7 +2979,30 @@ static void _notice_clicked(GtkWidget *button, * in its own key handling before any handler connected on the treeview runs, * so the delete keys are intercepted at the toplevel instead, before the key * event is propagated to the focus widget. GDK_KEY_BackSpace is the delete - * key on macOS. */ + * key on macOS. On GTK4 a CAPTURE-phase key controller on the toplevel runs + * before the focus widget's own handling, which is the same "first" + * position; GTK3 delivers key events to the toplevel and then propagates + * them, so there the classic signal is connected instead (see + * _shortcuts_view_realized). */ +#if GTK_CHECK_VERSION(4, 0, 0) +static gboolean _shortcuts_dialog_key_pressed(GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + gpointer user_data) +{ + GtkWidget *toplevel = dt_gui_get_widget(controller); + GtkWidget *view = user_data; + + if(gtk_window_get_focus(GTK_WINDOW(toplevel)) != view) + return GDK_EVENT_PROPAGATE; + + if(keyval == GDK_KEY_Delete || keyval == GDK_KEY_KP_Delete || keyval == GDK_KEY_BackSpace) + return _shortcut_delete_selected(GTK_TREE_VIEW(view)); + + return GDK_EVENT_PROPAGATE; +} +#else static gboolean _shortcuts_dialog_key_pressed(GtkWidget *widget, GdkEventKey *event, gpointer user_data) @@ -2993,6 +3020,7 @@ static gboolean _shortcuts_dialog_key_pressed(GtkWidget *widget, return FALSE; } +#endif static void _shortcuts_view_realized(GtkWidget *widget, gpointer user_data) { @@ -3004,8 +3032,16 @@ static void _shortcuts_view_realized(GtkWidget *widget, gpointer user_data) if(!toplevel || !GTK_IS_WINDOW(toplevel)) return; +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController *controller = gtk_event_controller_key_new(); + gtk_event_controller_set_propagation_phase(controller, GTK_PHASE_CAPTURE); + dt_gui_add_controller(toplevel, controller); + g_signal_connect(controller, "key-pressed", + G_CALLBACK(_shortcuts_dialog_key_pressed), widget); +#else g_signal_connect(G_OBJECT(toplevel), "key-press-event", G_CALLBACK(_shortcuts_dialog_key_pressed), widget); +#endif } GtkWidget *dt_shortcuts_prefs(GtkWidget *widget) @@ -4912,7 +4948,7 @@ gboolean dt_shortcut_dispatcher(GtkWidget *w, && (s.action || (s.action = dt_action_widget(darktable.control->mapping_widget)))) { int delta; - if(middle_click || dt_gui_get_scroll_unit_delta(&event->scroll, &delta)) + if(middle_click || dt_gui_get_scroll_unit_delta((const GdkEvent *)event, &delta)) { // delta < 0 -> 10^(-delta) increases speed s.speed = middle_click ? -1 : powf(10.0f, -delta); @@ -5014,7 +5050,7 @@ gboolean dt_shortcut_dispatcher(GtkWidget *w, _sc.mods = _key_modifiers_clean(dt_gdk_event_get_state(event)); int delta_x, delta_y; - if(dt_gui_get_scroll_unit_deltas(&event->scroll, &delta_x, &delta_y)) + if(dt_gui_get_scroll_unit_deltas((const GdkEvent *)event, &delta_x, &delta_y)) { if(delta_x) dt_shortcut_move(DT_SHORTCUT_DEVICE_KEYBOARD_MOUSE, dt_gdk_event_get_time(event), diff --git a/src/gui/color_picker_proxy.c b/src/gui/color_picker_proxy.c index 0280a904030c..81e72f712c23 100644 --- a/src/gui/color_picker_proxy.c +++ b/src/gui/color_picker_proxy.c @@ -290,24 +290,19 @@ static void _color_picker_clicked(GtkGestureSingle *gesture, * * 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(). + * definition below (dt_action_def_color_picker), and programmatic + * activation (temperature.c "spot" preset, colorpicker lib sample copy) + * through the public wrapper; all 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) +float dt_iop_color_picker_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)); @@ -343,7 +338,8 @@ static float _color_picker_widget_toggle(GtkWidget *target, * 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. + * process calls the shared entry dt_iop_color_picker_toggle(), the same + * one real clicks and programmatic activation go through. */ static const dt_action_element_def_t _color_picker_elements[] = { { NULL, dt_action_effect_toggle } }; @@ -359,7 +355,7 @@ static float _color_picker_process(gpointer target, const dt_action_effect_t effect, const float move_size) { - return _color_picker_widget_toggle(target, effect, move_size); + return dt_iop_color_picker_toggle(target, effect, move_size); } const dt_action_def_t dt_action_def_color_picker @@ -518,11 +514,13 @@ static GtkWidget *_color_picker_new(dt_iop_module_t *module, // 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). + // Shortcuts (dt_action_def_color_picker, see above) and programmatic + // activation (dt_iop_color_picker_toggle, e.g. temperature.c "spot" + // preset, colorpicker lib sample copy) call the shared entry 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); @@ -531,7 +529,7 @@ static GtkWidget *_color_picker_new(dt_iop_module_t *module, 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_signal_connect(gesture, "begin", G_CALLBACK(dt_gui_gesture_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); diff --git a/src/gui/color_picker_proxy.h b/src/gui/color_picker_proxy.h index 75c6101c8f53..76d45b5c5476 100644 --- a/src/gui/color_picker_proxy.h +++ b/src/gui/color_picker_proxy.h @@ -78,6 +78,15 @@ gboolean dt_iop_color_picker_is_visible(const dt_develop_t *dev); /* action definition for standalone picker toggle buttons (see color_picker_proxy.c) */ extern const struct dt_action_def_t dt_action_def_color_picker; +/* Shared activation entry for standalone picker toggle buttons, invoked by + * real clicks (CAPTURE-phase gesture), shortcuts (dt_action_def_color_picker) + * and programmatic activation (temperature.c "spot" preset, colorpicker lib + * sample copy). Same semantics as the toggle action: TOGGLE_CTRL/ON_CTRL + * switch to area mode, TOGGLE_RIGHT/ON_RIGHT select area mode as well. */ +float dt_iop_color_picker_toggle(GtkWidget *button, + const dt_action_effect_t effect, + const float move_size); + //* 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 cbb13e1334bd..a712f7f51cbe 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -447,42 +447,67 @@ static void _toggle_bottom_all_accel_callback(dt_action_t *action) dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_CENTER_BOTTOM, !v, TRUE); } -static gboolean _borders_button_pressed(GtkWidget *w, - GdkEventButton *event, - const gpointer user_data) +static void _borders_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { _panel_toggle(GPOINTER_TO_INT(user_data), darktable.gui->ui); - - return TRUE; } // FIXME: if this is only called from scroll handlers, move this logic to scroll proxy // FIXME: just call with GdkModifierType as state -gboolean dt_gui_ignore_scroll(GdkEventScroll *event) +static gboolean _dt_gui_ignore_scroll(const GdkModifierType mods_pressed) { const gboolean ignore_without_mods = dt_conf_get_bool("darkroom/ui/sidebar_scroll_default"); - const GdkModifierType mods_pressed = - (dt_gdk_event_get_state(event) & gtk_accelerator_get_default_mod_mask()); if(mods_pressed == 0) - { return ignore_without_mods; - } - else - { - if(mods_pressed == darktable.gui->sidebar_scroll_mask) - { - if(!ignore_without_mods) return TRUE; + if(mods_pressed == darktable.gui->sidebar_scroll_mask) + return !ignore_without_mods; + return FALSE; +} - event->state &= ~darktable.gui->sidebar_scroll_mask; - } +gboolean dt_gui_ignore_scroll(GdkEventScroll *event) +{ + const GdkModifierType mods_pressed = + (dt_gdk_event_get_state(event) & gtk_accelerator_get_default_mod_mask()); + const gboolean ignore = _dt_gui_ignore_scroll(mods_pressed); - return FALSE; + // the mask is the "handle it here" modifier: consume it so the event + // carries it no further once the widget decides to handle the scroll + if(!ignore && mods_pressed == darktable.gui->sidebar_scroll_mask) + event->state &= ~darktable.gui->sidebar_scroll_mask; + + return ignore; +} + +gboolean dt_gui_ignore_scroll_controller(GtkEventControllerScroll *controller) +{ + const GdkModifierType state = dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); + return _dt_gui_ignore_scroll(state & gtk_accelerator_get_default_mod_mask()); +} + +GdkModifierType dt_gui_get_current_event_state(GtkEventController *controller) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_event_controller_get_current_event_state(controller); +#else + (void)controller; + GdkEvent *event = gtk_get_current_event(); + GdkModifierType state = 0; + if(event) + { + gdk_event_get_state(event, &state); + gdk_event_free(event); } + return state; +#endif } -gboolean dt_gui_get_scroll_deltas(const GdkEventScroll *event, +gboolean dt_gui_get_scroll_deltas(const GdkEvent *event, gdouble *delta_x, gdouble *delta_y) { @@ -545,7 +570,7 @@ gboolean dt_gui_get_scroll_deltas(const GdkEventScroll *event, return handled; } -gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, +gboolean dt_gui_get_scroll_unit_deltas(const GdkEvent *event, int *delta_x, int *delta_y) { @@ -595,7 +620,7 @@ gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, // is trackpad (or touch) scroll case GDK_SCROLL_SMOOTH: // stop events reset accumulated delta - if(event->is_stop) + if(dt_gdk_event_is_scroll_stop(event)) { acc_x = acc_y = 0.0; break; @@ -630,7 +655,7 @@ gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, return handled; } -gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, +gboolean dt_gui_get_scroll_delta(const GdkEvent *event, gdouble *delta) { gdouble delta_x, delta_y; @@ -643,7 +668,7 @@ gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, return FALSE; } -gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, +gboolean dt_gui_get_scroll_unit_delta(const GdkEvent *event, int *delta) { int delta_x, delta_y; @@ -771,68 +796,65 @@ static void _touchpad_gestures_pref_changed(gpointer instance, gui->touchpad_gestures_enabled = dt_conf_get_bool("darkroom/ui/touchpad_gestures"); } -static gboolean _input_event(GtkWidget *widget, - GdkEvent *event, - gpointer user_data) +/* record the device that produced a touchpad gesture, so a follow-up smooth + * scroll stream from a possibly-different device is still panned (see + * dt_gui_scroll_should_pan). Replaces the old _input_event switch. */ +static void _record_touchpad_device(const GdkEvent *event) { - (void)user_data; - - switch(dt_gdk_event_get_type(event)) - { - case GDK_TOUCHPAD_PINCH: - case GDK_TOUCHPAD_SWIPE: - _touchpad = gdk_event_get_source_device(event); - if(_touchpad) - { - dt_print(DT_DEBUG_INPUT, - "[touchpad] gesture event type=%d source='%s' source_type=%d", - dt_gdk_event_get_type(event), - gdk_device_get_name(_touchpad), - gdk_device_get_source(_touchpad)); - } - else - { - dt_print(DT_DEBUG_INPUT, - "[touchpad] gesture event type=%d without source device", - dt_gdk_event_get_type(event)); - } - break; - default: - break; - } - - if(dt_gdk_event_get_type(event) == GDK_TOUCHPAD_PINCH && darktable.gui->touchpad_gestures_enabled) - { - const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; + _touchpad = dt_gdk_event_get_source_device(event); + if(_touchpad) dt_print(DT_DEBUG_INPUT, - "[touchpad] pinch x=%.2f y=%.2f phase=%d scale=%.6f state=0x%x", - pinch->x, pinch->y, pinch->phase, pinch->scale, pinch->state); - if(dt_view_manager_gesture_pinch(darktable.view_manager, pinch->x_root, pinch->y_root, - pinch->dx, pinch->dy, pinch->phase, - pinch->scale, pinch->state & 0xf)) - { - gtk_widget_queue_draw(widget); - return TRUE; - } + "[touchpad] gesture event type=%d source='%s' source_type=%d", + dt_gdk_event_get_type(event), + gdk_device_get_name(_touchpad), + gdk_device_get_source(_touchpad)); + else + dt_print(DT_DEBUG_INPUT, + "[touchpad] gesture event type=%d without source device", + dt_gdk_event_get_type(event)); +} + +/* GtkGestureZoom replaces the old "event" signal handler that forwarded raw + * GDK_TOUCHPAD_PINCH events: the phase field becomes the begin / + * scale-changed / end signals (and "end" fires on cancel as well, so the + * view's END/CANCEL reset still runs). The gesture setup, the touchscreen + * pinch filter, the enabled pref and the active tracking are all inside + * dt_gui_connect_pinch(); this handler only records the pinch device (for the + * follow-up scroll pan routing) and forwards the parsed event. */ +static void _pinch_event(GtkGesture *gesture, + const dt_gui_pinch_event_t *e, + gpointer user_data) +{ + (void)user_data; + GtkWidget *widget = dt_gui_get_widget(gesture); + if(e->event) _record_touchpad_device(e->event); + dt_print(DT_DEBUG_INPUT, + "[touchpad] pinch x=%.2f y=%.2f phase=%d dx=%.3f dy=%.3f scale=%.6f state=0x%x", + e->x, e->y, e->phase, e->dx, e->dy, e->scale, e->state); + if(dt_view_manager_gesture_pinch(darktable.view_manager, e->x, e->y, + e->dx, e->dy, e->phase, e->scale, e->state)) + gtk_widget_queue_draw(widget); + else dt_print(DT_DEBUG_INPUT, "[touchpad] pinch ignored by current view"); - } - else if(dt_gdk_event_get_type(event) == GDK_TOUCHPAD_PINCH) - { - dt_print(DT_DEBUG_INPUT, - "[touchpad] pinch received but disabled by preference darkroom/ui/touchpad_gestures"); - } +} - return FALSE; +/* touchpad swipe: only used to record the source device for the follow-up + * scroll-stream pan routing (GtkGestureSwipe handles GDK_TOUCHPAD_SWIPE in + * both GTK3 3.24 and GTK4) */ +static void _swipe_begin_cb(GtkGestureSwipe *gesture, gpointer user_data) +{ + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event) _record_touchpad_device(event); } -gboolean dt_gui_scroll_should_pan(const GdkEventScroll *event) +gboolean dt_gui_scroll_should_pan(const GdkEvent *event) { if(!darktable.gui->touchpad_gestures_enabled) return FALSE; if(dt_modifiers_include(dt_gdk_event_get_state(event), GDK_CONTROL_MASK)) return FALSE; if(dt_gdk_event_get_scroll_direction(event) != GDK_SCROLL_SMOOTH) return FALSE; - if(gdk_event_is_scroll_stop_event((GdkEvent *)event)) return FALSE; + if(dt_gdk_event_is_scroll_stop(event)) return FALSE; #ifdef GDK_WINDOWING_QUARTZ // On macOS/Quartz, the built-in trackpad reports as GDK_SOURCE_MOUSE, not // GDK_SOURCE_TOUCHPAD. Route every non-ctrl smooth scroll to pan so that @@ -849,7 +871,7 @@ gboolean dt_gui_scroll_should_pan(const GdkEventScroll *event) #endif } -float dt_gui_scroll_zoom_delta(const GdkEventScroll *event, +float dt_gui_scroll_zoom_delta(const GdkEvent *event, const gdouble dx, const gdouble dy) { // zoom direction convention: up (the backward scroll) zooms in, down zooms @@ -885,15 +907,21 @@ float dt_gui_scroll_zoom_delta(const GdkEventScroll *event, } } -static gboolean _scrolled(GtkWidget *widget, - const GdkEventScroll *event, - gpointer user_data) +static void _scrolled(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) { + (void)dx; + (void)dy; (void)user_data; - GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); + GtkWidget *widget = dt_gui_get_widget(controller); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(!event) return; + GdkDevice *device = dt_gdk_event_get_source_device(event); const gboolean ctrl_pressed = dt_modifier_is(dt_gdk_event_get_state(event), GDK_CONTROL_MASK); const gboolean is_smooth = dt_gdk_event_get_scroll_direction(event) == GDK_SCROLL_SMOOTH - && !gdk_event_is_scroll_stop_event((GdkEvent *)event); + && !dt_gdk_event_is_scroll_stop(event); dt_print(DT_DEBUG_INPUT, "[scroll] direction=%d smooth=%s stop=%s ctrl=%s" @@ -901,7 +929,7 @@ static gboolean _scrolled(GtkWidget *widget, " device='%s' source-type=%d", dt_gdk_event_get_scroll_direction(event), dt_gdk_event_get_scroll_direction(event) == GDK_SCROLL_SMOOTH ? "yes" : "no", - gdk_event_is_scroll_stop_event((GdkEvent *)event) ? "yes" : "no", + dt_gdk_event_is_scroll_stop(event) ? "yes" : "no", ctrl_pressed ? "yes" : "no", dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), dt_gdk_event_get_scroll_delta_x(event), dt_gdk_event_get_scroll_delta_y(event), dt_gdk_event_get_state(event), device ? gdk_device_get_name(device) : "", @@ -915,7 +943,7 @@ static gboolean _scrolled(GtkWidget *widget, "[touchpad] smooth scroll ignored (likely pointer emulated), source='%s' source_type=%d", device ? gdk_device_get_name(device) : "", device ? gdk_device_get_source(device) : -1); - return TRUE; + goto out; } delta_x *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; @@ -929,7 +957,7 @@ static gboolean _scrolled(GtkWidget *widget, dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), delta_x, delta_y, device ? gdk_device_get_name(device) : ""); gtk_widget_queue_draw(widget); - return TRUE; + goto out; } else if(delta_x != 0.0 || delta_y != 0.0) { @@ -949,22 +977,27 @@ static gboolean _scrolled(GtkWidget *widget, device ? (int)gdk_device_get_source(device) : -1); } - int delta_x, delta_y; - if(dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y)) { - const gboolean up = dt_gui_scroll_zoom_delta(event, delta_x, delta_y) > 0.0f; - dt_print(DT_DEBUG_INPUT, - "[scroll] discrete fallback x=%.2f y=%.2f up=%d state=0x%x source='%s' source_type=%d", - dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), up, dt_gdk_event_get_state(event), - device ? gdk_device_get_name(device) : "", - device ? gdk_device_get_source(device) : -1); - dt_view_manager_scrolled(darktable.view_manager, dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), - up, - dt_gdk_event_get_state(event) & 0xf); - gtk_widget_queue_draw(widget); + int delta_x, delta_y; + if(dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y)) + { + const gboolean up = dt_gui_scroll_zoom_delta(event, delta_x, delta_y) > 0.0f; + dt_print(DT_DEBUG_INPUT, + "[scroll] discrete fallback x=%.2f y=%.2f up=%d state=0x%x source='%s' source_type=%d", + dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), up, dt_gdk_event_get_state(event), + device ? gdk_device_get_name(device) : "", + device ? gdk_device_get_source(device) : -1); + dt_view_manager_scrolled(darktable.view_manager, dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), + up, + dt_gdk_event_get_state(event) & 0xf); + gtk_widget_queue_draw(widget); + } } - return TRUE; +out: +#if !GTK_CHECK_VERSION(4, 0, 0) + gdk_event_free(event); +#endif } static gboolean @@ -1311,63 +1344,86 @@ guint dt_gui_translated_key_state(const GdkEventKey *event) return dt_gdk_event_get_state(event) & gtk_accelerator_get_default_mod_mask(); } -static gboolean _button_pressed(GtkWidget *w, - GdkEventButton *event, - gpointer user_data) +static void _button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { double pressure = 1.0; - GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); - - if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(gesture)); + if(event) { - gdk_event_get_axis((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure); + GdkDevice *device = dt_gdk_event_get_source_device(event); + if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) + gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure); } - dt_control_button_pressed(dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), pressure, - dt_gdk_event_get_button(event), dt_gdk_event_get_type(event), dt_gdk_event_get_state(event) & 0xf); + GtkWidget *w = dt_gui_get_widget(gesture); + // n_press replaces the old GDK_BUTTON_PRESS/GDK_2BUTTON_PRESS/GDK_3BUTTON_PRESS + // event-type distinction that dt_control_button_pressed() stores + dt_control_button_pressed(x, y, pressure, + gtk_gesture_single_get_current_button(gesture), + n_press >= 3 ? GDK_3BUTTON_PRESS + : (n_press == 2 ? GDK_2BUTTON_PRESS : GDK_BUTTON_PRESS), + dt_gui_current_state(gesture) & 0xf); gtk_widget_grab_focus(w); gtk_widget_queue_draw(w); - return FALSE; +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } -static gboolean _button_released(GtkWidget *w, - const GdkEventButton *event, - gpointer user_data) +static void _button_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - dt_control_button_released(dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), dt_gdk_event_get_button(event), dt_gdk_event_get_state(event) & 0xf); + // the cancel handler re-emits a synthetic release with no button; the old + // handler never saw one (see the Phase 3 note in gtk.h) + const guint button = gtk_gesture_single_get_current_button(gesture); + if(!button) return; + + GtkWidget *w = dt_gui_get_widget(gesture); + dt_control_button_released(x, y, button, dt_gui_current_state(gesture) & 0xf); gtk_widget_queue_draw(w); - return TRUE; } -static gboolean _mouse_moved(GtkWidget *w, - GdkEventMotion *event, - dt_gui_gtk_t *gui) +static void _mouse_moved(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + dt_gui_gtk_t *gui) { double pressure = 1.0; - GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); - - if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) { - gdk_event_get_axis((GdkEvent *)event, GDK_AXIS_PRESSURE, &pressure); - gui->have_pen_pressure = pressure != 1.0; + GdkDevice *device = dt_gdk_event_get_source_device(event); + if(device && gdk_device_get_source(device) == GDK_SOURCE_PEN) + { + gdk_event_get_axis(event, GDK_AXIS_PRESSURE, &pressure); + gui->have_pen_pressure = pressure != 1.0; + } } - dt_control_mouse_moved(dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), pressure, dt_gdk_event_get_state(event) & 0xf); - return FALSE; + dt_control_mouse_moved(x, y, pressure, + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)) & 0xf); +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } -static gboolean _center_leave(GtkWidget *widget, - GdkEventCrossing *event, - gpointer user_data) +static void _center_leave(GtkEventControllerMotion *controller, + gpointer user_data) { dt_control_mouse_leave(); - return TRUE; } -static gboolean _center_enter(GtkWidget *widget, - GdkEventCrossing *event, - gpointer user_data) +static void _center_enter(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) { dt_control_mouse_enter(); - return TRUE; } static const char* _get_source_name(const int pos) @@ -1413,6 +1469,8 @@ void dt_open_url(const char* url) GtkWidget *win = dt_ui_main_window(darktable.gui->ui); // TODO: call the web browser directly so that file:// style base for local installs works + // GTK4: gtk_show_uri_on_window() is gone (use gtk_show_uri()); this + // whole helper is GTK3-only until then. const gboolean uri_success = gtk_show_uri_on_window(GTK_WINDOW(win), url, gtk_get_current_event_time(), @@ -1707,20 +1765,19 @@ int dt_gui_gtk_init(dt_gui_gtk_t *gui) g_signal_connect(G_OBJECT(widget), "draw", G_CALLBACK(_draw), NULL); - g_signal_connect(G_OBJECT(widget), "event", - G_CALLBACK(_input_event), NULL); - g_signal_connect(G_OBJECT(widget), "motion-notify-event", - G_CALLBACK(_mouse_moved), gui); - g_signal_connect(G_OBJECT(widget), "leave-notify-event", - G_CALLBACK(_center_leave), NULL); - g_signal_connect(G_OBJECT(widget), "enter-notify-event", - G_CALLBACK(_center_enter), NULL); - g_signal_connect(G_OBJECT(widget), "button-press-event", - G_CALLBACK(_button_pressed), NULL); - g_signal_connect(G_OBJECT(widget), "button-release-event", - G_CALLBACK(_button_released), NULL); - g_signal_connect(G_OBJECT(widget), "scroll-event", - G_CALLBACK(_scrolled), NULL); + + // input via controllers (GTK4-compatible; the old motion/button/scroll + // event signals and the pinch "event" handler are GTK3-only) + dt_gui_connect_motion(widget, _mouse_moved, _center_enter, _center_leave, gui); + dt_gui_connect_click(widget, _button_pressed, _button_released, NULL); + dt_gui_connect_scroll(widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, + _scrolled, NULL); + + dt_gui_connect_pinch(widget, _pinch_event, NULL); + + GtkGesture *swipe = gtk_gesture_swipe_new(widget); + dt_gui_add_controller(widget, swipe); + g_signal_connect(swipe, "begin", G_CALLBACK(_swipe_begin_cb), NULL); } // TODO: left, right, top, bottom: @@ -1854,11 +1911,11 @@ int dt_gui_gtk_init(dt_gui_gtk_t *gui) // create focus-peaking button darktable.gui->focus_peaking_button = - dtgtk_togglebutton_new(dtgtk_cairo_paint_focus_peaking, 0, NULL); - gtk_widget_set_tooltip_text(darktable.gui->focus_peaking_button, - _("toggle focus-peaking mode")); - g_signal_connect(G_OBJECT(darktable.gui->focus_peaking_button), "clicked", - G_CALLBACK(_focuspeaking_switch_button_callback), NULL); + dtgtk_togglebutton_new_full(dtgtk_cairo_paint_focus_peaking, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle focus-peaking mode"), + .clicked_cb = G_CALLBACK(_focuspeaking_switch_button_callback), + }); _update_focus_peaking_button(); // toggle focus peaking everywhere @@ -1967,20 +2024,13 @@ static gboolean _focus_in_out_event(GtkWidget *widget, } -static gboolean _ui_log_button_press_event(GtkWidget *widget, - GdkEvent *event, - const gpointer user_data) +static void _ui_log_button_press_event(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + const gpointer user_data) { gtk_widget_hide(GTK_WIDGET(user_data)); - return TRUE; -} - -static gboolean _ui_toast_button_press_event(GtkWidget *widget, - GdkEvent *event, - const gpointer user_data) -{ - gtk_widget_hide(GTK_WIDGET(user_data)); - return TRUE; } static GtkWidget *_init_outer_border(const gint width, @@ -1996,8 +2046,7 @@ static GtkWidget *_init_outer_border(const gint width, | darktable.gui->scroll_mask); g_signal_connect(widget, "draw", G_CALLBACK(_draw_borders), GINT_TO_POINTER(which)); - g_signal_connect(widget, "button-press-event", - G_CALLBACK(_borders_button_pressed), GINT_TO_POINTER(which)); + dt_gui_connect_click(widget, _borders_button_pressed, NULL, GINT_TO_POINTER(which)); gtk_widget_set_name(GTK_WIDGET(widget), "outer-border"); gtk_widget_show(widget); @@ -2137,9 +2186,7 @@ static void _init_main_table(GtkWidget *container) /* the log message */ GtkWidget *eb = gtk_event_box_new(); darktable.gui->ui->log_msg = gtk_label_new(""); - g_signal_connect(G_OBJECT(eb), "button-press-event", - G_CALLBACK(_ui_log_button_press_event), - darktable.gui->ui->log_msg); + dt_gui_connect_click(eb, _ui_log_button_press_event, NULL, darktable.gui->ui->log_msg); gtk_label_set_ellipsize(GTK_LABEL(darktable.gui->ui->log_msg), PANGO_ELLIPSIZE_MIDDLE); dt_gui_add_class(darktable.gui->ui->log_msg, "dt_messages"); gtk_container_add(GTK_CONTAINER(eb), darktable.gui->ui->log_msg); @@ -2151,11 +2198,9 @@ static void _init_main_table(GtkWidget *container) /* the toast message */ eb = gtk_event_box_new(); darktable.gui->ui->toast_msg = gtk_label_new(""); - g_signal_connect(G_OBJECT(eb), "button-press-event", - G_CALLBACK(_ui_toast_button_press_event), - darktable.gui->ui->toast_msg); - gtk_widget_set_events(eb, GDK_BUTTON_PRESS_MASK | darktable.gui->scroll_mask); - g_signal_connect(G_OBJECT(eb), "scroll-event", G_CALLBACK(_scrolled), NULL); + dt_gui_connect_click(eb, _ui_log_button_press_event, NULL, darktable.gui->ui->toast_msg); + gtk_widget_set_events(eb, darktable.gui->scroll_mask); + dt_gui_connect_scroll(eb, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, _scrolled, NULL); gtk_label_set_ellipsize(GTK_LABEL(darktable.gui->ui->toast_msg), PANGO_ELLIPSIZE_MIDDLE); PangoAttrList *attrlist = pango_attr_list_new(); @@ -2787,13 +2832,16 @@ static void _add_remove_modules(dt_action_t *action) dt_gui_menu_popup(GTK_MENU(menu), NULL, 0, 0); } -static gboolean _side_panel_press(GtkWidget *widget, - const GdkEvent *event, - gpointer user_data) +static void _side_panel_press(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - if(dt_gdk_event_get_button(event) == GDK_BUTTON_SECONDARY) - _add_remove_modules(NULL); - return TRUE; + // this gesture only serves right clicks; a primary press would be a + // shortcut-activated toggle/activate effect (see dt_gui_current_button) + if(dt_gui_current_button(gesture) != GDK_BUTTON_SECONDARY) return; + _add_remove_modules(NULL); } static gboolean _side_panel_draw(GtkWidget *widget, @@ -2810,6 +2858,52 @@ static gboolean _side_panel_draw(GtkWidget *widget, return FALSE; } +#if GTK_CHECK_VERSION(4, 0, 0) +/* GTK4 panel-scroll gating: a BUBBLE controller on the panel's content box + * decides whether the panel's scrolled window may scroll. It runs only when + * the event bubbles up from the background (a child control with its own + * scroll handling claims the event at TARGET phase first and never reaches + * this controller); returning PROPAGATE lets the scrolled window's internal + * controller do the actual scrolling, STOP consumes the scroll just like the + * GTK3 handler's return TRUE. See the GTK3 signal handler below. */ +static gboolean _panel_center_scroll(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) +{ + const GdkModifierType mods = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)) + & gtk_accelerator_get_default_mod_mask(); + return ((mods != darktable.gui->sidebar_scroll_mask) + != dt_conf_get_bool("darkroom/ui/sidebar_scroll_default")) + ? GDK_EVENT_STOP : GDK_EVENT_PROPAGATE; +} + +/* GTK4 border scroll: forward the scroll over a side border to the panel's + * scrolled window by scrolling its adjustment directly, honoring the same + * modifiers/config gate (the GTK3 path forwards the raw event, which the + * panel's scroll-event handler then gates). */ +static gboolean _borders_scrolled_controller(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) +{ + GtkWidget *sw = user_data; + const GdkModifierType mods = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)) + & gtk_accelerator_get_default_mod_mask(); + if(((mods != darktable.gui->sidebar_scroll_mask) + != dt_conf_get_bool("darkroom/ui/sidebar_scroll_default"))) + return GDK_EVENT_STOP; // gated away, same as the GTK3 consume + + GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)); + if(adj) + gtk_adjustment_set_value + (adj, gtk_adjustment_get_value(adj) + dy * gtk_adjustment_get_step_increment(adj)); + return GDK_EVENT_STOP; +} +#endif + static GtkWidget *_ui_init_panel_container_center(GtkWidget *container, const gboolean left) { @@ -2826,6 +2920,17 @@ static GtkWidget *_ui_init_panel_container_center(GtkWidget *container, : GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(sw), TRUE); +#if GTK_CHECK_VERSION(4, 0, 0) + { + GtkWidget *const border = + left ? darktable.gui->widgets.right_border : darktable.gui->widgets.left_border; + GtkEventController *border_scroll = + gtk_event_controller_scroll_new(border, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + dt_gui_add_controller(border, border_scroll); + g_signal_connect(border_scroll, "scroll", + G_CALLBACK(_borders_scrolled_controller), sw); + } +#else g_signal_connect( G_OBJECT(left ? darktable.gui->widgets.right_border : darktable.gui->widgets.left_border), "scroll-event", @@ -2834,17 +2939,25 @@ static GtkWidget *_ui_init_panel_container_center(GtkWidget *container, /* avoid scrolling with wheel, it's distracting (you'll end up over * a control, and scroll it's value), only scroll on modifier */ - // GTK4: this is absolutely not GTK4 compatible, but there is no way - // in GTK3 to have child widgets have their own scroll behavior and - // have a GtkEventControllerScroll on the parent GtkScrolledWindow. g_signal_connect(G_OBJECT(sw), "scroll-event", G_CALLBACK(_ui_init_panel_container_center_scroll_event), NULL); +#endif /* create the container */ GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_widget_set_name(box, "plugins_vbox_left"); gtk_container_add(GTK_CONTAINER(sw), box); +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4 panel-scroll gating, see _panel_center_scroll */ + { + GtkEventController *panel_scroll = + gtk_event_controller_scroll_new(box, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES); + gtk_event_controller_set_propagation_phase(panel_scroll, GTK_PHASE_BUBBLE); + dt_gui_add_controller(box, panel_scroll); + g_signal_connect(panel_scroll, "scroll", G_CALLBACK(_panel_center_scroll), NULL); + } +#endif g_signal_connect_swapped(box, "draw", G_CALLBACK(_side_panel_draw), NULL); GtkWidget *empty = gtk_event_box_new(); @@ -2854,8 +2967,7 @@ static GtkWidget *_ui_init_panel_container_center(GtkWidget *container, g_signal_connect(empty, "drag-motion", G_CALLBACK(_on_drag_motion_drop), GINT_TO_POINTER(FALSE)); g_signal_connect(empty, "drag-drop", G_CALLBACK(_on_drag_motion_drop), GINT_TO_POINTER(TRUE)); g_signal_connect(empty, "drag-leave", G_CALLBACK(_on_drag_leave), NULL); - g_signal_connect(empty, "button-press-event", G_CALLBACK(_side_panel_press), NULL); - gtk_widget_add_events(empty, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); + dt_gui_connect_click_secondary(empty, _side_panel_press, NULL, NULL); dt_action_t *ac = dt_action_define(&darktable.control->actions_global, NULL, N_("show/hide modules"), empty, NULL); dt_action_register(ac, NULL, _add_remove_modules, 0, 0); @@ -2872,61 +2984,78 @@ static GtkWidget *_ui_init_panel_container_bottom(GtkWidget *container) static int panel_drag_start_size = 0; static gdouble panel_drag_start_x = 0.0; -static gboolean _panel_handle_button_callback(GtkWidget *w, - const GdkEventButton *e, - gpointer user_data) +static void _panel_handle_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - if(e->button == GDK_BUTTON_PRIMARY) - { - if(e->type == GDK_BUTTON_PRESS) - { - GtkWidget *widget = (GtkWidget *)user_data; + if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; - panel_drag_start_x = e->x_root; + GtkWidget *handle = dt_gui_get_widget(gesture); + GtkWidget *widget = (GtkWidget *)user_data; - if(strcmp(gtk_widget_get_name(w), "panel-handle-bottom") == 0) - panel_drag_start_size = gtk_widget_get_allocated_height(widget); - else - panel_drag_start_size = gtk_widget_get_allocated_width(widget); + if(n_press == 2) + { + // double-click hides the panel + darktable.gui->widgets.panel_handle_dragging = FALSE; + if(strcmp(gtk_widget_get_name(handle), "panel-handle-right") == 0) + dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_RIGHT, FALSE, TRUE); + else if(strcmp(gtk_widget_get_name(handle), "panel-handle-left") == 0) + dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_LEFT, FALSE, TRUE); + else if(strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0) + dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_BOTTOM, FALSE, TRUE); + return; + } - darktable.gui->widgets.panel_handle_dragging = TRUE; - } - else if(e->type == GDK_BUTTON_RELEASE) - { - darktable.gui->widgets.panel_handle_dragging = FALSE; - } - else if(e->type == GDK_2BUTTON_PRESS) - { - darktable.gui->widgets.panel_handle_dragging = FALSE; - // we hide the panel - if(strcmp(gtk_widget_get_name(w), "panel-handle-right") == 0) - dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_RIGHT, FALSE, TRUE); - else if(strcmp(gtk_widget_get_name(w), "panel-handle-left") == 0) - dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_LEFT, FALSE, TRUE); - else if(strcmp(gtk_widget_get_name(w), "panel-handle-bottom") == 0) - dt_ui_panel_show(darktable.gui->ui, DT_UI_PANEL_BOTTOM, FALSE, TRUE); - } + { + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + gdouble root_x = 0, root_y = 0; + if(event) dt_gui_get_event_coords(event, &root_x, &root_y); + panel_drag_start_x = root_x; } - return TRUE; + + if(strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0) + panel_drag_start_size = gtk_widget_get_allocated_height(widget); + else + panel_drag_start_size = gtk_widget_get_allocated_width(widget); + + darktable.gui->widgets.panel_handle_dragging = TRUE; +} + +static void _panel_handle_button_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) +{ + darktable.gui->widgets.panel_handle_dragging = FALSE; } -static gboolean _panel_handle_cursor_callback(GtkWidget *w, - const GdkEventCrossing *e, - gpointer user_data) +static void _panel_handle_cursor_set(GtkWidget *handle, const gboolean entering) { // GTK produces a lot of GDK_NOTIFY_ANCESTOR when dragging handle, // but we only care about events when enter/leave the drag region if(darktable.gui->widgets.panel_handle_dragging) - return FALSE; - if(strcmp(gtk_widget_get_name(w), "panel-handle-bottom") == 0) - dt_control_change_cursor((e->type == GDK_ENTER_NOTIFY) - ? "ns-resize" - : "default"); + return; + if(strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0) + dt_control_change_cursor(entering ? "ns-resize" : "default"); else - dt_control_change_cursor((e->type == GDK_ENTER_NOTIFY) - ? "ew-resize" - : "default"); - return TRUE; + dt_control_change_cursor(entering ? "ew-resize" : "default"); +} + +static void _panel_handle_cursor_enter(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) +{ + _panel_handle_cursor_set(dt_gui_get_widget(controller), TRUE); +} + +static void _panel_handle_cursor_leave(GtkEventControllerMotion *controller, + gpointer user_data) +{ + _panel_handle_cursor_set(dt_gui_get_widget(controller), FALSE); } static void _panel_set_side_panel_width(GtkWidget *widget, const dt_ui_panel_t panel, const gdouble delta_x) @@ -2962,28 +3091,33 @@ static void _panel_set_side_panel_width(GtkWidget *widget, const dt_ui_panel_t p dt_ui_panel_set_size(darktable.gui->ui, panel, sx); } -static gboolean _panel_handle_motion_callback(GtkWidget *w, - const GdkEventMotion *e, - const gpointer user_data) +static void _panel_handle_motion_callback(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + const gpointer user_data) { + GtkWidget *handle = dt_gui_get_widget(controller); GtkWidget *widget = (GtkWidget *)user_data; if(darktable.gui->widgets.panel_handle_dragging) { - const gdouble delta_x = e->x_root - panel_drag_start_x; + gdouble root_x = 0, root_y = 0; + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) dt_gui_get_event_coords(event, &root_x, &root_y); + const gdouble delta_x = root_x - panel_drag_start_x; - if(strcmp(gtk_widget_get_name(w), "panel-handle-right") == 0) + if(strcmp(gtk_widget_get_name(handle), "panel-handle-right") == 0) { _panel_set_side_panel_width(widget, DT_UI_PANEL_RIGHT, -delta_x); } - else if(strcmp(gtk_widget_get_name(w), "panel-handle-left") == 0) + else if(strcmp(gtk_widget_get_name(handle), "panel-handle-left") == 0) { _panel_set_side_panel_width(widget, DT_UI_PANEL_LEFT, delta_x); } - else if(strcmp(gtk_widget_get_name(w), "panel-handle-bottom") == 0) + else if(strcmp(gtk_widget_get_name(handle), "panel-handle-bottom") == 0) { const gint sy = gtk_widget_get_allocated_height(widget); int sx = panel_drag_start_size; - sx = CLAMP((sy + darktable.gui->widgets.panel_handle_y - e->y), + sx = CLAMP((sy + darktable.gui->widgets.panel_handle_y - y), darktable.gui->dpi_factor * dt_conf_get_int("min_panel_height"), darktable.gui->dpi_factor * dt_conf_get_int("max_panel_height")); dt_ui_panel_set_size(darktable.gui->ui, DT_UI_PANEL_BOTTOM, sx); @@ -2991,10 +3125,10 @@ static gboolean _panel_handle_motion_callback(GtkWidget *w, } gtk_widget_queue_resize(widget); - return TRUE; +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif } - - return FALSE; } static void _ui_init_panel_left(dt_ui_t *ui, @@ -3015,22 +3149,11 @@ static void _ui_init_panel_left(dt_ui_t *ui, gtk_widget_set_valign(handle, GTK_ALIGN_FILL); gtk_widget_set_size_request(handle, DT_RESIZE_HANDLE_SIZE, -1); gtk_overlay_add_overlay(GTK_OVERLAY(over), handle); - gtk_widget_set_events(handle, - GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - | GDK_ENTER_NOTIFY_MASK - | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK); gtk_widget_set_name(GTK_WIDGET(handle), "panel-handle-left"); - g_signal_connect(G_OBJECT(handle), "button-press-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "button-release-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "motion-notify-event", - G_CALLBACK(_panel_handle_motion_callback), widget); - g_signal_connect(G_OBJECT(handle), "leave-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); - g_signal_connect(G_OBJECT(handle), "enter-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); + dt_gui_connect_click(handle, _panel_handle_button_pressed, _panel_handle_button_released, widget); + dt_gui_connect_motion(handle, _panel_handle_motion_callback, + _panel_handle_cursor_enter, _panel_handle_cursor_leave, NULL); gtk_widget_show(handle); gtk_grid_attach(GTK_GRID(container), over, 1, 1, 1, 1); @@ -3066,21 +3189,11 @@ static void _ui_init_panel_right(dt_ui_t *ui, gtk_widget_set_valign(handle, GTK_ALIGN_FILL); gtk_widget_set_size_request(handle, DT_RESIZE_HANDLE_SIZE, -1); gtk_overlay_add_overlay(GTK_OVERLAY(over), handle); - gtk_widget_set_events(handle, - GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - | GDK_ENTER_NOTIFY_MASK - | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK); gtk_widget_set_name(GTK_WIDGET(handle), "panel-handle-right"); - g_signal_connect(G_OBJECT(handle), "button-press-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "button-release-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "motion-notify-event", - G_CALLBACK(_panel_handle_motion_callback), widget); - g_signal_connect(G_OBJECT(handle), "leave-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); - g_signal_connect(G_OBJECT(handle), "enter-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); + + dt_gui_connect_click(handle, _panel_handle_button_pressed, _panel_handle_button_released, widget); + dt_gui_connect_motion(handle, _panel_handle_motion_callback, + _panel_handle_cursor_enter, _panel_handle_cursor_leave, NULL); gtk_widget_show(handle); gtk_grid_attach(GTK_GRID(container), over, 3, 1, 1, 1); @@ -3152,22 +3265,11 @@ static void _ui_init_panel_bottom(dt_ui_t *ui, gtk_widget_set_size_request(handle, -1, DT_RESIZE_HANDLE_SIZE); gtk_overlay_add_overlay(GTK_OVERLAY(over), handle); - gtk_widget_set_events(handle, - GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK - | GDK_ENTER_NOTIFY_MASK - | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK); gtk_widget_set_name(GTK_WIDGET(handle), "panel-handle-bottom"); - g_signal_connect(G_OBJECT(handle), "button-press-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "button-release-event", - G_CALLBACK(_panel_handle_button_callback), widget); - g_signal_connect(G_OBJECT(handle), "motion-notify-event", - G_CALLBACK(_panel_handle_motion_callback), widget); - g_signal_connect(G_OBJECT(handle), "leave-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); - g_signal_connect(G_OBJECT(handle), "enter-notify-event", - G_CALLBACK(_panel_handle_cursor_callback), handle); + dt_gui_connect_click(handle, _panel_handle_button_pressed, _panel_handle_button_released, widget); + dt_gui_connect_motion(handle, _panel_handle_motion_callback, + _panel_handle_cursor_enter, _panel_handle_cursor_leave, NULL); gtk_widget_show(handle); gtk_grid_attach(GTK_GRID(container), over, 1, 2, 3, 1); @@ -3988,12 +4090,15 @@ static void _notebook_size_callback(GtkNotebook *notebook, // GTK_STATE_FLAG_PRELIGHT does not seem to get set on the label on // hover so state-flags-changed cannot update // darktable.control->element for shortcut mapping -static gboolean _notebook_motion_notify_callback(GtkNotebook *notebook, - const GdkEventMotion *event, - gpointer user_data) -{ - if(gtk_get_event_widget((GdkEvent*)event) != GTK_WIDGET(notebook)) return FALSE; - +static void _notebook_motion_notify_callback(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) +{ + // a TARGET-phase controller only fires when the notebook itself is the + // event target, i.e. not while hovering the tab labels - same gate as the + // old gtk_get_event_widget() check + GtkNotebook *notebook = GTK_NOTEBOOK(dt_gui_get_widget(controller)); GtkAllocation notebook_alloc, label_alloc; gtk_widget_get_allocation(GTK_WIDGET(notebook), ¬ebook_alloc); @@ -4003,14 +4108,12 @@ static gboolean _notebook_motion_notify_callback(GtkNotebook *notebook, gtk_widget_get_allocation(gtk_notebook_get_tab_label (notebook, gtk_notebook_get_nth_page(notebook, i)), &label_alloc); - if(dt_gdk_event_get_x(event) + notebook_alloc.x < label_alloc.x + label_alloc.width) + if(x + notebook_alloc.x < label_alloc.x + label_alloc.width) { darktable.control->element = i; break; } } - - return FALSE; } static float _action_process_tabs(const gpointer target, @@ -4113,33 +4216,36 @@ GtkNotebook *dt_ui_notebook_new(dt_action_def_t *def) return _current_notebook; } -static gboolean _notebook_scroll_callback(GtkNotebook *notebook, - GdkEventScroll *event, - gpointer user_data) { - if(dt_gui_ignore_scroll(event)) return FALSE; +static void _notebook_scroll_callback(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) +{ + if(dt_gui_ignore_scroll_controller(controller)) return; - int delta_x = 0, delta_y = 0; - if(dt_gui_get_scroll_unit_deltas(event, &delta_x, &delta_y)) - { - // RIGHT: delta_x > 0, DOWN: delta_y > 0 -> next, like in filmstrip and lists - const int delta = abs(delta_x) > abs(delta_y) ? -delta_x : -delta_y; - _action_process_tabs(notebook, DT_ACTION_EFFECT_DEFAULT_KEY, - delta < 0 - ? DT_ACTION_EFFECT_NEXT - : DT_ACTION_EFFECT_PREVIOUS, delta); - } + // the DISCRETE controller already accumulated smooth deltas into units + const int delta_x = (int)dx, delta_y = (int)dy; + if(delta_x == 0 && delta_y == 0) return; - return TRUE; + // RIGHT: delta_x > 0, DOWN: delta_y > 0 -> next, like in filmstrip and lists + const int delta = abs(delta_x) > abs(delta_y) ? -delta_x : -delta_y; + _action_process_tabs(GTK_NOTEBOOK(dt_gui_get_widget(controller)), + DT_ACTION_EFFECT_DEFAULT_KEY, + delta < 0 + ? DT_ACTION_EFFECT_NEXT + : DT_ACTION_EFFECT_PREVIOUS, delta); } -static gboolean _notebook_button_press_callback(GtkNotebook *notebook, - const GdkEventButton *event, - gpointer user_data) +static void _notebook_button_press_callback(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - if(dt_gdk_event_get_type(event) == GDK_2BUTTON_PRESS && gtk_get_event_widget((GdkEvent*)event) == GTK_WIDGET(notebook)) - _reset_all_bauhaus(notebook, gtk_notebook_get_nth_page(notebook, gtk_notebook_get_current_page(notebook))); + if(n_press != 2) return; - return FALSE; + GtkNotebook *notebook = GTK_NOTEBOOK(dt_gui_get_widget(gesture)); + _reset_all_bauhaus(notebook, gtk_notebook_get_nth_page(notebook, gtk_notebook_get_current_page(notebook))); } GtkWidget *dt_ui_notebook_page(GtkNotebook *notebook, @@ -4167,13 +4273,22 @@ GtkWidget *dt_ui_notebook_page(GtkNotebook *notebook, { g_signal_connect(G_OBJECT(notebook), "size-allocate", G_CALLBACK(_notebook_size_callback), NULL); - g_signal_connect(G_OBJECT(notebook), "motion-notify-event", - G_CALLBACK(_notebook_motion_notify_callback), NULL); - g_signal_connect(G_OBJECT(notebook), "scroll-event", - G_CALLBACK(_notebook_scroll_callback), NULL); - g_signal_connect(G_OBJECT(notebook), "button-press-event", - G_CALLBACK(_notebook_button_press_callback), NULL); - gtk_widget_add_events(GTK_WIDGET(notebook), darktable.gui->scroll_mask); + dt_gui_connect_motion(GTK_WIDGET(notebook), _notebook_motion_notify_callback, NULL, NULL, NULL); + dt_gui_connect_scroll(GTK_WIDGET(notebook), + GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE, + _notebook_scroll_callback, NULL); + GtkGestureSingle *const notebook_click = + dt_gui_connect_click(GTK_WIDGET(notebook), _notebook_button_press_callback, NULL, NULL); +#if GTK_CHECK_VERSION(4, 0, 0) + (void)notebook_click; +#else + /* GtkNotebook overrides the button-press class handler and never chains + * up to gtk_widget_real_button_event(), so a BUBBLE-phase gesture never + * sees notebook presses; TARGET phase dispatches from + * gtk_widget_event_internal() for events delivered to the notebook + * itself (tab presses do target it -- GtkLabel has no own window). */ + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(notebook_click), GTK_PHASE_TARGET); +#endif } if(_current_action_def) { @@ -4319,7 +4434,7 @@ static gboolean _resize_wrap_scroll(GtkScrolledWindow *sw, { // no move needed int delta_y = 0; - dt_gui_get_scroll_unit_delta(event, &delta_y); + dt_gui_get_scroll_unit_delta((const GdkEvent *)event, &delta_y); if(delta_y == 0 ) return FALSE; @@ -4366,7 +4481,7 @@ static gboolean _scroll_wrap_height(GtkWidget *w, if(dt_modifier_is(dt_gdk_event_get_state(event), GDK_SHIFT_MASK | GDK_MOD1_MASK)) { int delta_y; - if(dt_gui_get_scroll_unit_delta(event, &delta_y)) + if(dt_gui_get_scroll_unit_delta((const GdkEvent *)event, &delta_y)) { //adjust height const int height = dt_conf_get_int(config_str) + delta_y; @@ -4379,6 +4494,85 @@ static gboolean _scroll_wrap_height(GtkWidget *w, return FALSE; } +#if GTK_CHECK_VERSION(4, 0, 0) +/* GTK4 version of _resize_wrap_scroll: the scroll controller callback can + * return GDK_EVENT_PROPAGATE, so the "at the end of the inner scrolled + * window, let the parent scroll" pass-through is expressible (the GTK3 + * handler had to gtk_propagate_event() manually). CAPTURE phase runs before + * the scrolled window's own BUBBLE controller, so no double scroll. */ +static gboolean _resize_wrap_scroll_controller(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) +{ + GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(dt_gui_get_widget(controller)); + const char *config_str = user_data; + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); + const int delta_y = (int)dy; + if(delta_y == 0) return GDK_EVENT_PROPAGATE; + + GtkWidget *w = gtk_bin_get_child(GTK_BIN(sw)); + if(GTK_IS_VIEWPORT(w)) w = gtk_bin_get_child(GTK_BIN(w)); + + const gint increment = _get_container_row_heigth(w); + + if(dt_modifier_is(state, GDK_SHIFT_MASK | GDK_MOD1_MASK)) + { + const gint new_size = dt_conf_get_int(config_str) + increment * delta_y; + + dt_toast_log(_("never show more than %d lines"), 1 + new_size / increment); + + dt_conf_set_int(config_str, new_size); + gtk_widget_queue_draw(w); + return GDK_EVENT_STOP; + } + + GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(sw); + + const gint before = gtk_adjustment_get_value(adj); + + gint value = before + increment * delta_y; + + value -= value % increment; + gtk_adjustment_set_value(adj, value); + const gint after = gtk_adjustment_get_value(adj); + + // pass through to the parent scrolled window when the inner one can't move + return after == before ? GDK_EVENT_PROPAGATE : GDK_EVENT_STOP; +} + +/* GTK4 version of _scroll_wrap_height: the drawing area has no scrolled + * window of its own, so plain scrolls propagate to the panel's scrolled + * window (the GTK3 handler returned FALSE for that). */ +static gboolean _scroll_wrap_height_controller(GtkEventControllerScroll *controller, + gdouble dx, + gdouble dy, + gpointer user_data) +{ + GtkWidget *w = dt_gui_get_widget(controller); + const char *config_str = user_data; + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); + + if(dt_modifier_is(state, GDK_SHIFT_MASK | GDK_MOD1_MASK)) + { + const int delta_y = (int)dy; + if(delta_y != 0) + { + //adjust height + const int height = dt_conf_get_int(config_str) + delta_y; + dt_conf_set_int(config_str, height); + dtgtk_drawing_area_set_height(w, height); + } + return GDK_EVENT_STOP; + } + + // otherwise let the enclosing scrolled window handle it + return GDK_EVENT_PROPAGATE; +} +#endif + static gboolean _resize_wrap_dragging = FALSE; static gboolean _resize_wrap_handle_hover = FALSE; static GtkWidget *_resize_wrap_hovered = NULL; @@ -4406,15 +4600,22 @@ static gboolean _resize_wrap_draw_handle(GtkWidget *w, return FALSE; } -static gboolean _resize_wrap_motion(GtkWidget *widget, - const GdkEventMotion *event, - const char *config_str) +/* controller version: the drag uses the controller-relative y, the hover + * check keeps the old window comparison via the current event (BUBBLE phase + * delivers motions over child widgets too, as the old signal did) */ +static void _resize_wrap_motion_controller(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) { + GtkWidget *widget = dt_gui_get_widget(controller); + const char *config_str = user_data; + if(_resize_wrap_dragging) { // keeps resize box from shrinking when user clicks above very // bottom of handle - const int new_height = round(dt_gdk_event_get_y(event) + 0.5*DT_RESIZE_HANDLE_SIZE); + const int new_height = round(y + 0.5 * DT_RESIZE_HANDLE_SIZE); if(DTGTK_IS_DRAWING_AREA(widget)) { // enforce configuration limits @@ -4427,15 +4628,16 @@ static gboolean _resize_wrap_motion(GtkWidget *widget, dt_conf_set_int(config_str, new_height); gtk_widget_queue_draw(gtk_bin_get_child(GTK_BIN(gtk_bin_get_child(GTK_BIN(widget))))); } - return TRUE; + return; } const gboolean prior = _resize_wrap_handle_hover; - if(!(dt_gdk_event_get_state(event) & GDK_BUTTON1_MASK) - && dt_gdk_event_get_window(event) == gtk_widget_get_window(widget)) + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(!(dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)) & GDK_BUTTON1_MASK) + && (!event || dt_gdk_event_get_window(event) == gtk_widget_get_window(widget))) { _resize_wrap_handle_hover = - dt_gdk_event_get_y(event) >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE; + y >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE; if(_resize_wrap_handle_hover != prior) { if(_resize_wrap_handle_hover) @@ -4446,46 +4648,67 @@ static gboolean _resize_wrap_motion(GtkWidget *widget, gtk_widget_queue_draw(widget); } } +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif +} - return _resize_wrap_handle_hover; +static void _resize_wrap_button_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) +{ + GtkWidget *widget = dt_gui_get_widget(gesture); + if(y >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE + && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) + { + _resize_wrap_dragging = TRUE; + } } -static gboolean _resize_wrap_button(GtkWidget *widget, - const GdkEventButton *event, - const char *config_str) +static void _resize_wrap_button_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - if(_resize_wrap_dragging - && dt_gdk_event_get_type(event) == GDK_BUTTON_RELEASE) + if(_resize_wrap_dragging) { _resize_wrap_dragging = FALSE; dt_control_clear_temp_cursor(); - return TRUE; } - else if(dt_gdk_event_get_y(event) >= gtk_widget_get_allocated_height(widget) - DT_RESIZE_HANDLE_SIZE - && dt_gdk_event_get_type(event) == GDK_BUTTON_PRESS - && dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) - { - _resize_wrap_dragging = TRUE; - return TRUE; - } - - return FALSE; } -static gboolean _resize_wrap_enter_leave(GtkWidget *widget, - const GdkEventCrossing *event, - const char *config_str) +static void _resize_wrap_enter_leave_controller(GtkEventControllerMotion *controller, + gpointer user_data, + const gboolean is_enter) { + GtkWidget *widget = dt_gui_get_widget(controller); + + // the crossing detail/mode distinguish child crossings and grab-break + // leaves; with the BUBBLE-phase motion controller they arrive as the + // controller's current event, exactly as the old signal's GdkEventCrossing + GdkCrossingMode mode = GDK_CROSSING_NORMAL; + GdkNotifyType detail = GDK_NOTIFY_NONLINEAR; + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) + { +#if GTK_CHECK_VERSION(4, 0, 0) + mode = gdk_crossing_event_get_mode(event); + detail = gdk_crossing_event_get_detail(event); +#else + mode = event->crossing.mode; + detail = event->crossing.detail; +#endif + } + _resize_wrap_hovered = - dt_gdk_event_get_type(event) == GDK_ENTER_NOTIFY - || event->detail == GDK_NOTIFY_INFERIOR - || _resize_wrap_dragging ? widget : NULL; + is_enter || detail == GDK_NOTIFY_INFERIOR || _resize_wrap_dragging ? widget : NULL; // When leave handle and widget, remove temp resize cursor. When // enter widget, motion event will handle cursor change for handle. - if(dt_gdk_event_get_type(event) == GDK_LEAVE_NOTIFY - && !_resize_wrap_dragging - && _resize_wrap_handle_hover) + if(!is_enter && !_resize_wrap_dragging && _resize_wrap_handle_hover) { dt_control_clear_temp_cursor(); _resize_wrap_handle_hover = FALSE; @@ -4493,12 +4716,28 @@ static gboolean _resize_wrap_enter_leave(GtkWidget *widget, gtk_widget_queue_draw(widget); - if(event->mode == GDK_CROSSING_GTK_UNGRAB) + if(mode == GDK_CROSSING_GTK_UNGRAB) _resize_wrap_dragging = FALSE; +#if !GTK_CHECK_VERSION(4, 0, 0) + if(event) gdk_event_free(event); +#endif +} - return FALSE; +static void _resize_wrap_enter_controller(GtkEventControllerMotion *controller, + gdouble x, + gdouble y, + gpointer user_data) +{ + _resize_wrap_enter_leave_controller(controller, user_data, TRUE); } +static void _resize_wrap_leave_controller(GtkEventControllerMotion *controller, + gpointer user_data) +{ + _resize_wrap_enter_leave_controller(controller, user_data, FALSE); +} + + GtkWidget *dt_ui_resize_wrap(GtkWidget *w, const gint min_size, char *config_str) @@ -4513,10 +4752,15 @@ GtkWidget *dt_ui_resize_wrap(GtkWidget *w, { const float height = dt_conf_get_int(config_str); dtgtk_drawing_area_set_height(w, height); - g_signal_connect(G_OBJECT(w), - "scroll-event", - G_CALLBACK(_scroll_wrap_height), - config_str); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController *scroll = gtk_event_controller_scroll_new + (w, GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE); + dt_gui_add_controller(w, scroll); + g_signal_connect(scroll, "scroll", G_CALLBACK(_scroll_wrap_height_controller), config_str); +#else + g_signal_connect(G_OBJECT(w), "scroll-event", + G_CALLBACK(_scroll_wrap_height), config_str); +#endif } else { @@ -4525,8 +4769,16 @@ GtkWidget *dt_ui_resize_wrap(GtkWidget *w, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); gtk_scrolled_window_set_min_content_height (GTK_SCROLLED_WINDOW(sw), - DT_PIXEL_APPLY_DPI(min_size)); +#if GTK_CHECK_VERSION(4, 0, 0) + GtkEventController *scroll = gtk_event_controller_scroll_new + (sw, GTK_EVENT_CONTROLLER_SCROLL_VERTICAL | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE); + gtk_event_controller_set_propagation_phase(scroll, GTK_PHASE_CAPTURE); + dt_gui_add_controller(sw, scroll); + g_signal_connect(scroll, "scroll", G_CALLBACK(_resize_wrap_scroll_controller), config_str); +#else g_signal_connect(G_OBJECT(sw), "scroll-event", G_CALLBACK(_resize_wrap_scroll), config_str); +#endif g_signal_connect(G_OBJECT(w), "draw", G_CALLBACK(_resize_wrap_draw), config_str); gtk_widget_set_margin_bottom(sw, DT_RESIZE_HANDLE_SIZE); @@ -4537,16 +4789,19 @@ GtkWidget *dt_ui_resize_wrap(GtkWidget *w, gtk_widget_add_events(w, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask); - g_signal_connect(G_OBJECT(w), "motion-notify-event", - G_CALLBACK(_resize_wrap_motion), config_str); - g_signal_connect(G_OBJECT(w), "button-press-event", - G_CALLBACK(_resize_wrap_button), config_str); - g_signal_connect(G_OBJECT(w), "button-release-event", - G_CALLBACK(_resize_wrap_button), config_str); - g_signal_connect(G_OBJECT(w), "enter-notify-event", - G_CALLBACK(_resize_wrap_enter_leave), config_str); - g_signal_connect(G_OBJECT(w), "leave-notify-event", - G_CALLBACK(_resize_wrap_enter_leave), config_str); + + // resize handle interaction: click gesture + motion/enter/leave controller + // (BUBBLE phase so motions over the wrapped child keep driving the drag, + // like the old signal did) + dt_gui_connect_click(w, _resize_wrap_button_pressed, _resize_wrap_button_released, config_str); + { + GtkEventController *motion = gtk_event_controller_motion_new(w); + gtk_event_controller_set_propagation_phase(motion, GTK_PHASE_BUBBLE); + dt_gui_add_controller(w, motion); + g_signal_connect(motion, "motion", G_CALLBACK(_resize_wrap_motion_controller), config_str); + g_signal_connect(motion, "enter", G_CALLBACK(_resize_wrap_enter_controller), config_str); + g_signal_connect(motion, "leave", G_CALLBACK(_resize_wrap_leave_controller), config_str); + } g_signal_connect_after(G_OBJECT(w), "draw", G_CALLBACK(_resize_wrap_draw_handle), NULL); @@ -4646,6 +4901,24 @@ void dt_gui_menu_popup(GtkMenu *menu, gdk_event_free(event); } +gboolean dt_gui_forward_scroll(GtkEventControllerScroll *controller, + GtkWidget *target) +{ +#if !GTK_CHECK_VERSION(4, 0, 0) + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(!event) return FALSE; + gtk_widget_event(target, event); + gdk_event_free(event); + return TRUE; +#else + /* GTK4: no gtk_widget_event() -- reimplement as a scroll controller on the + * target widget (see the declaration in gtk.h). */ + (void)controller; + (void)target; + return FALSE; +#endif +} + // draw rounded rectangle void dt_gui_draw_rounded_rectangle(cairo_t *cr, const float width, @@ -4720,18 +4993,18 @@ static void _collapse_button_changed(GtkDarktableToggleButton *widget, dt_conf_set_bool(cs->confname, active); } -static gboolean _collapse_expander_click(GtkWidget *widget, - const GdkEventButton *e, - const gpointer user_data) +static void _collapse_expander_click(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + gpointer user_data) { - if(e->button != 1) return FALSE; + if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY) return; const dt_gui_collapsible_section_t *cs = (dt_gui_collapsible_section_t *)user_data; const gboolean active = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(cs->toggle)); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cs->toggle), !active); - - return TRUE; } void dt_gui_update_collapsible_section(const dt_gui_collapsible_section_t *cs) @@ -4793,8 +5066,7 @@ void dt_gui_new_collapsible_section(dt_gui_collapsible_section_t *cs, g_signal_connect(G_OBJECT(cs->toggle), "toggled", G_CALLBACK(_collapse_button_changed), cs); - g_signal_connect(G_OBJECT(header_evb), "button-press-event", - G_CALLBACK(_collapse_expander_click), cs); + dt_gui_connect_click(header_evb, _collapse_expander_click, NULL, cs); } void dt_gui_collapsible_section_set_label(dt_gui_collapsible_section_t *cs, @@ -4828,28 +5100,6 @@ 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) @@ -4883,6 +5133,31 @@ GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, return (GtkGestureSingle *)gesture; } +GtkGestureSingle *(dt_gui_connect_click_secondary)(GtkWidget *widget, + GCallback pressed, + GCallback released, + gpointer data) +{ + GtkGestureSingle *gesture = dt_gui_connect_click(widget, pressed, released, data); + gtk_gesture_single_set_button(gesture, GDK_BUTTON_SECONDARY); + /* make the right-button shortcut effects reachable: store the gesture under + * DT_ACTION_GESTURE_KEY so _action_process_toggle/_action_process_button + * route "right-toggle"/"right-on"/"activate-right" through the same + * "pressed" signal a real right-click produces. Handlers must read + * dt_gui_current_button()/dt_gui_current_state() (gtk.h) so a + * primary-effect shortcut (which emits a synthetic primary press) is not + * mistaken for a right click. */ + g_object_set_data(G_OBJECT(widget), DT_ACTION_GESTURE_KEY, gesture); + return gesture; +} + +void dt_gui_gesture_claim(GtkGesture *gesture, + GdkEventSequence *sequence, + gpointer user_data) +{ + gtk_gesture_set_sequence_state(gesture, sequence, GTK_EVENT_SEQUENCE_CLAIMED); +} + GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, GCallback drag_begin, GCallback drag_end, @@ -4900,6 +5175,110 @@ GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, return gesture; } +/* per-gesture state for dt_gui_connect_pinch: handler + active tracking. Kept + * on the gesture object (g_object_set_data_full) so it lives exactly as long + * as the gesture -- no static state, safe for several pinch gestures at once. */ +typedef struct dt_gui_pinch_ctx_t +{ + dt_gui_pinch_handler_t handler; + gpointer user_data; + gboolean active; +} dt_gui_pinch_ctx_t; + +static void _pinch_dispatch(GtkGesture *gesture, + const GdkTouchpadGesturePhase phase, + const gboolean have_event) +{ + dt_gui_pinch_ctx_t *ctx = g_object_get_data(G_OBJECT(gesture), "dt-gui-pinch-ctx"); + if(!ctx) return; + + dt_gui_pinch_event_t e = { 0 }; + e.phase = phase; + e.scale = 1.0; /* the default when the sequence's last event is gone (END) */ + if(have_event) + { + e.event = gtk_gesture_get_last_event(gesture, NULL); + if(e.event) + { + if(dt_gdk_event_get_type(e.event) != GDK_TOUCHPAD_PINCH) + { + // touchscreen pinches were never handled before: keep ignoring them + return; + } + dt_gdk_touchpad_pinch_get_deltas(e.event, &e.dx, &e.dy); + e.scale = dt_gdk_touchpad_pinch_get_scale(e.event); + e.state = dt_gdk_event_get_state(e.event) & 0xf; +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4 has no root-coords API: surface-relative position (see gtk.c) + gdk_event_get_position(e.event, &e.x, &e.y); +#else + e.x = dt_gdk_event_get_root_x(e.event); + e.y = dt_gdk_event_get_root_y(e.event); +#endif + } + } + + ctx->handler(gesture, &e, ctx->user_data); +} + +static void _pinch_begin(GtkGestureZoom *gesture, gpointer user_data) +{ + (void)user_data; + dt_gui_pinch_ctx_t *ctx = g_object_get_data(G_OBJECT(gesture), "dt-gui-pinch-ctx"); + if(!ctx || !darktable.gui->touchpad_gestures_enabled) return; + // GtkGestureZoom also recognizes touchscreen pinches, which were never + // handled before: only touchpad pinches set the active flag + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(!event || dt_gdk_event_get_type(event) != GDK_TOUCHPAD_PINCH) return; + ctx->active = TRUE; + _pinch_dispatch(GTK_GESTURE(gesture), GDK_TOUCHPAD_GESTURE_PHASE_BEGIN, TRUE); +} + +static void _pinch_scale_changed(GtkGestureZoom *gesture, + gdouble scale, + gpointer user_data) +{ + (void)scale; + (void)user_data; + dt_gui_pinch_ctx_t *ctx = g_object_get_data(G_OBJECT(gesture), "dt-gui-pinch-ctx"); + if(!ctx || !darktable.gui->touchpad_gestures_enabled || !ctx->active) return; + _pinch_dispatch(GTK_GESTURE(gesture), GDK_TOUCHPAD_GESTURE_PHASE_UPDATE, TRUE); +} + +static void _pinch_end(GtkGestureZoom *gesture, gpointer user_data) +{ + (void)user_data; + dt_gui_pinch_ctx_t *ctx = g_object_get_data(G_OBJECT(gesture), "dt-gui-pinch-ctx"); + if(!ctx || !ctx->active) return; + ctx->active = FALSE; + if(!darktable.gui->touchpad_gestures_enabled) return; + // also fires after cancel (see gtkgesture.c: the cancel path ends the + // sequence), so the consumer's END/CANCEL handling always runs; the + // sequence's last event is already gone here, hence no event data + _pinch_dispatch(GTK_GESTURE(gesture), GDK_TOUCHPAD_GESTURE_PHASE_END, FALSE); +} + +GtkGesture *(dt_gui_connect_pinch)(GtkWidget *widget, + dt_gui_pinch_handler_t handler, + gpointer data) +{ + GtkGesture *gesture = gtk_gesture_zoom_new(widget); + dt_gui_add_controller(widget, gesture); + // GTK4 GtkGesture *gesture = gtk_gesture_zoom_new(); + // gtk_widget_add_controller(widget, GTK_EVENT_CONTROLLER(gesture)); + + dt_gui_pinch_ctx_t *ctx = g_new0(dt_gui_pinch_ctx_t, 1); + ctx->handler = handler; + ctx->user_data = data; + g_object_set_data_full(G_OBJECT(gesture), "dt-gui-pinch-ctx", ctx, g_free); + + g_signal_connect(gesture, "begin", G_CALLBACK(_pinch_begin), NULL); + g_signal_connect(gesture, "scale-changed", G_CALLBACK(_pinch_scale_changed), NULL); + g_signal_connect(gesture, "end", G_CALLBACK(_pinch_end), NULL); + + return gesture; +} + GtkEventController *(dt_gui_connect_motion)(GtkWidget *widget, GCallback motion, GCallback enter, @@ -4941,9 +5320,6 @@ static gboolean _scroll_sidebar(GtkEventControllerScroll* controller, gdouble dy, GdkEvent* event) { - // GTK4: the sidebar scroll controller can capture scrolls then - // decide whether to propogate them or scroll itself depending on - // modifiers state, and this function will no longer be needed GtkWidget *const widget = dt_gui_get_widget(controller); const GtkWidget *panel = NULL; @@ -4951,13 +5327,29 @@ static gboolean _scroll_sidebar(GtkEventControllerScroll* controller, panel = darktable.gui->ui->panels[DT_UI_PANEL_LEFT]; else if(dt_ui_panel_ancestor(darktable.gui->ui, DT_UI_PANEL_RIGHT, widget)) panel = darktable.gui->ui->panels[DT_UI_PANEL_RIGHT]; - if(panel && dt_gui_ignore_scroll(&event->scroll)) + if(!panel) return FALSE; + +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4 has no raw event forwarding: the decision is taken from the + // modifiers and the panel's scrolled window is scrolled directly. + const gboolean ignore = _dt_gui_ignore_scroll + (dt_gdk_event_get_state(event) & gtk_accelerator_get_default_mod_mask()); +#else + const gboolean ignore = dt_gui_ignore_scroll(&event->scroll); +#endif + if(ignore) { - // FIXME: do we need to even check if in left/right panel? will this break if mouse over a widget within a GtkScrolledWindow within the panel GtkScrolledWindow? GtkWidget *const sw = gtk_widget_get_ancestor(widget, GTK_TYPE_SCROLLED_WINDOW); if(sw) { +#if GTK_CHECK_VERSION(4, 0, 0) + GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)); + if(adj) + gtk_adjustment_set_value + (adj, gtk_adjustment_get_value(adj) + dy * gtk_adjustment_get_step_increment(adj)); +#else gtk_widget_event(sw, event); +#endif return TRUE; } } @@ -4988,7 +5380,7 @@ static void _scroll_proxy_real(GtkEventControllerScroll* controller, gpointer user_data, gboolean discrete) { - GdkEvent *const event = gtk_get_current_event(); + GdkEvent *const event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!event) return; // FIXME: make sure this logic is right -- want to ignore emulated pointer events, attenuate scroll events with data, and use any deltas not emulated if(gdk_event_get_event_type(event) == GDK_SCROLL @@ -5031,7 +5423,9 @@ static void _scroll_proxy_real(GtkEventControllerScroll* controller, real_handler(controller, dx, dy, user_data); } } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } static void _scroll_proxy(GtkEventControllerScroll* controller, @@ -5200,40 +5594,6 @@ void dt_gui_process_events() continue; } -void dt_gui_simulate_button_event(GtkWidget *widget, - const GdkEventType eventtype, - const int button) -{ - gboolean res = FALSE; - - // Create the event GdkEventButton - GdkEventButton event; - memset(&event, 0, sizeof(event)); - - event.type = eventtype; - event.window = gtk_widget_get_window(widget); - event.send_event = TRUE; - event.time = GDK_CURRENT_TIME; - event.x = 0; // not important in this case - event.y = 0; // not important in this case - event.button = button; - event.device = - gdk_seat_get_pointer(gdk_display_get_default_seat(gdk_display_get_default())); - - if(event.window != NULL) - { - g_object_ref(event.window); - } - - // send signal - g_signal_emit_by_name(G_OBJECT(widget), "button-press-event", &event, &res, NULL); - - if(event.window != NULL) - { - g_object_unref(event.window); - } -} - GtkWidget *(dt_gui_box_add)(const char *file, const int line, const char *function, GtkBox *box, gpointer list[]) { if(!GTK_IS_BOX(box)) dt_print(DT_DEBUG_ALWAYS, "%s:%d %s: trying to add widgets to non-box container using dt_gui_box_add", file, line, function); diff --git a/src/gui/gtk.h b/src/gui/gtk.h index 8544eed11255..62c3cae6723c 100644 --- a/src/gui/gtk.h +++ b/src/gui/gtk.h @@ -226,19 +226,26 @@ double dt_get_screen_resolution(GtkWidget *widget); * modifiers are pressed to indicate the control should be scrolled, then remove * the modifiers from the event before returning false */ gboolean dt_gui_ignore_scroll(GdkEventScroll *event); +/* Same decision for a GtkEventControllerScroll callback: the modifiers are + * taken from the current event (GTK4: gtk_event_controller_get_current_event_state). + * The GdkEvent flavor clears the sidebar_scroll_mask from the event to consume + * the modifier; the controller world cannot mutate events, so the decision is + * identical but there is no side effect. */ +gboolean dt_gui_ignore_scroll_controller(GtkEventControllerScroll *controller); /* Scale factor converting normalized smooth scroll deltas (as returned by * dt_gui_get_scroll_deltas() on macOS) back to pixels for panning. */ #define DT_UI_SCROLL_SMOOTH_DELTA_SCALE 50.0 /* Return requested scroll delta(s) from event. If delta_x or delta_y * is NULL, do not return that delta. Return TRUE if requested deltas * can be retrieved. Handles both GDK_SCROLL_UP/DOWN/LEFT/RIGHT and - * GDK_SCROLL_SMOOTH style scroll events. */ -gboolean dt_gui_get_scroll_deltas(const GdkEventScroll *event, gdouble *delta_x, gdouble *delta_y); + * GDK_SCROLL_SMOOTH style scroll events. Takes the opaque GdkEvent + * (GTK4-compatible); GTK3 callers may pass a GdkEventScroll*. */ +gboolean dt_gui_get_scroll_deltas(const GdkEvent *event, gdouble *delta_x, gdouble *delta_y); /* Same as above, except accumulate smooth scrolls deltas of < 1 and * only set deltas and return TRUE once scrolls accumulate to >= 1. * Effectively makes smooth scroll events act like old-style unit * scroll events. */ -gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x, int *delta_y); +gboolean dt_gui_get_scroll_unit_deltas(const GdkEvent *event, int *delta_x, int *delta_y); /* Note that on macOS Shift+vertical scroll can be reported as Shift+horizontal scroll. * So if Shift changes scrolling effect, both scrolls should be handled the same. @@ -247,12 +254,12 @@ gboolean dt_gui_get_scroll_unit_deltas(const GdkEventScroll *event, int *delta_x /* Return delta of larger magnitude from the event. Return TRUE if any deltas * can be retrieved. Handles both GDK_SCROLL_UP/DOWN/LEFT/RIGHT and * GDK_SCROLL_SMOOTH style scroll events. */ -gboolean dt_gui_get_scroll_delta(const GdkEventScroll *event, gdouble *delta); +gboolean dt_gui_get_scroll_delta(const GdkEvent *event, gdouble *delta); /* Same as above, except accumulate smooth scrolls deltas of < 1 and * only set delta and return TRUE once scrolls accumulate to >= 1. * Effectively makes smooth scroll events act like old-style unit * scroll events. */ -gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, int *delta); +gboolean dt_gui_get_scroll_unit_delta(const GdkEvent *event, int *delta); /* Return TRUE if a scroll event should be treated as a touchpad pan gesture * (two-finger swipe) rather than a plain scroll: requires the touchpad-gestures @@ -261,7 +268,7 @@ gboolean dt_gui_get_scroll_unit_delta(const GdkEventScroll *event, int *delta); * trackpad reports as a mouse; a device that just produced a pinch/swipe * gesture also qualifies). Mouse wheels therefore never pan, even where GTK * delivers wheel scrolls as smooth events. */ -gboolean dt_gui_scroll_should_pan(const GdkEventScroll *event); +gboolean dt_gui_scroll_should_pan(const GdkEvent *event); /* Return the zoom delta (+/-0.5, positive = zoom in) for a scroll event. * Vertical scrolls zoom in on up. Horizontal scrolls zoom in on LEFT when @@ -270,7 +277,47 @@ gboolean dt_gui_scroll_should_pan(const GdkEventScroll *event); * otherwise (wheel tilt, two-finger swipe). Discrete events are resolved * from the normalized GDK direction; smooth (fractional) scrolls use their * dominant delta. dx/dy are the caller's deltas (raw or accumulated units). */ -float dt_gui_scroll_zoom_delta(const GdkEventScroll *event, gdouble dx, gdouble dy); +float dt_gui_scroll_zoom_delta(const GdkEvent *event, gdouble dx, gdouble dy); + +/* Event timestamp as seen by a controller callback (same contract as + * dt_gui_get_current_event()). */ +static inline guint32 dt_gui_get_current_event_time(GtkEventController *controller) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + GdkEvent *event = gtk_event_controller_get_current_event(controller); + return event ? gdk_event_get_time(event) : GDK_CURRENT_TIME; +#else + return gtk_get_current_event_time(); +#endif +} + +/* Current event as seen by a controller callback. GTK4 reads the borrowed + * event from the controller (do NOT free); GTK3 returns an owned copy of + * gtk_get_current_event() which the caller must gdk_event_free(). */ +static inline GdkEvent *dt_gui_get_current_event(GtkEventController *controller) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + return gtk_event_controller_get_current_event(controller); +#else + return gtk_get_current_event(); +#endif +} + +/* Event coordinates in the frame the old root-coords convention used: + * GTK3 root (screen-absolute) coordinates, GTK4 the surface-relative + * position (GTK4 has no root-coords API). Delta-based tracking (panel + * drag, culling/thumbtable pan) is unaffected; absolute positioning needs + * a coordinate decision on the GTK4 port. */ +static inline void dt_gui_get_event_coords(const GdkEvent *event, + gdouble *x, + gdouble *y) +{ +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_event_get_position(event, x, y); +#else + gdk_event_get_root_coords(event, x, y); +#endif +} /* * new ui api @@ -532,6 +579,15 @@ void dt_gui_menu_popup(GtkMenu *menu, GdkGravity widget_anchor, GdkGravity menu_anchor); +/* Forward the scroll event a scroll controller is currently dispatching to + * another widget, as if that widget had received it directly (used for the + * alt+scroll channel-tab switching in the wavelet/channel-band modules). + * GTK3-only: raw event forwarding via gtk_widget_event(); the GTK4 migration + * is to reimplement the forwarding as a scroll controller on the target + * widget itself (GTK4 has no gtk_widget_event). */ +gboolean dt_gui_forward_scroll(GtkEventControllerScroll *controller, + GtkWidget *target); + void dt_gui_draw_rounded_rectangle(cairo_t *cr, const float width, const float height, @@ -594,6 +650,10 @@ void dt_gui_add_controller(GtkWidget *widget, * (borrowed) and drop this helper. */ gboolean dt_gui_get_current_root_coords(gdouble *x, gdouble *y); +/* Current modifier state as seen by a controller callback. GTK4 reads it from + * the controller; GTK3 (which has no gtk_event_controller_get_current_event*) + * reads the event currently being dispatched, same as dt_gui_get_current_root_coords. */ +GdkModifierType dt_gui_get_current_event_state(GtkEventController *controller); GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, GCallback pressed, @@ -609,6 +669,27 @@ GtkGestureSingle *(dt_gui_connect_click)(GtkWidget *widget, #define dt_gui_connect_click_all(widget, pressed, released, data) \ dt_gui_connect_click(widget, pressed, released, data) +GtkGestureSingle *(dt_gui_connect_click_secondary)(GtkWidget *widget, + GCallback pressed, + GCallback released, + gpointer data); +/* dt_gui_connect_click() restricted to the right button: the gesture's button + * filter does the exclusivity, so the handler needs no button check. This is + * the right-click-popup idiom, previously written by hand at every call site. */ +#define dt_gui_connect_click_secondary(widget, pressed, released, data) ( \ + 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_secondary(GTK_WIDGET(widget), G_CALLBACK(pressed), G_CALLBACK(released), (data))) + +/* claim the event sequence for a gesture so that all its events (press, + * release, motion) are consumed and never reach the widget's class handlers + * or other controllers. Connect to the gesture's "begin" signal; implies + * GTK_PHASE_CAPTURE. Same pattern as dt_iop_togglebutton_new and the + * standalone picker buttons, survives GTK4 unchanged. */ +void dt_gui_gesture_claim(GtkGesture *gesture, + GdkEventSequence *sequence, + gpointer user_data); + GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, GCallback drag_begin, GCallback drag_end, @@ -620,6 +701,39 @@ GtkGesture *(dt_gui_connect_drag)(GtkWidget *widget, ASSERT_FUNC_TYPE(drag_update, void(*)(GtkGestureDrag *, double, double, __typeof__(data))), \ dt_gui_connect_drag(GTK_WIDGET(widget), G_CALLBACK(drag_begin), G_CALLBACK(drag_end), G_CALLBACK(drag_update), (data))) +/* touchpad pinch via GtkGestureZoom: the old GTK3-only "event" signal + * handler forwarded raw GDK_TOUCHPAD_PINCH events; the phase field becomes + * the begin / scale-changed / end signals (and "end" fires on cancel as + * well, so the consumer's END/CANCEL reset still runs). GtkGestureZoom also + * recognizes touchscreen pinches, which were never handled before -- ignored + * here for parity, only touchpad pinches reach the handler. dx/dy, scale, + * state and the focal point are pulled from the gesture's last event (root + * coords on GTK3, surface-relative on GTK4 -- see dt_gui_get_event_coords); + * on END the event is already gone, so the handler gets NULL and the deltas + * default to zero. The touchpad_gestures_enabled pref and the per-gesture + * active tracking live inside the helper; handlers only forward the parsed + * event (e.g. to dt_view_manager_gesture_pinch). */ +typedef struct dt_gui_pinch_event_t +{ + const GdkEvent *event; /* borrowed, NULL on END (also on cancel) */ + GdkTouchpadGesturePhase phase; /* BEGIN / UPDATE / END */ + gdouble x, y; /* focal point */ + gdouble dx, dy; /* pan deltas */ + gdouble scale; /* pinch scale */ + guint state; /* modifier state (low 4 bits) */ +} dt_gui_pinch_event_t; + +typedef void (*dt_gui_pinch_handler_t)(GtkGesture *gesture, + const dt_gui_pinch_event_t *event, + gpointer user_data); + +GtkGesture *(dt_gui_connect_pinch)(GtkWidget *widget, + dt_gui_pinch_handler_t handler, + gpointer data); +#define dt_gui_connect_pinch(widget, handler, data) ( \ + ASSERT_FUNC_TYPE(handler, void(*)(GtkGesture *, const dt_gui_pinch_event_t *, __typeof__(data))), \ + dt_gui_connect_pinch(GTK_WIDGET(widget), (handler), (data))) + GtkEventController *(dt_gui_connect_motion)(GtkWidget *widget, GCallback motion, GCallback enter, @@ -740,10 +854,28 @@ static inline GtkWidget *dt_gui_scroll_wrap(GtkWidget *widget) return scrolled_window; } -// Simulate a mouse button event (button is 1, 2, 3 - mouse button) sent to a Widget -void dt_gui_simulate_button_event(GtkWidget *widget, - const GdkEventType eventtype, - const int button); +/* Menu items: the shell emits "activate" for mouse clicks as well as for + * keyboard (Enter/mnemonic/accel) activation, so handlers that apply on + * press (item gestures, see the menu conversion commits) mark the item + * here and skip the release-time activate. This replaces the + * gtk_get_current_event() GDK_KEY_PRESS check, which does not exist in + * GTK4. */ +#define DT_GUI_MENUITEM_MOUSE_KEY "dt-gui-menuitem-mouse" + +static inline void dt_gui_menuitem_mark_pressed(GtkWidget *menuitem) +{ + g_object_set_data(G_OBJECT(menuitem), DT_GUI_MENUITEM_MOUSE_KEY, GINT_TO_POINTER(TRUE)); +} + +static inline gboolean dt_gui_menuitem_activated_by_keyboard(GtkWidget *menuitem) +{ + if(g_object_get_data(G_OBJECT(menuitem), DT_GUI_MENUITEM_MOUSE_KEY)) + { + g_object_set_data(G_OBJECT(menuitem), DT_GUI_MENUITEM_MOUSE_KEY, NULL); + return FALSE; + } + return TRUE; +} // Setup auto-commit on focus loss for editable renderers void dt_gui_commit_on_focus_loss(GtkCellRenderer *renderer, diff --git a/src/gui/metadata_tags.c b/src/gui/metadata_tags.c index 36a8f4d49cc8..d39ca82d4ddf 100644 --- a/src/gui/metadata_tags.c +++ b/src/gui/metadata_tags.c @@ -1,167 +1,167 @@ -/* - This file is part of darktable, - Copyright (C) 2026 darktable developers. - - darktable is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - darktable is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with darktable. If not, see . -*/ - -#include "common/exif.h" -#include "gui/gtk.h" -#include "gui/metadata_tags.h" - -typedef enum dt_metadata_tag_cols_t -{ - DT_METADATA_TAGS_COL_XMP = 0, - DT_METADATA_TAGS_COL_TYPE, - DT_METADATA_TAGS_COL_VISIBLE, - DT_METADATA_TAGS_NUM_COLS -} dt_metadata_tag_cols_t; - -static GtkListStore *liststore; -static GtkWidget *sel_entry; -static const gchar *sel_entry_text; -static GtkTreeView *sel_view; -static GList *taglist = NULL; -static GtkWidget *add_button; - -// routine to set individual visibility flag -static gboolean _set_matching_tag_visibility(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_idata) -{ - gboolean visible; - gchar *tagname = NULL; - gtk_tree_model_get(model, iter, DT_METADATA_TAGS_COL_XMP, &tagname, -1); - if(!sel_entry_text[0]) - visible = TRUE; - else - { - gchar *haystack = g_utf8_strdown(tagname, -1); - gchar *needle = g_utf8_strdown(sel_entry_text, -1); - visible = (g_strrstr(haystack, needle) != NULL); - g_free(haystack); - g_free(needle); - } - gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_METADATA_TAGS_COL_VISIBLE, visible, -1); - g_free(tagname); - return FALSE; -} - -// set the metadata tag visibility aligned with filter -static void _tag_name_changed(GtkEntry *entry, gpointer user_data) -{ - sel_entry_text = gtk_entry_get_text(GTK_ENTRY(sel_entry)); - GtkTreeModel *model = gtk_tree_view_get_model(sel_view); - GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); - gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_set_matching_tag_visibility, NULL); -} - -gchar *dt_metadata_tags_get_selected() -{ - GtkTreeIter iter; - GtkTreeModel *model = gtk_tree_view_get_model(sel_view); - GtkTreeSelection *selection = gtk_tree_view_get_selection(sel_view); - if(gtk_tree_selection_get_selected(selection, &model, &iter)) - { - gchar *tagname; - gtk_tree_model_get(model, &iter, DT_METADATA_TAGS_COL_XMP, &tagname, -1); - return tagname; - } - return NULL; -} - -static void _tree_selection_change(GtkTreeSelection *selection, gpointer user_data) -{ - const int nb = gtk_tree_selection_count_selected_rows(selection); - gtk_widget_set_sensitive(add_button, nb > 0); -} - -GtkWidget *dt_metadata_tags_dialog(GtkWidget *parent, - gpointer metadata_activated_callback, - gpointer user_data) -{ - GtkWidget *dialog = gtk_dialog_new_with_buttons(_("select tag"), GTK_WINDOW(parent), - GTK_DIALOG_DESTROY_WITH_PARENT, - _("_add"), GTK_RESPONSE_ACCEPT, - _("_done"), GTK_RESPONSE_NONE, NULL); - gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_NONE); - gtk_window_set_default_size(GTK_WINDOW(dialog), DT_PIXEL_APPLY_DPI(500), DT_PIXEL_APPLY_DPI(300)); - gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT); - - // keep a reference to the "add" button to toggle its sensitivity - add_button = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); - - sel_entry = gtk_entry_new(); - gtk_entry_set_text(GTK_ENTRY(sel_entry), ""); - gtk_widget_set_tooltip_text(sel_entry, _("list filter")); - gtk_entry_set_activates_default(GTK_ENTRY(sel_entry), TRUE); - g_signal_connect(G_OBJECT(sel_entry), "changed", G_CALLBACK(_tag_name_changed), NULL); - - sel_view = GTK_TREE_VIEW(gtk_tree_view_new()); - GtkWidget *w = dt_gui_scroll_wrap(GTK_WIDGET(sel_view)); - gtk_widget_set_tooltip_text(GTK_WIDGET(sel_view), _("list of available tags. click 'add' button or double-click on tag to add the selected one")); - GtkTreeSelection *selection = gtk_tree_view_get_selection(sel_view); - gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); - g_signal_connect(selection, "changed", G_CALLBACK(_tree_selection_change), NULL); - GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); - GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes(_("tag"), renderer, "text", 0, NULL); - gtk_tree_view_append_column(sel_view, col); - renderer = gtk_cell_renderer_text_new(); - col = gtk_tree_view_column_new_with_attributes(_("type"), renderer, "text", 1, NULL); - gtk_tree_view_append_column(sel_view, col); - liststore = gtk_list_store_new(DT_METADATA_TAGS_NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); - GtkTreeModel *model = gtk_tree_model_filter_new(GTK_TREE_MODEL(liststore), NULL); - gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model), DT_METADATA_TAGS_COL_VISIBLE); - - // populate the metadata tag list with exiv2 information - if(!taglist) - taglist = (GList *) dt_exif_get_exiv2_taglist(); - - for(GList *tag = taglist; tag; tag = g_list_next(tag)) - { - const char *tagname = tag->data; - - char *type = g_strstr_len(tagname, -1, ","); - if(type) - { - type[0] = '\0'; - type++; - } - - gtk_list_store_insert_with_values(liststore, NULL, -1, - DT_METADATA_TAGS_COL_XMP, tagname, - DT_METADATA_TAGS_COL_TYPE, type, - DT_METADATA_TAGS_COL_VISIBLE, TRUE, - -1); - - if(type) - { - type--; - type[0] = ','; - } - } - - gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(liststore), DT_METADATA_TAGS_COL_XMP, GTK_SORT_ASCENDING); - gtk_tree_view_set_model(sel_view, model); - g_object_unref(model); - g_signal_connect(G_OBJECT(sel_view), "row-activated", G_CALLBACK(metadata_activated_callback), user_data); - - dt_gui_dialog_add(GTK_DIALOG(dialog), sel_entry, w); - return dialog; -} - -// clang-format off -// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py -// vim: shiftwidth=2 expandtab tabstop=2 cindent -// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; -// clang-format on - +/* + This file is part of darktable, + Copyright (C) 2026 darktable developers. + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +*/ + +#include "common/exif.h" +#include "gui/gtk.h" +#include "gui/metadata_tags.h" + +typedef enum dt_metadata_tag_cols_t +{ + DT_METADATA_TAGS_COL_XMP = 0, + DT_METADATA_TAGS_COL_TYPE, + DT_METADATA_TAGS_COL_VISIBLE, + DT_METADATA_TAGS_NUM_COLS +} dt_metadata_tag_cols_t; + +static GtkListStore *liststore; +static GtkWidget *sel_entry; +static const gchar *sel_entry_text; +static GtkTreeView *sel_view; +static GList *taglist = NULL; +static GtkWidget *add_button; + +// routine to set individual visibility flag +static gboolean _set_matching_tag_visibility(GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_idata) +{ + gboolean visible; + gchar *tagname = NULL; + gtk_tree_model_get(model, iter, DT_METADATA_TAGS_COL_XMP, &tagname, -1); + if(!sel_entry_text[0]) + visible = TRUE; + else + { + gchar *haystack = g_utf8_strdown(tagname, -1); + gchar *needle = g_utf8_strdown(sel_entry_text, -1); + visible = (g_strrstr(haystack, needle) != NULL); + g_free(haystack); + g_free(needle); + } + gtk_list_store_set(GTK_LIST_STORE(model), iter, DT_METADATA_TAGS_COL_VISIBLE, visible, -1); + g_free(tagname); + return FALSE; +} + +// set the metadata tag visibility aligned with filter +static void _tag_name_changed(GtkEntry *entry, gpointer user_data) +{ + sel_entry_text = gtk_entry_get_text(GTK_ENTRY(sel_entry)); + GtkTreeModel *model = gtk_tree_view_get_model(sel_view); + GtkTreeModel *store = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(model)); + gtk_tree_model_foreach(store, (GtkTreeModelForeachFunc)_set_matching_tag_visibility, NULL); +} + +gchar *dt_metadata_tags_get_selected() +{ + GtkTreeIter iter; + GtkTreeModel *model = gtk_tree_view_get_model(sel_view); + GtkTreeSelection *selection = gtk_tree_view_get_selection(sel_view); + if(gtk_tree_selection_get_selected(selection, &model, &iter)) + { + gchar *tagname; + gtk_tree_model_get(model, &iter, DT_METADATA_TAGS_COL_XMP, &tagname, -1); + return tagname; + } + return NULL; +} + +static void _tree_selection_change(GtkTreeSelection *selection, gpointer user_data) +{ + const int nb = gtk_tree_selection_count_selected_rows(selection); + gtk_widget_set_sensitive(add_button, nb > 0); +} + +GtkWidget *dt_metadata_tags_dialog(GtkWidget *parent, + gpointer metadata_activated_callback, + gpointer user_data) +{ + GtkWidget *dialog = gtk_dialog_new_with_buttons(_("select tag"), GTK_WINDOW(parent), + GTK_DIALOG_DESTROY_WITH_PARENT, + _("_add"), GTK_RESPONSE_ACCEPT, + _("_done"), GTK_RESPONSE_NONE, NULL); + gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_NONE); + gtk_window_set_default_size(GTK_WINDOW(dialog), DT_PIXEL_APPLY_DPI(500), DT_PIXEL_APPLY_DPI(300)); + gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER_ON_PARENT); + + // keep a reference to the "add" button to toggle its sensitivity + add_button = gtk_dialog_get_widget_for_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + + sel_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(sel_entry), ""); + gtk_widget_set_tooltip_text(sel_entry, _("list filter")); + gtk_entry_set_activates_default(GTK_ENTRY(sel_entry), TRUE); + g_signal_connect(G_OBJECT(sel_entry), "changed", G_CALLBACK(_tag_name_changed), NULL); + + sel_view = GTK_TREE_VIEW(gtk_tree_view_new()); + GtkWidget *w = dt_gui_scroll_wrap(GTK_WIDGET(sel_view)); + gtk_widget_set_tooltip_text(GTK_WIDGET(sel_view), _("list of available tags. click 'add' button or double-click on tag to add the selected one")); + GtkTreeSelection *selection = gtk_tree_view_get_selection(sel_view); + gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); + g_signal_connect(selection, "changed", G_CALLBACK(_tree_selection_change), NULL); + GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); + GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes(_("tag"), renderer, "text", 0, NULL); + gtk_tree_view_append_column(sel_view, col); + renderer = gtk_cell_renderer_text_new(); + col = gtk_tree_view_column_new_with_attributes(_("type"), renderer, "text", 1, NULL); + gtk_tree_view_append_column(sel_view, col); + liststore = gtk_list_store_new(DT_METADATA_TAGS_NUM_COLS, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN); + GtkTreeModel *model = gtk_tree_model_filter_new(GTK_TREE_MODEL(liststore), NULL); + gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(model), DT_METADATA_TAGS_COL_VISIBLE); + + // populate the metadata tag list with exiv2 information + if(!taglist) + taglist = (GList *) dt_exif_get_exiv2_taglist(); + + for(GList *tag = taglist; tag; tag = g_list_next(tag)) + { + const char *tagname = tag->data; + + char *type = g_strstr_len(tagname, -1, ","); + if(type) + { + type[0] = '\0'; + type++; + } + + gtk_list_store_insert_with_values(liststore, NULL, -1, + DT_METADATA_TAGS_COL_XMP, tagname, + DT_METADATA_TAGS_COL_TYPE, type, + DT_METADATA_TAGS_COL_VISIBLE, TRUE, + -1); + + if(type) + { + type--; + type[0] = ','; + } + } + + gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(liststore), DT_METADATA_TAGS_COL_XMP, GTK_SORT_ASCENDING); + gtk_tree_view_set_model(sel_view, model); + g_object_unref(model); + g_signal_connect(G_OBJECT(sel_view), "row-activated", G_CALLBACK(metadata_activated_callback), user_data); + + dt_gui_dialog_add(GTK_DIALOG(dialog), sel_entry, w); + return dialog; +} + +// clang-format off +// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py +// vim: shiftwidth=2 expandtab tabstop=2 cindent +// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; +// clang-format on + diff --git a/src/gui/preferences_ai.c b/src/gui/preferences_ai.c index a8b2dbc0d28f..b24e3eecadce 100644 --- a/src/gui/preferences_ai.c +++ b/src/gui/preferences_ai.c @@ -1652,10 +1652,11 @@ void init_tab_ai(GtkWidget *dialog, GtkWidget *stack) gtk_widget_set_hexpand(data->ort_path_entry, TRUE); g_free(cur_path); - GtkWidget *browse_btn = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + GtkWidget *browse_btn = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select a custom ONNX Runtime shared library"), + }); gtk_widget_set_name(browse_btn, "non-flat"); - gtk_widget_set_tooltip_text(browse_btn, - _("select a custom ONNX Runtime shared library")); GtkWidget *detect_btn = gtk_button_new_with_label(_("detect")); gtk_widget_set_tooltip_text(detect_btn, diff --git a/src/gui/presets.c b/src/gui/presets.c index 5507263f0d7a..5ba53dd81fbb 100644 --- a/src/gui/presets.c +++ b/src/gui/presets.c @@ -1260,73 +1260,85 @@ gboolean dt_gui_presets_autoapply_for_module(dt_iop_module_t *module, GtkWidget static guint _click_time = G_MAXUINT; -static gboolean _menuitem_motion_preset(GtkMenuItem *menuitem, - GdkEventMotion *event, - dt_iop_module_t *module) +static gpointer _active_menu_item = NULL; + +/* the preset applies on press so the effect is visible immediately; the + * shell still activates the item on release (gestures do not consume mouse + * events) and the menu closes. the old button-press-event handler returned + * TRUE to keep the menu open on a long press - that veto does not exist in + * the controller world and is dropped. */ +static void _menuitem_button_preset_pressed(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + dt_iop_module_t *module) { - if(!_click_time) _click_time = G_MAXUINT; + const guint event_time = + gdk_event_get_time(gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL)); + _click_time = event_time; - return FALSE; -} + /* the shell emits "activate" on release for every mouse click (any + * button); mark the item so _menuitem_activate_preset only applies for + * keyboard activation (see dt_gui_menuitem_activated_by_keyboard) */ + GtkWidget *menuitem = dt_gui_get_widget(gesture); + dt_gui_menuitem_mark_pressed(menuitem); -static gpointer _active_menu_item = NULL; + if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_PRIMARY + || n_press != 1) return; + + gchar *name = g_object_get_data(G_OBJECT(menuitem), "dt-preset-name"); + + if(_active_menu_item) + gtk_check_menu_item_set_active(_active_menu_item, FALSE); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); + g_set_weak_pointer(&_active_menu_item, menuitem); + + dt_gui_presets_apply_preset(name, module); + + if(dt_conf_get_bool("accel/prefer_enabled") + || dt_conf_get_bool("accel/prefer_unmasked")) + dt_iop_connect_accels_multi(module->so); +} -static gboolean _menuitem_button_preset(GtkMenuItem *menuitem, - GdkEventButton *event, - dt_iop_module_t *module) +/* secondary click duplicates the module with the preset applied, or copies + * the preset as lua on a long press / one-instance modules */ +static void _menuitem_button_preset_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + dt_iop_module_t *module) { - gboolean long_click = dt_gui_long_click(dt_gdk_event_get_time(event), _click_time); + if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_SECONDARY) return; + GtkMenuItem *menuitem = GTK_MENU_ITEM(dt_gui_get_widget(gesture)); gchar *name = g_object_get_data(G_OBJECT(menuitem), "dt-preset-name"); + const gboolean long_click = + dt_gui_long_click(gdk_event_get_time(gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL)), + _click_time); - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) - { - if(_click_time > dt_gdk_event_get_time(event)) - { - if(_active_menu_item) - gtk_check_menu_item_set_active(_active_menu_item, FALSE); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menuitem), TRUE); - g_set_weak_pointer(&_active_menu_item, menuitem); - - dt_gui_presets_apply_preset(name, module); - } - } - else if(dt_gdk_event_get_button(event) == GDK_BUTTON_SECONDARY - && dt_gdk_event_get_type(event) == GDK_BUTTON_RELEASE - && _click_time) + if(long_click || (module->flags() & IOP_FLAGS_ONE_INSTANCE)) + dt_shortcut_copy_lua((dt_action_t*)module, name); + else { - if(long_click || (module->flags() & IOP_FLAGS_ONE_INSTANCE)) - dt_shortcut_copy_lua((dt_action_t*)module, name); - else - { - dt_iop_module_t *new_module = dt_iop_gui_duplicate(module, FALSE); - if(new_module) dt_gui_presets_apply_preset(name, new_module); + dt_iop_module_t *new_module = dt_iop_gui_duplicate(module, FALSE); + if(new_module) dt_gui_presets_apply_preset(name, new_module); - if(dt_conf_get_bool("darkroom/ui/rename_new_instance")) - dt_iop_gui_rename_module(new_module); - } + if(dt_conf_get_bool("darkroom/ui/rename_new_instance")) + dt_iop_gui_rename_module(new_module); } if(dt_conf_get_bool("accel/prefer_enabled") || dt_conf_get_bool("accel/prefer_unmasked")) - { - // rebuild the accelerators dt_iop_connect_accels_multi(module->so); - } - - _click_time = dt_gdk_event_get_type(event) == GDK_BUTTON_PRESS ? dt_gdk_event_get_time(event) : G_MAXUINT; - return long_click; // keep menu open on long click } // need to catch "activate" signal as well to handle keyboard static void _menuitem_activate_preset(GtkMenuItem *menuitem, dt_iop_module_t *module) { - GdkEvent *event = gtk_get_current_event(); - if(dt_gdk_event_get_type(event) == GDK_KEY_PRESS) + if(dt_gui_menuitem_activated_by_keyboard(GTK_WIDGET(menuitem))) dt_gui_presets_apply_preset(g_object_get_data(G_OBJECT(menuitem), "dt-preset-name"), module); - gdk_event_free(event); } static void _menuitem_connect_preset(GtkWidget *mi, @@ -1338,12 +1350,7 @@ static void _menuitem_connect_preset(GtkWidget *mi, dt_action_define(&iop->so->actions, "preset", name, mi, NULL); g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_activate_preset), iop); - g_signal_connect(G_OBJECT(mi), "button-press-event", - G_CALLBACK(_menuitem_button_preset), iop); - g_signal_connect(G_OBJECT(mi), "button-release-event", - G_CALLBACK(_menuitem_button_preset), iop); - g_signal_connect(G_OBJECT(mi), "motion-notify-event", - G_CALLBACK(_menuitem_motion_preset), iop); + dt_gui_connect_click(mi, _menuitem_button_preset_pressed, _menuitem_button_preset_released, iop); gtk_widget_set_has_tooltip(mi, TRUE); } @@ -1622,141 +1629,71 @@ void dt_gui_favorite_presets_menu_show(GtkWidget *w) dt_gui_menu_popup(menu, w, GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST); } -GtkMenu *dt_gui_presets_popup_menu_show_for_module(dt_iop_module_t *module) +/* shared preset popup menu: the query walk, writeprotect separators, + * hierarchy insertion, active highlight and the manage/edit/delete/ + * store/update/prefs tail are identical for the darkroom iop and lib + * (modulegroups / header) presets menus. Everything that differs -- the + * SQL, the row evaluation, the per-item wiring and the trailing prefs + * section -- is supplied by ops. */ +GtkMenu *dt_gui_presets_popup_menu_show(const dt_gui_presets_menu_ops_t *ops) { - const int32_t version = module->version(); - dt_iop_params_t *params = module->params; - const int32_t params_size = module->params_size; - dt_develop_blend_params_t *bl_params = module->blend_params; - const dt_image_t *image = &module->dev->image_storage; - GtkMenu *menu = GTK_MENU(gtk_menu_new()); - const gboolean hide_default = dt_conf_get_bool("plugins/darkroom/hide_default_presets"); - const gboolean default_first = dt_conf_get_bool("modules/default_presets_first"); - gchar *query = NULL; + const gboolean hide_default = dt_conf_get_bool(ops->hide_defaults_pref); - GtkWidget *mi; - int active_preset = -1, cnt = 0, writeprotect = 0; //, selected_default = 0; + gchar *query = ops->query(ops->data); sqlite3_stmt *stmt; - // order: get shipped defaults first - if(image) - { - char *format_filter = dt_presets_get_filter(image); - - // clang-format off - query = g_strdup_printf - ("SELECT name, op_params, writeprotect, description, blendop_params, " - " op_version, enabled" - " FROM data.presets" - " WHERE operation=?1" - " AND (filter=0" - " OR" - " (((?2 LIKE model AND ?3 LIKE maker) OR (?4 LIKE model AND ?5 LIKE maker))" - " AND ?6 LIKE lens" - " AND ?7 BETWEEN iso_min AND iso_max" - " AND ?8 BETWEEN exposure_min AND exposure_max" - " AND ?9 BETWEEN aperture_min AND aperture_max" - " AND ?10 BETWEEN focal_length_min AND focal_length_max" - " AND (%s)))" - " ORDER BY writeprotect %s, LOWER(name), rowid", - format_filter, - default_first ? "DESC":"ASC"); - // clang-format on - - g_free(format_filter); - - DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, module->op, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, image->exif_model, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, image->exif_maker, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 4, image->camera_alias, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 5, image->camera_maker, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 6, image->exif_lens, -1, SQLITE_TRANSIENT); - DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 7, image->exif_iso); - DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 8, image->exif_exposure); - DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 9, image->exif_aperture); - DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 10, image->exif_focal_length); - } - else - { - // don't know for which image. show all we got: - - query = g_strdup_printf("SELECT name, op_params, writeprotect, " - " description, blendop_params, op_version, enabled" - " FROM data.presets" - " WHERE operation=?1" - " ORDER BY writeprotect %s, LOWER(name), rowid", - default_first ? "DESC":"ASC" - ); - - DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); - DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, module->op, -1, SQLITE_TRANSIENT); - } + DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); g_free(query); - // collect all presets for op from db + ops->bind(stmt, ops->data); + + GtkWidget *mi; + int active_preset = -1, cnt = 0; + gboolean selected_writeprotect = FALSE; gboolean found = FALSE; int last_wp = -1; gchar **prev_split = NULL; GtkWidget *submenu = GTK_WIDGET(menu); GtkWidget *mainmenu = submenu; GSList *menu_path = NULL; // stack of menuitems which are the parents of submenus on menu_stack + while(sqlite3_step(stmt) == SQLITE_ROW) { - const int chk_writeprotect = sqlite3_column_int(stmt, 2); - if(hide_default && chk_writeprotect) + // default vs built-in stuff + const gboolean writeprotect = sqlite3_column_int(stmt, 2); + if(hide_default && writeprotect) { - //skip default module if set to hide them. + // skip default module if set to hide them. continue; } if(last_wp == -1) { - last_wp = chk_writeprotect; + last_wp = writeprotect; } - else if(last_wp != chk_writeprotect) + else if(last_wp != writeprotect) { - last_wp = chk_writeprotect; + last_wp = writeprotect; gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); *prev_split[0] = '\0'; // make first level mismatch so we start over } - const void *op_params = (void *)sqlite3_column_blob(stmt, 1); - const int32_t op_params_size = sqlite3_column_bytes(stmt, 1); - const void *blendop_params = (void *)sqlite3_column_blob(stmt, 4); - const int32_t bl_params_size = sqlite3_column_bytes(stmt, 4); - const int32_t preset_version = sqlite3_column_int(stmt, 5); - const int32_t enabled = sqlite3_column_int(stmt, 6); - const int32_t isdisabled = (preset_version == version ? 0 : 1); - const char *name = (char *)sqlite3_column_text(stmt, 0); - gboolean isdefault = FALSE; + + const char *name = (const char *)sqlite3_column_text(stmt, 0); if(darktable.gui->last_preset && strcmp(darktable.gui->last_preset, name) == 0) found = TRUE; - if(module - && (op_params_size == 0 - || !memcmp(module->default_params, op_params, - MIN(op_params_size, module->params_size))) - && !memcmp(module->default_blendop_params, blendop_params, - MIN(bl_params_size, sizeof(dt_develop_blend_params_t)))) - isdefault = TRUE; - - mi = dt_insert_preset_in_menu_hierarchy(name, &menu_path, mainmenu, - &submenu, &prev_split, - isdefault, chk_writeprotect); - - if(module - && ((op_params_size == 0 - && !memcmp(params, module->default_params, - MIN(params_size, module->params_size))) - || (op_params_size > 0 - && !memcmp(params, op_params, MIN(op_params_size, params_size)))) - && !memcmp(bl_params, blendop_params, MIN(bl_params_size, - sizeof(dt_develop_blend_params_t))) - && (module->enabled && enabled) - ) + mi = dt_insert_preset_in_menu_hierarchy(name, &menu_path, mainmenu, &submenu, + &prev_split, + ops->is_default + ? ops->is_default(stmt, ops->data) + : FALSE, + writeprotect); + + gboolean wp = FALSE; + if(ops->is_active(stmt, ops->data, &wp)) { active_preset = cnt; - writeprotect = sqlite3_column_int(stmt, 2); + selected_writeprotect = wp; dt_gui_add_class(mi, "active_menu_item"); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), TRUE); g_set_weak_pointer(&_active_menu_item, mi); @@ -1766,15 +1703,16 @@ GtkMenu *dt_gui_presets_popup_menu_show_for_module(dt_iop_module_t *module) dt_gui_add_class(gtk_bin_get_child(GTK_BIN(mp->data)), "active_menu_item"); } - if(isdisabled) + if(ops->is_disabled && ops->is_disabled(stmt, ops->data)) { - gtk_widget_set_sensitive(mi, 0); + gtk_widget_set_sensitive(mi, FALSE); gtk_widget_set_tooltip_text(mi, _("disabled: wrong module version")); } else { gtk_widget_set_tooltip_text(mi, (const char *)sqlite3_column_text(stmt, 3)); - _menuitem_connect_preset(mi, name, module); + gtk_widget_set_has_tooltip(mi, TRUE); + ops->connect_row(mi, stmt, ops->data); } cnt++; } @@ -1782,64 +1720,220 @@ GtkMenu *dt_gui_presets_popup_menu_show_for_module(dt_iop_module_t *module) g_slist_free(menu_path); g_strfreev(prev_split); - if(cnt > 0) gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); + if(cnt > 0) + gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - if(module) + // tail: manage / edit+delete / store+update, then the optional prefs section + if(ops->manage_cb) { - if(active_preset >= 0 && !writeprotect) - { - mi = gtk_menu_item_new_with_label(_("edit this preset..")); - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_edit_preset), module); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + mi = gtk_menu_item_new_with_label(_("manage presets...")); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(ops->manage_cb), ops->data); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + } + else if(active_preset >= 0 && !selected_writeprotect) + { + mi = gtk_menu_item_new_with_label(_("edit this preset..")); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(ops->edit_cb), ops->data); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - mi = gtk_menu_item_new_with_label(_("delete this preset")); - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_delete_preset), module); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + mi = gtk_menu_item_new_with_label(_("delete this preset")); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(ops->del_cb), ops->data); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + } + else + { + /* no active preset, or the active preset is writeprotect (a shipped + * default): edit/delete are not allowed, so fall through to store/ + * update instead of leaving a bare separator behind the preset list. */ + mi = gtk_menu_item_new_with_label(_("store new preset..")); + if(ops->params_size == 0) + { + gtk_widget_set_sensitive(mi, FALSE); + gtk_widget_set_tooltip_text(mi, _("nothing to save")); } else + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(ops->store_cb), ops->data); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); + + if(darktable.gui->last_preset && found) { - mi = gtk_menu_item_new_with_label(_("store new preset..")); - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_new_preset), module); + char *local_last_name = dt_util_localize_segmented_name(darktable.gui->last_preset, + TRUE); + char *markup = g_markup_printf_escaped("%s %s", + _("update preset"), + local_last_name); + g_free(local_last_name); + mi = gtk_menu_item_new_with_label(""); + gtk_widget_set_sensitive(mi, ops->params_size > 0); + gtk_label_set_markup(GTK_LABEL(gtk_bin_get_child(GTK_BIN(mi))), markup); + g_object_set_data_full(G_OBJECT(mi), "dt-preset-name", + g_strdup(darktable.gui->last_preset), g_free); + g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(ops->update_cb), ops->data); gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - - if(darktable.gui->last_preset && found) - { - char *local_last_name = dt_util_localize_segmented_name(darktable.gui->last_preset, - TRUE); - char *markup = g_markup_printf_escaped("%s %s", - _("update preset"), - local_last_name); - g_free(local_last_name); - mi = gtk_menu_item_new_with_label(""); - gtk_label_set_markup(GTK_LABEL(gtk_bin_get_child(GTK_BIN(mi))), markup); - g_object_set_data_full(G_OBJECT(mi), "dt-preset-name", - g_strdup(darktable.gui->last_preset), g_free); - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_update_preset), module); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - g_free(markup); - } + g_free(markup); } } - // and the parameters entry if needed - if(module - && (module->set_preferences - || module->flags() & IOP_FLAGS_GUIDES_WIDGET)) + if(ops->prefs) { gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - // the guide checkbox - if(module->flags() & IOP_FLAGS_GUIDES_WIDGET) - dt_guides_add_module_menuitem(menu, module); - // the specific parameters - if(module->set_preferences) module->set_preferences(GTK_MENU_SHELL(menu), module); + ops->prefs(menu, ops->data); } - _click_time = 0; + return menu; +} + +/* ---------- darkroom iop preset menu ---------- */ + +static gchar *_iop_query(gpointer data) +{ + dt_iop_module_t *module = data; + const dt_image_t *image = &module->dev->image_storage; + const gboolean default_first = dt_conf_get_bool("modules/default_presets_first"); + + if(image) + { + char *format_filter = dt_presets_get_filter(image); + + // clang-format off + gchar *query = g_strdup_printf + ("SELECT name, op_params, writeprotect, description, blendop_params, " + " op_version, enabled" + " FROM data.presets" + " WHERE operation=?1" + " AND (filter=0" + " OR" + " (((?2 LIKE model AND ?3 LIKE maker) OR (?4 LIKE model AND ?5 LIKE maker))" + " AND ?6 LIKE lens" + " AND ?7 BETWEEN iso_min AND iso_max" + " AND ?8 BETWEEN exposure_min AND exposure_max" + " AND ?9 BETWEEN aperture_min AND aperture_max" + " AND ?10 BETWEEN focal_length_min AND focal_length_max" + " AND (%s)))" + " ORDER BY writeprotect %s, LOWER(name), rowid", + format_filter, + default_first ? "DESC":"ASC"); + // clang-format on + + g_free(format_filter); + return query; + } + + // don't know for which image. show all we got: + // clang-format off + return g_strdup_printf("SELECT name, op_params, writeprotect, " + " description, blendop_params, op_version, enabled" + " FROM data.presets" + " WHERE operation=?1" + " ORDER BY writeprotect %s, LOWER(name), rowid", + default_first ? "DESC":"ASC"); + // clang-format on +} + +static void _iop_bind(sqlite3_stmt *stmt, gpointer data) +{ + dt_iop_module_t *module = data; + const dt_image_t *image = &module->dev->image_storage; + + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, module->op, -1, SQLITE_TRANSIENT); + if(image) + { + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 2, image->exif_model, -1, SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 3, image->exif_maker, -1, SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 4, image->camera_alias, -1, SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 5, image->camera_maker, -1, SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 6, image->exif_lens, -1, SQLITE_TRANSIENT); + DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 7, image->exif_iso); + DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 8, image->exif_exposure); + DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 9, image->exif_aperture); + DT_DEBUG_SQLITE3_BIND_DOUBLE(stmt, 10, image->exif_focal_length); + } +} + +static gboolean _iop_is_default(sqlite3_stmt *stmt, gpointer data) +{ + dt_iop_module_t *module = data; + const void *op_params = sqlite3_column_blob(stmt, 1); + const int32_t op_params_size = sqlite3_column_bytes(stmt, 1); + const void *blendop_params = sqlite3_column_blob(stmt, 4); + const int32_t bl_params_size = sqlite3_column_bytes(stmt, 4); + + return module + && (op_params_size == 0 + || !memcmp(module->default_params, op_params, + MIN(op_params_size, module->params_size))) + && !memcmp(module->default_blendop_params, blendop_params, + MIN(bl_params_size, sizeof(dt_develop_blend_params_t))); +} + +static gboolean _iop_is_disabled(sqlite3_stmt *stmt, gpointer data) +{ + dt_iop_module_t *module = data; + return sqlite3_column_int(stmt, 5) != module->version(); +} + +static gboolean _iop_is_active(sqlite3_stmt *stmt, gpointer data, gboolean *writeprotect) +{ + dt_iop_module_t *module = data; + const void *op_params = sqlite3_column_blob(stmt, 1); + const int32_t op_params_size = sqlite3_column_bytes(stmt, 1); + const void *blendop_params = sqlite3_column_blob(stmt, 4); + const int32_t bl_params_size = sqlite3_column_bytes(stmt, 4); + const int32_t enabled = sqlite3_column_int(stmt, 6); + + if(((op_params_size == 0 + && !memcmp(module->params, module->default_params, + MIN(module->params_size, module->params_size))) + || (op_params_size > 0 + && !memcmp(module->params, op_params, MIN(op_params_size, module->params_size)))) + && !memcmp(module->blend_params, blendop_params, + MIN(bl_params_size, sizeof(dt_develop_blend_params_t))) + && (module->enabled && enabled)) + { + *writeprotect = sqlite3_column_int(stmt, 2); + return TRUE; + } + return FALSE; +} + +static void _iop_connect_row(GtkWidget *mi, sqlite3_stmt *stmt, gpointer data) +{ + _menuitem_connect_preset(mi, (const char *)sqlite3_column_text(stmt, 0), data); +} + +static void _iop_prefs(GtkMenu *menu, gpointer data) +{ + dt_iop_module_t *module = data; + // the guide checkbox + if(module->flags() & IOP_FLAGS_GUIDES_WIDGET) + dt_guides_add_module_menuitem(menu, module); + // the specific parameters + if(module->set_preferences) module->set_preferences(GTK_MENU_SHELL(menu), module); +} + +GtkMenu *dt_gui_presets_popup_menu_show_for_module(dt_iop_module_t *module) +{ + const dt_gui_presets_menu_ops_t ops = { + .data = module, + .hide_defaults_pref = "plugins/darkroom/hide_default_presets", + .query = _iop_query, + .bind = _iop_bind, + .is_default = _iop_is_default, + .is_disabled = _iop_is_disabled, + .is_active = _iop_is_active, + .connect_row = _iop_connect_row, + .params_size = module->params_size, + .edit_cb = G_CALLBACK(_menuitem_edit_preset), + .del_cb = G_CALLBACK(_menuitem_delete_preset), + .store_cb = G_CALLBACK(_menuitem_new_preset), + .update_cb = G_CALLBACK(_menuitem_update_preset), + .prefs = (module->set_preferences || module->flags() & IOP_FLAGS_GUIDES_WIDGET) + ? _iop_prefs : NULL, + }; + + GtkMenu *menu = dt_gui_presets_popup_menu_show(&ops); + _click_time = 0; return menu; } diff --git a/src/gui/presets.h b/src/gui/presets.h index f21f8abc2137..7f512e5f1b36 100644 --- a/src/gui/presets.h +++ b/src/gui/presets.h @@ -20,6 +20,8 @@ #include "develop/blend.h" +#include + // format flags stored into the presets database; the FOR_NOT_ // variants are negated to keep existing presets typedef enum dt_gui_presets_format_flag_t @@ -141,6 +143,36 @@ void dt_gui_presets_update_filter(const char *name, /** show the popup menu for the given module, with default behavior. */ GtkMenu *dt_gui_presets_popup_menu_show_for_module(dt_iop_module_t *module); +/** ops for building a preset popup menu (dt_gui_presets_popup_menu_show()). + * The SELECT returned by query() must start with the columns + * name, op_params, writeprotect, description; extra columns may follow and + * are read from the live statement inside the callbacks. */ +typedef struct dt_gui_presets_menu_ops_t +{ + gpointer data; /* caller context, passed to every callback */ + const gchar *hide_defaults_pref; /* conf key: which default presets to hide */ + gchar *(*query)(gpointer data); /* build the SELECT string */ + void (*bind)(sqlite3_stmt *stmt, gpointer data); /* bind the WHERE parameters */ + gboolean (*is_default)(sqlite3_stmt *stmt, gpointer data); /* dimmed built-in; NULL = none */ + gboolean (*is_disabled)(sqlite3_stmt *stmt, gpointer data); /* greyed out (wrong op version); NULL = none */ + gboolean (*is_active)(sqlite3_stmt *stmt, gpointer data, + gboolean *writeprotect); /* highlighted current-params match */ + void (*connect_row)(GtkWidget *mi, sqlite3_stmt *stmt, gpointer data); /* wire one preset item */ + int params_size; /* 0 disables "store new preset" */ + GCallback manage_cb; /* "manage presets..." item; NULL = none */ + GCallback edit_cb; /* "edit this preset.." */ + GCallback del_cb; /* "delete this preset" */ + GCallback store_cb; /* "store new preset.." */ + GCallback update_cb; /* "update preset" */ + void (*prefs)(GtkMenu *menu, gpointer data); /* trailing prefs section; NULL = none */ +} dt_gui_presets_menu_ops_t; + +/** build the shared preset popup menu (the iop darkroom and lib header/ + * modulegroups presets menus are the same menu). The caller owns popping + * the menu up and any per-menu destroy cleanup; the menu is not popped and + * not destroyed here. */ +GtkMenu *dt_gui_presets_popup_menu_show(const dt_gui_presets_menu_ops_t *ops); + /** show popupmenu for favorite modules */ void dt_gui_favorite_presets_menu_show(GtkWidget *w); diff --git a/src/gui/welcome.c b/src/gui/welcome.c index 9ad904160802..fbb795c36754 100644 --- a/src/gui/welcome.c +++ b/src/gui/welcome.c @@ -333,10 +333,12 @@ static GtkWidget *_build_page_widget(_dt_page_t *pg) g_signal_connect(G_OBJECT(entry), "changed", G_CALLBACK(_on_dir_entry_changed), q->conf_key); - GtkWidget *btn = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); - gtk_widget_set_tooltip_text(btn, _("select directory")); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_on_browse_dir_clicked), entry); + GtkWidget *btn = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select directory"), + .clicked_cb = G_CALLBACK(_on_browse_dir_clicked), + .clicked_data = entry, + }); dt_gui_box_add(GTK_BOX(hbox), entry, btn); gtk_widget_set_hexpand(hbox, TRUE); diff --git a/src/gui/workspace.c b/src/gui/workspace.c index ffecb9d162ec..76ef0be6d5ce 100644 --- a/src/gui/workspace.c +++ b/src/gui/workspace.c @@ -510,9 +510,11 @@ static GtkWidget *_insert_button(dt_workspace_t *session, if(with_del) { - del = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); - g_signal_connect(G_OBJECT(del), "clicked", - G_CALLBACK(_workspace_delete_db), session); + del = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .clicked_cb = G_CALLBACK(_workspace_delete_db), + .clicked_data = session, + }); g_object_set_data(G_OBJECT(del), "db", b); gtk_grid_attach(GTK_GRID(session->grid), del, 2, row, 1, 1); diff --git a/src/imageio/storage/disk.c b/src/imageio/storage/disk.c index 31f59436fbd9..f8d04fceaa18 100644 --- a/src/imageio/storage/disk.c +++ b/src/imageio/storage/disk.c @@ -280,10 +280,13 @@ void gui_init(dt_imageio_module_storage_t *self) dt_gtkentry_setup_variables_completion(d->entry); gtk_editable_set_position(GTK_EDITABLE(d->entry), -1); - GtkWidget *widget = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + GtkWidget *widget = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select directory"), + .clicked_cb = G_CALLBACK(button_clicked), + .clicked_data = self, + }); gtk_widget_set_name(widget, "non-flat"); - gtk_widget_set_tooltip_text(widget, _("select directory")); - g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(button_clicked), self); DT_BAUHAUS_COMBOBOX_NEW_FULL(d->onsave_action, self, NULL, N_("on conflict"), NULL, dt_conf_get_int("plugins/imageio/storage/disk/overwrite"), diff --git a/src/imageio/storage/gallery.c b/src/imageio/storage/gallery.c index 5b6549d5372e..c55c994aadf6 100644 --- a/src/imageio/storage/gallery.c +++ b/src/imageio/storage/gallery.c @@ -202,10 +202,13 @@ void gui_init(dt_imageio_module_storage_t *self) dt_conf_get_string_const("plugins/imageio/storage/gallery/file_directory"))); dt_gtkentry_setup_variables_completion(d->entry); - GtkWidget *widget = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + GtkWidget *widget = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select directory"), + .clicked_cb = G_CALLBACK(button_clicked), + .clicked_data = self, + }); gtk_widget_set_name(widget, "non-flat"); - gtk_widget_set_tooltip_text(widget, _("select directory")); - g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(button_clicked), self); d->title_entry = GTK_ENTRY(dt_action_entry_new diff --git a/src/imageio/storage/latex.c b/src/imageio/storage/latex.c index b3d7b441ac30..698b1939888e 100644 --- a/src/imageio/storage/latex.c +++ b/src/imageio/storage/latex.c @@ -188,10 +188,13 @@ void gui_init(dt_imageio_module_storage_t *self) dt_conf_get_string_const("plugins/imageio/storage/latex/file_directory"))); dt_gtkentry_setup_variables_completion(d->entry); - GtkWidget *widget = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + GtkWidget *widget = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select directory"), + .clicked_cb = G_CALLBACK(button_clicked), + .clicked_data = self, + }); gtk_widget_set_name(widget, "non-flat"); - gtk_widget_set_tooltip_text(widget, _("select directory")); - g_signal_connect(G_OBJECT(widget), "clicked", G_CALLBACK(button_clicked), self); d->title_entry = GTK_ENTRY(dt_action_entry_new(DT_ACTION(self), N_("path"), G_CALLBACK(title_changed_callback), self, _("enter the title of the book"), diff --git a/src/imageio/storage/piwigo.c b/src/imageio/storage/piwigo.c index 1bff22b702f3..9307ef74b8f9 100644 --- a/src/imageio/storage/piwigo.c +++ b/src/imageio/storage/piwigo.c @@ -1129,10 +1129,12 @@ void gui_init(dt_imageio_module_storage_t *self) G_CALLBACK(_piwigo_album_changed), (gpointer)ui); gtk_widget_set_sensitive(ui->album_list, FALSE); - GtkWidget *refresh_button = dtgtk_button_new(dtgtk_cairo_paint_refresh, CPF_NONE, NULL); - gtk_widget_set_tooltip_text(refresh_button, _("refresh album list")); - g_signal_connect(G_OBJECT(refresh_button), "clicked", - G_CALLBACK(_piwigo_refresh_clicked), (gpointer)ui); + GtkWidget *refresh_button = dtgtk_button_new_full(dtgtk_cairo_paint_refresh, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("refresh album list"), + .clicked_cb = G_CALLBACK(_piwigo_refresh_clicked), + .clicked_data = (gpointer)ui, + }); // new album ui->new_album_entry = GTK_ENTRY(gtk_entry_new()); // Album title diff --git a/src/iop/agx.c b/src/iop/agx.c index f426c8cc8021..853448c5c201 100644 --- a/src/iop/agx.c +++ b/src/iop/agx.c @@ -2075,10 +2075,16 @@ static void _add_exposure_box(dt_iop_module_t *self, dt_iop_agx_gui_data_t *g, d 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); - g->btn_read_exposure = dtgtk_button_new(dtgtk_cairo_paint_camera, 0, NULL); - gtk_widget_set_tooltip_text(g->btn_read_exposure, _("read exposure from metadata and exposure module")); - g_signal_connect(G_OBJECT(g->btn_read_exposure), "clicked", G_CALLBACK(_read_exposure_params_callback), real_self); - dt_action_define_iop(real_self, N_("exposure range"), N_("read exposure"), g->btn_read_exposure, &dt_action_def_button); + g->btn_read_exposure = dtgtk_button_new_full(dtgtk_cairo_paint_camera, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("read exposure from metadata and exposure module"), + .action = DT_ACTION(real_self), + .action_section = N_("exposure range"), + .action_label = N_("read exposure"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_read_exposure_params_callback), + .clicked_data = real_self, + }); dt_gui_box_add(g->range_exposure_picker_group, g->btn_read_exposure); dt_gui_box_add(self->widget, g->range_exposure_picker_group); @@ -2277,11 +2283,15 @@ static void _create_primaries_page(dt_iop_module_t *main, "especially with bright, saturated lights (e.g. LEDs).\n" "mainly intended to be used for experimenting.")); - GtkWidget *primaries_button = dtgtk_button_new(dtgtk_cairo_paint_styles, 0, NULL); - gtk_widget_set_tooltip_text(primaries_button, _("reset primaries to a predefined configuration")); - g_signal_connect(primaries_button, "clicked", G_CALLBACK(_primaries_popupmenu_callback), main); - dt_action_define_iop(main, NULL, N_("reset primaries"), - primaries_button, &dt_action_def_button); + GtkWidget *primaries_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("reset primaries to a predefined configuration"), + .action = DT_ACTION(main), + .action_label = N_("reset primaries"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_primaries_popupmenu_callback), + .clicked_data = main, + }); g->primaries_controls_vbox = dt_gui_vbox(dt_gui_hbox(dt_ui_label_new(_("reset primaries")), dt_gui_align_right(primaries_button))); diff --git a/src/iop/atrous.c b/src/iop/atrous.c index 2d3578945447..42ac2769c4a6 100644 --- a/src/iop/atrous.c +++ b/src/iop/atrous.c @@ -1532,13 +1532,7 @@ static void area_scrolled(GtkEventControllerScroll *controller, if(dt_modifier_eq(controller, GDK_MOD1_MASK)) { // alt+scroll switches the channel tab (as before the controller conversion) - // GTK4: no gtk_widget_event() -- reimplement as a controller on the notebook - GdkEvent *event = gtk_get_current_event(); - if(event) - { - gtk_widget_event(GTK_WIDGET(g->channel_tabs), event); - gdk_event_free(event); - } + dt_gui_forward_scroll(controller, GTK_WIDGET(g->channel_tabs)); return; } diff --git a/src/iop/basicadj.c b/src/iop/basicadj.c index 6f9d997b04f3..2dc8121d7f35 100644 --- a/src/iop/basicadj.c +++ b/src/iop/basicadj.c @@ -653,13 +653,15 @@ void gui_init(dt_iop_module_t *self) g->bt_auto_levels = dt_action_button_new(NULL, N_("auto"), _auto_levels_callback, self, _("apply auto exposure based on the entire image"), 0, 0); gtk_widget_set_size_request(g->bt_auto_levels, -1, DT_PIXEL_APPLY_DPI(24)); - g->bt_select_region = dtgtk_togglebutton_new(dtgtk_cairo_paint_colorpicker, 0, NULL); + g->bt_select_region = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_colorpicker, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("apply auto exposure based on a region defined by the user\n" + "click and drag to draw the area\n" + "right-click to cancel"), + .toggled_cb = G_CALLBACK(_select_region_toggled_callback), + .toggled_data = self, + }); dt_gui_add_class(g->bt_select_region, "dt_transparent_background"); - gtk_widget_set_tooltip_text(g->bt_select_region, - _("apply auto exposure based on a region defined by the user\n" - "click and drag to draw the area\n" - "right-click to cancel")); - g_signal_connect(G_OBJECT(g->bt_select_region), "toggled", G_CALLBACK(_select_region_toggled_callback), self); dt_gui_box_add(self->widget, dt_gui_expand(g->bt_auto_levels), dt_gui_expand(g->bt_select_region)); diff --git a/src/iop/channelmixerrgb.c b/src/iop/channelmixerrgb.c index b2d1902f5ab8..d4344186f7b1 100644 --- a/src/iop/channelmixerrgb.c +++ b/src/iop/channelmixerrgb.c @@ -4716,27 +4716,38 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_tooltip_text(g->label_delta_E, _("the delta E is using the CIE 2000 formula")); - g->button_commit = dtgtk_button_new(dtgtk_cairo_paint_check_mark, 0, NULL); - dt_action_define_iop(self, N_("calibrate"), N_("accept"), - g->button_commit, &dt_action_def_button); - g_signal_connect(G_OBJECT(g->button_commit), "clicked", - G_CALLBACK(_commit_profile_callback), (gpointer)self); - gtk_widget_set_tooltip_text(g->button_commit, - _("accept the computed profile and set it in the module")); - - g->button_profile = dtgtk_button_new(dtgtk_cairo_paint_refresh, 0, NULL); - dt_action_define_iop(self, N_("calibrate"), N_("recompute"), - g->button_profile, &dt_action_def_button); - g_signal_connect(G_OBJECT(g->button_profile), "clicked", - G_CALLBACK(_run_profile_callback), (gpointer)self); - gtk_widget_set_tooltip_text(g->button_profile, _("recompute the profile")); - - g->button_validate = dtgtk_button_new(dtgtk_cairo_paint_softproof, 0, NULL); - dt_action_define_iop(self, N_("calibrate"), N_("validate"), - g->button_validate, &dt_action_def_button); - g_signal_connect(G_OBJECT(g->button_validate), "clicked", - G_CALLBACK(_run_validation_callback), (gpointer)self); - gtk_widget_set_tooltip_text(g->button_validate, _("check the output delta E")); + g->button_commit = dtgtk_button_new_full(dtgtk_cairo_paint_check_mark, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("accept the computed profile and set it in the module"), + .action = DT_ACTION(self), + .action_section = N_("calibrate"), + .action_label = N_("accept"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_commit_profile_callback), + .clicked_data = (gpointer)self, + }); + + g->button_profile = dtgtk_button_new_full(dtgtk_cairo_paint_refresh, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("recompute the profile"), + .action = DT_ACTION(self), + .action_section = N_("calibrate"), + .action_label = N_("recompute"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_run_profile_callback), + .clicked_data = (gpointer)self, + }); + + g->button_validate = dtgtk_button_new_full(dtgtk_cairo_paint_softproof, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("check the output delta E"), + .action = DT_ACTION(self), + .action_section = N_("calibrate"), + .action_label = N_("validate"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_run_validation_callback), + .clicked_data = (gpointer)self, + }); dt_gui_box_add(g->cs.container, g->checkers_list, g->optimize, g->safety, g->label_delta_E, dt_gui_hbox(dt_gui_align_right(g->button_validate), diff --git a/src/iop/colorequal.c b/src/iop/colorequal.c index 670aa6c9de21..0cabcef2b6d5 100644 --- a/src/iop/colorequal.c +++ b/src/iop/colorequal.c @@ -2703,14 +2703,11 @@ static void _area_scrolled_callback(GtkEventControllerScroll *controller, { const dt_iop_colorequal_gui_data_t *g = self->gui_data; - GdkEvent *event = gtk_get_current_event(); - if(!event) return; - - GtkWidget *w = dt_modifier_is(dt_gdk_event_get_state(event), GDK_MOD1_MASK) - ? GTK_WIDGET(g->notebook) - : _get_slider(g, g->selected); - gtk_widget_event(w, event); - gdk_event_free(event); + const GdkModifierType state = dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); + dt_gui_forward_scroll(controller, + dt_modifier_is(state, GDK_MOD1_MASK) + ? GTK_WIDGET(g->notebook) + : _get_slider(g, g->selected)); } static void _area_motion_notify_callback(GtkEventControllerMotion *controller, diff --git a/src/iop/colorharmonizer.c b/src/iop/colorharmonizer.c index a644f9926ac1..426ffefa391f 100644 --- a/src/iop/colorharmonizer.c +++ b/src/iop/colorharmonizer.c @@ -1412,11 +1412,13 @@ void gui_init(dt_iop_module_t *self) gtk_widget_set_hexpand(g->rule, TRUE); self->widget = main_box; - g->auto_detect = dtgtk_button_new(dtgtk_cairo_paint_camera, CPF_NONE, NULL); + g->auto_detect = dtgtk_button_new_full(dtgtk_cairo_paint_camera, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("not yet available — wait for the preview to finish processing."), + .clicked_cb = G_CALLBACK(_auto_detect_callback), + .clicked_data = self, + }); gtk_widget_set_sensitive(g->auto_detect, FALSE); - gtk_widget_set_tooltip_text(g->auto_detect, - _("not yet available — wait for the preview to finish processing.")); - g_signal_connect(G_OBJECT(g->auto_detect), "clicked", G_CALLBACK(_auto_detect_callback), self); dt_gui_box_add(rule_row, g->auto_detect); gtk_widget_set_tooltip_text(g->rule, @@ -1528,12 +1530,13 @@ void gui_init(dt_iop_module_t *self) g_signal_connect(G_OBJECT(g->sync_to_vectorscope), "value-changed", G_CALLBACK(_sync_to_vectorscope_toggled), self); - g->set_from_vectorscope = dtgtk_button_new(dtgtk_cairo_paint_refresh, CPF_NONE, NULL); - gtk_widget_set_tooltip_text(g->set_from_vectorscope, - _("import the harmony rule and anchor hue currently displayed in the vectorscope.\n" - "also switches the histogram panel to the vectorscope view if it is not already active.")); - g_signal_connect(G_OBJECT(g->set_from_vectorscope), "clicked", - G_CALLBACK(_set_from_vectorscope_callback), self); + g->set_from_vectorscope = dtgtk_button_new_full(dtgtk_cairo_paint_refresh, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("import the harmony rule and anchor hue currently displayed in the vectorscope.\n" + "also switches the histogram panel to the vectorscope view if it is not already active."), + .clicked_cb = G_CALLBACK(_set_from_vectorscope_callback), + .clicked_data = self, + }); gtk_widget_set_hexpand(g->sync_to_vectorscope, TRUE); dt_gui_box_add(sync_row, g->sync_to_vectorscope); diff --git a/src/iop/colorzones.c b/src/iop/colorzones.c index 8a9652c160a9..a70231fb69d1 100644 --- a/src/iop/colorzones.c +++ b/src/iop/colorzones.c @@ -1868,13 +1868,7 @@ static void _area_scrolled_callback(GtkEventControllerScroll *controller, if(dt_modifier_is(dt_key_modifier_state(), GDK_MOD1_MASK)) { // alt+scroll switches the channel tab (as before the controller conversion) - // GTK4: no gtk_widget_event() -- reimplement as a controller on the notebook - GdkEvent *event = gtk_get_current_event(); - if(event) - { - gtk_widget_event(GTK_WIDGET(g->channel_tabs), event); - gdk_event_free(event); - } + dt_gui_forward_scroll(controller, GTK_WIDGET(g->channel_tabs)); return; } @@ -2678,11 +2672,13 @@ void gui_init(dt_iop_module_t *self) G_CALLBACK(_edit_by_area_callback), self); // display selection - g->bt_showmask = dtgtk_togglebutton_new(dtgtk_cairo_paint_showmask, 0, NULL); + g->bt_showmask = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_showmask, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("display selection"), + .toggled_cb = G_CALLBACK(_display_mask_callback), + .toggled_data = self, + }); dt_gui_add_class(g->bt_showmask, "dt_transparent_background"); - gtk_widget_set_tooltip_text(g->bt_showmask, _("display selection")); - g_signal_connect(G_OBJECT(g->bt_showmask), "toggled", - G_CALLBACK(_display_mask_callback), self); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_showmask), FALSE); self->widget = dt_gui_vbox diff --git a/src/iop/denoiseprofile.c b/src/iop/denoiseprofile.c index 41927feebeea..d01104f0e5aa 100644 --- a/src/iop/denoiseprofile.c +++ b/src/iop/denoiseprofile.c @@ -3546,16 +3546,10 @@ static void denoiseprofile_scrolled(GtkEventControllerScroll *controller, if(dt_modifier_eq(controller, GDK_MOD1_MASK)) { // alt+scroll switches the channel tab (as before the controller conversion) - // GTK4: no gtk_widget_event() -- reimplement as a controller on the notebook - GdkEvent *event = gtk_get_current_event(); - if(event) - { - gtk_widget_event(GTK_WIDGET(g->channel > DT_DENOISE_PROFILE_B - ? g->channel_tabs_Y0U0V0 - : g->channel_tabs), - event); - gdk_event_free(event); - } + dt_gui_forward_scroll(controller, + GTK_WIDGET(g->channel > DT_DENOISE_PROFILE_B + ? g->channel_tabs_Y0U0V0 + : g->channel_tabs)); return; } diff --git a/src/iop/lut3d.c b/src/iop/lut3d.c index 5df24ec94275..7fcde8ba582b 100644 --- a/src/iop/lut3d.c +++ b/src/iop/lut3d.c @@ -1694,18 +1694,22 @@ void gui_init(dt_iop_module_t *self) { dt_iop_lut3d_gui_data_t *g = IOP_GUI_ALLOC(lut3d); - g->button = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); - gtk_widget_set_name(g->button, "non-flat"); #ifdef HAVE_GMIC - gtk_widget_set_tooltip_text(g->button, _("select a png (haldclut)" + const gchar *tooltip = _("select a png (haldclut)" ", a cube, a 3dl or a gmz (compressed LUT) file " - "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file")); + "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file"); #else - gtk_widget_set_tooltip_text(g->button, _("select a png (haldclut)" + const gchar *tooltip = _("select a png (haldclut)" ", a cube or a 3dl file " - "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file")); + "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file"); #endif // HAVE_GMIC + g->button = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = tooltip, + }); + gtk_widget_set_name(g->button, "non-flat"); + g->filepath = dt_bauhaus_combobox_new(self); dt_bauhaus_combobox_set_entries_ellipsis(g->filepath, PANGO_ELLIPSIZE_MIDDLE); #ifdef HAVE_GMIC diff --git a/src/iop/rasterfile.c b/src/iop/rasterfile.c index a1a55fa97dd1..ad84c1122b4e 100644 --- a/src/iop/rasterfile.c +++ b/src/iop/rasterfile.c @@ -742,14 +742,14 @@ void gui_init(dt_iop_module_t *self) (g->mode, _("select the RGB channels taken into account to generate the raster mask")); - g->fbutton = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + g->fbutton = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select the PFM/PNG file recorded as a raster mask,\n" + "CAUTION: path must be set in preferences/processing before choosing"), + .clicked_cb = G_CALLBACK(_fbutton_clicked), + .clicked_data = self, + }); gtk_widget_set_name(g->fbutton, "non-flat"); - gtk_widget_set_tooltip_text - (g->fbutton, - _("select the PFM/PNG file recorded as a raster mask,\n" - "CAUTION: path must be set in preferences/processing before choosing")); - g_signal_connect(G_OBJECT(g->fbutton), "clicked", - G_CALLBACK(_fbutton_clicked), self); g->file = dt_bauhaus_combobox_new(self); dt_bauhaus_combobox_set_entries_ellipsis(g->file, PANGO_ELLIPSIZE_MIDDLE); diff --git a/src/iop/rawdenoise.c b/src/iop/rawdenoise.c index 4c34c9bfb851..c32004063410 100644 --- a/src/iop/rawdenoise.c +++ b/src/iop/rawdenoise.c @@ -853,13 +853,7 @@ static void rawdenoise_scrolled(GtkEventControllerScroll *controller, gdouble dx if(dt_modifier_eq(controller, GDK_MOD1_MASK)) { // alt+scroll switches the channel tab (as before the controller conversion) - // GTK4: no gtk_widget_event() -- reimplement as a controller on the notebook - GdkEvent *event = gtk_get_current_event(); - if(event) - { - gtk_widget_event(GTK_WIDGET(g->channel_tabs), event); - gdk_event_free(event); - } + dt_gui_forward_scroll(controller, GTK_WIDGET(g->channel_tabs)); return; } diff --git a/src/iop/rgblevels.c b/src/iop/rgblevels.c index 3291917bed45..09a469e50304 100644 --- a/src/iop/rgblevels.c +++ b/src/iop/rgblevels.c @@ -1069,14 +1069,16 @@ void gui_init(dt_iop_module_t *self) &dt_action_def_button); gtk_widget_set_tooltip_text(g->bt_auto_levels, _("apply auto levels")); - g->bt_select_region = dtgtk_togglebutton_new(dtgtk_cairo_paint_colorpicker, 0, NULL); + g->bt_select_region = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_colorpicker, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("apply auto levels based on a region defined by the user\n" + "click and drag to draw the area\n" + "right-click to cancel"), + .action = DT_ACTION(self), + .action_label = N_("auto region"), + .action_def = &dt_action_def_toggle, + }); dt_gui_add_class(g->bt_select_region, "dt_transparent_background"); - dt_action_define_iop(self, NULL, N_("auto region"), g->bt_select_region, - &dt_action_def_toggle); - gtk_widget_set_tooltip_text(g->bt_select_region, - _("apply auto levels based on a region defined by the user\n" - "click and drag to draw the area\n" - "right-click to cancel")); dt_gui_box_add(self->widget, g->channel_tabs, g->area, dt_gui_hbox(g->blackpick, g->greypick, g->whitepick), diff --git a/src/iop/temperature.c b/src/iop/temperature.c index 7231018cfc98..b7b37e0207b2 100644 --- a/src/iop/temperature.c +++ b/src/iop/temperature.c @@ -1802,11 +1802,7 @@ static void _preset_tune_callback(GtkWidget *widget, dt_iop_module_t *self) break; case DT_IOP_TEMP_SPOT: // from image area wb, expose callback will set p->rgbg2. if(!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->colorpicker))) - { - gboolean ret_val; - g_signal_emit_by_name(G_OBJECT(g->colorpicker), "button-press-event", - NULL, &ret_val); - } + dt_iop_color_picker_toggle(g->colorpicker, DT_ACTION_EFFECT_TOGGLE, 1.0); break; case DT_IOP_TEMP_USER: // directly changing one of the coeff // sliders also changes the mod_coeff so it diff --git a/src/libs/backgroundjobs.c b/src/libs/backgroundjobs.c index b41332c7af3d..3682cc607be2 100644 --- a/src/libs/backgroundjobs.c +++ b/src/libs/backgroundjobs.c @@ -244,8 +244,11 @@ static gboolean _cancellable_gui_thread(gpointer user_data) _cancellable_gui_thread_t *params = (_cancellable_gui_thread_t *)user_data; GtkBox *hbox = GTK_BOX(params->instance->hbox); - GtkWidget *button = dtgtk_button_new(dtgtk_cairo_paint_cancel, 0, NULL); - g_signal_connect(G_OBJECT(button), "clicked", G_CALLBACK(_lib_backgroundjobs_cancel_callback_new), params->progress); + GtkWidget *button = dtgtk_button_new_full(dtgtk_cairo_paint_cancel, 0, NULL, + &(dtgtk_button_config_t){ + .clicked_cb = G_CALLBACK(_lib_backgroundjobs_cancel_callback_new), + .clicked_data = params->progress, + }); gtk_box_pack_start(hbox, GTK_WIDGET(button), FALSE, FALSE, 0); gtk_widget_show_all(button); diff --git a/src/libs/collect.c b/src/libs/collect.c index eaef0f5d9184..b0dfc1569d81 100644 --- a/src/libs/collect.c +++ b/src/libs/collect.c @@ -648,8 +648,8 @@ static void _gesture_begin_claim(GtkGesture *gesture, { const dt_lib_collect_t *d = user_data; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); const gboolean modifier = dt_modifier_is(state, GDK_SHIFT_MASK) || dt_modifier_is(state, GDK_CONTROL_MASK) @@ -717,7 +717,7 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, GtkWidget *treeview = dt_gui_get_widget(gesture); static GdkModifierType last_mod_state = 0; - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); /* gesture coordinates are relative to the widget allocation, while * gtk_tree_view_get_path_at_pos() expects bin-window coordinates */ @@ -731,8 +731,8 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, bin_x, bin_y, &path, NULL, NULL, NULL); - GdkModifierType mod_state; - gtk_get_current_event_state(&mod_state); + const GdkModifierType mod_state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); 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) @@ -797,7 +797,6 @@ 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; } @@ -818,7 +817,6 @@ 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; } @@ -832,12 +830,10 @@ static void view_onButtonPressed_cb(GtkGestureSingle *gesture, int n_press, 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) @@ -3776,9 +3772,8 @@ static void popup_button_callback_cb(GtkGestureSingle *gesture, int n_press, gtk_widget_show_all(GTK_WIDGET(menu)); - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); gtk_menu_popup_at_pointer(GTK_MENU(menu), event); - gdk_event_free(event); return; } @@ -3864,7 +3859,7 @@ void _menuitem_preferences(GtkMenuItem *menuitem, _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_prefs_init_dialog_collect(dialog); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); diff --git a/src/libs/colorpicker.c b/src/libs/colorpicker.c index 6eedeb4d6b69..7227854cdb64 100644 --- a/src/libs/colorpicker.c +++ b/src/libs/colorpicker.c @@ -571,13 +571,15 @@ static void _live_sample_button_cb(GtkGestureSingle *gesture, if(simulate_event) { - dt_gui_simulate_button_event + // same entry real clicks and shortcuts use: a plain toggle starts a + // point pick, the right-variant an area pick (mimicking the old + // synthesized button-1/button-3 press on the picker button) + dt_iop_color_picker_toggle (data->picker_button, - GDK_BUTTON_PRESS, - /* button 1 to create use a point and 3 for a box */ data->primary_sample.size == DT_LIB_COLORPICKER_SIZE_POINT - ? 1 - : 3); + ? DT_ACTION_EFFECT_TOGGLE + : DT_ACTION_EFFECT_TOGGLE_RIGHT, + 1.0); } if(picker && picker->module) @@ -795,12 +797,15 @@ void gui_init(dt_lib_module_t *self) g_signal_connect(G_OBJECT(label), "size-allocate", G_CALLBACK(_label_size_allocate_callback), &data->primary_sample); - data->add_sample_button = dtgtk_button_new(dtgtk_cairo_paint_square_plus, 0, NULL); + data->add_sample_button = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, 0, NULL, + &(dtgtk_button_config_t){ + .action = DT_ACTION(self), + .action_label = N_("add sample"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_add_sample), + .clicked_data = self, + }); gtk_widget_set_sensitive(data->add_sample_button, FALSE); - g_signal_connect(G_OBJECT(data->add_sample_button), "clicked", - G_CALLBACK(_add_sample), self); - dt_action_define(DT_ACTION(self), NULL, N_("add sample"), - data->add_sample_button, &dt_action_def_button); gtk_container_add(GTK_CONTAINER(sample_row_events), dt_gui_hbox(sample_patch_wrapper, diff --git a/src/libs/duplicate.c b/src/libs/duplicate.c index 9f900cd002fd..13fe8d036e3c 100644 --- a/src/libs/duplicate.c +++ b/src/libs/duplicate.c @@ -359,29 +359,34 @@ static void _lib_duplicate_init_callback(gpointer instance, dt_lib_module_t *sel // duplicate this image with its history stack; only useful once the image // has actually been developed - otherwise it is equivalent to "original" const gboolean altered = dt_image_altered(imgid); - GtkWidget *bt_dup = dtgtk_button_new(dtgtk_cairo_paint_multiinstance, 0, NULL); + GtkWidget *bt_dup = dtgtk_button_new_full(dtgtk_cairo_paint_multiinstance, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("create a duplicate of this image with same history stack"), + .clicked_cb = G_CALLBACK(_lib_duplicate_duplicate_clicked_callback), + .clicked_data = self, + }); dt_gui_add_class(bt_dup, "dt_duplicate_duplicate"); - gtk_widget_set_tooltip_text(bt_dup, - _("create a duplicate of this image with same history stack")); g_object_set_data(G_OBJECT(bt_dup), "imgid", GINT_TO_POINTER(imgid)); - g_signal_connect( - G_OBJECT(bt_dup), "clicked", G_CALLBACK(_lib_duplicate_duplicate_clicked_callback), self); // create a 'virgin' duplicate of this image, discarding the source history stack - GtkWidget *bt_orig = dtgtk_button_new(dtgtk_cairo_paint_plus, 0, NULL); + GtkWidget *bt_orig = dtgtk_button_new_full(dtgtk_cairo_paint_plus, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("create a 'virgin' duplicate, discarding the source history stack"), + .clicked_cb = G_CALLBACK(_lib_duplicate_original_clicked_callback), + .clicked_data = self, + }); dt_gui_add_class(bt_orig, "dt_duplicate_original"); - gtk_widget_set_tooltip_text( - bt_orig, _("create a 'virgin' duplicate, discarding the source history stack")); g_object_set_data(G_OBJECT(bt_orig), "imgid", GINT_TO_POINTER(imgid)); - g_signal_connect( - G_OBJECT(bt_orig), "clicked", G_CALLBACK(_lib_duplicate_original_clicked_callback), self); - bt = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); + bt = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("delete this duplicate"), + .clicked_cb = G_CALLBACK(_lib_duplicate_delete), + .clicked_data = self, + }); dt_gui_add_class(bt, "dt_duplicate_delete"); - gtk_widget_set_tooltip_text(bt, _("delete this duplicate")); // gtk_widget_set_halign(bt, GTK_ALIGN_END); g_object_set_data(G_OBJECT(bt), "imgid", GINT_TO_POINTER(imgid)); - g_signal_connect(G_OBJECT(bt), "clicked", G_CALLBACK(_lib_duplicate_delete), self); gtk_grid_attach(GTK_GRID(hb), thumb->w_main, 0, 0, 1, 2); gtk_grid_attach(GTK_GRID(hb), lb, 1, 0, 1, 1); diff --git a/src/libs/export.c b/src/libs/export.c index 6b3772a92485..e49c329c0fa4 100644 --- a/src/libs/export.c +++ b/src/libs/export.c @@ -1187,27 +1187,22 @@ static void _update_style(const dt_stylemenu_data_t *menu_data) static void _apply_style_activate_callback(GtkMenuItem *menuitem, const dt_stylemenu_data_t *menu_data) { - GdkEvent *event = gtk_get_current_event(); - if(event && dt_gdk_event_get_type(event) == GDK_KEY_PRESS) - { + if(dt_gui_menuitem_activated_by_keyboard(GTK_WIDGET(menuitem))) _update_style(menu_data); - } - gdk_event_free(event); } -static gboolean _apply_style_button_callback(GtkMenuItem *menuitem, - GdkEventButton *event, - const dt_stylemenu_data_t *menu_data) +static void _apply_style_button_callback(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + const dt_stylemenu_data_t *menu_data) { - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) - { + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) _update_style(menu_data); - } else { //??? dt_shortcut_copy_lua(NULL, name); } - return FALSE; } static void _style_popupmenu_callback(GtkWidget *w, gpointer user_data) @@ -1651,10 +1646,13 @@ void gui_init(dt_lib_module_t *self) // Add style combo - GtkWidget *styles_button = dtgtk_button_new(dtgtk_cairo_paint_styles, 0, NULL); + GtkWidget *styles_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select style to be applied on export"), + .clicked_cb = G_CALLBACK(_style_popupmenu_callback), + .clicked_data = (gpointer)d, + }); gtk_widget_set_halign(styles_button,GTK_ALIGN_END); - g_signal_connect(G_OBJECT(styles_button), "clicked", G_CALLBACK(_style_popupmenu_callback), (gpointer)d); - gtk_widget_set_tooltip_text(styles_button, _("select style to be applied on export")); // dt_gui_add_help_link(styles, "bottom_panel_styles"); GtkBox *style_box = (GtkBox*)gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0); gtk_widget_set_tooltip_text(GTK_WIDGET(style_box), _("temporary style to use while exporting")); diff --git a/src/libs/export_metadata.c b/src/libs/export_metadata.c index 3f768fdc4425..aa4f035e549e 100644 --- a/src/libs/export_metadata.c +++ b/src/libs/export_metadata.c @@ -277,13 +277,19 @@ char *dt_lib_export_metadata_configuration_dialog(char *metadata_presets, const if(!ondisk) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(calculated), flags & DT_META_CALCULATED); - GtkWidget *plus = dtgtk_button_new(dtgtk_cairo_paint_plus_simple, 0, NULL); - gtk_widget_set_tooltip_text(plus, _("add an output metadata tag")); - g_signal_connect(G_OBJECT(plus), "clicked", G_CALLBACK(_add_tag_button_clicked), (gpointer)d); - - GtkWidget *minus = dtgtk_button_new(dtgtk_cairo_paint_minus_simple, 0, NULL); - gtk_widget_set_tooltip_text(minus, _("delete metadata tag")); - g_signal_connect(G_OBJECT(minus), "clicked", G_CALLBACK(_delete_tag_button_clicked), (gpointer)d); + GtkWidget *plus = dtgtk_button_new_full(dtgtk_cairo_paint_plus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add an output metadata tag"), + .clicked_cb = G_CALLBACK(_add_tag_button_clicked), + .clicked_data = (gpointer)d, + }); + + GtkWidget *minus = dtgtk_button_new_full(dtgtk_cairo_paint_minus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("delete metadata tag"), + .clicked_cb = G_CALLBACK(_delete_tag_button_clicked), + .clicked_data = (gpointer)d, + }); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); diff --git a/src/libs/filtering.c b/src/libs/filtering.c index c0bcf330ce41..abcb0b97df56 100644 --- a/src/libs/filtering.c +++ b/src/libs/filtering.c @@ -1976,10 +1976,12 @@ static gboolean _sort_init(_widgets_sort_t *sort, const dt_collection_sort_t sor if(num == 0) dt_action_define(DT_ACTION(self), NULL, _("sort direction"), sort->direction, &dt_action_def_toggle); - sort->close = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); + sort->close = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("remove this sort order"), + }); gtk_widget_set_no_show_all(sort->close, TRUE); g_object_set_data(G_OBJECT(sort->close), "sort", sort); - gtk_widget_set_tooltip_text(sort->close, _("remove this sort order")); dt_gui_connect_click_all(sort->close, _sort_close_cb, NULL, self); gtk_box_pack_start(GTK_BOX(sort->box), sort->close, FALSE, FALSE, 0); } diff --git a/src/libs/filters/colors.c b/src/libs/filters/colors.c index c10398b360a0..e3c2e8a3a6a3 100644 --- a/src/libs/filters/colors.c +++ b/src/libs/filters/colors.c @@ -339,29 +339,40 @@ static void _colors_widget_init(dt_lib_filtering_rule_t *rule, const dt_collecti gtk_widget_set_halign(hbox, GTK_ALIGN_CENTER); for(int k = 0; k < DT_COLORLABELS_LAST + 1; k++) { - colors->colors[k] = dtgtk_button_new(dtgtk_cairo_paint_label_sel, k, NULL); + colors->colors[k] = dtgtk_button_new_full(dtgtk_cairo_paint_label_sel, k, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("filter by images color label" + "\nclick to toggle the color label selection" + "\nctrl+click to exclude the color label" + "\nthe gray button affects all color labels"), + .action = DT_ACTION(self), + .action_section = N_("rules"), + .action_label = N_("color label"), + .action_def = &dt_action_def_colors_rule, + }); g_object_set_data(G_OBJECT(colors->colors[k]), "colors_index", GINT_TO_POINTER(k)); dt_gui_add_class(colors->colors[k], "dt_no_hover"); dt_gui_add_class(colors->colors[k], "dt_dimmed"); g_object_set_data(G_OBJECT(colors->colors[k]), "colors_self", colors); gtk_box_pack_start(GTK_BOX(hbox), colors->colors[k], FALSE, FALSE, 0); - gtk_widget_set_tooltip_text(colors->colors[k], _("filter by images color label" - "\nclick to toggle the color label selection" - "\nctrl+click to exclude the color label" - "\nthe gray button affects all color labels")); dt_gui_connect_click(colors->colors[k], _colors_clicked_gesture, NULL, colors); dt_gui_connect_motion(colors->colors[k], NULL, _colors_enter_cb, NULL, GINT_TO_POINTER(k)); - dt_action_define(DT_ACTION(self), N_("rules"), N_("color label"), colors->colors[k], &dt_action_def_colors_rule); } - colors->operator= dtgtk_button_new(dtgtk_cairo_paint_intersection, 0, NULL); + colors->operator = dtgtk_button_new_full(dtgtk_cairo_paint_intersection, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("filter by images color label" + "\nintersection: images having all selected color labels" + "\nunion: images with at least one of the selected color labels"), + .action = DT_ACTION(self), + .action_section = N_("rules"), + .action_label = N_("color label"), + .action_def = &dt_action_def_colors_rule, + .clicked_cb = G_CALLBACK(_colors_operator_clicked), + .clicked_data = colors, + }); gtk_box_pack_start(GTK_BOX(hbox), colors->operator, FALSE, FALSE, 2); - gtk_widget_set_tooltip_text(colors->operator, - _("filter by images color label" - "\nintersection: images having all selected color labels" - "\nunion: images with at least one of the selected color labels")); - g_signal_connect(G_OBJECT(colors->operator), "clicked", G_CALLBACK(_colors_operator_clicked), colors); dt_gui_connect_motion(colors->operator, NULL, _colors_enter_cb, NULL, GINT_TO_POINTER(-1)); - dt_action_t *ac = dt_action_define(DT_ACTION(self), N_("rules"), N_("color label"), colors->operator, &dt_action_def_colors_rule); + dt_action_t *ac = dt_action_widget(colors->operator); dt_shortcut_register(ac, DT_COLORLABELS_RED + 1, DT_ACTION_EFFECT_TOGGLE, GDK_KEY_F1, GDK_SHIFT_MASK); dt_shortcut_register(ac, DT_COLORLABELS_YELLOW + 1, DT_ACTION_EFFECT_TOGGLE, GDK_KEY_F2, GDK_SHIFT_MASK); diff --git a/src/libs/geotagging.c b/src/libs/geotagging.c index 4e50a22599b7..38321335dc5c 100644 --- a/src/libs/geotagging.c +++ b/src/libs/geotagging.c @@ -1807,11 +1807,13 @@ void gui_init(dt_lib_module_t *self) box = _gui_init_datetime(_("original date/time"), &d->dt0, 1, self, group, NULL, NULL); gtk_grid_attach(grid, box, 0, line++, 4, 1); - d->lock_offset = dtgtk_togglebutton_new(dtgtk_cairo_paint_lock, 0, NULL); - gtk_widget_set_tooltip_text(d->lock_offset, - _("lock date/time offset value to apply it onto another selection")); + d->lock_offset = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lock, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("lock date/time offset value to apply it onto another selection"), + .clicked_cb = G_CALLBACK(_toggle_lock_button_callback), + .clicked_data = (gpointer)self, + }); gtk_widget_set_halign(d->lock_offset, GTK_ALIGN_START); - g_signal_connect(G_OBJECT(d->lock_offset), "clicked", G_CALLBACK(_toggle_lock_button_callback), (gpointer)self); box = _gui_init_datetime(_("date/time offset"), &d->of, 2, self, group, d->lock_offset, _("offset or difference ([-]dd hh:mm:ss[.sss])")); @@ -1890,13 +1892,16 @@ void gui_init(dt_lib_module_t *self) label = dt_ui_section_label_new(C_("section", "GPX file")); gtk_grid_attach(grid, label, 0, line++, 4, 1); - d->map.gpx_button = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + d->map.gpx_button = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select a GPX track file..."), + .clicked_cb = G_CALLBACK(_choose_gpx_callback), + .clicked_data = self, + }); gtk_widget_set_hexpand(d->map.gpx_button, FALSE); gtk_widget_set_halign(d->map.gpx_button, GTK_ALIGN_START); gtk_widget_set_name(d->map.gpx_button, "non-flat"); - gtk_widget_set_tooltip_text(d->map.gpx_button, _("select a GPX track file...")); gtk_grid_attach(grid, d->map.gpx_button, 0, line, 1, 1); - g_signal_connect(G_OBJECT(d->map.gpx_button), "clicked", G_CALLBACK(_choose_gpx_callback), self); d->map.gpx_file = dt_ui_label_new(""); gtk_label_set_ellipsize(GTK_LABEL(d->map.gpx_file ), PANGO_ELLIPSIZE_MIDDLE); diff --git a/src/libs/histogram.c b/src/libs/histogram.c index d9b01fbb2a68..137085df6400 100644 --- a/src/libs/histogram.c +++ b/src/libs/histogram.c @@ -340,8 +340,7 @@ static void _drawable_drag_update(GtkGestureDrag* gesture, dt_scopes_t *s) { dt_scopes_mode_t *const cur_mode = s->cur_mode; - // GTK4: use gtk_event_controller_get_current_event_state() - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); if(!event) return; if(gdk_event_get_event_type(event) == GDK_MOTION_NOTIFY) { @@ -356,7 +355,6 @@ static void _drawable_drag_update(GtkGestureDrag* gesture, s->last_offset_x = offset_x; s->last_offset_y = offset_y; } - gdk_event_free(event); } static void _drawable_button_press(GtkGestureSingle *gesture, @@ -484,12 +482,7 @@ static void _eventbox_scroll_callback(GtkEventControllerScroll* self, const gdouble dy, dt_scopes_t *s) { - // GTK4 note: gtk_get_current_event() returns NULL inside event controller - // callbacks in GTK4. For state checks, use - // gtk_event_controller_get_current_event_state(GTK_EVENT_CONTROLLER(self), &state) - // instead. For forwarding the event via gtk_widget_event(), use - // gtk_gesture_get_last_event() and gdk_event_copy() if needed. - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(self)); if(!event) return; if(gdk_event_get_event_type(event) == GDK_SCROLL) { @@ -497,7 +490,12 @@ static void _eventbox_scroll_callback(GtkEventControllerScroll* self, GDK_SHIFT_MASK | GDK_MOD1_MASK)) { // bubble to adjusting the overall widget size - gtk_widget_event(s->scope_draw, event); + dt_gui_forward_scroll(self, s->scope_draw); +#if !GTK_CHECK_VERSION(4, 0, 0) + // free our own copy; dt_gui_forward_scroll() acquires and frees its own + gdk_event_free(event); +#endif + return; } else if(s->highlight != DT_SCOPES_HIGHLIGHT_NONE) { @@ -521,13 +519,22 @@ static void _eventbox_scroll_callback(GtkEventControllerScroll* self, else { int ebx, eby; +#if GTK_CHECK_VERSION(4, 0, 0) + gdouble gdk_x = 0.0, gdk_y = 0.0; + gdk_event_get_position(event, &gdk_x, &gdk_y); + gtk_widget_translate_coordinates(dt_gui_get_widget(GTK_EVENT_CONTROLLER(self)), s->scope_draw, + (int)gdk_x, (int)gdk_y, &ebx, &eby); +#else gtk_widget_translate_coordinates(gtk_get_event_widget(event), s->scope_draw, (int)dt_gdk_event_get_x(event), (int)dt_gdk_event_get_y(event), &ebx, &eby); +#endif dt_scopes_call_if_exists(s->cur_mode, eventbox_scroll, ebx, eby, dx, dy, dt_gdk_event_get_state(event)); } } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } // TRUE if the point (x, y) -- given in `from` widget coordinates -- lies @@ -598,9 +605,14 @@ static void _eventbox_leave_notify_callback(GtkEventControllerMotion *controller // when click between buttons on the buttonbox a leave event is // generated -- ignore it (for GTK 4, replace this with the simpler // gtk_event_controller_get_current_event()) - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_mode(event) == GDK_CROSSING_UNGRAB + && gdk_crossing_event_get_detail(event) == GDK_NOTIFY_INFERIOR) + return; +#else if(gdk_event_get_event_type(event) == GDK_LEAVE_NOTIFY) { const GdkEventCrossing *xc = &event->crossing; @@ -611,6 +623,7 @@ static void _eventbox_leave_notify_callback(GtkEventControllerMotion *controller } } gdk_event_free(event); +#endif } gtk_widget_hide(s->button_box_left); gtk_widget_hide(s->button_box_right); @@ -822,12 +835,16 @@ void gui_init(dt_lib_module_t *self) { const char *const name = dt_scopes_call(&s->modes[i], name); s->modes[i].button_activate = - dtgtk_togglebutton_new(dt_lib_histogram_scope_type_icons[i], CPF_NONE, NULL); + dtgtk_togglebutton_new_full(dt_lib_histogram_scope_type_icons[i], CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _(name), + .action = dark, + .action_section = N_("modes"), + .action_label = name, + .action_def = &dt_action_def_toggle, + }); gtk_widget_set_halign(s->modes[i].button_activate, GTK_ALIGN_CENTER); gtk_widget_set_valign(s->modes[i].button_activate, GTK_ALIGN_START); - gtk_widget_set_tooltip_text(s->modes[i].button_activate, _(name)); - dt_action_define(dark, N_("modes"), name, - s->modes[i].button_activate, &dt_action_def_toggle); // GTK4: use gtk_toggle_button_set_group(), GTK3: handle in callback s->modes[i].toggle_signal_handler = g_signal_connect_data(G_OBJECT(s->modes[i].button_activate), "toggled", diff --git a/src/libs/history.c b/src/libs/history.c index 68cc852afed1..e548d10e5c3f 100644 --- a/src/libs/history.c +++ b/src/libs/history.c @@ -147,15 +147,15 @@ void gui_init(dt_lib_module_t *self) dt_gui_connect_click(d->compress_button, _lib_history_compress_pressed_callback, NULL, self); /* add toolbar button for creating style */ - d->create_button = dtgtk_button_new(dtgtk_cairo_paint_styles, CPF_NONE, NULL); - g_signal_connect(G_OBJECT(d->create_button), "clicked", - G_CALLBACK(_lib_history_create_style_button_clicked_callback), NULL); + d->create_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("create a style from the current history stack"), + .action = DT_ACTION(self), + .action_label = N_("create style from history"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_lib_history_create_style_button_clicked_callback), + }); gtk_widget_set_name(d->create_button, "non-flat"); - gtk_widget_set_tooltip_text(d->create_button, - _("create a style from the current history stack")); - dt_action_define(DT_ACTION(self), NULL, - N_("create style from history"), - d->create_button, &dt_action_def_button); self->widget = dt_gui_vbox (dt_ui_resize_wrap(d->history_box, 1, "plugins/darkroom/history/windowheight"), @@ -206,22 +206,28 @@ static GtkWidget *_lib_history_create_button(dt_lib_module_t *self, gtk_label_set_markup (GTK_LABEL (lab), label); if(always_on) { - onoff = dtgtk_button_new(dtgtk_cairo_paint_switch_on, 0, NULL); + onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch_on, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("always-on module"), + }); dtgtk_button_set_active(DTGTK_BUTTON(onoff), TRUE); - gtk_widget_set_tooltip_text(onoff, _("always-on module")); } else if(default_enabled) { - onoff = dtgtk_button_new(dtgtk_cairo_paint_switch, 0, NULL); + onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("default enabled module"), + }); dtgtk_button_set_active(DTGTK_BUTTON(onoff), enabled); - gtk_widget_set_tooltip_text(onoff, _("default enabled module")); } else { if(deprecated) { - onoff = dtgtk_button_new(dtgtk_cairo_paint_switch_deprecated, 0, NULL); - gtk_widget_set_tooltip_text(onoff, _("deprecated module")); + onoff = dtgtk_button_new_full(dtgtk_cairo_paint_switch_deprecated, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("deprecated module"), + }); } else { diff --git a/src/libs/image.c b/src/libs/image.c index b48f95b12b2c..69300fe06b1e 100644 --- a/src/libs/image.c +++ b/src/libs/image.c @@ -545,27 +545,29 @@ void gui_init(dt_lib_module_t *self) GDK_KEY_d, GDK_CONTROL_MASK); gtk_grid_attach(grid, d->duplicate_button, 2, line++, 2, 1); - d->rotate_ccw_button = dtgtk_button_new(dtgtk_cairo_paint_refresh, CPF_NONE, NULL);; + d->rotate_ccw_button = dtgtk_button_new_full(dtgtk_cairo_paint_refresh, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("rotate selected images 90 degrees CCW"), + .action = DT_ACTION(self), + .action_label = N_("rotate selected images 90 degrees CCW"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(button_clicked), + .clicked_data = GINT_TO_POINTER(4), + }); gtk_widget_set_name(d->rotate_ccw_button, "non-flat"); - gtk_widget_set_tooltip_text(d->rotate_ccw_button, - _("rotate selected images 90 degrees CCW")); gtk_grid_attach(grid, d->rotate_ccw_button, 0, line, 1, 1); - g_signal_connect(G_OBJECT(d->rotate_ccw_button), "clicked", - G_CALLBACK(button_clicked), GINT_TO_POINTER(4)); - dt_action_define(DT_ACTION(self), NULL, - N_("rotate selected images 90 degrees CCW"), - d->rotate_ccw_button, &dt_action_def_button); - d->rotate_cw_button = dtgtk_button_new(dtgtk_cairo_paint_refresh, 1 | CPF_NONE, NULL); + d->rotate_cw_button = dtgtk_button_new_full(dtgtk_cairo_paint_refresh, 1 | CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("rotate selected images 90 degrees CW"), + .action = DT_ACTION(self), + .action_label = N_("rotate selected images 90 degrees CW"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(button_clicked), + .clicked_data = GINT_TO_POINTER(5), + }); gtk_widget_set_name(d->rotate_cw_button, "non-flat"); - gtk_widget_set_tooltip_text(d->rotate_cw_button, - _("rotate selected images 90 degrees CW")); gtk_grid_attach(grid, d->rotate_cw_button, 1, line, 1, 1); - g_signal_connect(G_OBJECT(d->rotate_cw_button), "clicked", - G_CALLBACK(button_clicked), GINT_TO_POINTER(5)); - dt_action_define(DT_ACTION(self), NULL, - N_("rotate selected images 90 degrees CW"), - d->rotate_cw_button, &dt_action_def_button); d->reset_button = dt_action_button_new(self, N_("reset rotation"), button_clicked, GINT_TO_POINTER(6), diff --git a/src/libs/import.c b/src/libs/import.c index b5d5572d45b5..53ad770006d8 100644 --- a/src/libs/import.c +++ b/src/libs/import.c @@ -1320,8 +1320,8 @@ static void _folders_button_press_cb(GtkGestureSingle *gesture, int n_press, gtk_tree_view_convert_widget_to_bin_window_coords(GTK_TREE_VIEW(view), (gint)x, (gint)y, &bin_x, &bin_y); - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(n_press == 1 && gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY @@ -1439,25 +1439,28 @@ static void _set_places_list(GtkWidget *places_paned, g_free(markup); gtk_box_pack_start(GTK_BOX(places_header), places_label, FALSE, FALSE, 0); - GtkWidget *places_reset = dtgtk_button_new(dtgtk_cairo_paint_reset, 0, NULL); - gtk_widget_set_tooltip_text - (places_reset, - _("restore all default places you have removed")); - g_signal_connect(places_reset, "clicked", G_CALLBACK(_places_reset_callback), self); + GtkWidget *places_reset = dtgtk_button_new_full(dtgtk_cairo_paint_reset, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("restore all default places you have removed"), + .clicked_cb = G_CALLBACK(_places_reset_callback), + .clicked_data = self, + }); gtk_box_pack_end(GTK_BOX(places_header), places_reset, FALSE, FALSE, 0); - d->remove_place_button = dtgtk_button_new(dtgtk_cairo_paint_minus_simple, 0, NULL); - gtk_widget_set_tooltip_text - (d->remove_place_button, - _("remove the selected custom place")); - g_signal_connect(d->remove_place_button, "clicked", G_CALLBACK(_remove_selected_place), self); + d->remove_place_button = dtgtk_button_new_full(dtgtk_cairo_paint_minus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("remove the selected custom place"), + .clicked_cb = G_CALLBACK(_remove_selected_place), + .clicked_data = self, + }); gtk_box_pack_end(GTK_BOX(places_header), d->remove_place_button, FALSE, FALSE, 0); - GtkWidget *places_add = dtgtk_button_new(dtgtk_cairo_paint_plus_simple, 0, NULL); - gtk_widget_set_tooltip_text - (places_add, - _("add a custom place")); - g_signal_connect(places_add, "clicked", G_CALLBACK(_lib_import_select_folder), self); + GtkWidget *places_add = dtgtk_button_new_full(dtgtk_cairo_paint_plus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add a custom place"), + .clicked_cb = G_CALLBACK(_lib_import_select_folder), + .clicked_data = self, + }); gtk_box_pack_end(GTK_BOX(places_header), places_add, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(places_top_box), places_header, FALSE, FALSE, 0); @@ -1991,13 +1994,15 @@ static void _set_expander_content(GtkWidget *rbox, gtk_container_remove(GTK_CONTAINER(grid), basedir); gtk_box_pack_start(GTK_BOX(hbox), basedir, TRUE, TRUE, 0); g_object_unref(basedir); - GtkWidget *browsedir = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL); + GtkWidget *browsedir = dtgtk_button_new_full(dtgtk_cairo_paint_directory, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select directory"), + .clicked_cb = G_CALLBACK(_browse_basedir_clicked), + .clicked_data = basedir, + }); gtk_widget_set_name(browsedir, "non-flat"); - gtk_widget_set_tooltip_text(browsedir, _("select directory")); gtk_box_pack_start(GTK_BOX(hbox), browsedir, FALSE, FALSE, 0); - g_signal_connect(G_OBJECT(browsedir), "clicked", G_CALLBACK(_browse_basedir_clicked), - basedir); gtk_grid_attach_next_to(grid, hbox, gtk_grid_get_child_at(grid, 0, line - 1), GTK_POS_RIGHT, 1, 1); @@ -2039,8 +2044,7 @@ static void _import_from_dialog_new(dt_lib_module_t* self) #endif dt_gui_dialog_restore_size(GTK_DIALOG(d->from.dialog), "import"); gtk_window_set_transient_for(GTK_WINDOW(d->from.dialog), GTK_WINDOW(win)); - g_signal_connect(d->from.dialog, "key-press-event", - G_CALLBACK(dt_handle_dialog_enter), self); + dt_gui_connect_key(d->from.dialog, dt_handle_dialog_enter, self); // images numbers in action-box GtkWidget *box = dt_gui_container_first_child(GTK_CONTAINER(d->from.dialog)); diff --git a/src/libs/lib.c b/src/libs/lib.c index 925469d3d26d..59dec8b21a80 100644 --- a/src/libs/lib.c +++ b/src/libs/lib.c @@ -47,8 +47,6 @@ typedef struct dt_lib_presets_edit_dialog_t gint old_id; } dt_lib_presets_edit_dialog_t; -static gpointer _active_menu_item = NULL; - static gchar *_get_lib_view_path(const dt_lib_module_t *module, const dt_view_t *cv, char *suffix); @@ -449,15 +447,19 @@ static void _menuitem_activate_preset(GtkMenuItem *menuitem, minfo->plugin_name, minfo->version); } -static gboolean _menuitem_button_preset(GtkMenuItem *menuitem, - GdkEventButton *event, - dt_lib_module_info_t *minfo) +/* secondary click copies the preset as lua and keeps the menu open: the + * gesture is in CAPTURE phase and claims the sequence, so the menu shell + * never sees the press or release and never activates the item. primary + * clicks flow normally and apply via the "activate" signal. */ +static void _menuitem_button_preset_released(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + dt_lib_module_info_t *minfo) { - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) return FALSE; - + GtkMenuItem *menuitem = GTK_MENU_ITEM(dt_gui_get_widget(gesture)); dt_shortcut_copy_lua((dt_action_t*)minfo->module, - g_object_get_data(G_OBJECT(menuitem), "dt-preset-name")); - return TRUE; + g_object_get_data(G_OBJECT(menuitem), "dt-preset-name")); } static void _free_module_info(GtkWidget *widget, @@ -469,174 +471,66 @@ static void _free_module_info(GtkWidget *widget, free(minfo); } -static void _dt_lib_presets_popup_menu_show(dt_lib_module_info_t *minfo, - GtkWidget *w) -{ - GtkMenu *menu = GTK_MENU(gtk_menu_new()); +/* ---------- lib preset menu (dt_gui_presets_popup_menu_show() ops) ---------- */ - const gboolean hide_default = dt_conf_get_bool("plugins/lighttable/hide_default_presets"); +static gchar *_lib_presets_query(gpointer data) +{ + (void)data; const gboolean default_first = dt_conf_get_bool("modules/default_presets_first"); - - g_signal_connect(G_OBJECT(menu), "destroy", G_CALLBACK(_free_module_info), minfo); - - GtkWidget *mi; - int active_preset = -1, cnt = 0; - gboolean selected_writeprotect = FALSE; - sqlite3_stmt *stmt; - // order like the pref value // clang-format off - gchar *query = g_strdup_printf("SELECT name, op_params, writeprotect, description" - " FROM data.presets" - " WHERE operation=?1 AND op_version=?2" - " ORDER BY writeprotect %s, LOWER(name), rowid", - default_first ? "DESC" : "ASC"); + return g_strdup_printf("SELECT name, op_params, writeprotect, description" + " FROM data.presets" + " WHERE operation=?1 AND op_version=?2" + " ORDER BY writeprotect %s, LOWER(name), rowid", + default_first ? "DESC" : "ASC"); // clang-format on - DT_DEBUG_SQLITE3_PREPARE_V2(dt_database_get(darktable.db), query, -1, &stmt, NULL); +} + +static void _lib_presets_bind(sqlite3_stmt *stmt, gpointer data) +{ + const dt_lib_module_info_t *minfo = data; DT_DEBUG_SQLITE3_BIND_TEXT(stmt, 1, minfo->plugin_name, -1, SQLITE_TRANSIENT); DT_DEBUG_SQLITE3_BIND_INT(stmt, 2, minfo->version); - g_free(query); - - // collect all presets for op from db - gboolean found = FALSE; - int last_wp = -1; - gchar **prev_split = NULL; - GtkWidget *submenu = GTK_WIDGET(menu); - GtkWidget *mainmenu = submenu; - GSList *menu_path = NULL; // stack of menuitems which are the parents of submenus on menu_stack - while(sqlite3_step(stmt) == SQLITE_ROW) - { - // default vs built-in stuff - const gboolean writeprotect = sqlite3_column_int(stmt, 2); - if(hide_default && writeprotect) - { - // skip default module if set to hide them. - continue; - } - if(last_wp == -1) - { - last_wp = writeprotect; - } - else if(last_wp != writeprotect) - { - last_wp = writeprotect; - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - *prev_split[0] = '\0'; // make first level mismatch so we start over - } +} - void *op_params = (void *)sqlite3_column_blob(stmt, 1); - int32_t op_params_size = sqlite3_column_bytes(stmt, 1); - const char *name = (char *)sqlite3_column_text(stmt, 0); - - if(darktable.gui->last_preset && strcmp(darktable.gui->last_preset, name) == 0) - found = TRUE; - - mi = dt_insert_preset_in_menu_hierarchy(name, - &menu_path, mainmenu, &submenu, &prev_split, - FALSE, writeprotect); - - // selected in bold: - // printf("comparing %d bytes to %d\n", op_params_size, minfo->params_size); - // for(int k=0;kparams, op_params, k);k++) - // printf("compare [%c %c] %d: - // %d\n", - // ((const char*)(minfo->params))[k], - // ((const char*)(op_params))[k], - // k, memcmp(minfo->params, op_params, k)); - if(op_params_size == minfo->params_size - && !memcmp(minfo->params, op_params, op_params_size)) - { - active_preset = cnt; - selected_writeprotect = writeprotect; - dt_gui_add_class(mi, "active_menu_item"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(mi), TRUE); - g_set_weak_pointer(&_active_menu_item, mi); - // walk back up the menu hierarchy and highlight the entire path down to the current leaf - for(const GSList *mp = menu_path; mp; mp = g_slist_next(mp)) - dt_gui_add_class(gtk_bin_get_child(GTK_BIN(mp->data)), "active_menu_item"); - } - g_object_set_data_full(G_OBJECT(mi), "dt-preset-name", g_strdup(name), g_free); - g_object_set_data(G_OBJECT(mi), "dt-preset-module", minfo->module); - dt_action_define(&minfo->module->actions, "preset", name, mi, NULL); - - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_activate_preset), minfo); - g_signal_connect(G_OBJECT(mi), "button-release-event", - G_CALLBACK(_menuitem_button_preset), minfo); - gtk_widget_set_tooltip_text(mi, (const char *)sqlite3_column_text(stmt, 3)); - gtk_widget_set_has_tooltip(mi, TRUE); - cnt++; - } - sqlite3_finalize(stmt); +static gboolean _lib_presets_is_active(sqlite3_stmt *stmt, gpointer data, + gboolean *writeprotect) +{ + const dt_lib_module_info_t *minfo = data; + const void *op_params = sqlite3_column_blob(stmt, 1); + const int32_t op_params_size = sqlite3_column_bytes(stmt, 1); - if(cnt > 0) + if(op_params_size == minfo->params_size + && !memcmp(minfo->params, op_params, op_params_size)) { - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - cnt = 0; + *writeprotect = sqlite3_column_int(stmt, 2); + return TRUE; } + return FALSE; +} - if(minfo->module->manage_presets) - { - mi = gtk_menu_item_new_with_label(_("manage presets...")); - g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_manage_presets), minfo); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - cnt++; - } - else if(active_preset >= 0) // FIXME: this doesn't seem to work. - { - if(!selected_writeprotect) - { - mi = gtk_menu_item_new_with_label(_("edit this preset..")); - g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_edit_preset), minfo); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - - mi = gtk_menu_item_new_with_label(_("delete this preset")); - g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_delete_preset), minfo); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - cnt++; - } - } - else - { - mi = gtk_menu_item_new_with_label(_("store new preset..")); - if(minfo->params_size == 0) - { - gtk_widget_set_sensitive(mi, FALSE); - gtk_widget_set_tooltip_text(mi, _("nothing to save")); - } - else - g_signal_connect(G_OBJECT(mi), "activate", G_CALLBACK(_menuitem_new_preset), minfo); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); +static void _lib_presets_connect_row(GtkWidget *mi, sqlite3_stmt *stmt, gpointer data) +{ + dt_lib_module_info_t *minfo = data; + const char *name = (const char *)sqlite3_column_text(stmt, 0); - if(darktable.gui->last_preset && found) - { - char *local_last_name = dt_util_localize_segmented_name(darktable.gui->last_preset, TRUE); - char *markup = g_markup_printf_escaped("%s %s", - _("update preset"), - local_last_name); - g_free(local_last_name); - mi = gtk_menu_item_new_with_label(""); - gtk_widget_set_sensitive(mi, minfo->params_size > 0); - gtk_label_set_markup(GTK_LABEL(gtk_bin_get_child(GTK_BIN(mi))), markup); - g_object_set_data_full(G_OBJECT(mi), "dt-preset-name", - g_strdup(darktable.gui->last_preset), g_free); - g_signal_connect(G_OBJECT(mi), "activate", - G_CALLBACK(_menuitem_update_preset), minfo); - gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi); - g_free(markup); - } - cnt++; - } + g_object_set_data_full(G_OBJECT(mi), "dt-preset-name", g_strdup(name), g_free); + g_object_set_data(G_OBJECT(mi), "dt-preset-module", minfo->module); + dt_action_define(&minfo->module->actions, "preset", name, mi, NULL); - if(minfo->module->set_preferences) - { - if(cnt>0) - { - gtk_menu_shell_append(GTK_MENU_SHELL(menu), gtk_separator_menu_item_new()); - } - minfo->module->set_preferences(GTK_MENU_SHELL(menu), minfo->module); - } + g_signal_connect(G_OBJECT(mi), "activate", + G_CALLBACK(_menuitem_activate_preset), minfo); + GtkGestureSingle *gesture = + dt_gui_connect_click(mi, NULL, _menuitem_button_preset_released, minfo); + gtk_gesture_single_set_button(gesture, GDK_BUTTON_SECONDARY); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(gesture), GTK_PHASE_CAPTURE); + g_signal_connect(G_OBJECT(gesture), "begin", G_CALLBACK(dt_gui_gesture_claim), NULL); +} - dt_gui_menu_popup(menu, w, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); +static void _lib_presets_prefs(GtkMenu *menu, gpointer data) +{ + const dt_lib_module_info_t *minfo = data; + minfo->module->set_preferences(GTK_MENU_SHELL(menu), minfo->module); } static int _lib_position(const dt_lib_module_t *module) @@ -1000,8 +894,27 @@ static void _lib_gui_reset_button_clicked_callback(GtkWidget *widget, static void _presets_popup_callback(GtkWidget *button, dt_lib_module_t *module) { - dt_lib_module_info_t *mi = _get_module_info_for_module(module); - _dt_lib_presets_popup_menu_show(mi, button); + dt_lib_module_info_t *minfo = _get_module_info_for_module(module); + + const dt_gui_presets_menu_ops_t ops = { + .data = minfo, + .hide_defaults_pref = "plugins/lighttable/hide_default_presets", + .query = _lib_presets_query, + .bind = _lib_presets_bind, + .is_active = _lib_presets_is_active, + .connect_row = _lib_presets_connect_row, + .params_size = minfo->params_size, + .manage_cb = minfo->module->manage_presets ? G_CALLBACK(_menuitem_manage_presets) : NULL, + .edit_cb = G_CALLBACK(_menuitem_edit_preset), + .del_cb = G_CALLBACK(_menuitem_delete_preset), + .store_cb = G_CALLBACK(_menuitem_new_preset), + .update_cb = G_CALLBACK(_menuitem_update_preset), + .prefs = minfo->module->set_preferences ? _lib_presets_prefs : NULL, + }; + + GtkMenu *menu = dt_gui_presets_popup_menu_show(&ops); + g_signal_connect(G_OBJECT(menu), "destroy", G_CALLBACK(_free_module_info), minfo); + dt_gui_menu_popup(menu, button, GDK_GRAVITY_SOUTH_EAST, GDK_GRAVITY_NORTH_EAST); if(button) dtgtk_button_set_active(DTGTK_BUTTON(button), FALSE); @@ -1057,8 +970,8 @@ static void _lib_plugin_arrow_button_press_cb(GtkGestureSingle *gesture, { if(n_press > 1) return; const guint button = gtk_gesture_single_get_current_button(gesture); - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(button == GDK_BUTTON_PRIMARY) { @@ -1123,17 +1036,22 @@ static void _lib_plugin_header_button_release_cb(GtkGestureSingle *gesture, dt_lib_module_t *module) { // ignore clicks on buttons inside the header (presets, reset, enable) - // GTK4: use gtk_gesture_get_last_event(gesture) instead of gtk_get_current_event() - GdkEvent *event = gtk_get_current_event(); - if(event) - { - if(GTK_IS_BUTTON(gtk_get_event_widget(event))) - { - gdk_event_free(event); - return; - } - gdk_event_free(event); - } +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4 has no gtk_get_event_widget(): find the widget under the release + * point inside the header instead, and ignore the release if it lands on + * (or inside) a button -- the gesture receives releases over the whole + * header, including its child buttons. The surface-compare alternative + * is wrong: gdk_event_get_surface() always returns this widget's own + * surface. */ + GtkWidget *const header = dt_gui_get_widget(gesture); + GtkWidget *target = gtk_widget_pick(header, x, y, GTK_PICK_DEFAULT); + for(; target && target != header; target = gtk_widget_get_parent(target)) + if(GTK_IS_BUTTON(target)) return; +#else + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event && GTK_IS_BUTTON(gtk_get_event_widget((GdkEvent *)event))) + return; +#endif _lib_plugin_arrow_button_press_cb(gesture, n_press, x, y, module); } @@ -1190,13 +1108,20 @@ static void _body_enter_callback(GtkEventControllerMotion *controller, { // set focused module when entering (not when opening popup) // only act on non-inferior (not from child) normal crossings - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR + && gdk_crossing_event_get_mode(event) == GDK_CROSSING_NORMAL) +#else if(event->crossing.detail != GDK_NOTIFY_INFERIOR && event->crossing.mode == GDK_CROSSING_NORMAL) +#endif darktable.lib->gui_module = module; +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } } @@ -1205,13 +1130,20 @@ static void _body_leave_callback(GtkEventControllerMotion *controller, { // clear focused module when leaving // only act on non-inferior (not to child) normal crossings - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_detail(event) != GDK_NOTIFY_INFERIOR + && gdk_crossing_event_get_mode(event) == GDK_CROSSING_NORMAL) +#else if(event->crossing.detail != GDK_NOTIFY_INFERIOR && event->crossing.mode == GDK_CROSSING_NORMAL) +#endif darktable.lib->gui_module = NULL; +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } } @@ -1329,12 +1261,26 @@ GtkWidget *dt_lib_gui_get_expander(dt_lib_module_t *module) /* check if module is expandable */ if(!module->expandable(module)) { - if(module->presets_button) + /* Non-expandable libs (modulegroups, ...) keep their whole widget -- and + * with it the presets button -- outside an expander, and that widget + * survives view switches: dt_lib_gui_get_expander() runs once per switch, + * so the "clicked" wiring and the action registration below must happen + * only on the first call, or every lighttable<->darkroom round trip would + * stack one more popup and one more action on the button. Registering the + * button with the lib actions lets shortcuts and the action fallback + * (DT_ACTION_ELEMENT_PRESETS in _action_process) reach it, like the + * expandable header path does. GtkButton::clicked is unchanged on GTK4 + * and dt_action_define() is qdata-guarded, so this needs no + * #if GTK_CHECK_VERSION split. */ + if(module->presets_button + && !g_object_get_data(G_OBJECT(module->presets_button), + "dt-lib-presets-wired")) { - // FIXME separately define as darkroom widget shortcut/action, - // because not automatically registered via lib if presets btn - // has been loaded to be shown outside expander - g_signal_connect(G_OBJECT(module->presets_button), "clicked", G_CALLBACK(_presets_popup_callback), module); + g_signal_connect(G_OBJECT(module->presets_button), "clicked", + G_CALLBACK(_presets_popup_callback), module); + dt_action_define(&module->actions, NULL, NULL, module->presets_button, NULL); + g_object_set_data(G_OBJECT(module->presets_button), + "dt-lib-presets-wired", GINT_TO_POINTER(TRUE)); } module->expander = NULL; return NULL; @@ -1370,11 +1316,13 @@ GtkWidget *dt_lib_gui_get_expander(dt_lib_module_t *module) * initialize the header widgets */ /* add the expand indicator icon */ - module->arrow = dtgtk_button_new(dtgtk_cairo_paint_solid_arrow, 0, NULL); + module->arrow = dtgtk_button_new_full(dtgtk_cairo_paint_solid_arrow, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("show module"), + .action = &module->actions, + }); - gtk_widget_set_tooltip_text(module->arrow, _("show module")); dt_gui_connect_click_all(module->arrow, _lib_plugin_arrow_button_press_cb, NULL, module); - dt_action_define(&module->actions, NULL, NULL, module->arrow, NULL); gtk_box_pack_start(GTK_BOX(header), module->arrow, FALSE, FALSE, 0); /* add module label */ @@ -1403,25 +1351,29 @@ GtkWidget *dt_lib_gui_get_expander(dt_lib_module_t *module) gtk_box_pack_start(GTK_BOX(header), module->preset_label, FALSE, FALSE, 0); /* add preset button if module has implementation */ - module->presets_button = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL); - gtk_widget_set_tooltip_text(module->presets_button, _("presets and preferences")); - g_signal_connect(G_OBJECT(module->presets_button), "clicked", G_CALLBACK(_presets_popup_callback), module); + module->presets_button = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("presets and preferences"), + .action = &module->actions, + .clicked_cb = G_CALLBACK(_presets_popup_callback), + .clicked_data = module, + }); dt_gui_connect_motion(module->presets_button, NULL, _header_enter_notify_callback, NULL, GINT_TO_POINTER(DT_ACTION_ELEMENT_PRESETS)); if(!module->get_params && !module->set_preferences) gtk_widget_set_sensitive(GTK_WIDGET(module->presets_button), FALSE); - - dt_action_define(&module->actions, NULL, NULL, module->presets_button, NULL); gtk_box_pack_end(GTK_BOX(header), module->presets_button, FALSE, FALSE, 0); /* add reset button if module has implementation */ - module->reset_button = dtgtk_button_new(dtgtk_cairo_paint_reset, 0, NULL); + module->reset_button = dtgtk_button_new_full(dtgtk_cairo_paint_reset, 0, NULL, + &(dtgtk_button_config_t){ + .action = &module->actions, + .clicked_cb = G_CALLBACK(_lib_gui_reset_button_clicked_callback), + .clicked_data = 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); gtk_box_pack_end(GTK_BOX(header), module->reset_button, FALSE, FALSE, 0); /* add button box - for module's specific action button */ @@ -1790,13 +1742,16 @@ const dt_action_def_t dt_action_def_lib _action_elements, _action_fallbacks }; -gboolean dt_handle_dialog_enter(GtkWidget *widget, - GdkEventKey *event, +gboolean dt_handle_dialog_enter(GtkEventControllerKey *key, + guint keyval, + guint keycode, + GdkModifierType state, gpointer data) { - if(dt_gdk_event_get_keyval(event) == GDK_KEY_Return || dt_gdk_event_get_keyval(event) == GDK_KEY_KP_Enter) + if(keyval == GDK_KEY_Return || keyval == GDK_KEY_KP_Enter) { - gtk_dialog_response(GTK_DIALOG(widget), GTK_RESPONSE_ACCEPT); + gtk_dialog_response(GTK_DIALOG(gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(key))), + GTK_RESPONSE_ACCEPT); return TRUE; } return FALSE; diff --git a/src/libs/lib.h b/src/libs/lib.h index dc166564838a..9f3b9084f365 100644 --- a/src/libs/lib.h +++ b/src/libs/lib.h @@ -261,8 +261,10 @@ gint dt_lib_sort_plugins(gconstpointer a, gconstpointer b); void dt_lib_init_presets(dt_lib_module_t *module); /** handle Enter key for dialog. Note it uses GTK_RESPONSE_ACCEPT code */ -gboolean dt_handle_dialog_enter(GtkWidget *widget, - GdkEventKey *event, +gboolean dt_handle_dialog_enter(GtkEventControllerKey *key, + guint keyval, + guint keycode, + GdkModifierType state, gpointer data); // clang-format off diff --git a/src/libs/masks.c b/src/libs/masks.c index 188c93faf08d..ba6afd8bfde8 100644 --- a/src/libs/masks.c +++ b/src/libs/masks.c @@ -1254,8 +1254,8 @@ static void _tree_button_pressed_cb(GtkGestureSingle *gesture, int n_press, doub } else if(button == GDK_BUTTON_SECONDARY) { - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); // if we are already inside the selection, no change if(on_row && !gtk_tree_selection_path_is_selected(selection, mouse_path)) @@ -1516,9 +1516,8 @@ static void _tree_button_pressed_cb(GtkGestureSingle *gesture, int n_press, doub gtk_widget_show_all(GTK_WIDGET(menu)); - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); gtk_menu_popup_at_pointer(GTK_MENU(menu), event); - gdk_event_free(event); } } @@ -2271,59 +2270,83 @@ void gui_init(dt_lib_module_t *self) _get_pixbuf_from_cairo(dtgtk_cairo_paint_masks_exclusion, bs2 * 2, bs2); // initialise widgets - 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); + d->bt_gradient = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_gradient, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add gradient"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add gradient"), + .action_def = &dt_action_def_toggle, + }); 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); + d->bt_path = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_path, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add path"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add path"), + .action_def = &dt_action_def_toggle, + }); 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); + d->bt_ellipse = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_ellipse, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add ellipse"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add ellipse"), + .action_def = &dt_action_def_toggle, + }); 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); + d->bt_circle = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_circle, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add circle"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add circle"), + .action_def = &dt_action_def_toggle, + }); 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); + d->bt_brush = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_brush, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add brush"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add brush"), + .action_def = &dt_action_def_toggle, + }); 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); #ifdef HAVE_AI - 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); + d->bt_object = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_masks_object, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add AI object"), + .action = DT_ACTION(self), + .action_section = N_("shapes"), + .action_label = N_("add object"), + .action_def = &dt_action_def_toggle, + }); 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 48f1828f08e5..479ad5a36995 100644 --- a/src/libs/metadata.c +++ b/src/libs/metadata.c @@ -448,11 +448,19 @@ static gboolean _key_pressed_cb(GtkEventControllerKey *controller, break; } - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); +#if GTK_CHECK_VERSION(4, 0, 0) + /* GTK4: GtkTextView's internal key controller does the IM filtering + * itself; gtk_text_view_im_context_filter_keypress() is gone. Let + * the event propagate so the textview handles it natively. */ + (void)event; + return FALSE; +#else const gboolean handled = gtk_text_view_im_context_filter_keypress(GTK_TEXT_VIEW(textview), (GdkEventKey *)event); gdk_event_free(event); return handled; +#endif } static gboolean _textview_focus(GtkWidget *widget, @@ -853,7 +861,7 @@ static void _menuitem_preferences(GtkMenuItem *menuitem, gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_gui_dialog_add_help(GTK_DIALOG(dialog), "metadata_preferences"); dt_gui_dialog_restore_size(GTK_DIALOG(dialog), "metadata"); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); GtkListStore *store = gtk_list_store_new(DT_METADATA_PREF_NUM_COLS, G_TYPE_INT, // key @@ -937,13 +945,19 @@ static void _menuitem_preferences(GtkMenuItem *menuitem, gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(w), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); - GtkWidget *plus = dtgtk_button_new(dtgtk_cairo_paint_plus_simple, 0, NULL); - gtk_widget_set_tooltip_text(plus, _("add metadata tags")); - g_signal_connect(G_OBJECT(plus), "clicked", G_CALLBACK(_add_tag_button_clicked), (gpointer)d); - - GtkWidget *minus = dtgtk_button_new(dtgtk_cairo_paint_minus_simple, 0, NULL); - gtk_widget_set_tooltip_text(minus, _("delete metadata tag")); - g_signal_connect(G_OBJECT(minus), "clicked", G_CALLBACK(_delete_tag_button_clicked), (gpointer)d); + GtkWidget *plus = dtgtk_button_new_full(dtgtk_cairo_paint_plus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add metadata tags"), + .clicked_cb = G_CALLBACK(_add_tag_button_clicked), + .clicked_data = (gpointer)d, + }); + + GtkWidget *minus = dtgtk_button_new_full(dtgtk_cairo_paint_minus_simple, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("delete metadata tag"), + .clicked_cb = G_CALLBACK(_delete_tag_button_clicked), + .clicked_data = (gpointer)d, + }); d->delete_button = minus; #ifdef GDK_WINDOWING_QUARTZ diff --git a/src/libs/metadata_view.c b/src/libs/metadata_view.c index 4354ab110a93..43d41beeeffd 100644 --- a/src/libs/metadata_view.c +++ b/src/libs/metadata_view.c @@ -1463,7 +1463,7 @@ void _menuitem_preferences(GtkMenuItem *menuitem, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); GtkListStore *store = gtk_list_store_new(DT_METADATA_PREF_NUM_COLS, G_TYPE_INT, G_TYPE_STRING, G_TYPE_BOOLEAN); diff --git a/src/libs/modulegroups.c b/src/libs/modulegroups.c index f69932eced75..887fe4340aeb 100644 --- a/src/libs/modulegroups.c +++ b/src/libs/modulegroups.c @@ -608,8 +608,7 @@ static void _basics_add_widget(dt_lib_module_t *self, dt_lib_modulegroups_basic_ gtk_container_add(GTK_CONTAINER(evb), header_box); gtk_widget_show_all(evb); g_object_set_data(G_OBJECT(evb), "module", item->module->so); - GtkGestureSingle *g_evb = dt_gui_connect_click(evb, _manage_direct_module_popup, NULL, self); - gtk_gesture_single_set_button(g_evb, GDK_BUTTON_SECONDARY); + dt_gui_connect_click_secondary(evb, _manage_direct_module_popup, NULL, self); gtk_widget_set_name(header_box, "basics-header-box"); dt_gui_add_class(header_box, "dt_big_btn_canvas"); gtk_box_pack_start(GTK_BOX(d->vbox_basic), evb, FALSE, FALSE, 0); @@ -623,10 +622,12 @@ static void _basics_add_widget(dt_lib_module_t *self, dt_lib_modulegroups_basic_ gtk_widget_show_all(hbox_basic); // we create the link to the full iop - GtkWidget *wbt = dtgtk_button_new(dtgtk_cairo_paint_link, 0, NULL); - gtk_widget_show(wbt); gchar *tt = g_strdup_printf(_("go to the full version of the %s module"), item->module->name()); - gtk_widget_set_tooltip_text(wbt, tt); + GtkWidget *wbt = dtgtk_button_new_full(dtgtk_cairo_paint_link, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = tt, + }); + gtk_widget_show(wbt); gtk_widget_set_name(wbt, "basics-link"); gtk_widget_set_valign(wbt, GTK_ALIGN_CENTER); g_free(tt); @@ -2061,12 +2062,14 @@ static void _manage_editor_basics_update_list(dt_lib_module_t *self) gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0); if(!d->edit_ro) { - GtkWidget *btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); + GtkWidget *btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("remove this widget"), + .clicked_cb = G_CALLBACK(_manage_editor_basics_remove), + .clicked_data = self, + }); - gtk_widget_set_tooltip_text(btn, _("remove this widget")); g_object_set_data(G_OBJECT(btn), "widget_id", item->id); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_basics_remove), self); gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0); } gtk_box_pack_start(GTK_BOX(d->edit_basics_box), hb, FALSE, TRUE, 0); @@ -2184,12 +2187,14 @@ static void _manage_editor_module_update_list(dt_lib_module_t *self, gtk_box_pack_start(GTK_BOX(hb), lb, FALSE, TRUE, 0); if(!d->edit_ro) { - GtkWidget *btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); - gtk_widget_set_tooltip_text(btn, _("remove this module")); + GtkWidget *btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("remove this module"), + .clicked_cb = G_CALLBACK(_manage_editor_module_remove), + .clicked_data = self, + }); g_object_set_data(G_OBJECT(btn), "module_name", module->op); g_object_set_data(G_OBJECT(btn), "group", gr); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_module_remove), self); gtk_box_pack_end(GTK_BOX(hb), btn, FALSE, TRUE, 0); } gtk_box_pack_start(GTK_BOX(gr->iop_box), hb, FALSE, TRUE, 0); @@ -2389,14 +2394,27 @@ static int _manage_editor_module_so_add_sort(gconstpointer a, gconstpointer b) return -res; } +/* shared modulegroups popup: the "modulegroups-popup" css name and the + * popup anchoring are the same for every tab/basic-button menu */ +static GtkWidget *_manage_popup_new(void) +{ + GtkWidget *pop = gtk_menu_new(); + gtk_widget_set_name(pop, "modulegroups-popup"); + return pop; +} + +static void _manage_popup_show(GtkWidget *pop, GtkWidget *widget) +{ + dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH); +} + static void _manage_module_add_popup(GtkWidget *widget, dt_lib_modulegroups_group_t *gr, GCallback callback, gpointer data, const gboolean toggle) { - GtkWidget *pop = gtk_menu_new(); - gtk_widget_set_name(pop, "modulegroups-popup"); + GtkWidget *pop = _manage_popup_new(); int nba = 0; // nb of already present items @@ -2480,7 +2498,7 @@ static void _manage_module_add_popup(GtkWidget *widget, gtk_menu_shell_prepend(GTK_MENU_SHELL(pop), smt); } - dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH); + _manage_popup_show(pop, widget); } static gchar *_action_label(dt_action_t *action) @@ -2656,8 +2674,7 @@ static void _manage_basics_add_popup(GtkWidget *widget, const gboolean full_menu) { int nba = 0; // nb of already present items - GtkWidget *pop = gtk_menu_new(); - gtk_widget_set_name(pop, "modulegroups-popup"); + GtkWidget *pop = _manage_popup_new(); GtkWidget *all_modules = _build_menu_from_actions(darktable.control->actions_iops.target, self, NULL, @@ -2693,7 +2710,7 @@ static void _manage_basics_add_popup(GtkWidget *widget, pop = all_modules; } - dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH); + _manage_popup_show(pop, widget); } static void _manage_editor_basics_add_popup(GtkWidget *widget, @@ -2716,8 +2733,8 @@ static void _presets_pressed_cb(GtkGestureSingle *gesture, double y, dt_lib_module_t *self) { - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(state & GDK_CONTROL_MASK) { manage_presets(self); @@ -2731,9 +2748,9 @@ static void _manage_direct_popup(GtkGestureSingle *gesture, gdouble y, dt_lib_module_t *self) { - if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_SECONDARY) - return; - + // this gesture only serves right clicks; a primary press would be a + // shortcut-activated toggle effect (see dt_gui_current_button in gtk.h) + if(dt_gui_current_button(gesture) != GDK_BUTTON_SECONDARY) return; GtkWidget *widget = dt_gui_get_widget(gesture); dt_lib_modulegroups_group_t *gr = g_object_get_data(G_OBJECT(widget), "group"); if(!g_strcmp0(gr->name, C_("modulegroup", "deprecated"))) return; @@ -2747,9 +2764,16 @@ static void _manage_direct_basic_popup(GtkGestureSingle *gesture, gdouble y, dt_lib_module_t *self) { - if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_SECONDARY) + /* a shortcut-activated primary press (toggle/on/off effect, see + * dt_gui_current_button() in gtk.h) flips the button; real clicks on + * this secondary-only gesture are always secondary and open the popup */ + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) + { + GtkWidget *widget = dt_gui_get_widget(gesture); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))); return; - + } _manage_basics_add_popup(dt_gui_get_widget(gesture), self, TRUE); } @@ -2759,15 +2783,14 @@ static void _manage_direct_module_popup(GtkGestureSingle *gesture, gdouble y, dt_lib_module_t *self) { - if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_SECONDARY) - return; - + // this gesture only serves right clicks; a primary press would be a + // shortcut-activated toggle effect (see dt_gui_current_button in gtk.h) + if(dt_gui_current_button(gesture) != GDK_BUTTON_SECONDARY) return; GtkWidget *widget = dt_gui_get_widget(gesture); dt_action_t *module = g_object_get_data(G_OBJECT(widget), "module"); int nba = 0; // nb of already present items - GtkWidget *pop = gtk_menu_new(); - gtk_widget_set_name(pop, "modulegroups-popup"); + GtkWidget *pop = _manage_popup_new(); GtkWidget *this_module = _build_menu_from_actions(module->target, self, NULL, pop, TRUE, &nba); @@ -2862,13 +2885,19 @@ static void _manage_direct_active_popup(GtkGestureSingle *gesture, gdouble y, dt_lib_module_t *self) { - if(gtk_gesture_single_get_current_button(gesture) != GDK_BUTTON_SECONDARY) + /* a shortcut-activated primary press (toggle/on/off effect, see + * dt_gui_current_button() in gtk.h) flips the button; real clicks on + * this secondary-only gesture are always secondary and open the popup */ + if(dt_gui_current_button(gesture) == GDK_BUTTON_PRIMARY) + { + GtkWidget *widget = dt_gui_get_widget(gesture); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), + !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))); return; - + } dt_lib_modulegroups_t *d = self->data; GtkWidget *widget = dt_gui_get_widget(gesture); - GtkWidget *pop = gtk_menu_new(); - gtk_widget_set_name(pop, "modulegroups-popup"); + GtkWidget *pop = _manage_popup_new(); const gboolean all_modules = d->current == DT_MODULEGROUP_NONE; const gboolean active_history = d->current == DT_MODULEGROUP_ACTIVE_PIPE && d->full_active; @@ -2888,7 +2917,7 @@ static void _manage_direct_active_popup(GtkGestureSingle *gesture, _("show only the modules that are currently enabled"), active_only, DT_MODULEGROUP_POPUP_ACTIVE, self)); - dt_gui_menu_popup(GTK_MENU(pop), widget, GDK_GRAVITY_SOUTH, GDK_GRAVITY_NORTH); + _manage_popup_show(pop, widget); } static void _dt_dev_image_changed_callback(gpointer instance, @@ -3105,32 +3134,33 @@ void gui_init(dt_lib_module_t *self) gtk_box_pack_start(GTK_BOX(d->hbox_buttons), scrollbox, TRUE, TRUE, 0); // basic group button - d->basic_btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_modulegroup_basics, 0, NULL); - { - GtkGestureSingle *g = dt_gui_connect_click(d->basic_btn, _manage_direct_basic_popup, NULL, self); - gtk_gesture_single_set_button(g, GDK_BUTTON_SECONDARY); - } - g_signal_connect(d->basic_btn, "toggled", G_CALLBACK(_lib_modulegroups_toggle), self); - gtk_widget_set_tooltip_text(d->basic_btn, _("quick access panel\n" - "right-click tab icon to add/remove widgets")); - dt_action_define(DT_ACTION(self), NULL, - N_("quick access panel"), d->basic_btn, &dt_action_def_toggle); + d->basic_btn = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_modulegroup_basics, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("quick access panel\n" + "right-click tab icon to add/remove widgets"), + .action = DT_ACTION(self), + .action_label = N_("quick access panel"), + .action_def = &dt_action_def_toggle, + .toggled_cb = G_CALLBACK(_lib_modulegroups_toggle), + .toggled_data = self, + }); + dt_gui_connect_click_secondary(d->basic_btn, _manage_direct_basic_popup, NULL, self); gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->basic_btn, TRUE, TRUE, 0); d->vbox_basic = NULL; d->basics = NULL; // active group button - d->active_btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_modulegroup_active, 0, NULL); - { - GtkGestureSingle *g = dt_gui_connect_click(d->active_btn, _manage_direct_active_popup, NULL, self); - gtk_gesture_single_set_button(g, GDK_BUTTON_SECONDARY); - } - g_signal_connect(d->active_btn, "toggled", - G_CALLBACK(_lib_modulegroups_toggle), self); - gtk_widget_set_tooltip_text(d->active_btn, _("show only active modules")); - dt_action_define(DT_ACTION(self), NULL, N_("active modules"), - d->active_btn, &dt_action_def_toggle); + d->active_btn = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_modulegroup_active, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("show only active modules"), + .action = DT_ACTION(self), + .action_label = N_("active modules"), + .action_def = &dt_action_def_toggle, + .toggled_cb = G_CALLBACK(_lib_modulegroups_toggle), + .toggled_data = self, + }); + dt_gui_connect_click_secondary(d->active_btn, _manage_direct_active_popup, NULL, self); gtk_box_pack_start(GTK_BOX(d->hbox_groups), d->active_btn, TRUE, TRUE, 0); // cycle module groups action @@ -3138,8 +3168,10 @@ void gui_init(dt_lib_module_t *self) DT_ACTION(self), NULL, N_("cycle module groups"), NULL, &_action_def_cycle_module_groups); // we load now the presets btn - self->presets_button = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL); - gtk_widget_set_tooltip_text(self->presets_button, _("presets\nctrl+click to manage")); + self->presets_button = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("presets\nctrl+click to manage"), + }); gtk_box_pack_start(GTK_BOX(d->hbox_buttons), self->presets_button, FALSE, FALSE, 0); dt_gui_connect_click(self->presets_button, _presets_pressed_cb, NULL, self); @@ -3283,16 +3315,16 @@ static void _buttons_update(dt_lib_module_t *self) for(l = d->groups; l; l = g_list_next(l)) { dt_lib_modulegroups_group_t *gr = l->data; - GtkWidget *bt = dtgtk_togglebutton_new(_buttons_get_icon_fct(gr->icon), 0, NULL); - g_object_set_data(G_OBJECT(bt), "group", gr); - { - GtkGestureSingle *g_bt = dt_gui_connect_click(bt, _manage_direct_popup, NULL, self); - gtk_gesture_single_set_button(g_bt, GDK_BUTTON_SECONDARY); - } - g_signal_connect(bt, "toggled", G_CALLBACK(_lib_modulegroups_toggle), self); char *tooltip = g_strdup_printf(_("%s\nright-click tab icon to add/remove modules"), gr->name); - gtk_widget_set_tooltip_text(bt, tooltip); + GtkWidget *bt = dtgtk_togglebutton_new_full(_buttons_get_icon_fct(gr->icon), 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = tooltip, + .toggled_cb = G_CALLBACK(_lib_modulegroups_toggle), + .toggled_data = self, + }); g_free(tooltip); + g_object_set_data(G_OBJECT(bt), "group", gr); + dt_gui_connect_click_secondary(bt, _manage_direct_popup, NULL, self); gr->button = bt; gtk_box_pack_start(GTK_BOX(d->hbox_groups), bt, TRUE, TRUE, 0); gtk_widget_show(bt); @@ -3557,12 +3589,13 @@ static GtkWidget *_manage_editor_group_init_basics_box(dt_lib_module_t *self) if(!d->edit_ro) { GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - GtkWidget *bt = dtgtk_button_new(dtgtk_cairo_paint_square_plus, - CPF_DIRECTION_LEFT, NULL); - gtk_widget_set_tooltip_text(bt, _("add widget to the quick access panel")); + GtkWidget *bt = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add widget to the quick access panel"), + .clicked_cb = G_CALLBACK(_manage_editor_basics_add_popup), + .clicked_data = self, + }); gtk_widget_set_name(bt, "modulegroups-btn"); - g_signal_connect(G_OBJECT(bt), "clicked", - G_CALLBACK(_manage_editor_basics_add_popup), self); gtk_widget_set_halign(hb4, GTK_ALIGN_CENTER); gtk_box_pack_start(GTK_BOX(hb4), bt, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0); @@ -3590,12 +3623,14 @@ static GtkWidget *_manage_editor_group_init_modules_box(dt_lib_module_t *self, gtk_widget_set_hexpand(hb3, TRUE); // icon - btn = dtgtk_button_new(_buttons_get_icon_fct(gr->icon), 0, NULL); + btn = dtgtk_button_new_full(_buttons_get_icon_fct(gr->icon), 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("group icon"), + .clicked_cb = G_CALLBACK(_manage_editor_group_icon_popup), + .clicked_data = self, + }); gtk_widget_set_name(btn, "modulegroups-group-icon"); - gtk_widget_set_tooltip_text(btn, _("group icon")); gtk_widget_set_sensitive(btn, !d->edit_ro); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_group_icon_popup), self); g_object_set_data(G_OBJECT(btn), "group", gr); gtk_box_pack_start(GTK_BOX(hb3), btn, FALSE, TRUE, 0); @@ -3613,11 +3648,13 @@ static GtkWidget *_manage_editor_group_init_modules_box(dt_lib_module_t *self, // remove button if(!d->edit_ro) { - btn = dtgtk_button_new(dtgtk_cairo_paint_remove, 0, NULL); - gtk_widget_set_tooltip_text(btn, _("remove group")); + btn = dtgtk_button_new_full(dtgtk_cairo_paint_remove, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("remove group"), + .clicked_cb = G_CALLBACK(_manage_editor_group_remove), + .clicked_data = self, + }); g_object_set_data(G_OBJECT(btn), "group", gr); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_group_remove), self); gtk_box_pack_end(GTK_BOX(hb3), btn, FALSE, TRUE, 0); } @@ -3640,34 +3677,39 @@ static GtkWidget *_manage_editor_group_init_modules_box(dt_lib_module_t *self, GtkWidget *hb4 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); // left arrow - btn = dtgtk_button_new(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_RIGHT, NULL); + btn = dtgtk_button_new_full(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_RIGHT, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("move group to the left"), + .clicked_cb = G_CALLBACK(_manage_editor_group_move_left), + .clicked_data = self, + }); gtk_widget_set_name(btn, "modulegroups-btn"); - gtk_widget_set_tooltip_text(btn, _("move group to the left")); g_object_set_data(G_OBJECT(btn), "group", gr); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_group_move_left), self); gtk_box_pack_start(GTK_BOX(hb4), btn, FALSE, FALSE, 2); // plus button GtkWidget *plusbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); - GtkWidget *bt = dtgtk_button_new(dtgtk_cairo_paint_square_plus, - CPF_DIRECTION_LEFT, NULL); - gtk_widget_set_tooltip_text(bt, _("add module to the group")); + GtkWidget *bt = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("add module to the group"), + .clicked_cb = G_CALLBACK(_manage_editor_module_add_popup), + .clicked_data = self, + }); gtk_widget_set_name(bt, "modulegroups-btn"); g_object_set_data(G_OBJECT(bt), "group", gr); - g_signal_connect(G_OBJECT(bt), "clicked", - G_CALLBACK(_manage_editor_module_add_popup), self); gtk_widget_set_halign(plusbox, GTK_ALIGN_CENTER); gtk_box_pack_start(GTK_BOX(plusbox), bt, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hb4), plusbox, TRUE, TRUE, 0); //right arrow - btn = dtgtk_button_new(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_LEFT, NULL); + btn = dtgtk_button_new_full(dtgtk_cairo_paint_line_arrow, CPF_DIRECTION_LEFT, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("move group to the right"), + .clicked_cb = G_CALLBACK(_manage_editor_group_move_right), + .clicked_data = self, + }); gtk_widget_set_name(btn, "modulegroups-btn"); - gtk_widget_set_tooltip_text(btn, _("move group to the right")); g_object_set_data(G_OBJECT(btn), "group", gr); - g_signal_connect(G_OBJECT(btn), "clicked", - G_CALLBACK(_manage_editor_group_move_right), self); gtk_box_pack_end(GTK_BOX(hb4), btn, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(vb2), hb4, FALSE, FALSE, 0); @@ -4253,9 +4295,11 @@ static void _manage_show_window(dt_lib_module_t *self) d->edit_autoapply_chkbox = gtk_check_button_new_with_label(_("auto-apply this preset")); gtk_widget_set_sensitive(d->edit_autoapply_chkbox, FALSE); // always readonly. change are done with the button... gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_chkbox, FALSE, TRUE, 0); - d->edit_autoapply_btn = dtgtk_button_new(dtgtk_cairo_paint_preferences, 0, NULL); - g_signal_connect(G_OBJECT(d->edit_autoapply_btn), "clicked", - G_CALLBACK(_preset_autoapply_edit), self); + d->edit_autoapply_btn = dtgtk_button_new_full(dtgtk_cairo_paint_preferences, 0, NULL, + &(dtgtk_button_config_t){ + .clicked_cb = G_CALLBACK(_preset_autoapply_edit), + .clicked_data = self, + }); gtk_widget_set_name(d->edit_autoapply_btn, "modulegroups-autoapply-btn"); gtk_box_pack_start(GTK_BOX(hb2), d->edit_autoapply_btn, FALSE, FALSE, 2); gtk_box_pack_start(GTK_BOX(vb), hb2, FALSE, TRUE, 0); @@ -4267,11 +4311,11 @@ static void _manage_show_window(dt_lib_module_t *self) hb = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_set_name(hb, "modulegroups-groups-title"); gtk_box_pack_start(GTK_BOX(hb), gtk_label_new(_("module groups")), FALSE, TRUE, 0); - d->preset_btn_add_group = dtgtk_button_new(dtgtk_cairo_paint_square_plus, - CPF_DIRECTION_LEFT, NULL); - g_signal_connect(G_OBJECT(d->preset_btn_add_group), "clicked", - G_CALLBACK(_manage_editor_group_add), - self); + d->preset_btn_add_group = dtgtk_button_new_full(dtgtk_cairo_paint_square_plus, CPF_DIRECTION_LEFT, NULL, + &(dtgtk_button_config_t){ + .clicked_cb = G_CALLBACK(_manage_editor_group_add), + .clicked_data = self, + }); gtk_box_pack_start(GTK_BOX(hb), d->preset_btn_add_group, FALSE, FALSE, 0); gtk_widget_set_halign(hb, GTK_ALIGN_CENTER); gtk_box_pack_start(GTK_BOX(vb_main), hb, FALSE, TRUE, 0); diff --git a/src/libs/navigation.c b/src/libs/navigation.c index aee0e986d990..982c2b718c40 100644 --- a/src/libs/navigation.c +++ b/src/libs/navigation.c @@ -591,10 +591,10 @@ static void _lib_navigation_scroll_callback(GtkEventControllerScroll *controller // FIXME: if unused don't pass dt_lib_module_t *self) { - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { - GdkDevice *device = gdk_event_get_source_device(event); + GdkDevice *device = dt_gdk_event_get_source_device(event); if(device && gdk_device_get_source(device) == GDK_SOURCE_TOUCHPAD && dt_gdk_event_get_scroll_direction(event) == GDK_SCROLL_SMOOTH) @@ -610,14 +610,16 @@ static void _lib_navigation_scroll_callback(GtkEventControllerScroll *controller dt_gdk_event_get_x(event), dt_gdk_event_get_y(event), &x, &y)) { - const gboolean zoom_in = dt_gui_scroll_zoom_delta((const GdkEventScroll *)event, + const gboolean zoom_in = dt_gui_scroll_zoom_delta(event, dx, dy) > 0.0f; dt_dev_zoom_move(&darktable.develop->full, DT_ZOOM_SCROLL, 0.0f, zoom_in ? 1 : 0, x, y, constrain); } } +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif } } diff --git a/src/libs/neural_restore.c b/src/libs/neural_restore.c index 7aea7cc149f3..315333fda488 100644 --- a/src/libs/neural_restore.c +++ b/src/libs/neural_restore.c @@ -4360,13 +4360,14 @@ void gui_init(dt_lib_module_t *self) d->patch_center[0] = 0.5f; d->patch_center[1] = 0.5f; d->picking_thumbnail = FALSE; - d->pick_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_colorpicker, - 0, NULL); - gtk_widget_set_tooltip_text(d->pick_button, - _("click to pick preview area on the image thumbnail\n" - "double-click to reset to center")); - g_signal_connect(d->pick_button, "toggled", - G_CALLBACK(_pick_toggled), self); + d->pick_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_colorpicker, + 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("click to pick preview area on the image thumbnail\n" + "double-click to reset to center"), + .toggled_cb = G_CALLBACK(_pick_toggled), + .toggled_data = self, + }); dt_gui_connect_click_all(d->pick_button, _pick_double_click_cb, NULL, self); // preview area (resizable via dt_ui_resize_wrap) @@ -4521,10 +4522,12 @@ void gui_init(dt_lib_module_t *self) g_signal_connect(d->output_dir_entry, "changed", G_CALLBACK(_output_dir_changed), self); - d->output_dir_button = dtgtk_button_new(dtgtk_cairo_paint_directory, 0, NULL); - gtk_widget_set_tooltip_text(d->output_dir_button, _("select output folder")); - g_signal_connect(d->output_dir_button, "clicked", - G_CALLBACK(_output_dir_browse), self); + d->output_dir_button = dtgtk_button_new_full(dtgtk_cairo_paint_directory, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select output folder"), + .clicked_cb = G_CALLBACK(_output_dir_browse), + .clicked_data = self, + }); dt_gui_box_add(dir_box, d->output_dir_entry); dt_gui_box_add(dir_box, d->output_dir_button); diff --git a/src/libs/print_settings.c b/src/libs/print_settings.c index 1fbc29400787..b0ceafaabae2 100644 --- a/src/libs/print_settings.c +++ b/src/libs/print_settings.c @@ -1211,27 +1211,22 @@ static void _update_style(const dt_stylemenu_data_t *menu_data) static void _apply_style_activate_callback(GtkMenuItem *menuitem, const dt_stylemenu_data_t *menu_data) { - GdkEvent *event = gtk_get_current_event(); - if(event && dt_gdk_event_get_type(event) == GDK_KEY_PRESS) - { + if(dt_gui_menuitem_activated_by_keyboard(GTK_WIDGET(menuitem))) _update_style(menu_data); - } - gdk_event_free(event); } -static gboolean _apply_style_button_callback(GtkMenuItem *menuitem, - GdkEventButton *event, - const dt_stylemenu_data_t *menu_data) +static void _apply_style_button_callback(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + const dt_stylemenu_data_t *menu_data) { - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) - { + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) _update_style(menu_data); - } else { //??? dt_shortcut_copy_lua(NULL, name); } - return FALSE; } static void _style_popupmenu_callback(GtkWidget *w, gpointer user_data) @@ -2929,10 +2924,13 @@ void gui_init(dt_lib_module_t *self) const char *current_style_name = dt_conf_get_string_const(PRINT_CONFIG_PREFIX "style"); - GtkWidget *styles_button = dtgtk_button_new(dtgtk_cairo_paint_styles, 0, NULL); + GtkWidget *styles_button = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("select style to be applied on printing"), + .clicked_cb = G_CALLBACK(_style_popupmenu_callback), + .clicked_data = (gpointer)d, + }); gtk_widget_set_halign(styles_button,GTK_ALIGN_END); - g_signal_connect(G_OBJECT(styles_button), "clicked", G_CALLBACK(_style_popupmenu_callback), (gpointer)d); - gtk_widget_set_tooltip_text(styles_button, _("select style to be applied on printing")); GtkBox *style_box = (GtkBox*)gtk_box_new(GTK_ORIENTATION_HORIZONTAL,0); gtk_widget_set_tooltip_text(GTK_WIDGET(style_box), _("temporary style to use while printing")); GtkWidget *styles_label = gtk_label_new(_("style")); diff --git a/src/libs/recentcollect.c b/src/libs/recentcollect.c index ef2d291aad63..ab9afc65302a 100644 --- a/src/libs/recentcollect.c +++ b/src/libs/recentcollect.c @@ -233,7 +233,7 @@ void _menuitem_preferences(GtkMenuItem *menuitem, dt_lib_module_t *self) _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_prefs_init_dialog_recentcollect(dialog); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); #ifdef GDK_WINDOWING_QUARTZ dt_osx_disallow_fullscreen(dialog); diff --git a/src/libs/scopes/histogram.c b/src/libs/scopes/histogram.c index 3aa32a60a212..5b529b8def43 100644 --- a/src/libs/scopes/histogram.c +++ b/src/libs/scopes/histogram.c @@ -239,13 +239,16 @@ static void _hist_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_hist_t *d = self->data; - d->scale_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); + d->scale_button = dtgtk_button_new_full(dtgtk_cairo_paint_empty, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .action = dark, + .action_label = N_("switch histogram scale"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_hist_scale_clicked), + .clicked_data = self, + }); gtk_widget_set_valign(d->scale_button, GTK_ALIGN_START); - dt_action_define(dark, NULL, N_("switch histogram scale"), - d->scale_button, &dt_action_def_button); dt_gui_box_add(self->options_box, d->scale_button); - g_signal_connect(G_OBJECT(d->scale_button), "clicked", - G_CALLBACK(_hist_scale_clicked), self); } static void _hist_gui_cleanup(dt_scopes_mode_t *const self) diff --git a/src/libs/scopes/vectorscope.c b/src/libs/scopes/vectorscope.c index 9d9b8824a5f9..8b8067571e95 100644 --- a/src/libs/scopes/vectorscope.c +++ b/src/libs/scopes/vectorscope.c @@ -1419,14 +1419,20 @@ static void _vec_add_options(dt_scopes_mode_t *const self, { dt_scopes_vec_t *const d = self->data; - d->colorspace_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); - dt_action_define(dark, NULL, N_("cycle vectorscope types"), - d->colorspace_button, &dt_action_def_button); - - d->vec_scale_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); + d->colorspace_button = dtgtk_button_new_full(dtgtk_cairo_paint_empty, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .action = dark, + .action_label = N_("cycle vectorscope types"), + .action_def = &dt_action_def_button, + }); + + d->vec_scale_button = dtgtk_button_new_full(dtgtk_cairo_paint_empty, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .action = dark, + .action_label = N_("switch vectorscope scale"), + .action_def = &dt_action_def_button, + }); gtk_widget_set_valign(d->vec_scale_button, GTK_ALIGN_START); - dt_action_define(dark, NULL, N_("switch vectorscope scale"), - d->vec_scale_button, &dt_action_def_button); d->harmony_viewport = gtk_viewport_new(NULL, NULL); gtk_widget_set_halign(d->harmony_viewport, GTK_ALIGN_END); diff --git a/src/libs/scopes/waveform.c b/src/libs/scopes/waveform.c index 7091365838c4..0fe6f4e2636b 100644 --- a/src/libs/scopes/waveform.c +++ b/src/libs/scopes/waveform.c @@ -399,13 +399,16 @@ static void _wave_add_options(dt_scopes_mode_t *const self, dt_action_t *dark) { dt_scopes_wave_t *const d = self->data; - d->orient_button = dtgtk_button_new(dtgtk_cairo_paint_empty, CPF_NONE, NULL); + d->orient_button = dtgtk_button_new_full(dtgtk_cairo_paint_empty, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .action = dark, + .action_label = N_("switch scope orientation"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_wave_orient_clicked), + .clicked_data = self, + }); gtk_widget_set_valign(d->orient_button, GTK_ALIGN_START); - dt_action_define(dark, NULL, N_("switch scope orientation"), - d->orient_button, &dt_action_def_button); dt_gui_box_add(self->options_box, d->orient_button); - g_signal_connect(G_OBJECT(d->orient_button), "clicked", - G_CALLBACK(_wave_orient_clicked), self); } static void _wave_gui_cleanup(dt_scopes_mode_t *const self) diff --git a/src/libs/snapshots.c b/src/libs/snapshots.c index 0ecfb0a17707..92cf1887ded4 100644 --- a/src/libs/snapshots.c +++ b/src/libs/snapshots.c @@ -608,12 +608,13 @@ static void _init_snapshot_entry(dt_lib_module_t *self, g_signal_connect(G_OBJECT(s->entry), "activate", G_CALLBACK(_entry_activated_callback), self); - s->restore_button = dtgtk_button_new(dtgtk_cairo_paint_snapshots_restore, CPF_NONE, NULL); + s->restore_button = dtgtk_button_new_full(dtgtk_cairo_paint_snapshots_restore, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("restore snapshot into current history"), + .clicked_cb = G_CALLBACK(_lib_snapshots_restore_callback), + .clicked_data = self, + }); gtk_widget_set_name(s->restore_button, "non-flat"); - gtk_widget_set_tooltip_text(s->restore_button, - _("restore snapshot into current history")); - g_signal_connect(G_OBJECT(s->restore_button), "clicked", - G_CALLBACK(_lib_snapshots_restore_callback), self); } static void _clear_snapshot_entry(dt_lib_snapshot_t *s) @@ -873,14 +874,16 @@ void gui_init(dt_lib_module_t *self) GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_box_pack_start(GTK_BOX(hbox), d->take_button, TRUE, TRUE, 0); - d->sidebyside_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_culling_dynamic, 0, NULL); - dt_action_define(DT_ACTION(self), NULL, N_("side-by-side"), - d->sidebyside_button, &dt_action_def_toggle); + d->sidebyside_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_culling_dynamic, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("place the snapshot side-by-side / above-below the current image instead of overlaying"), + .action = DT_ACTION(self), + .action_label = N_("side-by-side"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_sidebyside_button_clicked), + .clicked_data = self, + }); gtk_box_pack_start(GTK_BOX(hbox), d->sidebyside_button, FALSE, TRUE, 0); - g_signal_connect(G_OBJECT(d->sidebyside_button), "clicked", - G_CALLBACK(_sidebyside_button_clicked), self); - gtk_widget_set_tooltip_text(GTK_WIDGET(d->sidebyside_button), - _("place the snapshot side-by-side / above-below the current image instead of overlaying")); gtk_box_pack_start(GTK_BOX(self->widget), hbox, TRUE, TRUE, 0); diff --git a/src/libs/styles.c b/src/libs/styles.c index 2e98e95a4643..72cb0d520c6c 100644 --- a/src/libs/styles.c +++ b/src/libs/styles.c @@ -1019,7 +1019,7 @@ void _menuitem_preferences(GtkMenuItem *menuitem, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); GtkWidget *preview_size; DT_BAUHAUS_COMBOBOX_NEW_FULL(preview_size, self, NULL, N_("preview size"), diff --git a/src/libs/tagging.c b/src/libs/tagging.c index 95aba8e33de8..11b3024d79f5 100644 --- a/src/libs/tagging.c +++ b/src/libs/tagging.c @@ -1504,9 +1504,8 @@ static void _click_on_view_attached(GtkGestureSingle *gesture, if(n_press == 1 && button == GDK_BUTTON_SECONDARY) { dt_gui_claim(gesture); - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); _pop_menu_attached(view, (GdkEventButton *)event, self); - if(event) gdk_event_free(event); gtk_tree_path_free(path); return; } @@ -2657,7 +2656,7 @@ static void _click_on_view_dictionary(GtkGestureSingle *gesture, GtkWidget *view = dt_gui_get_widget(gesture); _unselect_all_in_view(d->attached_view); - GdkEvent *event = gtk_get_current_event(); + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); const GdkModifierType state = event ? dt_gdk_event_get_state(event) : 0; const guint button = gtk_gesture_single_get_current_button(gesture); const gboolean shift_pressed = dt_modifier_is(state, GDK_SHIFT_MASK); @@ -2688,7 +2687,7 @@ static void _click_on_view_dictionary(GtkGestureSingle *gesture, d->drag.path = path; if(d->drag.lastpath) gtk_tree_path_free(d->drag.lastpath); d->drag.lastpath = NULL; - if(event) gdk_event_free(event); + /* event is borrowed (gesture) */ return; } else @@ -2700,7 +2699,7 @@ static void _click_on_view_dictionary(GtkGestureSingle *gesture, dt_gui_claim(gesture); _pop_menu_dictionary(view, (GdkEventButton *)event, self); gtk_tree_path_free(path); - if(event) gdk_event_free(event); + /* event is borrowed (gesture) */ return; } else if(d->tree_flag @@ -2710,7 +2709,7 @@ static void _click_on_view_dictionary(GtkGestureSingle *gesture, dt_gui_claim(gesture); gtk_tree_view_expand_row(GTK_TREE_VIEW(view), path, TRUE); gtk_tree_path_free(path); - if(event) gdk_event_free(event); + /* event is borrowed (gesture) */ return; } else if(n_press == 2 && button == GDK_BUTTON_PRIMARY) @@ -2718,14 +2717,14 @@ static void _click_on_view_dictionary(GtkGestureSingle *gesture, dt_gui_claim(gesture); _attach_selected_tag(self, d); gtk_tree_path_free(path); - if(event) gdk_event_free(event); + /* event is borrowed (gesture) */ return; } } } gtk_tree_path_free(path); } - if(event) gdk_event_free(event); + /* event is borrowed (gesture) */ } static gboolean _dictionary_key_pressed(GtkEventControllerKey *controller, @@ -3618,15 +3617,19 @@ void gui_init(dt_lib_module_t *self) dt_gui_connect_key(w, _enter_key_pressed, self); d->entry = GTK_ENTRY(w); - button = dtgtk_button_new(dtgtk_cairo_paint_multiply_small, 0, NULL); - gtk_widget_set_tooltip_text(button, _("clear entry")); + button = dtgtk_button_new_full(dtgtk_cairo_paint_multiply_small, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("clear entry"), + .action = DT_ACTION(self), + .action_label = N_("clear entry"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_clear_entry_button_callback), + .clicked_data = (gpointer)self, + }); gtk_box_pack_end(hbox, button, FALSE, TRUE, 0); - g_signal_connect(G_OBJECT(button), "clicked", - G_CALLBACK(_clear_entry_button_callback), (gpointer)self); gtk_box_pack_start(box, GTK_WIDGET(hbox), FALSE, TRUE, 0); dt_gui_add_class(GTK_WIDGET(box), "dt_spacing_sw"); d->clear_button = button; - dt_action_define(DT_ACTION(self), NULL, N_("clear entry"), button, &dt_action_def_button); // dictionary_view tree view view = GTK_TREE_VIEW(gtk_tree_view_new()); @@ -4004,7 +4007,7 @@ void _menuitem_preferences(GtkMenuItem *menuitem, GTK_DIALOG_DESTROY_WITH_PARENT, _("_cancel"), GTK_RESPONSE_NONE, _("_save"), GTK_RESPONSE_ACCEPT, NULL); - g_signal_connect(dialog, "key-press-event", G_CALLBACK(dt_handle_dialog_enter), NULL); + dt_gui_connect_key(dialog, dt_handle_dialog_enter, NULL); gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); dt_prefs_init_dialog_tagging(dialog); diff --git a/src/libs/tools/colorlabels.c b/src/libs/tools/colorlabels.c index fd92d29e9a4a..452abfc08599 100644 --- a/src/libs/tools/colorlabels.c +++ b/src/libs/tools/colorlabels.c @@ -126,22 +126,26 @@ void gui_init(dt_lib_module_t *self) for(int k = 0; k < 6; k++) { - GtkWidget *button = - dtgtk_button_new(dtgtk_cairo_paint_label, (k | 8 | CPF_LABEL_PURPLE), NULL); - d->buttons[k] = button; - dt_gui_add_class(d->buttons[k], "dt_no_hover"); - dt_gui_add_class(d->buttons[k], "dt_dimmed"); // Only the first 5 buttons are color label togglers, the last one clears all labels char *tooltip = k < 5 ? _get_tooltip_for(k) : g_strdup(_("clear color labels of selected images")); - gtk_widget_set_tooltip_markup(button, tooltip); + GtkWidget *button = + dtgtk_button_new_full(dtgtk_cairo_paint_label, (k | 8 | CPF_LABEL_PURPLE), NULL, + &(dtgtk_button_config_t){ + .tooltip_markup = tooltip, + .action = &darktable.control->actions_thumb, + .action_label = N_("color label"), + .action_def = &dt_action_def_color_label, + }); g_free(tooltip); + d->buttons[k] = button; + dt_gui_add_class(d->buttons[k], "dt_no_hover"); + dt_gui_add_class(d->buttons[k], "dt_dimmed"); gtk_box_pack_start(GTK_BOX(self->widget), button, TRUE, TRUE, 0); dt_gui_connect_click_all(button, _lib_colorlabels_button_press_callback, NULL, self); dt_gui_connect_motion(button, NULL, _lib_colorlabels_enter_notify_callback, NULL, self); - ac = dt_action_define(&darktable.control->actions_thumb, NULL, - N_("color label"), button, &dt_action_def_color_label); + ac = dt_action_widget(button); } dt_shortcut_register(ac, 1, 0, GDK_KEY_F1, 0); @@ -283,8 +287,12 @@ static void _lib_colorlabels_button_press_callback(GtkGestureSingle *gesture, in { d->colorlabel = colorlabel; gdouble root_x, root_y; - if(dt_gui_get_current_root_coords(&root_x, &root_y)) + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(event) + { + dt_gui_get_event_coords(event, &root_x, &root_y); _lib_colorlabels_edit(self, root_x, root_y); + } } else { diff --git a/src/libs/tools/filter.c b/src/libs/tools/filter.c index 705925e9455c..b593fbb1a522 100644 --- a/src/libs/tools/filter.c +++ b/src/libs/tools/filter.c @@ -99,9 +99,12 @@ void gui_init(dt_lib_module_t *self) self->widget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); gtk_widget_set_valign(self->widget, GTK_ALIGN_CENTER); - GtkWidget *bt = dtgtk_button_new(dtgtk_cairo_paint_filtering_menu, 0, NULL); - gtk_widget_set_tooltip_text(bt, _("filter preferences")); - g_signal_connect(G_OBJECT(bt), "clicked", G_CALLBACK(_pref_show), self); + GtkWidget *bt = dtgtk_button_new_full(dtgtk_cairo_paint_filtering_menu, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("filter preferences"), + .clicked_cb = G_CALLBACK(_pref_show), + .clicked_data = self, + }); gtk_box_pack_start(GTK_BOX(self->widget), bt, FALSE, TRUE, 0); d->filter_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); diff --git a/src/libs/tools/global_toolbox.c b/src/libs/tools/global_toolbox.c index d1328fd2a50b..f7fbe49fe157 100644 --- a/src/libs/tools/global_toolbox.c +++ b/src/libs/tools/global_toolbox.c @@ -399,13 +399,18 @@ void gui_init(dt_lib_module_t *self) NULL); /* create the "show/hide overlays" button */ - d->overlays_button = dtgtk_button_new(dtgtk_cairo_paint_overlays, 0, NULL); - dt_action_define(&darktable.control->actions_global, NULL, N_("thumbnail overlays options"), d->overlays_button, &dt_action_def_button); - gtk_widget_set_tooltip_text(d->overlays_button, _("click to change the type of overlays shown on thumbnails")); + d->overlays_button = dtgtk_button_new_full(dtgtk_cairo_paint_overlays, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("click to change the type of overlays shown on thumbnails"), + .action = &darktable.control->actions_global, + .action_label = N_("thumbnail overlays options"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_overlays_show_popup), + .clicked_data = self, + }); d->over_popup = gtk_popover_new(d->overlays_button); gtk_widget_set_size_request(d->over_popup, 350, -1); g_object_set(G_OBJECT(d->over_popup), "transitions-enabled", FALSE, NULL); - g_signal_connect(G_OBJECT(d->overlays_button), "clicked", G_CALLBACK(_overlays_show_popup), self); // we register size of overlay icon to keep in sync thumbtable overlays g_signal_connect(G_OBJECT(d->overlays_button), "size-allocate", G_CALLBACK(_main_icons_register_size), NULL); @@ -479,41 +484,55 @@ void gui_init(dt_lib_module_t *self) gtk_widget_show_all(vbox); /* create the widget help button */ - d->help_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_help, 0, NULL); - dt_action_define(&darktable.control->actions_global, NULL, N_("help"), d->help_button, &dt_action_def_toggle); - gtk_widget_set_tooltip_text(d->help_button, _("enable this, then click on a control element to see its online help")); - g_signal_connect(G_OBJECT(d->help_button), "clicked", G_CALLBACK(_lib_help_button_clicked), d); + d->help_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_help, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("enable this, then click on a control element to see its online help"), + .action = &darktable.control->actions_global, + .action_label = N_("help"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_lib_help_button_clicked), + .clicked_data = d, + }); /* create the shortcuts button */ - d->keymap_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_shortcut, 0, NULL); - dt_action_define(&darktable.control->actions_global, NULL, N_("shortcuts"), d->keymap_button, &dt_action_def_toggle); - gtk_widget_set_tooltip_text(d->keymap_button, _("define keyboard shortcuts for on-screen controls\n" - "ctrl+click to switch off overwrite confirmations\n" - "\n" - "after activating:\n" - "\n" - "- hover over a control (button, slider, etc.) and press\n" - " a keystroke combination (optionally with mouse click,\n" - " move, or scroll while holding down the keys) to\n" - " define a shortcut for the control,\n" - "- type an existing combination to delete that mapping\n" - "\n" - "click on a control, module or screen area to open the\n" - "dialog for more detailed configuration\n" - "\n" - "right-click to exit mapping mode")); - g_signal_connect(G_OBJECT(d->keymap_button), "clicked", G_CALLBACK(_lib_keymap_button_clicked), d); + d->keymap_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_shortcut, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("define keyboard shortcuts for on-screen controls\n" + "ctrl+click to switch off overwrite confirmations\n" + "\n" + "after activating:\n" + "\n" + "- hover over a control (button, slider, etc.) and press\n" + " a keystroke combination (optionally with mouse click,\n" + " move, or scroll while holding down the keys) to\n" + " define a shortcut for the control,\n" + "- type an existing combination to delete that mapping\n" + "\n" + "click on a control, module or screen area to open the\n" + "dialog for more detailed configuration\n" + "\n" + "right-click to exit mapping mode"), + .action = &darktable.control->actions_global, + .action_label = N_("shortcuts"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_lib_keymap_button_clicked), + .clicked_data = d, + }); dt_gui_connect_click_all(d->keymap_button, _lib_keymap_button_pressed, _lib_keymap_button_released, (gpointer)(d)); // the rest of these is added in reverse order as they are always put at the end of the container. // that's done so that buttons added via Lua will come first. /* create the preference button */ - d->preferences_button = dtgtk_button_new(dtgtk_cairo_paint_preferences, 0, NULL); - ac = dt_action_define(&darktable.control->actions_global, NULL, N_("preferences"), d->preferences_button, &dt_action_def_button); - gtk_widget_set_tooltip_text(d->preferences_button, _("show global preferences")); - g_signal_connect(G_OBJECT(d->preferences_button), "clicked", G_CALLBACK(_lib_preferences_button_clicked), - NULL); + d->preferences_button = dtgtk_button_new_full(dtgtk_cairo_paint_preferences, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("show global preferences"), + .action = &darktable.control->actions_global, + .action_label = N_("preferences"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_lib_preferences_button_clicked), + }); + ac = dt_action_widget(d->preferences_button); // Register CMD+, for preferences (this is standard on macOS) dt_shortcut_register(ac, 0, DT_ACTION_EFFECT_ACTIVATE, GDK_KEY_comma, GDK_CONTROL_MASK); @@ -809,7 +828,7 @@ static void _lib_keymap_button_pressed(GtkGestureSingle *gesture, gint n_press, { darktable.control->confirm_mapping = !(dt_key_modifier_state() & GDK_CONTROL_MASK); - _keymap_button_start_time = gtk_get_current_event_time(); + _keymap_button_start_time = dt_gui_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)); if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_SECONDARY) { @@ -822,7 +841,7 @@ static void _lib_keymap_button_released(GtkGestureSingle *gesture, gint n_press, int delay = 0; g_object_get(gtk_settings_get_default(), "gtk-long-press-time", &delay, NULL); - if(gtk_get_current_event_time() - _keymap_button_start_time > (guint)delay) + if(dt_gui_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)) - _keymap_button_start_time > (guint)delay) { _show_shortcuts_prefs(NULL); } diff --git a/src/libs/tools/lighttable.c b/src/libs/tools/lighttable.c index 0c1a9852f68d..efdc0925d963 100644 --- a/src/libs/tools/lighttable.c +++ b/src/libs/tools/lighttable.c @@ -220,8 +220,8 @@ static void _lib_lighttable_layout_btn_release_cb(GtkGestureSingle *gesture, int = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(w)); // note : this is the state before the change dt_lighttable_layout_t new_layout = DT_LIGHTTABLE_LAYOUT_FILEMANAGER; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(!active) { @@ -468,35 +468,57 @@ void gui_init(dt_lib_module_t *self) dt_action_t *ltv = &darktable.view_manager->proxy.lighttable.view->actions; dt_action_t *ac = NULL; - d->layout_filemanager = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_grid, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("toggle filemanager layout"), d->layout_filemanager, NULL); + d->layout_filemanager = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_grid, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("click to enter filemanager layout."), + .action = ltv, + .action_label = N_("toggle filemanager layout"), + }); + ac = dt_action_widget(d->layout_filemanager); dt_action_register(ac, NULL, _lib_lighttable_key_accel_toggle_filemanager, 0, 0); dt_gui_add_help_link(d->layout_filemanager, "layout_filemanager"); - gtk_widget_set_tooltip_text(d->layout_filemanager, _("click to enter filemanager layout.")); dt_gui_connect_click(d->layout_filemanager, NULL, _lib_lighttable_layout_btn_release_cb, self); - d->layout_zoomable = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_zoom, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("toggle zoomable lighttable layout"), d->layout_zoomable, NULL); + d->layout_zoomable = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_zoom, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("click to enter zoomable lighttable layout."), + .action = ltv, + .action_label = N_("toggle zoomable lighttable layout"), + }); + ac = dt_action_widget(d->layout_zoomable); dt_action_register(ac, NULL, _lib_lighttable_key_accel_toggle_zoomable, 0, 0); dt_gui_add_help_link(d->layout_zoomable, "layout_zoomable"); - gtk_widget_set_tooltip_text(d->layout_zoomable, _("click to enter zoomable lighttable layout.")); dt_gui_connect_click(d->layout_zoomable, NULL, _lib_lighttable_layout_btn_release_cb, self); - d->layout_culling_fix = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_culling_fixed, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("toggle culling mode"), d->layout_culling_fix, &_action_def_culling); + d->layout_culling_fix = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_culling_fixed, 0, NULL, + &(dtgtk_button_config_t){ + .action = ltv, + .action_label = N_("toggle culling mode"), + .action_def = &_action_def_culling, + }); + ac = dt_action_widget(d->layout_culling_fix); dt_shortcut_register(ac, DT_ACTION_ELEMENT_DEFAULT, DT_ACTION_EFFECT_HOLD_TOGGLE, GDK_KEY_x, 0); dt_shortcut_register(ac, DT_ACTION_ELEMENT_CULLING_NO_RESTRICTION, DT_ACTION_EFFECT_HOLD_TOGGLE, GDK_KEY_x, GDK_SHIFT_MASK); dt_gui_add_help_link(d->layout_culling_fix, "layout_culling"); dt_gui_connect_click(d->layout_culling_fix, NULL, _lib_lighttable_layout_btn_release_cb, self); - d->layout_culling_dynamic = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_culling_dynamic, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("toggle culling dynamic mode"), d->layout_culling_dynamic, NULL); + d->layout_culling_dynamic = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_culling_dynamic, 0, NULL, + &(dtgtk_button_config_t){ + .action = ltv, + .action_label = N_("toggle culling dynamic mode"), + }); + ac = dt_action_widget(d->layout_culling_dynamic); dt_action_register(ac, NULL, _lib_lighttable_key_accel_toggle_culling_dynamic_mode, GDK_KEY_x, GDK_CONTROL_MASK); dt_gui_add_help_link(d->layout_culling_dynamic, "layout_culling"); dt_gui_connect_click(d->layout_culling_dynamic, NULL, _lib_lighttable_layout_btn_release_cb, self); - d->layout_preview = dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_fullpreview, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("preview"), d->layout_preview, &_action_def_preview); + d->layout_preview = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_fullpreview, 0, NULL, + &(dtgtk_button_config_t){ + .action = ltv, + .action_label = N_("preview"), + .action_def = &_action_def_preview, + }); + ac = dt_action_widget(d->layout_preview); dt_shortcut_register(ac, DT_ACTION_ELEMENT_DEFAULT, DT_ACTION_EFFECT_HOLD_TOGGLE, GDK_KEY_f, 0); dt_shortcut_register(ac, DT_ACTION_ELEMENT_PREVIEW_NO_RESTRICTION, DT_ACTION_EFFECT_HOLD_TOGGLE, GDK_KEY_f, GDK_SHIFT_MASK); dt_shortcut_register(ac, DT_ACTION_ELEMENT_DEFAULT, DT_ACTION_EFFECT_HOLD, GDK_KEY_w, 0); @@ -518,8 +540,12 @@ void gui_init(dt_lib_module_t *self) "or the total number of thumbnails shown in culling layouts.")); /* culling restricted icon */ - d->layout_culling_restricted = dtgtk_togglebutton_new(dtgtk_cairo_paint_lock, 0, NULL); - ac = dt_action_define(ltv, NULL, N_("toggle culling restricted"), d->layout_culling_restricted, NULL); + d->layout_culling_restricted = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lock, 0, NULL, + &(dtgtk_button_config_t){ + .action = ltv, + .action_label = N_("toggle culling restricted"), + }); + ac = dt_action_widget(d->layout_culling_restricted); dt_action_register(ac, NULL, _lib_lighttable_key_accel_toggle_restricted_mode, GDK_KEY_r, GDK_CONTROL_MASK); dt_gui_add_help_link(d->layout_culling_restricted, "layout_culling"); gtk_widget_set_no_show_all(d->layout_culling_restricted, TRUE); diff --git a/src/libs/tools/log_history.c b/src/libs/tools/log_history.c index e9e1435df933..f937cc245695 100644 --- a/src/libs/tools/log_history.c +++ b/src/libs/tools/log_history.c @@ -167,8 +167,10 @@ void gui_init(dt_lib_module_t *self) dt_lib_log_history_t *d = g_malloc0(sizeof(dt_lib_log_history_t)); self->data = d; - d->button = dtgtk_button_new(dtgtk_cairo_paint_messages, CPF_NONE, NULL); - gtk_widget_set_tooltip_text(d->button, _("view log history")); + d->button = dtgtk_button_new_full(dtgtk_cairo_paint_messages, CPF_NONE, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("view log history"), + }); dt_gui_add_help_link(d->button, "message_log"); // overlay container to host button and status badge diff --git a/src/libs/tools/timeline.c b/src/libs/tools/timeline.c index f3d5119a9cd6..94c1ab8f852d 100644 --- a/src/libs/tools/timeline.c +++ b/src/libs/tools/timeline.c @@ -1165,8 +1165,8 @@ static void _lib_timeline_button_release_callback(GtkGestureSingle *gesture, } strip->selecting = FALSE; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(!strip->move_edge && dt_modifier_is(state, GDK_SHIFT_MASK)) _selection_collect(strip, DT_LIB_TIMELINE_MODE_RESET); else @@ -1317,8 +1317,8 @@ static void _lib_timeline_scroll_cb(GtkEventControllerScroll *controller, { dt_lib_timeline_t *strip = self->data; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); // zoom change (with Ctrl key) if(dt_modifier_is(state, GDK_CONTROL_MASK)) diff --git a/src/views/darkroom.c b/src/views/darkroom.c index cc9258b05d37..96b029120c5b 100644 --- a/src/views/darkroom.c +++ b/src/views/darkroom.c @@ -1837,23 +1837,20 @@ static void _darkroom_ui_favorite_presets_popupmenu(GtkWidget *w, static void _darkroom_ui_apply_style_activate_callback(GtkMenuItem *menuitem, const dt_stylemenu_data_t *menu_data) { - GdkEvent *event = gtk_get_current_event(); - if(dt_gdk_event_get_type(event) == GDK_KEY_PRESS) + if(dt_gui_menuitem_activated_by_keyboard(GTK_WIDGET(menuitem))) dt_styles_apply_to_dev(menu_data->name, darktable.develop->image_storage.id); - gdk_event_free(event); } -static gboolean _darkroom_ui_apply_style_button_callback - (GtkMenuItem *menuitem, - GdkEventButton *event, - const dt_stylemenu_data_t *menu_data) +static void _darkroom_ui_apply_style_button_callback(GtkGestureSingle *gesture, + gint n_press, + gdouble x, + gdouble y, + const dt_stylemenu_data_t *menu_data) { - if(dt_gdk_event_get_button(event) == GDK_BUTTON_PRIMARY) + if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY) dt_styles_apply_to_dev(menu_data->name, darktable.develop->image_storage.id); else dt_shortcut_copy_lua(NULL, menu_data->name); - - return FALSE; } static void _darkroom_ui_apply_style_popupmenu(GtkWidget *w, @@ -2811,7 +2808,7 @@ static void _quickbutton_pressed_cb(GtkGestureSingle *gesture, } else { - _quickbutton_start_time = gtk_get_current_event_time(); + _quickbutton_start_time = dt_gui_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)); } } @@ -2826,7 +2823,7 @@ static void _quickbutton_released_cb(GtkGestureSingle *gesture, { int delay = 0; g_object_get(gtk_settings_get_default(), "gtk-long-press-time", &delay, NULL); - if(gtk_get_current_event_time() - _quickbutton_start_time > (guint)delay) + if(dt_gui_get_current_event_time(GTK_EVENT_CONTROLLER(gesture)) - _quickbutton_start_time > (guint)delay) _toolbar_show_popup(popover); } } @@ -3103,35 +3100,41 @@ void gui_init(dt_view_t *self) */ /* create favorite plugin preset popup tool */ - GtkWidget *favorite_presets = dtgtk_button_new(dtgtk_cairo_paint_presets, 0, NULL); - dt_action_define(sa, NULL, N_("quick access to presets"), - favorite_presets, &dt_action_def_button); - gtk_widget_set_tooltip_text(favorite_presets, _("quick access to presets")); - g_signal_connect(G_OBJECT(favorite_presets), "clicked", - G_CALLBACK(_darkroom_ui_favorite_presets_popupmenu), - NULL); + GtkWidget *favorite_presets = dtgtk_button_new_full(dtgtk_cairo_paint_presets, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("quick access to presets"), + .action = sa, + .action_label = N_("quick access to presets"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_darkroom_ui_favorite_presets_popupmenu), + }); dt_gui_add_help_link(favorite_presets, "favorite_presets"); dt_view_manager_view_toolbox_add(darktable.view_manager, favorite_presets, DT_VIEW_DARKROOM); /* create quick styles popup menu tool */ - GtkWidget *styles = dtgtk_button_new(dtgtk_cairo_paint_styles, 0, NULL); - dt_action_define(sa, NULL, N_("quick access to styles"), styles, &dt_action_def_button); - g_signal_connect(G_OBJECT(styles), "clicked", - G_CALLBACK(_darkroom_ui_apply_style_popupmenu), NULL); - gtk_widget_set_tooltip_text(styles, _("quick access for applying any of your styles")); + GtkWidget *styles = dtgtk_button_new_full(dtgtk_cairo_paint_styles, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("quick access for applying any of your styles"), + .action = sa, + .action_label = N_("quick access to styles"), + .action_def = &dt_action_def_button, + .clicked_cb = G_CALLBACK(_darkroom_ui_apply_style_popupmenu), + }); dt_gui_add_help_link(styles, "bottom_panel_styles"); dt_view_manager_view_toolbox_add(darktable.view_manager, styles, DT_VIEW_DARKROOM); /* ensure that we get strings from the style files shipped with darktable localized */ /* create second window display button */ - dev->second_wnd_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_display2, 0, NULL); - dt_action_define(sa, NULL, N_("second window"), - dev->second_wnd_button, &dt_action_def_toggle); - g_signal_connect(G_OBJECT(dev->second_wnd_button), "clicked", - G_CALLBACK(_second_window_quickbutton_clicked),dev); - gtk_widget_set_tooltip_text(dev->second_wnd_button, - _("display a second darkroom image window")); + dev->second_wnd_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_display2, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("display a second darkroom image window"), + .action = sa, + .action_label = N_("second window"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_second_window_quickbutton_clicked), + .clicked_data = dev, + }); dt_view_manager_view_toolbox_add(darktable.view_manager, dev->second_wnd_button, DT_VIEW_DARKROOM); @@ -3158,14 +3161,18 @@ void gui_init(dt_view_t *self) /* Enable color assessment conditions */ { - dev->color_assessment.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_bulb, 0, NULL); + dev->color_assessment.button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_bulb, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle color assessment conditions\nright-click for options"), + .action = DT_ACTION(self), + .action_label = N_("color assessment"), + .action_def = &dt_action_def_toggle, + .toggled_cb = G_CALLBACK(_full_color_assessment_callback), + .toggled_data = dev, + }); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dev->color_assessment.button), dev->full.color_assessment); - ac = dt_action_define(DT_ACTION(self), NULL, N_("color assessment"), dev->color_assessment.button, - &dt_action_def_toggle); - gtk_widget_set_tooltip_text(dev->color_assessment.button, _("toggle color assessment conditions\nright-click for options")); + ac = dt_action_widget(dev->color_assessment.button); dt_shortcut_register(ac, 0, 0, GDK_KEY_b, GDK_CONTROL_MASK); - g_signal_connect(G_OBJECT(dev->color_assessment.button), "toggled", - G_CALLBACK(_full_color_assessment_callback), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->color_assessment.button, DT_VIEW_DARKROOM); /* add pop-up window */ @@ -3204,15 +3211,17 @@ void gui_init(dt_view_t *self) /* Enable late-scaling button */ dev->late_scaling.button = - dtgtk_togglebutton_new(dtgtk_cairo_paint_lt_mode_fullpreview, 0, NULL); - ac = dt_action_define(sa, NULL, N_("high quality processing"), - dev->late_scaling.button, &dt_action_def_toggle); - gtk_widget_set_tooltip_text - (dev->late_scaling.button, - _("toggle high quality processing." - " if activated darktable processes image data as it does while exporting")); - g_signal_connect(G_OBJECT(dev->late_scaling.button), "clicked", - G_CALLBACK(_latescaling_quickbutton_clicked), dev); + dtgtk_togglebutton_new_full(dtgtk_cairo_paint_lt_mode_fullpreview, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle high quality processing." + " if activated darktable processes image data as it does while exporting"), + .action = sa, + .action_label = N_("high quality processing"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_latescaling_quickbutton_clicked), + .clicked_data = dev, + }); + ac = dt_action_widget(dev->late_scaling.button); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->late_scaling.button, DT_VIEW_DARKROOM); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(dev->late_scaling.button), @@ -3223,14 +3232,18 @@ void gui_init(dt_view_t *self) /* create rawoverexposed popup tool */ { // the button - dev->rawoverexposed.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_rawoverexposed, 0, NULL); - ac = dt_action_define(sa, N_("raw overexposed"), N_("toggle"), - dev->rawoverexposed.button, &dt_action_def_toggle); + dev->rawoverexposed.button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_rawoverexposed, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle indication of raw overexposure\nright-click for options"), + .action = sa, + .action_section = N_("raw overexposed"), + .action_label = N_("toggle"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_rawoverexposed_quickbutton_clicked), + .clicked_data = dev, + }); + ac = dt_action_widget(dev->rawoverexposed.button); dt_shortcut_register(ac, 0, 0, GDK_KEY_o, GDK_SHIFT_MASK); - gtk_widget_set_tooltip_text(dev->rawoverexposed.button, - _("toggle indication of raw overexposure\nright-click for options")); - g_signal_connect(G_OBJECT(dev->rawoverexposed.button), "clicked", - G_CALLBACK(_rawoverexposed_quickbutton_clicked), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->rawoverexposed.button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->rawoverexposed.button, "rawoverexposed"); @@ -3287,17 +3300,18 @@ void gui_init(dt_view_t *self) /* create overexposed popup tool */ { // the button - dev->overexposed.button = dtgtk_togglebutton_new(dtgtk_cairo_paint_overexposed, 0, NULL); - ac = dt_action_define(DT_ACTION(self), - N_("overexposed"), - N_("toggle"), - dev->overexposed.button, - &dt_action_def_toggle); + dev->overexposed.button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_overexposed, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle clipping indication\nright-click for options"), + .action = DT_ACTION(self), + .action_section = N_("overexposed"), + .action_label = N_("toggle"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_overexposed_quickbutton_clicked), + .clicked_data = dev, + }); + ac = dt_action_widget(dev->overexposed.button); dt_shortcut_register(ac, 0, 0, GDK_KEY_o, 0); - gtk_widget_set_tooltip_text(dev->overexposed.button, - _("toggle clipping indication\nright-click for options")); - g_signal_connect(G_OBJECT(dev->overexposed.button), "clicked", - G_CALLBACK(_overexposed_quickbutton_clicked), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->overexposed.button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->overexposed.button, "overexposed"); @@ -3373,27 +3387,33 @@ void gui_init(dt_view_t *self) /* create profile popup tool & buttons (softproof + gamut) */ { // the softproof button - dev->profile.softproof_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_softproof, 0, NULL); - ac = dt_action_define(sa, NULL, N_("softproof"), - dev->profile.softproof_button, &dt_action_def_toggle); + dev->profile.softproof_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_softproof, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle softproofing\nright-click for profile options"), + .action = sa, + .action_label = N_("softproof"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_softproof_quickbutton_clicked), + .clicked_data = dev, + }); + ac = dt_action_widget(dev->profile.softproof_button); dt_shortcut_register(ac, 0, 0, GDK_KEY_s, GDK_CONTROL_MASK); - gtk_widget_set_tooltip_text(dev->profile.softproof_button, - _("toggle softproofing\nright-click for profile options")); - g_signal_connect(G_OBJECT(dev->profile.softproof_button), "clicked", - G_CALLBACK(_softproof_quickbutton_clicked), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->profile.softproof_button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->profile.softproof_button, "softproof"); // the gamut check button - dev->profile.gamut_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_warning, 0, NULL); - ac = dt_action_define(sa, NULL, N_("gamut check"), - dev->profile.gamut_button, &dt_action_def_toggle); + dev->profile.gamut_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_warning, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle gamut checking\nright-click for profile options"), + .action = sa, + .action_label = N_("gamut check"), + .action_def = &dt_action_def_toggle, + .clicked_cb = G_CALLBACK(_gamut_quickbutton_clicked), + .clicked_data = dev, + }); + ac = dt_action_widget(dev->profile.gamut_button); dt_shortcut_register(ac, 0, 0, GDK_KEY_g, GDK_CONTROL_MASK); - gtk_widget_set_tooltip_text(dev->profile.gamut_button, - _("toggle gamut checking\nright-click for profile options")); - g_signal_connect(G_OBJECT(dev->profile.gamut_button), "clicked", - G_CALLBACK(_gamut_quickbutton_clicked), dev); dt_view_manager_module_toolbox_add(darktable.view_manager, dev->profile.gamut_button, DT_VIEW_DARKROOM); dt_gui_add_help_link(dev->profile.gamut_button, "gamut"); @@ -3560,13 +3580,16 @@ void gui_init(dt_view_t *self) { // the button darktable.view_manager->guides_toggle = - dtgtk_togglebutton_new(dtgtk_cairo_paint_grid, 0, NULL); - ac = dt_action_define(sa, N_("guide lines"), N_("toggle"), - darktable.view_manager->guides_toggle, - &dt_action_def_toggle); + dtgtk_togglebutton_new_full(dtgtk_cairo_paint_grid, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("toggle guide lines\nright-click for guides options"), + .action = sa, + .action_section = N_("guide lines"), + .action_label = N_("toggle"), + .action_def = &dt_action_def_toggle, + }); + ac = dt_action_widget(darktable.view_manager->guides_toggle); dt_shortcut_register(ac, 0, 0, GDK_KEY_g, 0); - gtk_widget_set_tooltip_text(darktable.view_manager->guides_toggle, - _("toggle guide lines\nright-click for guides options")); darktable.view_manager->guides_popover = dt_guides_popover(self, darktable.view_manager->guides_toggle); g_object_ref(darktable.view_manager->guides_popover); @@ -5066,23 +5089,25 @@ static void _second_window_scrolled_callback(GtkEventControllerScroll *controlle dt_dev_viewport_t *port = pinned_dev ? &pinned_dev->preview2 : &dev->preview2; - GdkEvent *current = gtk_get_current_event(); + GdkEvent *current = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!current) return; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); // A touchpad two-finger swipe pans the image, like the main darkroom view's // _scrolled()/gesture_pan path. dt_gui_scroll_should_pan() restricts this to // touchpad-sourced events, so the mouse wheel zooms even where GTK delivers // wheel scrolls as smooth events. - if(dt_gui_scroll_should_pan((const GdkEventScroll *)current)) + if(dt_gui_scroll_should_pan(current)) { - const GdkEventScroll *scroll = (const GdkEventScroll *)current; + const GdkEvent *scroll = current; gdouble pan_dx = 0.0, pan_dy = 0.0; if(!dt_gui_get_scroll_deltas(scroll, &pan_dx, &pan_dy)) { - gdk_event_free(current); + #if !GTK_CHECK_VERSION(4, 0, 0) + gdk_event_free(current); +#endif return; } pan_dx *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; @@ -5091,34 +5116,52 @@ static void _second_window_scrolled_callback(GtkEventControllerScroll *controlle "[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); + #if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(current); +#endif return; } int delta_x, delta_y; - if(!dt_gui_get_scroll_unit_deltas((const GdkEventScroll *)current, &delta_x, &delta_y)) + if(!dt_gui_get_scroll_unit_deltas(current, &delta_x, &delta_y)) { + #if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(current); +#endif return; } - const gboolean zoom_in = dt_gui_scroll_zoom_delta((const GdkEventScroll *)current, + const gboolean zoom_in = dt_gui_scroll_zoom_delta(current, delta_x, delta_y) > 0.0f; const gboolean constrained = dev->constrain_zoom && !dt_modifier_is(state, GDK_CONTROL_MASK); gdouble x = 0.0, y = 0.0; + #if GTK_CHECK_VERSION(4, 0, 0) + gdk_event_get_position(current, &x, &y); +#else gdk_event_get_coords(current, &x, &y); +#endif dt_print(DT_DEBUG_INPUT, "[darkroom second window] scroll zoom zoom_in=%d", zoom_in); dt_dev_zoom_move(port, DT_ZOOM_SCROLL, 0.0f, zoom_in ? 1 : 0, x, y, constrained); - gdk_event_free(current); + #if !GTK_CHECK_VERSION(4, 0, 0) + gdk_event_free(current); +#endif } -static gboolean _second_window_pinch_callback(GtkWidget *widget, - GdkEvent *event, - dt_develop_t *dev) +/* touchpad pinch in the second window via GtkGestureZoom: the old "event" + * signal handler (GTK3-only) forwarded raw GDK_TOUCHPAD_PINCH events; the + * phase field becomes the begin/scale-changed/end signals ("end" fires on + * cancel too). dx/dy are not used here (macOS delivers the pan as a separate + * scroll stream, see _second_window_scrolled_callback). */ +static gboolean _second_window_pinch_active = FALSE; + +static gboolean _second_window_pinch_phase(dt_develop_t *dev, + GtkWidget *widget, + const GdkTouchpadGesturePhase phase, + const gdouble scale, + const GdkEvent *event) { - if(dt_gdk_event_get_type(event) != GDK_TOUCHPAD_PINCH) return FALSE; if(!darktable.gui->touchpad_gestures_enabled) return FALSE; // Use pinned viewport if pinned, otherwise main dev's preview2 @@ -5127,49 +5170,102 @@ static gboolean _second_window_pinch_callback(GtkWidget *widget, dt_dev_viewport_t *port = pinned_dev ? &pinned_dev->preview2 : &dev->preview2; - const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; + int state = 0; + gdouble x_root = 0.0, y_root = 0.0; + if(event) + { + state = dt_gdk_event_get_state(event) & 0xf; + // GtkGestureZoom also recognizes touchscreen pinches, never handled before + if(dt_gdk_event_get_type(event) != GDK_TOUCHPAD_PINCH) return FALSE; +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4 has no root-coords API: surface-relative position (see gtk.c) + gdk_event_get_position(event, &x_root, &y_root); +#else + x_root = dt_gdk_event_get_root_x(event); + y_root = dt_gdk_event_get_root_y(event); +#endif + } const gboolean constrained = - dev->constrain_zoom && !dt_modifier_is(pinch->state, GDK_CONTROL_MASK); + dev->constrain_zoom && !dt_modifier_is(state, GDK_CONTROL_MASK); // Zoom the second window proportionally to the pinch scale, mirroring the // main darkroom view's gesture_pinch(). static float begin_tscale = 0.0f; - if(pinch->phase == GDK_TOUCHPAD_GESTURE_PHASE_BEGIN) + if(phase == GDK_TOUCHPAD_GESTURE_PHASE_BEGIN) { begin_tscale = dt_dev_get_zoom_scale(port, port->zoom, 1 << port->closeup, FALSE) * port->ppd; return TRUE; } - if(pinch->phase == GDK_TOUCHPAD_GESTURE_PHASE_END - || pinch->phase == GDK_TOUCHPAD_GESTURE_PHASE_CANCEL) + if(phase == GDK_TOUCHPAD_GESTURE_PHASE_END + || phase == GDK_TOUCHPAD_GESTURE_PHASE_CANCEL) { begin_tscale = 0.0f; return TRUE; } - if(pinch->phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) return FALSE; - if(begin_tscale <= 0.0f || pinch->scale <= 0.0) return FALSE; + if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) return FALSE; + if(begin_tscale <= 0.0f || scale <= 0.0) return FALSE; const float ppd = port->ppd; const float fitscale = dt_dev_get_zoom_scale(port, DT_ZOOM_FIT, 1, FALSE); const float tscalefloor = MIN(0.5f * fitscale * ppd, 1.0f); - const float tscale = CLAMP(begin_tscale * pinch->scale, tscalefloor, 16.0f); + const float tscale = CLAMP(begin_tscale * scale, tscalefloor, 16.0f); const float zoom_scale = tscale / ppd; // Convert root (screen-absolute) pinch coords to widget-local, which is the // coordinate space dt_dev_zoom_move() anchors the zoom to. int ox = 0, oy = 0; +#if GTK_CHECK_VERSION(4, 0, 0) + // GTK4: event coords are already surface-relative, i.e. widget-local here + ox = oy = 0; +#else GdkWindow *win = gtk_widget_get_window(widget); if(win) gdk_window_get_origin(win, &ox, &oy); - const float x_local = (float)pinch->x_root - ox; - const float y_local = (float)pinch->y_root - oy; +#endif + const float x_local = (float)x_root - ox; + const float y_local = (float)y_root - oy; dt_print(DT_DEBUG_INPUT, "[darkroom second window] pinch scale=%.6f tscale=%.6f zoom_scale=%.6f", - pinch->scale, tscale, zoom_scale); + scale, tscale, zoom_scale); dt_dev_zoom_move(port, DT_ZOOM_FREE, zoom_scale, 0, x_local, y_local, constrained); return TRUE; } +static void _second_window_pinch_begin(GtkGestureZoom *gesture, + gpointer user_data) +{ + dt_develop_t *dev = user_data; + // touchscreen pinches were never handled before: only touchpad pinches + // set the active flag + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + if(!event || dt_gdk_event_get_type(event) != GDK_TOUCHPAD_PINCH) return; + _second_window_pinch_active = TRUE; + _second_window_pinch_phase(dev, dt_gui_get_widget(gesture), + GDK_TOUCHPAD_GESTURE_PHASE_BEGIN, 1.0, event); +} + +static void _second_window_pinch_scale_changed(GtkGestureZoom *gesture, + gdouble scale, + gpointer user_data) +{ + if(!_second_window_pinch_active) return; + dt_develop_t *dev = user_data; + const GdkEvent *event = gtk_gesture_get_last_event(GTK_GESTURE(gesture), NULL); + _second_window_pinch_phase(dev, dt_gui_get_widget(gesture), + GDK_TOUCHPAD_GESTURE_PHASE_UPDATE, scale, event); +} + +static void _second_window_pinch_end(GtkGestureZoom *gesture, + gpointer user_data) +{ + if(!_second_window_pinch_active) return; + _second_window_pinch_active = FALSE; + dt_develop_t *dev = user_data; + _second_window_pinch_phase(dev, dt_gui_get_widget(gesture), + GDK_TOUCHPAD_GESTURE_PHASE_END, 1.0, NULL); +} + static void _second_window_button_pressed_callback(GtkGestureSingle *gesture, gint n_press, gdouble x, @@ -5201,8 +5297,8 @@ static void _second_window_button_pressed_callback(GtkGestureSingle *gesture, } if(button == GDK_BUTTON_MIDDLE) { - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); dt_dev_zoom_move(port, DT_ZOOM_1, 0.0f, -2, x, y, !dt_modifier_is(state, GDK_CONTROL_MASK)); return; } @@ -5227,8 +5323,8 @@ static void _second_window_mouse_moved_callback(GtkEventControllerMotion *contro { if(dev->gui_leaving) return; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(controller)); if(state & GDK_BUTTON1_MASK) { dt_control_t *ctl = darktable.control; @@ -5328,13 +5424,20 @@ static void _second_window_buttons_leave_notify_callback(GtkEventControllerMotio { // GDK_NOTIFY_INFERIOR means the pointer moved into a child window (still // within the second window); keep the buttons visible in that case. - GdkEvent *event = gtk_get_current_event(); - if(event && event->crossing.detail == GDK_NOTIFY_INFERIOR) + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); + if(event) { +#if GTK_CHECK_VERSION(4, 0, 0) + if(gdk_crossing_event_get_detail(event) == GDK_NOTIFY_INFERIOR) return; +#else + if(event->crossing.detail == GDK_NOTIFY_INFERIOR) + { + gdk_event_free(event); + return; + } gdk_event_free(event); - return; +#endif } - if(event) gdk_event_free(event); gtk_widget_set_opacity(button_box, 0.0); gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(gtk_widget_get_parent(button_box)), @@ -5364,12 +5467,14 @@ static void _darkroom_ui_second_window_init(GtkWidget *overlay, GtkWidget *button_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); // Create the pin button - GtkWidget *pin_button = dtgtk_togglebutton_new(dtgtk_cairo_paint_pin, 0, NULL); + GtkWidget *pin_button = dtgtk_togglebutton_new_full(dtgtk_cairo_paint_pin, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("pin current image"), + .toggled_cb = G_CALLBACK(_preview2_pin_button_clicked), + .toggled_data = dev, + }); gtk_widget_set_name(pin_button, "dt_window2_pin_button"); gtk_widget_set_size_request(pin_button, 24, 24); - gtk_widget_set_tooltip_text(pin_button, _("pin current image")); - g_signal_connect(G_OBJECT(pin_button), "toggled", - G_CALLBACK(_preview2_pin_button_clicked), dev); gtk_box_pack_start(GTK_BOX(button_box), pin_button, FALSE, FALSE, 0); // Associate the pin button with the toggle COMMAND action registered in @@ -5599,8 +5704,14 @@ static void _darkroom_display_second_window(dt_develop_t *dev) G_CALLBACK(_second_window_draw_callback), dev); dt_gui_connect_scroll(dev->preview2.widget, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES, _second_window_scrolled_callback, dev); - g_signal_connect(G_OBJECT(dev->preview2.widget), "event", - G_CALLBACK(_second_window_pinch_callback), dev); + // touchpad pinch via GtkGestureZoom (the old "event" signal handler is GTK3-only) + { + GtkGesture *zoom = gtk_gesture_zoom_new(dev->preview2.widget); + dt_gui_add_controller(dev->preview2.widget, zoom); + g_signal_connect(zoom, "begin", G_CALLBACK(_second_window_pinch_begin), dev); + g_signal_connect(zoom, "scale-changed", G_CALLBACK(_second_window_pinch_scale_changed), dev); + g_signal_connect(zoom, "end", G_CALLBACK(_second_window_pinch_end), dev); + } dt_gui_connect_click_all(dev->preview2.widget, _second_window_button_pressed_callback, _second_window_button_released_callback, dev); dt_gui_connect_motion(dev->preview2.widget, _second_window_mouse_moved_callback, NULL, _second_window_leave_callback, dev); g_signal_connect(G_OBJECT(dev->preview2.widget), "configure-event", diff --git a/src/views/lighttable.c b/src/views/lighttable.c index 3bba4308edf6..e1e5368af64a 100644 --- a/src/views/lighttable.c +++ b/src/views/lighttable.c @@ -1481,8 +1481,10 @@ void gui_init(dt_view_t *self) DT_VIEW_LIGHTTABLE | DT_VIEW_DARKROOM); // create display profile button - GtkWidget *const profile_button = dtgtk_button_new(dtgtk_cairo_paint_display, 0, NULL); - gtk_widget_set_tooltip_text(profile_button, _("set display profile")); + GtkWidget *const profile_button = dtgtk_button_new_full(dtgtk_cairo_paint_display, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("set display profile"), + }); dt_view_manager_module_toolbox_add(darktable.view_manager, profile_button, DT_VIEW_LIGHTTABLE); diff --git a/src/views/map.c b/src/views/map.c index 830d40f9d425..e1d4faf84a06 100644 --- a/src/views/map.c +++ b/src/views/map.c @@ -1879,7 +1879,7 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, dt_view_t *self) { dt_map_t *lib = self->data; - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(event) { @@ -1895,12 +1895,13 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, && lib->loc.main.id > 0) { gdouble root_x, root_y; - gdk_event_get_root_coords(event, &root_x, &root_y); + dt_gui_get_event_coords(event, &root_x, &root_y); if((abs(lib->start_drag_x - (int)ceil(root_x)) + abs(lib->start_drag_y - (int)ceil(root_y))) > DT_PIXEL_APPLY_DPI(8)) { lib->loc.drag = FALSE; osm_gps_map_image_remove(lib->map, lib->loc.main.location); +#if !GTK_CHECK_VERSION(4, 0, 0) GtkTargetList *targets = gtk_target_list_new(target_list_internal, n_targets_internal); GdkDragContext *context = @@ -1923,7 +1924,14 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, g_object_unref(location); } gtk_target_list_unref(targets); +#else + /* GTK4 TODO: map-view drag-and-drop needs a GdkContentProvider + * rewrite; gtk_drag_begin_with_coordinates() takes different + * arguments and GtkTargetList is gone. */ +#endif +#if !GTK_CHECK_VERSION(4, 0, 0) gdk_event_free(event); +#endif return; } } @@ -1932,7 +1940,7 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, && lib->selected_images) { gdouble root_x, root_y; - gdk_event_get_root_coords(event, &root_x, &root_y); + dt_gui_get_event_coords(event, &root_x, &root_y); if((abs(lib->start_drag_x - (int)ceil(root_x)) + abs(lib->start_drag_y - (int)ceil(root_y))) > DT_PIXEL_APPLY_DPI(8)) { @@ -1962,6 +1970,7 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, const int group_count = g_list_length(lib->selected_images); lib->start_drag = FALSE; +#if !GTK_CHECK_VERSION(4, 0, 0) GtkTargetList *targets = gtk_target_list_new(target_list_all, n_targets_all); GdkDragContext *context = gtk_drag_begin_with_coordinates(GTK_WIDGET(lib->map), targets, GDK_ACTION_MOVE, 1, @@ -1970,11 +1979,17 @@ static void _view_map_motion_cb(GtkEventControllerMotion *controller, group_count); gtk_target_list_unref(targets); gdk_event_free(event); +#else + /* GTK4 TODO: map-view drag-and-drop needs a GdkContentProvider + * rewrite (see above). */ +#endif return; } } +#if !GTK_CHECK_VERSION(4, 0, 0) if(event) gdk_event_free(event); +#endif dt_map_image_t *entry = _view_map_get_entry_at_pos(self, x, y); if(entry) @@ -2052,13 +2067,20 @@ static void _view_map_scroll_cb(GtkEventControllerScroll *controller, dt_map_t *lib = self->data; // get the current event for position and modifier state - GdkEvent *event = gtk_get_current_event(); + GdkEvent *event = dt_gui_get_current_event(GTK_EVENT_CONTROLLER(controller)); if(!event) return; gdouble x, y; GdkModifierType state; +#if GTK_CHECK_VERSION(4, 0, 0) + gdk_event_get_position(event, &x, &y); + state = gdk_event_get_modifier_state(event); +#else gdk_event_get_coords(event, &x, &y); gdk_event_get_state(event, &state); + // the GTK3 current event is an owned copy; free it once x/y/state are + // extracted -- the rest of the callback only uses dx/dy gdk_event_free(event); +#endif // determine scroll direction from dy const gboolean scroll_down = dy > 0; @@ -2137,8 +2159,8 @@ static void _view_map_click_pressed_cb(GtkGestureSingle *gesture, dt_view_t *self) { dt_map_t *lib = self->data; - GdkModifierType state; - gtk_get_current_event_state(&state); + const GdkModifierType state = + dt_gui_get_current_event_state(GTK_EVENT_CONTROLLER(gesture)); if(lib->selected_images) { diff --git a/src/views/view.c b/src/views/view.c index c443b0326b2d..e271e8f028a4 100644 --- a/src/views/view.c +++ b/src/views/view.c @@ -1654,12 +1654,12 @@ void dt_view_accels_show(dt_view_manager_t *vm) gtk_box_pack_start(GTK_BOX(hb), vm->accels_window.flow_box, TRUE, TRUE, 0); GtkWidget *vb = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - vm->accels_window.sticky_btn = dtgtk_button_new(dtgtk_cairo_paint_multiinstance, 0, NULL); - gtk_widget_set_tooltip_text(vm->accels_window.sticky_btn, - _("switch to a classic window which will stay open after key release")); - g_signal_connect(G_OBJECT(vm->accels_window.sticky_btn), "clicked", - G_CALLBACK(_accels_window_sticky), - vm); + vm->accels_window.sticky_btn = dtgtk_button_new_full(dtgtk_cairo_paint_multiinstance, 0, NULL, + &(dtgtk_button_config_t){ + .tooltip = _("switch to a classic window which will stay open after key release"), + .clicked_cb = G_CALLBACK(_accels_window_sticky), + .clicked_data = vm, + }); dt_gui_add_class(vm->accels_window.sticky_btn, "dt_accels_stick"); gtk_box_pack_start(GTK_BOX(vb), vm->accels_window.sticky_btn, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hb), vb, FALSE, FALSE, 0);