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',