Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a pan backend for touchscreen gestures #2096

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Gestures/Gesture.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
45 changes: 45 additions & 0 deletions src/Gestures/GestureSettings.vala
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
* Utility class to access the gesture settings. Easily accessible through GestureTracker.settings.
*/
public class Gala.GestureSettings : Object {
public enum GestureAction {
NONE,
SWITCH_WORKSPACE,
MOVE_TO_WORKSPACE,
SWITCH_WINDOWS,
MULTITASKING_VIEW
}

private static GLib.Settings gala_settings;
private static GLib.Settings touchpad_settings;

Expand Down Expand Up @@ -69,4 +77,41 @@ public class Gala.GestureSettings : Object {
public static string get_string (string setting_id) {
return gala_settings.get_string (setting_id);
}

public static GestureAction get_action (Gesture gesture) {
if (gesture.type == TOUCHPAD_SWIPE) {
var fingers = gesture.fingers;

if (gesture.direction == LEFT || gesture.direction == RIGHT) {
var three_finger_swipe_horizontal = get_string ("three-finger-swipe-horizontal");
var four_finger_swipe_horizontal = get_string ("four-finger-swipe-horizontal");

if (fingers == 3 && three_finger_swipe_horizontal == "switch-to-workspace" ||
fingers == 4 && four_finger_swipe_horizontal == "switch-to-workspace") {
return SWITCH_WORKSPACE;
}

if (fingers == 3 && three_finger_swipe_horizontal == "move-to-workspace" ||
fingers == 4 && four_finger_swipe_horizontal == "move-to-workspace") {
return MOVE_TO_WORKSPACE;
}


if (fingers == 3 && three_finger_swipe_horizontal == "switch-windows" ||
fingers == 4 && four_finger_swipe_horizontal == "switch-windows") {
return SWITCH_WINDOWS;
}
} else if (gesture.direction == UP || gesture.direction == DOWN) {
var three_finger_swipe_up = get_string ("three-finger-swipe-up");
var four_finger_swipe_up = get_string ("four-finger-swipe-up");

if (fingers == 3 && three_finger_swipe_up == "multitasking-view" ||
fingers == 4 && four_finger_swipe_up == "multitasking-view") {
return MULTITASKING_VIEW;
}
}
}

return NONE;
}
}
61 changes: 54 additions & 7 deletions src/Gestures/GestureTracker.vala
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

public interface Gala.GestureBackend : Object {
public signal bool on_gesture_detected (Gesture gesture, uint32 timestamp);
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 virtual void prepare_gesture_handling () { }
}

/**
* Allow to use multi-touch gestures from different sources (backends).
* Usage:
Expand Down Expand Up @@ -68,11 +77,24 @@ public class Gala.GestureTracker : Object {

/**
* Emitted when a new gesture is detected.
* If the receiving code needs to handle this gesture, it should call to connect_handlers to
* start receiving updates.
* This should only be used to determine whether the gesture should be handled. This shouldn't
* do any preparations instead those should be done in {@link on_gesture_handled}. This is because
* the backend might have to do some preparations itself before you are allowed to do some to avoid
* conflicts.
* @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 bool on_gesture_detected (Gesture gesture);

/**
* Emitted if true was returned form {@link on_gesture_detected}. This should
* be used to do any preparations for gesture handling and to call {@link connect_handlers} to
* start receiving updates.
* @param gesture the same gesture as in {@link on_gesture_detected}
* @param timestamp the timestamp of the event that initiated the gesture or {@link Meta.CURRENT_TIME}.
*/
public signal void on_gesture_detected (Gesture gesture);
public signal void on_gesture_handled (Gesture gesture, uint32 timestamp);

/**
* Emitted right after on_gesture_detected with the initial gesture information.
Expand All @@ -98,7 +120,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.
Expand Down Expand Up @@ -137,6 +164,22 @@ 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_distance_func this will be called if a gesture is detected and true is returned from {@link on_gesture_detected}.
* The returned distance 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 * distance.
*/
public void enable_pan (WindowManager wm, Clutter.Actor actor, owned PanBackend.GetTravelDistance travel_distance_func) {
pan_backend = new PanBackend (wm, actor, (owned) travel_distance_func);
pan_backend.on_gesture_detected.connect (gesture_detected);
pan_backend.on_begin.connect (gesture_begin);
pan_backend.on_update.connect (gesture_update);
pan_backend.on_end.connect (gesture_end);
}

/**
* Allow to receive scroll gestures.
* @param actor Clutter actor that will receive the scroll events.
Expand Down Expand Up @@ -201,10 +244,14 @@ public class Gala.GestureTracker : Object {
return value;
}

private void gesture_detected (Gesture gesture) {
if (enabled) {
on_gesture_detected (gesture);
private bool gesture_detected (GestureBackend backend, Gesture gesture, uint32 timestamp) {
if (enabled && on_gesture_detected (gesture)) {
backend.prepare_gesture_handling ();
on_gesture_handled (gesture, timestamp);
return true;
}

return false;
}

private void gesture_begin (double percentage, uint64 elapsed_time) {
Expand Down
161 changes: 161 additions & 0 deletions src/Gestures/PanBackend.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2024 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <[email protected]>
*/

public class Gala.PanBackend : Object, GestureBackend {
public delegate float GetTravelDistance ();

public WindowManager wm { get; construct; }
public Clutter.Actor actor { get; construct; }

private GetTravelDistance get_travel_distance_func;

private ModalProxy? modal_proxy;

private Clutter.PanAxis pan_axis;
private Clutter.PanAction pan_action;

private GestureDirection direction;

private float origin_x;
private float origin_y;

private float current_x;
private float current_y;

private float last_x_coord;
private float last_y_coord;
private uint last_n_points;

private float travel_distance;

public PanBackend (WindowManager wm, Clutter.Actor actor, owned GetTravelDistance get_travel_distance_func) {
Object (wm: wm, actor: actor);

this.get_travel_distance_func = (owned) get_travel_distance_func;
}

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);
pan_action.gesture_cancel.connect (on_gesture_end);
}

~PanBackend () {
actor.remove_action (pan_action);
}

private bool on_gesture_begin () {
if (pan_action.get_last_event (0).get_source_device ().get_device_type () != TOUCHSCREEN_DEVICE) {
return false;
}

float x_coord, y_coord;
pan_action.get_press_coords (0, out x_coord, out y_coord);

origin_x = current_x = x_coord;
origin_y = current_y = y_coord;

var time = pan_action.get_last_event (0).get_time ();

var handled = on_gesture_detected (build_gesture (), time);

if (!handled) {
reset ();
return false;
}

travel_distance = get_travel_distance_func ();

on_begin (0, time);

return true;
}

public override void prepare_gesture_handling () {
modal_proxy = wm.push_modal (actor);
}

private void on_gesture_end () {
on_end (calculate_percentage (), Meta.CURRENT_TIME);

reset ();
}

private void reset () {
if (modal_proxy != null) {
wm.pop_modal (modal_proxy);
modal_proxy = null;
}

direction = GestureDirection.UNKNOWN;
last_n_points = 0;
last_x_coord = 0;
last_y_coord = 0;
}

private bool on_pan (Clutter.PanAction pan_action, Clutter.Actor actor, bool interpolate) {
var time = pan_action.get_last_event (0).get_time ();

float x, y;
pan_action.get_motion_coords (0, out x, out y);

if (pan_action.get_n_current_points () == last_n_points) {
current_x += x - last_x_coord;
current_y += y - last_y_coord;
}

last_x_coord = x;
last_y_coord = y;
last_n_points = pan_action.get_n_current_points ();

on_update (calculate_percentage (), time);

return true;
}

private double calculate_percentage () {
float current, origin;
if (pan_axis == X_AXIS) {
current = direction == RIGHT ? float.max (current_x, origin_x) : float.min (current_x, origin_x);
origin = origin_x;
} else {
current = direction == DOWN ? float.max (current_y, origin_y) : float.min (current_y, origin_y);
origin = origin_y;
}

return (current - origin).abs () / travel_distance;
}

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
};
}
}
Loading