[gnome-shell] Slide workspace thumbnails in when added and out when removed



commit 7598fd45813d01726f92d359e49f96b1e8e27911
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Sat Feb 12 14:40:49 2011 -0500

    Slide workspace thumbnails in when added and out when removed
    
    To explain to the user what is happening, instead of abruptly changing
    updating the workspace thumbnail list, slide thubmnails in and out
    as they are added and removed.
    
    To implement this, we track a state for each thumbnail and when things
    change go through a process of first sliding removed thumbnails out,
    then ollapsing the left-over spaces and rescaling the thumbnails, then
    finally sliding newly added thumbnails in.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=641881

 js/ui/workspaceThumbnail.js |  285 ++++++++++++++++++++++++++++++++++++-------
 1 files changed, 239 insertions(+), 46 deletions(-)
---
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
index 0da3b12..a1dccb1 100644
--- a/js/ui/workspaceThumbnail.js
+++ b/js/ui/workspaceThumbnail.js
@@ -3,6 +3,7 @@
 const Clutter = imports.gi.Clutter;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
+const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 const St = imports.gi.St;
@@ -17,6 +18,7 @@ const WorkspacesView = imports.ui.workspacesView;
 let MAX_THUMBNAIL_SCALE = 1/8.;
 
 const RESCALE_ANIMATION_TIME = 0.2;
+const SLIDE_ANIMATION_TIME = 0.2;
 
 function WindowClone(realWindow) {
     this._init(realWindow);
@@ -123,6 +125,17 @@ WindowClone.prototype = {
 Signals.addSignalMethods(WindowClone.prototype);
 
 
+const ThumbnailState = {
+    NEW   :         0,
+    ANIMATING_IN :  1,
+    NORMAL:         2,
+    REMOVING :      3,
+    ANIMATING_OUT : 4,
+    ANIMATED_OUT :  5,
+    COLLAPSING :    6,
+    DESTROYED :     7
+};
+
 /**
  * @metaWorkspace: a #Meta.Workspace
  */
@@ -172,6 +185,10 @@ WorkspaceThumbnail.prototype = {
                                                           Lang.bind(this, this._windowAdded));
         this._windowRemovedId = this.metaWorkspace.connect('window-removed',
                                                            Lang.bind(this, this._windowRemoved));
+
+        this.state = ThumbnailState.NORMAL;
+        this._slidePosition = 0; // Fully slid in
+        this._collapseFraction = 0; // Not collapsed
     },
 
     _lookupIndex: function (metaWindow) {
@@ -198,6 +215,24 @@ WorkspaceThumbnail.prototype = {
         }
     },
 
+    set slidePosition(slidePosition) {
+        this._slidePosition = slidePosition;
+        this.actor.queue_relayout();
+    },
+
+    get slidePosition() {
+        return this._slidePosition;
+    },
+
+    set collapseFraction(collapseFraction) {
+        this._collapseFraction = collapseFraction;
+        this.actor.queue_relayout();
+    },
+
+    get collapseFraction() {
+        return this._collapseFraction;
+    },
+
     _windowRemoved : function(metaWorkspace, metaWin) {
         let win = metaWin.get_compositor_private();
 
@@ -344,8 +379,15 @@ ThumbnailsBox.prototype = {
         this._indicator = indicator;
         this.actor.add_actor(indicator);
         this._indicatorConstrained = false;
+
         this._targetScale = 0;
         this._scale = 0;
+        this._pendingScaleUpdate = false;
+        this._stateUpdateQueued = false;
+
+        this._stateCounts = {};
+        for (key in ThumbnailState)
+            this._stateCounts[ThumbnailState[key]] = 0;
 
         this._thumbnails = [];
     },
@@ -355,10 +397,18 @@ ThumbnailsBox.prototype = {
             global.window_manager.connect('switch-workspace',
                                           Lang.bind(this, this._activeWorkspaceChanged));
 
-        this.addThumbnails(0, global.screen.n_workspaces);
         this._constrainThumbnailIndicator();
+
         this._targetScale = 0;
         this._scale = 0;
+        this._pendingScaleUpdate = false;
+        this._stateUpdateQueued = false;
+
+        this._stateCounts = {};
+        for (key in ThumbnailState)
+            this._stateCounts[ThumbnailState[key]] = 0;
+
+        this.addThumbnails(0, global.screen.n_workspaces);
     },
 
     hide: function() {
@@ -378,18 +428,41 @@ ThumbnailsBox.prototype = {
         for (let k = start; k < start + count; k++) {
             let metaWorkspace = global.screen.get_workspace_by_index(k);
             let thumbnail = new WorkspaceThumbnail(metaWorkspace);
-            this._thumbnails[k] = thumbnail;
+            this._thumbnails.push(thumbnail);
             this.actor.add_actor(thumbnail.actor);
+
+            if (start > 0) { // not the initial fill
+                thumbnail.state = ThumbnailState.NEW;
+                thumbnail.slidePosition = 1; // start slid out
+                this._haveNewThumbnails = true;
+            } else {
+                thumbnail.state = ThumbnailState.NORMAL;
+            }
+
+            this._stateCounts[thumbnail.state]++;
         }
 
+        this._queueUpdateStates();
+
         // The thumbnails indicator actually needs to be on top of the thumbnails
         this._indicator.raise_top();
     },
 
     removeThumbmails: function(start, count) {
-        for (let k = start; k < start + count; k++)
-            this._thumbnails[k].destroy();
-        this._thumbnails.splice(start, count);
+        let currentPos = 0;
+        for (let k = 0; k < this._thumbnails.length; k++) {
+            let thumbnail = this._thumbnails[k];
+
+            if (thumbnail.state > ThumbnailState.NORMAL)
+                continue;
+
+            if (currentPos >= start && currentPos < start + count)
+                this._setThumbnailState(thumbnail, ThumbnailState.REMOVING);
+
+            currentPos++;
+        }
+
+        this._queueUpdateStates();
 
         // If we removed the current workspace, then metacity will have already
         // switched to an adjacent workspace. Leaving the animation we
@@ -415,6 +488,113 @@ ThumbnailsBox.prototype = {
         return this._scale;
     },
 
+    _setThumbnailState: function(thumbnail, state) {
+        this._stateCounts[thumbnail.state]--;
+        thumbnail.state = state;
+        this._stateCounts[thumbnail.state]++;
+    },
+
+    _iterateStateThumbnails: function(state, callback) {
+        if (this._stateCounts[state] == 0)
+            return;
+
+        for (let i = 0; i < this._thumbnails.length; i++) {
+            if (this._thumbnails[i].state == state)
+                callback.call(this, this._thumbnails[i]);
+        }
+    },
+
+    _tweenScale: function() {
+        Tweener.addTween(this,
+                         { scale: this._targetScale,
+                           time: RESCALE_ANIMATION_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: this._queueUpdateStates,
+                           onCompleteScope: this });
+    },
+
+    _updateStates: function() {
+        this._stateUpdateQueued = false;
+
+        // First thing to do is to start sliding out any thumbnails that have been destroyed
+        this._iterateStateThumbnails(ThumbnailState.REMOVING,
+            function(thumbnail) {
+                this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_OUT);
+
+                Tweener.addTween(thumbnail,
+                                 { slidePosition: 1,
+                                   time: SLIDE_ANIMATION_TIME,
+                                   transition: 'linear',
+                                   onComplete: function() {
+                                       this._setThumbnailState(thumbnail, ThumbnailState.ANIMATED_OUT);
+                                       this._queueUpdateStates();
+                                   },
+                                   onCompleteScope: this
+                                 });
+            });
+
+        // As long as things are sliding out, don't proceed
+        if (this._stateCounts[ThumbnailState.ANIMATING_OUT] > 0)
+            return;
+
+        // Once that's complete, we can start scaling to the new size and collapse any removed thumbnails
+        this._iterateStateThumbnails(ThumbnailState.ANIMATED_OUT,
+            function(thumbnail) {
+                this.actor.set_skip_paint(thumbnail.actor, true);
+                this._setThumbnailState(thumbnail, ThumbnailState.COLLAPSING);
+                Tweener.addTween(thumbnail,
+                                 { collapseFraction: 1,
+                                   time: RESCALE_ANIMATION_TIME,
+                                   transition: 'easeOutQuad',
+                                   onComplete: function() {
+                                       this._stateCounts[thumbnail.state]--;
+                                       thumbnail.state = ThumbnailState.DESTROYED;
+
+                                       let index = this._thumbnails.indexOf(thumbnail);
+                                       this._thumbnails.splice(index, 1);
+                                       thumbnail.destroy();
+
+                                       this._queueUpdateStates();
+                                   },
+                                   onCompleteScope: this
+                                 });
+                });
+
+        if (this._pendingScaleUpdate) {
+            this._tweenScale();
+            this._pendingScaleUpdate = false;
+        }
+
+        // Wait until that's done
+        if (this._scale != this._targetScale || this._stateCounts[ThumbnailState.COLLAPSING] > 0)
+            return;
+
+        // And then slide in any new thumbnails
+        this._iterateStateThumbnails(ThumbnailState.NEW,
+            function(thumbnail) {
+                this._setThumbnailState(thumbnail, ThumbnailState.ANIMATING_IN);
+                Tweener.addTween(thumbnail,
+                                 { slidePosition: 0,
+                                   time: SLIDE_ANIMATION_TIME,
+                                   transition: 'easeOutQuad',
+                                   onComplete: function() {
+                                       this._setThumbnailState(thumbnail, ThumbnailState.NORMAL);
+                                   },
+                                   onCompleteScope: this
+                                 });
+            });
+    },
+
+    _queueUpdateStates: function() {
+        if (this._stateUpdateQueued)
+            return;
+
+        Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
+                       Lang.bind(this, this._updateStates));
+
+        this._stateUpdateQueued = true;
+    },
+
     _getPreferredHeight: function(actor, forWidth, alloc) {
         // Note that for getPreferredWidth/Height we cheat a bit and skip propagating
         // the size request to our children because we know how big they are and know
@@ -424,21 +604,27 @@ ThumbnailsBox.prototype = {
             return;
 
         let spacing = this.actor.get_theme_node().get_length('spacing');
-        let totalSpacing = (this._thumbnails.length - 1) * spacing;
+        let nWorkspaces = global.screen.n_workspaces;
+        let totalSpacing = (nWorkspaces - 1) * spacing;
 
         alloc.min_size = totalSpacing;
-        alloc.natural_size = totalSpacing + this._thumbnails.length * global.screen_height * MAX_THUMBNAIL_SCALE;
+        alloc.natural_size = totalSpacing + nWorkspaces * global.screen_height * MAX_THUMBNAIL_SCALE;
     },
 
     _getPreferredWidth: function(actor, forHeight, alloc) {
         if (this._thumbnails.length == 0)
             return;
 
+        // We don't animate our preferred width, which is always reported according
+        // to the actual number of current workspaces, we just animate within that
+
         let spacing = this.actor.get_theme_node().get_length('spacing');
-        let totalSpacing = (this._thumbnails.length - 1) * spacing;
+        let nWorkspaces = global.screen.n_workspaces;
+        let totalSpacing = (nWorkspaces - 1) * spacing;
+
         let avail = forHeight - totalSpacing;
 
-        let scale = (avail / this._thumbnails.length) / global.screen_height;
+        let scale = (avail / nWorkspaces) / global.screen_height;
         scale = Math.min(scale, MAX_THUMBNAIL_SCALE);
 
         alloc.min_size = alloc.natural_size = Math.round(global.screen_width * scale);
@@ -448,44 +634,49 @@ ThumbnailsBox.prototype = {
         if (this._thumbnails.length == 0) // not visible
             return;
 
+        let screenWidth = global.screen_width;
         let screenHeight = global.screen_height;
-
         let spacing = this.actor.get_theme_node().get_length('spacing');
-        let totalSpacing = (this._thumbnails.length - 1) * spacing;
+
+        // Compute the scale we'll need once everything is updated
+        let nWorkspaces = global.screen.n_workspaces;
+        let totalSpacing = (nWorkspaces - 1) * spacing;
         let avail = (box.y2 - box.y1) - totalSpacing;
 
-        let scale = (avail / this._thumbnails.length) / screenHeight;
-        scale = Math.min(scale, MAX_THUMBNAIL_SCALE);
+        let newScale = (avail / nWorkspaces) / screenHeight;
+        newScale = Math.min(newScale, MAX_THUMBNAIL_SCALE);
 
-        if (scale != this._targetScale) {
+        if (newScale != this._targetScale) {
             if (this._targetScale > 0) {
-                this._targetScale = scale;
-                Tweener.addTween(this,
-                                 { scale: this._targetScale,
-                                   time: RESCALE_ANIMATION_TIME,
-                                   transition: 'easeOutQuad' });
+                // We don't do the tween immediately because we need to observe the ordering
+                // in queueUpdateStates - if workspaces have been removed we need to slide them
+                // out as the first thing.
+                this._targetScale = newScale;
+                this._pendingScaleUpdate = true;
             } else {
-                this._targetScale = this._scale = scale;
+                this._targetScale = this._scale = newScale;
             }
+
+            this._queueUpdateStates();
         }
 
         let thumbnailHeight = screenHeight * this._scale;
+        let thumbnailWidth = Math.round(screenWidth * this._scale);
+        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
+        let slideWidth = thumbnailWidth + rightPadding; // Amount to slide a thumbnail off to right
 
         let childBox = new Clutter.ActorBox();
 
         let indicatorWorkspace = this._indicatorConstrained ? global.screen.get_active_workspace() : null;
         let indicatorBox;
 
-        // Allocating a scaled actor is funny - x1/y1 correspond to the origin
-        // of the actor, but x2/y2 are increased by the *unscaled* size.
-        childBox.x1 = box.x1;
-        childBox.x2 = childBox.x1 + global.screen_width;
-
         let y = box.y1;
 
         for (let i = 0; i < this._thumbnails.length; i++) {
+            let thumbnail = this._thumbnails[i];
+
             if (i > 0)
-                y += spacing + thumbnailHeight;
+                y += (1 - thumbnail.collapseFraction) * spacing;
 
             // We might end up with thumbnailHeight being something like 99.33
             // pixels. To make this work and not end up with a gap at the bottom,
@@ -495,34 +686,30 @@ ThumbnailsBox.prototype = {
             let y2 = Math.round(y + thumbnailHeight);
             let roundedScale = (y2 - y1) / screenHeight;
 
-            // When animating to a smaller scale, don't include workspaces that don't
-            // yet fit in the visible portion
-            if (y2 > Math.round(box.y2)) {
+            let x1 = box.x2 - thumbnailWidth + slideWidth * thumbnail.slidePosition;
+            let x2 = x1 + thumbnailWidth;
 
-                this.actor.set_skip_paint(this._thumbnails[i].actor, true);
-                if (this._thumbnails[i].metaWorkspace == indicatorWorkspace)
-                    this.actor.set_skip_paint(this._indicator, true);
-
-                continue;
-            }
-
-            if (this._thumbnails[i].metaWorkspace == indicatorWorkspace) {
+            if (thumbnail.metaWorkspace == indicatorWorkspace) {
                 let indicatorBox = new Clutter.ActorBox();
-                indicatorBox.x1 = box.x1;
-                indicatorBox.x2 = box.x2;
+                indicatorBox.x1 = x1;
+                indicatorBox.x2 = x2;
                 indicatorBox.y1 = y1;
                 indicatorBox.y2 = y2;
 
-                this.actor.set_skip_paint(this._indicator, false);
                 this._indicator.allocate(indicatorBox, flags);
             }
 
+            // Allocating a scaled actor is funny - x1/y1 correspond to the origin
+            // of the actor, but x2/y2 are increased by the *unscaled* size.
+            childBox.x1 = x1;
+            childBox.x2 = x1 + screenWidth;
             childBox.y1 = y1;
-            childBox.y2 = childBox.y1 + screenHeight;
+            childBox.y2 = y1 + screenHeight;
 
-            this.actor.set_skip_paint(this._thumbnails[i].actor, false);
-            this._thumbnails[i].actor.set_scale(roundedScale, roundedScale);
-            this._thumbnails[i].actor.allocate(childBox, flags);
+            thumbnail.actor.set_scale(roundedScale, roundedScale);
+            thumbnail.actor.allocate(childBox, flags);
+
+            y += thumbnailHeight * (1 - thumbnail.collapseFraction);
         }
 
         if (indicatorWorkspace == null) {
@@ -541,8 +728,14 @@ ThumbnailsBox.prototype = {
     },
 
     _activeWorkspaceChanged: function(wm, from, to, direction) {
-        let active = global.screen.get_active_workspace_index();
-        let thumbnail = this._thumbnails[active];
+        let thumbnail;
+        let activeWorkspace = global.screen.get_active_workspace();
+        for (let i = 0; i < this._thumbnails.length; i++) {
+            if (this._thumbnails[i].metaWorkspace == activeWorkspace) {
+                thumbnail = this._thumbnails[i];
+                break;
+            }
+        }
 
         this._unconstrainThumbnailIndicator();
         let oldAllocation = this._indicator.allocation;



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