From d7af6d40e38b2c0ebc03a916f06574efd641dd2d Mon Sep 17 00:00:00 2001 From: Dan Winship Date: Fri, 2 Oct 2009 11:02:46 -0400 Subject: [PATCH] [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 | 15 +- js/ui/altTab.js | 872 ++++++++++++++++++++++++------------- 2 files changed, 583 insertions(+), 304 deletions(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index a67132124..42097fe8d 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: 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 f3d3d6a1a..a43731355 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 = new Clutter.Group({ reactive: true, + x: 0, + y: 0, + width: global.screen_width, + height: global.screen_height }); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - // 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)); - - 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._icons = []; - this._separator = null; - this._currentWindows = []; this._haveModal = false; - this._selected = 0; - this._highlightedWindow = null; - this._toplevels = global.window_group.get_children(); + + this._currentApp = 0; + this._currentWindows = []; + 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,13 +76,482 @@ AltTabPopup.prototype = { this._mouseActive = false; this._mouseMovement = 0; - // Contruct the AppIcons, sort by time, add to the popup + 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._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 updating + // selection.) + let mods = global.get_modifier_keys(); + if (!(mods & Gdk.ModifierType.MOD1_MASK)) { + this._finish(); + return false; + } + + return true; + }, + + _nextApp : function() { + return mod(this._currentApp + 1, this._appIcons.length); + }, + _previousApp : function() { + return mod(this._currentApp - 1, this._appIcons.length); + }, + + _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 true; + }, + + _keyReleaseEvent : function(actor, event) { + let keysym = event.get_key_symbol(); + + 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 { + 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'; + + 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 < 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.nat_size = this._items.length * maxChildNat + separatorWidth + totalSpacing; + }, + + _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 = Math.max(childNat, maxChildNat); + } + + alloc.min_size = maxChildMin; + alloc.nat_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(); + 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 AppSwitcher(apps) { + this._init(apps); +} + +AppSwitcher.prototype = { + __proto__ : SwitcherList.prototype, + + _init : function(apps) { + SwitcherList.prototype._init.call(this, 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], - menuType: AppIcon.MenuType.BELOW }); + size: POPUP_APPICON_SIZE }); if (this._hasWindowsOnWorkspace(appIcon, activeWorkspace)) workspaceIcons.push(appIcon); else @@ -120,51 +561,61 @@ AltTabPopup.prototype = { workspaceIcons.sort(Lang.bind(this, this._sortAppIcon)); otherIcons.sort(Lang.bind(this, this._sortAppIcon)); + 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(); + 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._shownArrow = -1; + }, - this.actor.show_all(); + _allocate: function (actor, box, flags) { + // Allocate the main list items + SwitcherList.prototype._allocate.call(this, actor, box, flags); - 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); + let arrowHeight = Math.floor(this.actor.get_theme_node().get_padding(St.Side.BOTTOM) / 3); + let arrowWidth = arrowHeight * 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); + // 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); } + }, - // 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.) - let mods = global.get_modifier_keys(); - if (!(mods & Gdk.ModifierType.MOD1_MASK)) { - this._finish(); - return false; - } + showArrow : function(n) { + if (this._shownArrow != -1) + this._arrows[this._shownArrow].hide(); - return true; + this._shownArrow = n; + + if (this._shownArrow != -1) + this._arrows[this._shownArrow].show(); + }, + + _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(); }, _hasWindowsOnWorkspace: function(appIcon, workspace) { @@ -197,215 +648,32 @@ AltTabPopup.prototype = { return (appIcon2.windows[0].get_user_time() - appIcon1.windows[0].get_user_time()); } - }, - - _appsBoxGetPreferredWidth: function (actor, forHeight, alloc) { - let children = this._appsBox.get_children(); - 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); - maxChildMin = Math.max(childMin, maxChildMin); - maxChildNat = Math.max(childNat, maxChildNat); - } - } - - let separatorWidth = 0; - if (this._separator) - separatorWidth = this._separator.get_preferred_width(forHeight)[0]; - - 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; - }, - - _appsBoxGetPreferredHeight: function (actor, forWidth, alloc) { - let children = this._appsBox.get_children(); - let maxChildMin = 0; - let maxChildNat = 0; - - for (let i = 0; i < children.length; i++) { - let [childMin, childNat] = children[i].get_preferred_height(forWidth); - maxChildMin = Math.max(childMin, maxChildMin); - maxChildNat = Math.max(childNat, maxChildNat); - } - - alloc.min_size = maxChildMin; - alloc.nat_size = maxChildNat; - }, - - _appsBoxAllocate: function (actor, box, flags) { - let children = this._appsBox.get_children(); - let totalSpacing = this._appsBox.spacing * (children.length - 1); - let childHeight = box.y2 - box.y1; - - let separatorWidth = 0; - if (this._separator) - separatorWidth = this._separator.get_preferred_width(childHeight)[0]; - - let childWidth = Math.max(0, box.x2 - box.x1 - totalSpacing) / this._icons.length; - - let x = box.x1; - for (let i = 0; i < children.length; i++) { - if (children[i] != this._separator) { - 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; - children[i].allocate(childBox, flags); - x += this._appsBox.spacing + childWidth; - } - else { - // 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; - } - } - }, - - _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(); - - return true; - }, - - _keyReleaseEvent : function(actor, event) { - let keysym = event.get_key_symbol(); - - if (keysym == Clutter.Alt_L || keysym == Clutter.Alt_R) - this._finish(); - - return true; - }, - - _appClicked : function(icon) { - Main.activateWindow(icon.windows[0]); - this.destroy(); - }, - - _windowClicked : function(icon, window) { - if (window) - Main.activateWindow(window); - this.destroy(); - }, - - _windowHovered : function(icon, window) { - if (window) - this._highlightWindow(window); - }, - - _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(); - } - }, - - _iconEntered : function(actor, event) { - let index = this._icons.indexOf(actor._delegate); - if (this._mouseActive) - this._updateSelection(index - this._selected); - }, - - _finish : function() { - if (this._highlightedWindow) - Main.activateWindow(this._highlightedWindow); - this.destroy(); - }, - - destroy : function() { - this.actor.destroy(); - }, - - _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); - }, - - _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]); - }, - - _menuPoppedUp : function(icon, menu) { - this._selectedMenu = menu; - }, - - _menuPoppedDown : function(icon, menu) { - this._selectedMenu = null; - }, - - _updateWindowSelection : function(delta) { - let icon = this._icons[this._selected]; - - if (!this._selectedMenu) - icon.popupMenu(); - if (!this._selectedMenu) - return; - - 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; - } - } - 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()); + } +}; + +function ThumbnailList(windows) { + this._init(windows); +} + +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); + } } };