From e8cf57f1579305cc77226e6c3dee6b5594a635bf Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Wed, 16 Mar 2022 23:59:20 +0200 Subject: [PATCH 1/2] app: Implement canvas support for touchpad gesture rotation by pinch --- app/display/gimpdisplayshell-tool-events.c | 64 ++++++++++++++++++++++ app/display/gimpdisplayshell-tool-events.h | 7 +++ app/display/gimpdisplayshell.c | 12 ++++ app/display/gimpdisplayshell.h | 5 ++ 4 files changed, 88 insertions(+) diff --git a/app/display/gimpdisplayshell-tool-events.c b/app/display/gimpdisplayshell-tool-events.c index e28e7199f75..16df65ff586 100644 --- a/app/display/gimpdisplayshell-tool-events.c +++ b/app/display/gimpdisplayshell-tool-events.c @@ -21,6 +21,7 @@ #include #include +#include "libgimpmath/gimpmath.h" #include "libgimpwidgets/gimpwidgets.h" #include "display-types.h" @@ -100,6 +101,10 @@ static void gimp_display_shell_handle_scrolling (GimpDisplayShell gint x, gint y); +static void gimp_display_shell_rotate_gesture_maybe_get_state (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + guint *maybe_out_state); + static void gimp_display_shell_space_pressed (GimpDisplayShell *shell, const GdkEvent *event); static void gimp_display_shell_released (GimpDisplayShell *shell, @@ -1273,6 +1278,38 @@ gimp_display_shell_zoom_gesture_update (GtkGestureZoom *gesture, GIMP_ZOOM_FOCUS_POINTER); } +void +gimp_display_shell_rotate_gesture_begin (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell) +{ + + shell->initial_gesture_rotate_angle = shell->rotate_angle; + shell->last_gesture_rotate_state = 0; + gimp_display_shell_rotate_gesture_maybe_get_state (gesture, sequence, + &shell->last_gesture_rotate_state); +} + +void +gimp_display_shell_rotate_gesture_update (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell) +{ + gdouble angle; + gboolean constrain; + + gimp_display_shell_rotate_gesture_maybe_get_state (gesture, sequence, + &shell->last_gesture_rotate_state); + + angle = shell->initial_gesture_rotate_angle + + 180.0 * gtk_gesture_rotate_get_angle_delta (gesture) / G_PI; + + constrain = (shell->last_gesture_rotate_state & GDK_CONTROL_MASK) ? TRUE : FALSE; + + gimp_display_shell_rotate_to (shell, + constrain ? RINT (angle / 15.0) * 15.0 : angle); +} + void gimp_display_shell_buffer_stroke (GimpMotionBuffer *buffer, const GimpCoords *coords, @@ -1671,6 +1708,33 @@ gimp_display_shell_handle_scrolling (GimpDisplayShell *shell, shell->scroll_last_y = y; } +static void +gimp_display_shell_rotate_gesture_maybe_get_state (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + guint *maybe_out_state) +{ + /* The only way to get any access to any data about events handled by the + * GtkGestureRotate is through its last_event. The set of events handled by + * GtkGestureRotate is not fully defined so we can't guarantee that last_event + * will be of event type that has a state field (though touch and gesture + * events do have that). + * + * Usually this would not be a problem, but when handling a gesture we don't + * want to repeatedly switch between a valid state and its default value if + * last_event happens to not have it. Thus we store the last valid state + * and only update it if we get a valid state from last_event. + */ + guint state = 0; + const GdkEvent *last_event; + + last_event = gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence); + if (last_event == NULL) + return; + + if (gdk_event_get_state (last_event, &state)) + *maybe_out_state = state; +} + static void gimp_display_shell_space_pressed (GimpDisplayShell *shell, const GdkEvent *event) diff --git a/app/display/gimpdisplayshell-tool-events.h b/app/display/gimpdisplayshell-tool-events.h index 6539640c11b..a491999d04e 100644 --- a/app/display/gimpdisplayshell-tool-events.h +++ b/app/display/gimpdisplayshell-tool-events.h @@ -37,6 +37,13 @@ void gimp_display_shell_zoom_gesture_update (GtkGestureZoom *gesture GdkEventSequence *sequence, GimpDisplayShell *shell); +void gimp_display_shell_rotate_gesture_begin (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell); +void gimp_display_shell_rotate_gesture_update (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell); + void gimp_display_shell_buffer_stroke (GimpMotionBuffer *buffer, const GimpCoords *coords, guint32 time, diff --git a/app/display/gimpdisplayshell.c b/app/display/gimpdisplayshell.c index a87b9402907..8ca3d1efa4c 100644 --- a/app/display/gimpdisplayshell.c +++ b/app/display/gimpdisplayshell.c @@ -516,6 +516,10 @@ gimp_display_shell_constructed (GObject *object) gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->zoom_gesture), GTK_PHASE_CAPTURE); + shell->rotate_gesture = gtk_gesture_rotate_new (GTK_WIDGET (shell->canvas)); + gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->rotate_gesture), + GTK_PHASE_CAPTURE); + /* the horizontal ruler */ shell->hrule = gimp_ruler_new (GTK_ORIENTATION_HORIZONTAL); gtk_widget_set_events (GTK_WIDGET (shell->hrule), @@ -608,6 +612,13 @@ gimp_display_shell_constructed (GObject *object) g_signal_connect (shell->zoom_gesture, "update", G_CALLBACK (gimp_display_shell_zoom_gesture_update), shell); + g_signal_connect (shell->rotate_gesture, "begin", + G_CALLBACK (gimp_display_shell_rotate_gesture_begin), + shell); + g_signal_connect (shell->rotate_gesture, "update", + G_CALLBACK (gimp_display_shell_rotate_gesture_update), + shell); + /* the zoom button */ shell->zoom_button = g_object_new (GTK_TYPE_CHECK_BUTTON, @@ -759,6 +770,7 @@ gimp_display_shell_dispose (GObject *object) } g_clear_object (&shell->zoom_gesture); + g_clear_object (&shell->rotate_gesture); g_clear_pointer (&shell->render_cache, cairo_surface_destroy); g_clear_pointer (&shell->render_cache_valid, cairo_region_destroy); diff --git a/app/display/gimpdisplayshell.h b/app/display/gimpdisplayshell.h index f9bf6360f03..7a4a0a41f3d 100644 --- a/app/display/gimpdisplayshell.h +++ b/app/display/gimpdisplayshell.h @@ -101,6 +101,7 @@ struct _GimpDisplayShell GtkWidget *canvas; /* GimpCanvas widget */ GtkGesture *zoom_gesture; /* Zoom gesture handler for the canvas*/ + GtkGesture *rotate_gesture; /* Rotation gesture handler */ GtkAdjustment *hsbdata; /* adjustments */ GtkAdjustment *vsbdata; @@ -203,6 +204,10 @@ struct _GimpDisplayShell /* the state of gimp_display_shell_zoom_gesture_*() */ gdouble last_zoom_scale; + /* the state of gimp_display_shell_rotate_gesture_*() */ + guint last_gesture_rotate_state; + gdouble initial_gesture_rotate_angle; + /* Two states are possible when the shell is grabbed: it can be * grabbed with space (or space+button1 which is the same state), * then if space is released but button1 was still pressed, we wait -- GitLab From edcbf18fe682cf78f8589b4b66208886deb8b0ff Mon Sep 17 00:00:00 2001 From: Povilas Kanapickas Date: Thu, 17 Mar 2022 14:55:04 +0200 Subject: [PATCH 2/2] app: Implement exclusiveness of zoom and rotate gestures Performing zoom and rotation at the same time is inconvenient because most of the time the user will want either zoom or rotation. This can be solved by recognizing a "significant enough" zoom or rotation change initiated by the gesture recognizer and then ignoring the other gesture. --- app/display/gimpdisplayshell-tool-events.c | 90 +++++++++++++++++++--- app/display/gimpdisplayshell-tool-events.h | 6 ++ app/display/gimpdisplayshell.c | 9 ++- app/display/gimpdisplayshell.h | 2 + 4 files changed, 97 insertions(+), 10 deletions(-) diff --git a/app/display/gimpdisplayshell-tool-events.c b/app/display/gimpdisplayshell-tool-events.c index 16df65ff586..35bc8b64247 100644 --- a/app/display/gimpdisplayshell-tool-events.c +++ b/app/display/gimpdisplayshell-tool-events.c @@ -1255,6 +1255,35 @@ gimp_display_shell_canvas_grab_notify (GtkWidget *canvas, } } +/* The ratio of the following defines what finger movement we interpret as + * a rotation versus zoom gesture. If finger movement is partially a zoom + * and partially a rotation, the detected gesture will be whichever gesture + * we detect first + * + * Let's define "finger movement angle" as the angle between the direction of + * finger movement and the line between fingers. If this angle is zero then + * the gesture is completely a zoom gesture. If this angle is 90 degrees + * then the gesture is completely a rotation gesture. + * + * The boundary finger movement angle (below which the gesture is zoom gesture + * and above which the gesture is rotate gesture) will be defined as follows: + * + * boundary = arctan(deg2rad(ROTATE_GESTURE_ACTIVATION_DEG_DIFF) / + * (ZOOM_GESTURE_ACTIVATION_SCALE_DIFF / 2)) + * + * Note that ZOOM_GESTURE_ACTIVATION_SCALE_DIFF needs to be divided by 2 + * because both fingers are moving so the distance between them is increasing + * twice as fast. + * + * We probably want boundary angle to be around 60 degrees to prevent + * accidentally starting rotations. + * + * With ZOOM_GESTURE_ACTIVATION_SCALE_DIFF==0.02 and + * ROTATE_GESTURE_ACTIVATION_DEG_DIFF==1 boundary is 60.2 degrees. + */ +#define ZOOM_GESTURE_ACTIVATION_SCALE_DIFF 0.02 +#define ROTATE_GESTURE_ACTIVATION_DEG_DIFF 1 + void gimp_display_shell_zoom_gesture_begin (GtkGestureZoom *gesture, GdkEventSequence *sequence, @@ -1268,8 +1297,23 @@ gimp_display_shell_zoom_gesture_update (GtkGestureZoom *gesture, GdkEventSequence *sequence, GimpDisplayShell *shell) { - gdouble current_scale = gtk_gesture_zoom_get_scale_delta (gesture); - gdouble delta = (current_scale - shell->last_zoom_scale) / shell->last_zoom_scale; + gdouble current_scale; + gdouble delta; + + if (shell->rotate_gesture_active) + return; + + /* we only activate zoom gesture handling if rotate gesture was inactive and + * the zoom difference is significant enough */ + current_scale = gtk_gesture_zoom_get_scale_delta (gesture); + if (!shell->zoom_gesture_active && + current_scale > (1 - ZOOM_GESTURE_ACTIVATION_SCALE_DIFF) && + current_scale < (1 + ZOOM_GESTURE_ACTIVATION_SCALE_DIFF)) + return; + + shell->zoom_gesture_active = TRUE; + + delta = (current_scale - shell->last_zoom_scale) / shell->last_zoom_scale; shell->last_zoom_scale = current_scale; gimp_display_shell_scale (shell, @@ -1278,6 +1322,14 @@ gimp_display_shell_zoom_gesture_update (GtkGestureZoom *gesture, GIMP_ZOOM_FOCUS_POINTER); } +void +gimp_display_shell_zoom_gesture_end (GtkGestureZoom *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell) +{ + shell->zoom_gesture_active = FALSE; +} + void gimp_display_shell_rotate_gesture_begin (GtkGestureRotate *gesture, GdkEventSequence *sequence, @@ -1295,19 +1347,39 @@ gimp_display_shell_rotate_gesture_update (GtkGestureRotate *gesture, GdkEventSequence *sequence, GimpDisplayShell *shell) { - gdouble angle; - gboolean constrain; + gdouble angle; + gdouble angle_delta_deg; + gboolean constrain; + + /* we only activate rotate gesture handling if zoom gesture was inactive and + * the rotation is significant enough */ + if (shell->zoom_gesture_active) + return; + + angle_delta_deg = 180.0 * gtk_gesture_rotate_get_angle_delta (gesture) / G_PI; + if (!shell->rotate_gesture_active && + angle_delta_deg > -ROTATE_GESTURE_ACTIVATION_DEG_DIFF && + angle_delta_deg < ROTATE_GESTURE_ACTIVATION_DEG_DIFF) + return; + + shell->rotate_gesture_active = TRUE; + + angle = shell->initial_gesture_rotate_angle + angle_delta_deg; gimp_display_shell_rotate_gesture_maybe_get_state (gesture, sequence, &shell->last_gesture_rotate_state); - angle = shell->initial_gesture_rotate_angle + - 180.0 * gtk_gesture_rotate_get_angle_delta (gesture) / G_PI; - constrain = (shell->last_gesture_rotate_state & GDK_CONTROL_MASK) ? TRUE : FALSE; - gimp_display_shell_rotate_to (shell, - constrain ? RINT (angle / 15.0) * 15.0 : angle); + gimp_display_shell_rotate_to (shell, constrain ? RINT (angle / 15.0) * 15.0 : angle); +} + +void +gimp_display_shell_rotate_gesture_end (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell) +{ + shell->rotate_gesture_active = FALSE; } void diff --git a/app/display/gimpdisplayshell-tool-events.h b/app/display/gimpdisplayshell-tool-events.h index a491999d04e..0e3cf93e027 100644 --- a/app/display/gimpdisplayshell-tool-events.h +++ b/app/display/gimpdisplayshell-tool-events.h @@ -36,6 +36,9 @@ void gimp_display_shell_zoom_gesture_begin (GtkGestureZoom *gesture void gimp_display_shell_zoom_gesture_update (GtkGestureZoom *gesture, GdkEventSequence *sequence, GimpDisplayShell *shell); +void gimp_display_shell_zoom_gesture_end (GtkGestureZoom *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell); void gimp_display_shell_rotate_gesture_begin (GtkGestureRotate *gesture, GdkEventSequence *sequence, @@ -43,6 +46,9 @@ void gimp_display_shell_rotate_gesture_begin (GtkGestureRotate *gesture void gimp_display_shell_rotate_gesture_update (GtkGestureRotate *gesture, GdkEventSequence *sequence, GimpDisplayShell *shell); +void gimp_display_shell_rotate_gesture_end (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell); void gimp_display_shell_buffer_stroke (GimpMotionBuffer *buffer, const GimpCoords *coords, diff --git a/app/display/gimpdisplayshell.c b/app/display/gimpdisplayshell.c index 8ca3d1efa4c..066d935c25a 100644 --- a/app/display/gimpdisplayshell.c +++ b/app/display/gimpdisplayshell.c @@ -515,10 +515,12 @@ gimp_display_shell_constructed (GObject *object) shell->zoom_gesture = gtk_gesture_zoom_new (GTK_WIDGET (shell->canvas)); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->zoom_gesture), GTK_PHASE_CAPTURE); + shell->zoom_gesture_active = FALSE; shell->rotate_gesture = gtk_gesture_rotate_new (GTK_WIDGET (shell->canvas)); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (shell->rotate_gesture), GTK_PHASE_CAPTURE); + shell->rotate_gesture_active = FALSE; /* the horizontal ruler */ shell->hrule = gimp_ruler_new (GTK_ORIENTATION_HORIZONTAL); @@ -612,13 +614,18 @@ gimp_display_shell_constructed (GObject *object) g_signal_connect (shell->zoom_gesture, "update", G_CALLBACK (gimp_display_shell_zoom_gesture_update), shell); + g_signal_connect (shell->zoom_gesture, "end", + G_CALLBACK (gimp_display_shell_zoom_gesture_end), + shell); g_signal_connect (shell->rotate_gesture, "begin", G_CALLBACK (gimp_display_shell_rotate_gesture_begin), shell); g_signal_connect (shell->rotate_gesture, "update", G_CALLBACK (gimp_display_shell_rotate_gesture_update), shell); - + g_signal_connect (shell->rotate_gesture, "end", + G_CALLBACK (gimp_display_shell_rotate_gesture_end), + shell); /* the zoom button */ shell->zoom_button = g_object_new (GTK_TYPE_CHECK_BUTTON, diff --git a/app/display/gimpdisplayshell.h b/app/display/gimpdisplayshell.h index 7a4a0a41f3d..9b81c639e4a 100644 --- a/app/display/gimpdisplayshell.h +++ b/app/display/gimpdisplayshell.h @@ -203,10 +203,12 @@ struct _GimpDisplayShell /* the state of gimp_display_shell_zoom_gesture_*() */ gdouble last_zoom_scale; + gboolean zoom_gesture_active; /* the state of gimp_display_shell_rotate_gesture_*() */ guint last_gesture_rotate_state; gdouble initial_gesture_rotate_angle; + gboolean rotate_gesture_active; /* Two states are possible when the shell is grabbed: it can be * grabbed with space (or space+button1 which is the same state), -- GitLab