From 2b7d8accc00e690799ea6dc68f37c190a64f1566 Mon Sep 17 00:00:00 2001
From: Adel Gadllah <adel.gadllah@gmail.com>
Date: Sat, 13 Mar 2010 22:54:51 +0100
Subject: [PATCH] [AppSwitcher] Prevent from being partly offscreen

Limit the AppSwitcher to the screen size by either downscaling or
scrolling.

We scale the icons down up from 96, 64, 48, 32 to 22 and start scrolling
if we still fail to fit on screen.

The thumbnail box is shifted to either left or right, when failing to
fit we scroll here to.
To prevent from being offscreen at the buttom we adjust the thumbnail
height to fit.

The old positioning logic is replaced with a ShellGenericContainer to
implement a custom allocation system.

https://bugzilla.gnome.org/show_bug.cgi?id=597983
---
 data/theme/gnome-shell.css |   17 +++-
 js/ui/altTab.js            |  315 ++++++++++++++++++++++++++++++++++++--------
 2 files changed, 276 insertions(+), 56 deletions(-)

diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index b5cce5c..5d2d665 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -675,6 +675,22 @@ StTooltip {
     color: white;
 }
 
+.thumbnail-scroll-gradient-left {
+    background-gradient-direction: horizontal;
+    background-gradient-start: rgba(51, 51, 51, 1.0);
+    background-gradient-end: rgba(51, 51, 51, 0);
+    border-radius: 8px;
+    width: 60px;
+}
+
+.thumbnail-scroll-gradient-right {
+    background-gradient-direction: horizontal;
+    background-gradient-start: rgba(51, 51, 51, 0);
+    background-gradient-end: rgba(51, 51, 51, 1.0);
+    border-radius: 8px;
+    width: 60px;
+}
+
 .switcher-list .item-box {
     padding: 8px;
     border-radius: 4px;
@@ -687,7 +703,6 @@ StTooltip {
 
 .switcher-list .thumbnail {
     width:  256px;
-    height: 256px;
 }
 
 .switcher-list .outlined-item-box {
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index 865e1f0..fb0bd01 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -23,13 +23,16 @@ TRANSPARENT_COLOR.from_pixel(0x00000000);
 
 const POPUP_APPICON_SIZE = 96;
 const POPUP_LIST_SPACING = 8;
+const POPUP_SCROLL_TIME = 0.10; // seconds
 
 const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
 
-const THUMBNAIL_SIZE = 256;
+const THUMBNAIL_DEFAULT_SIZE = 256;
 const THUMBNAIL_POPUP_TIME = 500; // milliseconds
 const THUMBNAIL_FADE_TIME = 0.2; // seconds
 
+const iconSizes = [96, 64, 48, 32, 22];
+
 function mod(a, b) {
     return (a + b) % b;
 }
@@ -40,11 +43,11 @@ function AltTabPopup() {
 
 AltTabPopup.prototype = {
     _init : function() {
-        this.actor = new Clutter.Group({ reactive: true,
-                                         x: 0,
-                                         y: 0,
-                                         width: global.screen_width,
-                                         height: global.screen_height });
+        this.actor = new Shell.GenericContainer({ reactive: true });
+
+        this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
+        this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
+        this.actor.connect('allocate', Lang.bind(this, this._allocate));
 
         this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
 
@@ -62,6 +65,55 @@ AltTabPopup.prototype = {
         global.stage.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 focus = global.get_focus_monitor();
+
+        // 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(focus.width - POPUP_LIST_SPACING * 2);
+        let [childMinWidth, childNaturalWidth] = this._appSwitcher.actor.get_preferred_width(childNaturalHeight);
+        childBox.x1 = Math.max(POPUP_LIST_SPACING, focus.x + Math.floor((focus.width - childNaturalWidth) / 2));
+        childBox.x2 = Math.min(childBox.x1 + focus.width - POPUP_LIST_SPACING * 2, childBox.x1 + childNaturalWidth);
+        childBox.y1 = focus.y + Math.floor((focus.height - childNaturalHeight) / 2);
+        childBox.y2 = childBox.y1 + childNaturalHeight;
+        this._appSwitcher.actor.allocate(childBox, 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 [posX, posY] = icon.get_transformed_position();
+            let thumbnailCenter = posX + icon.width / 2;
+            let [childMinWidth, childNaturalWidth] = this._thumbnails.actor.get_preferred_width(-1);
+            childBox.x1 = Math.max(POPUP_LIST_SPACING, Math.floor(thumbnailCenter - childNaturalWidth / 2));
+            if (childBox.x1 + childNaturalWidth > focus.width - POPUP_LIST_SPACING * 2) {
+                let offset = childBox.x1 + childNaturalWidth - focus.width + POPUP_LIST_SPACING * 2;
+                childBox.x1 = Math.max(POPUP_LIST_SPACING, childBox.x1 - offset - POPUP_LIST_SPACING * 2);
+            }
+
+            childBox.x2 = childBox.x1 +  childNaturalWidth;
+            if (childBox.x2 > focus.width - POPUP_LIST_SPACING)
+                childBox.x2 = focus.width - POPUP_LIST_SPACING;
+            childBox.y1 = this._appSwitcher.actor.allocation.y2 + POPUP_LIST_SPACING * 2;
+            this._thumbnails.addClones(focus.height - POPUP_LIST_SPACING - childBox.y1);
+            let [childMinHeight, childNaturalHeight] = this._thumbnails.actor.get_preferred_height(-1);
+            childBox.y2 = childBox.y1 + childNaturalHeight;
+            this._thumbnails.actor.allocate(childBox, flags);
+        }
+    },
+
     show : function(backward) {
         let tracker = Shell.WindowTracker.get_default();
         let apps = tracker.get_running_apps ("");
@@ -84,10 +136,6 @@ AltTabPopup.prototype = {
         this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated));
         this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered));
 
-        let focus = global.get_focus_monitor();
-        this._appSwitcher.actor.x = focus.x + Math.floor((focus.width - this._appSwitcher.actor.width) / 2);
-        this._appSwitcher.actor.y = focus.y + Math.floor((focus.height - this._appSwitcher.actor.height) / 2);
-
         this._appIcons = this._appSwitcher.icons;
 
         // Make the initial selection
@@ -289,6 +337,9 @@ AltTabPopup.prototype = {
         if (this._haveModal)
             Main.popModal(this.actor);
 
+        if (this._thumbnails)
+            this._destroyThumbnails();
+
         if (this._keyPressEventId)
             global.stage.disconnect(this._keyPressEventId);
         if (this._keyReleaseEventId)
@@ -375,33 +426,6 @@ AltTabPopup.prototype = {
 
         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 focus = global.get_focus_monitor();
-            thumbnailCenter = focus.x + focus.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;
-
         this._thumbnails.actor.opacity = 0;
         Tweener.addTween(this._thumbnails.actor,
                          { opacity: 255,
@@ -417,7 +441,7 @@ function SwitcherList(squareItems) {
 
 SwitcherList.prototype = {
     _init : function(squareItems) {
-        this.actor = new St.Bin({ style_class: 'switcher-list' });
+        this.actor = new St.BoxLayout({ style_class: 'switcher-list' });
 
         // Here we use a GenericContainer so that we can force all the
         // children except the separator to have the same width.
@@ -428,12 +452,36 @@ SwitcherList.prototype = {
         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._clipBin = new St.Bin({style_class: 'cbin'});
+        this._clipBin.child = this._list;
+        this.actor.add_actor(this._clipBin);
+
+        this._leftGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-left', vertical: true});
+        this._rightGradient = new St.BoxLayout({style_class: 'thumbnail-scroll-gradient-right', vertical: true});
+        this.actor.add_actor(this._leftGradient);
+        this.actor.add_actor(this._rightGradient);
+
+        // Those arrows indicate whether scrolling in one direction is possible
+        this._leftArrow = new St.DrawingArea();
+        this._leftArrow.connect('repaint', Lang.bind(this,
+                                            function (area) {
+                                                Shell.draw_box_pointer(area, Shell.PointerDirection.LEFT, TRANSPARENT_COLOR, POPUP_ARROW_COLOR);
+                                            }));
+
+        this._rightArrow = new St.DrawingArea();
+        this._rightArrow.connect('repaint', Lang.bind(this,
+                                            function (area) {
+                                                Shell.draw_box_pointer(area, Shell.PointerDirection.RIGHT, TRANSPARENT_COLOR, POPUP_ARROW_COLOR);
+                                            }));
+
+        this._leftGradient.add_actor(this._leftArrow);
+        this._rightGradient.add_actor(this._rightArrow);
 
         this._items = [];
         this._highlighted = -1;
         this._separator = null;
         this._squareItems = squareItems;
+        this._scrollable = false;
     },
 
     addItem : function(item) {
@@ -472,6 +520,45 @@ SwitcherList.prototype = {
             else
                 this._items[this._highlighted].style_class = 'selected-item-box';
         }
+
+        let monitor = global.get_focus_monitor();
+        let itemSize = this._items[index].allocation.x2 - this._items[index].allocation.x1;
+        let [posX, posY] = this._items[index].get_transformed_position();
+        posX += this.actor.x;
+
+        if (posX + itemSize > monitor.width)
+            this._scrollToRight();
+        else if (posX < 0)
+            this._scrollToLeft();
+
+    },
+
+    _scrollToLeft : function() {
+        let x = this._items[this._highlighted].allocation.x1;
+        this._rightGradient.show();
+        Tweener.addTween(this._list, { anchor_x: x,
+                                        time: POPUP_SCROLL_TIME,
+                                        transition: 'easeOutQuad',
+                                        onComplete: Lang.bind(this, function () {
+                                                                        if (this._highlighted == 0)
+                                                                            this._leftGradient.hide();
+                                                             })
+                        });
+    },
+
+    _scrollToRight : function() {
+        let monitor = global.get_focus_monitor();
+        let padding = this.actor.get_theme_node().get_horizontal_padding();
+        let x = this._items[this._highlighted].allocation.x2 - monitor.width + padding + POPUP_LIST_SPACING * 2;
+        this._leftGradient.show();
+        Tweener.addTween(this._list, { anchor_x: x,
+                                        time: POPUP_SCROLL_TIME,
+                                        transition: 'easeOutQuad',
+                                        onComplete: Lang.bind(this, function () {
+                                                                        if (this._highlighted == this._items.length - 1)
+                                                                            this._rightGradient.hide();
+                                                             })
+                        });
     },
 
     _itemActivated: function(n) {
@@ -553,6 +640,15 @@ SwitcherList.prototype = {
         let x = 0;
         let children = this._list.get_children();
         let childBox = new Clutter.ActorBox();
+
+        let focus = global.get_focus_monitor();
+        if (this.actor.allocation.x2 == focus.width - POPUP_LIST_SPACING) {
+            if (this._squareItems)
+                childWidth = childHeight;
+            else
+                childWidth = children[0].get_preferred_width(childHeight)[0];
+        }
+
         for (let i = 0; i < children.length; i++) {
             if (this._items.indexOf(children[i]) != -1) {
                 let [childMin, childNat] = children[i].get_preferred_height(childWidth);
@@ -577,6 +673,44 @@ SwitcherList.prototype = {
                 // we don't allocate it.
             }
         }
+
+        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 topPadding = this.actor.get_theme_node().get_padding(St.Side.TOP);
+        let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM);
+
+        // Show the arrows and gradients when scrolling is needed
+        if (children[children.length - 1].allocation.x2 > this.actor.width - leftPadding - rightPadding && !this._scrollable) {
+            this._leftGradient.set_height(this.actor.height);
+            this._leftGradient.x = this.actor.x;
+            this._leftGradient.y = this.actor.y;
+
+            this._rightGradient.set_height(this.actor.height);
+            this._rightGradient.x = this.actor.x + (this.actor.allocation.x2 - this.actor.allocation.x1) - this._rightGradient.width + POPUP_LIST_SPACING / 2;
+            this._rightGradient.y = this.actor.y;
+
+            let arrowWidth = Math.floor(leftPadding / 3);
+            let arrowHeight = arrowWidth * 2;
+            this._leftArrow.set_size(arrowWidth, arrowHeight);
+            this._leftArrow.set_position(POPUP_LIST_SPACING, this.actor.height / 2 - arrowWidth);
+
+            arrowWidth = Math.floor(rightPadding / 3);
+            arrowHeight = arrowWidth * 2;
+            this._rightArrow.set_size(arrowWidth, arrowHeight);
+            this._rightArrow.set_position(this._rightGradient.width - arrowHeight, this.actor.height / 2 - arrowWidth);
+
+            this._scrollable = true;
+
+            this._leftGradient.hide();
+            this._rightGradient.show();
+        }
+        else if (!this._scrollable){
+            this._leftGradient.hide();
+            this._rightGradient.hide();
+        }
+
+        // Clip the area for scrolling
+        this._clipBin.set_clip(0, -topPadding, (this.actor.allocation.x2 - this.actor.allocation.x1) - leftPadding - rightPadding, this.actor.height + bottomPadding);
     }
 };
 
@@ -591,13 +725,18 @@ AppIcon.prototype = {
         this.app = app;
         this.actor = new St.BoxLayout({ style_class: "alt-tab-app",
                                          vertical: true });
-        this._icon = this.app.create_icon_texture(POPUP_APPICON_SIZE);
-        let iconBin = new St.Bin({height: POPUP_APPICON_SIZE, width: POPUP_APPICON_SIZE});
-        iconBin.child = this._icon;
+        this.icon = null;
+        this._iconBin = new St.Bin();
 
-        this.actor.add(iconBin, { x_fill: false, y_fill: false } );
-        this._label = new St.Label({ text: this.app.get_name() });
-        this.actor.add(this._label, { x_fill: false });
+        this.actor.add(this._iconBin, { x_fill: false, y_fill: false } );
+        this.label = new St.Label({ text: this.app.get_name() });
+        this.actor.add(this.label, { x_fill: false });
+    },
+
+    set_size: function(size) {
+        this.icon = this.app.create_icon_texture(size);
+        this._iconBin.set_size(size, size);
+        this._iconBin.child = this.icon;
     }
 }
 
@@ -639,9 +778,50 @@ AppSwitcher.prototype = {
             this._addIcon(otherIcons[i]);
 
         this._curApp = -1;
+        this._iconSize = 0;
+    },
+
+    _getPreferredHeight: function (actor, forWidth, alloc) {
+        let j = 0;
+        while(this._items.length > 1 && this._items[j].style_class != 'item-box') {
+                j++;
+        }
+        let iconPadding = this._items[j].get_theme_node().get_horizontal_padding();
+        let [iconMinHeight, iconNaturalHeight] = this.icons[j].label.get_preferred_height(-1);
+        let iconSpacing = iconNaturalHeight + iconPadding;
+        let totalSpacing = this._list.spacing * (this._items.length - 1);
+        if (this._separator)
+           totalSpacing += this._separator.width + this._list.spacing;
+
+        // We just assume the whole screen here due to weirdness happing with the passed width
+        let focus = global.get_focus_monitor();
+        let availWidth = focus.width - POPUP_LIST_SPACING * 2;
+        let height = 0;
+
+        for(let i =  0; i < iconSizes.length; i++) {
+                this._iconSize = iconSizes[i];
+                height = iconSizes[i] + iconSpacing;
+                let w = height * this._items.length + totalSpacing;
+                if (w <= availWidth)
+                        break;
+        }
+
+        if (this._items.length == 1) {
+            this._iconSize = iconSizes[0];
+            height = iconSizes[0] + iconSpacing;
+        }
+
+        alloc.min_size = height;
+        alloc.natural_size = height;
     },
 
     _allocate: function (actor, box, flags) {
+        for(let i = 0; i < this.icons.length; i++) {
+            if (this.icons[i].icon != null)
+                break;
+            this.icons[i].set_size(this._iconSize);
+        }
+
         // Allocate the main list items
         SwitcherList.prototype._allocate.call(this, actor, box, flags);
 
@@ -739,39 +919,64 @@ ThumbnailList.prototype = {
         // that case.
         let separatorAdded = windows.length == 0 || windows[0].get_workspace() != activeWorkspace;
 
+        this._labels = new Array();
+        this._thumbnailBins = new Array();
+        this._clones = new Array();
+        this._windows = windows;
+
         for (let i = 0; i < windows.length; i++) {
             if (!separatorAdded && windows[i].get_workspace() != activeWorkspace) {
               this.addSeparator();
               separatorAdded = true;
             }
 
-            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 box = new St.BoxLayout({ style_class: "thumbnail-box",
                                          vertical: true });
 
             let bin = new St.Bin({ style_class: "thumbnail" });
-            let clone = new Clutter.Clone ({ source: windowTexture,
-                                             reactive: true,
-                                             width: width * scale,
-                                             height: height * scale });
 
-            bin.add_actor(clone);
             box.add_actor(bin);
+            this._thumbnailBins.push(bin);
 
             let title = windows[i].get_title();
             if (title) {
                 let name = new St.Label({ text: title });
                 // St.Label doesn't support text-align so use a Bin
                 let bin = new St.Bin({ x_align: St.Align.MIDDLE });
+                this._labels.push(bin);
                 bin.add_actor(name);
                 box.add_actor(bin);
             }
 
             this.addItem(box);
         }
+    },
+
+    addClones : function (availHeight) {
+        let totalPadding = this._items[0].get_theme_node().get_horizontal_padding() + this._items[0].get_theme_node().get_vertical_padding();
+        totalPadding += this.actor.get_theme_node().get_horizontal_padding() + this.actor.get_theme_node().get_vertical_padding();
+        let [labelMinHeight, labelNaturalHeight] = this._labels[0].get_preferred_height(-1);
+        availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - 4, THUMBNAIL_DEFAULT_SIZE);
+        let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.actor.get_theme_node().get_vertical_padding() - 4;
+        binHeight = Math.min(THUMBNAIL_DEFAULT_SIZE, binHeight);
+
+        for (let i = 0; i < this._thumbnailBins.length; i++) {
+            let mutterWindow = this._windows[i].get_compositor_private();
+            let windowTexture = mutterWindow.get_texture ();
+            let [width, height] = windowTexture.get_size();
+            let scale = Math.min(1.0, THUMBNAIL_DEFAULT_SIZE / width, availHeight / height);
+            let clone = new Clutter.Clone ({ source: windowTexture,
+                                                reactive: true,
+                                                width: width * scale,
+                                                height: height * scale });
+
+            this._thumbnailBins[i].set_height(binHeight);
+            log(availHeight);
+            this._thumbnailBins[i].add_actor(clone);
+            this._clones.push(clone);
+        }
+
+        // Make sure we only do this once
+        this._thumbnailBins = new Array();
     }
 };
-- 
1.6.6.1

