[gnome-shell] Add workspace thumbnails to the overview



commit 6514bc845f3232804914386489b86fad06fd6428
Author: Owen W. Taylor <otaylor fishsoup net>
Date:   Sun Jan 30 21:18:12 2011 -0500

    Add workspace thumbnails to the overview
    
    Add workspace thumbnails to the workspace controls area. The user can
    click on the thumbnail to switch workspaces and can also drag windows
    out of the thumbnail to other workspaces.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=640996

 data/theme/gnome-shell.css  |   16 ++-
 js/Makefile.am              |    1 +
 js/ui/workspaceThumbnail.js |  295 +++++++++++++++++++++++++++++++++++++++++++
 js/ui/workspacesView.js     |   91 +++++++++++++
 4 files changed, 399 insertions(+), 4 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index b0dda27..bbe76a7 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -261,21 +261,29 @@ StTooltip StLabel {
 }
 
 .workspace-controls {
-    width: 48px;
     font-size: 32px;
     font-weight: bold;
     color: #ffffff;
-    border: 2px solid rgba(128, 128, 128, 0.4);
+    border: 1px solid #424242;
     border-right: 0px;
     border-radius: 9px 0px 0px 9px;
+    background: #071524;
+}
+
+.workspace-thumbnails {
+    spacing: 7px;
+    padding: 8px;
+}
+
+.workspace-thumbnail-indicator {
+    outline: 2px 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/Makefile.am b/js/Makefile.am
index 21683af..955c308 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -58,6 +58,7 @@ nobase_dist_js_DATA = 	\
 	ui/windowAttentionHandler.js	\
 	ui/windowManager.js	\
 	ui/workspace.js		\
+	ui/workspaceThumbnail.js	\
 	ui/workspacesView.js	\
 	ui/workspaceSwitcherPopup.js    \
 	ui/xdndHandler.js
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
new file mode 100644
index 0000000..8f8586b
--- /dev/null
+++ b/js/ui/workspaceThumbnail.js
@@ -0,0 +1,295 @@
+/* -*- 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;
+
+// Fraction of original screen size for thumbnails
+let THUMBNAIL_SCALE = 1/8.;
+
+function WindowClone(realWindow) {
+    this._init(realWindow);
+}
+
+WindowClone.prototype = {
+    _init : function(realWindow) {
+        this.actor = new Clutter.Clone({ source: realWindow.get_texture(),
+                                         reactive: true,
+                                         x: realWindow.x,
+                                         y: realWindow.y });
+        this.actor._delegate = this;
+        this.realWindow = realWindow;
+        this.metaWindow = realWindow.meta_window;
+
+        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;
+    },
+
+    setStackAbove: function (actor) {
+        this._stackAbove = actor;
+        if (this._stackAbove == null)
+            this.actor.lower_bottom();
+        else
+            this.actor.raise(this._stackAbove);
+    },
+
+    destroy: function () {
+        this.actor.destroy();
+    },
+
+    _onDestroy: function() {
+        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) {
+        this.metaWorkspace = metaWorkspace;
+
+        this.actor = new St.Bin({ reactive: true,
+                                  clip_to_allocation: 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(THUMBNAIL_SCALE * global.screen_width, THUMBNAIL_SCALE * global.screen_height);
+        this._group.set_scale(THUMBNAIL_SCALE, THUMBNAIL_SCALE);
+
+        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) {
+        this._windows.sort(function (a, b) { return stackIndices[a.metaWindow.get_stable_sequence()] - stackIndices[b.metaWindow.get_stable_sequence()]; });
+
+        for (let i = 0; i < this._windows.length; i++) {
+            let clone = this._windows[i];
+            let metaWindow = clone.metaWindow;
+            if (i == 0) {
+                clone.setStackAbove(this._background);
+            } else {
+                let previousClone = this._windows[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 8f97d0e..5e24141 100644
--- a/js/ui/workspacesView.js
+++ b/js/ui/workspacesView.js
@@ -14,6 +14,7 @@ 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
@@ -725,6 +726,25 @@ 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.POSITION }));
+        this._thumbnailIndicatorConstraints.push(new Clutter.BindConstraint({ coordinate: Clutter.BindCoordinate.SIZE }));
+        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() {
@@ -759,11 +779,22 @@ WorkspacesDisplay.prototype = {
         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);
         }
 
+        // 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 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 rtl = (St.Widget.get_default_direction () == St.TextDirection.RTL);
 
         let totalAllocation = this.actor.allocation;
@@ -810,6 +841,9 @@ WorkspacesDisplay.prototype = {
         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));
 
         this._restackedNotifyId =
             global.screen.connect('restacked',
@@ -829,6 +863,9 @@ WorkspacesDisplay.prototype = {
                                                           Lang.bind(this, this._dragEnd));
 
         this._onRestacked();
+        this._constrainThumbnailIndicator();
+        this._zoomOut = false;
+        this._updateZoom();
     },
 
     hide: function() {
@@ -838,6 +875,10 @@ WorkspacesDisplay.prototype = {
             global.screen.disconnect(this._nWorkspacesNotifyId);
             this._nWorkspacesNotifyId = 0;
         }
+        if (this._switchWorkspaceNotifyId > 0) {
+            global.window_manager.disconnect(this._switchWorkspaceNotifyId);
+            this._switchWorkspaceNotifyId = 0;
+        }
         if (this._restackedNotifyId > 0){
             global.screen.disconnect(this._restackedNotifyId);
             this._restackedNotifyId = 0;
@@ -861,12 +902,51 @@ WorkspacesDisplay.prototype = {
 
         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)
+                         });
+    },
+
     _onRestacked: function() {
         let stack = global.get_window_actors();
         let stackIndices = {};
@@ -877,6 +957,8 @@ WorkspacesDisplay.prototype = {
         }
 
         this.workspacesView.syncStacking(stackIndices);
+        for (let i = 0; i < this._workspaceThumbnails.length; i++)
+            this._workspaceThumbnails[i].syncStacking(stackIndices);
     },
 
     _workspacesChanged: function() {
@@ -893,7 +975,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)
@@ -914,6 +1001,10 @@ 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,



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