[gnome-shell/workspace-thumbnails] First draft of "Thumbnails" workspace management



commit 3a0c87d958a6089bfb09393ba370b775f57b269d
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Mon Jan 24 11:35:53 2011 -0500

    First draft of "Thumbnails" workspace management
    
    - Make workspace arrangement vertical
    - Add thumbnails to workspace controls on right
    - Make adding a new workspace not switch to it immediately
    - Remove old workspace indicators
    - Etc.
    
    Requires Mutter patch in https://bugzilla.gnome.org/show_bug.cgi?id=640552

 data/theme/gnome-shell.css  |   13 +-
 js/ui/main.js               |    8 +
 js/ui/windowManager.js      |   41 +++-
 js/ui/workspace.js          |    2 +-
 js/ui/workspaceThumbnail.js |  288 ++++++++++++++++++++
 js/ui/workspacesView.js     |  610 +++++++++++++++++++++----------------------
 6 files changed, 643 insertions(+), 319 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index cf6f1eb..4ad9c31 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -275,7 +275,6 @@ StTooltip StLabel {
 }
 
 .workspace-controls {
-    width: 48px;
     font-size: 32px;
     font-weight: bold;
     color: #ffffff;
@@ -284,12 +283,20 @@ StTooltip StLabel {
     border-radius: 9px 0px 0px 9px;
 }
 
+.workspace-thumbnails {
+    spacing: 8px;
+    border: 8px transparent;
+}
+
+.workspace-thumbnail-indicator {
+    outline: 4px solid white;
+}
+
 .add-workspace {
-    background-color: rgba(128, 128, 128, 0.4);
 }
 
 .add-workspace:hover {
-    background-color: rgba(128, 128, 128, 0.6);
+    background-color: rgba(128, 128, 128, 0.2);
 }
 
 .remove-workspace {
diff --git a/js/ui/main.js b/js/ui/main.js
index 1d456af..c8e87e1 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -172,6 +172,8 @@ function start() {
         }
     });
 
+    global.screen.override_workspace_layout(Meta.ScreenCorner.TOPLEFT, false, -1, 1);
+
     // Provide the bus object for gnome-session to
     // initiate logouts.
     EndSessionDialog.init();
@@ -346,6 +348,12 @@ function _globalKeyPressHandler(actor, event) {
         case Meta.KeyBindingAction.WORKSPACE_RIGHT:
             wm.actionMoveWorkspaceRight();
             return true;
+        case Meta.KeyBindingAction.WORKSPACE_UP:
+            wm.actionMoveWorkspaceUp();
+            return true;
+        case Meta.KeyBindingAction.WORKSPACE_DOWN:
+            wm.actionMoveWorkspaceDown();
+            return true;
         case Meta.KeyBindingAction.PANEL_RUN_DIALOG:
         case Meta.KeyBindingAction.COMMAND_2:
             getRunDialog().open();
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index 447e479..a6c6ffc 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -525,23 +525,20 @@ WindowManager.prototype = {
     },
 
     _showWorkspaceSwitcher : function(shellwm, binding, window, backwards) {
-        /* We don't support this kind of layout */
-        if (binding == 'switch_to_workspace_up' || binding == 'switch_to_workspace_down')
-            return;
-
         if (global.screen.n_workspaces == 1)
             return;
 
         if (this._workspaceSwitcherPopup == null)
             this._workspaceSwitcherPopup = new WorkspaceSwitcherPopup.WorkspaceSwitcherPopup();
 
-        if (binding == 'switch_to_workspace_left') {
+        if (binding == 'switch_to_workspace_left')
             this.actionMoveWorkspaceLeft();
-        }
-
-        if (binding == 'switch_to_workspace_right') {
+        else if (binding == 'switch_to_workspace_right')
             this.actionMoveWorkspaceRight();
-        }
+        else if (binding == 'switch_to_workspace_up')
+            this.actionMoveWorkspaceUp();
+        else if (binding == 'switch_to_workspace_down')
+            this.actionMoveWorkspaceDown();
     },
 
     actionMoveWorkspaceLeft: function() {
@@ -574,5 +571,31 @@ WindowManager.prototype = {
 
         if (!Main.overview.visible)
             this._workspaceSwitcherPopup.display(WorkspaceSwitcherPopup.RIGHT, indexToActivate);
+    },
+
+    actionMoveWorkspaceUp: function() {
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        let indexToActivate = activeWorkspaceIndex;
+        if (activeWorkspaceIndex > 0)
+            indexToActivate--;
+
+        if (indexToActivate != activeWorkspaceIndex)
+            global.screen.get_workspace_by_index(indexToActivate).activate(global.get_current_time());
+
+        if (!Main.overview.visible)
+            this._workspaceSwitcherPopup.display(WorkspaceSwitcherPopup.LEFT, indexToActivate);
+    },
+
+    actionMoveWorkspaceDown: function() {
+        let activeWorkspaceIndex = global.screen.get_active_workspace_index();
+        let indexToActivate = activeWorkspaceIndex;
+        if (activeWorkspaceIndex < global.screen.n_workspaces - 1)
+            indexToActivate++;
+
+        if (indexToActivate != activeWorkspaceIndex)
+            global.screen.get_workspace_by_index(indexToActivate).activate(global.get_current_time());
+
+        if (!Main.overview.visible)
+            this._workspaceSwitcherPopup.display(WorkspaceSwitcherPopup.RIGHT, indexToActivate);
     }
 };
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index e44f558..7d40716 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -1391,7 +1391,7 @@ Workspace.prototype = {
     },
 
     acceptDrop : function(source, actor, x, y, time) {
-        if (source instanceof WindowClone) {
+        if (source.realWindow) {
             let win = source.realWindow;
             if (this._isMyWindow(win))
                 return false;
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
new file mode 100644
index 0000000..3588627
--- /dev/null
+++ b/js/ui/workspaceThumbnail.js
@@ -0,0 +1,288 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const DND = imports.ui.dnd;
+const Main = imports.ui.main;
+const Workspace = imports.ui.workspace;
+
+function WindowClone(realWindow) {
+    this._init(realWindow);
+}
+
+WindowClone.prototype = {
+    _init : function(realWindow) {
+        this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
+                                         clip_to_allocation: true,
+                                         reactive: true,
+                                         x: realWindow.x,
+                                         y: realWindow.y });
+        this.actor._delegate = this;
+        this.realWindow = realWindow;
+        this.metaWindow = realWindow.meta_window;
+        this.metaWindow._delegate = this;
+
+        this.actor.connect('button-release-event',
+                           Lang.bind(this, this._onButtonRelease));
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        this._draggable = DND.makeDraggable(this.actor,
+                                            { restoreOnSuccess: true,
+                                              dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
+                                              dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY });
+        this._draggable.connect('drag-begin', Lang.bind(this, this._onDragBegin));
+        this._draggable.connect('drag-end', Lang.bind(this, this._onDragEnd));
+        this.inDrag = false;
+
+        this._selected = false;
+    },
+
+    destroy: function () {
+        this.actor.destroy();
+    },
+
+    _onDestroy: function() {
+        this.metaWindow._delegate = null;
+        this.actor._delegate = null;
+
+        if (this.inDrag) {
+            this.emit('drag-end');
+            this.inDrag = false;
+        }
+
+        this.disconnectAll();
+    },
+
+    _onButtonRelease : function (actor, event) {
+        this._selected = true;
+        this.emit('selected', event.get_time());
+    },
+
+    _onDragBegin : function (draggable, time) {
+        this.inDrag = true;
+        this.emit('drag-begin');
+    },
+
+    _onDragEnd : function (draggable, time, snapback) {
+        this.inDrag = false;
+
+        // We may not have a parent if DnD completed successfully, in
+        // which case our clone will shortly be destroyed and replaced
+        // with a new one on the target workspace.
+        if (this.actor.get_parent() != null) {
+            if (this._stackAbove == null)
+                this.actor.lower_bottom();
+            else
+                this.actor.raise(this._stackAbove);
+        }
+
+
+        this.emit('drag-end');
+    }
+};
+Signals.addSignalMethods(WindowClone.prototype);
+
+
+/**
+ * @metaWorkspace: a #Meta.Workspace
+ */
+function WorkspaceThumbnail(metaWorkspace) {
+    this._init(metaWorkspace);
+}
+
+WorkspaceThumbnail.prototype = {
+    _init : function(metaWorkspace) {
+        // When dragging a window, we use this slot for reserve space.
+        this.metaWorkspace = metaWorkspace;
+
+        this.actor = new St.Bin({ reactive: true,
+                                  style_class: 'workspace-thumbnail' });
+        this.actor._delegate = this;
+
+        this._group = new Clutter.Group();
+        this.actor.add_actor(this._group);
+
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+        this.actor.connect('button-press-event', Lang.bind(this,
+            function(actor, event) {
+                return true;
+            }));
+        this.actor.connect('button-release-event', Lang.bind(this,
+            function(actor, event) {
+                this.metaWorkspace.activate(event.get_time());
+                return true;
+            }));
+
+        this._background = new Clutter.Clone({ source: global.background_actor });
+        this._group.add_actor(this._background);
+
+        this._group.set_size(global.screen_width / 10., global.screen_height / 10.);
+        this._group.set_scale(1/10., 1/10.);
+
+        let windows = global.get_window_actors().filter(this._isMyWindow, this);
+
+        // Create clones for windows that should be visible in the Overview
+        this._windows = [];
+        for (let i = 0; i < windows.length; i++) {
+            if (this._isOverviewWindow(windows[i])) {
+                this._addWindowClone(windows[i]);
+            }
+        }
+
+        // Track window changes
+        this._windowAddedId = this.metaWorkspace.connect('window-added',
+                                                          Lang.bind(this, this._windowAdded));
+        this._windowRemovedId = this.metaWorkspace.connect('window-removed',
+                                                           Lang.bind(this, this._windowRemoved));
+    },
+
+    _lookupIndex: function (metaWindow) {
+        for (let i = 0; i < this._windows.length; i++) {
+            if (this._windows[i].metaWindow == metaWindow) {
+                return i;
+            }
+        }
+        return -1;
+    },
+
+    syncStacking: function(stackIndices) {
+        let visibleClones = this._getVisibleClones();
+        visibleClones.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
+
+        for (let i = 0; i < visibleClones.length; i++) {
+            let clone = visibleClones[i];
+            let metaWindow = clone.metaWindow;
+            if (i == 0) {
+                clone.setStackAbove(null);
+            } else {
+                let previousClone = visibleClones[i - 1];
+                clone.setStackAbove(previousClone.actor);
+            }
+        }
+    },
+
+    _windowRemoved : function(metaWorkspace, metaWin) {
+        let win = metaWin.get_compositor_private();
+
+        // find the position of the window in our list
+        let index = this._lookupIndex (metaWin);
+
+        if (index == -1)
+            return;
+
+        let clone = this._windows[index];
+        this._windows.splice(index, 1);
+        clone.destroy();
+    },
+
+    _windowAdded : function(metaWorkspace, metaWin) {
+        if (this.leavingOverview)
+            return;
+
+        let win = metaWin.get_compositor_private();
+
+        if (!win) {
+            // Newly-created windows are added to a workspace before
+            // the compositor finds out about them...
+            Mainloop.idle_add(Lang.bind(this,
+                                        function () {
+                                            if (this.actor && metaWin.get_compositor_private())
+                                                this._windowAdded(metaWorkspace, metaWin);
+                                            return false;
+                                        }));
+            return;
+        }
+
+        if (!this._isOverviewWindow(win))
+            return;
+
+        let clone = this._addWindowClone(win);
+    },
+
+    destroy : function() {
+        this.actor.destroy();
+    },
+
+    _onDestroy: function(actor) {
+        this.metaWorkspace.disconnect(this._windowAddedId);
+        this.metaWorkspace.disconnect(this._windowRemovedId);
+
+        this._windows = [];
+        this.actor = null;
+    },
+
+    // Tests if @win belongs to this workspaces
+    _isMyWindow : function (win) {
+        return win.get_workspace() == this.metaWorkspace.index() ||
+            (win.get_meta_window() && win.get_meta_window().is_on_all_workspaces());
+    },
+
+    // Tests if @win should be shown in the Overview
+    _isOverviewWindow : function (win) {
+        let tracker = Shell.WindowTracker.get_default();
+        return tracker.is_window_interesting(win.get_meta_window());
+    },
+
+    // Create a clone of a (non-desktop) window and add it to the window list
+    _addWindowClone : function(win) {
+        let clone = new WindowClone(win);
+
+        clone.connect('selected',
+                      Lang.bind(this, this._onCloneSelected));
+        clone.connect('drag-begin',
+                      Lang.bind(this, function(clone) {
+                          Main.overview.beginWindowDrag();
+                      }));
+        clone.connect('drag-end',
+                      Lang.bind(this, function(clone) {
+                          Main.overview.endWindowDrag();
+                      }));
+        this._group.add_actor(clone.actor);
+
+        this._windows.push(clone);
+
+        return clone;
+    },
+
+    _onCloneSelected : function (clone, time) {
+        this.metaWorkspace.activate(time);
+    },
+
+    // Draggable target interface
+    handleDragOver : function(source, actor, x, y, time) {
+        if (source.realWindow)
+            return DND.DragMotionResult.MOVE_DROP;
+        if (source.shellWorkspaceLaunch)
+            return DND.DragMotionResult.COPY_DROP;
+
+        return DND.DragMotionResult.CONTINUE;
+    },
+
+    acceptDrop : function(source, actor, x, y, time) {
+        if (source.realWindow) {
+            let win = source.realWindow;
+            if (this._isMyWindow(win))
+                return false;
+
+            let metaWindow = win.get_meta_window();
+            metaWindow.change_workspace_by_index(this.metaWorkspace.index(),
+                                                 false, // don't create workspace
+                                                 time);
+            return true;
+        } else if (source.shellWorkspaceLaunch) {
+            this.metaWorkspace.activate(time);
+            source.shellWorkspaceLaunch();
+            return true;
+        }
+
+        return false;
+    }
+};
+
+Signals.addSignalMethods(WorkspaceThumbnail.prototype);
diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js
index 40b7320..9323116 100644
--- a/js/ui/workspacesView.js
+++ b/js/ui/workspacesView.js
@@ -14,29 +14,29 @@ const Main = imports.ui.main;
 const Overview = imports.ui.overview;
 const Tweener = imports.ui.tweener;
 const Workspace = imports.ui.workspace;
+const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
 
 const WORKSPACE_SWITCH_TIME = 0.25;
 // Note that mutter has a compile-time limit of 36
 const MAX_WORKSPACES = 16;
 
-const WORKSPACE_DRAGGING_SCALE = 0.85;
-
 
 const CONTROLS_POP_IN_FRACTION = 0.8;
 const CONTROLS_POP_IN_TIME = 0.1;
 
-const INDICATOR_HOVER_SCALE = 1.1;
-
 
-function WorkspacesView(width, height, x, y, workspaces) {
-    this._init(width, height, x, y, workspaces);
+function WorkspacesView(width, height, x, y, zoomScale, workspaces) {
+    this._init(width, height, x, y, zoomScale, workspaces);
 }
 
 WorkspacesView.prototype = {
-    _init: function(width, height, x, y, workspaces) {
+    _init: function(width, height, x, y, zoomScale, workspaces) {
         this.actor = new St.Group({ style_class: 'workspaces-view' });
         this.actor.set_clip(x, y, width, height);
 
+        // The actor itself isn't a drop target, so we don't want to pick on its aea
+        this.actor.set_size(0, 0);
+
         this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
 
         this.actor.connect('style-changed', Lang.bind(this,
@@ -52,6 +52,7 @@ WorkspacesView.prototype = {
         this._height = height;
         this._x = x;
         this._y = y;
+        this._zoomScale = zoomScale;
         this._spacing = 0;
         this._activeWorkspaceX = 0; // x offset of active ws while dragging
         this._activeWorkspaceY = 0; // y offset of active ws while dragging
@@ -59,6 +60,7 @@ WorkspacesView.prototype = {
         this._animating = false; // tweening
         this._scrolling = false; // swipe-scrolling
         this._animatingScroll = false; // programatically updating the adjustment
+        this._zoomOut = false; // zoom to a larger area
         this._inDrag = false; // dragging a window
 
         let activeWorkspaceIndex = global.screen.get_active_workspace_index();
@@ -244,7 +246,6 @@ WorkspacesView.prototype = {
         } else {
             let currentTime = global.get_current_time();
             ws = global.screen.append_new_workspace(false, currentTime);
-            ws.activate(currentTime);
         }
 
         return ws;
@@ -265,8 +266,26 @@ WorkspacesView.prototype = {
                                        global.get_current_time());
     },
 
+    zoomOut: function() {
+        if (this._zoomOut)
+            return;
+
+        this._zoomOut = true;
+        this._computeWorkspacePositions();
+        this._updateWorkspaceActors(true);
+    },
+
+    zoomIn: function() {
+        if (!this._zoomOut)
+            return;
+
+        this._zoomOut = false;
+        this._computeWorkspacePositions();
+        this._updateWorkspaceActors(true);
+    },
+
     _handleDragOverNewWorkspace: function(source, dropActor, x, y, time) {
-        if (source instanceof Workspace.WindowClone)
+        if (source.realWindow)
             return DND.DragMotionResult.MOVE_DROP;
         if (source.shellWorkspaceLaunch)
             return DND.DragMotionResult.COPY_DROP;
@@ -286,8 +305,8 @@ WorkspacesView.prototype = {
         let active = global.screen.get_active_workspace_index();
 
         let scale = this._width / global.screen_width;
-        if (this._inDrag)
-            scale *= WORKSPACE_DRAGGING_SCALE;
+        if (this._zoomOut)
+            scale *= this._zoomScale;
 
         let _width = this._workspaces[0].actor.width * scale;
         let _height = this._workspaces[0].actor.height * scale;
@@ -301,14 +320,19 @@ WorkspacesView.prototype = {
             workspace.opacity = (this._inDrag && w != active) ? 200 : 255;
 
             workspace.scale = scale;
-            if (St.Widget.get_default_direction() == St.TextDirection.RTL) {
-                workspace.x = this._x + this._activeWorkspaceX
-                              - (w - active) * (_width + this._spacing);
-            } else {
-                workspace.x = this._x + this._activeWorkspaceX
-                              + (w - active) * (_width + this._spacing);
-            }
-            workspace.y = this._y + this._activeWorkspaceY;
+            workspace.x = this._x + this._activeWorkspaceX;
+
+            // We adjust the center because the zoomScale is to leave space for
+            // the expanded workspace control so we want to zoom to either the
+            // left part of the area or the right part of the area
+            let offset = 0.5 * (1 - this._zoomScale) * this._width;
+            let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
+            if (this._zoomOut)
+                workspace.x += rtl ? offset : - offset;
+
+            workspace.y = this._y + this._activeWorkspaceY
+                              + (w - active) * (_height + this._spacing);
+
         }
     },
 
@@ -325,9 +349,9 @@ WorkspacesView.prototype = {
     // @showAnimation: iff %true, transition between states
     _updateWorkspaceActors: function(showAnimation) {
         let active = global.screen.get_active_workspace_index();
-        let targetWorkspaceNewX = this._x + this._activeWorkspaceX;
-        let targetWorkspaceCurrentX = this._workspaces[active].x;
-        let dx = targetWorkspaceNewX - targetWorkspaceCurrentX;
+        let targetWorkspaceNewY = this._y + this._activeWorkspaceY;
+        let targetWorkspaceCurrentY = this._workspaces[active].y;
+        let dy = targetWorkspaceNewY - targetWorkspaceCurrentY;
 
         this._animating = showAnimation;
 
@@ -336,7 +360,7 @@ WorkspacesView.prototype = {
 
             Tweener.removeTweens(workspace.actor);
 
-            workspace.x += dx;
+            workspace.y += dy;
 
             if (showAnimation) {
                 let params = { x: workspace.x,
@@ -373,13 +397,13 @@ WorkspacesView.prototype = {
 
             Tweener.removeTweens(workspace.actor);
 
-            workspace.x += dx;
+            workspace.y += dy;
             workspace.actor.show();
             workspace.hideWindowsOverlays();
 
             if (showAnimation) {
                 Tweener.addTween(workspace.actor,
-                                 { x: workspace.x,
+                                 { y: workspace.x,
                                    time: WORKSPACE_SWITCH_TIME,
                                    transition: 'easeOutQuad',
                                    onComplete: Lang.bind(this,
@@ -504,7 +528,7 @@ WorkspacesView.prototype = {
 
     _onMappedChanged: function() {
         if (this.actor.mapped) {
-            let direction = Overview.SwipeScrollDirection.HORIZONTAL;
+            let direction = Overview.SwipeScrollDirection.VERTICAL;
             Main.overview.setScrollAdjustment(this._scrollAdjustment,
                                               direction);
             this._swipeScrollBeginId = Main.overview.connect('swipe-scroll-begin',
@@ -522,8 +546,6 @@ WorkspacesView.prototype = {
             return;
 
         this._inDrag = true;
-        this._computeWorkspacePositions();
-        this._updateWorkspaceActors(true);
 
         this._dragMonitor = {
             dragMotion: Lang.bind(this, this._onDragMotion)
@@ -538,56 +560,51 @@ WorkspacesView.prototype = {
         let primary = global.get_primary_monitor();
 
         let activeWorkspaceIndex = global.screen.get_active_workspace_index();
-        let leftWorkspace, rightWorkspace;
-        if (St.Widget.get_default_direction() == St.TextDirection.RTL) {
-            leftWorkspace  = this._workspaces[activeWorkspaceIndex + 1];
-            rightWorkspace = this._workspaces[activeWorkspaceIndex - 1];
-        } else {
-            leftWorkspace  = this._workspaces[activeWorkspaceIndex - 1];
-            rightWorkspace = this._workspaces[activeWorkspaceIndex + 1];
-        }
+        let topWorkspace, bottomWorkspace;
+        topWorkspace  = this._workspaces[activeWorkspaceIndex - 1];
+        bottomWorkspace = this._workspaces[activeWorkspaceIndex + 1];
         let hoverWorkspace = null;
 
         // reactive monitor edges
-        let leftEdge = primary.x;
-        let switchLeft = (dragEvent.x <= leftEdge && leftWorkspace);
-        if (switchLeft && this._dragOverLastX != leftEdge) {
-            leftWorkspace.metaWorkspace.activate(global.get_current_time());
-            leftWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
-            this._dragOverLastX = leftEdge;
+        let topEdge = primary.y;
+        let switchTop = (dragEvent.y <= topEdge && topWorkspace);
+        if (switchTop && this._dragOverLastY != topEdge) {
+            topWorkspace.metaWorkspace.activate(global.get_current_time());
+            topWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
+            this._dragOverLastY = topEdge;
 
             return DND.DragMotionResult.CONTINUE;
         }
-        let rightEdge = primary.x + primary.width - 1;
-        let switchRight = (dragEvent.x >= rightEdge && rightWorkspace);
-        if (switchRight && this._dragOverLastX != rightEdge) {
-            rightWorkspace.metaWorkspace.activate(global.get_current_time());
-            rightWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
-            this._dragOverLastX = rightEdge;
+        let bottomEdge = primary.y + primary.height - 1;
+        let switchBottom = (dragEvent.y >= bottomEdge && bottomWorkspace);
+        if (switchBottom && this._dragOverLastY != bottomEdge) {
+            bottomWorkspace.metaWorkspace.activate(global.get_current_time());
+            bottomWorkspace.setReservedSlot(dragEvent.dragActor._delegate);
+            this._dragOverLastY = bottomEdge;
 
             return DND.DragMotionResult.CONTINUE;
         }
-        this._dragOverLastX = dragEvent.x;
+        this._dragOverLastY = dragEvent.y;
         let result = DND.DragMotionResult.CONTINUE;
 
         // check hover state of new workspace area / inactive workspaces
-        if (leftWorkspace) {
-            if (leftWorkspace.actor.contains(dragEvent.targetActor)) {
-                hoverWorkspace = leftWorkspace;
-                leftWorkspace.opacity = leftWorkspace.actor.opacity = 255;
-                result = leftWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
+        if (topWorkspace) {
+            if (topWorkspace.actor.contains(dragEvent.targetActor)) {
+                hoverWorkspace = topWorkspace;
+                topWorkspace.opacity = topWorkspace.actor.opacity = 255;
+                result = topWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
             } else {
-                leftWorkspace.opacity = leftWorkspace.actor.opacity = 200;
+                topWorkspace.opacity = topWorkspace.actor.opacity = 200;
             }
         }
 
-        if (rightWorkspace) {
-            if (rightWorkspace.actor.contains(dragEvent.targetActor)) {
-                hoverWorkspace = rightWorkspace;
-                rightWorkspace.opacity = rightWorkspace.actor.opacity = 255;
-                result = rightWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
+        if (bottomWorkspace) {
+            if (bottomWorkspace.actor.contains(dragEvent.targetActor)) {
+                hoverWorkspace = bottomWorkspace;
+                bottomWorkspace.opacity = bottomWorkspace.actor.opacity = 255;
+                result = bottomWorkspace.handleDragOver(dragEvent.source, dragEvent.dragActor);
             } else {
-                rightWorkspace.opacity = rightWorkspace.actor.opacity = 200;
+                bottomWorkspace.opacity = bottomWorkspace.actor.opacity = 200;
             }
         }
 
@@ -617,8 +634,6 @@ WorkspacesView.prototype = {
         }
         DND.removeMonitor(this._dragMonitor);
         this._inDrag = false;
-        this._computeWorkspacePositions();
-        this._updateWorkspaceActors(true);
 
         for (let i = 0; i < this._workspaces.length; i++)
             this._workspaces[i].setReservedSlot(null);
@@ -670,22 +685,22 @@ WorkspacesView.prototype = {
         }
 
         let last = this._workspaces.length - 1;
-        let firstWorkspaceX = this._workspaces[0].actor.x;
-        let lastWorkspaceX = this._workspaces[last].actor.x;
-        let workspacesWidth = lastWorkspaceX - firstWorkspaceX;
+        let firstWorkspaceY = this._workspaces[0].actor.y;
+        let lastWorkspaceY = this._workspaces[last].actor.y;
+        let workspacesHeight = lastWorkspaceY - firstWorkspaceY;
 
         if (adj.upper == 1)
             return;
 
-        let currentX = firstWorkspaceX;
-        let newX = this._x - adj.value / (adj.upper - 1) * workspacesWidth;
+        let currentY = firstWorkspaceY;
+        let newY = this._y - adj.value / (adj.upper - 1) * workspacesHeight;
 
-        let dx = newX - currentX;
+        let dy = newY - currentY;
 
         for (let i = 0; i < this._workspaces.length; i++) {
             this._workspaces[i]._hideAllOverlays();
             this._workspaces[i].actor.visible = Math.abs(i - adj.value) <= 1;
-            this._workspaces[i].actor.x += dx;
+            this._workspaces[i].actor.y += dy;
         }
     },
 
@@ -696,155 +711,6 @@ WorkspacesView.prototype = {
 Signals.addSignalMethods(WorkspacesView.prototype);
 
 
-function WorkspaceIndicatorPanel() {
-    this._init();
-}
-
-WorkspaceIndicatorPanel.prototype = {
-    _init: function() {
-        this.actor = new Shell.GenericContainer({ clip_to_allocation: true });
-        this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
-        this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
-        this.actor.connect('allocate', Lang.bind(this, this._allocate));
-
-        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
-
-        this._box = new St.BoxLayout({ style_class: 'workspace-indicator-panel' });
-        this.actor.add_actor(this._box);
-
-        this._switchWorkspaceNotifyId =
-            global.window_manager.connect('switch-workspace',
-                                          Lang.bind(this, this._updateActive));
-    },
-
-    _onDestroy: function() {
-        if (this._switchWorkspaceNotifyId > 0)
-            global.window_manager.disconnect(this._switchWorkspaceNotifyId);
-        this._switchWorkspaceNotifyId = 0;
-        this._workspaces = null;
-    },
-
-    // Allocate the box centered to the available area like StBin would do,
-    // except that the full height is used even if the box is not actually
-    // shown. This is a workaround, as the size of the workspace area is
-    // determined once when entering the overview, so if it would take up
-    // the indicator space in that case, it would overlap it later when
-    // additional workspaces were added.
-    _allocate: function(actor, box, flags) {
-        let children = this._box.get_children();
-
-        let availWidth = box.x2 - box.x1;
-        let availHeight = box.y2 - box.y1;
-        let [minWidth, natWidth] = this._box.get_preferred_width(-1);
-        let [minHeight, natHeight] = this._box.get_preferred_height(-1);
-
-        let childBox = new Clutter.ActorBox();
-        childBox.x1 = Math.floor((availWidth - natWidth) / 2);
-        childBox.x2 = childBox.x1 + natWidth;
-        childBox.y1 = Math.floor((availHeight - natHeight) / 2);
-        childBox.y2 = childBox.y2 + natHeight;
-
-        this._box.allocate(childBox, flags);
-    },
-
-    _getPreferredWidth: function(actor, forHeight, alloc) {
-        let [minWidth, natWidth] = this._box.get_preferred_width(-1);
-        alloc.min_size = 0;
-        alloc.natural_size = natWidth;
-    },
-
-    _getPreferredHeight: function(actor, forWidth, alloc) {
-        let [minHeight, natHeight] = this._box.get_preferred_height(-1);
-        alloc.min_size = minHeight * INDICATOR_HOVER_SCALE;
-        alloc.natural_size = natHeight * INDICATOR_HOVER_SCALE;
-    },
-
-    updateWorkspaces: function(workspaces) {
-        this._workspaces = workspaces;
-
-        // Do not display a single indicator
-        if (this._workspaces.length == 1)
-            this.actor.set_skip_paint(this._box, true);
-        else
-            this.actor.set_skip_paint(this._box, false);
-
-        this._box.remove_all();
-        for (let i = 0; i < this._workspaces.length; i++) {
-            let actor = new St.Button({ style_class: 'workspace-indicator',
-                                        track_hover: true });
-            let workspace = this._workspaces[i];
-            let metaWorkspace = this._workspaces[i].metaWorkspace;
-
-            actor.connect('clicked', Lang.bind(this, function() {
-                metaWorkspace.activate(global.get_current_time());
-            }));
-            actor.connect('notify::hover', Lang.bind(this, function() {
-                if (actor.hover)
-                    actor.set_scale_with_gravity(INDICATOR_HOVER_SCALE,
-                                                 INDICATOR_HOVER_SCALE,
-                                                 Clutter.Gravity.CENTER);
-                else
-                    actor.set_scale(1.0, 1.0);
-            }));
-
-            actor._delegate = {
-                acceptDrop: Lang.bind(this,
-                    function(source, actor, x, y, time) {
-                        if (workspace.acceptDrop(source, actor, x, y, time)) {
-                            metaWorkspace.activate(time);
-                            return true;
-                        }
-                        return false;
-                    }),
-                handleDragOver: Lang.bind(this,
-                    function(source, actor, x, y, time) {
-                        return workspace.handleDragOver(source, actor, x, y, time);
-                    })
-            };
-
-            actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
-
-            this._box.add(actor);
-        }
-
-        this._updateActive();
-    },
-
-    _updateActive: function() {
-        let children = this._box.get_children();
-        let activeIndex = global.screen.get_active_workspace_index();
-        for (let i = 0; i < children.length; i++) {
-            if (i == activeIndex)
-                children[i].add_style_class_name('active');
-            else
-                children[i].remove_style_class_name('active');
-        }
-    },
-
-    // handle scroll wheel events:
-    // activate the next or previous workspace and let the signal handler
-    // manage the animation
-    _onScrollEvent: function(actor, event) {
-        let direction = event.get_scroll_direction();
-        let current = global.screen.get_active_workspace_index();
-        let last = global.screen.n_workspaces - 1;
-        let activate = current;
-
-        let difference = direction == Clutter.ScrollDirection.UP ? -1 : 1;
-        if (St.Widget.get_default_direction() == St.TextDirection.RTL)
-            difference *= -1;
-
-        if (activate + difference >= 0 && activate + difference <= last)
-            activate += difference;
-
-        if (activate != current) {
-            let metaWorkspace = this._workspaces[activate].metaWorkspace;
-            metaWorkspace.activate(global.get_current_time());
-        }
-    }
-};
-
-
 function WorkspaceControlsContainer(controls) {
     this._init(controls);
 }
@@ -861,50 +727,13 @@ WorkspaceControlsContainer.prototype = {
         this.actor.add_actor(controls);
 
         this._controls = controls;
-        this._controls.reactive = true;
-        this._controls.track_hover = true;
-        this._controls.connect('notify::hover',
-                               Lang.bind(this, this._onHoverChanged));
-
-        this._itemDragBeginId = 0;
-        this._itemDragEndId = 0;
-        this._windowDragBeginId = 0;
-        this._windowDragEndId = 0;
     },
 
     show: function() {
-        if (this._itemDragBeginId == 0)
-            this._itemDragBeginId = Main.overview.connect('item-drag-begin',
-                                                          Lang.bind(this, this.popOut));
-        if (this._itemDragEndId == 0)
-            this._itemDragEndId = Main.overview.connect('item-drag-end',
-                                                        Lang.bind(this, this.popIn));
-        if (this._windowDragBeginId == 0)
-            this._windowDragBeginId = Main.overview.connect('window-drag-begin',
-                                                            Lang.bind(this, this.popOut));
-        if (this._windowDragEndId == 0)
-            this._windowDragEndId = Main.overview.connect('window-drag-end',
-                                                          Lang.bind(this, this.popIn));
         this._controls.x = this._poppedInX();
     },
 
     hide: function() {
-        if (this._itemDragBeginId > 0) {
-            Main.overview.disconnect(this._itemDragBeginId);
-            this._itemDragBeginId = 0;
-        }
-        if (this._itemEndBeginId > 0) {
-            Main.overview.disconnect(this._itemDragEndId);
-            this._itemDragEndId = 0;
-        }
-        if (this._windowDragBeginId > 0) {
-            Main.overview.disconnect(this._windowDragBeginId);
-            this._windowDragBeginId = 0;
-        }
-        if (this._windowDragEndId > 0) {
-            Main.overview.disconnect(this._windowDragEndId);
-            this._windowDragEndId = 0;
-        }
     },
 
     _getPreferredWidth: function(actor, forHeight, alloc) {
@@ -935,13 +764,6 @@ WorkspaceControlsContainer.prototype = {
         this._controls.allocate(childBox, flags);
     },
 
-    _onHoverChanged: function() {
-        if (this._controls.hover)
-            this.popOut();
-        else
-            this.popIn();
-    },
-
     _poppedInX: function() {
         let x = CONTROLS_POP_IN_FRACTION * this._controls.width;
         if (St.Widget.get_default_direction() == St.TextDirection.RTL)
@@ -956,12 +778,6 @@ WorkspaceControlsContainer.prototype = {
                            transition: 'easeOutQuad' });
     },
 
-    popIn: function() {
-        Tweener.addTween(this._controls,
-                         { x: this._poppedInX(),
-                           time: CONTROLS_POP_IN_TIME,
-                           transition: 'easeOutQuad' });
-    }
 };
 
 function WorkspacesDisplay() {
@@ -970,22 +786,18 @@ function WorkspacesDisplay() {
 
 WorkspacesDisplay.prototype = {
     _init: function() {
-        this.actor = new St.BoxLayout();
-
-        let workspacesBox = new St.BoxLayout({ vertical: true });
-        this.actor.add(workspacesBox, { expand: true });
-
-        // placeholder for window previews
-        this._workspacesBin = new St.Bin();
-        workspacesBox.add(this._workspacesBin, { expand: true });
-
-        this._workspaceIndicatorPanel = new WorkspaceIndicatorPanel();
-        workspacesBox.add(this._workspaceIndicatorPanel.actor);
+        this.actor = new St.Group();
+        this.actor.set_size(0, 0);
 
         let controls = new St.BoxLayout({ vertical: true,
                                           style_class: 'workspace-controls' });
-        this._controlsContainer = new WorkspaceControlsContainer(controls);
-        this.actor.add(this._controlsContainer.actor);
+        this._controls = controls;
+        this.actor.add_actor(controls);
+
+        controls.reactive = true;
+        controls.track_hover = true;
+        controls.connect('notify::hover',
+                         Lang.bind(this, this._onControlsHoverChanged));
 
         // Add/remove workspace buttons
         this._removeButton = new St.Button({ label: '&#8211;', // n-dash
@@ -995,6 +807,27 @@ WorkspacesDisplay.prototype = {
         }));
         controls.add(this._removeButton);
 
+        this._thumbnailsBox = new St.BoxLayout({ vertical: true,
+                                                 style_class: 'workspace-thumbnails' });
+        controls.add(this._thumbnailsBox, { expand: false });
+
+        let indicator = new St.Bin({ style_class: 'workspace-thumbnail-indicator',
+                                     fixed_position_set: true });
+
+        // We don't want the indicator to affect drag-and-drop
+        Shell.util_set_hidden_from_pick(indicator, true);
+
+        this._thumbnailIndicator = indicator;
+        this._thumbnailsBox.add(this._thumbnailIndicator);
+        this._thumbnailIndicatorConstraints = [];
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.X }));
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.Y }));
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.WIDTH }));
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.HEIGHT }));
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        indicator.add_constraint(constraint);
+                                                    });
+
         this._addButton = new St.Button({ label: '+',
                                           style_class: 'add-workspace' });
         this._addButton.connect('clicked', Lang.bind(this, function() {
@@ -1012,70 +845,181 @@ WorkspacesDisplay.prototype = {
         controls.add(this._addButton, { expand: true });
 
         this.workspacesView = null;
+
+        this._inDrag = false;
+        this._zoomOut = false;
+
         this._nWorkspacesNotifyId = 0;
+        this._switchWorkspaceNotifyId = 0;
+
+        this._itemDragBeginId = 0;
+        this._itemDragEndId = 0;
+        this._windowDragBeginId = 0;
+        this._windowDragEndId = 0;
     },
 
    show: function() {
-        this._controlsContainer.show();
+        this._controls.show();
 
         this._workspaces = [];
+        this._workspaceThumbnails = [];
         for (let i = 0; i < global.screen.n_workspaces; i++) {
             let metaWorkspace = global.screen.get_workspace_by_index(i);
             this._workspaces[i] = new Workspace.Workspace(metaWorkspace);
+
+            let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+            this._workspaceThumbnails[i] = thumbnail;
+            this._thumbnailsBox.add(thumbnail.actor);
         }
 
-        this._nWorkspacesNotifyId =
-            global.screen.connect('notify::n-workspaces',
-                                  Lang.bind(this, this._workspacesChanged));
+        // The thumbnails indicator actually needs to be on top of the thumbnails, but
+        // there is also something more subtle going on as well - actors in a StBoxLayout
+        // are allocated from bottom to to top (start to end), and we need the
+        // thumnail indicator to be allocated after the actors it is constrained to.
+        this._thumbnailIndicator.raise_top();
 
-        let binAllocation = this._workspacesBin.allocation;
-        let binWidth = binAllocation.x2 - binAllocation.x1;
-        let binHeight = binAllocation.y2 - binAllocation.y1;
+        let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
+
+        let totalAllocation = this.actor.allocation;
+        let totalWidth = totalAllocation.x2 - totalAllocation.x1;
+        // XXXX: 50 is just a hack for message tray compensation
+        let totalHeight = totalAllocation.y2 - totalAllocation.y1 - 50;
+
+        let [controlsMin, controlsNatural] = this._controls.get_preferred_width(-1);
+        let controlsReserved = controlsNatural * (1 - CONTROLS_POP_IN_FRACTION);
+
+        totalWidth -= controlsReserved;
 
         // Workspaces expect to have the same ratio as the screen, so take
-        // this into account when fitting the workspace into the bin
+        // this into account when fitting the workspace into the available space
         let width, height;
-        let binRatio = binWidth / binHeight;
+        let totalRatio = totalWidth / totalHeight;
         let wsRatio = global.screen_width / global.screen_height;
-        if (wsRatio > binRatio) {
-            width = binWidth;
-            height = Math.floor(binWidth / wsRatio);
+        if (wsRatio > totalRatio) {
+            width = totalWidth;
+            height = Math.floor(totalWidth / wsRatio);
         } else {
-            width = Math.floor(binHeight * wsRatio);
-            height = binHeight;
+            width = Math.floor(totalHeight * wsRatio);
+            height = totalHeight;
         }
 
-        // Position workspaces as if they were parented to this._workspacesBin
-        let [x, y] = this._workspacesBin.get_transformed_position();
-        x = Math.floor(x + Math.abs(binWidth - width) / 2);
-        y = Math.floor(y + Math.abs(binHeight - height) / 2);
+        // Position workspaces in the available space
+        let [x, y] = this.actor.get_transformed_position();
+        x = Math.floor(x + Math.abs(totalWidth - width) / 2);
+        y = Math.floor(y + Math.abs(totalHeight - height) / 2);
+
+        if (rtl)
+            x += controlsReserved;
 
-        let newView = new WorkspacesView(width, height, x, y, this._workspaces);
+        this._controls.x = this._getControlsX();
+        this._controls.height = totalHeight;
+
+        let zoomScale = (totalWidth - controlsNatural) / totalWidth;
+        let newView = new WorkspacesView(width, height, x, y, zoomScale, this._workspaces);
 
         if (this.workspacesView)
             this.workspacesView.destroy();
         this.workspacesView = newView;
 
-        this._workspaceIndicatorPanel.updateWorkspaces(this._workspaces);
-
         this._nWorkspacesNotifyId =
             global.screen.connect('notify::n-workspaces',
                                   Lang.bind(this, this._workspacesChanged));
+        this._switchWorkspaceNotifyId =
+            global.window_manager.connect('switch-workspace',
+                                          Lang.bind(this, this._activeWorkspaceChanged));
+
+        if (this._itemDragBeginId == 0)
+            this._itemDragBeginId = Main.overview.connect('item-drag-begin',
+                                                          Lang.bind(this, this._dragBegin));
+        if (this._itemDragEndId == 0)
+            this._itemDragEndId = Main.overview.connect('item-drag-end',
+                                                        Lang.bind(this, this._dragEnd));
+        if (this._windowDragBeginId == 0)
+            this._windowDragBeginId = Main.overview.connect('window-drag-begin',
+                                                            Lang.bind(this, this._dragBegin));
+        if (this._windowDragEndId == 0)
+            this._windowDragEndId = Main.overview.connect('window-drag-end',
+                                                          Lang.bind(this, this._dragEnd));
+
+        this._constrainThumbnailIndicator();
     },
 
     hide: function() {
-        this._controlsContainer.hide();
+        this._controls.hide();
 
-        if (this._nWorkspacesNotifyId > 0)
+        if (this._nWorkspacesNotifyId > 0) {
             global.screen.disconnect(this._nWorkspacesNotifyId);
+            this._nWorkspacesNotifyId = 0;
+        }
+        if (this._switchWorkspaceNotifyId > 0) {
+            global.window_manager.disconnect(this._switchWorkspaceNotifyId);
+            this._switchWorkspaceNotifyId = 0;
+        }
+
+        if (this._itemDragBeginId > 0) {
+            Main.overview.disconnect(this._itemDragBeginId);
+            this._itemDragBeginId = 0;
+        }
+        if (this._itemEndBeginId > 0) {
+            Main.overview.disconnect(this._itemDragEndId);
+            this._itemDragEndId = 0;
+        }
+        if (this._windowDragBeginId > 0) {
+            Main.overview.disconnect(this._windowDragBeginId);
+            this._windowDragBeginId = 0;
+        }
+        if (this._windowDragEndId > 0) {
+            Main.overview.disconnect(this._windowDragEndId);
+            this._windowDragEndId = 0;
+        }
+
         this.workspacesView.destroy();
         this.workspacesView = null;
+        this._unconstrainThumbnailIndicator();
         for (let w = 0; w < this._workspaces.length; w++) {
             this._workspaces[w].disconnectAll();
             this._workspaces[w].destroy();
+            this._workspaceThumbnails[w].destroy();
         }
     },
 
+    _constrainThumbnailIndicator: function() {
+        let active = global.screen.get_active_workspace_index();
+        let thumbnail = this._workspaceThumbnails[active];
+
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        constraint.set_source(thumbnail.actor);
+                                                        constraint.set_enabled(true);
+                                                    });
+    },
+
+    _unconstrainThumbnailIndicator: function() {
+        this._thumbnailIndicatorConstraints.forEach(function(constraint) {
+                                                        constraint.set_enabled(false);
+                                                    });
+    },
+
+    _activeWorkspaceChanged: function(wm, from, to, direction) {
+        let active = global.screen.get_active_workspace_index();
+        let thumbnail = this._workspaceThumbnails[active];
+
+        this._unconstrainThumbnailIndicator();
+        let oldAllocation = this._thumbnailIndicator.allocation;
+        this._thumbnailIndicator.x = oldAllocation.x1;
+        this._thumbnailIndicator.y = oldAllocation.y1;
+        this._thumbnailIndicator.width = oldAllocation.x2 - oldAllocation.x1;
+        this._thumbnailIndicator.height = oldAllocation.y2 - oldAllocation.y1;
+
+        Tweener.addTween(this._thumbnailIndicator,
+                         { x: thumbnail.actor.allocation.x1,
+                           y: thumbnail.actor.allocation.y1,
+                           time: WORKSPACE_SWITCH_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this,
+                                                 this._constrainThumbnailIndicator)
+                         });
+    },
+
     _workspacesChanged: function() {
         let oldNumWorkspaces = this._workspaces.length;
         let newNumWorkspaces = global.screen.n_workspaces;
@@ -1090,7 +1034,12 @@ WorkspacesDisplay.prototype = {
             for (let w = oldNumWorkspaces; w < newNumWorkspaces; w++) {
                 let metaWorkspace = global.screen.get_workspace_by_index(w);
                 this._workspaces[w] = new Workspace.Workspace(metaWorkspace);
+
+                let thumbnail = new WorkspaceThumbnail.WorkspaceThumbnail(metaWorkspace);
+                this._workspaceThumbnails[w] = thumbnail;
+                this._thumbnailsBox.add(thumbnail.actor);
             }
+            this._thumbnailIndicator.raise_top();
         } else {
             // Assume workspaces are only removed sequentially
             // (e.g. 2,3,4 - not 2,4,7)
@@ -1111,12 +1060,61 @@ WorkspacesDisplay.prototype = {
             // making its exit.
             for (let l = 0; l < lostWorkspaces.length; l++)
                 lostWorkspaces[l].setReactive(false);
+
+            for (let k = removedIndex; k < removedIndex + removedNum; k++)
+                this._workspaceThumbnails[k].destroy();
+            this._workspaceThumbnails.splice(removedIndex, removedNum);
         }
 
         this.workspacesView.updateWorkspaces(oldNumWorkspaces,
                                              newNumWorkspaces,
                                              lostWorkspaces);
-        this._workspaceIndicatorPanel.updateWorkspaces(this._workspaces);
+    },
+
+    _getControlsX: function() {
+        let totalAllocation = this.actor.allocation;
+        let totalWidth = totalAllocation.x2 - totalAllocation.x1;
+        let [controlsMin, controlsNatural] = this._controls.get_preferred_width(-1);
+        let controlsReserved = controlsNatural * (1 - CONTROLS_POP_IN_FRACTION);
+
+        let rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
+        let width = this._zoomOut ? controlsNatural : controlsReserved;
+        if (rtl)
+            return width;
+        else
+            return totalWidth - width;
+    },
+
+    _updateZoom : function() {
+        let shouldZoom = this._controls.hover || this._inDrag;
+        if (shouldZoom != this._zoomOut) {
+            this._zoomOut = shouldZoom;
+
+            Tweener.addTween(this._controls,
+                             { x: this._getControlsX(),
+//                               time: CONTROLS_POP_IN_TIME,
+                               time: WORKSPACE_SWITCH_TIME,
+                               transition: 'easeOutQuad' });
+
+            if (shouldZoom)
+                this.workspacesView.zoomOut();
+            else
+                this.workspacesView.zoomIn();
+        }
+    },
+
+    _onControlsHoverChanged: function() {
+        this._updateZoom();
+    },
+
+    _dragBegin: function() {
+        this._inDrag = true;
+        this._updateZoom();
+    },
+
+    _dragEnd: function() {
+        this._inDrag = false;
+        this._updateZoom();
     }
 };
 Signals.addSignalMethods(WorkspacesDisplay.prototype);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]