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