[gnome-shell] switcherPopup: Factor out altTab and ctrlAltTab's common code



commit 00338bbc4bb34041568c215c32017b872a0a290e
Author: Rui Matos <tiagomatos gmail com>
Date:   Fri Nov 30 16:16:48 2012 +0100

    switcherPopup: Factor out altTab and ctrlAltTab's common code
    
    https://bugzilla.gnome.org/show_bug.cgi?id=689528

 data/theme/gnome-shell.css |    2 +-
 js/Makefile.am             |    1 +
 js/ui/altTab.js            |  697 ++++++--------------------------------------
 js/ui/ctrlAltTab.js        |  183 ++----------
 js/ui/switcherPopup.js     |  640 ++++++++++++++++++++++++++++++++++++++++
 js/ui/windowManager.js     |    2 +-
 6 files changed, 753 insertions(+), 772 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index c2b69b1..a4b9842 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -1542,7 +1542,7 @@ StScrollBar StButton#vhandle:active {
 }
 
 /* App Switcher */
-#altTabPopup {
+.switcher-popup {
     padding: 8px;
     spacing: 16px;
 }
diff --git a/js/Makefile.am b/js/Makefile.am
index a3e4917..6d13262 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -85,6 +85,7 @@ nobase_dist_js_DATA = 	\
 	ui/status/power.js	\
 	ui/status/volume.js	\
 	ui/status/bluetooth.js	\
+	ui/switcherPopup.js	\
 	ui/tweener.js		\
 	ui/unlockDialog.js	\
 	ui/userMenu.js		\
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index 5de7d7b..74a03a0 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -1,117 +1,56 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
 const Clutter = imports.gi.Clutter;
-const Gdk = imports.gi.Gdk;
-const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
 const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
-const Signals = imports.signals;
 const St = imports.gi.St;
 const Atk = imports.gi.Atk;
 
 const Main = imports.ui.main;
+const SwitcherPopup = imports.ui.switcherPopup;
 const Tweener = imports.ui.tweener;
 
-const POPUP_APPICON_SIZE = 96;
-const POPUP_SCROLL_TIME = 0.10; // seconds
-const POPUP_DELAY_TIMEOUT = 150; // milliseconds
-const POPUP_FADE_OUT_TIME = 0.1; // seconds
-
 const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds
 
-const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
-
 const THUMBNAIL_DEFAULT_SIZE = 256;
 const THUMBNAIL_POPUP_TIME = 500; // milliseconds
 const THUMBNAIL_FADE_TIME = 0.1; // seconds
 
 const iconSizes = [96, 64, 48, 32, 22];
 
-function mod(a, b) {
-    return (a + b) % b;
-}
-
-function primaryModifier(mask) {
-    if (mask == 0)
-        return 0;
-
-    let primary = 1;
-    while (mask > 1) {
-        mask >>= 1;
-        primary <<= 1;
-    }
-    return primary;
-}
-
 const AltTabPopup = new Lang.Class({
     Name: 'AltTabPopup',
+    Extends: SwitcherPopup.SwitcherPopup,
 
     _init : function() {
-        this.actor = new Shell.GenericContainer({ name: 'altTabPopup',
-                                                  reactive: true,
-                                                  visible: false });
+        this.parent();
 
-        this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
-        this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
-        this.actor.connect('allocate', Lang.bind(this, this._allocate));
-
-        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
-
-        this._haveModal = false;
-        this._modifierMask = 0;
-
-        this._currentApp = 0;
-        this._currentWindow = -1;
+        this._thumbnails = null;
         this._thumbnailTimeoutId = 0;
-        this._motionTimeoutId = 0;
-        this._initialDelayTimeoutId = 0;
+        this._currentWindow = -1;
 
         this.thumbnailsVisible = false;
-
-        // Initially disable hover so we ignore the enter-event if
-        // the switcher appears underneath the current pointer location
-        this._disableHover();
-
-        Main.uiGroup.add_actor(this.actor);
-    },
-
-    _getPreferredWidth: function (actor, forHeight, alloc) {
-        alloc.min_size = global.screen_width;
-        alloc.natural_size = global.screen_width;
-    },
-
-    _getPreferredHeight: function (actor, forWidth, alloc) {
-        alloc.min_size = global.screen_height;
-        alloc.natural_size = global.screen_height;
     },
 
     _allocate: function (actor, box, flags) {
-        let childBox = new Clutter.ActorBox();
-        let primary = Main.layoutManager.primaryMonitor;
-
-        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
-        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
-        let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
-        let vPadding = this.actor.get_theme_node().get_vertical_padding();
-        let hPadding = leftPadding + rightPadding;
-
-        // Allocate the appSwitcher
-        // We select a size based on an icon size that does not overflow the screen
-        let [childMinHeight, childNaturalHeight] = this._appSwitcher.actor.get_preferred_height(primary.width - hPadding);
-        let [childMinWidth, childNaturalWidth] = this._appSwitcher.actor.get_preferred_width(childNaturalHeight);
-        childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2));
-        childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth);
-        childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2);
-        childBox.y2 = childBox.y1 + childNaturalHeight;
-        this._appSwitcher.actor.allocate(childBox, flags);
+        this.parent(actor, box, flags);
 
         // Allocate the thumbnails
         // We try to avoid overflowing the screen so we base the resulting size on
         // those calculations
         if (this._thumbnails) {
-            let icon = this._appIcons[this._currentApp].actor;
+            let childBox = this._switcherList.actor.get_allocation_box();
+            let primary = Main.layoutManager.primaryMonitor;
+
+            let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
+            let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
+            let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
+            let vPadding = this.actor.get_theme_node().get_vertical_padding();
+            let hPadding = leftPadding + rightPadding;
+
+            let icon = this._items[this._selectedIndex].actor;
             let [posX, posY] = icon.get_transformed_position();
             let thumbnailCenter = posX + icon.width / 2;
             let [childMinWidth, childNaturalWidth] = this._thumbnails.actor.get_preferred_width(-1);
@@ -126,7 +65,7 @@ const AltTabPopup = new Lang.Class({
             childBox.x2 = childBox.x1 +  childNaturalWidth;
             if (childBox.x2 > primary.x + primary.width - rightPadding)
                 childBox.x2 = primary.x + primary.width - rightPadding;
-            childBox.y1 = this._appSwitcher.actor.allocation.y2 + spacing;
+            childBox.y1 = this._switcherList.actor.allocation.y2 + spacing;
             this._thumbnails.addClones(primary.y + primary.height - bottomPadding - childBox.y1);
             let [childMinHeight, childNaturalHeight] = this._thumbnails.actor.get_preferred_height(-1);
             childBox.y2 = childBox.y1 + childNaturalHeight;
@@ -164,280 +103,158 @@ const AltTabPopup = new Lang.Class({
         return [apps, allApps];
     },
 
-    show : function(backward, binding, mask) {
+    _createSwitcher: function() {
         let [localApps, otherApps] = this._getAppLists();
 
         if (localApps.length == 0 && otherApps.length == 0)
             return false;
 
-        if (!Main.pushModal(this.actor)) {
-            // Probably someone else has a pointer grab, try again with keyboard only
-            if (!Main.pushModal(this.actor, { options: Meta.ModalOptions.POINTER_ALREADY_GRABBED })) {
-                return false;
-            }
-        }
-        this._haveModal = true;
-        this._modifierMask = primaryModifier(mask);
+        this._switcherList = new AppSwitcher(localApps, otherApps, this);
+        this._items = this._switcherList.icons;
 
-        this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
-        this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
-
-        this.actor.connect('button-press-event', Lang.bind(this, this._clickedOutside));
-        this.actor.connect('scroll-event', Lang.bind(this, this._onScroll));
-
-        this._appSwitcher = new AppSwitcher(localApps, otherApps, this);
-        this.actor.add_actor(this._appSwitcher.actor);
-        this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated));
-        this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered));
-
-        this._appIcons = this._appSwitcher.icons;
-
-        // Need to force an allocation so we can figure out whether we
-        // need to scroll when selecting
-        this.actor.opacity = 0;
-        this.actor.show();
-        this.actor.get_allocation_box();
+        return true;
+    },
 
-        // Make the initial selection
+    _initialSelection: function(backward, binding) {
         if (binding == 'switch-group') {
             if (backward) {
-                this._select(0, this._appIcons[0].cachedWindows.length - 1);
+                this._select(0, this._items[0].cachedWindows.length - 1);
             } else {
-                if (this._appIcons[0].cachedWindows.length > 1)
+                if (this._items[0].cachedWindows.length > 1)
                     this._select(0, 1);
                 else
                     this._select(0, 0);
             }
         } else if (binding == 'switch-group-backward') {
-            this._select(0, this._appIcons[0].cachedWindows.length - 1);
+            this._select(0, this._items[0].cachedWindows.length - 1);
         } else if (binding == 'switch-windows-backward') {
-            this._select(this._appIcons.length - 1);
-        } else if (this._appIcons.length == 1) {
+            this._select(this._items.length - 1);
+        } else if (this._items.length == 1) {
             this._select(0);
         } else if (backward) {
-            this._select(this._appIcons.length - 1);
+            this._select(this._items.length - 1);
         } else {
             this._select(1);
         }
-
-        // There's a race condition; if the user released Alt before
-        // we got the grab, then we won't be notified. (See
-        // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for
-        // details.) So we check now. (Have to do this after updating
-        // selection.)
-        let [x, y, mods] = global.get_pointer();
-        if (!(mods & this._modifierMask)) {
-            this._finish();
-            return false;
-        }
-
-        // We delay showing the popup so that fast Alt+Tab users aren't
-        // disturbed by the popup briefly flashing.
-        this._initialDelayTimeoutId = Mainloop.timeout_add(POPUP_DELAY_TIMEOUT,
-                                                           Lang.bind(this, function () {
-                                                               this.actor.opacity = 255;
-                                                               this._initialDelayTimeoutId = 0;
-                                                           }));
-
-        return true;
-    },
-
-    _nextApp : function() {
-        return mod(this._currentApp + 1, this._appIcons.length);
-    },
-    _previousApp : function() {
-        return mod(this._currentApp - 1, this._appIcons.length);
     },
 
     _nextWindow : function() {
         // We actually want the second window if we're in the unset state
         if (this._currentWindow == -1)
             this._currentWindow = 0;
-        return mod(this._currentWindow + 1,
-                   this._appIcons[this._currentApp].cachedWindows.length);
+        return SwitcherPopup.mod(this._currentWindow + 1,
+                                 this._items[this._selectedIndex].cachedWindows.length);
     },
     _previousWindow : function() {
         // Also assume second window here
         if (this._currentWindow == -1)
             this._currentWindow = 1;
-        return mod(this._currentWindow - 1,
-                   this._appIcons[this._currentApp].cachedWindows.length);
+        return SwitcherPopup.mod(this._currentWindow - 1,
+                                 this._items[this._selectedIndex].cachedWindows.length);
     },
 
-    _keyPressEvent : function(actor, event) {
-        let keysym = event.get_key_symbol();
-        let event_state = event.get_state();
-        let backwards = event_state & Clutter.ModifierType.SHIFT_MASK;
-        let action = global.display.get_keybinding_action(event.get_key_code(), event_state);
-
-        this._disableHover();
-
-        if (keysym == Clutter.Escape) {
-            this.destroy();
-        } else if (action == Meta.KeyBindingAction.SWITCH_GROUP) {
-            this._select(this._currentApp, backwards ? this._previousWindow() : this._nextWindow());
+    _keyPressHandler: function(keysym, backwards, action) {
+        if (action == Meta.KeyBindingAction.SWITCH_GROUP) {
+            this._select(this._selectedIndex, backwards ? this._previousWindow() : this._nextWindow());
         } else if (action == Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD) {
-            this._select(this._currentApp, this._previousWindow());
+            this._select(this._selectedIndex, this._previousWindow());
         } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS) {
-            this._select(backwards ? this._previousApp() : this._nextApp());
+            this._select(backwards ? this._previous() : this._next());
         } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD) {
-            this._select(this._previousApp());
+            this._select(this._previous());
         } else if (this._thumbnailsFocused) {
             if (keysym == Clutter.Left)
-                this._select(this._currentApp, this._previousWindow());
+                this._select(this._selectedIndex, this._previousWindow());
             else if (keysym == Clutter.Right)
-                this._select(this._currentApp, this._nextWindow());
+                this._select(this._selectedIndex, this._nextWindow());
             else if (keysym == Clutter.Up)
-                this._select(this._currentApp, null, true);
+                this._select(this._selectedIndex, null, true);
         } else {
             if (keysym == Clutter.Left)
-                this._select(this._previousApp());
+                this._select(this._previous());
             else if (keysym == Clutter.Right)
-                this._select(this._nextApp());
+                this._select(this._next());
             else if (keysym == Clutter.Down)
-                this._select(this._currentApp, 0);
+                this._select(this._selectedIndex, 0);
         }
-
-        return true;
-    },
-
-    _keyReleaseEvent : function(actor, event) {
-        let [x, y, mods] = global.get_pointer();
-        let state = mods & this._modifierMask;
-
-        if (state == 0)
-            this._finish();
-
-        return true;
     },
 
-    _onScroll : function(actor, event) {
-        let direction = event.get_scroll_direction();
+    _scrollHandler: function(direction) {
         if (direction == Clutter.ScrollDirection.UP) {
             if (this._thumbnailsFocused) {
                 if (this._currentWindow == 0 || this._currentWindow == -1)
-                    this._select(this._previousApp());
+                    this._select(this._previous());
                 else
-                    this._select(this._currentApp, this._previousWindow());
+                    this._select(this._selectedIndex, this._previousWindow());
             } else {
-                let nwindows = this._appIcons[this._currentApp].cachedWindows.length;
+                let nwindows = this._items[this._selectedIndex].cachedWindows.length;
                 if (nwindows > 1)
-                    this._select(this._currentApp, nwindows - 1);
+                    this._select(this._selectedIndex, nwindows - 1);
                 else
-                    this._select(this._previousApp());
+                    this._select(this._previous());
             }
         } else if (direction == Clutter.ScrollDirection.DOWN) {
             if (this._thumbnailsFocused) {
-                if (this._currentWindow == this._appIcons[this._currentApp].cachedWindows.length - 1)
-                    this._select(this._nextApp());
+                if (this._currentWindow == this._items[this._selectedIndex].cachedWindows.length - 1)
+                    this._select(this._next());
                 else
-                    this._select(this._currentApp, this._nextWindow());
+                    this._select(this._selectedIndex, this._nextWindow());
             } else {
-                let nwindows = this._appIcons[this._currentApp].cachedWindows.length;
+                let nwindows = this._items[this._selectedIndex].cachedWindows.length;
                 if (nwindows > 1)
-                    this._select(this._currentApp, 0);
+                    this._select(this._selectedIndex, 0);
                 else
-                    this._select(this._nextApp());
+                    this._select(this._next());
             }
         }
     },
 
-    _clickedOutside : function(actor, event) {
-        this.destroy();
-    },
-
-    _appActivated : function(appSwitcher, n) {
+    _itemActivatedHandler: function(n) {
         // If the user clicks on the selected app, activate the
         // selected window; otherwise (eg, they click on an app while
         // !mouseActive) activate the clicked-on app.
-        let window;
-        if (n == this._currentApp && this._currentWindow >= 0)
-            window = this._appIcons[n].cachedWindows[this._currentWindow];
+        if (n == this._selectedIndex && this._currentWindow >= 0)
+            this._select(n, this._currentWindow);
         else
-            window = null;
-        this._appIcons[n].app.activate_window(window, global.get_current_time());
-        this.destroy();
+            this._select(n);
     },
 
-    _appEntered : function(appSwitcher, n) {
-        if (!this._mouseActive)
-            return;
-
+    _itemEnteredHandler: function(n) {
         this._select(n);
     },
 
     _windowActivated : function(thumbnailList, n) {
-        let appIcon = this._appIcons[this._currentApp];
+        let appIcon = this._items[this._selectedIndex];
         Main.activateWindow(appIcon.cachedWindows[n]);
         this.destroy();
     },
 
     _windowEntered : function(thumbnailList, n) {
-        if (!this._mouseActive)
+        if (!this.mouseActive)
             return;
 
-        this._select(this._currentApp, n);
-    },
-
-    _disableHover : function() {
-        this._mouseActive = false;
-
-        if (this._motionTimeoutId != 0)
-            Mainloop.source_remove(this._motionTimeoutId);
-
-        this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut));
-    },
-
-    _mouseTimedOut : function() {
-        this._motionTimeoutId = 0;
-        this._mouseActive = true;
+        this._select(this._selectedIndex, n);
     },
 
     _finish : function() {
-        let app = this._appIcons[this._currentApp];
-        if (this._currentWindow >= 0) {
-            Main.activateWindow(app.cachedWindows[this._currentWindow]);
-        } else {
-            app.app.activate_window(null, global.get_current_time());
-        }
-        this.destroy();
-    },
-
-    _popModal: function() {
-        if (this._haveModal) {
-            Main.popModal(this.actor);
-            this._haveModal = false;
-        }
-    },
+        this.parent();
 
-    destroy : function() {
-        this._popModal();
-        if (this.actor.visible) {
-            Tweener.addTween(this.actor,
-                             { opacity: 0,
-                               time: POPUP_FADE_OUT_TIME,
-                               transition: 'easeOutQuad',
-                               onComplete: Lang.bind(this,
-                                   function() {
-                                       this.actor.destroy();
-                                   })
-                             });
-        } else
-            this.actor.destroy();
+        let appIcon = this._items[this._selectedIndex];
+        let window;
+        if (this._currentWindow >= 0)
+            window = appIcon.cachedWindows[this._currentWindow];
+        else
+            window = null;
+        appIcon.app.activate_window(window, global.get_current_time());
     },
 
     _onDestroy : function() {
-        this._popModal();
+        this.parent();
 
         if (this._thumbnails)
             this._destroyThumbnails();
-
-        if (this._motionTimeoutId != 0)
-            Mainloop.source_remove(this._motionTimeoutId);
         if (this._thumbnailTimeoutId != 0)
             Mainloop.source_remove(this._thumbnailTimeoutId);
-        if (this._initialDelayTimeoutId != 0)
-            Mainloop.source_remove(this._initialDelayTimeoutId);
     },
 
     /**
@@ -466,7 +283,7 @@ const AltTabPopup = new Lang.Class({
      * app list will have the keyboard focus.
      */
     _select : function(app, window, forceAppFocus) {
-        if (app != this._currentApp || window == null) {
+        if (app != this._selectedIndex || window == null) {
             if (this._thumbnails)
                 this._destroyThumbnails();
         }
@@ -478,16 +295,16 @@ const AltTabPopup = new Lang.Class({
 
         this._thumbnailsFocused = (window != null) && !forceAppFocus;
 
-        this._currentApp = app;
+        this._selectedIndex = app;
         this._currentWindow = window ? window : -1;
-        this._appSwitcher.highlight(app, this._thumbnailsFocused);
+        this._switcherList.highlight(app, this._thumbnailsFocused);
 
         if (window != null) {
             if (!this._thumbnails)
                 this._createThumbnails();
             this._currentWindow = window;
             this._thumbnails.highlight(window, forceAppFocus);
-        } else if (this._appIcons[this._currentApp].cachedWindows.length > 1 &&
+        } else if (this._items[this._selectedIndex].cachedWindows.length > 1 &&
                    !forceAppFocus) {
             this._thumbnailTimeoutId = Mainloop.timeout_add (
                 THUMBNAIL_POPUP_TIME,
@@ -515,11 +332,11 @@ const AltTabPopup = new Lang.Class({
                                                         })
                          });
         this._thumbnails = null;
-        this._appSwitcher._items[this._currentApp].remove_accessible_state (Atk.StateType.EXPANDED);
+        this._switcherList._items[this._selectedIndex].remove_accessible_state (Atk.StateType.EXPANDED);
     },
 
     _createThumbnails : function() {
-        this._thumbnails = new ThumbnailList (this._appIcons[this._currentApp].cachedWindows);
+        this._thumbnails = new ThumbnailList (this._items[this._selectedIndex].cachedWindows);
         this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
         this._thumbnails.connect('item-entered', Lang.bind(this, this._windowEntered));
 
@@ -537,314 +354,10 @@ const AltTabPopup = new Lang.Class({
                            onComplete: Lang.bind(this, function () { this.thumbnailsVisible = true; })
                          });
 
-        this._appSwitcher._items[this._currentApp].add_accessible_state (Atk.StateType.EXPANDED);
-    }
-});
-
-const SwitcherList = new Lang.Class({
-    Name: 'SwitcherList',
-
-    _init : function(squareItems) {
-        this.actor = new Shell.GenericContainer({ style_class: 'switcher-list' });
-        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._allocateTop));
-
-        // Here we use a GenericContainer so that we can force all the
-        // children except the separator to have the same width.
-        this._list = new Shell.GenericContainer({ style_class: 'switcher-list-item-container' });
-        this._list.spacing = 0;
-        this._list.connect('style-changed', Lang.bind(this, function() {
-                                                        this._list.spacing = this._list.get_theme_node().get_length('spacing');
-                                                     }));
-
-        this._list.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
-        this._list.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
-        this._list.connect('allocate', Lang.bind(this, this._allocate));
-
-        this._scrollView = new St.ScrollView({ style_class: 'hfade',
-                                               enable_mouse_scrolling: false });
-        this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER);
-
-        let scrollBox = new St.BoxLayout();
-        scrollBox.add_actor(this._list);
-        this._scrollView.add_actor(scrollBox);
-        this.actor.add_actor(this._scrollView);
-
-        // Those arrows indicate whether scrolling in one direction is possible
-        this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
-                                               pseudo_class: 'highlighted' });
-        this._leftArrow.connect('repaint', Lang.bind(this,
-            function() { _drawArrow(this._leftArrow, St.Side.LEFT); }));
-        this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
-                                                pseudo_class: 'highlighted' });
-        this._rightArrow.connect('repaint', Lang.bind(this,
-            function() { _drawArrow(this._rightArrow, St.Side.RIGHT); }));
-
-        this.actor.add_actor(this._leftArrow);
-        this.actor.add_actor(this._rightArrow);
-
-        this._items = [];
-        this._highlighted = -1;
-        this._separator = null;
-        this._squareItems = squareItems;
-        this._minSize = 0;
-        this._scrollableRight = true;
-        this._scrollableLeft = false;
-    },
-
-    _allocateTop: function(actor, box, flags) {
-        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
-        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
-
-        let childBox = new Clutter.ActorBox();
-        let scrollable = this._minSize > box.x2 - box.x1;
-
-        box.y1 -= this.actor.get_theme_node().get_padding(St.Side.TOP);
-        box.y2 += this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
-        this._scrollView.allocate(box, flags);
-
-        let arrowWidth = Math.floor(leftPadding / 3);
-        let arrowHeight = arrowWidth * 2;
-        childBox.x1 = leftPadding / 2;
-        childBox.y1 = this.actor.height / 2 - arrowWidth;
-        childBox.x2 = childBox.x1 + arrowWidth;
-        childBox.y2 = childBox.y1 + arrowHeight;
-        this._leftArrow.allocate(childBox, flags);
-        this._leftArrow.opacity = (this._scrollableLeft && scrollable) ? 255 : 0;
-
-        arrowWidth = Math.floor(rightPadding / 3);
-        arrowHeight = arrowWidth * 2;
-        childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2;
-        childBox.y1 = this.actor.height / 2 - arrowWidth;
-        childBox.x2 = childBox.x1 + arrowWidth;
-        childBox.y2 = childBox.y1 + arrowHeight;
-        this._rightArrow.allocate(childBox, flags);
-        this._rightArrow.opacity = (this._scrollableRight && scrollable) ? 255 : 0;
-    },
-
-    addItem : function(item, label) {
-        let bbox = new St.Button({ style_class: 'item-box',
-                                   reactive: true });
-
-        bbox.set_child(item);
-        this._list.add_actor(bbox);
-
-        let n = this._items.length;
-        bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); }));
-        bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); }));
-
-        bbox.label_actor = label;
-
-        this._items.push(bbox);
-
-        return bbox;
-    },
-
-    _onItemClicked: function (index) {
-        this._itemActivated(index);
-    },
-
-    _onItemEnter: function (index) {
-        this._itemEntered(index);
-    },
-
-    addSeparator: function () {
-        let box = new St.Bin({ style_class: 'separator' });
-        this._separator = box;
-        this._list.add_actor(box);
-    },
-
-    highlight: function(index, justOutline) {
-        if (this._highlighted != -1) {
-            this._items[this._highlighted].remove_style_pseudo_class('outlined');
-            this._items[this._highlighted].remove_style_pseudo_class('selected');
-        }
-
-        this._highlighted = index;
-
-        if (this._highlighted != -1) {
-            if (justOutline)
-                this._items[this._highlighted].add_style_pseudo_class('outlined');
-            else
-                this._items[this._highlighted].add_style_pseudo_class('selected');
-        }
-
-        let adjustment = this._scrollView.hscroll.adjustment;
-        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
-        let [absItemX, absItemY] = this._items[index].get_transformed_position();
-        let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0);
-        let [containerWidth, containerHeight] = this.actor.get_transformed_size();
-        if (posX + this._items[index].get_width() > containerWidth)
-            this._scrollToRight();
-        else if (this._items[index].allocation.x1 - value < 0)
-            this._scrollToLeft();
-
-    },
-
-    _scrollToLeft : function() {
-        let adjustment = this._scrollView.hscroll.adjustment;
-        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
-
-        let item = this._items[this._highlighted];
-
-        if (item.allocation.x1 < value)
-            value = Math.min(0, item.allocation.x1);
-        else if (item.allocation.x2 > value + pageSize)
-            value = Math.max(upper, item.allocation.x2 - pageSize);
-
-        this._scrollableRight = true;
-        Tweener.addTween(adjustment,
-                         { value: value,
-                           time: POPUP_SCROLL_TIME,
-                           transition: 'easeOutQuad',
-                           onComplete: Lang.bind(this, function () {
-                                if (this._highlighted == 0) {
-                                    this._scrollableLeft = false;
-                                    this.actor.queue_relayout();
-                                }
-                           })
-                          });
-    },
-
-    _scrollToRight : function() {
-        let adjustment = this._scrollView.hscroll.adjustment;
-        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
-
-        let item = this._items[this._highlighted];
-
-        if (item.allocation.x1 < value)
-            value = Math.max(0, item.allocation.x1);
-        else if (item.allocation.x2 > value + pageSize)
-            value = Math.min(upper, item.allocation.x2 - pageSize);
-
-        this._scrollableLeft = true;
-        Tweener.addTween(adjustment,
-                         { value: value,
-                           time: POPUP_SCROLL_TIME,
-                           transition: 'easeOutQuad',
-                           onComplete: Lang.bind(this, function () {
-                                if (this._highlighted == this._items.length - 1) {
-                                    this._scrollableRight = false;
-                                    this.actor.queue_relayout();
-                                }
-                            })
-                          });
-    },
-
-    _itemActivated: function(n) {
-        this.emit('item-activated', n);
-    },
-
-    _itemEntered: function(n) {
-        this.emit('item-entered', n);
-    },
-
-    _maxChildWidth: function (forHeight) {
-        let maxChildMin = 0;
-        let maxChildNat = 0;
-
-        for (let i = 0; i < this._items.length; i++) {
-            let [childMin, childNat] = this._items[i].get_preferred_width(forHeight);
-            maxChildMin = Math.max(childMin, maxChildMin);
-            maxChildNat = Math.max(childNat, maxChildNat);
-
-            if (this._squareItems) {
-                let [childMin, childNat] = this._items[i].get_preferred_height(-1);
-                maxChildMin = Math.max(childMin, maxChildMin);
-                maxChildNat = Math.max(childNat, maxChildNat);
-            }
-        }
-
-        return [maxChildMin, maxChildNat];
-    },
-
-    _getPreferredWidth: function (actor, forHeight, alloc) {
-        let [maxChildMin, maxChildNat] = this._maxChildWidth(forHeight);
-
-        let separatorWidth = 0;
-        if (this._separator) {
-            let [sepMin, sepNat] = this._separator.get_preferred_width(forHeight);
-            separatorWidth = sepNat + this._list.spacing;
-        }
-
-        let totalSpacing = this._list.spacing * (this._items.length - 1);
-        alloc.min_size = this._items.length * maxChildMin + separatorWidth + totalSpacing;
-        alloc.natural_size = alloc.min_size;
-        this._minSize = alloc.min_size;
-    },
-
-    _getPreferredHeight: function (actor, forWidth, alloc) {
-        let maxChildMin = 0;
-        let maxChildNat = 0;
-
-        for (let i = 0; i < this._items.length; i++) {
-            let [childMin, childNat] = this._items[i].get_preferred_height(-1);
-            maxChildMin = Math.max(childMin, maxChildMin);
-            maxChildNat = Math.max(childNat, maxChildNat);
-        }
-
-        if (this._squareItems) {
-            let [childMin, childNat] = this._maxChildWidth(-1);
-            maxChildMin = Math.max(childMin, maxChildMin);
-            maxChildNat = maxChildMin;
-        }
-
-        alloc.min_size = maxChildMin;
-        alloc.natural_size = maxChildNat;
-    },
-
-    _allocate: function (actor, box, flags) {
-        let childHeight = box.y2 - box.y1;
-
-        let [maxChildMin, maxChildNat] = this._maxChildWidth(childHeight);
-        let totalSpacing = this._list.spacing * (this._items.length - 1);
-
-        let separatorWidth = 0;
-        if (this._separator) {
-            let [sepMin, sepNat] = this._separator.get_preferred_width(childHeight);
-            separatorWidth = sepNat;
-            totalSpacing += this._list.spacing;
-        }
-
-        let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing - separatorWidth) / this._items.length);
-
-        let x = 0;
-        let children = this._list.get_children();
-        let childBox = new Clutter.ActorBox();
-
-        let primary = Main.layoutManager.primaryMonitor;
-        let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT);
-
-        for (let i = 0; i < children.length; i++) {
-            if (this._items.indexOf(children[i]) != -1) {
-                let [childMin, childNat] = children[i].get_preferred_height(childWidth);
-                let vSpacing = (childHeight - childNat) / 2;
-                childBox.x1 = x;
-                childBox.y1 = vSpacing;
-                childBox.x2 = x + childWidth;
-                childBox.y2 = childBox.y1 + childNat;
-                children[i].allocate(childBox, flags);
-
-                x += this._list.spacing + childWidth;
-            } else if (children[i] == this._separator) {
-                // We want the separator to be more compact than the rest.
-                childBox.x1 = x;
-                childBox.y1 = 0;
-                childBox.x2 = x + separatorWidth;
-                childBox.y2 = childHeight;
-                children[i].allocate(childBox, flags);
-                x += this._list.spacing + separatorWidth;
-            } else {
-                // Something else, eg, AppSwitcher's arrows;
-                // we don't allocate it.
-            }
-        }
+        this._switcherList._items[this._selectedIndex].add_accessible_state (Atk.StateType.EXPANDED);
     }
 });
 
-Signals.addSignalMethods(SwitcherList.prototype);
-
 const AppIcon = new Lang.Class({
     Name: 'AppIcon',
 
@@ -869,7 +382,7 @@ const AppIcon = new Lang.Class({
 
 const AppSwitcher = new Lang.Class({
     Name: 'AppSwitcher',
-    Extends: SwitcherList,
+    Extends: SwitcherPopup.SwitcherList,
 
     _init : function(localApps, otherApps, altTabPopup) {
         this.parent(true);
@@ -1023,7 +536,7 @@ const AppSwitcher = new Lang.Class({
 
         let n = this._arrows.length;
         let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' });
-        arrow.connect('repaint', function() { _drawArrow(arrow, St.Side.BOTTOM); });
+        arrow.connect('repaint', function() { SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM); });
         this._list.add_actor(arrow);
         this._arrows.push(arrow);
 
@@ -1036,7 +549,7 @@ const AppSwitcher = new Lang.Class({
 
 const ThumbnailList = new Lang.Class({
     Name: 'ThumbnailList',
-    Extends: SwitcherList,
+    Extends: SwitcherPopup.SwitcherList,
 
     _init : function(windows) {
         this.parent(false);
@@ -1119,45 +632,3 @@ const ThumbnailList = new Lang.Class({
     }
 });
 
-function _drawArrow(area, side) {
-    let themeNode = area.get_theme_node();
-    let borderColor = themeNode.get_border_color(side);
-    let bodyColor = themeNode.get_foreground_color();
-
-    let [width, height] = area.get_surface_size ();
-    let cr = area.get_context();
-
-    cr.setLineWidth(1.0);
-    Clutter.cairo_set_source_color(cr, borderColor);
-
-    switch (side) {
-    case St.Side.TOP:
-        cr.moveTo(0, height);
-        cr.lineTo(Math.floor(width * 0.5), 0);
-        cr.lineTo(width, height);
-        break;
-
-    case St.Side.BOTTOM:
-        cr.moveTo(width, 0);
-        cr.lineTo(Math.floor(width * 0.5), height);
-        cr.lineTo(0, 0);
-        break;
-
-    case St.Side.LEFT:
-        cr.moveTo(width, height);
-        cr.lineTo(0, Math.floor(height * 0.5));
-        cr.lineTo(width, 0);
-        break;
-
-    case St.Side.RIGHT:
-        cr.moveTo(0, 0);
-        cr.lineTo(width, Math.floor(height * 0.5));
-        cr.lineTo(0, height);
-        break;
-    }
-
-    cr.strokePreserve();
-
-    Clutter.cairo_set_source_color(cr, bodyColor);
-    cr.fill();
-}
diff --git a/js/ui/ctrlAltTab.js b/js/ui/ctrlAltTab.js
index 0519c99..14fba02 100644
--- a/js/ui/ctrlAltTab.js
+++ b/js/ui/ctrlAltTab.js
@@ -1,15 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
 const Clutter = imports.gi.Clutter;
-const Gdk = imports.gi.Gdk;
 const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
 const Meta = imports.gi.Meta;
 const Shell = imports.gi.Shell;
 const St = imports.gi.St;
 
-const AltTab = imports.ui.altTab;
 const Main = imports.ui.main;
+const SwitcherPopup = imports.ui.switcherPopup;
 const Params = imports.misc.params;
 const Tweener = imports.ui.tweener;
 
@@ -92,7 +91,7 @@ const CtrlAltTabManager = new Lang.Class({
         return a.x - b.x;
     },
 
-    popup: function(backwards, mask) {
+    popup: function(backward, binding, mask) {
         // Start with the set of focus groups that are currently mapped
         let items = this._items.filter(function (item) { return item.proxy.mapped; });
 
@@ -123,8 +122,8 @@ const CtrlAltTabManager = new Lang.Class({
         items.sort(Lang.bind(this, this._sortItems));
 
         if (!this._popup) {
-            this._popup = new CtrlAltTabPopup();
-            this._popup.show(items, backwards, mask);
+            this._popup = new CtrlAltTabPopup(items);
+            this._popup.show(backward, binding, mask);
 
             this._popup.actor.connect('destroy',
                                       Lang.bind(this, function() {
@@ -134,176 +133,46 @@ const CtrlAltTabManager = new Lang.Class({
     }
 });
 
-function mod(a, b) {
-    return (a + b) % b;
-}
-
 const CtrlAltTabPopup = new Lang.Class({
     Name: 'CtrlAltTabPopup',
+    Extends: SwitcherPopup.SwitcherPopup,
 
-    _init : function() {
-        this.actor = new Shell.GenericContainer({ name: 'ctrlAltTabPopup',
-                                                  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.actor.connect('destroy', Lang.bind(this, this._onDestroy));
-
-        this._haveModal = false;
-        this._modifierMask = 0;
-        this._selection = 0;
-
-        Main.uiGroup.add_actor(this.actor);
-    },
-
-    _getPreferredWidth: function (actor, forHeight, alloc) {
-        let primary = Main.layoutManager.primaryMonitor;
-
-        alloc.min_size = primary.width;
-        alloc.natural_size = primary.width;
-    },
-
-    _getPreferredHeight: function (actor, forWidth, alloc) {
-        let primary = Main.layoutManager.primaryMonitor;
-
-        alloc.min_size = primary.height;
-        alloc.natural_size = primary.height;
-    },
-
-    _allocate: function (actor, box, flags) {
-        let childBox = new Clutter.ActorBox();
-        let primary = Main.layoutManager.primaryMonitor;
-
-        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
-        let vPadding = this.actor.get_theme_node().get_vertical_padding();
-        let hPadding = this.actor.get_theme_node().get_horizontal_padding();
-
-        let [childMinHeight, childNaturalHeight] = this._switcher.actor.get_preferred_height(primary.width - hPadding);
-        let [childMinWidth, childNaturalWidth] = this._switcher.actor.get_preferred_width(childNaturalHeight);
-        childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2));
-        childBox.x2 = Math.min(primary.x + primary.width - hPadding, childBox.x1 + childNaturalWidth);
-        childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2);
-        childBox.y2 = childBox.y1 + childNaturalHeight;
-        this._switcher.actor.allocate(childBox, flags);
-    },
-
-    show : function(items, startBackwards, mask) {
-        if (!Main.pushModal(this.actor))
-            return false;
-        this._haveModal = true;
-        this._modifierMask = AltTab.primaryModifier(mask);
-
-        this._keyPressEventId = this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
-        this._keyReleaseEventId = this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
-
-        this._items = items;
-        this._switcher = new CtrlAltTabSwitcher(items);
-        this.actor.add_actor(this._switcher.actor);
-
-        if (startBackwards)
-            this._selection = this._items.length - 1;
-        this._select(this._selection);
-
-        let [x, y, mods] = global.get_pointer();
-        if (!(mods & this._modifierMask)) {
-            this._finish();
-            return false;
-        }
-
-        this.actor.opacity = 0;
-        this.actor.show();
-        Tweener.addTween(this.actor,
-                         { opacity: 255,
-                           time: POPUP_FADE_TIME,
-                           transition: 'easeOutQuad'
-                         });
-
+    _createSwitcher: function() {
+        this._switcherList = new CtrlAltTabSwitcher(this._items);
         return true;
     },
 
-    _next : function() {
-        return mod(this._selection + 1, this._items.length);
-    },
-
-    _previous : function() {
-        return mod(this._selection - 1, this._items.length);
+    _initialSelection: function(backward, binding) {
+        if (binding == 'switch-panels') {
+            if (backward)
+                this._selectedIndex = this._items.length - 1;
+        } else if (binding == 'switch-panels-backward') {
+            if (!backward)
+                this._selectedIndex = this._items.length - 1;
+        }
+        this._select(this._selectedIndex);
     },
 
-    _keyPressEvent : function(actor, event) {
-        let keysym = event.get_key_symbol();
-        let shift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK);
-        if (shift && keysym == Clutter.KEY_Tab)
-            keysym = Clutter.ISO_Left_Tab;
-
-        if (keysym == Clutter.KEY_Escape)
-            this.destroy();
-        else if (keysym == Clutter.KEY_Tab)
-            this._select(this._next());
-        else if (keysym == Clutter.KEY_ISO_Left_Tab)
-            this._select(this._previous());
-        else if (keysym == Clutter.KEY_Left)
+    _keyPressHandler: function(keysym, backwards, action) {
+        if (action == Meta.KeyBindingAction.SWITCH_PANELS)
+            this._select(backwards ? this._previous() : this._next());
+        else if (action == Meta.KeyBindingAction.SWITCH_PANELS_BACKWARD)
+            this._select(backwards ? this._next() : this._previous());
+        else if (keysym == Clutter.Left)
             this._select(this._previous());
-        else if (keysym == Clutter.KEY_Right)
+        else if (keysym == Clutter.Right)
             this._select(this._next());
-
-        return true;
-    },
-
-    _keyReleaseEvent : function(actor, event) {
-        let [x, y, mods] = global.get_pointer();
-        let state = mods & this._modifierMask;
-
-        if (state == 0)
-            this._finish();
-
-        return true;
     },
 
     _finish : function() {
-        this.destroy();
-
-        Main.ctrlAltTabManager.focusGroup(this._items[this._selection]);
-    },
-
-    _popModal: function() {
-        if (this._haveModal) {
-            Main.popModal(this.actor);
-            this._haveModal = false;
-        }
+        this.parent();
+        Main.ctrlAltTabManager.focusGroup(this._items[this._selectedIndex]);
     },
-
-    destroy : function() {
-        this._popModal();
-        Tweener.addTween(this.actor,
-                         { opacity: 0,
-                           time: POPUP_FADE_TIME,
-                           transition: 'easeOutQuad',
-                           onComplete: Lang.bind(this,
-                               function() {
-                                   this.actor.destroy();
-                               })
-                         });
-    },
-
-    _onDestroy : function() {
-        this._popModal();
-        if (this._keyPressEventId)
-            this.actor.disconnect(this._keyPressEventId);
-        if (this._keyReleaseEventId)
-            this.actor.disconnect(this._keyReleaseEventId);
-    },
-
-    _select : function(num) {
-        this._selection = num;
-        this._switcher.highlight(num);
-    }
 });
 
 const CtrlAltTabSwitcher = new Lang.Class({
     Name: 'CtrlAltTabSwitcher',
-    Extends: AltTab.SwitcherList,
+    Extends: SwitcherPopup.SwitcherList,
 
     _init : function(items) {
         this.parent(true);
diff --git a/js/ui/switcherPopup.js b/js/ui/switcherPopup.js
new file mode 100644
index 0000000..82ebcbf
--- /dev/null
+++ b/js/ui/switcherPopup.js
@@ -0,0 +1,640 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+const Clutter = imports.gi.Clutter;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+const St = imports.gi.St;
+
+const Main = imports.ui.main;
+const Tweener = imports.ui.tweener;
+
+const POPUP_DELAY_TIMEOUT = 150; // milliseconds
+
+const POPUP_SCROLL_TIME = 0.10; // seconds
+const POPUP_FADE_OUT_TIME = 0.1; // seconds
+
+const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
+
+function mod(a, b) {
+    return (a + b) % b;
+}
+
+function primaryModifier(mask) {
+    if (mask == 0)
+        return 0;
+
+    let primary = 1;
+    while (mask > 1) {
+        mask >>= 1;
+        primary <<= 1;
+    }
+    return primary;
+}
+
+const SwitcherPopup = new Lang.Class({
+    Name: 'SwitcherPopup',
+    Abstract: true,
+
+    _init: function(items) {
+        this._switcherList = null;
+
+        this._items = items || [];
+        this._selectedIndex = 0;
+
+        this.actor = new Shell.GenericContainer({ style_class: 'switcher-popup',
+                                                  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.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        Main.uiGroup.add_actor(this.actor);
+
+        this._haveModal = false;
+        this._modifierMask = 0;
+
+        this._motionTimeoutId = 0;
+        this._initialDelayTimeoutId = 0;
+
+        // Initially disable hover so we ignore the enter-event if
+        // the switcher appears underneath the current pointer location
+        this._disableHover();
+    },
+
+    _getPreferredWidth: function(actor, forHeight, alloc) {
+        let primary = Main.layoutManager.primaryMonitor;
+
+        alloc.min_size = primary.width;
+        alloc.natural_size = primary.width;
+    },
+
+    _getPreferredHeight: function(actor, forWidth, alloc) {
+        let primary = Main.layoutManager.primaryMonitor;
+
+        alloc.min_size = primary.height;
+        alloc.natural_size = primary.height;
+    },
+
+    _allocate: function(actor, box, flags) {
+        let childBox = new Clutter.ActorBox();
+        let primary = Main.layoutManager.primaryMonitor;
+
+        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
+        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
+        let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
+        let vPadding = this.actor.get_theme_node().get_vertical_padding();
+        let hPadding = leftPadding + rightPadding;
+
+        // Allocate the switcherList
+        // We select a size based on an icon size that does not overflow the screen
+        let [childMinHeight, childNaturalHeight] = this._switcherList.actor.get_preferred_height(primary.width - hPadding);
+        let [childMinWidth, childNaturalWidth] = this._switcherList.actor.get_preferred_width(childNaturalHeight);
+        childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2));
+        childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth);
+        childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2);
+        childBox.y2 = childBox.y1 + childNaturalHeight;
+        this._switcherList.actor.allocate(childBox, flags);
+    },
+
+    _createSwitcher: function() {
+        throw new Error('Not implemented');
+    },
+
+    _initialSelection: function(backward, binding) {
+        throw new Error('Not implemented');
+    },
+
+    show: function(backward, binding, mask) {
+        if (!this._createSwitcher())
+            return false;
+
+        if (!Main.pushModal(this.actor)) {
+            // Probably someone else has a pointer grab, try again with keyboard only
+            if (!Main.pushModal(this.actor, { options: Meta.ModalOptions.POINTER_ALREADY_GRABBED })) {
+                return false;
+            }
+        }
+        this._haveModal = true;
+        this._modifierMask = primaryModifier(mask);
+
+        this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
+        this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
+
+        this.actor.connect('button-press-event', Lang.bind(this, this._clickedOutside));
+        this.actor.connect('scroll-event', Lang.bind(this, this._scrollEvent));
+
+        this.actor.add_actor(this._switcherList.actor);
+        this._switcherList.connect('item-activated', Lang.bind(this, this._itemActivated));
+        this._switcherList.connect('item-entered', Lang.bind(this, this._itemEntered));
+
+        // Need to force an allocation so we can figure out whether we
+        // need to scroll when selecting
+        this.actor.opacity = 0;
+        this.actor.show();
+        this.actor.get_allocation_box();
+
+        if (this._items.length > 1)
+            this._selectedIndex = 1;
+        else
+            this._selectedIndex = 0;
+
+        this._initialSelection(backward, binding);
+
+        // There's a race condition; if the user released Alt before
+        // we got the grab, then we won't be notified. (See
+        // https://bugzilla.gnome.org/show_bug.cgi?id=596695 for
+        // details.) So we check now. (Have to do this after updating
+        // selection.)
+        let [x, y, mods] = global.get_pointer();
+        if (!(mods & this._modifierMask)) {
+            this._finish();
+            return false;
+        }
+
+        // We delay showing the popup so that fast Alt+Tab users aren't
+        // disturbed by the popup briefly flashing.
+        this._initialDelayTimeoutId = Mainloop.timeout_add(POPUP_DELAY_TIMEOUT,
+                                                           Lang.bind(this, function () {
+                                                               this.actor.opacity = 255;
+                                                               this._initialDelayTimeoutId = 0;
+                                                           }));
+        return true;
+    },
+
+    _next: function() {
+        return mod(this._selectedIndex + 1, this._items.length);
+    },
+
+    _previous: function() {
+        return mod(this._selectedIndex - 1, this._items.length);
+    },
+
+    _keyPressHandler: function(keysym, backwards, action) {
+        throw new Error('Not implemented');
+    },
+
+    _keyPressEvent: function(actor, event) {
+        let keysym = event.get_key_symbol();
+        let event_state = event.get_state();
+        let backwards = event_state & Clutter.ModifierType.SHIFT_MASK;
+        let action = global.display.get_keybinding_action(event.get_key_code(), event_state);
+
+        this._disableHover();
+
+        if (keysym == Clutter.Escape)
+            this.destroy();
+        else
+            this._keyPressHandler(keysym, backwards, action);
+
+        return true;
+    },
+
+    _keyReleaseEvent: function(actor, event) {
+        let [x, y, mods] = global.get_pointer();
+        let state = mods & this._modifierMask;
+
+        if (state == 0)
+            this._finish();
+
+        return true;
+    },
+
+    _clickedOutside: function(actor, event) {
+        this.destroy();
+    },
+
+    _scrollHandler: function(direction) {
+        if (direction == Clutter.ScrollDirection.UP)
+            this._select(this._previous());
+        else if (direction == Clutter.ScrollDirection.DOWN)
+            this._select(this._next());
+    },
+
+    _scrollEvent: function(actor, event) {
+        this._scrollHandler(event.get_scroll_direction());
+    },
+
+    _itemActivatedHandler: function(n) {
+        this._select(n);
+    },
+
+    _itemActivated: function(switcher, n) {
+        this._itemActivatedHandler(n);
+        this._finish();
+    },
+
+    _itemEnteredHandler: function(n) {
+        this._select(n);
+    },
+
+    _itemEntered: function(switcher, n) {
+        if (!this.mouseActive)
+            return;
+        this._itemEnteredHandler(n);
+    },
+
+    _disableHover: function() {
+        this.mouseActive = false;
+
+        if (this._motionTimeoutId != 0)
+            Mainloop.source_remove(this._motionTimeoutId);
+
+        this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut));
+    },
+
+    _mouseTimedOut: function() {
+        this._motionTimeoutId = 0;
+        this.mouseActive = true;
+    },
+
+    _popModal: function() {
+        if (this._haveModal) {
+            Main.popModal(this.actor);
+            this._haveModal = false;
+        }
+    },
+
+    destroy: function() {
+        this._popModal();
+        if (this.actor.visible) {
+            Tweener.addTween(this.actor,
+                             { opacity: 0,
+                               time: POPUP_FADE_OUT_TIME,
+                               transition: 'easeOutQuad',
+                               onComplete: Lang.bind(this,
+                                   function() {
+                                       this.actor.destroy();
+                                   })
+                             });
+        } else
+            this.actor.destroy();
+    },
+
+    _finish: function() {
+        this.destroy();
+    },
+
+    _onDestroy: function() {
+        this._popModal();
+
+        if (this._motionTimeoutId != 0)
+            Mainloop.source_remove(this._motionTimeoutId);
+        if (this._initialDelayTimeoutId != 0)
+            Mainloop.source_remove(this._initialDelayTimeoutId);
+    },
+
+    _select: function(num) {
+        this._selectedIndex = num;
+        this._switcherList.highlight(num);
+    }
+});
+
+const SwitcherList = new Lang.Class({
+    Name: 'SwitcherList',
+
+    _init : function(squareItems) {
+        this.actor = new Shell.GenericContainer({ style_class: 'switcher-list' });
+        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._allocateTop));
+
+        // Here we use a GenericContainer so that we can force all the
+        // children except the separator to have the same width.
+        this._list = new Shell.GenericContainer({ style_class: 'switcher-list-item-container' });
+        this._list.spacing = 0;
+        this._list.connect('style-changed', Lang.bind(this, function() {
+                                                        this._list.spacing = this._list.get_theme_node().get_length('spacing');
+                                                     }));
+
+        this._list.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
+        this._list.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
+        this._list.connect('allocate', Lang.bind(this, this._allocate));
+
+        this._scrollView = new St.ScrollView({ style_class: 'hfade',
+                                               enable_mouse_scrolling: false });
+        this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER);
+
+        let scrollBox = new St.BoxLayout();
+        scrollBox.add_actor(this._list);
+        this._scrollView.add_actor(scrollBox);
+        this.actor.add_actor(this._scrollView);
+
+        // Those arrows indicate whether scrolling in one direction is possible
+        this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
+                                               pseudo_class: 'highlighted' });
+        this._leftArrow.connect('repaint', Lang.bind(this,
+            function() { drawArrow(this._leftArrow, St.Side.LEFT); }));
+        this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow',
+                                                pseudo_class: 'highlighted' });
+        this._rightArrow.connect('repaint', Lang.bind(this,
+            function() { drawArrow(this._rightArrow, St.Side.RIGHT); }));
+
+        this.actor.add_actor(this._leftArrow);
+        this.actor.add_actor(this._rightArrow);
+
+        this._items = [];
+        this._highlighted = -1;
+        this._separator = null;
+        this._squareItems = squareItems;
+        this._minSize = 0;
+        this._scrollableRight = true;
+        this._scrollableLeft = false;
+    },
+
+    _allocateTop: function(actor, box, flags) {
+        let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT);
+        let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT);
+
+        let childBox = new Clutter.ActorBox();
+        let scrollable = this._minSize > box.x2 - box.x1;
+
+        box.y1 -= this.actor.get_theme_node().get_padding(St.Side.TOP);
+        box.y2 += this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
+        this._scrollView.allocate(box, flags);
+
+        let arrowWidth = Math.floor(leftPadding / 3);
+        let arrowHeight = arrowWidth * 2;
+        childBox.x1 = leftPadding / 2;
+        childBox.y1 = this.actor.height / 2 - arrowWidth;
+        childBox.x2 = childBox.x1 + arrowWidth;
+        childBox.y2 = childBox.y1 + arrowHeight;
+        this._leftArrow.allocate(childBox, flags);
+        this._leftArrow.opacity = (this._scrollableLeft && scrollable) ? 255 : 0;
+
+        arrowWidth = Math.floor(rightPadding / 3);
+        arrowHeight = arrowWidth * 2;
+        childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2;
+        childBox.y1 = this.actor.height / 2 - arrowWidth;
+        childBox.x2 = childBox.x1 + arrowWidth;
+        childBox.y2 = childBox.y1 + arrowHeight;
+        this._rightArrow.allocate(childBox, flags);
+        this._rightArrow.opacity = (this._scrollableRight && scrollable) ? 255 : 0;
+    },
+
+    addItem : function(item, label) {
+        let bbox = new St.Button({ style_class: 'item-box',
+                                   reactive: true });
+
+        bbox.set_child(item);
+        this._list.add_actor(bbox);
+
+        let n = this._items.length;
+        bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); }));
+        bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); }));
+
+        bbox.label_actor = label;
+
+        this._items.push(bbox);
+
+        return bbox;
+    },
+
+    _onItemClicked: function (index) {
+        this._itemActivated(index);
+    },
+
+    _onItemEnter: function (index) {
+        this._itemEntered(index);
+    },
+
+    addSeparator: function () {
+        let box = new St.Bin({ style_class: 'separator' });
+        this._separator = box;
+        this._list.add_actor(box);
+    },
+
+    highlight: function(index, justOutline) {
+        if (this._highlighted != -1) {
+            this._items[this._highlighted].remove_style_pseudo_class('outlined');
+            this._items[this._highlighted].remove_style_pseudo_class('selected');
+        }
+
+        this._highlighted = index;
+
+        if (this._highlighted != -1) {
+            if (justOutline)
+                this._items[this._highlighted].add_style_pseudo_class('outlined');
+            else
+                this._items[this._highlighted].add_style_pseudo_class('selected');
+        }
+
+        let adjustment = this._scrollView.hscroll.adjustment;
+        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
+        let [absItemX, absItemY] = this._items[index].get_transformed_position();
+        let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0);
+        let [containerWidth, containerHeight] = this.actor.get_transformed_size();
+        if (posX + this._items[index].get_width() > containerWidth)
+            this._scrollToRight();
+        else if (this._items[index].allocation.x1 - value < 0)
+            this._scrollToLeft();
+
+    },
+
+    _scrollToLeft : function() {
+        let adjustment = this._scrollView.hscroll.adjustment;
+        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
+
+        let item = this._items[this._highlighted];
+
+        if (item.allocation.x1 < value)
+            value = Math.min(0, item.allocation.x1);
+        else if (item.allocation.x2 > value + pageSize)
+            value = Math.max(upper, item.allocation.x2 - pageSize);
+
+        this._scrollableRight = true;
+        Tweener.addTween(adjustment,
+                         { value: value,
+                           time: POPUP_SCROLL_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this, function () {
+                                if (this._highlighted == 0) {
+                                    this._scrollableLeft = false;
+                                    this.actor.queue_relayout();
+                                }
+                           })
+                          });
+    },
+
+    _scrollToRight : function() {
+        let adjustment = this._scrollView.hscroll.adjustment;
+        let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values();
+
+        let item = this._items[this._highlighted];
+
+        if (item.allocation.x1 < value)
+            value = Math.max(0, item.allocation.x1);
+        else if (item.allocation.x2 > value + pageSize)
+            value = Math.min(upper, item.allocation.x2 - pageSize);
+
+        this._scrollableLeft = true;
+        Tweener.addTween(adjustment,
+                         { value: value,
+                           time: POPUP_SCROLL_TIME,
+                           transition: 'easeOutQuad',
+                           onComplete: Lang.bind(this, function () {
+                                if (this._highlighted == this._items.length - 1) {
+                                    this._scrollableRight = false;
+                                    this.actor.queue_relayout();
+                                }
+                            })
+                          });
+    },
+
+    _itemActivated: function(n) {
+        this.emit('item-activated', n);
+    },
+
+    _itemEntered: function(n) {
+        this.emit('item-entered', n);
+    },
+
+    _maxChildWidth: function (forHeight) {
+        let maxChildMin = 0;
+        let maxChildNat = 0;
+
+        for (let i = 0; i < this._items.length; i++) {
+            let [childMin, childNat] = this._items[i].get_preferred_width(forHeight);
+            maxChildMin = Math.max(childMin, maxChildMin);
+            maxChildNat = Math.max(childNat, maxChildNat);
+
+            if (this._squareItems) {
+                let [childMin, childNat] = this._items[i].get_preferred_height(-1);
+                maxChildMin = Math.max(childMin, maxChildMin);
+                maxChildNat = Math.max(childNat, maxChildNat);
+            }
+        }
+
+        return [maxChildMin, maxChildNat];
+    },
+
+    _getPreferredWidth: function (actor, forHeight, alloc) {
+        let [maxChildMin, maxChildNat] = this._maxChildWidth(forHeight);
+
+        let separatorWidth = 0;
+        if (this._separator) {
+            let [sepMin, sepNat] = this._separator.get_preferred_width(forHeight);
+            separatorWidth = sepNat + this._list.spacing;
+        }
+
+        let totalSpacing = this._list.spacing * (this._items.length - 1);
+        alloc.min_size = this._items.length * maxChildMin + separatorWidth + totalSpacing;
+        alloc.natural_size = alloc.min_size;
+        this._minSize = alloc.min_size;
+    },
+
+    _getPreferredHeight: function (actor, forWidth, alloc) {
+        let maxChildMin = 0;
+        let maxChildNat = 0;
+
+        for (let i = 0; i < this._items.length; i++) {
+            let [childMin, childNat] = this._items[i].get_preferred_height(-1);
+            maxChildMin = Math.max(childMin, maxChildMin);
+            maxChildNat = Math.max(childNat, maxChildNat);
+        }
+
+        if (this._squareItems) {
+            let [childMin, childNat] = this._maxChildWidth(-1);
+            maxChildMin = Math.max(childMin, maxChildMin);
+            maxChildNat = maxChildMin;
+        }
+
+        alloc.min_size = maxChildMin;
+        alloc.natural_size = maxChildNat;
+    },
+
+    _allocate: function (actor, box, flags) {
+        let childHeight = box.y2 - box.y1;
+
+        let [maxChildMin, maxChildNat] = this._maxChildWidth(childHeight);
+        let totalSpacing = this._list.spacing * (this._items.length - 1);
+
+        let separatorWidth = 0;
+        if (this._separator) {
+            let [sepMin, sepNat] = this._separator.get_preferred_width(childHeight);
+            separatorWidth = sepNat;
+            totalSpacing += this._list.spacing;
+        }
+
+        let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing - separatorWidth) / this._items.length);
+
+        let x = 0;
+        let children = this._list.get_children();
+        let childBox = new Clutter.ActorBox();
+
+        let primary = Main.layoutManager.primaryMonitor;
+        let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT);
+
+        for (let i = 0; i < children.length; i++) {
+            if (this._items.indexOf(children[i]) != -1) {
+                let [childMin, childNat] = children[i].get_preferred_height(childWidth);
+                let vSpacing = (childHeight - childNat) / 2;
+                childBox.x1 = x;
+                childBox.y1 = vSpacing;
+                childBox.x2 = x + childWidth;
+                childBox.y2 = childBox.y1 + childNat;
+                children[i].allocate(childBox, flags);
+
+                x += this._list.spacing + childWidth;
+            } else if (children[i] == this._separator) {
+                // We want the separator to be more compact than the rest.
+                childBox.x1 = x;
+                childBox.y1 = 0;
+                childBox.x2 = x + separatorWidth;
+                childBox.y2 = childHeight;
+                children[i].allocate(childBox, flags);
+                x += this._list.spacing + separatorWidth;
+            } else {
+                // Something else, eg, AppSwitcher's arrows;
+                // we don't allocate it.
+            }
+        }
+    }
+});
+Signals.addSignalMethods(SwitcherList.prototype);
+
+function drawArrow(area, side) {
+    let themeNode = area.get_theme_node();
+    let borderColor = themeNode.get_border_color(side);
+    let bodyColor = themeNode.get_foreground_color();
+
+    let [width, height] = area.get_surface_size ();
+    let cr = area.get_context();
+
+    cr.setLineWidth(1.0);
+    Clutter.cairo_set_source_color(cr, borderColor);
+
+    switch (side) {
+    case St.Side.TOP:
+        cr.moveTo(0, height);
+        cr.lineTo(Math.floor(width * 0.5), 0);
+        cr.lineTo(width, height);
+        break;
+
+    case St.Side.BOTTOM:
+        cr.moveTo(width, 0);
+        cr.lineTo(Math.floor(width * 0.5), height);
+        cr.lineTo(0, 0);
+        break;
+
+    case St.Side.LEFT:
+        cr.moveTo(width, height);
+        cr.lineTo(0, Math.floor(height * 0.5));
+        cr.lineTo(width, 0);
+        break;
+
+    case St.Side.RIGHT:
+        cr.moveTo(0, 0);
+        cr.lineTo(width, Math.floor(height * 0.5));
+        cr.lineTo(0, height);
+        break;
+    }
+
+    cr.strokePreserve();
+
+    Clutter.cairo_set_source_color(cr, bodyColor);
+    cr.fill();
+}
+
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index c4896ee..42b2e66 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -621,7 +621,7 @@ const WindowManager = new Lang.Class({
     _startA11ySwitcher : function(display, screen, window, binding) {
         let modifiers = binding.get_modifiers();
         let backwards = modifiers & Meta.VirtualModifier.SHIFT_MASK;
-        Main.ctrlAltTabManager.popup(backwards, binding.get_mask());
+        Main.ctrlAltTabManager.popup(backwards, binding.get_name(), binding.get_mask());
     },
 
     _openAppMenu : function(display, screen, window, event, binding) {



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