[gnome-shell] [AppSwitcher] misc refinements
- From: Dan Winship <danw src gnome org>
- To: svn-commits-list gnome org
- Cc:
- Subject: [gnome-shell] [AppSwitcher] misc refinements
- Date: Thu, 22 Oct 2009 16:14:56 +0000 (UTC)
commit 8fd00ac6cc7fef8d451fa4bc6bcb4679125e7bd4
Author: Dan Winship <danw gnome org>
Date: Fri Oct 9 09:43:31 2009 -0400
[AppSwitcher] misc refinements
https://bugzilla.gnome.org/show_bug.cgi?id=597900
data/theme/gnome-shell.css | 16 ++-
js/ui/altTab.js | 341 ++++++++++++++++++++++++++++----------------
2 files changed, 231 insertions(+), 126 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index dfabaac..3e89954 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -162,6 +162,9 @@ StScrollBar StButton#vhandle:hover
border: 1px solid rgba(128,128,128,0.40);
border-radius: 8px;
padding: 18px;
+
+ font: 12px sans-serif;
+ color: white;
}
.switcher-list .item-box {
@@ -169,6 +172,17 @@ StScrollBar StButton#vhandle:hover
border-radius: 4px;
}
+.switcher-list .thumbnail-box {
+ padding: 2px;
+ spacing: 4px;
+}
+
+.switcher-list .outlined-item-box {
+ padding: 6px;
+ border: 2px solid rgba(85,85,85,1.0);
+ border-radius: 4px;
+}
+
.switcher-list .selected-item-box {
padding: 8px;
border-radius: 4px;
@@ -178,4 +192,4 @@ StScrollBar StButton#vhandle:hover
.switcher-list .separator {
width: 1px;
background: rgba(255,255,255,0.33);
-}
+}
\ No newline at end of file
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index 570b594..da4c996 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -13,21 +13,23 @@ const St = imports.gi.St;
const AppIcon = imports.ui.appIcon;
const Main = imports.ui.main;
+const Tweener = imports.ui.tweener;
const POPUP_ARROW_COLOR = new Clutter.Color();
POPUP_ARROW_COLOR.from_pixel(0xffffffff);
+const POPUP_UNFOCUSED_ARROW_COLOR = new Clutter.Color();
+POPUP_UNFOCUSED_ARROW_COLOR.from_pixel(0x808080ff);
const TRANSPARENT_COLOR = new Clutter.Color();
TRANSPARENT_COLOR.from_pixel(0x00000000);
const POPUP_APPICON_SIZE = 96;
const POPUP_LIST_SPACING = 8;
-const POPUP_POINTER_SELECTION_THRESHOLD = 3;
+const DISABLE_HOVER_TIMEOUT = 500; // milliseconds
const THUMBNAIL_SIZE = 256;
-const THUMBNAIL_POPUP_TIME = 1000; // milliseconds
-
-const HOVER_TIME = 500; // milliseconds
+const THUMBNAIL_POPUP_TIME = 500; // milliseconds
+const THUMBNAIL_FADE_TIME = 0.2; // seconds
function mod(a, b) {
return (a + b) % b;
@@ -50,8 +52,13 @@ AltTabPopup.prototype = {
this._haveModal = false;
this._currentApp = 0;
- this._currentWindows = [];
+ this._currentWindow = 0;
this._thumbnailTimeoutId = 0;
+ this._motionTimeoutId = 0;
+
+ // Initially disable hover so we ignore the enter-event if
+ // the switcher appears underneath the current pointer location
+ this._disableHover();
global.stage.add_actor(this.actor);
},
@@ -70,14 +77,13 @@ AltTabPopup.prototype = {
this._keyPressEventId = global.stage.connect('key-press-event', Lang.bind(this, this._keyPressEvent));
this._keyReleaseEventId = global.stage.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent));
- this._motionEventId = this.actor.connect('motion-event', Lang.bind(this, this._mouseMoved));
- this._mouseActive = false;
- this._mouseMovement = 0;
+ this.actor.connect('button-press-event', Lang.bind(this, this._clickedOutside));
+ this.actor.connect('scroll-event', Lang.bind(this, this._onScroll));
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));
+ this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered));
let primary = global.get_primary_monitor();
this._appSwitcher.actor.x = primary.x + Math.floor((primary.width - this._appSwitcher.actor.width) / 2);
@@ -85,15 +91,11 @@ AltTabPopup.prototype = {
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].cachedWindows.length > 1) {
// For compatibility with the multi-app case below
- this._select(0, 1);
+ this._select(0, 1, true);
} else
this._select(0);
} else if (backward) {
@@ -108,7 +110,7 @@ AltTabPopup.prototype = {
// 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);
+ this._select(0, 1, true);
else
this._select(1);
} else
@@ -137,11 +139,11 @@ AltTabPopup.prototype = {
},
_nextWindow : function() {
- return mod(this._currentWindows[this._currentApp] + 1,
+ return mod(this._currentWindow + 1,
this._appIcons[this._currentApp].cachedWindows.length);
},
_previousWindow : function() {
- return mod(this._currentWindows[this._currentApp] - 1,
+ return mod(this._currentWindow - 1,
this._appIcons[this._currentApp].cachedWindows.length);
},
@@ -149,29 +151,38 @@ AltTabPopup.prototype = {
let keysym = event.get_key_symbol();
let shift = (Shell.get_event_state(event) & Clutter.ModifierType.SHIFT_MASK);
+ this._disableHover();
+
// 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)
+ 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)
+ else if (this._thumbnailsFocused) {
+ if (keysym == Clutter.Tab) {
+ if (shift && this._currentWindow == 0)
+ this._select(this._previousApp());
+ else if (!shift && this._currentWindow == this._appIcons[this._currentApp].cachedWindows.length - 1)
+ this._select(this._nextApp());
+ else
+ this._select(this._currentApp, shift ? this._previousWindow() : this._nextWindow());
+ } else 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)
+ if (keysym == Clutter.Tab)
+ this._select(shift ? this._previousApp() : this._nextApp());
+ 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]);
+ this._select(this._currentApp, this._currentWindow);
}
return true;
@@ -186,16 +197,56 @@ AltTabPopup.prototype = {
return true;
},
+ _onScroll : function(actor, event) {
+ let direction = event.get_scroll_direction();
+ if (direction == Clutter.ScrollDirection.UP) {
+ if (this._thumbnailsFocused) {
+ if (this._currentWindow == 0)
+ this._select(this._previousApp());
+ else
+ this._select(this._currentApp, this._previousWindow());
+ } else {
+ let nwindows = this._appIcons[this._currentApp].cachedWindows.length;
+ if (nwindows > 1)
+ this._select(this._currentApp, nwindows - 1);
+ else
+ this._select(this._previousApp());
+ }
+ } else if (direction == Clutter.ScrollDirection.DOWN) {
+ if (this._thumbnailsFocused) {
+ if (this._currentWindow == this._appIcons[this._currentApp].cachedWindows.length - 1)
+ this._select(this._nextApp());
+ else
+ this._select(this._currentApp, this._nextWindow());
+ } else {
+ let nwindows = this._appIcons[this._currentApp].cachedWindows.length;
+ if (nwindows > 1)
+ this._select(this._currentApp, 0);
+ else
+ this._select(this._nextApp());
+ }
+ }
+ },
+
+ _clickedOutside : function(actor, event) {
+ this.destroy();
+ },
+
_appActivated : function(appSwitcher, n) {
- Main.activateWindow(this._appIcons[n].cachedWindows[this._currentWindows[n]]);
+ // If the user clicks on the selected app, activate the
+ // selected window; otherwise (eg, they click on an app while
+ // !mouseActive) activate the first window of the clicked-on
+ // app.
+ let window = (n == this._currentApp) ? this._currentWindow : 0;
+ Main.activateWindow(this._appIcons[n].cachedWindows[window]);
this.destroy();
},
- _appHovered : function(appSwitcher, n) {
+ _appEntered : function(appSwitcher, n) {
if (!this._mouseActive)
return;
- this._select(n, this._currentWindows[n]);
+ this._select(n);
},
_windowActivated : function(thumbnailList, n) {
@@ -203,28 +254,30 @@ AltTabPopup.prototype = {
this.destroy();
},
- _windowHovered : function(thumbnailList, n) {
+ _windowEntered : function(thumbnailList, n) {
if (!this._mouseActive)
return;
this._select(this._currentApp, n);
},
- _mouseMoved : function(actor, event) {
- if (++this._mouseMovement < POPUP_POINTER_SELECTION_THRESHOLD)
- return;
+ _disableHover : function() {
+ this._mouseActive = false;
- this.actor.disconnect(this._motionEventId);
- this._mouseActive = true;
+ if (this._motionTimeoutId != 0)
+ Mainloop.source_remove(this._motionTimeoutId);
- this._appSwitcher.checkHover();
- if (this._thumbnails)
- this._thumbnails.checkHover();
+ this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut));
+ },
+
+ _mouseTimedOut : function() {
+ this._motionTimeoutId = 0;
+ this._mouseActive = true;
},
_finish : function() {
let app = this._appIcons[this._currentApp];
- let window = app.cachedWindows[this._currentWindows[this._currentApp]];
+ let window = app.cachedWindows[this._currentWindow];
Main.activateWindow(window);
this.destroy();
},
@@ -242,17 +295,41 @@ AltTabPopup.prototype = {
if (this._keyReleaseEventId)
global.stage.disconnect(this._keyReleaseEventId);
+ if (this._motionTimeoutId != 0)
+ Mainloop.source_remove(this._motionTimeoutId);
if (this._thumbnailTimeoutId != 0)
Mainloop.source_remove(this._thumbnailTimeoutId);
},
- _select : function(app, window, noTimeout) {
- if (app != this._currentApp || !window) {
- if (this._thumbnails) {
- this._thumbnails.actor.destroy();
- this._thumbnails = null;
- }
- this._appSwitcher.showArrow(-1);
+ /**
+ * _select:
+ * @app: index of the app to select
+ * @window: (optional) index of which of @app's windows to select
+ * @forceAppFocus: optional flag, see below
+ *
+ * Selects the indicated @app, and optional @window, and sets
+ * this._thumbnailsFocused appropriately to indicate whether the
+ * arrow keys should act on the app list or the thumbnail list.
+ *
+ * If @app is specified and @window is unspecified or %null, then
+ * the app is highlighted (ie, given a light background), and the
+ * current thumbnail list, if any, is destroyed. If @app has
+ * multiple windows, and @forceAppFocus is not %true, then a
+ * timeout is started to open a thumbnail list.
+ *
+ * If @app and @window are specified (and @forceAppFocus is not),
+ * then @app will be outlined, a thumbnail list will be created
+ * and focused (if it hasn't been already), and the @window'th
+ * window in it will be highlighted.
+ *
+ * If @app and @window are specified and @forceAppFocus is %true,
+ * then @app will be highlighted, and @window outlined, and the
+ * app list will have the keyboard focus.
+ */
+ _select : function(app, window, forceAppFocus) {
+ if (app != this._currentApp || window == null) {
+ if (this._thumbnails)
+ this._destroyThumbnails();
}
if (this._thumbnailTimeoutId != 0) {
@@ -260,37 +337,42 @@ AltTabPopup.prototype = {
this._thumbnailTimeoutId = 0;
}
+ this._thumbnailsFocused = (window != null) && !forceAppFocus;
+
this._currentApp = app;
- if (window != null) {
- this._appSwitcher.highlight(-1);
- this._appSwitcher.showArrow(app);
- } else {
- this._appSwitcher.highlight(app);
- if (this._appIcons[this._currentApp].cachedWindows.length > 1)
- this._appSwitcher.showArrow(app);
- }
+ this._currentWindow = window ? window : 0;
+ this._appSwitcher.highlight(app, this._thumbnailsFocused);
if (window != null) {
if (!this._thumbnails)
this._createThumbnails();
- this._currentWindows[this._currentApp] = window;
- this._thumbnails.highlight(window);
+ this._currentWindow = window;
+ this._thumbnails.highlight(window, forceAppFocus);
} else if (this._appIcons[this._currentApp].cachedWindows.length > 1 &&
- !noTimeout) {
+ !forceAppFocus) {
this._thumbnailTimeoutId = Mainloop.timeout_add (
THUMBNAIL_POPUP_TIME,
Lang.bind(this, function () {
- this._select(this._currentApp,
- this._currentWindows[this._currentApp]);
+ this._select(this._currentApp, 0, true);
return false;
}));
}
},
+ _destroyThumbnails : function() {
+ Tweener.addTween(this._thumbnails.actor,
+ { opacity: 0,
+ time: THUMBNAIL_FADE_TIME,
+ transition: "easeOutQuad",
+ onComplete: function() { this.destroy(); }
+ });
+ this._thumbnails = null;
+ },
+
_createThumbnails : function() {
this._thumbnails = new ThumbnailList (this._appIcons[this._currentApp].cachedWindows);
this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated));
- this._thumbnails.connect('item-hovered', Lang.bind(this, this._windowHovered));
+ this._thumbnails.connect('item-entered', Lang.bind(this, this._windowEntered));
this.actor.add_actor(this._thumbnails.actor);
@@ -320,6 +402,13 @@ AltTabPopup.prototype = {
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,
+ time: THUMBNAIL_FADE_TIME,
+ transition: "easeOutQuad"
+ });
}
};
@@ -330,7 +419,6 @@ function SwitcherList(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.
@@ -347,39 +435,29 @@ SwitcherList.prototype = {
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);
+ // We want the St.Bin's padding to be clickable (since it will
+ // be part of the highlighted background color), so we put the
+ // bin inside the ButtonBox rather than vice versa.
+ let bin = new St.Bin({ style_class: 'item-box' });
+ let bbox = new Shell.ButtonBox({ reactive: true });
+
+ bin.add_actor(item);
+ bbox.append(bin, Big.BoxPackFlags.NONE);
+ this._list.add_actor(bbox);
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);
- }));
+ bbox.connect('enter-event', Lang.bind(this, function () {
+ this._itemEntered(n);
+ }));
- this._items.push(box);
+ bbox._bin = bin;
+ this._items.push(bbox);
},
addSeparator: function () {
@@ -388,24 +466,17 @@ SwitcherList.prototype = {
this._list.add_actor(box);
},
- highlight: function(index) {
+ highlight: function(index, justOutline) {
if (this._highlighted != -1)
- this._items[this._highlighted].style_class = 'item-box';
+ this._items[this._highlighted]._bin.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;
- }
+ if (this._highlighted != -1) {
+ if (justOutline)
+ this._items[this._highlighted]._bin.style_class = 'outlined-item-box';
+ else
+ this._items[this._highlighted]._bin.style_class = 'selected-item-box';
}
},
@@ -413,23 +484,8 @@ SwitcherList.prototype = {
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);
+ _itemEntered: function(n) {
+ this.emit('item-entered', n);
},
_maxChildWidth: function (forHeight) {
@@ -570,7 +626,7 @@ AppSwitcher.prototype = {
for (let i = 0; i < otherIcons.length; i++)
this._addIcon(otherIcons[i]);
- this._shownArrow = -1;
+ this._curApp = -1;
},
_allocate: function (actor, box, flags) {
@@ -592,30 +648,56 @@ AppSwitcher.prototype = {
}
},
- showArrow : function(n) {
- if (this._shownArrow != -1)
- this._arrows[this._shownArrow].hide();
+ // We override SwitcherList's highlight() method to also deal with
+ // the AppSwitcher->ThumbnailList arrows. Apps with only 1 window
+ // will hide their arrows by default, but show them when their
+ // thumbnails are visible (ie, when the app icon is supposed to be
+ // in justOutline mode). Apps with multiple windows will normally
+ // show a dim arrow, but show a bright arrow when they are
+ // highlighted; their redraw handler will use the right color
+ // based on this._curApp; we just need to do a queue_relayout() to
+ // force it to redraw. (queue_redraw() doesn't work because
+ // ShellDrawingArea only redraws on allocate.)
+ highlight : function(n, justOutline) {
+ if (this._curApp != -1) {
+ if (this.icons[this._curApp].cachedWindows.length == 1)
+ this._arrows[this._curApp].hide();
+ else
+ this._arrows[this._curApp].queue_relayout();
+ }
- this._shownArrow = n;
+ SwitcherList.prototype.highlight.call(this, n, justOutline);
+ this._curApp = n;
- if (this._shownArrow != -1)
- this._arrows[this._shownArrow].show();
+ if (this._curApp != -1) {
+ if (justOutline && this.icons[this._curApp].cachedWindows.length == 1)
+ this._arrows[this._curApp].show();
+ else
+ this._arrows[this._curApp].queue_relayout();
+ }
},
_addIcon : function(appIcon) {
this.icons.push(appIcon);
this.addItem(appIcon.actor);
+ // SwitcherList creates its own Shell.ButtonBox; we want to
+ // avoid intercepting the events it wants.
+ appIcon.actor.reactive = false;
+
+ let n = this._arrows.length;
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._curApp == n ? POPUP_ARROW_COLOR : POPUP_UNFOCUSED_ARROW_COLOR);
}));
this._list.add_actor(arrow);
this._arrows.push(arrow);
- arrow.hide();
+
+ if (appIcon.cachedWindows.length == 1)
+ arrow.hide();
},
_hasWindowsOnWorkspace: function(appIcon, workspace) {
@@ -648,12 +730,21 @@ ThumbnailList.prototype = {
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 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);
+ box.add_actor(clone);
+
+ let name = new St.Label({ text: windows[i].get_title() });
+ // St.Label doesn't support text-align so use a Bin
+ let bin = new St.Bin({ x_align: St.Align.MIDDLE });
+ bin.add_actor(name);
+ box.add_actor(bin);
+
this.addItem(box);
}
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]