From 933ca7bf7f09ebc6bb5c7358a7859caa918ccad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Exp=C3=B3sito?= Date: Wed, 16 Dec 2020 22:04:52 +0100 Subject: [PATCH] Multi-touch support (#983) --- data/gala.gschema.xml | 25 +++ lib/ActivatableComponent.vala | 5 +- lib/WindowManager.vala | 8 +- meson.build | 1 + plugins/touchegg/Client.vala | 200 ++++++++++++++++++++ plugins/touchegg/Gesture.vala | 54 ++++++ plugins/touchegg/Main.vala | 153 +++++++++++++++ plugins/touchegg/meson.build | 15 ++ src/GestureAnimationDirector.vala | 84 ++++++++ src/Widgets/MonitorClone.vala | 11 +- src/Widgets/MultitaskingView.vala | 263 ++++++++++++++++++-------- src/Widgets/WindowClone.vala | 139 +++++++++++--- src/Widgets/WindowCloneContainer.vala | 23 ++- src/Widgets/WindowOverview.vala | 8 +- src/Widgets/WorkspaceClone.vala | 76 ++++++-- src/WindowManager.vala | 179 +++++++++++++----- src/meson.build | 1 + 17 files changed, 1056 insertions(+), 189 deletions(-) create mode 100644 plugins/touchegg/Client.vala create mode 100644 plugins/touchegg/Gesture.vala create mode 100644 plugins/touchegg/Main.vala create mode 100644 plugins/touchegg/meson.build create mode 100644 src/GestureAnimationDirector.vala diff --git a/data/gala.gschema.xml b/data/gala.gschema.xml index d84e6526..9b576203 100644 --- a/data/gala.gschema.xml +++ b/data/gala.gschema.xml @@ -313,4 +313,29 @@ Only show corner masks on primary monitor + + + + true + Multitasking view gesture + If enabled, swipe up with the number of fingers set in io.elementary.desktop.wm.gestures.multitasking-gesture-fingers to show the multitasking view + + + 3 + + Multitasking view gesture fingers + Number of fingers used in the multitasking view gesture + + + true + Switch workspace gesture + If enabled, swipe left/right with the number of fingers set in io.elementary.desktop.wm.gestures.workspaces-gesture-fingers to switch between workspaces + + + 3 + + Switch workspace gesture fingers + Number of fingers used in the switch workspaces gesture + + diff --git a/lib/ActivatableComponent.vala b/lib/ActivatableComponent.vala index 96ba987a..a0d7dd2f 100644 --- a/lib/ActivatableComponent.vala +++ b/lib/ActivatableComponent.vala @@ -34,8 +34,11 @@ namespace Gala { /** * The component was requested to be closed. + * + * @param hints The hashmap may contain special parameters that are useful + * to the component. */ - public abstract void close (); + public abstract void close (HashTable? hints = null); /** * Should return whether the component is currently opened. Used mainly for diff --git a/lib/WindowManager.vala b/lib/WindowManager.vala index aff60e87..a7c17834 100644 --- a/lib/WindowManager.vala +++ b/lib/WindowManager.vala @@ -106,6 +106,11 @@ namespace Gala { */ public abstract Meta.BackgroundGroup background_group { get; protected set; } + /** + * View that allows to see and manage all your windows and desktops. + */ + public abstract Gala.ActivatableComponent workspace_view { get; protected set; } + /** * Whether animations should be displayed. */ @@ -164,6 +169,7 @@ namespace Gala { * * @param direction The direction in which to switch */ - public abstract void switch_to_next_workspace (Meta.MotionDirection direction); + public abstract void switch_to_next_workspace (Meta.MotionDirection direction, + HashTable? hints = null); } } diff --git a/meson.build b/meson.build index 79ad1660..2f34110e 100644 --- a/meson.build +++ b/meson.build @@ -193,6 +193,7 @@ subdir('daemon') subdir('plugins/maskcorners') subdir('plugins/pip') subdir('plugins/template') +subdir('plugins/touchegg') subdir('plugins/zoom') if get_option('documentation') subdir('docs') diff --git a/plugins/touchegg/Client.vala b/plugins/touchegg/Client.vala new file mode 100644 index 00000000..654b75ad --- /dev/null +++ b/plugins/touchegg/Client.vala @@ -0,0 +1,200 @@ +/* + * Copyright 2020 elementary, Inc (https://elementary.io) + * 2020 José Expósito + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace Gala.Plugins.Touchegg { + /** + * Daemon event type. + */ + private enum GestureEventType { + UNKNOWN = 0, + BEGIN = 1, + UPDATE = 2, + END = 3, + } + + /** + * Daemon event. + */ + private struct GestureEvent { + public uint32 event_size; + public GestureEventType event_type; + public GestureType type; + public GestureDirection direction; + public int percentage; + public int fingers; + public uint64 elapsed_time; + public DeviceType performed_on_device_type; + } + + /** + * This class connects to the Touchégg daemon to receive touch events. + * See: https://github.com/JoseExposito/touchegg + */ + public class Client : Object { + public signal void on_gesture_begin (Gesture gesture); + public signal void on_gesture_update (Gesture gesture); + public signal void on_gesture_end (Gesture gesture); + + /** + * Maximum number of reconnection attempts to the daemon. + */ + private const int MAX_RECONNECTION_ATTEMPTS = 5; + + /** + * Time to sleep between reconnection attempts. + */ + private const int RECONNECTION_USLEEP_TIME = 5000000; + + /** + * Socket used to connect to the daemon. + */ + private Socket? socket = null; + + /** + * Current number of reconnection attempts. + */ + private int reconnection_attempts = 0; + + /** + * Struct to store the received event. It is useful to keep it to be able to finish ongoing + * actions in case of disconnection + */ + private GestureEvent *event = null; + + /** + * Start receiving gestures. + */ + public void run () throws IOError { + new Thread (null, receive_events); + } + + public void stop () { + if (socket != null) { + try { + reconnection_attempts = MAX_RECONNECTION_ATTEMPTS; + socket.close (); + } catch (Error e) { + // Ignore this error, the process is being killed as this point + } + } + } + + private void* receive_events () { + uint8[] event_buffer = new uint8[sizeof (GestureEvent)]; + + while (reconnection_attempts < MAX_RECONNECTION_ATTEMPTS) { + try { + if (socket == null || !socket.is_connected ()) { + debug ("Connecting to Touchégg daemon"); + socket = new Socket (SocketFamily.UNIX, SocketType.STREAM, 0); + if (socket == null) { + throw new GLib.IOError.CONNECTION_REFUSED ( + "Error connecting to Touchégg daemon: Can not create socket" + ); + } + + UnixSocketAddress address = new UnixSocketAddress.as_abstract ("/touchegg", -1); + bool connected = socket.connect (address); + if (!connected) { + throw new GLib.IOError.CONNECTION_REFUSED ("Error connecting to Touchégg daemon"); + } + + reconnection_attempts = 0; + debug ("Connection to Touchégg daemon established"); + } + + // Read the event + ssize_t bytes_received = socket.receive (event_buffer); + if (bytes_received <= 0) { + throw new GLib.IOError.CONNECTION_CLOSED ("Error reading socket"); + } + event = (GestureEvent *) event_buffer; + + // The daemon could add events not supported by this plugin yet + // Discard any extra data + if (bytes_received < event.event_size) { + ssize_t pending_bytes = event.event_size - bytes_received; + uint8[] discard_buffer = new uint8[pending_bytes]; + bytes_received = socket.receive (discard_buffer); + if (bytes_received <= 0) { + throw new GLib.IOError.CONNECTION_CLOSED ("Error reading socket"); + } + } + + emit_event (event); + } catch (Error e) { + warning ("Connection to Touchégg daemon lost: %s", e.message); + handle_disconnection (); + } + } + + return null; + } + + private void handle_disconnection () { + reconnection_attempts++; + + if (event != null + && event.event_type != GestureEventType.UNKNOWN + && event.event_type != GestureEventType.END) { + event.event_type = GestureEventType.END; + emit_event (event); + } + + if (socket != null) { + try { + socket.close (); + } catch (Error e) { + // The connection is already closed at this point, ignore this error + } + } + + if (reconnection_attempts < MAX_RECONNECTION_ATTEMPTS) { + debug ("Reconnecting to Touchégg daemon in 5 seconds"); + Thread.usleep (RECONNECTION_USLEEP_TIME); + } else { + warning ("Maximum number of reconnections reached, aborting"); + } + } + + private void emit_event (GestureEvent *event) { + Gesture gesture = new Gesture () { + type = event.type, + direction = event.direction, + percentage = event.percentage, + fingers = event.fingers, + elapsed_time = event.elapsed_time, + performed_on_device_type = event.performed_on_device_type + }; + + switch (event.event_type) { + case GestureEventType.BEGIN: + on_gesture_begin (gesture); + break; + case GestureEventType.UPDATE: + on_gesture_update (gesture); + break; + case GestureEventType.END: + on_gesture_end (gesture); + break; + default: + break; + } + } + } +} diff --git a/plugins/touchegg/Gesture.vala b/plugins/touchegg/Gesture.vala new file mode 100644 index 00000000..f42e2fbe --- /dev/null +++ b/plugins/touchegg/Gesture.vala @@ -0,0 +1,54 @@ +/* + * Copyright 2020 elementary, Inc (https://elementary.io) + * 2020 José Expósito + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace Gala.Plugins.Touchegg { + public enum GestureType { + NOT_SUPPORTED = 0, + SWIPE = 1, + PINCH = 2, + } + + public enum GestureDirection { + UNKNOWN = 0, + + // GestureType.SWIPE + UP = 1, + DOWN = 2, + LEFT = 3, + RIGHT = 4, + + // GestureType.PINCH + IN = 5, + OUT = 6, + } + + public enum DeviceType { + UNKNOWN = 0, + TOUCHPAD = 1, + TOUCHSCREEN = 2, + } + + public class Gesture { + public GestureType type; + public GestureDirection direction; + public int percentage; + public int fingers; + public uint64 elapsed_time; + public DeviceType performed_on_device_type; + } +} diff --git a/plugins/touchegg/Main.vala b/plugins/touchegg/Main.vala new file mode 100644 index 00000000..23dbed07 --- /dev/null +++ b/plugins/touchegg/Main.vala @@ -0,0 +1,153 @@ +/* + * Copyright 2020 elementary, Inc (https://elementary.io) + * 2020 José Expósito + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +public class Gala.Plugins.Touchegg.Plugin : Gala.Plugin { + private Gala.WindowManager? wm = null; + private Client? client = null; + private GLib.Settings gala_settings; + private GLib.Settings touchpad_settings; + + /** + * Percentage of the animation to be completed to apply the action. + */ + private const int SUCCESS_THRESHOLD = 20; + + public override void initialize (Gala.WindowManager window_manager) { + wm = window_manager; + gala_settings = new GLib.Settings ("io.elementary.desktop.wm.gestures"); + touchpad_settings = new GLib.Settings ("org.gnome.desktop.peripherals.touchpad"); + + client = new Client (); + client.on_gesture_begin.connect ((gesture) => Idle.add (() => { + on_handle_gesture (gesture, "begin"); + return false; + })); + client.on_gesture_update.connect ((gesture) => Idle.add (() => { + on_handle_gesture (gesture, "update"); + return false; + })); + client.on_gesture_end.connect ((gesture) => Idle.add (() => { + on_handle_gesture (gesture, "end"); + return false; + })); + + try { + client.run (); + } catch (Error e) { + warning ("Error initializing Touchégg client: %s", e.message); + } + } + + public override void destroy () { + if (client != null) { + client.stop (); + } + } + + + private void on_handle_gesture (Gesture gesture, string event) { + // debug (@"Gesture $(event): $(gesture.type) - $(gesture.direction) - $(gesture.fingers) fingers - $(gesture.percentage)% - $(gesture.elapsed_time) - $(gesture.performed_on_device_type)"); + var hints = build_hints_from_gesture (gesture, event); + + if (is_open_workspace_gesture (gesture)) { + wm.workspace_view.open (hints); + } else if (is_close_workspace_gesture (gesture)) { + wm.workspace_view.close (hints); + } else if (is_next_desktop_gesture (gesture)) { + if (!wm.workspace_view.is_opened ()) { + wm.switch_to_next_workspace (Meta.MotionDirection.RIGHT, hints); + } + } else if (is_previous_desktop_gesture (gesture)) { + if (!wm.workspace_view.is_opened ()) { + wm.switch_to_next_workspace (Meta.MotionDirection.LEFT, hints); + } + } + } + + private GLib.HashTable build_hints_from_gesture (Gesture gesture, string event) { + var hints = new GLib.HashTable (str_hash, str_equal); + hints.insert ("manual_animation", new Variant.boolean (true)); + hints.insert ("event", new Variant.string (event)); + hints.insert ("percentage", new Variant.int32 (gesture.percentage)); + + if (event == "end") { + hints.insert ("cancel_action", new Variant.boolean (gesture.percentage < SUCCESS_THRESHOLD)); + } + + return hints; + } + + private bool is_open_workspace_gesture (Gesture gesture) { + bool enabled = gala_settings.get_boolean ("multitasking-gesture-enabled"); + int fingers = gala_settings.get_int ("multitasking-gesture-fingers"); + + return enabled + && gesture.type == GestureType.SWIPE + && gesture.direction == GestureDirection.UP + && gesture.fingers == fingers; + } + + private bool is_close_workspace_gesture (Gesture gesture) { + bool enabled = gala_settings.get_boolean ("multitasking-gesture-enabled"); + int fingers = gala_settings.get_int ("multitasking-gesture-fingers"); + + return enabled + && gesture.type == GestureType.SWIPE + && gesture.direction == GestureDirection.DOWN + && gesture.fingers == fingers; + } + + private bool is_next_desktop_gesture (Gesture gesture) { + bool enabled = gala_settings.get_boolean ("workspaces-gesture-enabled"); + int fingers = gala_settings.get_int ("workspaces-gesture-fingers"); + bool natural_scroll = (gesture.performed_on_device_type == DeviceType.TOUCHSCREEN) + ? true + : touchpad_settings.get_boolean ("natural-scroll"); + var direction = natural_scroll ? GestureDirection.LEFT : GestureDirection.RIGHT; + + return enabled + && gesture.type == GestureType.SWIPE + && gesture.direction == direction + && gesture.fingers == fingers; + } + + private bool is_previous_desktop_gesture (Gesture gesture) { + bool enabled = gala_settings.get_boolean ("workspaces-gesture-enabled"); + int fingers = gala_settings.get_int ("workspaces-gesture-fingers"); + bool natural_scroll = (gesture.performed_on_device_type == DeviceType.TOUCHSCREEN) + ? true + : touchpad_settings.get_boolean ("natural-scroll"); + var direction = natural_scroll ? GestureDirection.RIGHT : GestureDirection.LEFT; + + return enabled + && gesture.type == GestureType.SWIPE + && gesture.direction == direction + && gesture.fingers == fingers; + } +} + + +public Gala.PluginInfo register_plugin () { + return Gala.PluginInfo () { + name = "Touchégg", + author = "José Expósito ", + plugin_type = typeof (Gala.Plugins.Touchegg.Plugin), + provides = Gala.PluginFunction.ADDITION, + load_priority = Gala.LoadPriority.DEFERRED + }; +} diff --git a/plugins/touchegg/meson.build b/plugins/touchegg/meson.build new file mode 100644 index 00000000..4967a296 --- /dev/null +++ b/plugins/touchegg/meson.build @@ -0,0 +1,15 @@ +gala_touchegg_sources = [ + 'Main.vala', + 'Client.vala', + 'Gesture.vala', +] + +gala_touchegg_lib = shared_library( + 'gala-touchegg', + gala_touchegg_sources, + dependencies: [gala_dep, gala_base_dep], + include_directories: config_inc_dir, + install: true, + install_dir: plugins_dir, + install_rpath: mutter_typelib_dir, +) diff --git a/src/GestureAnimationDirector.vala b/src/GestureAnimationDirector.vala new file mode 100644 index 00000000..e4a1260e --- /dev/null +++ b/src/GestureAnimationDirector.vala @@ -0,0 +1,84 @@ +/* + * Copyright 2020 elementary, Inc (https://elementary.io) + * 2020 José Expósito + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +public class Gala.GestureAnimationDirector : Object { + public bool running { get; set; default = false; } + public bool canceling { get; set; default = false; } + + public signal void on_animation_begin (int percentage); + public signal void on_animation_update (int percentage); + public signal void on_animation_end (int percentage, bool cancel_action); + + public delegate void OnBegin (int percentage); + public delegate void OnUpdate (int percentage); + public delegate void OnEnd (int percentage, bool cancel_action); + + private Gee.ArrayList handlers; + + construct { + handlers = new Gee.ArrayList (); + } + + public void connect_handlers (owned OnBegin? on_begin, owned OnUpdate? on_update, owned OnEnd? on_end) { + if (on_begin != null) { + ulong handler_id = on_animation_begin.connect ((percentage) => on_begin (percentage)); + handlers.add (handler_id); + } + + if (on_update != null) { + ulong handler_id = on_animation_update.connect ((percentage) => on_update (percentage)); + handlers.add (handler_id); + } + + if (on_end != null) { + ulong handler_id = on_animation_end.connect ((percentage, cancel_action) => on_end (percentage, cancel_action)); + handlers.add (handler_id); + } + } + + public void disconnect_all_handlers () { + foreach (var handler in handlers) { + disconnect (handler); + } + + handlers.clear (); + } + + public void update_animation (HashTable hints) { + string event = hints.get ("event").get_string (); + int32 percentage = hints.get ("percentage").get_int32 (); + + switch (event) { + case "begin": + on_animation_begin (percentage); + break; + case "update": + on_animation_update (percentage); + break; + case "end": + default: + var cancel_action = hints.get ("cancel_action").get_boolean (); + on_animation_end (percentage, cancel_action); + break; + } + } + + public static float animation_value (float initial_value, float target_value, int percentage) { + return (((target_value - initial_value) * percentage) / 100) + initial_value; + } +} diff --git a/src/Widgets/MonitorClone.vala b/src/Widgets/MonitorClone.vala index c87114ff..daf46d7e 100644 --- a/src/Widgets/MonitorClone.vala +++ b/src/Widgets/MonitorClone.vala @@ -35,17 +35,18 @@ namespace Gala { public Screen screen { get; construct; } #endif public int monitor { get; construct; } + public GestureAnimationDirector gesture_animation_director { get; construct; } WindowCloneContainer window_container; BackgroundManager background; #if HAS_MUTTER330 - public MonitorClone (Meta.Display display, int monitor) { - Object (display: display, monitor: monitor); + public MonitorClone (Meta.Display display, int monitor, GestureAnimationDirector gesture_animation_director) { + Object (display: display, monitor: monitor, gesture_animation_director: gesture_animation_director); } #else - public MonitorClone (Screen screen, int monitor) { - Object (screen: screen, monitor: monitor); + public MonitorClone (Screen screen, int monitor, GestureAnimationDirector gesture_animation_director) { + Object (screen: screen, monitor: monitor, gesture_animation_director: gesture_animation_director); } #endif @@ -59,7 +60,7 @@ namespace Gala { #endif background.set_easing_duration (MultitaskingView.ANIMATION_DURATION); - window_container = new WindowCloneContainer (); + window_container = new WindowCloneContainer (gesture_animation_director); window_container.window_selected.connect ((w) => { window_selected (w); }); #if HAS_MUTTER330 display.restacked.connect (window_container.restack_windows); diff --git a/src/Widgets/MultitaskingView.vala b/src/Widgets/MultitaskingView.vala index fd60c2a7..e4e2eb98 100644 --- a/src/Widgets/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView.vala @@ -27,6 +27,9 @@ namespace Gala { public class MultitaskingView : Actor, ActivatableComponent { public const int ANIMATION_DURATION = 250; public const AnimationMode ANIMATION_MODE = AnimationMode.EASE_OUT_QUAD; + + public GestureAnimationDirector gesture_animation_director { get; construct; } + const int SMOOTH_SCROLL_DELAY = 500; public WindowManager wm { get; construct; } @@ -48,8 +51,8 @@ namespace Gala { Actor workspaces; Actor dock_clones; - public MultitaskingView (WindowManager wm) { - Object (wm: wm); + public MultitaskingView (WindowManager wm, GestureAnimationDirector gesture_animation_director) { + Object (wm: wm, gesture_animation_director: gesture_animation_director); } construct { @@ -63,6 +66,7 @@ namespace Gala { #else screen = wm.get_screen (); #endif + gesture_animation_director = new GestureAnimationDirector (); workspaces = new Actor (); workspaces.set_easing_mode (AnimationMode.EASE_OUT_QUAD); @@ -182,7 +186,7 @@ namespace Gala { if (monitor == primary) continue; - var monitor_clone = new MonitorClone (display, monitor); + var monitor_clone = new MonitorClone (display, monitor, gesture_animation_director); monitor_clone.window_selected.connect (window_selected); monitor_clone.visible = opened; @@ -200,7 +204,7 @@ namespace Gala { if (monitor == primary) continue; - var monitor_clone = new MonitorClone (screen, monitor); + var monitor_clone = new MonitorClone (screen, monitor, gesture_animation_director); monitor_clone.window_selected.connect (window_selected); monitor_clone.visible = opened; @@ -359,9 +363,9 @@ namespace Gala { void add_workspace (int num) { #if HAS_MUTTER330 unowned Meta.WorkspaceManager manager = display.get_workspace_manager (); - var workspace = new WorkspaceClone (manager.get_workspace_by_index (num)); + var workspace = new WorkspaceClone (manager.get_workspace_by_index (num), gesture_animation_director); #else - var workspace = new WorkspaceClone (screen.get_workspace_by_index (num)); + var workspace = new WorkspaceClone (screen.get_workspace_by_index (num), gesture_animation_director); #endif workspace.window_selected.connect (window_selected); workspace.selected.connect (activate_workspace); @@ -544,20 +548,40 @@ namespace Gala { * {@inheritDoc} */ public void open (HashTable? hints = null) { - if (opened) - return; + bool manual_animation = hints != null && hints.get ("manual_animation").get_boolean (); - toggle (); + if (!opened) { + if (manual_animation && !animating) { + debug ("Starting MultitaskingView manual open animation"); + gesture_animation_director.running = true; + } + + toggle (); + } + + if (opened && manual_animation && gesture_animation_director.running) { + gesture_animation_director.update_animation (hints); + } } /** * {@inheritDoc} */ - public void close () { - if (!opened) - return; + public void close (HashTable? hints = null) { + bool manual_animation = hints != null && hints.get ("manual_animation").get_boolean (); - toggle (); + if (opened) { + if (manual_animation && !animating) { + debug ("Starting MultitaskingView manual close animation"); + gesture_animation_director.running = true; + } + + toggle (); + } + + if (!opened && manual_animation && gesture_animation_director.running) { + gesture_animation_director.update_animation (hints); + } } /** @@ -566,8 +590,9 @@ namespace Gala { * to animate to their positions. */ void toggle () { - if (animating) + if (animating) { return; + } animating = true; @@ -578,8 +603,9 @@ namespace Gala { if (opening) { container.visible = true; container.open (); - } else + } else { container.close (); + } } if (opening) { @@ -625,96 +651,167 @@ namespace Gala { foreach (var child in workspaces.get_children ()) { unowned WorkspaceClone workspace = (WorkspaceClone) child; - if (opening) + if (opening) { workspace.open (); - else + } else { workspace.close (); + } } + if (opening) { + show_docks (); + } else { + hide_docks (); + } + + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + var animation_duration = cancel_action ? 0 : ANIMATION_DURATION; + Timeout.add (animation_duration, () => { + if (!opening) { + foreach (var container in window_containers_monitors) { + container.visible = false; + } + + hide (); + + wm.background_group.show (); + wm.window_group.show (); + wm.top_window_group.show (); + + dock_clones.destroy_all_children (); + + wm.pop_modal (modal_proxy); + } + + animating = false; + gesture_animation_director.disconnect_all_handlers (); + gesture_animation_director.running = false; + gesture_animation_director.canceling = cancel_action; + + if (cancel_action) { + toggle (); + } + + return false; + }); + }; + + if (!gesture_animation_director.running) { + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers (null, null, (owned) on_animation_end); + } + } + + void show_docks () { float clone_offset_x, clone_offset_y; dock_clones.get_transformed_position (out clone_offset_x, out clone_offset_y); - if (opening) { #if HAS_MUTTER330 - unowned GLib.List window_actors = display.get_window_actors (); + unowned GLib.List window_actors = display.get_window_actors (); #else - unowned GLib.List window_actors = screen.get_window_actors (); -#endif - foreach (unowned Meta.WindowActor actor in window_actors) { - const int MAX_OFFSET = 100; - - if (actor.is_destroyed ()) - continue; - - unowned Meta.Window window = actor.get_meta_window (); - var monitor = window.get_monitor (); - - if (window.window_type != WindowType.DOCK) - continue; - -#if HAS_MUTTER330 - if (display.get_monitor_in_fullscreen (monitor)) - continue; - - var monitor_geom = display.get_monitor_geometry (monitor); -#else - if (screen.get_monitor_in_fullscreen (monitor)) - continue; - - var monitor_geom = screen.get_monitor_geometry (monitor); + unowned GLib.List window_actors = screen.get_window_actors (); #endif - var window_geom = window.get_frame_rect (); - var top = monitor_geom.y + MAX_OFFSET > window_geom.y; - var bottom = monitor_geom.y + monitor_geom.height - MAX_OFFSET > window_geom.y; + foreach (unowned Meta.WindowActor actor in window_actors) { + const int MAX_OFFSET = 100; - if (!top && !bottom) - continue; + if (actor.is_destroyed ()) + continue; - var clone = new SafeWindowClone (window, true); - clone.set_position (actor.x - clone_offset_x, actor.y - clone_offset_y); - clone.set_easing_duration (ANIMATION_DURATION); + unowned Meta.Window window = actor.get_meta_window (); + var monitor = window.get_monitor (); + + if (window.window_type != WindowType.DOCK) + continue; + +#if HAS_MUTTER330 + if (display.get_monitor_in_fullscreen (monitor)) + continue; + + var monitor_geom = display.get_monitor_geometry (monitor); +#else + if (screen.get_monitor_in_fullscreen (monitor)) + continue; + + var monitor_geom = screen.get_monitor_geometry (monitor); +#endif + + var window_geom = window.get_frame_rect (); + var top = monitor_geom.y + MAX_OFFSET > window_geom.y; + var bottom = monitor_geom.y + monitor_geom.height - MAX_OFFSET > window_geom.y; + + if (!top && !bottom) + continue; + + var initial_x = actor.x - clone_offset_x; + var initial_y = actor.y - clone_offset_y; + var target_y = (top) + ? actor.y - actor.height - clone_offset_y + : actor.y + actor.height - clone_offset_y; + + var clone = new SafeWindowClone (window, true); + dock_clones.add_child (clone); + + GestureAnimationDirector.OnBegin on_animation_begin = () => { + clone.set_position (initial_x, initial_y); + clone.set_easing_mode (0); + }; + + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var y = GestureAnimationDirector.animation_value (initial_y, target_y, percentage); + clone.y = y; + }; + + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { clone.set_easing_mode (ANIMATION_MODE); - dock_clones.add_child (clone); - if (top) - clone.y = actor.y - actor.height - clone_offset_y; - else if (bottom) - clone.y = actor.y + actor.height - clone_offset_y; - } - } else { - foreach (var child in dock_clones.get_children ()) { - var dock = (Clone) child; - - dock.y = dock.source.y - clone_offset_y; - } - } - - if (!opening) { - Timeout.add (ANIMATION_DURATION, () => { - foreach (var container in window_containers_monitors) { - container.visible = false; + if (cancel_action) { + return; } - hide (); + clone.set_easing_duration (gesture_animation_director.canceling ? 0 : ANIMATION_DURATION); + clone.y = target_y; + }; - wm.background_group.show (); - wm.window_group.show (); - wm.top_window_group.show (); + if (!gesture_animation_director.running) { + on_animation_begin (0); + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers ((owned) on_animation_begin, (owned) on_animation_update, (owned) on_animation_end); + } + } + } - dock_clones.destroy_all_children (); + void hide_docks () { + float clone_offset_x, clone_offset_y; + dock_clones.get_transformed_position (out clone_offset_x, out clone_offset_y); - wm.pop_modal (modal_proxy); + foreach (var child in dock_clones.get_children ()) { + var dock = (Clone) child; + var initial_y = dock.y; + var target_y = dock.source.y - clone_offset_y; - animating = false; + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var y = GestureAnimationDirector.animation_value (initial_y, target_y, percentage); + dock.y = y; + }; - return false; - }); - } else { - Timeout.add (ANIMATION_DURATION, () => { - animating = false; - return false; - }); + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + if (cancel_action) { + return; + } + + dock.set_easing_duration (ANIMATION_DURATION); + dock.set_easing_mode (ANIMATION_MODE); + dock.y = target_y; + }; + + if (!gesture_animation_director.running) { + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers (null, (owned) on_animation_update, (owned) on_animation_end); + } } } diff --git a/src/Widgets/WindowClone.vala b/src/Widgets/WindowClone.vala index 877997d3..26665fdc 100644 --- a/src/Widgets/WindowClone.vala +++ b/src/Widgets/WindowClone.vala @@ -92,6 +92,7 @@ namespace Gala { } public bool overview_mode { get; construct; } + public GestureAnimationDirector? gesture_animation_director { get; construct; } [CCode (notify = false)] public uint8 shadow_opacity { @@ -120,8 +121,8 @@ namespace Gala { Actor active_shape; Actor window_icon; - public WindowClone (Meta.Window window, bool overview_mode = false) { - Object (window: window, overview_mode: overview_mode); + public WindowClone (Meta.Window window, GestureAnimationDirector? gesture_animation_director, bool overview_mode = false) { + Object (window: window, gesture_animation_director: gesture_animation_director, overview_mode: overview_mode); } construct { @@ -295,24 +296,67 @@ namespace Gala { var offset_x = monitor_geom.x; var offset_y = monitor_geom.y; - save_easing_state (); - set_easing_mode (MultitaskingView.ANIMATION_MODE); - set_easing_duration (animate ? MultitaskingView.ANIMATION_DURATION : 0); + var initial_x = x; + var initial_y = y; + var initial_width = width; + var initial_height = height; - set_position (outer_rect.x - offset_x, outer_rect.y - offset_y); - set_size (outer_rect.width, outer_rect.height); + var target_x = outer_rect.x - offset_x; + var target_y = outer_rect.y - offset_y; - if (should_fade ()) - opacity = 0; + GestureAnimationDirector.OnBegin on_animation_begin = () => { + window_icon.set_easing_duration (0); + }; - restore_easing_state (); + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var x = GestureAnimationDirector.animation_value (initial_x, target_x, percentage); + var y = GestureAnimationDirector.animation_value (initial_y, target_y, percentage); + var width = GestureAnimationDirector.animation_value (initial_width, outer_rect.width, percentage); + var height = GestureAnimationDirector.animation_value (initial_height, outer_rect.height, percentage); + var opacity = GestureAnimationDirector.animation_value (255f, 0f, percentage); - if (animate) - toggle_shadow (false); + set_size (width, height); + set_position (x, y); + window_icon.opacity = (uint) opacity; + window_icon.set_position ((width - WINDOW_ICON_SIZE) / 2, + height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); + }; - window_icon.set_position ((outer_rect.width - WINDOW_ICON_SIZE) / 2, outer_rect.height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); - window_icon.opacity = 0; - close_button.opacity = 0; + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + window_icon.set_easing_duration (MultitaskingView.ANIMATION_DURATION); + + if (cancel_action) { + return; + } + + save_easing_state (); + set_easing_mode (MultitaskingView.ANIMATION_MODE); + set_easing_duration (animate ? MultitaskingView.ANIMATION_DURATION : 0); + + set_position (target_x, target_y); + set_size (outer_rect.width, outer_rect.height); + + if (should_fade ()) { + opacity = 0; + } + + restore_easing_state (); + + if (animate) { + toggle_shadow (false); + } + + window_icon.set_position ((outer_rect.width - WINDOW_ICON_SIZE) / 2, outer_rect.height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); + window_icon.opacity = 0; + close_button.opacity = 0; + }; + + if (!animate || gesture_animation_director == null || !gesture_animation_director.running) { + on_animation_begin (0); + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers ((owned) on_animation_begin, (owned) on_animation_update, (owned) on_animation_end); + } } /** @@ -320,28 +364,67 @@ namespace Gala { */ public void take_slot (Meta.Rectangle rect) { slot = rect; + var initial_x = x; + var initial_y = y; + var initial_width = width; + var initial_height = height; - save_easing_state (); - set_easing_duration (MultitaskingView.ANIMATION_DURATION); - set_easing_mode (MultitaskingView.ANIMATION_MODE); + GestureAnimationDirector.OnBegin on_animation_begin = () => { + window_icon.opacity = 0; + window_icon.set_easing_duration (0); + }; - set_size (rect.width, rect.height); - set_position (rect.x, rect.y); + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var x = GestureAnimationDirector.animation_value (initial_x, rect.x, percentage); + var y = GestureAnimationDirector.animation_value (initial_y, rect.y, percentage); + var width = GestureAnimationDirector.animation_value (initial_width, rect.width, percentage); + var height = GestureAnimationDirector.animation_value (initial_height, rect.height, percentage); + var opacity = GestureAnimationDirector.animation_value (0f, 255f, percentage); - window_icon.opacity = 255; - window_icon.set_position ((rect.width - WINDOW_ICON_SIZE) / 2, rect.height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); + set_size (width, height); + set_position (x, y); + window_icon.opacity = (uint) opacity; + window_icon.set_position ((width - WINDOW_ICON_SIZE) / 2, + height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); + }; - restore_easing_state (); + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + window_icon.set_easing_duration (MultitaskingView.ANIMATION_DURATION); - toggle_shadow (true); + if (cancel_action) { + return; + } - if (opacity < 255) { save_easing_state (); - set_easing_mode (AnimationMode.EASE_OUT_QUAD); - set_easing_duration (300); + set_easing_duration (MultitaskingView.ANIMATION_DURATION); + set_easing_mode (MultitaskingView.ANIMATION_MODE); + + set_size (rect.width, rect.height); + set_position (rect.x, rect.y); + + window_icon.opacity = 255; + window_icon.set_position ((rect.width - WINDOW_ICON_SIZE) / 2, + rect.height - (WINDOW_ICON_SIZE * scale_factor) * 0.75f); - opacity = 255; restore_easing_state (); + + toggle_shadow (true); + + if (opacity < 255) { + save_easing_state (); + set_easing_mode (AnimationMode.EASE_OUT_QUAD); + set_easing_duration (300); + + opacity = 255; + restore_easing_state (); + } + }; + + if (gesture_animation_director == null || !gesture_animation_director.running) { + on_animation_begin (0); + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers ((owned) on_animation_begin, (owned) on_animation_update, (owned) on_animation_end); } } diff --git a/src/Widgets/WindowCloneContainer.vala b/src/Widgets/WindowCloneContainer.vala index fac127f3..9ac0e775 100644 --- a/src/Widgets/WindowCloneContainer.vala +++ b/src/Widgets/WindowCloneContainer.vala @@ -30,6 +30,7 @@ namespace Gala { public int padding_right { get; set; default = 12; } public int padding_bottom { get; set; default = 12; } + public GestureAnimationDirector? gesture_animation_director { get; construct; } public bool overview_mode { get; construct; } bool opened; @@ -40,8 +41,8 @@ namespace Gala { */ WindowClone? current_window; - public WindowCloneContainer (bool overview_mode = false) { - Object (overview_mode: overview_mode); + public WindowCloneContainer (GestureAnimationDirector? gesture_animation_director, bool overview_mode = false) { + Object (gesture_animation_director: gesture_animation_director, overview_mode: overview_mode); } construct { @@ -68,7 +69,7 @@ namespace Gala { var windows_ordered = display.sort_windows_by_stacking (windows); - var new_window = new WindowClone (window, overview_mode); + var new_window = new WindowClone (window, gesture_animation_director, overview_mode); new_window.selected.connect (window_selected_cb); new_window.destroy.connect (window_destroyed); @@ -337,8 +338,9 @@ namespace Gala { * When opened the WindowClones are animated to a tiled layout */ public void open (Window? selected_window = null) { - if (opened) + if (opened) { return; + } opened = true; @@ -361,8 +363,11 @@ namespace Gala { // make sure our windows are where they belong in case they were moved // while were closed. - foreach (var window in get_children ()) - ((WindowClone) window).transition_to_original_state (false); + if (gesture_animation_director == null || !gesture_animation_director.canceling) { + foreach (var window in get_children ()) { + ((WindowClone) window).transition_to_original_state (false); + } + } reflow (); } @@ -372,13 +377,15 @@ namespace Gala { * to make them take their original locations again. */ public void close () { - if (!opened) + if (!opened) { return; + } opened = false; - foreach (var window in get_children ()) + foreach (var window in get_children ()) { ((WindowClone) window).transition_to_original_state (true); + } } } } diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 0e31e4c2..c03d52f2 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -54,12 +54,12 @@ namespace Gala { #if HAS_MUTTER330 display = wm.get_display (); - display.get_workspace_manager ().workspace_switched.connect (close); + display.get_workspace_manager ().workspace_switched.connect (() => { close (); }); display.restacked.connect (restack_windows); #else screen = wm.get_screen (); - screen.workspace_switched.connect (close); + screen.workspace_switched.connect (() => { close (); }); screen.restacked.connect (restack_windows); #endif @@ -203,7 +203,7 @@ namespace Gala { var geometry = screen.get_monitor_geometry (i); #endif - var container = new WindowCloneContainer (true) { + var container = new WindowCloneContainer (null, true) { padding_top = TOP_GAP, padding_left = BORDER, padding_right = BORDER, @@ -322,7 +322,7 @@ namespace Gala { /** * {@inheritDoc} */ - public void close () { + public void close (HashTable? hints = null) { if (!visible || !ready) return; diff --git a/src/Widgets/WorkspaceClone.vala b/src/Widgets/WorkspaceClone.vala index c429aef8..fed83f16 100644 --- a/src/Widgets/WorkspaceClone.vala +++ b/src/Widgets/WorkspaceClone.vala @@ -143,6 +143,7 @@ namespace Gala { public signal void selected (bool close_view); public Workspace workspace { get; construct; } + public GestureAnimationDirector gesture_animation_director { get; construct; } public IconGroup icon_group { get; private set; } public WindowCloneContainer window_container { get; private set; } @@ -166,8 +167,8 @@ namespace Gala { uint hover_activate_timeout = 0; - public WorkspaceClone (Workspace workspace) { - Object (workspace: workspace); + public WorkspaceClone (Workspace workspace, GestureAnimationDirector gesture_animation_director) { + Object (workspace: workspace, gesture_animation_director: gesture_animation_director); } construct { @@ -192,7 +193,7 @@ namespace Gala { return false; }); - window_container = new WindowCloneContainer (); + window_container = new WindowCloneContainer (gesture_animation_director); window_container.window_selected.connect ((w) => { window_selected (w); }); window_container.set_size (monitor_geometry.width, monitor_geometry.height); #if HAS_MUTTER330 @@ -369,8 +370,9 @@ namespace Gala { * if it belongs to this workspace. */ public void open () { - if (opened) + if (opened) { return; + } opened = true; @@ -390,13 +392,33 @@ namespace Gala { update_size (monitor); - background.set_pivot_point (0.5f, pivotY); + GestureAnimationDirector.OnBegin on_animation_begin = () => { + background.set_pivot_point (0.5f, pivotY); + }; - background.save_easing_state (); - background.set_easing_duration (MultitaskingView.ANIMATION_DURATION); - background.set_easing_mode (MultitaskingView.ANIMATION_MODE); - background.set_scale (scale, scale); - background.restore_easing_state (); + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + double update_scale = (double)GestureAnimationDirector.animation_value (1.0f, (float)scale, percentage); + background.set_scale (update_scale, update_scale); + }; + + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + if (cancel_action) { + return; + } + + background.save_easing_state (); + background.set_easing_duration (MultitaskingView.ANIMATION_DURATION); + background.set_easing_mode (MultitaskingView.ANIMATION_MODE); + background.set_scale (scale, scale); + background.restore_easing_state (); + }; + + if (!gesture_animation_director.running) { + on_animation_begin (0); + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers ((owned) on_animation_begin, (owned) on_animation_update, (owned)on_animation_end); + } Meta.Rectangle area = { (int)Math.floorf (monitor.x + monitor.width - monitor.width * scale) / 2, @@ -425,16 +447,38 @@ namespace Gala { * the windows back to their old locations. */ public void close () { - if (!opened) + if (!opened) { return; + } opened = false; - background.save_easing_state (); - background.set_easing_duration (MultitaskingView.ANIMATION_DURATION); - background.set_easing_mode (MultitaskingView.ANIMATION_MODE); - background.set_scale (1, 1); - background.restore_easing_state (); + double initial_scale_x, initial_scale_y; + background.get_scale (out initial_scale_x, out initial_scale_y); + + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + double scale_x = (double) GestureAnimationDirector.animation_value ((float) initial_scale_x, 1.0f, percentage); + double scale_y = (double) GestureAnimationDirector.animation_value ((float) initial_scale_y, 1.0f, percentage); + background.set_scale (scale_x, scale_y); + }; + + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + if (cancel_action) { + return; + } + + background.save_easing_state (); + background.set_easing_duration (MultitaskingView.ANIMATION_DURATION); + background.set_easing_mode (MultitaskingView.ANIMATION_MODE); + background.set_scale (1, 1); + background.restore_easing_state (); + }; + + if (!gesture_animation_director.running) { + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers (null, (owned) on_animation_update, (owned) on_animation_end); + } window_container.close (); } diff --git a/src/WindowManager.vala b/src/WindowManager.vala index d8ddad40..9290001d 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -51,6 +51,11 @@ namespace Gala { */ public Meta.BackgroundGroup background_group { get; protected set; } + /** + * {@inheritDoc} + */ + public Gala.ActivatableComponent workspace_view { get; protected set; } + /** * {@inheritDoc} */ @@ -65,7 +70,6 @@ namespace Gala { Meta.PluginInfo info; WindowSwitcher? winswitcher = null; - ActivatableComponent? workspace_view = null; ActivatableComponent? window_overview = null; // used to detect which corner was used to trigger an action @@ -95,7 +99,12 @@ namespace Gala { private GLib.Settings animations_settings; private GLib.Settings behavior_settings; + private bool animating_switch_workspace = false; + private GestureAnimationDirector gesture_animation_director; + construct { + gesture_animation_director = new GestureAnimationDirector (); + info = Meta.PluginInfo () {name = "Gala", version = Config.VERSION, author = "Gala Developers", license = "GPLv3", description = "A nice elementary window manager"}; @@ -331,7 +340,7 @@ namespace Gala { if (plugin_manager.workspace_view_provider == null || (workspace_view = (plugin_manager.get_plugin (plugin_manager.workspace_view_provider) as ActivatableComponent)) == null) { - workspace_view = new MultitaskingView (this); + workspace_view = new MultitaskingView (this, gesture_animation_director); ui_group.add_child ((Clutter.Actor) workspace_view); } @@ -573,7 +582,23 @@ namespace Gala { /** * {@inheritDoc} */ - public void switch_to_next_workspace (Meta.MotionDirection direction) { + public void switch_to_next_workspace (Meta.MotionDirection direction, HashTable? hints = null) { + if (animating_switch_workspace) { + return; + } + + bool manual_animation = hints != null && hints.get ("manual_animation").get_boolean (); + if (manual_animation) { + string event = hints.get ("event").get_string (); + + if (event == "begin") { + gesture_animation_director.running = true; + } else { + gesture_animation_director.update_animation (hints); + return; + } + } + #if HAS_MUTTER330 unowned Meta.Display display = get_display (); var active_workspace = display.get_workspace_manager ().get_active_workspace (); @@ -586,29 +611,57 @@ namespace Gala { if (neighbor != active_workspace) { neighbor.activate (display.get_current_time ()); - return; + if (manual_animation) { + gesture_animation_director.update_animation (hints); + } + } else { + // if we didnt switch, show a nudge-over animation if one is not already in progress + play_nudge_animation (direction); } + } - // if we didnt switch, show a nudge-over animation if one is not already in progress - if (ui_group.get_transition ("nudge") != null) - return; - + private void play_nudge_animation (Meta.MotionDirection direction) { + int duration = 360; var dest = (direction == Meta.MotionDirection.LEFT ? 32.0f : -32.0f); - double[] keyframes = { 0.5 }; - GLib.Value[] x = { dest }; - - var nudge = new Clutter.KeyframeTransition ("translation-x") { - duration = 360, - remove_on_complete = true, - progress_mode = Clutter.AnimationMode.EASE_IN_QUAD + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var x = GestureAnimationDirector.animation_value (0.0f, dest, percentage); + ui_group.x = x; }; - nudge.set_from_value (0.0f); - nudge.set_to_value (0.0f); - nudge.set_key_frames (keyframes); - nudge.set_values (x); - ui_group.add_transition ("nudge", nudge); + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + var nudge_gesture = new Clutter.PropertyTransition ("x") { + duration = (duration / 2), + remove_on_complete = true, + progress_mode = Clutter.AnimationMode.LINEAR + }; + nudge_gesture.set_from_value ((float) ui_group.x); + nudge_gesture.set_to_value (0.0f); + ui_group.add_transition ("nudge", nudge_gesture); + + gesture_animation_director.disconnect_all_handlers (); + gesture_animation_director.running = false; + gesture_animation_director.canceling = false; + }; + + if (!gesture_animation_director.running) { + double[] keyframes = { 0.5 }; + GLib.Value[] x = { dest }; + + var nudge = new Clutter.KeyframeTransition ("translation-x") { + duration = duration, + remove_on_complete = true, + progress_mode = Clutter.AnimationMode.EASE_IN_QUAD + }; + nudge.set_from_value (0.0f); + nudge.set_to_value (0.0f); + nudge.set_key_frames (keyframes); + nudge.set_values (x); + + ui_group.add_transition ("nudge", nudge); + } else { + gesture_animation_director.connect_handlers (null, (owned) on_animation_update, (owned) on_animation_end); + } } #if HAS_MUTTER330 @@ -1872,11 +1925,11 @@ namespace Gala { List? tmp_actors; public override void switch_workspace (int from, int to, Meta.MotionDirection direction) { - const int animation_duration = AnimationDuration.WORKSPACE_SWITCH; - if (!enable_animations - || animation_duration == 0 - || (direction != Meta.MotionDirection.LEFT && direction != Meta.MotionDirection.RIGHT)) { + || AnimationDuration.WORKSPACE_SWITCH == 0 + || (direction != Meta.MotionDirection.LEFT && direction != Meta.MotionDirection.RIGHT) + || gesture_animation_director.canceling) { + gesture_animation_director.canceling = false; switch_workspace_completed (); return; } @@ -2075,29 +2128,69 @@ namespace Gala { var animation_mode = Clutter.AnimationMode.EASE_OUT_CUBIC; - out_group.set_easing_mode (animation_mode); - out_group.set_easing_duration (animation_duration); - in_group.set_easing_mode (animation_mode); - in_group.set_easing_duration (animation_duration); - wallpaper_clone.set_easing_mode (animation_mode); - wallpaper_clone.set_easing_duration (animation_duration); + GestureAnimationDirector.OnUpdate on_animation_update = (percentage) => { + var x_out = GestureAnimationDirector.animation_value (0.1f, x2, percentage); + var x_in = GestureAnimationDirector.animation_value (-x2, 0.1f, percentage); - wallpaper.save_easing_state (); - wallpaper.set_easing_mode (animation_mode); - wallpaper.set_easing_duration (animation_duration); + out_group.x = x_out; + in_group.x = x_in; - out_group.x = x2; - in_group.x = 0.0f; + wallpaper.x = x_out; + wallpaper_clone.x = x_in; + }; - wallpaper.x = x2; - wallpaper_clone.x = 0.0f; - wallpaper.restore_easing_state (); + GestureAnimationDirector.OnEnd on_animation_end = (percentage, cancel_action) => { + animating_switch_workspace = true; - var transition = in_group.get_transition ("x"); - if (transition != null) - transition.completed.connect (end_switch_workspace); - else - end_switch_workspace (); + out_group.set_easing_mode (animation_mode); + out_group.set_easing_duration (AnimationDuration.WORKSPACE_SWITCH); + in_group.set_easing_mode (animation_mode); + in_group.set_easing_duration (AnimationDuration.WORKSPACE_SWITCH); + wallpaper_clone.set_easing_mode (animation_mode); + wallpaper_clone.set_easing_duration (AnimationDuration.WORKSPACE_SWITCH); + + wallpaper.save_easing_state (); + wallpaper.set_easing_mode (animation_mode); + wallpaper.set_easing_duration (AnimationDuration.WORKSPACE_SWITCH); + + out_group.x = cancel_action ? 0.0f : x2; + in_group.x = cancel_action ? -x2 : 0.0f; + + wallpaper.x = cancel_action ? 0.0f : x2; + wallpaper_clone.x = cancel_action ? -x2 : 0.0f; + wallpaper.restore_easing_state (); + + var transition = in_group.get_transition ("x"); + if (transition != null) { + transition.completed.connect (() => { + switch_workspace_animation_finished (direction, cancel_action); + }); + } else { + switch_workspace_animation_finished (direction, cancel_action); + } + }; + + if (!gesture_animation_director.running) { + on_animation_end (100, false); + } else { + gesture_animation_director.connect_handlers (null, (owned) on_animation_update, (owned) on_animation_end); + } + } + + private void switch_workspace_animation_finished (Meta.MotionDirection animation_direction, + bool cancel_action) { + end_switch_workspace (); + gesture_animation_director.disconnect_all_handlers (); + gesture_animation_director.running = false; + gesture_animation_director.canceling = cancel_action; + animating_switch_workspace = false; + + if (cancel_action) { + var cancel_direction = (animation_direction == Meta.MotionDirection.LEFT) + ? Meta.MotionDirection.RIGHT + : Meta.MotionDirection.LEFT; + switch_to_next_workspace (cancel_direction); + } } void end_switch_workspace () { diff --git a/src/meson.build b/src/meson.build index 7540d1a3..4b930963 100644 --- a/src/meson.build +++ b/src/meson.build @@ -2,6 +2,7 @@ gala_bin_sources = files( 'DBus.vala', 'DBusAccelerator.vala', 'DockThemeManager.vala', + 'GestureAnimationDirector.vala', 'InternalUtils.vala', 'KeyboardManager.vala', 'NotificationStack.vala',