[gnome-shell] Bug 591763 - Add application window menu



commit 25410a730ea354b772e0424f7adeb9ec65b80e46
Author: Colin Walters <walters verbum org>
Date:   Tue Sep 1 14:15:29 2009 -0400

    Bug 591763 - Add application window menu
    
    When we have multiple windows for an application, implement the following
    behavior:
    
    * On click + immediate release, go to the most recently used
    * On click, hold for 0.6s, pop up a menu with windows, filtering
      the window list to just those windows.
      Mouse over on the window list highlights the moused-over window.
    
    Implement this by splitting well item into InactiveWellItem
    and RunningWellItem, sharing a base class BaseWellItem.

 js/ui/appDisplay.js |  378 +++++++++++++++++++++++++++++++++++++++++++++++----
 js/ui/appIcon.js    |   33 ++++-
 js/ui/overview.js   |   25 ++++-
 js/ui/workspaces.js |  148 +++++++++++++++++++-
 src/shell-drawing.c |   30 ++++
 src/shell-drawing.h |    4 +
 6 files changed, 578 insertions(+), 40 deletions(-)
---
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index b313605..36279f9 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -26,6 +26,27 @@ const WELL_DEFAULT_COLUMNS = 4;
 const WELL_ITEM_HSPACING = 0;
 const WELL_ITEM_VSPACING = 4;
 
+const WELL_MENU_POPUP_TIMEOUT_MS = 600;
+
+const TRANSPARENT_COLOR = new Clutter.Color();
+TRANSPARENT_COLOR.from_pixel(0x00000000);
+
+const WELL_MENU_BACKGROUND_COLOR = new Clutter.Color();
+WELL_MENU_BACKGROUND_COLOR.from_pixel(0x292929ff);
+const WELL_MENU_FONT = 'Sans 14px';
+const WELL_MENU_COLOR = new Clutter.Color();
+WELL_MENU_COLOR.from_pixel(0xffffffff);
+const WELL_MENU_SELECTED_COLOR = new Clutter.Color();
+WELL_MENU_SELECTED_COLOR.from_pixel(0x005b97ff);
+const WELL_MENU_BORDER_COLOR = new Clutter.Color();
+WELL_MENU_BORDER_COLOR.from_pixel(0x787878ff);
+const WELL_MENU_SEPARATOR_COLOR = new Clutter.Color();
+WELL_MENU_SEPARATOR_COLOR.from_pixel(0x787878ff);
+const WELL_MENU_BORDER_WIDTH = 1;
+const WELL_MENU_ARROW_SIZE = 12;
+const WELL_MENU_CORNER_RADIUS = 4;
+const WELL_MENU_PADDING = 4;
+
 const MENU_ICON_SIZE = 24;
 const MENU_SPACING = 15;
 
@@ -161,7 +182,6 @@ MenuItem.prototype = {
 }
 Signals.addSignalMethods(MenuItem.prototype);
 
-
 /* This class represents a display containing a collection of application items.
  * The applications are sorted based on their popularity by default, and based on
  * their name if some search filter is applied.
@@ -448,37 +468,204 @@ AppDisplay.prototype = {
 
 Signals.addSignalMethods(AppDisplay.prototype);
 
-function WellDisplayItem(appInfo, isFavorite) {
-    this._init(appInfo, isFavorite);
+function WellMenu(source) {
+    this._init(source);
 }
 
-WellDisplayItem.prototype = {
-    __proto__ : AppIcon.AppIcon.prototype,
+WellMenu.prototype = {
+    _init: function(source) {
+        this._source = source;
 
-    _init : function(appInfo, isFavorite) {
-        AppIcon.AppIcon.prototype._init.call(this, appInfo);
 
-        this.isFavorite = isFavorite;
+        this.actor = new Shell.GenericContainer({ reactive: 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._windowContainer = new Shell.Menu({ orientation: Big.BoxOrientation.VERTICAL,
+                                                 border_color: WELL_MENU_BORDER_COLOR,
+                                                 border: WELL_MENU_BORDER_WIDTH,
+                                                 background_color: WELL_MENU_BACKGROUND_COLOR,
+                                                 padding: 4,
+                                                 corner_radius: WELL_MENU_CORNER_RADIUS,
+                                                 width: Main.overview._dash.actor.width * 0.75 });
+        this._windowContainer.connect('popdown', Lang.bind(this, this._onPopdown));
+        this._windowContainer.connect('unselected', Lang.bind(this, this._onWindowUnselected));
+        this._windowContainer.connect('selected', Lang.bind(this, this._onWindowSelected));
+        this._windowContainer.connect('activate', Lang.bind(this, this._onWindowActivate));
+        this.actor.add_actor(this._windowContainer);
+
+        this._arrow = new Shell.DrawingArea();
+        this._arrow.connect('redraw', Lang.bind(this, function (area, texture) {
+            Shell.draw_box_pointer(texture, WELL_MENU_BORDER_COLOR, WELL_MENU_BACKGROUND_COLOR);
+        }));
+        this.actor.add_actor(this._arrow);
+
+        let stage = Shell.Global.get().stage;
 
-        this.actor.connect('button-release-event', Lang.bind(this, function (b, e) {
-            this._handleActivate();
+        // Chain our visibility and lifecycle to that of the source
+        source.actor.connect('notify::mapped', Lang.bind(this, function () {
+            if (!source.actor.mapped)
+                this._windowContainer.popdown();
         }));
+        source.actor.connect('destroy', Lang.bind(this, function () { this.actor.destroy(); }));
 
-        let draggable = DND.makeDraggable(this.actor);
+        stage.add_actor(this.actor);
     },
 
-    _handleActivate: function () {
-        if (this._windows.length == 0) {
-            this.appInfo.launch();
-            Main.overview.hide();
-        } else {
-            /* Pick the first window and activate it;
-             * In the future, we want to have a menu dropdown here. */
-            let first = this._windows[0];
-            Main.overview.activateWindow(first, Clutter.get_current_event_time());
+    _getPreferredWidth: function(actor, forHeight, alloc) {
+        let [min, natural] = this._windowContainer.get_preferred_width(forHeight);
+        alloc.min_size = min + WELL_MENU_ARROW_SIZE;
+        alloc.natural_size = natural + WELL_MENU_ARROW_SIZE;
+    },
+
+    _getPreferredHeight: function(actor, forWidth, alloc) {
+        let [min, natural] = this._windowContainer.get_preferred_height(forWidth);
+        alloc.min_size = min;
+        alloc.natural_size = natural;
+    },
+
+    _allocate: function(actor, box, flags) {
+        let childBox = new Clutter.ActorBox();
+
+        let width = box.x2 - box.x1;
+        let height = box.y2 - box.y1;
+
+        childBox.x1 = 0;
+        childBox.x2 = WELL_MENU_ARROW_SIZE;
+        childBox.y1 = (height / 2) - (WELL_MENU_ARROW_SIZE / 2);
+        childBox.y2 = childBox.y1 + WELL_MENU_ARROW_SIZE;
+        this._arrow.allocate(childBox, flags);
+
+        /* overlap by one pixel to hide the border */
+        childBox.x1 = WELL_MENU_ARROW_SIZE - 1;
+        childBox.x2 = width;
+        childBox.y1 = 0;
+        childBox.y2 = height;
+        this._windowContainer.allocate(childBox, flags);
+    },
+
+    _redisplay: function() {
+        this._windowContainer.remove_all();
+
+        let windows = this._source.windows;
+
+        this._windowContainer.show();
+
+        let iconsDiffer = false;
+        let texCache = Shell.TextureCache.get_default();
+        let firstIcon = windows[0].mini_icon;
+        for (let i = 1; i < windows.length; i++) {
+            if (!texCache.pixbuf_equal(windows[i].mini_icon, firstIcon)) {
+                iconsDiffer = true;
+                break;
+            }
+        }
+
+        let activeWorkspace = Shell.Global.get().screen.get_active_workspace();
+
+        let currentWorkspaceWindows = windows.filter(function (w) {
+            return w.get_workspace() == activeWorkspace;
+        });
+        let otherWorkspaceWindows = windows.filter(function (w) {
+            return w.get_workspace() != activeWorkspace;
+        });
+
+        this._appendWindows(currentWorkspaceWindows, iconsDiffer);
+        if (currentWorkspaceWindows.length > 0 && otherWorkspaceWindows.length > 0) {
+            let box = new Big.Box({ padding_top: 2, padding_bottom: 2 });
+            box.append(new Clutter.Rectangle({ height: 1,
+                                               color: WELL_MENU_SEPARATOR_COLOR }),
+                       Big.BoxPackFlags.EXPAND);
+            this._windowContainer.append_separator(box, Big.BoxPackFlags.NONE);
+        }
+        this._appendWindows(otherWorkspaceWindows, iconsDiffer);
+    },
+
+    _appendWindows: function (windows, iconsDiffer) {
+        for (let i = 0; i < windows.length; i++) {
+            let window = windows[i];
+            /* Use padding here rather than spacing in the box above so that
+             * we have a larger reactive area.
+             */
+            let box = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                    padding_top: 4,
+                                    padding_bottom: 4,
+                                    spacing: 4,
+                                    reactive: true });
+            box._window = window;
+            let vCenter;
+            if (iconsDiffer) {
+                vCenter = new Big.Box({ y_align: Big.BoxAlignment.CENTER });
+                let icon = Shell.TextureCache.get_default().bind_pixbuf_property(window, "mini-icon");
+                vCenter.append(icon, Big.BoxPackFlags.NONE);
+                box.append(vCenter, Big.BoxPackFlags.NONE);
+            }
+            vCenter = new Big.Box({ y_align: Big.BoxAlignment.CENTER });
+            let label = new Clutter.Text({ text: window.title,
+                                           font_name: WELL_MENU_FONT,
+                                           ellipsize: Pango.EllipsizeMode.END,
+                                           color: WELL_MENU_COLOR });
+            vCenter.append(label, Big.BoxPackFlags.NONE);
+            box.append(vCenter, Big.BoxPackFlags.NONE);
+            this._windowContainer.append(box, Big.BoxPackFlags.NONE);
         }
     },
 
+    popup: function() {
+        let [stageX, stageY] = this._source.actor.get_transformed_position();
+        let [stageWidth, stageHeight] = this._source.actor.get_transformed_size();
+
+        this._redisplay();
+
+        this._windowContainer.popup(0, Clutter.get_current_event_time());
+
+        this.emit('popup', true);
+
+        let y = Math.floor(stageY + (stageHeight / 2) - (this.actor.height / 2));
+        this.actor.set_position(stageX + stageWidth, y);
+        this.actor.show();
+    },
+
+    _onWindowUnselected: function (actor, child) {
+        child.background_color = TRANSPARENT_COLOR;
+
+        this.emit('highlight-window', null);
+    },
+
+    _onWindowSelected: function (actor, child) {
+        child.background_color = WELL_MENU_SELECTED_COLOR;
+
+        let window = child._window;
+        this.emit('highlight-window', window);
+    },
+
+    _onWindowActivate: function (actor, child) {
+        let window = child._window;
+        Main.overview.activateWindow(window, Clutter.get_current_event_time());
+    },
+
+    _onPopdown: function () {
+        this.emit('highlight-window', null);
+        this.emit('popup', false);
+        this.actor.hide();
+    }
+}
+
+Signals.addSignalMethods(WellMenu.prototype);
+
+function BaseWellItem(appInfo, isFavorite) {
+    this._init(appInfo, isFavorite);
+}
+
+BaseWellItem.prototype = {
+    _init: function(appInfo, isFavorite) {
+        this.appInfo = appInfo;
+        this.isFavorite = isFavorite;
+        this.icon = new AppIcon.AppIcon(appInfo);
+        this.windows = this.icon.windows;
+    },
+
     shellWorkspaceLaunch : function() {
         // Here we just always launch the application again, even if we know
         // it was already running.  For most applications this
@@ -492,17 +679,149 @@ WellDisplayItem.prototype = {
     },
 
     getDragActor: function(stageX, stageY) {
-        return this.appInfo.create_icon_texture(this._icon.height);
+        return this.icon.getDragActor(stageX, stageY);
     },
 
     // Returns the original icon that is being used as a source for the cloned texture
     // that represents the item as it is being dragged.
     getDragActorSource: function() {
-        return this._icon;
+        return this.icon.getDragActorSource();
+    }
+}
+
+function RunningWellItem(appInfo, isFavorite) {
+    this._init(appInfo, isFavorite);
+}
+
+RunningWellItem.prototype = {
+    __proto__: BaseWellItem.prototype,
+
+    _init: function(appInfo, isFavorite) {
+        BaseWellItem.prototype._init.call(this, appInfo, isFavorite);
+
+        this._menuTimeoutId = 0;
+        this._menu = null;
+        this._dragStartX = 0;
+        this._dragStartY = 0;
+
+        this.actor = new Shell.ButtonBox({ orientation: Big.BoxOrientation.VERTICAL,
+                                           border: WELL_MENU_BORDER_WIDTH,
+                                           corner_radius: WELL_MENU_CORNER_RADIUS,
+                                           reactive: true });
+        this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
+        this.actor.connect('notify::hover', Lang.bind(this, this._onHoverChanged));
+        this.actor.connect('activate', Lang.bind(this, this.activateMostRecentWindow));
+
+        this.icon.actor._delegate = this;
+        this._draggable = DND.makeDraggable(this.icon.actor, true);
+
+        this.actor.append(this.icon.actor, Big.BoxPackFlags.NONE);
+    },
+
+    activateMostRecentWindow: function () {
+        // The _get_windows_for_app sorts them for us
+        let mostRecentWindow = this.windows[0];
+        Main.overview.activateWindow(mostRecentWindow, Clutter.get_current_event_time());
+    },
+
+    _onHoverChanged: function() {
+        let hover = this.actor.hover;
+        if (!hover && this._menuTimeoutId > 0) {
+            Mainloop.source_remove(this._menuTimeoutId);
+            this._menuTimeoutId = 0;
+            if (this.actor.pressed && this._dragStartX != null) {
+                this.actor.fake_release();
+                this._draggable.startDrag(this.icon.actor, this._dragStartX, this._dragStartY,
+                                          Clutter.get_current_event_time());
+            } else {
+                this._dragStartX = null;
+                this._dragStartY = null;
+            }
+        }
     },
 
-    setWidth: function(width) {
-        this._nameBox.width = width + GLOW_PADDING * 2;
+    _onButtonPress: function(actor, event) {
+        let [stageX, stageY] = event.get_coords();
+        this._dragStartX = stageX;
+        this._dragStartY = stageY;
+        if (this._menuTimeoutId > 0)
+            Mainloop.source_remove(this._menuTimeoutId);
+        this._menuTimeoutId = Mainloop.timeout_add(WELL_MENU_POPUP_TIMEOUT_MS,
+                                                   Lang.bind(this, this._popupMenu));
+        return false;
+    },
+
+    _popupMenu: function() {
+        this._menuTimeoutId = 0;
+
+        this.actor.fake_release();
+
+        if (this._menu == null) {
+            this._menu = new WellMenu(this);
+            this._menu.connect('highlight-window', Lang.bind(this, function (menu, window) {
+                Main.overview.setHighlightWindow(window);
+            }));
+            this._menu.connect('popup', Lang.bind(this, function (menu, isPoppedUp) {
+                let id;
+                if (isPoppedUp)
+                    id = this.appInfo.get_id();
+                else
+                    id = null;
+                Main.overview.setWindowApplicationFilter(id);
+            }));
+        }
+
+        this._menu.popup();
+
+        return false;
+    }
+}
+
+function InactiveWellItem(appInfo, isFavorite) {
+    this._init(appInfo, isFavorite);
+}
+
+InactiveWellItem.prototype = {
+    __proto__: BaseWellItem.prototype,
+
+    _init : function(appInfo, isFavorite) {
+        BaseWellItem.prototype._init.call(this, appInfo, isFavorite);
+
+        this.actor = new Shell.ButtonBox({ orientation: Big.BoxOrientation.VERTICAL,
+                                           border: WELL_MENU_BORDER_WIDTH,
+                                           corner_radius: WELL_MENU_CORNER_RADIUS,
+                                           reactive: true });
+        this.actor._delegate = this;
+        this.actor.connect('notify::pressed', Lang.bind(this, this._onPressedChanged));
+        this.actor.connect('notify::hover', Lang.bind(this, this._onHoverChanged));
+        this.actor.connect('activate', Lang.bind(this, this._onActivate));
+
+        this.icon.actor._delegate = this;
+        let draggable = DND.makeDraggable(this.icon.actor);
+
+        this.actor.append(this.icon.actor, Big.BoxPackFlags.NONE);
+    },
+
+    _onPressedChanged: function() {
+        let pressed = this.actor.pressed;
+        if (pressed) {
+            this.actor.border_color = WELL_MENU_BORDER_COLOR;
+        } else {
+            this.actor.border_color = TRANSPARENT_COLOR;
+        }
+    },
+
+    _onHoverChanged: function() {
+        let hover = this.actor.hover;
+    },
+
+    _onActivate: function() {
+        if (this.windows.length == 0) {
+            this.appInfo.launch();
+            Main.overview.hide();
+            return true;
+        }
+        return false;
     }
 };
 
@@ -755,10 +1074,15 @@ AppWell.prototype = {
         this._displays = displays;
     },
 
-    _addApps: function(apps) {
+    _addApps: function(apps, isFavorite) {
         for (let i = 0; i < apps.length; i++) {
             let app = apps[i];
-            let display = new WellDisplayItem(app, this.isFavorite);
+            let windows = this._appMonitor.get_windows_for_app(app.get_id());
+            let display;
+            if (windows.length > 0)
+                display = new RunningWellItem(app, isFavorite);
+            else
+                display = new InactiveWellItem(app, isFavorite);
             this._grid.actor.add_actor(display.actor);
         }
     },
@@ -770,7 +1094,7 @@ AppWell.prototype = {
         let appSystem = Shell.AppSystem.get_default();
 
         let app = null;
-        if (source instanceof WellDisplayItem) {
+        if (source instanceof BaseWellItem) {
             app = source.appInfo;
         } else if (source instanceof AppDisplayItem) {
             app = appSystem.lookup_cached_app(source.getId());
diff --git a/js/ui/appIcon.js b/js/ui/appIcon.js
index 46bce10..6cc5fd9 100644
--- a/js/ui/appIcon.js
+++ b/js/ui/appIcon.js
@@ -24,20 +24,22 @@ function AppIcon(appInfo) {
 AppIcon.prototype = {
     _init : function(appInfo) {
         this.appInfo = appInfo;
+        this.windows = Shell.AppMonitor.get_default().get_windows_for_app(appInfo.get_id());
+        for (let i = 0; i < this.windows.length; i++) {
+            this.windows[i].connect('notify::user-time', Lang.bind(this, this._resortWindows));
+        }
+        this._resortWindows();
 
         this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                    corner_radius: 2,
-                                   border: 0,
                                    padding: 1,
-                                   border_color: GenericDisplay.ITEM_DISPLAY_SELECTED_BACKGROUND_COLOR,
                                    reactive: true });
-        this.actor._delegate = this;
 
         let iconBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
                                     x_align: Big.BoxAlignment.CENTER,
                                     y_align: Big.BoxAlignment.CENTER });
-        this._icon = appInfo.create_icon_texture(APP_ICON_SIZE);
-        iconBox.append(this._icon, Big.BoxPackFlags.NONE);
+        this.icon = appInfo.create_icon_texture(APP_ICON_SIZE);
+        iconBox.append(this.icon, Big.BoxPackFlags.NONE);
 
         this.actor.append(iconBox, Big.BoxPackFlags.EXPAND);
 
@@ -98,7 +100,6 @@ AppIcon.prototype = {
         this._name.allocate(childBox, flags);
 
         // Now the glow
-
         if (this._glowBox != null) {
             let glowPaddingHoriz = Math.max(0, xPadding - GLOW_PADDING_HORIZONTAL);
             glowPaddingHoriz = Math.max(GLOW_PADDING_HORIZONTAL, glowPaddingHoriz);
@@ -108,5 +109,25 @@ AppIcon.prototype = {
             childBox.y2 = availHeight;
             this._glowBox.allocate(childBox, flags);
         }
+    },
+
+    _resortWindows: function() {
+        this.windows.sort(function (a, b) {
+            let timeA = a.get_user_time();
+            let timeB = b.get_user_time();
+            if (timeA == timeB)
+                return 0;
+            else if (timeA > timeB)
+                return -1;
+            return 1;
+        });
+    },
+
+    getDragActor: function() {
+        return this.appInfo.create_icon_texture(APP_ICON_SIZE);
+    },
+
+    getDragActorSource: function() {
+        return this.icon;
     }
 };
diff --git a/js/ui/overview.js b/js/ui/overview.js
index bdfea25..a3da235 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -232,7 +232,7 @@ Overview.prototype = {
     // This allows the user to place the item on any workspace.
     handleDragOver : function(source, actor, x, y, time) {
         if (source instanceof GenericDisplay.GenericDisplayItem
-            || source instanceof AppDisplay.WellDisplayItem) {
+            || source instanceof AppDisplay.BaseWellItem) {
             if (this._activeDisplayPane != null)
                 this._activeDisplayPane.close();
             return true;
@@ -390,6 +390,29 @@ Overview.prototype = {
          this.hide();
     },
 
+    /**
+     * setHighlightWindow:
+     * @metaWindow: A #MetaWindow
+     *
+     * Draw the user's attention to the given window @metaWindow.
+     */
+    setHighlightWindow: function (metaWindow) {
+        if (this._workspaces)
+            this._workspaces.setHighlightWindow(metaWindow);
+    },
+
+    /**
+     * setWindowApplicationFilter:
+     * @id: A string application identifier
+     *
+     * Hide all windows which are not owned by the application
+     * identified by @id.
+     */
+    setWindowApplicationFilter: function (id) {
+        if (this._workspaces)
+            this._workspaces.setWindowApplicationFilter(id);
+    },
+
     //// Private methods ////
 
     _showDone: function() {
diff --git a/js/ui/workspaces.js b/js/ui/workspaces.js
index 4f0ee0a..8dceba8 100644
--- a/js/ui/workspaces.js
+++ b/js/ui/workspaces.js
@@ -25,6 +25,8 @@ const WINDOWCLONE_TITLE_COLOR = new Clutter.Color();
 WINDOWCLONE_TITLE_COLOR.from_pixel(0xffffffff);
 const FRAME_COLOR = new Clutter.Color();
 FRAME_COLOR.from_pixel(0xffffffff);
+const LIGHTBOX_COLOR = new Clutter.Color();
+LIGHTBOX_COLOR.from_pixel(0x00000044);
 
 // Define a layout scheme for small window counts. For larger
 // counts we fall back to an algorithm. We need more schemes here
@@ -64,6 +66,8 @@ WindowClone.prototype = {
         this.origX = realWindow.x;
         this.origY = realWindow.y;
 
+        this._title = null;
+
         this.actor.connect('button-release-event',
                            Lang.bind(this, this._onButtonRelease));
 
@@ -79,6 +83,29 @@ WindowClone.prototype = {
         this._inDrag = false;
     },
 
+    setHighlighted: function (highlighted) {
+        let factor = 0.1;
+        if (highlighted) {
+            this.actor.scale_x += factor;
+            this.actor.scale_y += factor;
+        } else {
+            this.actor.scale_x -= factor;
+            this.actor.scale_y -= factor;
+        }
+    },
+
+    setVisibleWithChrome: function(visible) {
+        if (visible) {
+            this.actor.show();
+            if (this._title)
+                this._title.show();
+        } else {
+            this.actor.hide();
+            if (this._title)
+                this._title.hide();
+        }
+    },
+
     destroy: function () {
         this.actor.destroy();
         if (this._title)
@@ -280,6 +307,14 @@ Workspace.prototype = {
         this.actor.height = global.screen_height;
         this.scale = 1.0;
 
+        this._lightbox = new Clutter.Rectangle({ color: LIGHTBOX_COLOR });
+        this.actor.connect('notify::allocation', Lang.bind(this, function () {
+            let [width, height] = this.actor.get_size();
+            this._lightbox.set_size(width, height);
+        }));
+        this.actor.add_actor(this._lightbox);
+        this._lightbox.hide();
+
         let windows = global.get_windows().filter(this._isMyWindow, this);
 
         // Find the desktop window
@@ -311,6 +346,9 @@ Workspace.prototype = {
             }
         }
 
+        // A filter for what windows we display
+        this._showOnlyWindows = null;
+
         // Track window changes
         this._windowAddedId = this._metaWorkspace.connect('window-added',
                                                           Lang.bind(this, this._windowAdded));
@@ -386,6 +424,32 @@ Workspace.prototype = {
         return index < 0 ? null : this._windows[index];
     },
 
+    containsMetaWindow: function (metaWindow) {
+        return this._lookupIndex(metaWindow) >= 0;
+    },
+
+    setShowOnlyWindows: function(showOnlyWindows) {
+        this._showOnlyWindows = showOnlyWindows;
+        this.positionWindows(false);
+    },
+
+    setLightboxMode: function (showLightbox) {
+        if (showLightbox)
+            this._lightbox.show();
+        else
+            this._lightbox.hide();
+    },
+
+    setHighlightWindow: function (metaWindow) {
+        for (let i = 0; i < this._windows.length; i++) {
+            this._windows[i].actor.lower(this._lightbox);
+        }
+        if (metaWindow != null) {
+            let clone = this.lookupCloneForMetaWindow(metaWindow);
+            clone.actor.raise(this._lightbox);
+        }
+    },
+
     _adjustRemoveButton : function() {
         this._removeButton.set_scale(1.0 / this.actor.scale_x,
                                      1.0 / this.actor.scale_y);
@@ -440,12 +504,37 @@ Workspace.prototype = {
     positionWindows : function(workspaceZooming) {
         let global = Shell.Global.get();
 
+        let totalVisible = 0;
+
+        for (let i = 1; i < this._windows.length; i++) {
+            let clone = this._windows[i];
+
+            if (this._showOnlyWindows != null && !(clone.metaWindow in this._showOnlyWindows))
+                continue;
+
+            totalVisible += 1;
+        }
+
+        let previousWindow = this._windows[0];
+        let visibleIndex = 0;
         for (let i = 1; i < this._windows.length; i++) {
             let clone = this._windows[i];
             let icon = this._windowIcons[i];
-            clone.stackAbove = this._windows[i - 1].actor;
 
-            let [xCenter, yCenter, fraction] = this._computeWindowPosition(i);
+            if (this._showOnlyWindows != null && !(clone.metaWindow in this._showOnlyWindows)) {
+                clone.setVisibleWithChrome(false);
+                icon.hide();
+                continue;
+            } else {
+                clone.setVisibleWithChrome(true);
+            }
+
+            clone.stackAbove = previousWindow.actor;
+            previousWindow = clone;
+
+            visibleIndex += 1;
+
+            let [xCenter, yCenter, fraction] = this._computeWindowPosition(visibleIndex, totalVisible);
             xCenter = xCenter * global.screen_width;
             yCenter = yCenter * global.screen_height;
 
@@ -508,6 +597,8 @@ Workspace.prototype = {
         for (let i = 1; i < this._windows.length; i++) {
             let clone = this._windows[i];
             let icon = this._windowIcons[i];
+            if (this._showOnlyWindows != null && !(clone.metaWindow in this._showOnlyWindows))
+                continue;
             this._fadeInWindowIcon(clone, icon);
         }
     },
@@ -794,10 +885,10 @@ Workspace.prototype = {
         return clone;
     },
 
-    _computeWindowPosition : function(index) {
+    _computeWindowPosition : function(index, totalWindows) {
         // ignore this._windows[0], which is the desktop
         let windowIndex = index - 1;
-        let numberOfWindows = this._windows.length - 1;
+        let numberOfWindows = totalWindows;
 
         if (numberOfWindows in POSITIONS)
             return POSITIONS[numberOfWindows][windowIndex];
@@ -872,15 +963,19 @@ Workspaces.prototype = {
 
         this.actor = new Clutter.Group();
 
+        this._appIdFilter = null;
+
         let screenHeight = global.screen_height;
-          
+
         this._width = width;
         this._height = height;
         this._x = x;
         this._y = y;
 
         this._workspaces = [];
-        
+
+        this._highlightWindow = null;
+
         let activeWorkspaceIndex = global.screen.get_active_workspace_index();
         let activeWorkspace;
 
@@ -927,6 +1022,14 @@ Workspaces.prototype = {
                                           Lang.bind(this, this._activeWorkspaceChanged));
     },
 
+    _lookupWorkspaceForMetaWindow: function (metaWindow) {
+        for (let i = 0; i < this._workspaces.length; i++) {
+            if (this._workspaces[i].containsMetaWindow(metaWindow))
+                return this._workspaces[i];
+        }
+        return null;
+    },
+
     _lookupCloneForMetaWindow: function (metaWindow) {
         for (let i = 0; i < this._workspaces.length; i++) {
             let clone = this._workspaces[i].lookupCloneForMetaWindow(metaWindow);
@@ -936,6 +1039,39 @@ Workspaces.prototype = {
         return null;
     },
 
+    setHighlightWindow: function (metaWindow) {
+        // Looping over all workspaces is easier than keeping track of the last
+        // highlighted window while trying to handle the window or workspace possibly
+        // going away.
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._workspaces[i].setLightboxMode(metaWindow != null);
+            this._workspaces[i].setHighlightWindow(null);
+        }
+        if (metaWindow != null) {
+            let workspace = this._lookupWorkspaceForMetaWindow(metaWindow);
+            workspace.setHighlightWindow(metaWindow);
+        }
+    },
+
+    setWindowApplicationFilter: function (appId) {
+        let appSys = Shell.AppMonitor.get_default();
+
+        let showOnlyWindows;
+        if (appId) {
+            let windows = appSys.get_windows_for_app(appId);
+            showOnlyWindows = {};
+            for (let i = 0; i < windows.length; i++) {
+                showOnlyWindows[windows[i]] = 1;
+            }
+        } else {
+            showOnlyWindows = null;
+        }
+        this._appIdFilter = appId;
+        for (let i = 0; i < this._workspaces.length; i++) {
+            this._workspaces[i].setShowOnlyWindows(showOnlyWindows);
+        }
+    },
+
     // Should only be called from active Overview context
     activateWindowFromOverview: function (metaWindow, time) {
         let global = Shell.Global.get();
diff --git a/src/shell-drawing.c b/src/shell-drawing.c
index 1032547..2fcfec0 100644
--- a/src/shell-drawing.c
+++ b/src/shell-drawing.c
@@ -146,6 +146,36 @@ shell_draw_clock (ClutterCairoTexture *texture,
   cairo_destroy (cr);
 }
 
+void
+shell_draw_box_pointer (ClutterCairoTexture *texture,
+                        ClutterColor        *border_color,
+                        ClutterColor        *background_color)
+{
+  guint width, height;
+  cairo_t *cr;
+
+  clutter_cairo_texture_get_surface_size (texture, &width, &height);
+
+  clutter_cairo_texture_clear (texture);
+  cr = clutter_cairo_texture_create (texture);
+
+  cairo_set_line_width (cr, 1.0);
+
+  clutter_cairo_set_source_color (cr, border_color);
+
+  cairo_move_to (cr, width, 0);
+  cairo_line_to (cr, 0, floor (height * 0.5));
+  cairo_line_to (cr, width, height);
+
+  cairo_stroke_preserve (cr);
+
+  clutter_cairo_set_source_color (cr, background_color);
+
+  cairo_fill (cr);
+
+  cairo_destroy (cr);
+}
+
 static void
 hook_paint_red_border (ClutterActor  *actor,
                        gpointer       user_data)
diff --git a/src/shell-drawing.h b/src/shell-drawing.h
index aea9a24..3dd477a 100644
--- a/src/shell-drawing.h
+++ b/src/shell-drawing.h
@@ -13,6 +13,10 @@ ClutterCairoTexture *shell_create_vertical_gradient (ClutterColor *top,
 ClutterCairoTexture *shell_create_horizontal_gradient (ClutterColor *left,
                                                        ClutterColor *right);
 
+void shell_draw_box_pointer (ClutterCairoTexture *texture,
+                             ClutterColor        *border_color,
+                             ClutterColor        *background_color);
+
 void shell_draw_clock (ClutterCairoTexture *texture,
 	               int                  hour,
 	               int                  minute);



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