diff --git a/lib/Utils.vala b/lib/Utils.vala index d48eb7545..2c8197bb1 100644 --- a/lib/Utils.vala +++ b/lib/Utils.vala @@ -17,6 +17,31 @@ namespace Gala { public class Utils { + /** + * Represents a size as an object in order for it to be updatable. + * Additionally it has some utilities like {@link Gala.Utils.Size.actor_tracking}. + */ + public class Size { + public float width; + public float height; + + public Size (float width, float height) { + this.width = width; + this.height = height; + } + + /** + * Constructs a new size that will keep in sync with the width and height of an actor. + */ + public Size.actor_tracking (Clutter.Actor actor) { + actor.notify["width"].connect ((obj, pspec) => width = ((Clutter.Actor) obj).width); + actor.notify["height"].connect ((obj, pspec) => height = ((Clutter.Actor) obj).height); + + width = actor.width; + height = actor.height; + } + } + private struct CachedIcon { public Gdk.Pixbuf icon; public int icon_size; diff --git a/src/Gestures/Gesture.vala b/src/Gestures/Gesture.vala index 9bccedaf7..5ffd2831d 100644 --- a/src/Gestures/Gesture.vala +++ b/src/Gestures/Gesture.vala @@ -35,9 +35,25 @@ namespace Gala { } public class Gesture { + public const float INVALID_COORD = float.MAX; + public Clutter.EventType type; public GestureDirection direction; public int fingers; public Clutter.InputDeviceType performed_on_device_type; + + /** + * The x coordinate of the initial contact point for the gesture. + * Doesn't have to be set. In that case it is set to {@link INVALID_COORD}. + * Currently the only backend not setting this is {@link GestureTracker.enable_touchpad}. + */ + public float origin_x = INVALID_COORD; + + /** + * The y coordinate of the initial contact point for the gesture. + * Doesn't have to be set. In that case it is set to {@link INVALID_COORD}. + * Currently the only backend not setting this is {@link GestureTracker.enable_touchpad}. + */ + public float origin_y = INVALID_COORD; } } diff --git a/src/Gestures/GestureTracker.vala b/src/Gestures/GestureTracker.vala index aba66f648..14e5686b7 100644 --- a/src/Gestures/GestureTracker.vala +++ b/src/Gestures/GestureTracker.vala @@ -71,8 +71,10 @@ public class Gala.GestureTracker : Object { * If the receiving code needs to handle this gesture, it should call to connect_handlers to * start receiving updates. * @param gesture Information about the gesture. + * @return true if the gesture will be handled false otherwise. If false is returned the other + * signals may still be emitted but aren't guaranteed to be. */ - public signal void on_gesture_detected (Gesture gesture); + public signal bool on_gesture_detected (Gesture gesture); /** * Emitted right after on_gesture_detected with the initial gesture information. @@ -98,7 +100,12 @@ public class Gala.GestureTracker : Object { /** * Backend used if enable_touchpad is called. */ - private ToucheggBackend touchpad_backend; + private ToucheggBackend? touchpad_backend; + + /** + * Pan backend used if enable_pan is called. + */ + private PanBackend pan_backend; /** * Scroll backend used if enable_scroll is called. @@ -137,6 +144,34 @@ public class Gala.GestureTracker : Object { touchpad_backend.on_end.connect (gesture_end); } + /** + * Allow to receive pan gestures. + * @param actor Clutter actor that will receive the events. + * @param travel_distances {@link Utils.Size} with width and height. The given size wil be + * used to calculate the percentage. It should be set to the amount something will travel (e.g. + * when moving an actor) based on the gesture to allow exact finger tracking. It can also be used + * to calculate the raw pixels the finger travelled at a given time with percentage * corresponding distance + * (height for {@link GestureDirection.UP} or DOWN, width for LEFT or RIGHT). If set to null the size of the + * actor will be used. If the values change those changes will apply. + */ + public void enable_pan (Clutter.Actor actor, Utils.Size? travel_distances) { + pan_backend = new PanBackend (actor, travel_distances); + pan_backend.on_gesture_detected.connect (gesture_detected); + pan_backend.on_begin.connect ((percentage, time) => { + gesture_begin (percentage, time); + if (touchpad_backend != null) { + touchpad_backend.ignore_touchscreen = true; + } + }); + pan_backend.on_update.connect (gesture_update); + pan_backend.on_end.connect ((percentage, time) => { + gesture_end (percentage, time); + if (touchpad_backend != null) { + touchpad_backend.ignore_touchscreen = false; + } + }); + } + /** * Allow to receive scroll gestures. * @param actor Clutter actor that will receive the scroll events. @@ -201,10 +236,12 @@ public class Gala.GestureTracker : Object { return value; } - private void gesture_detected (Gesture gesture) { + private bool gesture_detected (Gesture gesture) { if (enabled) { - on_gesture_detected (gesture); + return on_gesture_detected (gesture); } + + return false; } private void gesture_begin (double percentage, uint64 elapsed_time) { diff --git a/src/Gestures/PanBackend.vala b/src/Gestures/PanBackend.vala new file mode 100644 index 000000000..08f8fa27f --- /dev/null +++ b/src/Gestures/PanBackend.vala @@ -0,0 +1,114 @@ +/* + * Copyright 2024 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.PanBackend : Object { + public signal bool on_gesture_detected (Gesture gesture); + public signal void on_begin (double delta, uint64 time); + public signal void on_update (double delta, uint64 time); + public signal void on_end (double delta, uint64 time); + + public Clutter.Actor actor { get; construct; } + public Utils.Size? travel_distances { get; construct; } + + private Clutter.PanAxis pan_axis; + private Clutter.PanAction pan_action; + + private GestureDirection direction; + + private float origin_x; + private float origin_y; + + public PanBackend (Clutter.Actor actor, Utils.Size? travel_distances) { + Object (actor: actor, travel_distances: travel_distances); + } + + construct { + pan_action = new Clutter.PanAction () { + n_touch_points = 1 + }; + + actor.add_action_full ("pan-gesture", CAPTURE, pan_action); + + pan_action.gesture_begin.connect (on_gesture_begin); + pan_action.pan.connect (on_pan); + pan_action.gesture_end.connect (on_gesture_end); + } + + private bool on_gesture_begin () { + float x_coord, y_coord; + pan_action.get_press_coords (0, out x_coord, out y_coord); + + origin_x = x_coord; + origin_y = y_coord; + + var handled = on_gesture_detected (build_gesture ()); + + if (!handled) { + return false; + } + + on_begin (0, pan_action.get_last_event (0).get_time ()); + + return true; + } + + private void on_gesture_end () { + float x_coord, y_coord; + pan_action.get_motion_coords (0, out x_coord, out y_coord); + on_end (calculate_percentage (x_coord, y_coord), pan_action.get_last_event (0).get_time ()); + + direction = GestureDirection.UNKNOWN; + } + + private bool on_pan (Clutter.PanAction pan_action, Clutter.Actor actor, bool interpolate) { + uint64 time = pan_action.get_last_event (0).get_time (); + + float x_coord, y_coord; + pan_action.get_motion_coords (0, out x_coord, out y_coord); + + on_update (calculate_percentage (x_coord, y_coord), time); + + return true; + } + + private double calculate_percentage (float current_x, float current_y) { + float current, origin, size; + if (pan_axis == X_AXIS) { + current = direction == RIGHT ? float.max (current_x, origin_x) : float.min (current_x, origin_x); + origin = origin_x; + size = travel_distances != null ? travel_distances.width : actor.width; + } else { + current = direction == DOWN ? float.max (current_y, origin_y) : float.min (current_y, origin_y); + origin = origin_y; + size = travel_distances != null ? travel_distances.height : actor.height; + } + + return (current - origin).abs () / size; + } + + private Gesture build_gesture () { + float delta_x, delta_y; + ((Clutter.GestureAction) pan_action).get_motion_delta (0, out delta_x, out delta_y); + + pan_axis = delta_x.abs () > delta_y.abs () ? Clutter.PanAxis.X_AXIS : Clutter.PanAxis.Y_AXIS; + + if (pan_axis == X_AXIS) { + direction = delta_x > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT; + } else { + direction = delta_y > 0 ? GestureDirection.DOWN : GestureDirection.UP; + } + + return new Gesture () { + type = Clutter.EventType.TOUCHPAD_SWIPE, + direction = direction, + fingers = (int) pan_action.get_n_current_points (), + performed_on_device_type = Clutter.InputDeviceType.TOUCHSCREEN_DEVICE, + origin_x = origin_x, + origin_y = origin_y + }; + } +} diff --git a/src/Gestures/ScrollBackend.vala b/src/Gestures/ScrollBackend.vala index ed933a243..5fe9e8098 100644 --- a/src/Gestures/ScrollBackend.vala +++ b/src/Gestures/ScrollBackend.vala @@ -26,7 +26,7 @@ public class Gala.ScrollBackend : Object { private const double FINISH_DELTA_HORIZONTAL = 40; private const double FINISH_DELTA_VERTICAL = 30; - public signal void on_gesture_detected (Gesture gesture); + public signal bool on_gesture_detected (Gesture gesture); public signal void on_begin (double delta, uint64 time); public signal void on_update (double delta, uint64 time); public signal void on_end (double delta, uint64 time); @@ -80,7 +80,9 @@ public class Gala.ScrollBackend : Object { if (!started) { if (delta_x != 0 || delta_y != 0) { - Gesture gesture = build_gesture (delta_x, delta_y, orientation); + float origin_x, origin_y; + event.get_coords (out origin_x, out origin_y); + Gesture gesture = build_gesture (origin_x, origin_y, delta_x, delta_y, orientation); started = true; direction = gesture.direction; on_gesture_detected (gesture); @@ -114,7 +116,7 @@ public class Gala.ScrollBackend : Object { && event.get_scroll_direction () == Clutter.ScrollDirection.SMOOTH; } - private static Gesture build_gesture (double delta_x, double delta_y, Clutter.Orientation orientation) { + private static Gesture build_gesture (float origin_x, float origin_y, double delta_x, double delta_y, Clutter.Orientation orientation) { GestureDirection direction; if (orientation == Clutter.Orientation.HORIZONTAL) { direction = delta_x > 0 ? GestureDirection.RIGHT : GestureDirection.LEFT; @@ -126,7 +128,9 @@ public class Gala.ScrollBackend : Object { type = Clutter.EventType.SCROLL, direction = direction, fingers = 2, - performed_on_device_type = Clutter.InputDeviceType.TOUCHPAD_DEVICE + performed_on_device_type = Clutter.InputDeviceType.TOUCHPAD_DEVICE, + origin_x = origin_x, + origin_y = origin_y }; } diff --git a/src/Gestures/ToucheggBackend.vala b/src/Gestures/ToucheggBackend.vala index d4eeb224b..9c00dee0a 100644 --- a/src/Gestures/ToucheggBackend.vala +++ b/src/Gestures/ToucheggBackend.vala @@ -21,11 +21,13 @@ * See: [[https://github.com/JoseExposito/touchegg]] */ public class Gala.ToucheggBackend : Object { - public signal void on_gesture_detected (Gesture gesture); + public signal bool on_gesture_detected (Gesture gesture); public signal void on_begin (double delta, uint64 time); public signal void on_update (double delta, uint64 time); public signal void on_end (double delta, uint64 time); + public bool ignore_touchscreen { get; set; default = false; } + /** * Gesture type as returned by the daemon. */ @@ -197,6 +199,10 @@ public class Gala.ToucheggBackend : Object { signal_params.get ("(uudiut)", out type, out direction, out percentage, out fingers, out performed_on_device_type, out elapsed_time); + if (ignore_touchscreen && performed_on_device_type == TOUCHSCREEN) { + return; + } + var delta = percentage * DELTA_MULTIPLIER; switch (signal_name) { diff --git a/src/Widgets/MultitaskingView.vala b/src/Widgets/MultitaskingView.vala index 50deea0a9..9d3adba91 100644 --- a/src/Widgets/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView.vala @@ -290,24 +290,28 @@ namespace Gala { workspaces.add_transition ("nudge", nudge); } - private void on_multitasking_gesture_detected (Gesture gesture) { + private bool on_multitasking_gesture_detected (Gesture gesture) { if (gesture.type != Clutter.EventType.TOUCHPAD_SWIPE || (gesture.fingers == 3 && GestureSettings.get_string ("three-finger-swipe-up") != "multitasking-view") || (gesture.fingers == 4 && GestureSettings.get_string ("four-finger-swipe-up") != "multitasking-view") ) { - return; + return false; } if (gesture.direction == GestureDirection.UP && !opened) { toggle (true, false); + return true; } else if (gesture.direction == GestureDirection.DOWN && opened) { toggle (true, false); + return true; } + + return false; } - private void on_workspace_gesture_detected (Gesture gesture) { + private bool on_workspace_gesture_detected (Gesture gesture) { if (!opened) { - return; + return false; } var can_handle_swipe = gesture.type == Clutter.EventType.TOUCHPAD_SWIPE && @@ -319,7 +323,10 @@ namespace Gala { if (gesture.type == Clutter.EventType.SCROLL || (can_handle_swipe && fingers)) { var direction = workspace_gesture_tracker.settings.get_natural_scroll_direction (gesture); switch_workspace_with_gesture (direction); + return true; } + + return false; } private void switch_workspace_with_gesture (Meta.MotionDirection direction) { diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 8d1122132..942966b12 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -539,14 +539,14 @@ namespace Gala { } } - private void on_gesture_detected (Gesture gesture) { + private bool on_gesture_detected (Gesture gesture) { if (workspace_view.is_opened ()) { - return; + return false; } if (gesture.type != Clutter.EventType.TOUCHPAD_SWIPE || (gesture.direction != GestureDirection.LEFT && gesture.direction != GestureDirection.RIGHT)) { - return; + return false; } unowned var display = get_display (); @@ -569,7 +569,7 @@ namespace Gala { if (switch_workspace_with_gesture) { var direction = gesture_tracker.settings.get_natural_scroll_direction (gesture); switch_to_next_workspace (direction, display.get_current_time ()); - return; + return true; } switch_workspace_with_gesture = three_fingers_move_to_workspace || four_fingers_move_to_workspace; @@ -584,13 +584,16 @@ namespace Gala { } switch_to_next_workspace (direction, display.get_current_time ()); - return; + return true; } var switch_windows = three_fingers_switch_windows || four_fingers_switch_windows; if (switch_windows && !window_switcher.opened) { window_switcher.handle_gesture (gesture.direction); + return true; } + + return false; } /** diff --git a/src/Zoom.vala b/src/Zoom.vala index c3c27f5be..ef63eb83b 100644 --- a/src/Zoom.vala +++ b/src/Zoom.vala @@ -61,18 +61,21 @@ public class Gala.Zoom : Object { zoom (-SHORTCUT_DELTA, true, wm.enable_animations); } - private void on_gesture_detected (Gesture gesture) { + private bool on_gesture_detected (Gesture gesture) { if (gesture.type != Clutter.EventType.TOUCHPAD_PINCH || (gesture.direction != GestureDirection.IN && gesture.direction != GestureDirection.OUT) ) { - return; + return false; } if ((gesture.fingers == 3 && GestureSettings.get_string ("three-finger-pinch") == "zoom") || (gesture.fingers == 4 && GestureSettings.get_string ("four-finger-pinch") == "zoom") ) { zoom_with_gesture (gesture.direction); + return true; } + + return false; } private void zoom_with_gesture (GestureDirection direction) { diff --git a/src/meson.build b/src/meson.build index e2e61807b..5e942075a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -36,6 +36,7 @@ gala_bin_sources = files( 'Gestures/Gesture.vala', 'Gestures/GestureSettings.vala', 'Gestures/GestureTracker.vala', + 'Gestures/PanBackend.vala', 'Gestures/ScrollBackend.vala', 'Gestures/ToucheggBackend.vala', 'HotCorners/Barrier.vala',