diff --git a/src/Gestures/ActorTarget.vala b/src/Gestures/ActorTarget.vala new file mode 100644 index 000000000..988857a82 --- /dev/null +++ b/src/Gestures/ActorTarget.vala @@ -0,0 +1,92 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * An {@link GestureTarget} implementation that derives from a {@link Clutter.Actor}. + * It will propagate gesture events to all direct descendants that are also {@link ActorTarget}s. + * If a new child (or target via {@link add_target}) is added, its progress will be synced. + */ +public class Gala.ActorTarget : Clutter.Actor, GestureTarget { + public Clutter.Actor? actor { + get { + return this; + } + } + + private HashTable current_progress; + private Gee.List targets; + + construct { + current_progress = new HashTable (str_hash, str_equal); + targets = new Gee.ArrayList (); + + child_added.connect (on_child_added); + } + + private void sync_target (GestureTarget target) { + foreach (var id in current_progress.get_keys ()) { + target.propagate (UPDATE, id, current_progress[id]); + } + } + + public void add_target (GestureTarget target) { + targets.add (target); + sync_target (target); + } + + public void remove_target (GestureTarget target) { + targets.remove (target); + } + + public void remove_all_targets () { + targets.clear (); + } + + public double get_current_progress (string id) { + return current_progress[id] ?? 0; + } + + public virtual void start_progress (string id) {} + public virtual void update_progress (string id, double progress) {} + public virtual void commit_progress (string id, double to) {} + public virtual void end_progress (string id) {} + + public override void propagate (UpdateType update_type, string id, double progress) { + current_progress[id] = progress; + + switch (update_type) { + case START: + start_progress (id); + break; + case UPDATE: + update_progress (id, progress); + break; + case COMMIT: + commit_progress (id, progress); + break; + case END: + end_progress (id); + break; + } + + foreach (var target in targets) { + target.propagate (update_type, id, progress); + } + + for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) { + if (child is ActorTarget) { + child.propagate (update_type, id, progress); + } + } + } + + private void on_child_added (Clutter.Actor child) { + if (child is ActorTarget) { + sync_target ((GestureTarget) child); + } + } +} diff --git a/src/Gestures/Gesture.vala b/src/Gestures/Gesture.vala index 5ffd2831d..4826e0133 100644 --- a/src/Gestures/Gesture.vala +++ b/src/Gestures/Gesture.vala @@ -34,6 +34,14 @@ namespace Gala { OUT = 6, } + public enum GestureAction { + NONE, + SWITCH_WORKSPACE, + MOVE_TO_WORKSPACE, + SWITCH_WINDOWS, + MULTITASKING_VIEW + } + public class Gesture { public const float INVALID_COORD = float.MAX; diff --git a/src/Gestures/GestureController.vala b/src/Gestures/GestureController.vala new file mode 100644 index 000000000..69c51dcbe --- /dev/null +++ b/src/Gestures/GestureController.vala @@ -0,0 +1,270 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public interface Gala.GestureTarget : Object { + public enum UpdateType { + START, + UPDATE, + COMMIT, + END + } + + /** + * The actor manipulated by the gesture. The associated frame clock + * will be used for animation timelines. + */ + public abstract Clutter.Actor? actor { get; } + + public virtual void propagate (UpdateType update_type, string id, double progress) { } +} + +/** + * The class responsible for handling gestures and updating the target. It has a persistent + * double progress that is either updated by a gesture that is configured with the given + * {@link GestureAction} from various backends (see the enable_* methods) or manually + * by calling {@link goto} or setting {@link progress} directly. + * You shouldn't connect a notify to the progress directly though, but rather use a + * {@link GestureTarget} implementation. + * The {@link progress} can be seen as representing the state that the UI the gesture affects + * is currently in (e.g. 0 for multitasking view closed, 1 for it opend, or 0 for first workspace, + * -1 for second, -2 for third, etc.). Therefore the progress often needs boundaries which can be + * set with {@link overshoot_lower_clamp} and {@link overshoot_upper_clamp}. If the values are integers + * it will be a hard boundary, if they are fractional it will slow the gesture progress when over the + * limit simulating a kind of spring that pushes against it. + * Note that the progress snaps to full integer values after a gesture ends. + */ +public class Gala.GestureController : Object { + /** + * When a gesture ends with a velocity greater than this constant, the action is not cancelled, + * even if the animation threshold has not been reached. + */ + private const double SUCCESS_VELOCITY_THRESHOLD = 0.003; + + /** + * Maximum velocity allowed on gesture update. + */ + private const double MAX_VELOCITY = 0.01; + + public string id { get; construct; } + public GestureAction action { get; construct; } + + private GestureTarget? _target; + public GestureTarget target { + get { return _target; } + set { + _target = value; + target.propagate (UPDATE, id, calculate_bounded_progress ()); + } + } + + public double distance { get; construct set; } + public double overshoot_lower_clamp { get; construct set; default = 0d; } + public double overshoot_upper_clamp { get; construct set; default = 1d; } + + private double _progress = 0; + public double progress { + get { return _progress; } + set { + _progress = value; + target.propagate (UPDATE, id, calculate_bounded_progress ()); + } + } + + private bool _enabled = true; + public bool enabled { + get { return _enabled; } + set { + cancel_gesture (); + _enabled = value; + } + } + + public bool recognizing { get; private set; } + + private ToucheggBackend? touchpad_backend; + private ScrollBackend? scroll_backend; + + private GestureBackend? recognizing_backend; + private double previous_percentage; + private uint64 previous_time; + private double previous_delta; + private double velocity; + private int direction_multiplier; + + private Clutter.Timeline? timeline; + + public GestureController (string id, GestureAction action, GestureTarget target) { + Object (id: id, action: action, target: target); + } + + private double calculate_bounded_progress () { + var lower_clamp_int = (int) overshoot_lower_clamp; + var upper_clamp_int = (int) overshoot_upper_clamp; + + double stretched_percentage = 0; + if (_progress < lower_clamp_int) { + stretched_percentage = (_progress - lower_clamp_int) * - (overshoot_lower_clamp - lower_clamp_int); + } else if (_progress > upper_clamp_int) { + stretched_percentage = (_progress - upper_clamp_int) * (overshoot_upper_clamp - upper_clamp_int); + } + + var clamped = _progress.clamp (lower_clamp_int, upper_clamp_int); + + return clamped + stretched_percentage; + } + + public void enable_touchpad () { + touchpad_backend = ToucheggBackend.get_default (); + touchpad_backend.on_gesture_detected.connect (gesture_detected); + touchpad_backend.on_begin.connect (gesture_begin); + touchpad_backend.on_update.connect (gesture_update); + touchpad_backend.on_end.connect (gesture_end); + } + + public void enable_scroll (Clutter.Actor actor, Clutter.Orientation orientation) { + scroll_backend = new ScrollBackend (actor, orientation, new GestureSettings ()); + scroll_backend.on_gesture_detected.connect (gesture_detected); + scroll_backend.on_begin.connect (gesture_begin); + scroll_backend.on_update.connect (gesture_update); + scroll_backend.on_end.connect (gesture_end); + } + + private void prepare () { + if (timeline != null) { + timeline.stop (); + timeline = null; + } + + target.propagate (START, id, calculate_bounded_progress ()); + } + + private bool gesture_detected (GestureBackend backend, Gesture gesture, uint32 timestamp) { + recognizing = enabled && (GestureSettings.get_action (gesture) == action + || backend == scroll_backend && GestureSettings.get_action (gesture) == NONE); + + if (recognizing) { + if (gesture.direction == UP || gesture.direction == RIGHT) { + direction_multiplier = 1; + } else { + direction_multiplier = -1; + } + + recognizing_backend = backend; + } + + return recognizing; + } + + private void gesture_begin (double percentage, uint64 elapsed_time) { + if (!recognizing) { + return; + } + + prepare (); + + previous_percentage = percentage; + previous_time = elapsed_time; + } + + private void gesture_update (double percentage, uint64 elapsed_time) { + if (!recognizing) { + return; + } + + var updated_delta = previous_delta; + if (elapsed_time != previous_time) { + double distance = percentage - previous_percentage; + double time = (double)(elapsed_time - previous_time); + velocity = (distance / time); + + if (velocity > MAX_VELOCITY) { + velocity = MAX_VELOCITY; + var used_percentage = MAX_VELOCITY * time + previous_percentage; + updated_delta += percentage - used_percentage; + } + } + + progress += calculate_applied_delta (percentage, updated_delta); + + previous_percentage = percentage; + previous_time = elapsed_time; + previous_delta = updated_delta; + } + + private void gesture_end (double percentage, uint64 elapsed_time) { + if (!recognizing) { + return; + } + + progress += calculate_applied_delta (percentage, previous_delta); + + int completions = (int) Math.round (progress); + + if (velocity.abs () > SUCCESS_VELOCITY_THRESHOLD) { + completions += velocity > 0 ? direction_multiplier : -direction_multiplier; + } + + var lower_clamp_int = (int) overshoot_lower_clamp; + var upper_clamp_int = (int) overshoot_upper_clamp; + + completions = completions.clamp (lower_clamp_int, upper_clamp_int); + + recognizing = false; + + finish (velocity, (double) completions); + + previous_percentage = 0; + previous_time = 0; + previous_delta = 0; + velocity = 0; + direction_multiplier = 0; + } + + private inline double calculate_applied_delta (double percentage, double percentage_delta) { + return ((percentage - percentage_delta) - (previous_percentage - previous_delta)) * direction_multiplier; + } + + private void finish (double velocity, double to) { + target.propagate (COMMIT, id, to); + + if (progress == to) { + target.propagate (END, id, calculate_bounded_progress ()); + return; + } + + var spring = new SpringTimeline (target.actor, progress, to, velocity, 1, 0.5, 500); + spring.progress.connect ((value) => progress = value); + spring.stopped.connect (() => { + target.propagate (END, id, calculate_bounded_progress ()); + timeline = null; + }); + + timeline = spring; + } + + /** + * Animates to the given progress value. + * If the gesture is currently recognizing, it will do nothing. + * If that's not what you want, you should call {@link cancel_gesture} first. + * If you don't want animation but an immediate jump, you should set {@link progress} directly. + */ + public void goto (double to) { + if (progress == to || recognizing) { + return; + } + + prepare (); + finish (0.005, to); + } + + public void cancel_gesture () { + if (recognizing) { + recognizing_backend.cancel_gesture (); + gesture_end (previous_percentage, previous_time); + } + } +} diff --git a/src/Gestures/GestureSettings.vala b/src/Gestures/GestureSettings.vala index 4dd6da0bb..5fc436288 100644 --- a/src/Gestures/GestureSettings.vala +++ b/src/Gestures/GestureSettings.vala @@ -20,14 +20,6 @@ * Utility class to access the gesture settings. Easily accessible through GestureTracker.settings. */ public class Gala.GestureSettings : Object { - public enum GestureAction { - NONE, - SWITCH_WORKSPACE, - MOVE_TO_WORKSPACE, - SWITCH_WINDOWS, - MULTITASKING_VIEW - } - private static GLib.Settings gala_settings; private static GLib.Settings touchpad_settings; diff --git a/src/Gestures/GestureTracker.vala b/src/Gestures/GestureTracker.vala index 2a92b67d4..7bb948170 100644 --- a/src/Gestures/GestureTracker.vala +++ b/src/Gestures/GestureTracker.vala @@ -23,6 +23,14 @@ public interface Gala.GestureBackend : Object { public signal void on_end (double delta, uint64 time); public virtual void prepare_gesture_handling () { } + + /** + * The gesture should be cancelled. The implementation should stop emitting + * signals and reset any internal state. In particular it should not emit on_end. + * The implementation has to make sure that any further events from the same gesture will + * will be ignored. Once the gesture ends a new gesture should be treated as usual. + */ + public virtual void cancel_gesture () { } } /** diff --git a/src/Gestures/PropertyTarget.vala b/src/Gestures/PropertyTarget.vala new file mode 100644 index 000000000..c76ef1995 --- /dev/null +++ b/src/Gestures/PropertyTarget.vala @@ -0,0 +1,33 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.PropertyTarget : Object, GestureTarget { + public string id { get; construct; } + + //we don't want to hold a strong reference to the actor because we might've been added to it which would form a reference cycle + private weak Clutter.Actor? _actor; + public Clutter.Actor? actor { get { return _actor; } } + + public string property { get; construct; } + + public Clutter.Interval interval { get; construct; } + + public PropertyTarget (string id, Clutter.Actor actor, string property, Type value_type, Value from_value, Value to_value) { + Object (id: id, property: property, interval: new Clutter.Interval.with_values (value_type, from_value, to_value)); + + _actor = actor; + _actor.destroy.connect (() => _actor = null); + } + + public override void propagate (UpdateType update_type, string id, double progress) { + if (update_type != UPDATE || id != this.id) { + return; + } + + actor.set_property (property, interval.compute (progress)); + } +} diff --git a/src/Gestures/ScrollBackend.vala b/src/Gestures/ScrollBackend.vala index d833264cb..54f5f9f54 100644 --- a/src/Gestures/ScrollBackend.vala +++ b/src/Gestures/ScrollBackend.vala @@ -35,6 +35,8 @@ public class Gala.ScrollBackend : Object, GestureBackend { private double delta_y; private GestureDirection direction; + private bool ignoring = false; + construct { started = false; delta_x = 0; @@ -46,6 +48,7 @@ public class Gala.ScrollBackend : Object, GestureBackend { Object (actor: actor, orientation: orientation, settings: settings); actor.scroll_event.connect (on_scroll_event); + actor.notify["visible"].connect (() => ignoring = false); } #if HAS_MUTTER45 @@ -57,6 +60,13 @@ public class Gala.ScrollBackend : Object, GestureBackend { return false; } + if (ignoring) { + if (event.get_scroll_finish_flags () != NONE) { + ignoring = false; + } + return false; + } + var time = event.get_time (); double x, y; event.get_scroll_delta (out x, out y); @@ -88,11 +98,8 @@ public class Gala.ScrollBackend : Object, GestureBackend { } else { double delta = calculate_delta (delta_x, delta_y, direction); if (x == 0 && y == 0) { - started = false; - delta_x = 0; - delta_y = 0; - direction = GestureDirection.UNKNOWN; on_end (delta, time); + reset (); } else { on_update (delta, time); } @@ -111,6 +118,20 @@ public class Gala.ScrollBackend : Object, GestureBackend { && event.get_scroll_direction () == Clutter.ScrollDirection.SMOOTH; } + private void reset () { + started = false; + delta_x = 0; + delta_y = 0; + direction = GestureDirection.UNKNOWN; + } + + public override void cancel_gesture () { + if (started) { + ignoring = true; + reset (); + } + } + private static Gesture build_gesture (float origin_x, float origin_y, double delta_x, double delta_y, Clutter.Orientation orientation, uint32 timestamp) { GestureDirection direction; if (orientation == Clutter.Orientation.HORIZONTAL) { diff --git a/src/Gestures/SpringTimeline.vala b/src/Gestures/SpringTimeline.vala new file mode 100644 index 000000000..5fc04dea0 --- /dev/null +++ b/src/Gestures/SpringTimeline.vala @@ -0,0 +1,203 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +/** + * Based on libadwaita's AdwSpringAnimation + * [[https://gitlab.gnome.org/GNOME/libadwaita/-/blob/main/src/adw-spring-animation.c?ref_type=heads]] + * + * For documentation about the params refer to [[https://valadoc.org/libadwaita-1/Adw.SpringAnimation.html]] + * and [[https://valadoc.org/libadwaita-1/Adw.SpringParams.html]]. + */ +public class Gala.SpringTimeline : Clutter.Timeline { + private const double DELTA = 0.001; + + public signal void progress (double value); + + public double value_from { get; construct; } + public double value_to { get; construct; } + public double initial_velocity { get; construct; } + + public double damping { get; construct; } + public double mass { get; construct; } + public double stiffness { get; construct; } + + public double epsilon { get; construct; default = 0.0001; } + + public bool clamp { get; construct; } + + public SpringTimeline (Clutter.Actor actor, double value_from, double value_to, double initial_velocity, double damping_ratio, double mass, double stiffness) { + var critical_damping = 2 * Math.sqrt (mass * stiffness); + + Object ( + actor: actor, + value_from: value_from, + value_to: value_to, + initial_velocity: initial_velocity, + damping: critical_damping * damping_ratio, + mass: mass, + stiffness: stiffness + ); + + duration = calculate_duration (); + + start (); + } + + private bool approx (double a, double b, double epsilon) { + return (a - b).abs () < epsilon || a == b; + } + + private double oscillate (uint time, out double? velocity) { + double b = damping; + double m = mass; + double k = stiffness; + double v0 = initial_velocity; + + double t = time / 1000.0; + + double beta = b / (2 * m); + double omega0 = Math.sqrt (k / m); + + double x0 = value_from - value_to; + + double envelope = Math.exp (-beta * t); + + /* + * Solutions of the form C1*e^(lambda1*x) + C2*e^(lambda2*x) + * for the differential equation m*ẍ+b*ẋ+kx = 0 + */ + + /* Critically damped */ + /* double.EPSILON is too small for this specific comparison, so we use + * FLT_EPSILON even though it's doubles */ + if (approx (beta, omega0, float.EPSILON)) { + velocity = envelope * (-beta * t * v0 - beta * beta * t * x0 + v0); + + return value_to + envelope * (x0 + (beta * x0 + v0) * t); + } + + /* Underdamped */ + if (beta < omega0) { + double omega1 = Math.sqrt ((omega0 * omega0) - (beta * beta)); + + velocity = envelope * (v0 * Math.cos (omega1 * t) - (x0 * omega1 + (beta * beta * x0 + beta * v0) / (omega1)) * Math.sin (omega1 * t)); + + return value_to + envelope * (x0 * Math.cos (omega1 * t) + ((beta * x0 + v0) / omega1) * Math.sin (omega1 * t)); + } + + /* Overdamped */ + if (beta > omega0) { + double omega2 = Math.sqrt ((beta * beta) - (omega0 * omega0)); + + velocity = envelope * (v0 * Math.cosh (omega2 * t) + (omega2 * x0 - (beta * beta * x0 + beta * v0) / omega2) * Math.sinh (omega2 * t)); + + return value_to + envelope * (x0 * Math.cosh (omega2 * t) + ((beta * x0 + v0) / omega2) * Math.sinh (omega2 * t)); + } + + assert_not_reached (); + } + + private const int MAX_ITERATIONS = 20000; + private uint get_first_zero () { + /* The first frame is not that important and we avoid finding the trivial 0 + * for in-place animations. */ + uint i = 1; + double y = oscillate (i, null); + + while ((value_to - value_from > double.EPSILON && value_to - y > epsilon) || + (value_from - value_to > double.EPSILON && y - value_to > epsilon) + ) { + if (i > MAX_ITERATIONS) { + return 0; + } + + y = oscillate (++i, null); + } + + return i; + } + + private uint calculate_duration () { + double beta = damping / (2 * mass); + double omega0; + double x0, y0; + double x1, y1; + double m; + + int i = 0; + + if (approx (beta, 0, double.EPSILON) || beta < 0) { + warning ("INFINITE"); + return -1; + } + + if (clamp) { + if (approx (value_to, value_from, double.EPSILON)) { + return 0; + } + + return get_first_zero (); + } + + omega0 = Math.sqrt (stiffness / mass); + + /* + * As first ansatz for the overdamped solution, + * and general estimation for the oscillating ones + * we take the value of the envelope when it's < epsilon + */ + x0 = -Math.log (epsilon) / beta; + + /* double.EPSILON is too small for this specific comparison, so we use + * FLT_EPSILON even though it's doubles */ + if (approx (beta, omega0, float.EPSILON) || beta < omega0) { + return (uint) (x0 * 1000); + } + + /* + * Since the overdamped solution decays way slower than the envelope + * we need to use the value of the oscillation itself. + * Newton's root finding method is a good candidate in this particular case: + * https://en.wikipedia.org/wiki/Newton%27s_method + */ + y0 = oscillate ((uint) (x0 * 1000), null); + m = (oscillate ((uint) ((x0 + DELTA) * 1000), null) - y0) / DELTA; + + x1 = (value_to - y0 + m * x0) / m; + y1 = oscillate ((uint) (x1 * 1000), null); + + while ((value_to - y1).abs () > epsilon) { + if (i > 1000) { + return 0; + } + + x0 = x1; + y0 = y1; + + m = (oscillate ((uint) ((x0 + DELTA) * 1000), null) - y0) / DELTA; + + x1 = (value_to - y0 + m * x0) / m; + y1 = oscillate ((uint) (x1 * 1000), null); + i++; + } + + return (uint) (x1 * 1000); + } + + public override void new_frame (int time) { + double velocity; + double val = oscillate (time, out velocity); + + progress (val); + } + + public override void stopped (bool is_finished) { + if (is_finished) { + progress (value_to); + } + } +} diff --git a/src/ShellClients/PanelWindow.vala b/src/ShellClients/PanelWindow.vala index f9a1b0587..7d95c87ea 100644 --- a/src/ShellClients/PanelWindow.vala +++ b/src/ShellClients/PanelWindow.vala @@ -35,7 +35,7 @@ public class Gala.PanelWindow : ShellWindow { } } - private GestureTracker default_gesture_tracker; + private GestureController gesture_controller; private HideTracker? hide_tracker; private int width = -1; @@ -50,6 +50,8 @@ public class Gala.PanelWindow : ShellWindow { if (window_struts.remove (window)) { update_struts (); } + + gesture_controller = null; // make it release its reference on us }); notify["anchor"].connect (() => position = Position.from_anchor (anchor)); @@ -61,7 +63,7 @@ public class Gala.PanelWindow : ShellWindow { window.size_changed.connect (update_strut); window.position_changed.connect (update_strut); - default_gesture_tracker = new GestureTracker (ANIMATION_DURATION, ANIMATION_DURATION); + gesture_controller = new GestureController (GESTURE_ID, NONE, this); window.display.in_fullscreen_changed.connect (() => { if (wm.get_display ().get_monitor_in_fullscreen (window.get_monitor ())) { @@ -105,7 +107,7 @@ public class Gala.PanelWindow : ShellWindow { } private void hide () { - add_state (CUSTOM_HIDDEN, default_gesture_tracker, false); + gesture_controller.goto (1); } private void show () { @@ -113,7 +115,7 @@ public class Gala.PanelWindow : ShellWindow { return; } - remove_state (CUSTOM_HIDDEN, default_gesture_tracker, false); + gesture_controller.goto (0); } private void make_exclusive () { diff --git a/src/ShellClients/ShellClientsManager.vala b/src/ShellClients/ShellClientsManager.vala index bbf49e656..e26096901 100644 --- a/src/ShellClients/ShellClientsManager.vala +++ b/src/ShellClients/ShellClientsManager.vala @@ -5,7 +5,7 @@ * Authored by: Leonhard Kargl */ -public class Gala.ShellClientsManager : Object { +public class Gala.ShellClientsManager : Object, GestureTarget { private static ShellClientsManager instance; public static void init (WindowManager wm) { @@ -20,6 +20,8 @@ public class Gala.ShellClientsManager : Object { return instance; } + public Clutter.Actor? actor { get { return wm.stage; } } + public WindowManager wm { get; construct; } private NotificationsClient notifications_client; @@ -190,23 +192,13 @@ public class Gala.ShellClientsManager : Object { window.unmanaging.connect_after ((_window) => positioned_windows.remove (_window)); } - public void add_state (ShellWindow.State state, GestureTracker gesture_tracker, bool with_gesture) { - foreach (var window in positioned_windows.get_values ()) { - window.add_state (state, gesture_tracker, with_gesture); - } - - foreach (var window in panel_windows.get_values ()) { - window.add_state (state, gesture_tracker, with_gesture); - } - } - - public void remove_state (ShellWindow.State state, GestureTracker gesture_tracker, bool with_gesture) { + public override void propagate (UpdateType update_type, string id, double progress) { foreach (var window in positioned_windows.get_values ()) { - window.remove_state (state, gesture_tracker, with_gesture); + window.propagate (update_type, id, progress); } foreach (var window in panel_windows.get_values ()) { - window.remove_state (state, gesture_tracker, with_gesture); + window.propagate (update_type, id, progress); } } diff --git a/src/ShellClients/ShellWindow.vala b/src/ShellClients/ShellWindow.vala index 8327a9e7c..28453aafa 100644 --- a/src/ShellClients/ShellWindow.vala +++ b/src/ShellClients/ShellWindow.vala @@ -5,80 +5,97 @@ * Authored by: Leonhard Kargl */ -public class Gala.ShellWindow : PositionedWindow { - [Flags] - public enum State { - CUSTOM_HIDDEN, - MULTITASKING_VIEW, - DESKTOP - } +public class Gala.ShellWindow : PositionedWindow, GestureTarget { + public const string GESTURE_ID = "shell-window"; + + public Clutter.Actor? actor { get { return window_actor; } } - private const State HIDING_STATES = CUSTOM_HIDDEN | MULTITASKING_VIEW; + private Meta.WindowActor window_actor; + private double custom_progress = 0; + private double multitasking_view_progress = 0; - private Meta.WindowActor actor; - private State pending_state = DESKTOP; - private State current_state = DESKTOP; + private int animations_ongoing = 0; - private bool gesture_ongoing = false; + private PropertyTarget property_target; public ShellWindow (Meta.Window window, Position position, Variant? position_data = null) { base (window, position, position_data); } construct { - actor = (Meta.WindowActor) window.get_compositor_private (); + window_actor = (Meta.WindowActor) window.get_compositor_private (); + + window_actor.notify["height"].connect (update_target); + notify["position"].connect (update_target); + update_target (); } - public void add_state (State state, GestureTracker gesture_tracker, bool with_gesture) { - pending_state |= state; - animate (pending_state, gesture_tracker, with_gesture); + private void update_target () { + property_target = new PropertyTarget ( + GESTURE_ID, window_actor, + get_animation_property (), + get_property_type (), + calculate_value (false), + calculate_value (true) + ); } - public void remove_state (State state, GestureTracker gesture_tracker, bool with_gesture) { - pending_state &= ~state; - animate (pending_state, gesture_tracker, with_gesture); + private void update_property () { + var hidden_progress = double.max (custom_progress, multitasking_view_progress); + property_target.propagate (UPDATE, GESTURE_ID, hidden_progress); } - private void animate (State new_state, GestureTracker gesture_tracker, bool with_gesture) { - if (new_state == current_state || gesture_ongoing) { - return; - } + public override void propagate (UpdateType update_type, string id, double progress) { + switch (update_type) { + case START: + animations_ongoing++; + update_visibility (); + break; - gesture_ongoing = true; + case UPDATE: + on_update (id, progress); + break; - update_visibility (true); + case END: + animations_ongoing--; + update_visibility (); + break; - new GesturePropertyTransition ( - actor, gesture_tracker, get_animation_property (), null, calculate_value ((new_state & HIDING_STATES) != 0) - ).start (with_gesture, () => update_visibility (false)); + default: + break; + } + } - gesture_tracker.add_end_callback (with_gesture, (percentage, completions) => { - gesture_ongoing = false; + private void on_update (string id, double progress) { + switch (id) { + case MultitaskingView.GESTURE_ID: + multitasking_view_progress = progress; + break; - if (completions != 0) { - current_state = new_state; - } + case GESTURE_ID: + custom_progress = progress; + break; - if (!Meta.Util.is_wayland_compositor ()) { - if ((current_state & HIDING_STATES) != 0) { - Utils.x11_set_window_pass_through (window); - } else { - Utils.x11_unset_window_pass_through (window); - } - } + default: + break; + } - if (pending_state != new_state) { // We have received new state while animating - animate (pending_state, gesture_tracker, false); - } else { - pending_state = current_state; - } - }); + update_property (); } - private void update_visibility (bool animating) { - var visible = (current_state & HIDING_STATES) == 0; + private void update_visibility () { + var visible = double.max (multitasking_view_progress, custom_progress) < 0.1; + var animating = animations_ongoing > 0; + + if (!Meta.Util.is_wayland_compositor ()) { + if (!visible) { + Utils.x11_set_window_pass_through (window); + } else { + Utils.x11_unset_window_pass_through (window); + } + } - actor.visible = animating || visible; + window_actor.visible = animating || visible; unowned var manager = ShellClientsManager.get_instance (); window.foreach_transient ((transient) => { @@ -86,9 +103,9 @@ public class Gala.ShellWindow : PositionedWindow { return true; } - unowned var actor = (Meta.WindowActor) transient.get_compositor_private (); + unowned var window_actor = (Meta.WindowActor) transient.get_compositor_private (); - actor.visible = visible && !animating; + window_actor.visible = visible && !animating; return true; }); @@ -104,12 +121,22 @@ public class Gala.ShellWindow : PositionedWindow { } } + private Type get_property_type () { + switch (position) { + case TOP: + case BOTTOM: + return typeof (float); + default: + return typeof (uint); + } + } + private Value calculate_value (bool hidden) { switch (position) { case TOP: - return hidden ? -actor.height : 0f; + return hidden ? -window_actor.height : 0f; case BOTTOM: - return hidden ? actor.height : 0f; + return hidden ? window_actor.height : 0f; default: return hidden ? 0u : 255u; } diff --git a/src/Widgets/IconGroupContainer.vala b/src/Widgets/IconGroupContainer.vala index 1919b583a..dd95dddca 100644 --- a/src/Widgets/IconGroupContainer.vala +++ b/src/Widgets/IconGroupContainer.vala @@ -110,30 +110,6 @@ public class Gala.IconGroupContainer : Clutter.Actor { request_reposition (true); } - /** - * Calculates the width that will be occupied taking currently running animations - * end states into account - */ - public float calculate_total_width () { - var spacing = InternalUtils.scale_to_int (SPACING, scale_factor); - var group_width = InternalUtils.scale_to_int (GROUP_WIDTH, scale_factor); - - var width = 0.0f; - foreach (var child in get_children ()) { - if (child is WorkspaceInsertThumb) { - if (((WorkspaceInsertThumb) child).expanded) - width += group_width + spacing * 2; - else - width += spacing; - } else - width += group_width; - } - - width += spacing; - - return width; - } - public void force_reposition () { var children = get_children (); diff --git a/src/Widgets/MonitorClone.vala b/src/Widgets/MonitorClone.vala index efefd6d9a..3e82f4e7b 100644 --- a/src/Widgets/MonitorClone.vala +++ b/src/Widgets/MonitorClone.vala @@ -11,18 +11,17 @@ * as the WindowGroup is hidden while the view is active. Only used when * workspaces-only-on-primary is set to true. */ -public class Gala.MonitorClone : Clutter.Actor { +public class Gala.MonitorClone : ActorTarget { public signal void window_selected (Meta.Window window); public Meta.Display display { get; construct; } public int monitor { get; construct; } - public GestureTracker gesture_tracker { get; construct; } private WindowCloneContainer window_container; private BackgroundManager background; - public MonitorClone (Meta.Display display, int monitor, GestureTracker gesture_tracker) { - Object (display: display, monitor: monitor, gesture_tracker: gesture_tracker); + public MonitorClone (Meta.Display display, int monitor) { + Object (display: display, monitor: monitor); } construct { @@ -32,7 +31,7 @@ public class Gala.MonitorClone : Clutter.Actor { var scale = display.get_monitor_scale (monitor); - window_container = new WindowCloneContainer (display, gesture_tracker, scale); + window_container = new WindowCloneContainer (display, scale); window_container.window_selected.connect ((w) => { window_selected (w); }); display.window_entered_monitor.connect (window_entered); @@ -77,22 +76,6 @@ public class Gala.MonitorClone : Clutter.Actor { window_container.monitor_scale = scale; } - /** - * Animate the windows from their old location to a tiled layout - */ - public void open (bool with_gesture = false, bool is_cancel_animation = false) { - window_container.restack_windows (); - window_container.open (null, with_gesture, is_cancel_animation); - } - - /** - * Animate the windows back to their old location - */ - public void close (bool with_gesture = false, bool is_cancel_animation = false) { - window_container.restack_windows (); - window_container.close (with_gesture); - } - private void window_left (int window_monitor, Meta.Window window) { if (window_monitor != monitor) return; diff --git a/src/Widgets/MultitaskingView.vala b/src/Widgets/MultitaskingView.vala index e38d2e4d7..1e8aae0f8 100644 --- a/src/Widgets/MultitaskingView.vala +++ b/src/Widgets/MultitaskingView.vala @@ -21,32 +21,31 @@ namespace Gala { * preparing the wm, opening the components and holds containers for * the icon groups, the WorkspaceClones and the MonitorClones. */ - public class MultitaskingView : Clutter.Actor, ActivatableComponent { + public class MultitaskingView : ActorTarget, ActivatableComponent { + public const string GESTURE_ID = "multitaskingview"; + public const int ANIMATION_DURATION = 250; private const string OPEN_MULTITASKING_VIEW = "dbus-send --session --dest=org.pantheon.gala --print-reply /org/pantheon/gala org.pantheon.gala.PerformAction int32:1"; - private GestureTracker multitasking_gesture_tracker; - private GestureTracker workspace_gesture_tracker; + private GestureController workspaces_gesture_controller; + private GestureController multitasking_gesture_controller; public WindowManagerGala wm { get; construct; } private Meta.Display display; private ModalProxy modal_proxy; private bool opened = false; - private bool animating = false; private List window_containers_monitors; private IconGroupContainer icon_groups; - private Clutter.Actor workspaces; - private Clutter.Actor primary_monitor_container; + private ActorTarget workspaces; + private PrimaryMonitorClone primary_monitor_clone; private Clutter.BrightnessContrastEffect brightness_effect; private GLib.Settings gala_behavior_settings; private Drawing.StyleManager style_manager; - private bool switching_workspace_with_gesture = false; - public MultitaskingView (WindowManagerGala wm) { Object (wm: wm); } @@ -62,20 +61,10 @@ namespace Gala { opened = false; display = wm.get_display (); - multitasking_gesture_tracker = new GestureTracker (ANIMATION_DURATION, ANIMATION_DURATION); - multitasking_gesture_tracker.enable_touchpad (); - multitasking_gesture_tracker.on_gesture_detected.connect (on_multitasking_gesture_detected); - multitasking_gesture_tracker.on_gesture_handled.connect (on_multitasking_gesture_handled); - - workspace_gesture_tracker = new GestureTracker (AnimationDuration.WORKSPACE_SWITCH_MIN, AnimationDuration.WORKSPACE_SWITCH); - workspace_gesture_tracker.enable_touchpad (); - workspace_gesture_tracker.enable_scroll (this, Clutter.Orientation.HORIZONTAL); - workspace_gesture_tracker.on_gesture_detected.connect (on_workspace_gesture_detected); - workspace_gesture_tracker.on_gesture_handled.connect (switch_workspace_with_gesture); + multitasking_gesture_controller = new GestureController (GESTURE_ID, MULTITASKING_VIEW, this); + multitasking_gesture_controller.enable_touchpad (); - workspaces = new Clutter.Actor (); - - icon_groups = new IconGroupContainer (display.get_monitor_scale (display.get_primary_monitor ())); + add_target (ShellClientsManager.get_instance ()); // For hiding the panels brightness_effect = new Clutter.BrightnessContrastEffect (); update_brightness_effect (); @@ -86,13 +75,24 @@ namespace Gala { add_child (blurred_bg); + icon_groups = new IconGroupContainer (display.get_monitor_scale (display.get_primary_monitor ())); + + workspaces = new ActorTarget () { + layout_manager = new Clutter.BoxLayout () + }; + // Create a child container that will be sized to fit the primary monitor, to contain the "main" // multitasking view UI. The Clutter.Actor of this class has to be allowed to grow to the size of the // stage as it contains MonitorClones for each monitor. - primary_monitor_container = new Clutter.Actor (); - primary_monitor_container.add_child (icon_groups); - primary_monitor_container.add_child (workspaces); - add_child (primary_monitor_container); + primary_monitor_clone = new PrimaryMonitorClone (display, icon_groups, workspaces); + add_child (primary_monitor_clone); + + workspaces_gesture_controller = new GestureController (PrimaryMonitorClone.GESTURE_ID, SWITCH_WORKSPACE, primary_monitor_clone) { + enabled = false, + overshoot_upper_clamp = 0.1 + }; + workspaces_gesture_controller.enable_touchpad (); + workspaces_gesture_controller.enable_scroll (this, HORIZONTAL); unowned var manager = display.get_workspace_manager (); manager.workspace_added.connect (add_workspace); @@ -173,7 +173,7 @@ namespace Gala { continue; } - var monitor_clone = new MonitorClone (display, monitor, multitasking_gesture_tracker); + var monitor_clone = new MonitorClone (display, monitor); monitor_clone.window_selected.connect (window_selected); monitor_clone.visible = opened; @@ -186,8 +186,8 @@ namespace Gala { var scale = display.get_monitor_scale (primary); icon_groups.scale_factor = scale; - primary_monitor_container.set_position (primary_geometry.x, primary_geometry.y); - primary_monitor_container.set_size (primary_geometry.width, primary_geometry.height); + primary_monitor_clone.set_position (primary_geometry.x, primary_geometry.y); + primary_monitor_clone.set_size (primary_geometry.width, primary_geometry.height); foreach (unowned var child in workspaces.get_children ()) { unowned var workspace_clone = (WorkspaceClone) child; @@ -211,7 +211,7 @@ namespace Gala { /** * Scroll through workspaces with the mouse wheel. Smooth scrolling is handled by - * GestureTracker. + * GestureController. */ #if HAS_MUTTER45 public override bool scroll_event (Clutter.Event scroll_event) { @@ -283,98 +283,43 @@ namespace Gala { workspaces.add_transition ("nudge", nudge); } - private bool on_multitasking_gesture_detected (Gesture gesture) { - if (GestureSettings.get_action (gesture) != MULTITASKING_VIEW) { - return false; - } - - if (gesture.direction == UP && !opened || gesture.direction == DOWN && opened) { - return true; - } - - return false; - } - - private double on_multitasking_gesture_handled (Gesture gesture, uint32 timestamp) { - toggle (true, false); - return 0; - } - - private bool on_workspace_gesture_detected (Gesture gesture) { + public override void start_progress (string id) { if (!opened) { - return false; - } - - if (gesture.type == SCROLL || GestureSettings.get_action (gesture) == SWITCH_WORKSPACE) { - return true; - } - - return false; - } - - private double switch_workspace_with_gesture (Gesture gesture, uint32 timestamp) { - var direction = workspace_gesture_tracker.settings.get_natural_scroll_direction (gesture); - - unowned var manager = display.get_workspace_manager (); - var num_workspaces = manager.get_n_workspaces (); - var relative_dir = (direction == Meta.MotionDirection.LEFT) ? -1 : 1; - - unowned var active_workspace = manager.get_active_workspace (); - - var target_workspace_index = active_workspace.index () + relative_dir; - var target_workspace_exists = target_workspace_index >= 0 && target_workspace_index < num_workspaces; - unowned var target_workspace = manager.get_workspace_by_index (target_workspace_index); - - float initial_x = workspaces.x; - float target_x = 0; - bool is_nudge_animation = !target_workspace_exists; + modal_proxy = wm.push_modal (this); + modal_proxy.set_keybinding_filter (keybinding_filter); - if (is_nudge_animation) { - var workspaces_geometry = InternalUtils.get_workspaces_geometry (display); - target_x = initial_x + (workspaces_geometry.width * -relative_dir); + icon_groups.force_reposition (); } else { - foreach (unowned var child in workspaces.get_children ()) { - unowned var workspace_clone = (WorkspaceClone) child; - var workspace = workspace_clone.workspace; - - if (workspace == target_workspace) { - target_x = -workspace_clone.multitasking_view_x (); - } - } + DragDropAction.cancel_all_by_id ("multitaskingview-window"); } - debug ("Starting MultitaskingView switch workspace animation:"); - debug ("Active workspace index: %d", active_workspace.index ()); - debug ("Target workspace index: %d", target_workspace_index); - debug ("Total number of workspaces: %d", num_workspaces); - debug ("Is nudge animation: %s", is_nudge_animation ? "Yes" : "No"); - debug ("Initial X: %f", initial_x); - debug ("Target X: %f", target_x); + wm.kill_switch_workspace (); + wm.background_group.hide (); + wm.window_group.hide (); + wm.top_window_group.hide (); + show (); + grab_key_focus (); - switching_workspace_with_gesture = true; - - var upper_clamp = (direction == LEFT) ? (active_workspace.index () + 0.1) : (num_workspaces - active_workspace.index () - 0.9); - var lower_clamp = (direction == RIGHT) ? - (active_workspace.index () + 0.1) : - (num_workspaces - active_workspace.index () - 0.9); + update_positions (opened); + workspaces_gesture_controller.cancel_gesture (); - var initial_percentage = new GesturePropertyTransition (workspaces, workspace_gesture_tracker, "x", null, target_x) { - overshoot_lower_clamp = lower_clamp, - overshoot_upper_clamp = upper_clamp - }.start (true); + opened = true; + } - GestureTracker.OnEnd on_animation_end = (percentage, completions, calculated_duration) => { - switching_workspace_with_gesture = false; + public override void commit_progress (string id, double to) { + opened = to > 0.5; + workspaces_gesture_controller.enabled = opened; + } - completions = completions.clamp ((int) lower_clamp, (int) upper_clamp); - manager.get_workspace_by_index (active_workspace.index () + completions * relative_dir).activate (display.get_current_time ()); - }; + public override void end_progress (string id) { + if (!opened) { + wm.background_group.show (); + wm.window_group.show (); + wm.top_window_group.show (); + hide (); - if (!AnimationsSettings.get_enable_animations ()) { - on_animation_end (1, 1, 0); - } else { - workspace_gesture_tracker.connect_handlers (null, null, (owned) on_animation_end); + wm.pop_modal (modal_proxy); } - - return initial_percentage; } /** @@ -385,59 +330,18 @@ namespace Gala { * positions immediately. */ private void update_positions (bool animate) { - if (switching_workspace_with_gesture) { - return; - } - - unowned var manager = display.get_workspace_manager (); - var active_workspace = manager.get_active_workspace (); - var active_x = 0.0f; - - foreach (unowned var child in workspaces.get_children ()) { + for (var child = workspaces.get_first_child (); child != null; child = child.get_next_sibling ()) { unowned var workspace_clone = (WorkspaceClone) child; - var workspace = workspace_clone.workspace; - var dest_x = workspace_clone.multitasking_view_x (); - - if (workspace == active_workspace) { - active_x = dest_x; - } - - workspace_clone.save_easing_state (); - workspace_clone.set_easing_duration ((animate && AnimationsSettings.get_enable_animations ()) ? 200 : 0); - workspace_clone.x = dest_x; - workspace_clone.restore_easing_state (); - } - - workspaces.save_easing_state (); - workspaces.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); - workspaces.set_easing_duration ((animate && AnimationsSettings.get_enable_animations ()) ? AnimationDuration.WORKSPACE_SWITCH_MIN : 0); - workspaces.x = -active_x; - workspaces.restore_easing_state (); - - reposition_icon_groups (animate); - } - - private void reposition_icon_groups (bool animate) { - unowned Meta.WorkspaceManager manager = display.get_workspace_manager (); - var active_index = manager.get_active_workspace ().index (); - - if (animate) { - icon_groups.save_easing_state (); - icon_groups.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); - icon_groups.set_easing_duration (200); + workspaces.set_child_at_index (workspace_clone, workspace_clone.workspace.index ()); } - var scale = display.get_monitor_scale (display.get_primary_monitor ()); - // make sure the active workspace's icongroup is always visible - var icon_groups_width = icon_groups.calculate_total_width (); - if (icon_groups_width > primary_monitor_container.width) { - icon_groups.x = (-active_index * InternalUtils.scale_to_int (IconGroupContainer.SPACING + IconGroup.SIZE, scale) + primary_monitor_container.width / 2) - .clamp (primary_monitor_container.width - icon_groups_width - InternalUtils.scale_to_int (64, scale), InternalUtils.scale_to_int (64, scale)); - } else - icon_groups.x = primary_monitor_container.width / 2 - icon_groups_width / 2; + unowned var manager = display.get_workspace_manager (); + workspaces_gesture_controller.overshoot_lower_clamp = -manager.n_workspaces - 0.1 + 1; if (animate) { - icon_groups.restore_easing_state (); + workspaces_gesture_controller.goto (-manager.get_active_workspace_index ()); + } else { + workspaces_gesture_controller.progress = -manager.get_active_workspace_index (); } } @@ -445,7 +349,7 @@ namespace Gala { unowned var manager = display.get_workspace_manager (); var scale = display.get_monitor_scale (display.get_primary_monitor ()); - var workspace = new WorkspaceClone (manager.get_workspace_by_index (num), multitasking_gesture_tracker, scale); + var workspace = new WorkspaceClone (manager.get_workspace_by_index (num), scale); workspaces.insert_child_at_index (workspace, num); icon_groups.add_group (workspace.icon_group); @@ -453,10 +357,6 @@ namespace Gala { workspace.selected.connect (activate_workspace); update_positions (false); - - if (opened) { - workspace.open (); - } } private void remove_workspace (int num) { @@ -508,7 +408,7 @@ namespace Gala { clone.workspace.activate (display.get_current_time ()); if (close_view) { - toggle (); + close (); } } @@ -555,7 +455,7 @@ namespace Gala { workspace.activate (time); } else { window.activate (time); - toggle (); + close (); } } @@ -570,142 +470,14 @@ namespace Gala { * {@inheritDoc} */ public void open (HashTable? hints = null) { - if (!opened) { - toggle (); - } + multitasking_gesture_controller.goto (1); } /** * {@inheritDoc} */ public void close (HashTable? hints = null) { - if (opened) { - toggle (); - } - } - - /** - * Toggles the view open or closed. Takes care of all the wm related tasks, like - * starting the modal mode and hiding the WindowGroup. Finally tells all components - * to animate to their positions. - */ - private void toggle (bool with_gesture = false, bool is_cancel_animation = false) { - if (animating) { - return; - } - - // we don't want to handle cancel animation when animation are off - if (is_cancel_animation && !AnimationsSettings.get_enable_animations ()) { - return; - } - - animating = true; - - opened = !opened; - var opening = opened; - - // https://github.com/elementary/gala/issues/1728 - if (opening) { - wm.kill_switch_workspace (); - } - - foreach (var container in window_containers_monitors) { - if (opening) { - container.visible = true; - container.open (with_gesture, is_cancel_animation); - } else { - container.close (with_gesture, is_cancel_animation); - } - } - - if (opening) { - modal_proxy = wm.push_modal (this); - modal_proxy.set_keybinding_filter (keybinding_filter); - - wm.background_group.hide (); - wm.window_group.hide (); - wm.top_window_group.hide (); - show (); - grab_key_focus (); - - var scale = display.get_monitor_scale (display.get_primary_monitor ()); - icon_groups.force_reposition (); - icon_groups.y = primary_monitor_container.height - InternalUtils.scale_to_int (WorkspaceClone.BOTTOM_OFFSET - 20, scale); - } else { - DragDropAction.cancel_all_by_id ("multitaskingview-window"); - } - - // find active workspace clone and raise it, so there are no overlaps while transitioning - WorkspaceClone? active_workspace = null; - unowned Meta.WorkspaceManager manager = display.get_workspace_manager (); - var active = manager.get_active_workspace (); - foreach (unowned var child in workspaces.get_children ()) { - unowned WorkspaceClone workspace = (WorkspaceClone) child; - if (workspace.workspace == active) { - active_workspace = workspace; - break; - } - } - if (active_workspace != null) { - workspaces.set_child_above_sibling (active_workspace, null); - } - - workspaces.remove_all_transitions (); - foreach (unowned var child in workspaces.get_children ()) { - child.remove_all_transitions (); - } - - if (!is_cancel_animation) { - update_positions (false); - } - - foreach (unowned var child in workspaces.get_children ()) { - unowned WorkspaceClone workspace = (WorkspaceClone) child; - if (opening) { - workspace.open (with_gesture, is_cancel_animation); - } else { - workspace.close (with_gesture, is_cancel_animation); - } - } - - if (opening) { - ShellClientsManager.get_instance ().add_state (MULTITASKING_VIEW, multitasking_gesture_tracker, with_gesture); - } else { - ShellClientsManager.get_instance ().remove_state (MULTITASKING_VIEW, multitasking_gesture_tracker, with_gesture); - } - - GestureTracker.OnEnd on_animation_end = (percentage, completions) => { - var animation_duration = completions == 0 ? 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 (); - - wm.pop_modal (modal_proxy); - } - - animating = false; - - if (completions == 0) { - toggle (false, true); - } - - return Source.REMOVE; - }); - }; - - if (!with_gesture) { - on_animation_end (1, 1, 0); - } else { - multitasking_gesture_tracker.connect_handlers (null, null, (owned) on_animation_end); - } + multitasking_gesture_controller.goto (0); } private bool keybinding_filter (Meta.KeyBinding binding) { diff --git a/src/Widgets/PrimaryMonitorClone.vala b/src/Widgets/PrimaryMonitorClone.vala new file mode 100644 index 000000000..376d2ea1a --- /dev/null +++ b/src/Widgets/PrimaryMonitorClone.vala @@ -0,0 +1,58 @@ +/* + * Copyright 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class Gala.PrimaryMonitorClone : ActorTarget { + public const string GESTURE_ID = "workspace-row"; + + public Meta.Display display { get; construct; } + + public IconGroupContainer icon_groups { get; construct; } + public ActorTarget workspaces { get; construct; } + + public PrimaryMonitorClone (Meta.Display display, IconGroupContainer icon_groups, ActorTarget workspaces) { + Object (display: display, icon_groups: icon_groups, workspaces: workspaces); + } + + construct { + add_child (icon_groups); + add_child (workspaces); + } + + public override void allocate (Clutter.ActorBox allocation) { + set_allocation (allocation); + + float workspaces_x = (float) (get_current_progress (GESTURE_ID) * workspaces.get_first_child ().width); + workspaces.allocate_preferred_size (Math.roundf (workspaces_x), 0); + + var scale = display.get_monitor_scale (display.get_primary_monitor ()); + float icon_groups_y = allocation.get_height () - InternalUtils.scale_to_int (WorkspaceClone.BOTTOM_OFFSET - 20, scale); + + float icon_groups_x, icon_groups_width; + icon_groups.get_preferred_width (-1, null, out icon_groups_width); + if (icon_groups_width <= allocation.get_width ()) { + icon_groups_x = allocation.get_width () / 2 - icon_groups_width / 2; + } else { + icon_groups_x = (float) (get_current_progress (GESTURE_ID) * InternalUtils.scale_to_int (IconGroupContainer.SPACING + IconGroup.SIZE, scale) + allocation.get_width () / 2) + .clamp (allocation.get_width () - icon_groups_width - InternalUtils.scale_to_int (64, scale), InternalUtils.scale_to_int (64, scale)); + } + + icon_groups.allocate_preferred_size (Math.roundf (icon_groups_x), Math.roundf (icon_groups_y)); + } + + public override void update_progress (string id, double progress) { + if (id == GESTURE_ID) { + queue_relayout (); + } + } + + public override void commit_progress (string id, double to) { + if (id == GESTURE_ID) { + unowned var workspace_manager = display.get_workspace_manager (); + workspace_manager.get_workspace_by_index ((int) (-to)).activate (display.get_current_time ()); + } + } +} diff --git a/src/Widgets/WindowClone.vala b/src/Widgets/WindowClone.vala index d7beb7f67..80ed72362 100644 --- a/src/Widgets/WindowClone.vala +++ b/src/Widgets/WindowClone.vala @@ -8,7 +8,7 @@ * A container for a clone of the texture of a MetaWindow, a WindowIcon, a Tooltip with the title, * a close button and a shadow. Used together with the WindowCloneContainer. */ -public class Gala.WindowClone : Clutter.Actor { +public class Gala.WindowClone : ActorTarget { private const int WINDOW_ICON_SIZE = 64; private const int ACTIVE_SHAPE_SIZE = 12; private const int FADE_ANIMATION_DURATION = 200; @@ -53,7 +53,6 @@ public class Gala.WindowClone : Clutter.Actor { } public bool overview_mode { get; construct; } - public GestureTracker gesture_tracker { get; construct; } private float _monitor_scale_factor = 1.0f; public float monitor_scale_factor { get { @@ -94,11 +93,10 @@ public class Gala.WindowClone : Clutter.Actor { private Clutter.Actor window_icon; private Tooltip window_title; - public WindowClone (Meta.Display display, Meta.Window window, GestureTracker gesture_tracker, float scale, bool overview_mode = false) { + public WindowClone (Meta.Display display, Meta.Window window, float scale, bool overview_mode = false) { Object ( display: display, window: window, - gesture_tracker: gesture_tracker, monitor_scale_factor: scale, overview_mode: overview_mode ); @@ -113,6 +111,7 @@ public class Gala.WindowClone : Clutter.Actor { window.notify["maximized-horizontally"].connect (check_shadow_requirements); window.notify["maximized-vertically"].connect (check_shadow_requirements); window.size_changed.connect (() => request_reposition ()); + window.position_changed.connect (update_targets); if (overview_mode) { var click_action = new Clutter.ClickAction (); @@ -199,10 +198,6 @@ public class Gala.WindowClone : Clutter.Actor { set_child_above_sibling (window_title, clone); check_shadow_requirements (); - - if (should_fade ()) { - opacity = 0; - } } private void check_shadow_requirements () { @@ -241,69 +236,58 @@ public class Gala.WindowClone : Clutter.Actor { } /** - * Place the window at the location of the original MetaWindow - * - * @param animate Animate the transformation of the placement + * Animate the window to the given slot */ - public void transition_to_original_state (bool with_gesture = false) { - var outer_rect = window.get_frame_rect (); +#if HAS_MUTTER45 + public void take_slot (Mtk.Rectangle rect, bool animate) { +#else + public void take_slot (Meta.Rectangle rect, bool animate) { +#endif + slot = rect; - unowned var display = window.get_display (); - var monitor_geom = display.get_monitor_geometry (window.get_monitor ()); + if (animate) { + save_easing_state (); + set_easing_duration (AnimationsSettings.get_animation_duration (MultitaskingView.ANIMATION_DURATION)); + set_easing_mode (EASE_OUT_QUAD); + } - var target_x = outer_rect.x - monitor_geom.x; - var target_y = outer_rect.y - monitor_geom.y; + update_targets (); - active = false; - update_hover_widgets (true); + if (animate) { + restore_easing_state (); + } + } - new GesturePropertyTransition (this, gesture_tracker, "x", null, (float) target_x).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "y", null, (float) target_y).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "width", null, (float) outer_rect.width).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "height", null, (float) outer_rect.height).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "shadow-opacity", (uint8) 255, (uint8) 0).start (with_gesture); - new GesturePropertyTransition (window_icon, gesture_tracker, "opacity", 255u, 0u).start (with_gesture, () => { - update_hover_widgets (false); - toggle_shadow (false); - }); + private void update_targets () { + remove_all_targets (); - if (should_fade ()) { - new GesturePropertyTransition (this, gesture_tracker, "opacity", null, 0u).start (with_gesture); + if (slot == null) { + return; } - } - /** - * Animate the window to the given slot - */ -#if HAS_MUTTER45 - public void take_slot (Mtk.Rectangle rect, bool from_window_position, bool with_gesture = false) { -#else - public void take_slot (Meta.Rectangle rect, bool from_window_position, bool with_gesture = false) { -#endif - slot = rect; - active = false; + var window_rect = window.get_frame_rect (); - var outer_rect = window.get_frame_rect (); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "x", typeof (float), (float) window_rect.x, (float) slot.x)); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "y", typeof (float), (float) window_rect.y, (float) slot.y)); + + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "width", typeof (float), (float) window_rect.width, (float) slot.width)); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "height", typeof (float), (float) window_rect.height, (float) slot.height)); - float initial_width = from_window_position ? outer_rect.width : width; - float initial_height = from_window_position ? outer_rect.height : height; + if (should_fade ()) { + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "opacity", typeof (uint8), (uint8) 0u, (uint8) 255u)); + } - var monitor_geom = display.get_monitor_geometry (window.get_monitor ()); - float intial_x = from_window_position ? outer_rect.x - monitor_geom.x : x; - float intial_y = from_window_position ? outer_rect.y - monitor_geom.y : y; + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, window_icon, "opacity", typeof (uint), 0u, 255u)); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "shadow-opacity", typeof (uint8), (uint8) 0u, (uint8) 255u)); + } + + public override void start_progress (string id) { update_hover_widgets (true); + } - new GesturePropertyTransition (this, gesture_tracker, "x", intial_x, (float) rect.x).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "y", intial_y, (float) rect.y).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "width", (float) initial_width, (float) rect.width).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "height", (float) initial_height, (float) rect.height).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "opacity", null, 255u).start (with_gesture); - new GesturePropertyTransition (this, gesture_tracker, "shadow-opacity", (uint8) 0, (uint8) 255).start (with_gesture); - new GesturePropertyTransition (window_icon, gesture_tracker, "opacity", 0u, 255u).start (with_gesture, () => { - update_hover_widgets (false); - toggle_shadow (true); - }); + public override void end_progress (string id) { + update_hover_widgets (false); } public override void allocate (Clutter.ActorBox box) { @@ -403,25 +387,6 @@ public class Gala.WindowClone : Clutter.Actor { window_title.restore_easing_state (); } - private void toggle_shadow (bool show) { - if (get_transition ("shadow-opacity") != null) { - remove_transition ("shadow-opacity"); - } - - if (AnimationsSettings.get_enable_animations ()) { - var shadow_transition = new Clutter.PropertyTransition ("shadow-opacity") { - duration = MultitaskingView.ANIMATION_DURATION, - remove_on_complete = true, - progress_mode = Clutter.AnimationMode.EASE_OUT_QUAD, - interval = new Clutter.Interval (typeof (uint8), shadow_opacity, show ? 255 : 0) - }; - - add_transition ("shadow-opacity", shadow_transition); - } else { - shadow_opacity = show ? 255 : 0; - } - } - /** * Send the window the delete signal and listen for new windows to be added * to the window's workspace, in which case we check if the new window is a @@ -671,10 +636,17 @@ public class Gala.WindowClone : Clutter.Actor { */ private void drag_canceled () { get_parent ().remove_child (this); - prev_parent.add_child (this); // Add above so that it is above while it animates back to its place var duration = AnimationsSettings.get_animation_duration (MultitaskingView.ANIMATION_DURATION); + // Adding to the previous parent will automatically update it to take it's slot + // so to animate it we set the easing + save_easing_state (); + set_easing_duration (duration); + set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD); + prev_parent.add_child (this); // Add above so that it is above while it animates back to its place + restore_easing_state (); + clone.set_pivot_point (0.0f, 0.0f); clone.save_easing_state (); clone.set_easing_duration (duration); diff --git a/src/Widgets/WindowCloneContainer.vala b/src/Widgets/WindowCloneContainer.vala index 6cab571af..1b4827d91 100644 --- a/src/Widgets/WindowCloneContainer.vala +++ b/src/Widgets/WindowCloneContainer.vala @@ -7,7 +7,7 @@ /** * Container which controls the layout of a set of WindowClones. */ -public class Gala.WindowCloneContainer : Clutter.Actor { +public class Gala.WindowCloneContainer : ActorTarget { public signal void window_selected (Meta.Window window); public signal void requested_close (); @@ -17,7 +17,6 @@ public class Gala.WindowCloneContainer : Clutter.Actor { public int padding_bottom { get; set; default = 12; } public Meta.Display display { get; construct; } - public GestureTracker gesture_tracker { get; construct; } public bool overview_mode { get; construct; } private float _monitor_scale = 1.0f; @@ -41,8 +40,8 @@ public class Gala.WindowCloneContainer : Clutter.Actor { */ private unowned WindowClone? current_window = null; - public WindowCloneContainer (Meta.Display display, GestureTracker gesture_tracker, float scale, bool overview_mode = false) { - Object (display: display, gesture_tracker: gesture_tracker, monitor_scale: scale, overview_mode: overview_mode); + public WindowCloneContainer (Meta.Display display, float scale, bool overview_mode = false) { + Object (display: display, monitor_scale: scale, overview_mode: overview_mode); } private void reallocate () { @@ -67,7 +66,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { var windows_ordered = InternalUtils.sort_windows (display, windows); - var new_window = new WindowClone (display, window, gesture_tracker, monitor_scale, overview_mode); + var new_window = new WindowClone (display, window, monitor_scale, overview_mode); new_window.selected.connect ((clone) => window_selected (clone.window)); new_window.destroy.connect ((_new_window) => { @@ -81,9 +80,9 @@ public class Gala.WindowCloneContainer : Clutter.Actor { current_window = null; } - reflow (); + reflow (false); }); - new_window.request_reposition.connect (() => reflow ()); + new_window.request_reposition.connect (() => reflow (false)); unowned Meta.Window? target = null; foreach (unowned var w in windows_ordered) { @@ -107,7 +106,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { } } - reflow (); + reflow (false); } /** @@ -117,7 +116,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { foreach (unowned var child in get_children ()) { if (((WindowClone) child).window == window) { remove_child (child); - reflow (); + reflow (false); break; } } @@ -127,7 +126,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { * Sort the windows z-order by their actual stacking to make intersections * during animations correct. */ - public void restack_windows () { + private void restack_windows () { var children = get_children (); var windows = new List (); @@ -155,7 +154,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { * Recalculate the tiling positions of the windows and animate them to * the resulting spots. */ - public void reflow (bool with_gesture = false, bool is_cancel_animation = false, bool opening = false) { + private void reflow (bool view_toggle) { if (!opened) { return; } @@ -194,7 +193,7 @@ public class Gala.WindowCloneContainer : Clutter.Actor { foreach (var tilable in window_positions) { unowned var clone = (WindowClone) tilable.id; - clone.take_slot (tilable.rect, opening && !is_cancel_animation, with_gesture); + clone.take_slot (tilable.rect, !view_toggle); } } @@ -354,49 +353,36 @@ public class Gala.WindowCloneContainer : Clutter.Actor { return false; } - /** - * When opened the WindowClones are animated to a tiled layout - */ - public void open (Meta.Window? selected_window, bool with_gesture, bool is_cancel_animation) { - if (opened) { + public override void start_progress (string id) { + if (id != MultitaskingView.GESTURE_ID) { return; } - opened = true; + if (!opened) { + if (current_window != null) { + current_window.active = false; + } - // hide the highlight when opened - if (selected_window != null) { - foreach (var child in get_children ()) { + for (var child = get_first_child (); child != null; child = child.get_next_sibling ()) { unowned var clone = (WindowClone) child; - if (clone.window == selected_window) { + if (clone.window == display.focus_window) { current_window = clone; break; } } - - if (current_window != null) { - current_window.active = false; - } - } else { - current_window = null; } - reflow (with_gesture, is_cancel_animation, true); + opened = true; + + restack_windows (); + reflow (true); } - /** - * Calls the transition_to_original_state() function on each child - * to make them take their original locations again. - */ - public void close (bool with_gesture = false) { - if (!opened) { + public override void commit_progress (string id, double to) { + if (id != MultitaskingView.GESTURE_ID) { return; } - opened = false; - - foreach (var window in get_children ()) { - ((WindowClone) window).transition_to_original_state (with_gesture); - } + opened = to > 0.5; } } diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index dda2272e9..d98a0e9a2 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -5,26 +5,28 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -public class Gala.WindowOverview : Clutter.Actor, ActivatableComponent { +public class Gala.WindowOverview : ActorTarget, ActivatableComponent { private const int BORDER = 10; private const int TOP_GAP = 30; private const int BOTTOM_GAP = 100; public WindowManager wm { get; construct; } - public GestureTracker gesture_tracker { get; construct; } // Currently only used to allow us to use GesturePropertyTransitions + + private GestureController gesture_controller; // Currently not used for actual touchpad gestures but only as controller private ModalProxy modal_proxy; // the workspaces which we expose right now private List workspaces; private WindowCloneContainer window_clone_container; - public WindowOverview (WindowManager wm, GestureTracker gesture_tracker) { - Object (wm : wm, gesture_tracker: gesture_tracker); + public WindowOverview (WindowManager wm) { + Object (wm : wm); } construct { visible = false; reactive = true; + gesture_controller = new GestureController (MultitaskingView.GESTURE_ID, NONE, this); } @@ -124,7 +126,7 @@ public class Gala.WindowOverview : Clutter.Actor, ActivatableComponent { var geometry = display.get_monitor_geometry (i); var scale = display.get_monitor_scale (i); - window_clone_container = new WindowCloneContainer (display, gesture_tracker, scale, true) { + window_clone_container = new WindowCloneContainer (display, scale, true) { padding_top = TOP_GAP, padding_left = BORDER, padding_right = BORDER, @@ -152,8 +154,9 @@ public class Gala.WindowOverview : Clutter.Actor, ActivatableComponent { } container.add_window (window); - container.open (display.get_focus_window (), false, false); } + + gesture_controller.goto (1); } private bool keybinding_filter (Meta.KeyBinding binding) { @@ -179,12 +182,6 @@ public class Gala.WindowOverview : Clutter.Actor, ActivatableComponent { return true; } - private void restack_windows () { - foreach (var child in get_children ()) { - ((WindowCloneContainer) child).restack_windows (); - } - } - private void window_left_monitor (int num, Meta.Window window) { unowned var container = (WindowCloneContainer) get_child_at_index (num); if (container == null) { @@ -262,23 +259,19 @@ public class Gala.WindowOverview : Clutter.Actor, ActivatableComponent { return; } - restack_windows (); - foreach (var workspace in workspaces) { workspace.window_added.disconnect (add_window); workspace.window_removed.disconnect (remove_window); } wm.get_display ().window_left_monitor.disconnect (window_left_monitor); - foreach (unowned var child in get_children ()) { - ((WindowCloneContainer) child).close (); - } - Clutter.Threads.Timeout.add (MultitaskingView.ANIMATION_DURATION, () => { cleanup (); return Source.REMOVE; }); + + gesture_controller.goto (0); } private void cleanup () { diff --git a/src/Widgets/WorkspaceClone.vala b/src/Widgets/WorkspaceClone.vala index 0b624c87d..dec69899a 100644 --- a/src/Widgets/WorkspaceClone.vala +++ b/src/Widgets/WorkspaceClone.vala @@ -107,7 +107,7 @@ namespace Gala { * The latter is not added to the WorkspaceClone itself though but to a container * of the MultitaskingView. */ - public class WorkspaceClone : Clutter.Actor { + public class WorkspaceClone : ActorTarget { /** * The offset of the scaled background to the bottom of the monitor bounds */ @@ -124,12 +124,6 @@ namespace Gala { */ private const int HOVER_ACTIVATE_DELAY = 400; - /** - * The MultitaskingView shows the workspaces overlapping them WorkspaceClone.X_OFFSET pixels - * making it possible to move windows to the next/previous workspace. - */ - public const int X_OFFSET = 150; - /** * A window has been selected, the MultitaskingView should consider activating * and closing the view. @@ -145,7 +139,6 @@ namespace Gala { public signal void selected (bool close_view); public Meta.Workspace workspace { get; construct; } - public GestureTracker gesture_tracker { get; construct; } public IconGroup icon_group { get; private set; } public WindowCloneContainer window_container { get; private set; } @@ -158,6 +151,7 @@ namespace Gala { if (value != _scale_factor) { _scale_factor = value; reallocate (); + update_targets (); } } } @@ -167,8 +161,8 @@ namespace Gala { private uint hover_activate_timeout = 0; - public WorkspaceClone (Meta.Workspace workspace, GestureTracker gesture_tracker, float scale) { - Object (workspace: workspace, gesture_tracker: gesture_tracker, scale_factor: scale); + public WorkspaceClone (Meta.Workspace workspace, float scale) { + Object (workspace: workspace, scale_factor: scale); } construct { @@ -185,7 +179,7 @@ namespace Gala { background = new FramedBackground (display); background.add_action (background_click_action); - window_container = new WindowCloneContainer (display, gesture_tracker, scale_factor) { + window_container = new WindowCloneContainer (display, scale_factor) { width = monitor_geometry.width, height = monitor_geometry.height, }; @@ -238,13 +232,10 @@ namespace Gala { var listener = WindowListener.get_default (); listener.window_no_longer_on_all_workspaces.connect (add_window); - parent_set.connect ((old_parent) => { - if (old_parent != null) { - old_parent.notify["x"].disconnect (update_icon_group_opacity); - } + unowned var monitor_manager = display.get_context ().get_backend ().get_monitor_manager (); + monitor_manager.monitors_changed.connect (update_targets); - get_parent ().notify["x"].connect (update_icon_group_opacity); - }); + update_targets (); } ~WorkspaceClone () { @@ -263,14 +254,6 @@ namespace Gala { icon_group.destroy (); } - private void update_icon_group_opacity () { - var offset = (multitasking_view_x () + get_parent ().x).abs (); - - var adjusted_width = width - InternalUtils.scale_to_int (X_OFFSET, scale_factor); - - icon_group.backdrop_opacity = (1 - (offset / adjusted_width)).clamp (0, 1); - } - private void reallocate () { icon_group.scale_factor = scale_factor; window_container.monitor_scale = scale_factor; @@ -323,120 +306,34 @@ namespace Gala { } } - /** - * @return The position on the X axis of this workspace. - */ - public float multitasking_view_x () { - return workspace.index () * (width - InternalUtils.scale_to_int (X_OFFSET, scale_factor)); - } - - /** - * @return The amount of pixels the workspace is overlapped in the X axis. - */ - private float current_x_overlap () { - var display = workspace.get_display (); - unowned Meta.WorkspaceManager manager = display.get_workspace_manager (); - var active_index = manager.get_active_workspace ().index (); - if (workspace.index () == active_index) { - return 0; - } else { - var x_offset = InternalUtils.scale_to_int (X_OFFSET, scale_factor) + WindowManagerGala.WORKSPACE_GAP; - return (workspace.index () < active_index) ? -x_offset : x_offset; - } - } - - /** - * Utility function to shrink a MetaRectangle on all sides for the given amount. - * Negative amounts will scale it instead. - * - * @param amount The amount in px to shrink. - */ -#if HAS_MUTTER45 - private static inline void shrink_rectangle (ref Mtk.Rectangle rect, int amount) { -#else - private static inline void shrink_rectangle (ref Meta.Rectangle rect, int amount) { -#endif - rect.x += amount; - rect.y += amount; - rect.width -= amount * 2; - rect.height -= amount * 2; - } - - /** - * Animates the background to its scale, causes a redraw on the IconGroup and - * makes sure the WindowCloneContainer animates its windows to their tiled layout. - * Also sets the current_window of the WindowCloneContainer to the active window - * if it belongs to this workspace. - */ - public void open (bool with_gesture = false, bool is_cancel_animation = false) { - if (opened) { - return; - } - - opened = true; - - window_container.restack_windows (); + private void update_targets () { + remove_all_targets (); unowned var display = workspace.get_display (); var monitor = display.get_monitor_geometry (display.get_primary_monitor ()); - var initial_x = is_cancel_animation ? x : x + current_x_overlap (); - var target_x = multitasking_view_x (); var scale = (float)(monitor.height - InternalUtils.scale_to_int (TOP_OFFSET + BOTTOM_OFFSET, scale_factor)) / monitor.height; var pivot_y = InternalUtils.scale_to_int (TOP_OFFSET, scale_factor) / (monitor.height - monitor.height * scale); background.set_pivot_point (0.5f, pivot_y); - update_size (monitor); - - new GesturePropertyTransition (this, gesture_tracker, "x", initial_x, target_x).start (with_gesture); - new GesturePropertyTransition (background, gesture_tracker, "scale-x", null, (double) scale).start (with_gesture); - new GesturePropertyTransition (background, gesture_tracker, "scale-y", null, (double) scale).start (with_gesture); + var initial_width = monitor.width; + var target_width = monitor.width * scale + WindowManagerGala.WORKSPACE_GAP * 2; -#if HAS_MUTTER45 - Mtk.Rectangle area = { -#else - Meta.Rectangle area = { -#endif - (int)Math.floorf (monitor.x + monitor.width - monitor.width * scale) / 2, - (int)Math.floorf (monitor.y + InternalUtils.scale_to_int (TOP_OFFSET, scale_factor)), - (int)Math.floorf (monitor.width * scale), - (int)Math.floorf (monitor.height * scale) - }; - shrink_rectangle (ref area, 32); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, this, "width", typeof (float), (float) initial_width, (float) target_width)); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, background, "scale-x", typeof (double), 1d, (double) scale)); + add_target (new PropertyTarget (MultitaskingView.GESTURE_ID, background, "scale-y", typeof (double), 1d, (double) scale)); window_container.padding_top = InternalUtils.scale_to_int (TOP_OFFSET, scale_factor); window_container.padding_left = window_container.padding_right = (int)(monitor.width - monitor.width * scale) / 2; window_container.padding_bottom = InternalUtils.scale_to_int (BOTTOM_OFFSET, scale_factor); - - icon_group.redraw (); - - Meta.Window? selected_window = display.get_workspace_manager ().get_active_workspace () == workspace ? display.get_focus_window () : null; - window_container.open (selected_window, with_gesture, is_cancel_animation); } - /** - * Close the view again by animating the background back to its scale and - * the windows back to their old locations. - */ - public void close (bool with_gesture = false, bool is_cancel_animation = false) { - if (!opened) { - return; + public override void update_progress (string id, double progress) { + if (id == PrimaryMonitorClone.GESTURE_ID) { + icon_group.backdrop_opacity = 1 - (float) (workspace.index () + progress).abs ().clamp (0, 1); } - - opened = false; - - window_container.restack_windows (); - - var initial_x = is_cancel_animation ? x : multitasking_view_x (); - var target_x = multitasking_view_x () + current_x_overlap (); - - new GesturePropertyTransition (this, gesture_tracker, "x", initial_x, target_x).start (with_gesture); - new GesturePropertyTransition (background, gesture_tracker, "scale-x", null, 1.0d).start (with_gesture); - new GesturePropertyTransition (background, gesture_tracker, "scale-y", null, 1.0d).start (with_gesture); - - window_container.close (with_gesture); } } } diff --git a/src/WindowManager.vala b/src/WindowManager.vala index 5c836e39b..b9f9c73fb 100644 --- a/src/WindowManager.vala +++ b/src/WindowManager.vala @@ -289,7 +289,7 @@ namespace Gala { if (plugin_manager.window_overview_provider == null || (window_overview = (plugin_manager.get_plugin (plugin_manager.window_overview_provider) as ActivatableComponent)) == null ) { - window_overview = new WindowOverview (this, gesture_tracker); + window_overview = new WindowOverview (this); ui_group.add_child ((Clutter.Actor) window_overview); } diff --git a/src/meson.build b/src/meson.build index a039dd096..97ca33672 100644 --- a/src/meson.build +++ b/src/meson.build @@ -36,11 +36,15 @@ gala_bin_sources = files( 'Dialogs/AccessDialog.vala', 'Dialogs/CloseDialog.vala', 'Dialogs/InhibitShortcutsDialog.vala', + 'Gestures/ActorTarget.vala', 'Gestures/Gesture.vala', + 'Gestures/GestureController.vala', 'Gestures/GesturePropertyTransition.vala', 'Gestures/GestureSettings.vala', 'Gestures/GestureTracker.vala', + 'Gestures/PropertyTarget.vala', 'Gestures/ScrollBackend.vala', + 'Gestures/SpringTimeline.vala', 'Gestures/ToucheggBackend.vala', 'HotCorners/Barrier.vala', 'HotCorners/HotCorner.vala', @@ -59,6 +63,7 @@ gala_bin_sources = files( 'Widgets/MultitaskingView.vala', 'Widgets/PixelPicker.vala', 'Widgets/PointerLocator.vala', + 'Widgets/PrimaryMonitorClone.vala', 'Widgets/ScreenShield.vala', 'Widgets/SelectionArea.vala', 'Widgets/Tooltip.vala',