diff --git a/src/gui/gtk.c b/src/gui/gtk.c index e498c0c9488a..6f927af8fab0 100644 --- a/src/gui/gtk.c +++ b/src/gui/gtk.c @@ -738,6 +738,11 @@ static gboolean _input_event(GtkWidget *widget, case GDK_TOUCHPAD_PINCH: case GDK_TOUCHPAD_SWIPE: _touchpad = gdk_event_get_source_device(event); + dt_print(DT_DEBUG_INPUT, + "[touchpad] gesture-type event: type=%d device='%s' source-type=%d", + event->type, + _touchpad ? gdk_device_get_name(_touchpad) : "", + _touchpad ? (int)gdk_device_get_source(_touchpad) : -1); break; default: break; @@ -746,12 +751,18 @@ static gboolean _input_event(GtkWidget *widget, if(event->type == GDK_TOUCHPAD_PINCH) { const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch; + dt_print(DT_DEBUG_INPUT, + "[touchpad] 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); if(dt_view_manager_gesture_pinch(darktable.view_manager, pinch->x, pinch->y, - pinch->phase, pinch->scale, pinch->state & 0xf)) + pinch->dx, pinch->dy, pinch->phase, + pinch->scale, pinch->state & 0xf)) { gtk_widget_queue_draw(widget); return TRUE; } + dt_print(DT_DEBUG_INPUT, "[touchpad] pinch not handled by current view"); } return FALSE; @@ -764,13 +775,46 @@ static gboolean _scrolled(GtkWidget *widget, (void)user_data; GdkDevice *device = gdk_event_get_source_device((GdkEvent *)event); - if(((device && gdk_device_get_source(device) == GDK_SOURCE_TOUCHPAD) - || device == _touchpad) - && event->direction == GDK_SCROLL_SMOOTH && !event->is_stop) + dt_print(DT_DEBUG_INPUT, + "[scroll] direction=%d smooth=%s stop=%s ctrl=%s" + " x=%.1f y=%.1f dx=%.3f dy=%.3f state=0x%x" + " device='%s' source-type=%d", + event->direction, + event->direction == GDK_SCROLL_SMOOTH ? "yes" : "no", + event->is_stop ? "yes" : "no", + dt_modifier_is(event->state, GDK_CONTROL_MASK) ? "yes" : "no", + event->x, event->y, event->delta_x, event->delta_y, event->state, + device ? gdk_device_get_name(device) : "", + device ? (int)gdk_device_get_source(device) : -1); + + const gboolean ctrl_held = dt_modifier_is(event->state, GDK_CONTROL_MASK); + const gboolean is_touchpad_source = device && gdk_device_get_source(device) == GDK_SOURCE_TOUCHPAD; + const gboolean is_known_gesture_device = (device == _touchpad); + const gboolean is_smooth = event->direction == GDK_SCROLL_SMOOTH && !event->is_stop; + +#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 gesture_pan so + // that two-finger panning works in views like darkroom (both standalone and + // interleaved with a pinch-zoom gesture whose translational component macOS + // delivers as a separate scroll stream). + const gboolean route_as_pan = !ctrl_held && is_smooth; +#else + const gboolean route_as_pan = !ctrl_held + && (is_touchpad_source || is_known_gesture_device) + && is_smooth; +#endif + if(route_as_pan) { gdouble delta_x = 0.0, delta_y = 0.0; if(!dt_gui_get_scroll_deltas(event, &delta_x, &delta_y)) + { + dt_print(DT_DEBUG_INPUT, + "[touchpad] smooth scroll skipped (pointer-emulated) device='%s' source-type=%d", + device ? gdk_device_get_name(device) : "", + device ? (int)gdk_device_get_source(device) : -1); return TRUE; + } delta_x *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; delta_y *= DT_UI_SCROLL_SMOOTH_DELTA_SCALE; @@ -778,10 +822,24 @@ static gboolean _scrolled(GtkWidget *widget, && dt_view_manager_gesture_pan(darktable.view_manager, event->x, event->y, delta_x, delta_y, event->state & 0xf)) { + dt_print(DT_DEBUG_INPUT, + "[touchpad] pan x=%.1f y=%.1f dx=%.3f dy=%.3f state=0x%x device='%s'", + event->x, event->y, delta_x, delta_y, event->state, + device ? gdk_device_get_name(device) : ""); gtk_widget_queue_draw(widget); return TRUE; } } + else if(is_smooth) + { + dt_print(DT_DEBUG_INPUT, + "[touchpad] smooth scroll not routed as pan:" + " ctrl=%d touchpad_source=%d known_gesture_dev=%d route_as_pan=%d" + " device='%s' source-type=%d", + ctrl_held, is_touchpad_source, is_known_gesture_device, route_as_pan, + device ? gdk_device_get_name(device) : "", + device ? (int)gdk_device_get_source(device) : -1); + } int delta_y; if(dt_gui_get_scroll_unit_delta(event, &delta_y)) diff --git a/src/views/darkroom.c b/src/views/darkroom.c index 98bfb8fc9be6..2a2ac2d11472 100644 --- a/src/views/darkroom.c +++ b/src/views/darkroom.c @@ -4168,17 +4168,29 @@ gboolean gesture_pan(dt_view_t *self, // Mask editing (brush etc.) uses scroll for tool parameters. if(dev->form_visible && !darktable.develop->darkroom_skip_mouse_events) + { + dt_print(DT_DEBUG_INPUT, + "[darkroom pan] ignored: mask form active"); return FALSE; + } // Let active modules consume scroll for their own interactions (e.g. brush size). if(dev->gui_module && dev->gui_module->scrolled && !darktable.develop->darkroom_skip_mouse_events && !dt_iop_color_picker_is_visible(dev) && dt_dev_modulegroups_test_activated(darktable.develop)) + { + dt_print(DT_DEBUG_INPUT, + "[darkroom pan] ignored: active module '%s' consumes scroll", + dev->gui_module->name()); return FALSE; + } if(dx == 0.0 && dy == 0.0) return FALSE; + dt_print(DT_DEBUG_INPUT, + "[darkroom pan] x=%.1f y=%.1f dx=%.3f dy=%.3f state=0x%x", + x, y, dx, dy, state); dt_dev_zoom_move(&dev->full, DT_ZOOM_MOVE, 1.0f, 0, dx, dy, TRUE); return TRUE; } @@ -4186,44 +4198,85 @@ gboolean gesture_pan(dt_view_t *self, gboolean gesture_pinch(dt_view_t *self, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state) { dt_develop_t *dev = self->data; if(!dev) return FALSE; - const gboolean constrained = !dt_modifier_is(state, GDK_CONTROL_MASK); - const double pinch_step_ratio = 1.1; - - static double pinch_last_scale = 0.0; + (void)state; + static float pinch_begin_tscale = 0.0f; if(phase == GDK_TOUCHPAD_GESTURE_PHASE_BEGIN) { - pinch_last_scale = scale > 0.0 ? scale : 1.0; + pinch_begin_tscale = + dt_dev_get_zoom_scale(&dev->full, dev->full.zoom, 1 << dev->full.closeup, FALSE) + * dev->full.ppd; + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] begin x=%.1f y=%.1f scale=%.6f state=0x%x" + " -> begin_tscale=%.6f ppd=%.2f", + x, y, scale, state, pinch_begin_tscale, dev->full.ppd); return TRUE; } else if(phase == GDK_TOUCHPAD_GESTURE_PHASE_END || phase == GDK_TOUCHPAD_GESTURE_PHASE_CANCEL) { - pinch_last_scale = 0.0; + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] %s x=%.1f y=%.1f scale=%.6f state=0x%x", + phase == GDK_TOUCHPAD_GESTURE_PHASE_END ? "end" : "cancel", + x, y, scale, state); + pinch_begin_tscale = 0.0f; return TRUE; } - if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) return FALSE; - if(pinch_last_scale <= 0.0 || scale <= 0.0) return FALSE; - - const double ratio = scale / pinch_last_scale; - int zoom_step = -1; - if(ratio > pinch_step_ratio) - zoom_step = 1; - else if(ratio < 1.0 / pinch_step_ratio) - zoom_step = 0; - - if(zoom_step >= 0) + if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) { - dt_dev_zoom_move(&dev->full, DT_ZOOM_SCROLL, 0.0f, zoom_step, x, y, constrained); - pinch_last_scale = scale; + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] unknown phase=%d ignored", phase); + return FALSE; } + if(pinch_begin_tscale <= 0.0f || scale <= 0.0) + { + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] update skipped: begin_tscale=%.6f scale=%.6f", + pinch_begin_tscale, scale); + return FALSE; + } + + // On macOS (GDK Quartz), NSEventTypeMagnify never populates dx/dy and the + // gesture focal-point x/y is set once at phase=BEGIN and does not update + // during the gesture — so both approaches to infer translation are zero. + // Pan on macOS therefore arrives as a separate smooth-scroll stream which is + // routed to gesture_pan by _scrolled() in gtk.c. + // On other platforms (Wayland/X11), dx/dy carry the actual translational delta. + const double eff_dx = dx; + const double eff_dy = dy; + + if(eff_dx != 0.0 || eff_dy != 0.0) + { + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] pan component eff_dx=%.3f eff_dy=%.3f (combined with scale)", + eff_dx, eff_dy); + dt_dev_zoom_move(&dev->full, DT_ZOOM_MOVE, 1.0f, 0, eff_dx, eff_dy, TRUE); + } + + const float ppd = dev->full.ppd; + const float fitscale = dt_dev_get_zoom_scale(&dev->full, DT_ZOOM_FIT, 1.0f, FALSE); + const float tscalefloor = MIN(0.5f * fitscale * ppd, 1.0f); + const float tscaletop = 16.0f; + const float tscale = CLAMP(pinch_begin_tscale * scale, tscalefloor, tscaletop); + + // Keep pinch fully continuous for a smartphone-like feeling, including at high zoom. + const float zoom_scale = tscale / ppd; + dt_print(DT_DEBUG_INPUT, + "[darkroom pinch] update x=%.1f y=%.1f raw_dx=%.3f raw_dy=%.3f" + " eff_dx=%.3f eff_dy=%.3f scale=%.6f state=0x%x" + " -> tscale=%.6f (floor=%.6f top=%.1f) zoom_scale=%.6f", + x, y, dx, dy, eff_dx, eff_dy, scale, state, + tscale, tscalefloor, tscaletop, zoom_scale); + dt_dev_zoom_move(&dev->full, DT_ZOOM_FREE, zoom_scale, 0, x, y, TRUE); return TRUE; } diff --git a/src/views/view.c b/src/views/view.c index 5be26a02990b..e658236ab2f0 100644 --- a/src/views/view.c +++ b/src/views/view.c @@ -715,6 +715,8 @@ gboolean dt_view_manager_gesture_pan(dt_view_manager_t *vm, gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state) @@ -725,7 +727,7 @@ gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, } else if(vm->current_view->gesture_pinch) { - return vm->current_view->gesture_pinch(vm->current_view, x, y, phase, scale, state); + return vm->current_view->gesture_pinch(vm->current_view, x, y, dx, dy, phase, scale, state); } else { diff --git a/src/views/view.h b/src/views/view.h index dfa572f6e46a..6eea0fea404f 100644 --- a/src/views/view.h +++ b/src/views/view.h @@ -462,6 +462,8 @@ gboolean dt_view_manager_gesture_pan(dt_view_manager_t *vm, gboolean dt_view_manager_gesture_pinch(dt_view_manager_t *vm, const double x, const double y, + const double dx, + const double dy, const int phase, const double scale, const int state); diff --git a/src/views/view_api.h b/src/views/view_api.h index 565bd858e34c..70e4ca324cad 100644 --- a/src/views/view_api.h +++ b/src/views/view_api.h @@ -59,7 +59,8 @@ OPTIONAL(void, configure, struct dt_view_t *self, int width, int height); OPTIONAL(void, scrolled, struct dt_view_t *self, double x, double y, int up, int state); // mouse scrolled in view OPTIONAL(void, scrollbar_changed, struct dt_view_t *self, double x, double y); // scrollbars changed in view OPTIONAL(gboolean, gesture_pan, struct dt_view_t *self, double x, double y, double dx, double dy, int state); -OPTIONAL(gboolean, gesture_pinch, struct dt_view_t *self, double x, double y, int phase, double scale, int state); +OPTIONAL(gboolean, gesture_pinch, struct dt_view_t *self, double x, double y, double dx, double dy, + int phase, double scale, int state); // list of mouse actions OPTIONAL(GSList *, mouse_actions, const struct dt_view_t *self); @@ -77,4 +78,3 @@ G_END_DECLS // 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 -