diff --git a/CMakeLists.txt b/CMakeLists.txt index 523948bd..6ce42cfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,9 @@ vala_precompile(VALA_C src/Widgets/WindowThumb.vala src/Widgets/WorkspaceThumb.vala src/Widgets/WorkspaceView.vala + src/Widgets/MultitaskingView/IconGroup.vala + src/Widgets/MultitaskingView/MultitaskingView.vala + src/Widgets/MultitaskingView/Workspace.vala ${CMAKE_BINARY_DIR}/src/Config.vala PACKAGES granite @@ -108,6 +111,7 @@ PACKAGES xfixes-4.0 OPTIONS -g + --enable-deprecated --vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi/ ${MUTTER36_FLAGS} ${MUTTER38_FLAGS} diff --git a/data/texture.png b/data/texture.png index c8624371..c5189f8c 100644 Binary files a/data/texture.png and b/data/texture.png differ diff --git a/src/Plugin.vala b/src/Plugin.vala index bef4596c..a13f2de3 100644 --- a/src/Plugin.vala +++ b/src/Plugin.vala @@ -45,6 +45,7 @@ namespace Gala WorkspaceView workspace_view; Zooming zooming; WindowOverview window_overview; + MultitaskingView multitasking_view; // used to detect which corner was used to trigger an action Clutter.Actor? last_hotcorner; @@ -52,6 +53,8 @@ namespace Gala #if HAS_MUTTER38 public Meta.BackgroundGroup background_group { get; private set; } + public Clutter.Actor window_group { get; private set; } + public Clutter.Actor top_window_group { get; private set; } public Clutter.Actor ui_group { get; private set; } #endif @@ -131,7 +134,7 @@ namespace Gala ui_group.reactive = true; stage.add_child (ui_group); - var window_group = Compositor.get_window_group_for_screen (screen); + window_group = Compositor.get_window_group_for_screen (screen); stage.remove_child (window_group); ui_group.add_child (window_group); @@ -143,6 +146,8 @@ namespace Gala workspace_view = new WorkspaceView (this); workspace_view.visible = false; + multitasking_view = new MultitaskingView (this); + winswitcher = new WindowSwitcher (this); zooming = new Zooming (this); @@ -152,8 +157,9 @@ namespace Gala ui_group.add_child (workspace_view); ui_group.add_child (winswitcher); ui_group.add_child (window_overview); + ui_group.add_child (multitasking_view); - var top_window_group = Compositor.get_top_window_group_for_screen (screen); + top_window_group = Compositor.get_top_window_group_for_screen (screen); stage.remove_child (top_window_group); ui_group.add_child (top_window_group); #else @@ -223,7 +229,8 @@ namespace Gala }); KeyBinding.set_custom_handler ("show-desktop", () => { - workspace_view.show (true); + //workspace_view.show (true); + multitasking_view.toggle (); }); #if HAS_MUTTER38 diff --git a/src/Widgets/MultitaskingView/IconGroup.vala b/src/Widgets/MultitaskingView/IconGroup.vala new file mode 100644 index 00000000..53e67244 --- /dev/null +++ b/src/Widgets/MultitaskingView/IconGroup.vala @@ -0,0 +1,160 @@ +using Clutter; + +namespace Gala +{ + public class IconGroup : Actor + { + List windows; + + const int SIZE = 64; + + static const int PLUS_SIZE = 8; + static const int PLUS_WIDTH = 24; + + public signal void selected (); + + public IconGroup () + { + clear (); + + width = SIZE; + height = SIZE; + reactive = true; + + var canvas = new Canvas (); + canvas.set_size (SIZE, SIZE); + canvas.draw.connect (draw); + content = canvas; + } + + public override bool button_release_event (ButtonEvent event) + { + selected (); + + return false; + } + + public void clear () + { + windows = new List (); + } + + public void add_window (Meta.Window window, bool no_redraw = false) + { + windows.append (window); + + if (!no_redraw) + redraw (); + } + + public void redraw () + { + content.invalidate (); + } + + bool draw (Cairo.Context cr) + { + cr.set_operator (Cairo.Operator.CLEAR); + cr.paint (); + cr.set_operator (Cairo.Operator.OVER); + + // we never show more than 9, TODO: show an ellipsis when we do cut + var n_windows = uint.min (windows.length (), 9); + + if (n_windows == 1) { + var pix = Utils.get_icon_for_window (windows.nth_data (0), 64); + Gdk.cairo_set_source_pixbuf (cr, pix, 0, 0); + cr.paint (); + return false; + } + + // folder + Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, 0.5, 0.5, (int)width - 1, (int)height - 1, 5); + + cr.set_source_rgba (0, 0, 0, 0.1); + cr.fill_preserve (); + + cr.set_line_width (1); + + var grad = new Cairo.Pattern.linear (0, 0, 0, height); + grad.add_color_stop_rgba (0.8, 0, 0, 0, 0); + grad.add_color_stop_rgba (1.0, 1, 1, 1, 0.1); + + cr.set_source (grad); + cr.stroke (); + + Granite.Drawing.Utilities.cairo_rounded_rectangle (cr, 1.5, 1.5, (int)width - 3, (int)height - 3, 5); + + cr.set_source_rgba (0, 0, 0, 0.3); + cr.stroke (); + + // the only workspace that can be empty is the last one, so we draw our + // plus here + if (n_windows < 1) { + var buffer = new Granite.Drawing.BufferSurface (SIZE, SIZE); + var offset = SIZE / 2 - PLUS_WIDTH / 2; + + buffer.context.rectangle (PLUS_WIDTH / 2 - PLUS_SIZE / 2 + 0.5 + offset, + 0.5 + offset, + PLUS_SIZE - 1, + PLUS_WIDTH - 1); + + buffer.context.rectangle (0.5 + offset, + PLUS_WIDTH / 2 - PLUS_SIZE / 2 + 0.5 + offset, + PLUS_WIDTH - 1, + PLUS_SIZE - 1); + + buffer.context.set_source_rgb (0, 0, 0); + buffer.context.fill_preserve (); + buffer.exponential_blur (5); + + buffer.context.set_source_rgb (1, 1, 1); + buffer.context.set_line_width (1); + buffer.context.stroke_preserve (); + + buffer.context.set_source_rgb (0.8, 0.8, 0.8); + buffer.context.fill (); + + cr.set_source_surface (buffer.surface, 0, 0); + cr.paint (); + + return false; + } + + int size; + if (n_windows < 5) + size = 22; + else + size = 16; + + var columns = (int)Math.ceil (Math.sqrt (n_windows)); + var rows = (int)Math.ceil (n_windows / (double)columns); + + const int spacing = 6; + + var width = columns * size + (columns - 1) * spacing; + var height = rows * size + (rows - 1) * spacing; + var x_offset = SIZE / 2 - width / 2; + var y_offset = SIZE / 2 - height / 2; + + var x = x_offset; + var y = y_offset; + for (var i = 0; i < n_windows; i++) { + var pix = Utils.get_icon_for_window (windows.nth_data (i), size); + + Gdk.cairo_set_source_pixbuf (cr, pix, x, y); + + x += size + spacing; + if (x + size >= SIZE) { + x = x_offset; + y += size + spacing; + } + + cr.paint (); + } + + return false; + } + } +} + diff --git a/src/Widgets/MultitaskingView/MultitaskingView.vala b/src/Widgets/MultitaskingView/MultitaskingView.vala new file mode 100644 index 00000000..1fdc8abc --- /dev/null +++ b/src/Widgets/MultitaskingView/MultitaskingView.vala @@ -0,0 +1,237 @@ +using Clutter; + +namespace Gala +{ + public class MultitaskingView : Actor + { + public Meta.Screen screen { get; construct set; } + public Plugin plugin { get; construct set; } + + Actor icon_groups; + Actor workspaces; + + public bool opened { get; private set; default = false; } + + const int HIDING_DURATION = 300; + + public MultitaskingView (Plugin plugin) + { + Object (plugin: plugin, screen: plugin.get_screen ()); + + visible = false; + reactive = true; + + workspaces = new Actor (); + + icon_groups = new Actor (); + icon_groups.layout_manager = new BoxLayout (); + (icon_groups.layout_manager as BoxLayout).spacing = 48; + + add_child (icon_groups); + add_child (workspaces); + + foreach (var workspace in screen.get_workspaces ()) + add_workspace (workspace.index ()); + + screen.workspace_added.connect (add_workspace); + screen.workspace_removed.connect (remove_workspace); + screen.workspace_switched.connect ((from, to, direction) => { + update_positions (opened); + }); + } + + public override bool scroll_event (ScrollEvent event) + { + var active_workspace = screen.get_active_workspace (); + var new_workspace = active_workspace.get_neighbor ( + event.direction == ScrollDirection.LEFT || + event.direction == ScrollDirection.UP ? + Meta.MotionDirection.LEFT : Meta.MotionDirection.RIGHT); + if (active_workspace != new_workspace) + new_workspace.activate (screen.get_display ().get_current_time ()); + + return false; + } + + void update_positions (bool animate = false, bool closing = false) + { + float x = 0; + WorkspaceClone? active = null; + var active_index = screen.get_active_workspace ().index ();; + + foreach (var child in workspaces.get_children ()) { + var workspace_clone = child as WorkspaceClone; + var index = workspace_clone.workspace.index (); + + if (index == active_index) + active = workspace_clone; + + workspace_clone.x = index * (workspace_clone.width - 150); + } + + if (active != null) { + var dest_x = -active.x; + if (animate) + workspaces.animate (AnimationMode.EASE_OUT_QUAD, 300, x: dest_x); + else + workspaces.x = dest_x; + } + } + + void add_workspace (int num) + { + var workspace = new WorkspaceClone (screen.get_workspace_by_index (num)); + workspace.window_selected.connect (window_selected); + workspace.selected.connect (activate_workspace); + + workspaces.insert_child_at_index (workspace, num); + icon_groups.insert_child_at_index (workspace.icon_group, num); + + update_positions (); + } + + void remove_workspace (int num) + { + WorkspaceClone? workspace = null; + + // FIXME is there a better way to get the removed workspace? + unowned List existing_workspaces = screen.get_workspaces (); + + foreach (var child in workspaces.get_children ()) { + var clone = child as WorkspaceClone; + + if (existing_workspaces.index (clone.workspace) < 0) { + workspace = clone; + break; + } + } + + if (workspace == null) + return; + + workspace.window_selected.disconnect (window_selected); + workspace.selected.disconnect (activate_workspace); + workspace.icon_group.destroy (); + workspace.destroy (); + + update_positions (); + } + + void activate_workspace (WorkspaceClone clone, bool close_view) + { + close_view = close_view && screen.get_active_workspace () == clone.workspace; + + clone.workspace.activate (screen.get_display ().get_current_time ()); + + if (close_view) + toggle (); + } + + public override bool key_press_event (Clutter.KeyEvent event) + { + if (event.keyval == Clutter.Key.Escape) + toggle (); + + return false; + } + + void window_selected (Meta.Window window) + { + window.activate (screen.get_display ().get_current_time ()); + toggle (); + } + + public void toggle () + { + opened = !opened; + + var opening = opened; + + unowned List windows = Meta.Compositor.get_window_actors (screen); + var primary_monitor = screen.get_primary_monitor (); + var monitor = screen.get_monitor_geometry (primary_monitor); + + if (opening) { + plugin.begin_modal (); + + plugin.background_group.hide (); + plugin.window_group.hide (); + plugin.top_window_group.hide (); + show (); + grab_key_focus (); + + icon_groups.x = monitor.width / 2 - icon_groups.width / 2; + icon_groups.y = monitor.height - WorkspaceClone.BOTTOM_OFFSET + 20; + } + + // find active workspace clone and raise it, so there are no overlaps while transitioning + WorkspaceClone? active_workspace = null; + var active = screen.get_active_workspace (); + foreach (var child in workspaces.get_children ()) { + var workspace = child as WorkspaceClone; + if (workspace.workspace == active) { + active_workspace = workspace; + break; + } + } + if (active_workspace != null) + workspaces.set_child_above_sibling (active_workspace, null); + + update_positions (false); + + foreach (var child in workspaces.get_children ()) { + if (opening) + (child as WorkspaceClone).open (); + else + (child as WorkspaceClone).close (); + } + + if (!opening) { + Timeout.add (290, () => { + hide (); + + plugin.background_group.show (); + plugin.window_group.show (); + plugin.top_window_group.show (); + + plugin.end_modal (); + + return false; + }); + } + + /** + * three types of animation for docks: + * - window appears to be at the bottom --> slide up + * - window appears to be at the top --> slide down + * - window appears to be somewhere else --> fade out + var rect = meta_window.get_outer_rect (); + + if (about_same (rect.y, monitor_geometry.y)) { + + float dest = opening ? monitor_geometry.y - rect.height : rect.y; + window.animate (AnimationMode.EASE_OUT_QUAD, HIDING_DURATION, y: dest); + + } else if (about_same (rect.y + rect.height, + monitor_geometry.y + monitor_geometry.height)) { + + float dest = opening ? monitor_geometry.y + monitor_geometry.height : rect.y; + window.animate (AnimationMode.EASE_OUT_QUAD, HIDING_DURATION, y: dest); + + } else { + uint dest = opening ? 0 : 255; + window.animate (AnimationMode.LINEAR, HIDING_DURATION, opacity: dest); + } + */ + } + + /** + * checks if val1 is about the same as val2 with a threshold of 2 by default + */ + private bool about_same (float val1, float val2, float threshold = 2.0f) + { + return Math.fabsf (val1 - val2) <= threshold; + } + } +} + diff --git a/src/Widgets/MultitaskingView/Workspace.vala b/src/Widgets/MultitaskingView/Workspace.vala new file mode 100644 index 00000000..19803346 --- /dev/null +++ b/src/Widgets/MultitaskingView/Workspace.vala @@ -0,0 +1,220 @@ +using Clutter; + +namespace Gala +{ + class FramedBackground : BackgroundManager + { + public FramedBackground (Meta.Screen screen) + { + base (screen); + + add_effect (new BackgroundShadowEffect (screen)); + } + + public override void paint () + { + base.paint (); + + Cogl.set_source_color4ub (0, 0, 0, 100); + Cogl.Path.rectangle (0, 0, width, height); + Cogl.Path.stroke (); + + Cogl.set_source_color4ub (255, 255, 255, 80); + Cogl.Path.rectangle (1, 1, width - 2, height - 2); + Cogl.Path.stroke (); + } + } + + class BackgroundShadowEffect : Effect + { + static Meta.Screen screen; + static Cogl.Texture? bitmap; + + const int SHADOW_SIZE = 40; + const int SHADOW_OFFSET = 5; + + static int width; + static int height; + + public BackgroundShadowEffect (Meta.Screen _screen) + { + if (bitmap == null) { + screen = _screen; + + int screen_width, screen_height; + screen.get_size (out screen_width, out screen_height); + + width = screen_width + SHADOW_SIZE * 2; + height = screen_height + SHADOW_SIZE * 2; + + var buffer = new Granite.Drawing.BufferSurface (width, height); + buffer.context.rectangle (SHADOW_SIZE - SHADOW_OFFSET, SHADOW_SIZE - SHADOW_OFFSET, + screen_width + SHADOW_OFFSET * 2, screen_height + SHADOW_OFFSET * 2); + buffer.context.set_source_rgba (0, 0, 0, 0.5); + buffer.context.fill (); + + buffer.exponential_blur (20); + + var surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, width, height); + var cr = new Cairo.Context (surface); + + cr.set_source_surface (buffer.surface, 0, 0); + cr.paint (); + + bitmap = new Cogl.Texture.from_data (width, height, 0, Cogl.PixelFormat.BGRA_8888_PRE, + Cogl.PixelFormat.ANY, surface.get_stride (), surface.get_data ()); + } + } + + public override void paint (EffectPaintFlags flags) + { + Cogl.set_source_texture (bitmap); + Cogl.rectangle (-SHADOW_SIZE, -SHADOW_SIZE, width - SHADOW_SIZE, height - SHADOW_SIZE); + + actor.continue_paint (); + } + } + + public class WorkspaceClone : Clutter.Actor + { + public Meta.Workspace workspace { get; construct set; } + public BackgroundManager background { get; private set; } + public IconGroup icon_group { get; private set; } + + public signal void window_selected (Meta.Window window); + public signal void selected (bool close_view); + + const int TOP_OFFSET = 20; + public static const int BOTTOM_OFFSET = 100; + + public WorkspaceClone (Meta.Workspace workspace) + { + Object (workspace: workspace); + + background = new FramedBackground (workspace.get_screen ()); + background.reactive = true; + background.button_press_event.connect (() => { + selected (true); + return false; + }); + + icon_group = new IconGroup (); + icon_group.selected.connect (() => { + selected (false); + }); + + var screen = workspace.get_screen (); + screen.window_left_monitor.connect ((monitor, window) => { + if (monitor == screen.get_primary_monitor ()) + remove_window (window); + }); + workspace.window_removed.connect (remove_window); + + screen.window_entered_monitor.connect ((monitor, window) => { + if (monitor == screen.get_primary_monitor ()) + add_window (window); + }); + workspace.window_added.connect (add_window); + + add_child (background); + } + + private void add_window (Meta.Window window) + { + } + + private void remove_window (Meta.Window window) + { + var window_actor = window.get_compositor_private (); + + foreach (var child in get_children ()) { + if (child is Clone && (child as Clone).source == window_actor) { + child.destroy (); + break; + } + } + } + + private void shrink_rectangle (ref Meta.Rectangle rect, int amount) + { + rect.x += amount; + rect.y += amount; + rect.width -= amount * 2; + rect.height -= amount * 2; + } + + public void open () + { + var screen = workspace.get_screen (); + var display = screen.get_display (); + + var monitor = screen.get_monitor_geometry (screen.get_primary_monitor ()); + var scale = (float)(monitor.height - TOP_OFFSET - BOTTOM_OFFSET) / monitor.height; + var pivotY = TOP_OFFSET / (monitor.height - monitor.height * scale); + background.set_pivot_point (0.5f, pivotY); + background.animate (AnimationMode.EASE_OUT_QUAD, 250, scale_x: scale, scale_y: scale); + + Meta.Rectangle area = { + (int)Math.floorf (monitor.x + monitor.width - monitor.width * scale) / 2, + (int)Math.floorf (monitor.y + TOP_OFFSET), + (int)Math.floorf (monitor.width * scale), + (int)Math.floorf (monitor.height * scale) + }; + shrink_rectangle (ref area, 32); + + icon_group.clear (); + + var unsorted_windows = workspace.list_windows (); + var used_windows = new SList (); + foreach (var window in unsorted_windows) { + if (window.window_type == Meta.WindowType.NORMAL) { + used_windows.append (window); + icon_group.add_window (window, true); + } + } + icon_group.redraw (); + + var windows = display.sort_windows_by_stacking (used_windows); + var clones = new List (); + + foreach (var window in windows) { + var window_actor = window.get_compositor_private () as Meta.WindowActor; + + var clone = new WindowThumb (window, false); + clone.selected.connect (() => { + window_selected (clone.window); + }); + clone.set_position (window_actor.x, window_actor.y); + + add_child (clone); + clones.append (clone); + } + + if (clones.length () > 0) + WindowOverview.grid_placement (area, clones, place_window); + } + + void place_window (Actor window, Meta.Rectangle rect) + { + window.animate (AnimationMode.EASE_OUT_CUBIC, 250, + x: rect.x + 0.0f, y: rect.y + 0.0f, width: rect.width + 0.0f, height: rect.height + 0.0f); + (window as WindowThumb).place_children (rect.width, rect.height); + } + + public void close () + { + background.animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, scale_x: 1.0f, scale_y: 1.0f); + + foreach (var child in get_children ()) { + if (child is WindowThumb) + (child as WindowThumb).close (true, false); + } + } + + ~Workspace () + { + background.destroy (); + } + } +} + diff --git a/src/Widgets/WindowOverview.vala b/src/Widgets/WindowOverview.vala index 85e8b225..6dc212e7 100644 --- a/src/Widgets/WindowOverview.vala +++ b/src/Widgets/WindowOverview.vala @@ -26,6 +26,8 @@ namespace Gala GRID = 0, NATURAL } + + public delegate void WindowPlacer (Actor window, Meta.Rectangle rect); public class WindowOverview : Actor { @@ -93,7 +95,7 @@ namespace Gala const int BOTTOM_GAP = 100; //some math utilities - int squared_distance (Gdk.Point a, Gdk.Point b) + static int squared_distance (Gdk.Point a, Gdk.Point b) { var k1 = b.x - a.x; var k2 = b.y - a.y; @@ -101,7 +103,7 @@ namespace Gala return k1*k1 + k2*k2; } - bool rect_is_overlapping_any (Meta.Rectangle rect, Meta.Rectangle[] rects, Meta.Rectangle border) + static bool rect_is_overlapping_any (Meta.Rectangle rect, Meta.Rectangle[] rects, Meta.Rectangle border) { if (!border.contains_rect (rect)) return true; @@ -116,12 +118,12 @@ namespace Gala return false; } - Meta.Rectangle rect_adjusted (Meta.Rectangle rect, int dx1, int dy1, int dx2, int dy2) + static Meta.Rectangle rect_adjusted (Meta.Rectangle rect, int dx1, int dy1, int dx2, int dy2) { return {rect.x + dx1, rect.y + dy1, rect.width + (-dx1 + dx2), rect.height + (-dy1 + dy2)}; } - Gdk.Point rect_center (Meta.Rectangle rect) + static Gdk.Point rect_center (Meta.Rectangle rect) { return {rect.x + rect.width / 2, rect.y + rect.height / 2}; } @@ -163,13 +165,13 @@ namespace Gala (int)Math.floorf (geom.height - BOTTOM_GAP)}; if (BehaviorSettings.get_default ().schema.get_enum ("window-overview-type") == WindowOverviewType.GRID) - grid_placement (area, monitors[i]); + grid_placement (area, monitors[i], place_window); else natural_placement (area, monitors[i]); } } - void grid_placement (Meta.Rectangle area, List clones) + public static void grid_placement (Meta.Rectangle area, List clones, WindowPlacer place) { int columns = (int)Math.ceil (Math.sqrt (clones.length ())); int rows = (int)Math.ceil (clones.length () / (double)columns); @@ -277,7 +279,7 @@ namespace Gala if (left_over != columns && slot >= columns * (rows - 1)) target.x += (columns - left_over) * slot_width / 2; - place_window (window, target); + place (window, target); } } @@ -492,8 +494,10 @@ namespace Gala } // animate a window to the given position - void place_window (WindowThumb clone, Meta.Rectangle rect) + void place_window (Actor actor, Meta.Rectangle rect) { + var clone = actor as WindowThumb; + var fscale = rect.width / clone.width; //animate the windows and icons to the calculated positions diff --git a/src/Widgets/WindowThumb.vala b/src/Widgets/WindowThumb.vala index a882d4b4..762c2b6a 100644 --- a/src/Widgets/WindowThumb.vala +++ b/src/Widgets/WindowThumb.vala @@ -32,7 +32,7 @@ namespace Gala public signal void selected (Window window); public signal void closed (); - public WindowThumb (Window _window) + public WindowThumb (Window _window, bool add_children_to_stage = true) { window = _window; @@ -40,10 +40,12 @@ namespace Gala var actor = window.get_compositor_private () as WindowActor; clone = new Clone (actor); + clone.add_constraint (new BindConstraint (this, BindCoordinate.SIZE, 0)); icon = new GtkClutter.Texture (); icon.scale_x = 0.0f; icon.scale_y = 0.0f; + icon.opacity = 0; icon.scale_gravity = Gravity.CENTER; try { @@ -65,9 +67,40 @@ namespace Gala add_child (clone); - var stage = Compositor.get_stage_for_screen (window.get_screen ()); - stage.add_child (icon); - stage.add_child (close_button); + if (add_children_to_stage) { + var stage = Compositor.get_stage_for_screen (window.get_screen ()); + stage.add_child (icon); + stage.add_child (close_button); + } else { + add_child (close_button); + add_child (icon); + } + } + + public void place_children (int width, int height) + { + float offset_x, offset_y, offset_width; + Utils.get_window_frame_offset (window, out offset_x, out offset_y, out offset_width, null); + float button_offset = close_button.width * 0.25f; + + float scale = width / (window.get_compositor_private () as WindowActor).width; + + Granite.CloseButtonPosition pos; + Granite.Widgets.Utils.get_default_close_button_position (out pos); + switch (pos) { + case Granite.CloseButtonPosition.LEFT: + close_button.x = -offset_x * scale - button_offset; + break; + case Granite.CloseButtonPosition.RIGHT: + close_button.x = width - offset_width * scale - close_button.width / 2; + break; + } + close_button.y = -offset_y * scale - button_offset; + + icon.x = Math.floorf (width / 2.0f - icon.width / 2.0f); + icon.y = Math.floorf (height - 50.0f); + + icon.animate (AnimationMode.EASE_OUT_CUBIC, 350, scale_x: 1.0f, scale_y: 1.0f, opacity: 255); } bool close_button_clicked (ButtonEvent event) @@ -155,15 +188,15 @@ namespace Gala return true; } - public void close (bool do_animate = true) + public void close (bool do_animate = true, bool use_scale = true) { unowned Meta.Rectangle rect = window.get_outer_rect (); - //FIXME need to subtract 10 here to remove jump for most windows, but adds jump for maximized ones - float delta = window.maximized_horizontally || window.maximized_vertically ? 0 : 10; + float x, y, w, h; + Utils.get_window_frame_offset (window, out x, out y, out w, out h); - float dest_x = rect.x - delta; - float dest_y = rect.y - delta; + float dest_x = rect.x + x; + float dest_y = rect.y + y; //stop all running animations detach_animation (); @@ -177,7 +210,17 @@ namespace Gala icon.animate (AnimationMode.EASE_IN_CUBIC, 100, scale_x:0.0f, scale_y:0.0f); close_button.animate (AnimationMode.EASE_IN_QUAD, 200, scale_x : 0.0f, scale_y : 0.0f); - animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, scale_x:1.0f, scale_y:1.0f, x:dest_x, y:dest_y).completed.connect (() => { + Animation a; + if (use_scale) { + a = animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, scale_x: 1.0f, scale_y: 1.0f, + x: dest_x, y: dest_y); + } else { + var window = window.get_compositor_private () as WindowActor; + a = animate (AnimationMode.EASE_IN_OUT_CUBIC, 300, width: window.width, height: window.height, + x: dest_x, y: dest_y); + } + + a.completed.connect (() => { clone.source.show (); destroy (); });