diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in
index d4bf3bb7cc87..12952516b6df 100644
--- a/data/darktableconfig.xml.in
+++ b/data/darktableconfig.xml.in
@@ -2398,6 +2398,13 @@
enabled: the mouse wheel scrolls the modules panel and with Ctrl+Alt adjusts a control's value\n\ndisabled: the mouse wheel adjusts the control under the pointer and with Ctrl+Alt scrolls the panel.]]>
+
+ darkroom/ui/touchpad_gestures
+ bool
+ true
+ enable touchpad gestures in darkroom
+ enabled: touchpad pinch gestures zoom the image and two-finger touchpad scrolling pans it; Ctrl+scroll still uses the legacy zoom behavior. disabled: touchpad gestures are ignored and darkroom falls back to the legacy scroll behavior, including Ctrl+scroll for zooming in and out.]]>
+
plugins/darkroom/ui/border_size
int
diff --git a/src/gui/gtk.c b/src/gui/gtk.c
index e498c0c9488a..5c1c7a9dfd35 100644
--- a/src/gui/gtk.c
+++ b/src/gui/gtk.c
@@ -726,6 +726,21 @@ static gboolean _draw(GtkWidget *da,
}
static GdkDevice *_touchpad = NULL;
+static gboolean _touchpad_gestures_enabled(void)
+{
+ // If conf_gen.h was built before darktableconfig.xml.in gained this key
+ // (incremental build without cmake reconfigure), dt_confgen_value_exists
+ // returns FALSE and dt_conf_get_bool gets an empty string โ FALSE.
+ // Default to enabled in that case so a stale build doesn't silently break gestures.
+ if(!dt_confgen_value_exists("darkroom/ui/touchpad_gestures", DT_DEFAULT))
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] 'darkroom/ui/touchpad_gestures' missing from confgen"
+ " (stale conf_gen.h โ run cmake reconfigure), defaulting to enabled");
+ return TRUE;
+ }
+ return dt_conf_get_bool("darkroom/ui/touchpad_gestures");
+}
static gboolean _input_event(GtkWidget *widget,
GdkEvent *event,
@@ -738,20 +753,45 @@ static gboolean _input_event(GtkWidget *widget,
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",
+ event->type,
+ gdk_device_get_name(_touchpad),
+ gdk_device_get_source(_touchpad));
+ }
+ else
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] gesture event type=%d without source device",
+ event->type);
+ }
break;
default:
break;
}
- if(event->type == GDK_TOUCHPAD_PINCH)
+ if(event->type == GDK_TOUCHPAD_PINCH && _touchpad_gestures_enabled())
{
const GdkEventTouchpadPinch *pinch = &event->touchpad_pinch;
+ 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, pinch->y,
pinch->phase, pinch->scale, pinch->state & 0xf))
{
gtk_widget_queue_draw(widget);
return TRUE;
}
+
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] pinch ignored by current view");
+ }
+ else if(event->type == GDK_TOUCHPAD_PINCH)
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] pinch received but disabled by preference darkroom/ui/touchpad_gestures");
}
return FALSE;
@@ -763,14 +803,25 @@ 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)
+ const gboolean touchpad_enabled = _touchpad_gestures_enabled();
+ const gboolean ctrl_pressed = dt_modifier_is(event->state, GDK_CONTROL_MASK);
+ const gboolean is_touchpad_source = device && gdk_device_get_source(device) == GDK_SOURCE_TOUCHPAD;
+ const gboolean matches_last_gesture_device = (device == _touchpad);
+
+ if(touchpad_enabled
+ && !ctrl_pressed
+ && (is_touchpad_source || matches_last_gesture_device)
&& event->direction == GDK_SCROLL_SMOOTH && !event->is_stop)
{
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 ignored (likely pointer emulated), source='%s' source_type=%d",
+ device ? gdk_device_get_name(device) : "",
+ device ? 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,14 +829,41 @@ 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=%.2f y=%.2f dx=%.3f dy=%.3f source='%s'",
+ event->x, event->y, delta_x, delta_y,
+ device ? gdk_device_get_name(device) : "");
gtk_widget_queue_draw(widget);
return TRUE;
}
+ else if(delta_x != 0.0 || delta_y != 0.0)
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] pan not handled by current view (no gesture_pan handler?)"
+ " dx=%.3f dy=%.3f",
+ delta_x, delta_y);
+ }
+ }
+ else if(event->direction == GDK_SCROLL_SMOOTH && !event->is_stop)
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[touchpad] smooth scroll not treated as pan: enabled=%d ctrl=%d touchpad_source=%d matches_last_gesture=%d source='%s' source_type=%d",
+ touchpad_enabled,
+ ctrl_pressed,
+ is_touchpad_source,
+ matches_last_gesture_device,
+ device ? gdk_device_get_name(device) : "",
+ device ? gdk_device_get_source(device) : -1);
}
int delta_y;
if(dt_gui_get_scroll_unit_delta(event, &delta_y))
{
+ dt_print(DT_DEBUG_INPUT,
+ "[scroll] discrete fallback x=%.2f y=%.2f up=%d state=0x%x source='%s' source_type=%d",
+ event->x, event->y, delta_y < 0, event->state,
+ device ? gdk_device_get_name(device) : "",
+ device ? gdk_device_get_source(device) : -1);
dt_view_manager_scrolled(darktable.view_manager, event->x, event->y,
delta_y < 0,
event->state & 0xf);
diff --git a/src/views/darkroom.c b/src/views/darkroom.c
index 98bfb8fc9be6..205852be4293 100644
--- a/src/views/darkroom.c
+++ b/src/views/darkroom.c
@@ -4192,7 +4192,7 @@ gboolean gesture_pinch(dt_view_t *self,
{
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;
@@ -4200,17 +4200,35 @@ gboolean gesture_pinch(dt_view_t *self,
if(phase == GDK_TOUCHPAD_GESTURE_PHASE_BEGIN)
{
pinch_last_scale = scale > 0.0 ? scale : 1.0;
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] begin x=%.2f y=%.2f scale=%.6f state=0x%x last=%.6f",
+ x, y, scale, state, pinch_last_scale);
return TRUE;
}
else if(phase == GDK_TOUCHPAD_GESTURE_PHASE_END
|| phase == GDK_TOUCHPAD_GESTURE_PHASE_CANCEL)
{
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] end/cancel phase=%d x=%.2f y=%.2f scale=%.6f state=0x%x",
+ phase, x, y, scale, state);
pinch_last_scale = 0.0;
return TRUE;
}
- if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE) return FALSE;
- if(pinch_last_scale <= 0.0 || scale <= 0.0) return FALSE;
+ if(phase != GDK_TOUCHPAD_GESTURE_PHASE_UPDATE)
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] ignored phase=%d x=%.2f y=%.2f scale=%.6f state=0x%x",
+ phase, x, y, scale, state);
+ return FALSE;
+ }
+ if(pinch_last_scale <= 0.0 || scale <= 0.0)
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] invalid scale update last=%.6f scale=%.6f",
+ pinch_last_scale, scale);
+ return FALSE;
+ }
const double ratio = scale / pinch_last_scale;
int zoom_step = -1;
@@ -4221,9 +4239,18 @@ gboolean gesture_pinch(dt_view_t *self,
if(zoom_step >= 0)
{
- dt_dev_zoom_move(&dev->full, DT_ZOOM_SCROLL, 0.0f, zoom_step, x, y, constrained);
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] zoom step=%d x=%.2f y=%.2f ratio=%.6f scale=%.6f last=%.6f",
+ zoom_step, x, y, ratio, scale, pinch_last_scale);
+ dt_dev_zoom_move(&dev->full, DT_ZOOM_SCROLL, 0.0f, zoom_step, x, y, FALSE);
pinch_last_scale = scale;
}
+ else
+ {
+ dt_print(DT_DEBUG_INPUT,
+ "[darkroom pinch] below threshold ratio=%.6f scale=%.6f last=%.6f",
+ ratio, scale, pinch_last_scale);
+ }
return TRUE;
}