From 367eaf91613607dd7834ab23aeed3bc4774a98d8 Mon Sep 17 00:00:00 2001 From: Adel Gadllah Date: Wed, 17 Mar 2010 18:31:03 +0100 Subject: [PATCH] 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 | 320 ++++++++++++++++++++++++++++++------- 2 files changed, 281 insertions(+), 56 deletions(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 4068a27b3..3eb74329c 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -702,6 +702,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; @@ -714,7 +730,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 7c8eb042f..fb0acc696 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; + 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 - this.actor.get_theme_node().get_horizontal_padding(); + 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,69 @@ 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) { + if (!this._thumbnailBins.length) + return; + 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); + let [found, spacing] = this._items[0].child.get_theme_node().get_length('spacing', false); + if (!found) + spacing = 0; + + availHeight = Math.min(availHeight - labelNaturalHeight - totalPadding - spacing, THUMBNAIL_DEFAULT_SIZE); + let binHeight = availHeight + this._items[0].get_theme_node().get_vertical_padding() + this.actor.get_theme_node().get_vertical_padding() - spacing; + 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); + this._thumbnailBins[i].add_actor(clone); + this._clones.push(clone); + } + + // Make sure we only do this once + this._thumbnailBins = new Array(); } };