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

https://bugzilla.gnome.org/show_bug.cgi?id=590563
This commit is contained in:
Dan Winship 2009-10-02 11:02:46 -04:00
parent 90ebcd32e3
commit d7af6d40e3
2 changed files with 583 additions and 304 deletions

View File

@ -157,9 +157,20 @@ StScrollBar StButton#vhandle:hover
} }
/* App Switcher */ /* App Switcher */
#appSwitcher { .switcher-list {
background: rgba(0,0,0,0.8); background: rgba(0,0,0,0.8);
border: 1px solid rgba(128,128,128,0.40); border: 1px solid rgba(128,128,128,0.40);
border-radius: 8px; 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);
} }

View File

@ -4,88 +4,60 @@ const Big = imports.gi.Big;
const Clutter = imports.gi.Clutter; const Clutter = imports.gi.Clutter;
const Gdk = imports.gi.Gdk; const Gdk = imports.gi.Gdk;
const Lang = imports.lang; const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Meta = imports.gi.Meta; const Meta = imports.gi.Meta;
const Pango = imports.gi.Pango; const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell; const Shell = imports.gi.Shell;
const Signals = imports.signals;
const St = imports.gi.St; const St = imports.gi.St;
const AppIcon = imports.ui.appIcon; const AppIcon = imports.ui.appIcon;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main; const Main = imports.ui.main;
const POPUP_APPICON_BORDER_COLOR = new Clutter.Color(); const POPUP_ARROW_COLOR = new Clutter.Color();
POPUP_APPICON_BORDER_COLOR.from_pixel(0xffffffff); POPUP_ARROW_COLOR.from_pixel(0xffffffff);
const POPUP_APPICON_SEPARATOR_COLOR = new Clutter.Color(); const TRANSPARENT_COLOR = new Clutter.Color();
POPUP_APPICON_SEPARATOR_COLOR.from_pixel(0x80808066); 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 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() { function AltTabPopup() {
this._init(); this._init();
} }
AltTabPopup.prototype = { AltTabPopup.prototype = {
_init : function() { _init : function() {
this.actor = new St.Bin({ name: 'appSwitcher', this.actor = new Clutter.Group({ reactive: true,
reactive: true }); x: 0,
y: 0,
width: global.screen_width,
height: global.screen_height });
this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); 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._haveModal = false;
this._selected = 0;
this._highlightedWindow = null; this._currentApp = 0;
this._toplevels = global.window_group.get_children(); this._currentWindows = [];
this._thumbnailTimeoutId = 0;
global.stage.add_actor(this.actor); 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) { show : function(backward) {
let appMonitor = Shell.AppMonitor.get_default(); let appMonitor = Shell.AppMonitor.get_default();
let apps = appMonitor.get_running_apps (""); let apps = appMonitor.get_running_apps ("");
@ -104,13 +76,482 @@ AltTabPopup.prototype = {
this._mouseActive = false; this._mouseActive = false;
this._mouseMovement = 0; 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 activeWorkspace = global.screen.get_active_workspace();
let workspaceIcons = []; let workspaceIcons = [];
let otherIcons = []; let otherIcons = [];
for (let i = 0; i < apps.length; i++) { for (let i = 0; i < apps.length; i++) {
let appIcon = new AppIcon.AppIcon({ appInfo: apps[i], let appIcon = new AppIcon.AppIcon({ appInfo: apps[i],
menuType: AppIcon.MenuType.BELOW }); size: POPUP_APPICON_SIZE });
if (this._hasWindowsOnWorkspace(appIcon, activeWorkspace)) if (this._hasWindowsOnWorkspace(appIcon, activeWorkspace))
workspaceIcons.push(appIcon); workspaceIcons.push(appIcon);
else else
@ -120,51 +561,61 @@ AltTabPopup.prototype = {
workspaceIcons.sort(Lang.bind(this, this._sortAppIcon)); workspaceIcons.sort(Lang.bind(this, this._sortAppIcon));
otherIcons.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++) for (let i = 0; i < workspaceIcons.length; i++)
this._addIcon(workspaceIcons[i]); this._addIcon(workspaceIcons[i]);
if (workspaceIcons.length > 0 && otherIcons.length > 0) if (workspaceIcons.length > 0 && otherIcons.length > 0)
this._addSeparator(); this.addSeparator();
for (let i = 0; i < otherIcons.length; i++) for (let i = 0; i < otherIcons.length; i++)
this._addIcon(otherIcons[i]); this._addIcon(otherIcons[i]);
// Need to specify explicit width and height because the this._shownArrow = -1;
// 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(); _allocate: function (actor, box, flags) {
// Allocate the main list items
SwitcherList.prototype._allocate.call(this, actor, box, flags);
let primary = global.get_primary_monitor(); let arrowHeight = Math.floor(this.actor.get_theme_node().get_padding(St.Side.BOTTOM) / 3);
this.actor.x = primary.x + Math.floor((primary.width - this.actor.width) / 2); let arrowWidth = arrowHeight * 2;
this.actor.y = primary.y + Math.floor((primary.height - this.actor.height) / 2);
if (!backward && this._icons[this._selected].windows.length > 1) { // Now allocate each arrow underneath its item
let candidateWindow = this._icons[this._selected].windows[1]; let childBox = new Clutter.ActorBox();
if (candidateWindow.get_workspace() == activeWorkspace) { for (let i = 0; i < this._items.length; i++) {
this._currentWindows[this._selected] = candidateWindow; let itemBox = this._items[i].allocation;
this._updateSelection(0); childBox.x1 = Math.floor(itemBox.x1 + (itemBox.x2 - itemBox.x1 - arrowWidth) / 2);
} childBox.x2 = childBox.x1 + arrowWidth;
else { childBox.y1 = itemBox.y2 + arrowHeight;
this._updateSelection(1); childBox.y2 = childBox.y1 + arrowHeight;
} this._arrows[i].allocate(childBox, flags);
}
else {
this._updateSelection(backward ? -1 : 1);
} }
},
// There's a race condition; if the user released Alt before showArrow : function(n) {
// we got the grab, then we won't be notified. (See if (this._shownArrow != -1)
// https://bugzilla.gnome.org/show_bug.cgi?id=596695 for this._arrows[this._shownArrow].hide();
// 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;
}
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) { _hasWindowsOnWorkspace: function(appIcon, workspace) {
@ -197,215 +648,32 @@ AltTabPopup.prototype = {
return (appIcon2.windows[0].get_user_time() - return (appIcon2.windows[0].get_user_time() -
appIcon1.windows[0].get_user_time()); appIcon1.windows[0].get_user_time());
} }
}, }
};
_appsBoxGetPreferredWidth: function (actor, forHeight, alloc) {
let children = this._appsBox.get_children(); function ThumbnailList(windows) {
let maxChildMin = 0; this._init(windows);
let maxChildNat = 0; }
for (let i = 0; i < children.length; i++) { ThumbnailList.prototype = {
if (children[i] != this._separator) { __proto__ : SwitcherList.prototype,
let [childMin, childNat] = children[i].get_preferred_width(forHeight);
maxChildMin = Math.max(childMin, maxChildMin); _init : function(windows) {
maxChildNat = Math.max(childNat, maxChildNat); SwitcherList.prototype._init.call(this);
}
} for (let i = 0; i < windows.length; i++) {
let mutterWindow = windows[i].get_compositor_private();
let separatorWidth = 0; let windowTexture = mutterWindow.get_texture ();
if (this._separator) let [width, height] = windowTexture.get_size();
separatorWidth = this._separator.get_preferred_width(forHeight)[0]; let scale = Math.min(1.0, THUMBNAIL_SIZE / width, THUMBNAIL_SIZE / height);
let totalSpacing = this._appsBox.spacing * (children.length - 1); let clone = new Clutter.Clone ({ source: windowTexture,
alloc.min_size = this._icons.length * maxChildMin + separatorWidth + totalSpacing; reactive: true,
alloc.nat_size = this._icons.length * maxChildNat + separatorWidth + totalSpacing; width: width * scale,
}, height: height * scale });
let box = new Big.Box({ padding: AppIcon.APPICON_BORDER_WIDTH + AppIcon.APPICON_PADDING });
_appsBoxGetPreferredHeight: function (actor, forWidth, alloc) { box.append(clone, Big.BoxPackFlags.NONE);
let children = this._appsBox.get_children(); this.addItem(box);
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());
} }
}; };