diff --git a/app/display/gimpdisplayshell-tool-events.c b/app/display/gimpdisplayshell-tool-events.c index e28e7199f750b4c9f115dfdaeebdf942eb219642..35bc8b64247b4c9f5d990d641d9ff2cf190c0a37 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, @@ -1250,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, @@ -1263,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, @@ -1273,6 +1322,66 @@ 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, + 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; + 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); + + 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_rotate_gesture_end (GtkGestureRotate *gesture, + GdkEventSequence *sequence, + GimpDisplayShell *shell) +{ + shell->rotate_gesture_active = FALSE; +} + void gimp_display_shell_buffer_stroke (GimpMotionBuffer *buffer, const GimpCoords *coords, @@ -1671,6 +1780,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 6539640c11ba74c0ad2d44451e867a77e089e9a9..0e3cf93e02739bbd1fe123eca2ed20d450b88a1d 100644 --- a/app/display/gimpdisplayshell-tool-events.h +++ b/app/display/gimpdisplayshell-tool-events.h @@ -36,6 +36,19 @@ 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, + GimpDisplayShell *shell); +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 a87b94029074af062d9a22e43222440a332ec1e4..066d935c25a3faa2fc643779db7408088dfec93a 100644 --- a/app/display/gimpdisplayshell.c +++ b/app/display/gimpdisplayshell.c @@ -515,6 +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); @@ -608,6 +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, @@ -759,6 +777,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 f9bf6360f0318f4607f66d4c45a82b2820657cc7..9b81c639e4a2f3ca9a166f77568b5748bd104f77 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; @@ -202,6 +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),