diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index b313605..4f035ad 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -26,6 +26,19 @@ const WELL_DEFAULT_COLUMNS = 4;
 const WELL_ITEM_HSPACING = 0;
 const WELL_ITEM_VSPACING = 4;
 
+const WELL_MENU_POPUP_TIMEOUT_MS = 600;
+
+const WELL_MENU_BACKGROUND_COLOR = new Clutter.Color();
+WELL_MENU_BACKGROUND_COLOR.from_pixel(0x000000dd);
+const WELL_MENU_FONT = 'Sans 14px';
+const WELL_MENU_COLOR = new Clutter.Color();
+WELL_MENU_COLOR.from_pixel(0xffffffff);
+const WELL_MENU_BORDER_COLOR = new Clutter.Color();
+WELL_MENU_BORDER_COLOR.from_pixel(0x6d6d6dff);
+const WELL_MENU_BORDER_WIDTH = 1;
+const WELL_MENU_CORNER_RADIUS = 4;
+const WELL_MENU_PADDING = 4;
+
 const MENU_ICON_SIZE = 24;
 const MENU_SPACING = 15;
 
@@ -161,7 +174,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,35 +460,210 @@ 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;
+
+        // True if the user is holding the mouse down
+        this._heldDown = false;
+        // True if we have a Clutter pointer grab
+        this._haveGrab = false;
+        // True if the mouse is over a window entry
+        this._inWindow = false;
+
+        this.actor = new Big.Box({ 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 = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
+                                              spacing: 8,
+                                              padding: 8 });
+        this.actor.append(this._windowContainer, Big.BoxPackFlags.EXPAND);
+        this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress));
+        this.actor.connect('button-release-event', Lang.bind(this, this._onButtonRelease));
+        this.actor.connect('enter-event', Lang.bind(this, this._onEnter));
+        this.actor.connect('leave-event', Lang.bind(this, this._onLeave));
+
+        let stage = Shell.Global.get().stage;
+
+        // Chain our visibility and lifecycle to that of the source
+        source.actor.connect('notify::mapped', Lang.bind(this, function () {
+            if (!source.actor.mapped)
+                this._popdown();
+        }));
+        source.actor.connect('destroy', Lang.bind(this, function () { this.actor.destroy(); }));
 
-    _init : function(appInfo, isFavorite) {
-        AppIcon.AppIcon.prototype._init.call(this, appInfo);
+        stage.add_actor(this.actor);
+    },
 
-        this.isFavorite = isFavorite;
+    _redisplay: function() {
+        this._windowContainer.remove_all();
 
-        this.actor.connect('button-release-event', Lang.bind(this, function (b, e) {
-            this._handleActivate();
-        }));
+        let windows = this._source.windows;
+
+        // Don't show a window list with just one window
+        /*
+        if (windows.length == 1) {
+            let window = windows[0];
+            this.emit('highlight-window', window);
+            this._windowContainer.hide();
+            return;
+        }
+        */
 
-        let draggable = DND.makeDraggable(this.actor);
+        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;
+            }
+        }
+        for (let i = 0; i < windows.length; i++) {
+            let window = windows[i];
+            let box = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                    spacing: 4,
+                                    reactive: true });
+            box._window = windows[i];
+            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);
+        }
     },
 
-    _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());
+    popup: function() {
+        let [stageX, stageY] = this._source.actor.get_transformed_position();
+        let [stageWidth, stageHeight] = this._source.actor.get_transformed_size();
+
+        this._redisplay();
+
+        this._heldDown = true;
+        this._haveGrab = true;
+        Clutter.grab_pointer(this.actor);
+
+        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();
+    },
+
+    _containsActor : function(actor) {
+        while (actor && actor != this.actor) {
+            actor = actor.get_parent();
         }
+        return actor != null;
+    },
+
+    _onWindowEnter: function (actor, event) {
+        let window = actor._window;
+        this.emit('highlight-window', window);
+    },
+
+    _onWindowLeave: function (actor, event) {
+        this.emit('highlight-window', null);
+    },
+
+    _onWindowRelease: function (actor, event) {
+        let window = actor._window;
+        Main.overview.activateWindow(window, event.get_time());
+        this._popdown();
+    },
+
+    _onButtonPress: function(actor, event) {
+        let source = event.get_source();
+        if (!this._heldDown)
+            this._popdown();
+    },
+
+    _onButtonRelease: function(actor, event) {
+        let source = event.get_source();
+        if (source == this.actor) {
+            if (this._source.windows.length == 1) {
+                // If we only have one window, there's no menu, so
+                // on mouse release activate it.
+                let window = this._source.windows[0];
+                Main.overview.activateWindow(window, event.get_time());
+                this._popdown();
+            } else {
+                // Keep the menu popped up
+                log("heldDown -> false");
+                this._heldDown = false;
+            }
+        } else if (this._containsActor(source)) {
+            this._onWindowRelease(source, event);
+        } else if (this._heldDown) {
+            // A release outside our hierarchy when we're
+            // in "held down" mode means we need to hide.
+            this._popdown();
+        }
+    },
+
+    _onEnter: function (actor, event) {
+        let source = event.get_source();
+        let related = Shell.get_event_related(event);
+        if (source == this.actor) {
+            return;
+        } else if (this._containsActor(source)) {
+            this._onWindowEnter(source, event);
+        }
+        return;
+    },
+
+    _onLeave: function (actor, event) {
+        let source = event.get_source();
+        let related = Shell.get_event_related(event);
+        if (related == this.actor && this._containsActor(source)) {
+            this._onWindowLeave(actor, event);
+        }
+    },
+
+    _popdown: function () {
+        log("popdown");
+        if (this._haveGrab) {
+            Clutter.ungrab_pointer();
+            this._haveGrab = false;
+        }
+        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() {
@@ -492,17 +679,148 @@ 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 = {
+    _init: function(appInfo, isFavorite) {
+        BaseWellItem.prototype._init.call(this, appInfo, isFavorite);
+
+        this._menuTimeoutId = 0;
+        this._menu = null;
+
+        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('hover-event', Lang.bind(this, this._onHoverEvent));
+        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);
     },
 
-    setWidth: function(width) {
-        this._nameBox.width = width + GLOW_PADDING * 2;
+    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;
+        }
+    },
+
+    _onHoverEvent: function (actor, event) {
+        let [stageX, stageY] = event.get_coords();
+        if (this.actor.pressed) {
+            this.actor.fake_release();
+            this._draggable.startDrag(this.icon.actor, stageX, stageY,
+                                      event.get_time());
+        }
+    },
+
+    _onButtonPress: function() {
+        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;
+
+        log("popupMenu");
+
+        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;
+                log("popup: " + isPoppedUp + " setting filter to " + id);
+                Main.overview.setWindowApplicationFilter(id);
+            }));
+        }
+
+        this._menu.popup();
+
+        return false;
+    }
+}
+
+function InactiveWellItem(appInfo, isFavorite) {
+    this._init(appInfo, isFavorite);
+}
+
+InactiveWellItem.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 {
+            let transparent = new Clutter.Color();
+            transparent.from_pixel(0x00000000);
+            this.actor.border_color = transparent;
+        }
+    },
+
+    _onHoverChanged: function() {
+        let hover = this.actor.hover;
+        log("hover: " + hover);
+    },
+
+    _onActivate: function() {
+        if (this.windows.length == 0) {
+            this.appInfo.launch();
+            Main.overview.hide();
+            return true;
+        }
+        return false;
     }
 };
 
@@ -755,10 +1073,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 +1093,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 12ac9b3..3ea4487 100644
--- a/js/ui/appIcon.js
+++ b/js/ui/appIcon.js
@@ -22,25 +22,25 @@ 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);
 
-        this._windows = Shell.AppMonitor.get_default().get_windows_for_app(appInfo.get_id());
-
         let nameBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
                                     x_align: Big.BoxAlignment.CENTER });
         this._nameBox = nameBox;
@@ -51,11 +51,11 @@ AppIcon.prototype = {
                                         ellipsize: Pango.EllipsizeMode.END,
                                         text: appInfo.get_name() });
         nameBox.append(this._name, Big.BoxPackFlags.NONE);
-        if (this._windows.length > 0) {
+        if (this.windows.length > 0) {
             let glow = new Shell.DrawingArea({});
             glow.connect('redraw', Lang.bind(this, function (e, tex) {
                 Shell.draw_app_highlight(tex,
-                                         this._windows.length,
+                                         this.windows.length,
                                          GLOW_COLOR.red / 255,
                                          GLOW_COLOR.green / 255,
                                          GLOW_COLOR.blue / 255,
@@ -79,5 +79,25 @@ AppIcon.prototype = {
             glow.lower(this._name);
         }
         this.actor.append(nameBox, Big.BoxPackFlags.NONE);
+    },
+
+    _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/dnd.js b/js/ui/dnd.js
index 87cb5d8..5828b06 100644
--- a/js/ui/dnd.js
+++ b/js/ui/dnd.js
@@ -8,15 +8,17 @@ const Tweener = imports.ui.tweener;
 
 const SNAP_BACK_ANIMATION_TIME = 0.25;
 
-function _Draggable(actor) {
-    this._init(actor);
+function _Draggable(actor, manualMode) {
+    this._init(actor, manualMode);
 }
 
 _Draggable.prototype = {
-    _init : function(actor) {
+    _init : function(actor, manualMode) {
         this.actor = actor;
-        this.actor.connect('button-press-event',
-                           Lang.bind(this, this._onButtonPress));
+        if (!manualMode)
+            this.actor.connect('button-press-event',
+                               Lang.bind(this, this._onButtonPress));
+        this._haveSourceGrab = false;
     },
 
     _onButtonPress : function (actor, event) {
@@ -25,6 +27,7 @@ _Draggable.prototype = {
         if (Tweener.getTweenCount(actor))
             return false;
 
+        this._haveSourceGrab = true;
         this._grabActor(actor);
 
         let [stageX, stageY] = event.get_coords();
@@ -66,6 +69,81 @@ _Draggable.prototype = {
             return false;
     },
 
+    /**
+     * startDrag:
+     * @actor: Origin actor for drag and drop
+     * @stageX: X coordinate of event
+     * @stageY: Y coordinate of event
+     * @time: Event timestamp
+     *
+     * Directly initiate a drag and drop operation from the given actor.
+     * This function is useful to call if you've specified manualMode
+     * for the draggable.
+     */
+    startDrag: function (actor, stageX, stageY, time) {
+        this.emit('drag-begin', time);
+
+        this._dragStartX = stageX;
+        this._dragStartY = stageY;
+
+        if (this.actor._delegate && this.actor._delegate.getDragActor) {
+            this._dragActor = this.actor._delegate.getDragActor(this._dragStartX, this._dragStartY);
+            // Drag actor does not always have to be the same as actor. For example drag actor
+            // can be an image that's part of the actor. So to perform "snap back" correctly we need
+             // to know what was the drag actor source.
+            if (this.actor._delegate.getDragActorSource) {
+                this._dragActorSource = this.actor._delegate.getDragActorSource();
+                // If the user dragged from the source, then position
+                // the dragActor over it. Otherwise, center it
+                // around the pointer
+                let [sourceX, sourceY] = this._dragActorSource.get_transformed_position();
+                let [sourceWidth, sourceHeight] = this._dragActorSource.get_transformed_size();
+                let x, y;
+                if (stageX > sourceX && stageX <= sourceX + sourceWidth &&
+                    stageY > sourceY && stageY <= sourceY + sourceHeight) {
+                    x = sourceX;
+                    y = sourceY;
+                } else {
+                    x = stageX - this._dragActor.width / 2;
+                    y = stageY - this._dragActor.height / 2;
+                }
+                log("setting position to: " + x + " " + y);
+                this._dragActor.set_position(x, y);
+            } else {
+                this._dragActorSource = this.actor;
+            }
+            this._dragOrigParent = undefined;
+            if (this._haveSourceGrab) {
+                this._haveSourceGrab = false;
+                this._ungrabActor(actor);
+            }
+            this._grabActor(this._dragActor);
+
+            this._dragOffsetX = this._dragActor.x - this._dragStartX;
+            this._dragOffsetY = this._dragActor.y - this._dragStartY;
+        } else {
+            this._dragActor = actor;
+            this._dragActorSource = undefined;
+            this._dragOrigParent = actor.get_parent();
+            this._dragOrigX = this._dragActor.x;
+            this._dragOrigY = this._dragActor.y;
+            this._dragOrigScale = this._dragActor.scale_x;
+
+            let [actorStageX, actorStageY] = actor.get_transformed_position();
+            this._dragOffsetX = actorStageX - this._dragStartX;
+            this._dragOffsetY = actorStageY - this._dragStartY;
+
+            // Set the actor's scale such that it will keep the same
+            // transformed size when it's reparented to the stage
+            let [scaledWidth, scaledHeight] = actor.get_transformed_size();
+            actor.set_scale(scaledWidth / actor.width,
+                           scaledHeight / actor.height);
+        }
+
+        this._dragActor.reparent(actor.get_stage());
+        this._dragActor.raise_top();
+    },
+
     _onMotion : function (actor, event) {
         let [stageX, stageY] = event.get_coords();
 
@@ -75,55 +153,7 @@ _Draggable.prototype = {
         if (!this._dragActor &&
             (Math.abs(stageX - this._dragStartX) > threshold ||
              Math.abs(stageY - this._dragStartY) > threshold)) {
-            this.emit('drag-begin', event.get_time());
-
-            if (this.actor._delegate && this.actor._delegate.getDragActor) {
-                this._dragActor = this.actor._delegate.getDragActor(this._dragStartX, this._dragStartY);
-                // Drag actor does not always have to be the same as actor. For example drag actor
-                // can be an image that's part of the actor. So to perform "snap back" correctly we need
-                // to know what was the drag actor source.
-                if (this.actor._delegate.getDragActorSource) {
-                    this._dragActorSource = this.actor._delegate.getDragActorSource();
-                    // If the user dragged from the source, then position
-                    // the dragActor over it. Otherwise, center it
-                    // around the pointer
-                    let [sourceX, sourceY] = this._dragActorSource.get_transformed_position();
-                    let [sourceWidth, sourceHeight] = this._dragActorSource.get_transformed_size();
-                    if (stageX > sourceX && stageX <= sourceX + sourceWidth &&
-                        stageY > sourceY && stageY <= sourceY + sourceHeight)
-                        this._dragActor.set_position(sourceX, sourceY);
-                    else
-                        this._dragActor.set_position(stageX - this._dragActor.width / 2, stageY - this._dragActor.height / 2);
-                } else {
-                    this._dragActorSource = this.actor;
-                }
-                this._dragOrigParent = undefined;
-                this._ungrabActor(actor);
-                this._grabActor(this._dragActor);
-
-                this._dragOffsetX = this._dragActor.x - this._dragStartX;
-                this._dragOffsetY = this._dragActor.y - this._dragStartY;
-            } else {
-                this._dragActor = actor;
-                this._dragActorSource = undefined;
-                this._dragOrigParent = actor.get_parent();
-                this._dragOrigX = this._dragActor.x;
-                this._dragOrigY = this._dragActor.y;
-                this._dragOrigScale = this._dragActor.scale_x;
-
-                let [actorStageX, actorStageY] = actor.get_transformed_position();
-                this._dragOffsetX = actorStageX - this._dragStartX;
-                this._dragOffsetY = actorStageY - this._dragStartY;
-
-                // Set the actor's scale such that it will keep the same
-                // transformed size when it's reparented to the stage
-                let [scaledWidth, scaledHeight] = actor.get_transformed_size();
-                actor.set_scale(scaledWidth / actor.width,
-                                scaledHeight / actor.height);
-            }
-
-            this._dragActor.reparent(actor.get_stage());
-            this._dragActor.raise_top();
+                this.startDrag(actor, stageX, stageY, event.get_time());
         }
 
         // If we are dragging, update the position
@@ -149,7 +179,7 @@ _Draggable.prototype = {
                                                     event.get_time());
                 }
                 target = target.get_parent();
-            }  
+            }
         }
 
         return true;
@@ -225,6 +255,13 @@ _Draggable.prototype = {
 
 Signals.addSignalMethods(_Draggable.prototype);
 
-function makeDraggable(actor) {
-    return new _Draggable(actor);
+/**
+ * makeDraggable:
+ * @actor: Source actor
+ * @manualMode: If given, do not automatically start drag and drop on click
+ *
+ * Create an object which controls drag and drop for the given actor.
+ */
+function makeDraggable(actor, manualMode) {
+    return new _Draggable(actor, manualMode);
 }
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 7fa0bb1..eb095cf 100644
--- a/js/ui/workspaces.js
+++ b/js/ui/workspaces.js
@@ -64,6 +64,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 +81,30 @@ WindowClone.prototype = {
         this._inDrag = false;
     },
 
+    // FIXME temporary hack
+    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)
@@ -311,6 +337,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 +415,11 @@ Workspace.prototype = {
         return index < 0 ? null : this._windows[index];
     },
 
+    setShowOnlyWindows: function(showOnlyWindows) {
+        this._showOnlyWindows = showOnlyWindows;
+        this.positionWindows(false);
+    },
+
     _adjustRemoveButton : function() {
         this._removeButton.set_scale(1.0 / this.actor.scale_x,
                                      1.0 / this.actor.scale_y);
@@ -440,12 +474,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 +567,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);
         }
     },
@@ -797,10 +858,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];
@@ -875,8 +936,10 @@ Workspaces.prototype = {
 
         this.actor = new Clutter.Group();
 
+        this._appIdFilter = null;
+
         let screenHeight = global.screen_height;
-          
+
         this._width = width;
         this._height = height;
         this._x = x;
@@ -939,6 +1002,37 @@ Workspaces.prototype = {
         return null;
     },
 
+    setHighlightWindow: function (metaWindow) {
+        if (this._highlightWindow != null)
+            this._highlightWindow.setHighlighted(false);
+        if (metaWindow != null) {
+            this._highlightWindow = this._lookupCloneForMetaWindow(metaWindow);
+            this._highlightWindow.setHighlighted(true);
+        } else {
+            this._highlightWindow = null;
+        }
+    },
+
+    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;
+            }
+            log("windows: " + windows + " visible: " + showOnlyWindows);
+        } 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/Makefile.am b/src/Makefile.am
index b855287..b516286 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -59,6 +59,8 @@ libgnome_shell_la_SOURCES =			\
 	shell-app-system.h			\
 	shell-arrow.c			\
 	shell-arrow.h			\
+	shell-arrow-box.c           \
+	shell-arrow-box.h           \
 	shell-button-box.c           \
 	shell-button-box.h           \
 	shell-drawing.c            \
diff --git a/src/shell-arrow-box.c b/src/shell-arrow-box.c
new file mode 100644
index 0000000..58f0678
--- /dev/null
+++ b/src/shell-arrow-box.c
@@ -0,0 +1,503 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
+
+/**
+ * SECTION:shell-arrow-box
+ * @short_description: A container which can draw a triangle pointer to a location
+ *
+ * A bin container which draws an indication triangle from a given side.  Useful
+ * to implement a popup preview or the like.
+ */
+
+#include "shell-arrow-box.h"
+
+G_DEFINE_TYPE(ShellArrowBox, shell_arrow_box, CLUTTER_TYPE_GROUP);
+
+struct _ShellArrowBoxPrivate {
+  GtkPositionType arrow_side;
+  float corner_radius;
+  float border;
+  ClutterColor border_color;
+  float arrow_width;
+  float arrow_height;
+  float arrow_offset;
+};
+
+enum {
+  PROP_0,
+
+  PROP_CORNER_RADIUS,
+  PROP_BORDER,
+  PROP_BORDER_COLOR,
+  PROP_ARROW_SIDE,
+  PROP_ARROW_WIDTH,
+  PROP_ARROW_HEIGHT,
+  PROP_ARROW_OFFSET
+};
+
+static void
+shell_arrow_box_set_property(GObject         *object,
+                              guint            prop_id,
+                              const GValue    *value,
+                              GParamSpec      *pspec)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_CORNER_RADIUS:
+      box->priv->corner_radius = g_value_get_float (value);
+      clutter_actor_queue_redraw (CLUTTER_ACTOR (object));
+    break;
+    case PROP_BORDER:
+      box->priv->border = g_value_get_float (value);
+      clutter_actor_queue_relayout (CLUTTER_ACTOR (object));
+      break;
+    case PROP_BORDER_COLOR:
+      box->priv->border_color = *(ClutterColor*)g_value_get_boxed (value);
+      clutter_actor_queue_redraw (CLUTTER_ACTOR (object));
+      break;
+    case PROP_ARROW_SIDE:
+      box->priv->arrow_side = g_value_get_enum (value);
+      clutter_actor_queue_relayout (CLUTTER_ACTOR (object));
+    break;
+    case PROP_ARROW_WIDTH:
+      box->priv->arrow_width = g_value_get_float (value);
+      clutter_actor_queue_relayout (CLUTTER_ACTOR (object));
+    break;
+    case PROP_ARROW_HEIGHT:
+      box->priv->arrow_height = g_value_get_float (value);
+      clutter_actor_queue_relayout (CLUTTER_ACTOR (object));
+    break;
+    case PROP_ARROW_OFFSET:
+      box->priv->arrow_offset = g_value_get_float (value);
+      clutter_actor_queue_redraw (CLUTTER_ACTOR (object));
+    break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+shell_arrow_box_get_property(GObject         *object,
+                              guint            prop_id,
+                              GValue          *value,
+                              GParamSpec      *pspec)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_CORNER_RADIUS:
+      g_value_set_float (value, box->priv->corner_radius);
+      break;
+    case PROP_BORDER:
+      g_value_set_float (value, box->priv->border);
+      break;
+    case PROP_BORDER_COLOR:
+      g_value_set_boxed (value, &box->priv->border_color);
+      break;
+    case PROP_ARROW_SIDE:
+      g_value_set_enum (value, box->priv->arrow_side);
+      break;
+    case PROP_ARROW_WIDTH:
+      g_value_set_float (value, box->priv->arrow_width);
+      break;
+    case PROP_ARROW_HEIGHT:
+      g_value_set_float (value, box->priv->arrow_height);
+      break;
+    case PROP_ARROW_OFFSET:
+      g_value_set_float (value, box->priv->arrow_offset);
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+shell_arrow_box_get_preferred_width (ClutterActor *actor,
+                                     gfloat for_height,
+                                     gfloat *min_width_p,
+                                     gfloat *natural_width_p)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (actor);
+  GList *children;
+  ClutterActor *child;
+  float arrow_height;
+
+  children = clutter_container_get_children (CLUTTER_CONTAINER (actor));
+
+  if (children == NULL)
+    return;
+
+  child = children->data;
+
+  if (box->priv->arrow_side == GTK_POS_LEFT || box->priv->arrow_side == GTK_POS_RIGHT)
+    arrow_height = box->priv->arrow_height;
+  else
+    arrow_height = 0;
+
+  clutter_actor_get_preferred_width (child, for_height, min_width_p, natural_width_p);
+  if (min_width_p)
+    *min_width_p += arrow_height;
+  if (natural_width_p)
+    *natural_width_p += arrow_height;
+
+  g_list_free (children);
+}
+
+static void
+shell_arrow_box_get_preferred_height (ClutterActor *actor,
+                                      gfloat for_width,
+                                      gfloat *min_height_p,
+                                      gfloat *natural_height_p)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (actor);
+  GList *children;
+  ClutterActor *child;
+  float arrow_height;
+
+  children = clutter_container_get_children (CLUTTER_CONTAINER (actor));
+
+  if (children == NULL)
+    return;
+
+  child = children->data;
+
+  if (box->priv->arrow_side == GTK_POS_TOP || box->priv->arrow_side == GTK_POS_BOTTOM)
+    arrow_height = box->priv->arrow_height;
+  else
+    arrow_height = 0;
+
+  clutter_actor_get_preferred_height (child, for_width, min_height_p, natural_height_p);
+  if (min_height_p)
+    *min_height_p += arrow_height + box->priv->border * 2;
+  if (natural_height_p)
+    *natural_height_p += arrow_height + box->priv->border * 2;
+
+  g_list_free (children);
+}
+
+static void
+compute_child_box (ShellArrowBox   *box,
+                   float            width,
+                   float            height,
+                   ClutterActorBox *child_box)
+{
+  child_box->x1 = box->priv->border;
+  child_box->y1 = box->priv->border;
+  child_box->x2 = width - box->priv->border;
+  child_box->y2 = height - box->priv->border;
+
+  switch (box->priv->arrow_side)
+    {
+    case GTK_POS_TOP:
+      child_box->y1 += box->priv->arrow_height;
+      break;
+    case GTK_POS_RIGHT:
+      child_box->x2 -= box->priv->arrow_height;
+      break;
+    case GTK_POS_BOTTOM:
+      child_box->y2 -= box->priv->arrow_height;
+      break;
+    case GTK_POS_LEFT:
+      child_box->x1 += box->priv->arrow_height;
+      break;
+    }
+}
+
+static void
+shell_arrow_box_allocate (ClutterActor *actor,
+                          const ClutterActorBox *alloc_box,
+                          ClutterAllocationFlags flags)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (actor);
+  GList *children;
+  ClutterActor *child;
+  ClutterActorBox child_box;
+  float width, height;
+
+  /* chain up directly to ClutterActor to set actor->allocation */
+  (CLUTTER_ACTOR_CLASS (g_type_class_peek (clutter_actor_get_type ())))->allocate (actor, alloc_box, flags);
+
+  children = clutter_container_get_children (CLUTTER_CONTAINER (actor));
+  if (children == NULL)
+    return;
+
+  width = alloc_box->x2 - alloc_box->x1;
+  height = alloc_box->y2 - alloc_box->y1;
+
+  child = children->data;
+
+  compute_child_box (box, width, height, &child_box);
+
+  clutter_actor_allocate (child, &child_box, flags);
+
+  g_list_free (children);
+}
+
+static void
+shell_arrow_box_paint (ClutterActor *actor)
+{
+  ShellArrowBox *box = SHELL_ARROW_BOX (actor);
+  GList *children;
+  ClutterGeometry allocation;
+  ClutterActor *child;
+  float inner_width, inner_height, radius;
+  ClutterActorBox child_box;
+  guint8 actor_opacity;
+  CoglColor tmp_color;
+  float arrow_offset;
+
+  children = clutter_container_get_children (CLUTTER_CONTAINER (actor));
+  if (children == NULL)
+    return;
+
+  clutter_actor_get_allocation_geometry (actor, &allocation);
+
+  compute_child_box (box, allocation.width, allocation.height, &child_box);
+
+  if (box->priv->arrow_offset == -1)
+    {
+      if (box->priv->arrow_side == GTK_POS_RIGHT ||
+          box->priv->arrow_side == GTK_POS_LEFT)
+        arrow_offset = allocation.width / 2;
+      else
+        arrow_offset = allocation.height / 2;
+    }
+  else
+    {
+      arrow_offset = box->priv->arrow_offset;
+    }
+
+  radius = box->priv->corner_radius;
+
+  actor_opacity = clutter_actor_get_paint_opacity (actor);
+
+  cogl_push_matrix ();
+
+  cogl_color_set_from_4ub(&tmp_color,
+                          box->priv->border_color.red,
+                          box->priv->border_color.green,
+                          box->priv->border_color.blue,
+                          actor_opacity * box->priv->border_color.alpha / 255);
+
+  /* This code adapated from clutter/cogl/common/cogl-primitives.c:cogl_path_round_rectangle
+   *
+   * An object oriented GL/GLES Abstraction/Utility Layer
+   *
+   * Copyright (C) 2007,2008,2009 Intel Corporation.
+   *
+   * This library is free software; you can redistribute it and/or
+   * modify it under the terms of the GNU Lesser General Public
+   * License as published by the Free Software Foundation; either
+   * version 2 of the License, or (at your option) any later version.
+   **/
+
+  inner_width = (child_box.x2 - child_box.x1) - radius * 2;
+  inner_height = (child_box.y2 - child_box.y1) - radius * 2;
+
+  cogl_path_new ();
+
+  cogl_path_move_to (child_box.x1, child_box.y1 + radius);
+
+  cogl_path_arc (child_box.x1 + radius, child_box.y1 + radius,
+                 radius,
+                 radius,
+                 180,
+                 270);
+
+  if (box->priv->arrow_side == GTK_POS_TOP)
+    {
+      cogl_path_line_to (child_box.x1 + box->priv->arrow_offset, child_box.y1);
+      cogl_path_rel_line_to (box->priv->arrow_width / 2, -box->priv->arrow_height);
+      cogl_path_rel_line_to (box->priv->arrow_width / 2, box->priv->arrow_height);
+    }
+
+  cogl_path_arc (child_box.x2 - radius, child_box.y1 + radius,
+                 radius,
+                 radius,
+                 -90,
+                 0);
+
+  if (box->priv->arrow_side == GTK_POS_RIGHT)
+    {
+      cogl_path_line_to (child_box.x2, child_box.y1 + box->priv->arrow_offset);
+      cogl_path_rel_line_to (box->priv->arrow_height, box->priv->arrow_width / 2);
+      cogl_path_rel_line_to (-box->priv->arrow_height, box->priv->arrow_width / 2);
+    }
+
+  cogl_path_arc (child_box.x2 - radius, child_box.y2 - radius,
+                 radius,
+                 radius,
+                 0,
+                 90);
+
+  if (box->priv->arrow_side == GTK_POS_BOTTOM)
+    {
+      cogl_path_line_to (child_box.x1 + box->priv->arrow_offset + box->priv->arrow_width, child_box.y2);
+      cogl_path_rel_line_to (box->priv->arrow_height, box->priv->arrow_width / 2);
+      cogl_path_rel_line_to (-box->priv->arrow_height, box->priv->arrow_width / 2);
+    }
+
+  cogl_path_arc (child_box.x1 + radius, child_box.y2 - radius,
+                 radius,
+                 radius,
+                 90,
+                 180);
+
+  if (box->priv->arrow_side == GTK_POS_LEFT)
+    {
+      cogl_path_line_to (child_box.x1, child_box.y1 + box->priv->arrow_offset);
+      cogl_path_rel_line_to (-box->priv->arrow_height, -box->priv->arrow_width / 2);
+      cogl_path_rel_line_to (box->priv->arrow_height,  -box->priv->arrow_width / 2);
+    }
+
+  cogl_path_close ();
+
+  cogl_path_stroke ();
+
+  cogl_pop_matrix ();
+
+  child = children->data;
+
+  clutter_actor_paint (child);
+
+  g_list_free (children);
+}
+
+static void
+shell_arrow_box_class_init (ShellArrowBoxClass *klass)
+{
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
+
+  gobject_class->get_property = shell_arrow_box_get_property;
+  gobject_class->set_property = shell_arrow_box_set_property;
+
+  actor_class->get_preferred_width = shell_arrow_box_get_preferred_width;
+  actor_class->get_preferred_height = shell_arrow_box_get_preferred_height;
+  actor_class->allocate = shell_arrow_box_allocate;
+  actor_class->paint = shell_arrow_box_paint;
+
+  /**
+   * ShellArrowBox:border-width
+   *
+   * The width in pixels of the border
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_BORDER,
+                                   g_param_spec_float ("border",
+                                                       "Border width",
+                                                       "Width of border",
+                                                       0,
+                                                       G_MAXFLOAT,
+                                                       1,
+                                                       G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:border-color
+   *
+   * Border color
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_BORDER_COLOR,
+                                   g_param_spec_boxed ("border-color",
+                                                       "Border color",
+                                                       "Border color",
+                                                       CLUTTER_TYPE_COLOR,
+                                                       G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:arrow-side
+   *
+   * The side of the box that extrudes the pointer arrow.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ARROW_SIDE,
+                                   g_param_spec_enum ("arrow-side",
+                                                      "Arrow side",
+                                                      "Side which contains the arrow",
+                                                      gtk_position_type_get_type (),
+                                                      GTK_POS_LEFT,
+                                                      G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:arrow-width
+   *
+   * The length in pixels of the arrow's origin in the box.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ARROW_WIDTH,
+                                   g_param_spec_float ("arrow-width",
+                                                       "Arrow width",
+                                                       "Length of arrow origin",
+                                                       0,
+                                                       G_MAXFLOAT,
+                                                       4,
+                                                       G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:arrow-height
+   *
+   * Distance in pixels from box to arrow's tip.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ARROW_HEIGHT,
+                                   g_param_spec_float ("arrow-height",
+                                                       "Arrow height",
+                                                       "Distance from box to arrow tip",
+                                                       0,
+                                                       G_MAXFLOAT,
+                                                       4,
+                                                       G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:arrow-offset
+   *
+   * Offset from top left corner of the box where the arrow should begin.  For
+   * TOP or BOTTOM, this is along the X axis, for LEFT or RIGHT, the offset
+   * is for the Y axis.
+   *
+   * The special value of -1 means "center".
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ARROW_OFFSET,
+                                   g_param_spec_float ("arrow-offset",
+                                                       "Arrow offset",
+                                                       "Distance from origin to arrow start",
+                                                       -1,
+                                                       G_MAXFLOAT,
+                                                       -1,
+                                                       G_PARAM_READWRITE));
+
+  /**
+   * ShellArrowBox:corner-radius
+   *
+   * Radius of rounded corner.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_CORNER_RADIUS,
+                                   g_param_spec_float ("corner-radius",
+                                                       "Corner radius",
+                                                       "Radius of rounded corner",
+                                                       0,
+                                                       G_MAXFLOAT,
+                                                       0,
+                                                       G_PARAM_READWRITE));
+
+  g_type_class_add_private (gobject_class, sizeof (ShellArrowBoxPrivate));
+}
+
+static void
+shell_arrow_box_init (ShellArrowBox *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, SHELL_TYPE_ARROW_BOX,
+                                            ShellArrowBoxPrivate);
+  self->priv->arrow_offset = -1;
+  self->priv->arrow_height = 4;
+  self->priv->arrow_width = 4;
+  self->priv->arrow_side = GTK_POS_LEFT;
+  self->priv->border = 1;
+}
diff --git a/src/shell-arrow-box.h b/src/shell-arrow-box.h
new file mode 100644
index 0000000..7e70e83
--- /dev/null
+++ b/src/shell-arrow-box.h
@@ -0,0 +1,33 @@
+#ifndef __SHELL_ARROW_BOX_H__
+#define __SHELL_ARROW_BOX_H__
+
+#include <clutter/clutter.h>
+#include <gtk/gtk.h>
+
+#define SHELL_TYPE_ARROW_BOX                 (shell_arrow_box_get_type ())
+#define SHELL_ARROW_BOX(obj)                 (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_ARROW_BOX, ShellArrowBox))
+#define SHELL_ARROW_BOX_CLASS(klass)         (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_ARROW_BOX, ShellArrowBoxClass))
+#define SHELL_IS_ARROW_BOX(obj)              (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_ARROW_BOX))
+#define SHELL_IS_ARROW_BOX_CLASS(klass)      (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_ARROW_BOX))
+#define SHELL_ARROW_BOX_GET_CLASS(obj)       (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_ARROW_BOX, ShellArrowBoxClass))
+
+typedef struct _ShellArrowBox        ShellArrowBox;
+typedef struct _ShellArrowBoxClass   ShellArrowBoxClass;
+
+typedef struct _ShellArrowBoxPrivate ShellArrowBoxPrivate;
+
+struct _ShellArrowBox
+{
+    ClutterGroup parent;
+
+    ShellArrowBoxPrivate *priv;
+};
+
+struct _ShellArrowBoxClass
+{
+    ClutterGroupClass parent_class;
+};
+
+GType shell_arrow_box_get_type (void) G_GNUC_CONST;
+
+#endif /* __SHELL_ARROW_BOX_H__ */
diff --git a/src/shell-button-box.c b/src/shell-button-box.c
index a7f54c3..7c50011 100644
--- a/src/shell-button-box.c
+++ b/src/shell-button-box.c
@@ -24,6 +24,7 @@ struct _ShellButtonBoxPrivate {
 enum
 {
   ACTIVATE,
+  HOVER_EVENT,
   LAST_SIGNAL
 };
 
@@ -89,9 +90,16 @@ shell_button_box_enter_event (ClutterActor         *actor,
   if (!shell_button_box_contains (box, event->source))
     return TRUE;
 
-  set_hover (box, TRUE);
+  /* Special case these two since a hover handler may depend
+   * on the pressed state.
+   */
+  if (box->priv->held)
+    box->priv->pressed = TRUE;
+  box->priv->hover = TRUE;
+
   if (box->priv->held)
-    set_pressed (box, TRUE);
+    g_object_notify (G_OBJECT (actor), "pressed");
+  g_object_notify (G_OBJECT (actor), "hover");
 
   return TRUE;
 }
@@ -106,6 +114,7 @@ shell_button_box_leave_event (ClutterActor         *actor,
     return TRUE;
 
   set_hover (box, FALSE);
+  g_signal_emit (G_OBJECT (actor), shell_button_box_signals[HOVER_EVENT], 0, event);
   set_pressed (box, FALSE);
 
   return TRUE;
@@ -159,6 +168,31 @@ shell_button_box_button_release_event (ClutterActor       *actor,
   return TRUE;
 }
 
+/**
+ * shell_button_box_fake_release:
+ * @box:
+ *
+ * If this button box is holding a pointer grab, this function will
+ * will ungrab it, and reset the pressed state.  The effect is
+ * similar to if the user had released the mouse button, but without
+ * emitting the activate signal.
+ *
+ * This function is useful if for example you want to do something after the user
+ * is holding the mouse button for a given period of time, breaking the
+ * grab.
+ */
+void
+shell_button_box_fake_release (ShellButtonBox *box)
+{
+  if (!box->priv->held)
+    return;
+
+  box->priv->held = FALSE;
+  clutter_ungrab_pointer ();
+
+  set_pressed (box, FALSE);
+}
+
 static void
 shell_button_box_set_property(GObject         *object,
                               guint            prop_id,
@@ -234,6 +268,24 @@ shell_button_box_class_init (ShellButtonBoxClass *klass)
                   G_TYPE_NONE, 0);
 
   /**
+   * ShellButtonBox::hover-event
+   * @box: The #ShellButtonBox
+   * @event: Associated event which caused hover to change
+   *
+   * This signal is emitted when the hover property changes, but also
+   * gives the associated event, which can be used for X/Y coordinates
+   * and timestamp information.
+   */
+  shell_button_box_signals[HOVER_EVENT] =
+    g_signal_new ("hover-event",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__BOXED,
+                  G_TYPE_NONE, 1, CLUTTER_TYPE_EVENT);
+
+  /**
    * ShellButtonBox:active
    *
    * The property allows the button to be used as a "toggle button"; it's up to the
diff --git a/src/shell-button-box.h b/src/shell-button-box.h
index 20d136e..4bd756a 100644
--- a/src/shell-button-box.h
+++ b/src/shell-button-box.h
@@ -30,4 +30,6 @@ struct _ShellButtonBoxClass
 
 GType shell_button_box_get_type (void) G_GNUC_CONST;
 
+void shell_button_box_fake_release (ShellButtonBox *box);
+
 #endif /* __SHELL_BUTTON_BOX_H__ */
diff --git a/src/shell-drawing.c b/src/shell-drawing.c
index ff3ab3d..a880b82 100644
--- a/src/shell-drawing.c
+++ b/src/shell-drawing.c
@@ -217,6 +217,64 @@ shell_draw_app_highlight (ClutterCairoTexture *texture,
           cairo_translate (cr, 1.8, 0.0);
         }
     }
+  cairo_stroke (cr);
+
+  cairo_restore (cr);
+  cairo_destroy (cr);
+}
+
+/**
+ * shell_draw_perimeter:
+ * @texture:
+ * @color: Color for the line
+ * @width: Width in pixels of the line
+ * @corner_radius: Radius in pixels of the corner
+ * @n_points: Number of coordinate elements
+ * @points: (array length=n_points): Array containing coordinate pairs, must have an even number of elements
+ *
+ * Draw a line of width @width with the given color @color.  The line will be between
+ * coordinate points specified by @points, which is an even-length array where pairs
+ * form X,Y coordinates.  @n_points is the total number of elements, not the total
+ * number of coordinates.  The given line will have rounded corners of radius @corner_radius.
+ */
+void
+shell_draw_perimeter (ClutterCairoTexture  *texture,
+                      ClutterColor         *color,
+                      float                 width,
+                      float                 corner_radius,
+                      guint                 n_points,
+                      float                *points)
+{
+  guint i;
+  cairo_t *cr;
+  guint surface_width, surface_height;
+
+  g_return_if_fail ((n_points % 2) == 0);
+
+  if (n_points == 0)
+    return;
+
+  clutter_cairo_texture_get_surface_size (texture, &surface_width, &surface_height);
+
+  clutter_cairo_texture_clear (texture);
+  cr = clutter_cairo_texture_create (texture);
+
+  cairo_save (cr);
+
+  clutter_cairo_set_source_color (cr, color);
+
+  cairo_set_line_width (cr, width);
+  cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
+
+  cairo_new_path (cr);
+
+  for (i = 0; i < n_points; i += 2)
+    {
+      cairo_line_to (cr, points[i], points[i+1]);
+    }
+  cairo_close_path (cr);
+
+  cairo_stroke (cr);
 
   cairo_restore (cr);
   cairo_destroy (cr);
diff --git a/src/shell-drawing.h b/src/shell-drawing.h
index d462d41..0b5c376 100644
--- a/src/shell-drawing.h
+++ b/src/shell-drawing.h
@@ -24,6 +24,13 @@ void shell_draw_app_highlight (ClutterCairoTexture *texture,
                                double               green,
                                double               alpha);
 
+void shell_draw_perimeter (ClutterCairoTexture  *texture,
+                           ClutterColor         *color,
+                           float                 width,
+                           float                 corner_radius,
+                           guint                 n_points,
+                           float                *points);
+
 guint shell_add_hook_paint_red_border (ClutterActor *actor);
 
 G_END_DECLS
diff --git a/src/shell-texture-cache.c b/src/shell-texture-cache.c
index 0ed3446..5a46ad4 100644
--- a/src/shell-texture-cache.c
+++ b/src/shell-texture-cache.c
@@ -1236,6 +1236,28 @@ shell_texture_cache_evict_recent_thumbnail (ShellTextureCache *cache,
   shell_texture_cache_evict_thumbnail (cache, gtk_recent_info_get_uri (info));
 }
 
+static size_t
+pixbuf_byte_size (GdkPixbuf *pixbuf)
+{
+  return (gdk_pixbuf_get_height (pixbuf) - 1) * gdk_pixbuf_get_rowstride (pixbuf) +
+    + gdk_pixbuf_get_width (pixbuf) * ((gdk_pixbuf_get_n_channels (pixbuf) * gdk_pixbuf_get_bits_per_sample (pixbuf) + 7) / 8);
+}
+
+/**
+ * shell_texture_cache_pixbuf_equal:
+ *
+ * Returns: %TRUE iff the given pixbufs are bytewise-equal
+ */
+gboolean
+shell_texture_cache_pixbuf_equal (ShellTextureCache *cache, GdkPixbuf *a, GdkPixbuf *b)
+{
+  size_t size_a = pixbuf_byte_size (a);
+  size_t size_b = pixbuf_byte_size (b);
+  if (size_a != size_b)
+    return FALSE;
+  return memcmp (a, b, size_a);
+}
+
 static ShellTextureCache *instance = NULL;
 
 /**
diff --git a/src/shell-texture-cache.h b/src/shell-texture-cache.h
index 940e101..057eae3 100644
--- a/src/shell-texture-cache.h
+++ b/src/shell-texture-cache.h
@@ -79,4 +79,6 @@ ClutterActor *shell_texture_cache_load_uri_sync (ShellTextureCache *cache,
                                                  int                available_height,
                                                  GError           **error);
 
+gboolean shell_texture_cache_pixbuf_equal (ShellTextureCache *cache, GdkPixbuf *a, GdkPixbuf *b);
+
 #endif /* __SHELL_TEXTURE_CACHE_H__ */
