[gnome-shell] [AppSwitcher] Use thumbnails instead of a window menu, and other UI changes



commit d7af6d40e38b2c0ebc03a916f06574efd641dd2d
Author: Dan Winship <danw gnome org>
Date:   Fri Oct 2 11:02:46 2009 -0400

    [AppSwitcher] Use thumbnails instead of a window menu, and other UI changes
    
    https://bugzilla.gnome.org/show_bug.cgi?id=590563

 data/theme/gnome-shell.css |   13 +-
 js/ui/altTab.js            |  794 +++++++++++++++++++++++++++++---------------
 2 files changed, 543 insertions(+), 264 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index a671321..42097fe 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -157,9 +157,20 @@ StScrollBar StButton#vhandle:hover
 }
 
 /* App Switcher */
-#appSwitcher {
+.switcher-list {
     background: rgba(0,0,0,0.8);
     border: 1px solid rgba(128,128,128,0.40);
     border-radius: 8px;
+    padding: 18px;
+}
+
+.switcher-list .item-box {
+    padding: 8px;
+    border-radius: 4px;
+}
+
+.switcher-list .selected-item-box {
     padding: 8px;
+    border-radius: 4px;
+    background: rgba(255,255,255,0.33);
 }
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index f3d3d6a..a437313 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -4,88 +4,60 @@ const Big = imports.gi.Big;
 const Clutter = imports.gi.Clutter;
 const Gdk = imports.gi.Gdk;
 const Lang = imports.lang;
+const Mainloop = imports.mainloop;
 const Meta = imports.gi.Meta;
 const Pango = imports.gi.Pango;
 const Shell = imports.gi.Shell;
+const Signals = imports.signals;
 const St = imports.gi.St;
 
 const AppIcon = imports.ui.appIcon;
-const Lightbox = imports.ui.lightbox;
 const Main = imports.ui.main;
 
-const POPUP_APPICON_BORDER_COLOR = new Clutter.Color();
-POPUP_APPICON_BORDER_COLOR.from_pixel(0xffffffff);
-const POPUP_APPICON_SEPARATOR_COLOR = new Clutter.Color();
-POPUP_APPICON_SEPARATOR_COLOR.from_pixel(0x80808066);
+const POPUP_ARROW_COLOR = new Clutter.Color();
+POPUP_ARROW_COLOR.from_pixel(0xffffffff);
+const TRANSPARENT_COLOR = new Clutter.Color();
+TRANSPARENT_COLOR.from_pixel(0x00000000);
+const POPUP_SEPARATOR_COLOR = new Clutter.Color();
+POPUP_SEPARATOR_COLOR.from_pixel(0x80808066);
 
-const POPUP_APPS_BOX_SPACING = 8;
+const POPUP_APPICON_SIZE = 96;
+const POPUP_LIST_SPACING = 8;
 
 const POPUP_POINTER_SELECTION_THRESHOLD = 3;
 
+const THUMBNAIL_SIZE = 256;
+const THUMBNAIL_POPUP_TIME = 1000; // milliseconds
+
+const HOVER_TIME = 500; // milliseconds
+
+function mod(a, b) {
+    return (a + b) % b;
+}
+
 function AltTabPopup() {
     this._init();
 }
 
 AltTabPopup.prototype = {
     _init : function() {
-        this.actor = new St.Bin({ name: 'appSwitcher',
-                                  reactive: true });
-        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+        this.actor = new Clutter.Group({ reactive: true,
+                                         x: 0,
+                                         y: 0,
+                                         width: global.screen_width,
+                                         height: global.screen_height });
 
-        // Here we use a GenericContainer instead of a BigBox in order to be
-        // able to handle allocation. In particular, we want all the alt+tab
-        // popup items to be the same size.
-        this._appsBox = new Shell.GenericContainer();
-        this._appsBox.spacing = POPUP_APPS_BOX_SPACING;
-
-        this._appsBox.connect('get-preferred-width', Lang.bind(this, this._appsBoxGetPreferredWidth));
-        this._appsBox.connect('get-preferred-height', Lang.bind(this, this._appsBoxGetPreferredHeight));
-        this._appsBox.connect('allocate', Lang.bind(this, this._appsBoxAllocate));
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
 
-        let gcenterbox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
-                                       x_align: Big.BoxAlignment.CENTER });
-        gcenterbox.append(this._appsBox, Big.BoxPackFlags.NONE);
-        this.actor.add_actor(gcenterbox);
+        this._haveModal = false;
 
-        this._icons = [];
-        this._separator = null;
+        this._currentApp = 0;
         this._currentWindows = [];
-        this._haveModal = false;
-        this._selected = 0;
-        this._highlightedWindow = null;
-        this._toplevels = global.window_group.get_children();
+        this._thumbnailTimeoutId = 0;
 
         global.stage.add_actor(this.actor);
     },
 
-    _addIcon : function(appIcon) {
-        appIcon.connect('activate', Lang.bind(this, this._appClicked));
-        appIcon.connect('activate-window', Lang.bind(this, this._windowClicked));
-        appIcon.connect('highlight-window', Lang.bind(this, this._windowHovered));
-        appIcon.connect('menu-popped-up', Lang.bind(this, this._menuPoppedUp));
-        appIcon.connect('menu-popped-down', Lang.bind(this, this._menuPoppedDown));
-
-        appIcon.actor.connect('enter-event', Lang.bind(this, this._iconEntered));
-
-        // FIXME?
-        appIcon.actor.border = 2;
-        appIcon.highlight_border_color = POPUP_APPICON_BORDER_COLOR;
-
-        this._icons.push(appIcon);
-        this._currentWindows.push(appIcon.windows[0]);
-
-        this._appsBox.add_actor(appIcon.actor);
-    },
-
-    _addSeparator: function () {
-        let box = new Big.Box({ padding_top: 2, padding_bottom: 2 });
-        box.append(new Clutter.Rectangle({ width: 1,
-                                           color: POPUP_APPICON_SEPARATOR_COLOR }),
-                   Big.BoxPackFlags.EXPAND);
-        this._separator = box;
-        this._appsBox.add_actor(box);
-    },
-
     show : function(backward) {
         let appMonitor = Shell.AppMonitor.get_default();
         let apps = appMonitor.get_running_apps ("");
@@ -104,60 +76,51 @@ AltTabPopup.prototype = {
         this._mouseActive = false;
         this._mouseMovement = 0;
 
-        // Contruct the AppIcons, sort by time, add to the popup
-        let activeWorkspace = global.screen.get_active_workspace();
-        let workspaceIcons = [];
-        let otherIcons = [];
-        for (let i = 0; i < apps.length; i++) {
-            let appIcon = new AppIcon.AppIcon({ appInfo: apps[i],
-                                                menuType: AppIcon.MenuType.BELOW });
-            if (this._hasWindowsOnWorkspace(appIcon, activeWorkspace))
-              workspaceIcons.push(appIcon);
-            else
-              otherIcons.push(appIcon);
-        }
-
-        workspaceIcons.sort(Lang.bind(this, this._sortAppIcon));
-        otherIcons.sort(Lang.bind(this, this._sortAppIcon));
-
-        for (let i = 0; i < workspaceIcons.length; i++)
-            this._addIcon(workspaceIcons[i]);
-        if (workspaceIcons.length > 0 && otherIcons.length > 0)
-            this._addSeparator();
-        for (let i = 0; i < otherIcons.length; i++)
-            this._addIcon(otherIcons[i]);
-
-        // Need to specify explicit width and height because the
-        // window_group may not actually cover the whole screen
-        this._lightbox = new Lightbox.Lightbox(global.window_group,
-                                               global.screen_width,
-                                               global.screen_height);
-
-        this.actor.show_all();
+        this._appSwitcher = new AppSwitcher(apps);
+        this.actor.add_actor(this._appSwitcher.actor);
+        this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated));
+        this._appSwitcher.connect('item-hovered', Lang.bind(this, this._appHovered));
 
         let primary = global.get_primary_monitor();
-        this.actor.x = primary.x + Math.floor((primary.width - this.actor.width) / 2);
-        this.actor.y = primary.y + Math.floor((primary.height - this.actor.height) / 2);
-
-        if (!backward && this._icons[this._selected].windows.length > 1) {
-            let candidateWindow = this._icons[this._selected].windows[1];
-            if (candidateWindow.get_workspace() == activeWorkspace) {
-                this._currentWindows[this._selected] = candidateWindow;
-                this._updateSelection(0);
-            }
-            else {
-                this._updateSelection(1);
-            }
-        }
-        else {
-            this._updateSelection(backward ? -1 : 1);
+        this._appSwitcher.actor.x = primary.x + Math.floor((primary.width - this._appSwitcher.actor.width) / 2);
+        this._appSwitcher.actor.y = primary.y + Math.floor((primary.height - this._appSwitcher.actor.height) / 2);
+
+        this._appIcons = this._appSwitcher.icons;
+
+        // _currentWindows give the index of the selected window for
+        // each app; they all start at 0.
+        this._currentWindows = this._appIcons.map(function (app) { return 0; });
+
+        // Make the initial selection
+        if (this._appIcons.length == 1) {
+            if (!backward && this._appIcons[0].windows.length > 1) {
+                // For compatibility with the multi-app case below
+                this._select(0, 1);
+            } else
+                this._select(0);
+        } else if (backward) {
+            this._select(this._appIcons.length - 1);
+        } else {
+            if (this._appIcons[0].windows.length > 1) {
+                let curAppNextWindow = this._appIcons[0].windows[1];
+                let nextAppWindow = this._appIcons[1].windows[0];
+
+                // If the next window of the current app is more-recently-used
+                // than the first window of the next app, then select it.
+                if (curAppNextWindow.get_workspace() == global.screen.get_active_workspace() &&
+                    curAppNextWindow.get_user_time() > nextAppWindow.get_user_time())
+                    this._select(0, 1);
+                else
+                    this._select(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 calling
-        // _updateSelection.)
+        // details.) So we check now. (Have to do this after updating
+        // selection.)
         let mods = global.get_modifier_keys();
         if (!(mods & Gdk.ModifierType.MOD1_MASK)) {
             this._finish();
@@ -167,67 +130,356 @@ AltTabPopup.prototype = {
         return true;
     },
 
-    _hasWindowsOnWorkspace: function(appIcon, workspace) {
-        for (let i = 0; i < appIcon.windows.length; i++) {
-            if (appIcon.windows[i].get_workspace() == workspace)
-                return true;
-        }
-        return false;
+    _nextApp : function() {
+        return mod(this._currentApp + 1, this._appIcons.length);
+    },
+    _previousApp : function() {
+        return mod(this._currentApp - 1, this._appIcons.length);
     },
 
-    _hasVisibleWindows : function(appIcon) {
-        for (let i = 0; i < appIcon.windows.length; i++) {
-            if (appIcon.windows[i].showing_on_its_workspace())
-                return true;
+    _nextWindow : function() {
+        return mod(this._currentWindows[this._currentApp] + 1,
+                   this._appIcons[this._currentApp].windows.length);
+    },
+    _previousWindow : function() {
+        return mod(this._currentWindows[this._currentApp] - 1,
+                   this._appIcons[this._currentApp].windows.length);
+    },
+
+    _keyPressEvent : function(actor, event) {
+        let keysym = event.get_key_symbol();
+        let shift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK);
+
+        // The WASD stuff is for debugging in Xephyr, where the arrow
+        // keys aren't mapped correctly
+
+        if (keysym == Clutter.Tab)
+            this._select(shift ? this._previousApp() : this._nextApp());
+        else if (keysym == Clutter.grave)
+            this._select(this._currentApp, shift ? this._previousWindow() : this._nextWindow());
+        else if (keysym == Clutter.Escape)
+            this.destroy();
+        else if (this._thumbnails) {
+            if (keysym == Clutter.Left || keysym == Clutter.a)
+                this._select(this._currentApp, this._previousWindow());
+            else if (keysym == Clutter.Right || keysym == Clutter.d)
+                this._select(this._currentApp, this._nextWindow());
+            else if (keysym == Clutter.Up || keysym == Clutter.w)
+                this._select(this._currentApp, null, true);
+        } else {
+            if (keysym == Clutter.Left || keysym == Clutter.a)
+                this._select(this._previousApp());
+            else if (keysym == Clutter.Right || keysym == Clutter.d)
+                this._select(this._nextApp());
+            else if (keysym == Clutter.Down || keysym == Clutter.s)
+                this._select(this._currentApp, this._currentWindows[this._currentApp]);
         }
-        return false;
+
+        return true;
     },
 
-    _sortAppIcon : function(appIcon1, appIcon2) {
-        let vis1 = this._hasVisibleWindows(appIcon1);
-        let vis2 = this._hasVisibleWindows(appIcon2);
+    _keyReleaseEvent : function(actor, event) {
+        let keysym = event.get_key_symbol();
 
-        if (vis1 && !vis2) {
-            return -1;
-        } else if (vis2 && !vis1) {
-            return 1;
+        if (keysym == Clutter.Alt_L || keysym == Clutter.Alt_R)
+            this._finish();
+
+        return true;
+    },
+
+    _appActivated : function(appSwitcher, n) {
+        Main.activateWindow(this._appIcons[n].windows[this._currentWindows[n]]);
+        this.destroy();
+    },
+
+    _appHovered : function(appSwitcher, n) {
+        if (!this._mouseActive)
+            return;
+
+        this._select(n, this._currentWindows[n]);
+    },
+
+    _windowActivated : function(thumbnailList, n) {
+        Main.activateWindow(this._appIcons[this._currentApp].windows[n]);
+        this.destroy();
+    },
+
+    _windowHovered : function(thumbnailList, n) {
+        if (!this._mouseActive)
+            return;
+
+        this._select(this._currentApp, n);
+    },
+
+    _mouseMoved : function(actor, event) {
+        if (++this._mouseMovement < POPUP_POINTER_SELECTION_THRESHOLD)
+            return;
+
+        this.actor.disconnect(this._motionEventId);
+        this._mouseActive = true;
+
+        this._appSwitcher.checkHover();
+        if (this._thumbnails)
+            this._thumbnails.checkHover();
+    },
+
+    _finish : function() {
+        let app = this._appIcons[this._currentApp];
+        let window = app.windows[this._currentWindows[this._currentApp]];
+        Main.activateWindow(window);
+        this.destroy();
+    },
+
+    destroy : function() {
+        this.actor.destroy();
+    },
+
+    _onDestroy : function() {
+        if (this._haveModal)
+            Main.popModal(this.actor);
+
+        if (this._keyPressEventId)
+            global.stage.disconnect(this._keyPressEventId);
+        if (this._keyReleaseEventId)
+            global.stage.disconnect(this._keyReleaseEventId);
+
+        if (this._thumbnailTimeoutId != 0)
+            Mainloop.source_remove(this._thumbnailTimeoutId);
+    },
+
+    _select : function(app, window, noTimeout) {
+        if ((app != this._currentApp || !window) && this._thumbnails) {
+            this._thumbnails.actor.destroy();
+            this._thumbnails = null;
+            this._appSwitcher.showArrow(-1);
+        }
+
+        if (this._thumbnailTimeoutId != 0) {
+            Mainloop.source_remove(this._thumbnailTimeoutId);
+            this._thumbnailTimeoutId = 0;
+        }
+
+        this._currentApp = app;
+        if (window != null) {
+            this._appSwitcher.highlight(-1);
+            this._appSwitcher.showArrow(app);
         } else {
-            // The app's most-recently-used window is first
-            // in its list
-            return (appIcon2.windows[0].get_user_time() -
-                    appIcon1.windows[0].get_user_time());
+            this._appSwitcher.highlight(app);
+            if (this._appIcons[this._currentApp].windows.length > 1)
+                this._appSwitcher.showArrow(app);
+        }
+
+        if (window != null) {
+            if (!this._thumbnails)
+                this._createThumbnails();
+            this._currentWindows[this._currentApp] = window;
+            this._thumbnails.highlight(window);
+        } else if (this._appIcons[this._currentApp].windows.length > 1 &&
+                   !noTimeout) {
+            this._thumbnailTimeoutId = Mainloop.timeout_add (
+                THUMBNAIL_POPUP_TIME,
+                Lang.bind(this, function () {
+                              this._select(this._currentApp,
+                                           this._currentWindows[this._currentApp]);
+                              return false;
+                          }));
+        }
+    },
+
+    _createThumbnails : function() {
+        this._thumbnails = new ThumbnailList (this._appIcons[this._currentApp].windows);
+        this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
+        this._thumbnails.connect('item-hovered', Lang.bind(this, this._windowHovered));
+
+        this.actor.add_actor(this._thumbnails.actor);
+
+        let thumbnailCenter;
+        if (this._thumbnails.actor.width < this._appSwitcher.actor.width) {
+            // Center the thumbnails under the corresponding AppIcon.
+            // If this is being called when the switcher is first
+            // being brought up, then nothing will have been assigned
+            // an allocation yet, and the get_transformed_position()
+            // call will return 0,0.
+            // (http://bugzilla.openedhand.com/show_bug.cgi?id=1115).
+            // Calling clutter_actor_get_allocation_box() would force
+            // it to properly allocate itself, but we can't call that
+            // because it has an out-caller-allocates arg. So we use
+            // clutter_stage_get_actor_at_pos(), which will force a
+            // reallocation as a side effect.
+            global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, 0, 0);
+
+            let icon = this._appIcons[this._currentApp].actor;
+            let [stageX, stageY] = icon.get_transformed_position();
+            thumbnailCenter = stageX + icon.width / 2;
+        } else {
+            // Center the thumbnails on the monitor
+            let primary = global.get_primary_monitor();
+            thumbnailCenter = primary.x + primary.width / 2;
+        }
+
+        this._thumbnails.actor.x = Math.floor(thumbnailCenter - this._thumbnails.actor.width / 2);
+        this._thumbnails.actor.y = this._appSwitcher.actor.y + this._appSwitcher.actor.height + POPUP_LIST_SPACING;
+    }
+};
+
+function SwitcherList(squareItems) {
+    this._init(squareItems);
+}
+
+SwitcherList.prototype = {
+    _init : function(squareItems) {
+        this.actor = new St.Bin({ style_class: 'switcher-list' });
+        this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
+
+        // 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();
+        this._list.spacing = POPUP_LIST_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.actor.add_actor(this._list);
+
+        this._items = [];
+        this._highlighted = -1;
+        this._separator = null;
+        this._squareItems = squareItems;
+
+        this._hoverTimeout = 0;
+    },
+
+    _onDestroy: function() {
+        if (this._hoverTimeout != 0) {
+            Mainloop.source_remove(this._hoverTimeout);
+            this._hoverTimeout = 0;
+        }
+    },
+
+    addItem : function(item) {
+        let box = new St.Bin({ style_class: 'item-box' });
+        let bbox;
+
+        if (item instanceof Shell.ButtonBox)
+            bbox = item;
+        else {
+            bbox = new Shell.ButtonBox({ reactive: true });
+            bbox.append(item, Big.BoxPackFlags.NONE);
         }
+        box.add_actor(bbox);
+        this._list.add_actor(box);
+
+        let n = this._items.length;
+        bbox.connect('activate', Lang.bind(this, function () {
+                                               this._itemActivated(n);
+                                          }));
+        bbox.connect('notify::hover', Lang.bind(this, function () {
+                                                    this._hoverChanged(bbox, n);
+                                               }));
+
+        this._items.push(box);
+    },
+
+    addSeparator: function () {
+        // FIXME: make this work with StWidgets and CSS
+        let box = new Big.Box({ padding_top: 2, padding_bottom: 2 });
+        box.append(new Clutter.Rectangle({ width: 1,
+                                           color: POPUP_SEPARATOR_COLOR }),
+                   Big.BoxPackFlags.EXPAND);
+        this._separator = box;
+        this._list.add_actor(box);
     },
+    
+    highlight: function(index) {
+        if (this._highlighted != -1)
+            this._items[this._highlighted].style_class = 'item-box';
 
-    _appsBoxGetPreferredWidth: function (actor, forHeight, alloc) {
-        let children = this._appsBox.get_children();
+        this._highlighted = index;
+
+        if (this._highlighted != -1)
+            this._items[this._highlighted].style_class = 'selected-item-box';
+    },
+
+    // Used after the mouse movement exceeds the threshold, to check
+    // if it's already hovering over an icon
+    checkHover: function() {
+        for (let i = 0; i < this._items.length; i++) {
+            if (this._items[i].get_child().hover) {
+                this._hoverChanged(this._items[i].get_child(), i);
+                return;
+            }
+        }
+    },
+
+    _itemActivated: function(n) {
+        this.emit('item-activated', n);
+    },
+    
+    _hoverChanged: function(box, n) {
+        if (this._hoverTimeout != 0) {
+            Mainloop.source_remove(this._hoverTimeout);
+            this._hoverTimeout = 0;
+        }
+
+        if (box.hover) {
+            this._hoverTimeout = Mainloop.timeout_add(
+                HOVER_TIME,
+                Lang.bind (this, function () {
+                               this._itemHovered(n);
+                           }));
+        }
+    },
+
+    _itemHovered: function(n) {
+        this.emit('item-hovered', n);
+    },
+
+    _maxChildWidth: function (forHeight) {
         let maxChildMin = 0;
         let maxChildNat = 0;
 
-        for (let i = 0; i < children.length; i++) {
-            if (children[i] != this._separator) {
-                let [childMin, childNat] = children[i].get_preferred_width(forHeight);
+        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)
-            separatorWidth = this._separator.get_preferred_width(forHeight)[0];
+        if (this._separator) {
+            let [sepMin, sepNat] = this._separator.get_preferred_width(forHeight);
+            separatorWidth = sepNat + this._list.spacing;
+        }
 
-        let totalSpacing = this._appsBox.spacing * (children.length - 1);
-        alloc.min_size = this._icons.length * maxChildMin + separatorWidth + totalSpacing;
-        alloc.nat_size = this._icons.length * maxChildNat + separatorWidth + totalSpacing;
+        let totalSpacing = this._list.spacing * (this._items.length - 1);
+        alloc.min_size = this._items.length * maxChildMin + separatorWidth + totalSpacing;
+        alloc.nat_size = this._items.length * maxChildNat + separatorWidth + totalSpacing;
     },
 
-    _appsBoxGetPreferredHeight: function (actor, forWidth, alloc) {
-        let children = this._appsBox.get_children();
+    _getPreferredHeight: function (actor, forWidth, alloc) {
         let maxChildMin = 0;
         let maxChildNat = 0;
 
-        for (let i = 0; i < children.length; i++) {
-            let [childMin, childNat] = children[i].get_preferred_height(forWidth);
+        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 = Math.max(childNat, maxChildNat);
         }
@@ -236,176 +488,192 @@ AltTabPopup.prototype = {
         alloc.nat_size = maxChildNat;
     },
 
-    _appsBoxAllocate: function (actor, box, flags) {
-        let children = this._appsBox.get_children();
-        let totalSpacing = this._appsBox.spacing * (children.length - 1);
+    _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)
-            separatorWidth = this._separator.get_preferred_width(childHeight)[0];
+        if (this._separator) {
+            let [sepMin, sepNat] = this._separator.get_preferred_width(childHeight);
+            separatorWidth = sepNat;
+            totalSpacing += this._list.spacing;
+        }
 
-        let childWidth = Math.max(0, box.x2 - box.x1 - totalSpacing) / this._icons.length;
+        let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing - separatorWidth) / this._items.length);
 
-        let x = box.x1;
+        let x = 0;
+        let children = this._list.get_children();
+        let childBox = new Clutter.ActorBox();
         for (let i = 0; i < children.length; i++) {
-            if (children[i] != this._separator) {
+            if (this._items.indexOf(children[i]) != -1) {
                 let [childMin, childNat] = children[i].get_preferred_height(childWidth);
                 let vSpacing = (childHeight - childNat) / 2;
-                let childBox = new Clutter.ActorBox();
                 childBox.x1 = x;
                 childBox.y1 = vSpacing;
                 childBox.x2 = x + childWidth;
-                childBox.y2 = childHeight - vSpacing;
+                childBox.y2 = childBox.y1 + childNat;
                 children[i].allocate(childBox, flags);
-                x += this._appsBox.spacing + childWidth;
-            }
-            else {
+
+                x += this._list.spacing + childWidth;
+            } else if (children[i] == this._separator) {
                 // We want the separator to be more compact than the rest.
-                let childBox = new Clutter.ActorBox();
                 childBox.x1 = x;
                 childBox.y1 = 0;
                 childBox.x2 = x + separatorWidth;
                 childBox.y2 = childHeight;
                 children[i].allocate(childBox, flags);
-                x += this._appsBox.spacing + separatorWidth;
+                x += this._list.spacing + separatorWidth;
+            } else {
+                // Something else, eg, AppSwitcher's arrows;
+                // we don't allocate it.
             }
         }
-    },
-
-    _keyPressEvent : function(actor, event) {
-        let keysym = event.get_key_symbol();
-        let backwards = (event.get_state() & Clutter.ModifierType.SHIFT_MASK);
+    }
+};
 
-        if (keysym == Clutter.Tab)
-            this._updateSelection(backwards ? -1 : 1);
-        else if (keysym == Clutter.Left)
-            this._updateSelection(-1);
-        else if (keysym == Clutter.Right)
-            this._updateSelection(1);
-        else if (keysym == Clutter.grave)
-            this._updateWindowSelection(backwards ? -1 : 1);
-        else if (keysym == Clutter.Up)
-            this._updateWindowSelection(-1);
-        else if (keysym == Clutter.Down)
-            this._updateWindowSelection(1);
-        else if (keysym == Clutter.Escape)
-            this.destroy();
+Signals.addSignalMethods(SwitcherList.prototype);
 
-        return true;
-    },
+function AppSwitcher(apps) {
+    this._init(apps);
+}
 
-    _keyReleaseEvent : function(actor, event) {
-        let keysym = event.get_key_symbol();
+AppSwitcher.prototype = {
+    __proto__ : SwitcherList.prototype,
 
-        if (keysym == Clutter.Alt_L || keysym == Clutter.Alt_R)
-            this._finish();
+    _init : function(apps) {
+        SwitcherList.prototype._init.call(this, true);
 
-        return true;
-    },
+        // Construct the AppIcons, sort by time, add to the popup
+        let activeWorkspace = global.screen.get_active_workspace();
+        let workspaceIcons = [];
+        let otherIcons = [];
+        for (let i = 0; i < apps.length; i++) {
+            let appIcon = new AppIcon.AppIcon({ appInfo: apps[i],
+                                                size: POPUP_APPICON_SIZE });
+            if (this._hasWindowsOnWorkspace(appIcon, activeWorkspace))
+              workspaceIcons.push(appIcon);
+            else
+              otherIcons.push(appIcon);
+        }
 
-    _appClicked : function(icon) {
-        Main.activateWindow(icon.windows[0]);
-        this.destroy();
-    },
+        workspaceIcons.sort(Lang.bind(this, this._sortAppIcon));
+        otherIcons.sort(Lang.bind(this, this._sortAppIcon));
 
-    _windowClicked : function(icon, window) {
-        if (window)
-            Main.activateWindow(window);
-        this.destroy();
-    },
+        this.icons = [];
+        this._arrows = [];
+        for (let i = 0; i < workspaceIcons.length; i++)
+            this._addIcon(workspaceIcons[i]);
+        if (workspaceIcons.length > 0 && otherIcons.length > 0)
+            this.addSeparator();
+        for (let i = 0; i < otherIcons.length; i++)
+            this._addIcon(otherIcons[i]);
 
-    _windowHovered : function(icon, window) {
-        if (window)
-            this._highlightWindow(window);
+        this._shownArrow = -1;
     },
 
-    _mouseMoved : function(actor, event) {
-        if (++this._mouseMovement < POPUP_POINTER_SELECTION_THRESHOLD)
-            return;
-
-        this.actor.disconnect(this._motionEventId);
-        this._mouseActive = true;
-
-        actor = event.get_source();
-        while (actor) {
-            if (actor._delegate instanceof AppIcon.AppIcon) {
-                this._iconEntered(actor, event);
-                return;
-            }
-            actor = actor.get_parent();
+    _allocate: function (actor, box, flags) {
+        // Allocate the main list items
+        SwitcherList.prototype._allocate.call(this, actor, box, flags);
+
+        let arrowHeight = Math.floor(this.actor.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
+        let arrowWidth = arrowHeight * 2;
+
+        // Now allocate each arrow underneath its item
+        let childBox = new Clutter.ActorBox();
+        for (let i = 0; i < this._items.length; i++) {
+            let itemBox = this._items[i].allocation;
+            childBox.x1 = Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
+            childBox.x2 = childBox.x1 + arrowWidth;
+            childBox.y1 = itemBox.y2 + arrowHeight;
+            childBox.y2 = childBox.y1 + arrowHeight;
+            this._arrows[i].allocate(childBox, flags);
         }
     },
 
-    _iconEntered : function(actor, event) {
-        let index = this._icons.indexOf(actor._delegate);
-        if (this._mouseActive)
-            this._updateSelection(index - this._selected);
-    },
+    showArrow : function(n) {
+        if (this._shownArrow != -1)
+            this._arrows[this._shownArrow].hide();
 
-    _finish : function() {
-        if (this._highlightedWindow)
-            Main.activateWindow(this._highlightedWindow);
-        this.destroy();
-    },
+        this._shownArrow = n;
 
-    destroy : function() {
-        this.actor.destroy();
+        if (this._shownArrow != -1)
+            this._arrows[this._shownArrow].show();
     },
 
-    _onDestroy : function() {
-        if (this._haveModal)
-            Main.popModal(this.actor);
-
-        if (this._lightbox)
-            this._lightbox.destroy();
-
-        if (this._keyPressEventId)
-            global.stage.disconnect(this._keyPressEventId);
-        if (this._keyReleaseEventId)
-            global.stage.disconnect(this._keyReleaseEventId);
+    _addIcon : function(appIcon) {
+        this.icons.push(appIcon);
+        this.addItem(appIcon.actor);
+
+        let arrow = new Shell.DrawingArea();
+        arrow.connect('redraw', Lang.bind(this,
+            function (area, texture) {
+                Shell.draw_box_pointer(texture, Shell.PointerDirection.DOWN,
+                                       TRANSPARENT_COLOR,
+                                       POPUP_ARROW_COLOR);
+            }));
+        this._list.add_actor(arrow);
+        this._arrows.push(arrow);
+        arrow.hide();
     },
 
-    _updateSelection : function(delta) {
-        this._icons[this._selected].setHighlight(false);
-        if (delta != 0 && this._selectedMenu)
-                this._selectedMenu.popdown();
-
-        this._selected = (this._selected + this._icons.length + delta) % this._icons.length;
-        this._icons[this._selected].setHighlight(true);
-
-        this._highlightWindow(this._currentWindows[this._selected]);
+    _hasWindowsOnWorkspace: function(appIcon, workspace) {
+        for (let i = 0; i < appIcon.windows.length; i++) {
+            if (appIcon.windows[i].get_workspace() == workspace)
+                return true;
+        }
+        return false;
     },
 
-    _menuPoppedUp : function(icon, menu) {
-        this._selectedMenu = menu;
+    _hasVisibleWindows : function(appIcon) {
+        for (let i = 0; i < appIcon.windows.length; i++) {
+            if (appIcon.windows[i].showing_on_its_workspace())
+                return true;
+        }
+        return false;
     },
 
-    _menuPoppedDown : function(icon, menu) {
-        this._selectedMenu = null;
-    },
+    _sortAppIcon : function(appIcon1, appIcon2) {
+        let vis1 = this._hasVisibleWindows(appIcon1);
+        let vis2 = this._hasVisibleWindows(appIcon2);
 
-    _updateWindowSelection : function(delta) {
-        let icon = this._icons[this._selected];
+        if (vis1 && !vis2) {
+            return -1;
+        } else if (vis2 && !vis1) {
+            return 1;
+        } else {
+            // The app's most-recently-used window is first
+            // in its list
+            return (appIcon2.windows[0].get_user_time() -
+                    appIcon1.windows[0].get_user_time());
+        }
+    }
+};
 
-        if (!this._selectedMenu)
-            icon.popupMenu();
-        if (!this._selectedMenu)
-            return;
+function ThumbnailList(windows) {
+    this._init(windows);
+}
 
-        let next = 0;
-        for (let i = 0; i < icon.windows.length; i++) {
-            if (icon.windows[i] == this._highlightedWindow) {
-                next = (i + icon.windows.length + delta) % icon.windows.length;
-                break;
-            }
+ThumbnailList.prototype = {
+    __proto__ : SwitcherList.prototype,
+
+    _init : function(windows) {
+        SwitcherList.prototype._init.call(this);
+
+        for (let i = 0; i < windows.length; i++) {
+            let mutterWindow = windows[i].get_compositor_private();
+            let windowTexture = mutterWindow.get_texture ();
+            let [width, height] = windowTexture.get_size();
+            let scale = Math.min(1.0, THUMBNAIL_SIZE / width, THUMBNAIL_SIZE / height);
+
+            let clone = new Clutter.Clone ({ source: windowTexture,
+                                             reactive: true,
+                                             width: width * scale,
+                                             height: height * scale });
+            let box = new Big.Box({ padding: AppIcon.APPICON_BORDER_WIDTH + AppIcon.APPICON_PADDING });
+            box.append(clone, Big.BoxPackFlags.NONE);
+            this.addItem(box);
         }
-        this._selectedMenu.selectWindow(icon.windows[next]);
-    },
-
-    _highlightWindow : function(metaWin) {
-        this._highlightedWindow = metaWin;
-        this._currentWindows[this._selected] = metaWin;
-        this._lightbox.highlight(this._highlightedWindow.get_compositor_private());
     }
 };



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