diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index c2b69b15b..a4b984275 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -1542,7 +1542,7 @@ StScrollBar StButton#vhandle:active { } /* App Switcher */ -#altTabPopup { +.switcher-popup { padding: 8px; spacing: 16px; } diff --git a/js/Makefile.am b/js/Makefile.am index a3e4917db..6d1326252 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -85,6 +85,7 @@ nobase_dist_js_DATA = \ ui/status/power.js \ ui/status/volume.js \ ui/status/bluetooth.js \ + ui/switcherPopup.js \ ui/tweener.js \ ui/unlockDialog.js \ ui/userMenu.js \ diff --git a/js/ui/altTab.js b/js/ui/altTab.js index 5de7d7b26..74a03a0fb 100644 --- a/js/ui/altTab.js +++ b/js/ui/altTab.js @@ -1,117 +1,56 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; -const Gdk = imports.gi.Gdk; -const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Mainloop = imports.mainloop; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; -const Signals = imports.signals; const St = imports.gi.St; const Atk = imports.gi.Atk; const Main = imports.ui.main; +const SwitcherPopup = imports.ui.switcherPopup; const Tweener = imports.ui.tweener; -const POPUP_APPICON_SIZE = 96; -const POPUP_SCROLL_TIME = 0.10; // seconds -const POPUP_DELAY_TIMEOUT = 150; // milliseconds -const POPUP_FADE_OUT_TIME = 0.1; // seconds - const APP_ICON_HOVER_TIMEOUT = 200; // milliseconds -const DISABLE_HOVER_TIMEOUT = 500; // milliseconds - const THUMBNAIL_DEFAULT_SIZE = 256; const THUMBNAIL_POPUP_TIME = 500; // milliseconds const THUMBNAIL_FADE_TIME = 0.1; // seconds const iconSizes = [96, 64, 48, 32, 22]; -function mod(a, b) { - return (a + b) % b; -} - -function primaryModifier(mask) { - if (mask == 0) - return 0; - - let primary = 1; - while (mask > 1) { - mask >>= 1; - primary <<= 1; - } - return primary; -} - const AltTabPopup = new Lang.Class({ Name: 'AltTabPopup', + Extends: SwitcherPopup.SwitcherPopup, _init : function() { - this.actor = new Shell.GenericContainer({ name: 'altTabPopup', - reactive: true, - visible: false }); + this.parent(); - this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); - this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); - this.actor.connect('allocate', Lang.bind(this, this._allocate)); - - this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - - this._haveModal = false; - this._modifierMask = 0; - - this._currentApp = 0; - this._currentWindow = -1; + this._thumbnails = null; this._thumbnailTimeoutId = 0; - this._motionTimeoutId = 0; - this._initialDelayTimeoutId = 0; + this._currentWindow = -1; this.thumbnailsVisible = false; - - // Initially disable hover so we ignore the enter-event if - // the switcher appears underneath the current pointer location - this._disableHover(); - - Main.uiGroup.add_actor(this.actor); - }, - - _getPreferredWidth: function (actor, forHeight, alloc) { - alloc.min_size = global.screen_width; - alloc.natural_size = global.screen_width; - }, - - _getPreferredHeight: function (actor, forWidth, alloc) { - alloc.min_size = global.screen_height; - alloc.natural_size = global.screen_height; }, _allocate: function (actor, box, flags) { - let childBox = new Clutter.ActorBox(); - let primary = Main.layoutManager.primaryMonitor; - - let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); - let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); - let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); - let vPadding = this.actor.get_theme_node().get_vertical_padding(); - let hPadding = leftPadding + rightPadding; - - // Allocate the appSwitcher - // We select a size based on an icon size that does not overflow the screen - let [childMinHeight, childNaturalHeight] = this._appSwitcher.actor.get_preferred_height(primary.width - hPadding); - let [childMinWidth, childNaturalWidth] = this._appSwitcher.actor.get_preferred_width(childNaturalHeight); - childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2)); - childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth); - childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2); - childBox.y2 = childBox.y1 + childNaturalHeight; - this._appSwitcher.actor.allocate(childBox, flags); + this.parent(actor, box, flags); // Allocate the thumbnails // We try to avoid overflowing the screen so we base the resulting size on // those calculations if (this._thumbnails) { - let icon = this._appIcons[this._currentApp].actor; + let childBox = this._switcherList.actor.get_allocation_box(); + let primary = Main.layoutManager.primaryMonitor; + + let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); + let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); + let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); + let vPadding = this.actor.get_theme_node().get_vertical_padding(); + let hPadding = leftPadding + rightPadding; + + let icon = this._items[this._selectedIndex].actor; let [posX, posY] = icon.get_transformed_position(); let thumbnailCenter = posX + icon.width / 2; let [childMinWidth, childNaturalWidth] = this._thumbnails.actor.get_preferred_width(-1); @@ -126,7 +65,7 @@ const AltTabPopup = new Lang.Class({ childBox.x2 = childBox.x1 + childNaturalWidth; if (childBox.x2 > primary.x + primary.width - rightPadding) childBox.x2 = primary.x + primary.width - rightPadding; - childBox.y1 = this._appSwitcher.actor.allocation.y2 + spacing; + childBox.y1 = this._switcherList.actor.allocation.y2 + spacing; this._thumbnails.addClones(primary.y + primary.height - bottomPadding - childBox.y1); let [childMinHeight, childNaturalHeight] = this._thumbnails.actor.get_preferred_height(-1); childBox.y2 = childBox.y1 + childNaturalHeight; @@ -164,280 +103,158 @@ const AltTabPopup = new Lang.Class({ return [apps, allApps]; }, - show : function(backward, binding, mask) { + _createSwitcher: function() { let [localApps, otherApps] = this._getAppLists(); if (localApps.length == 0 && otherApps.length == 0) return false; - if (!Main.pushModal(this.actor)) { - // Probably someone else has a pointer grab, try again with keyboard only - if (!Main.pushModal(this.actor, { options: Meta.ModalOptions.POINTER_ALREADY_GRABBED })) { - return false; - } - } - this._haveModal = true; - this._modifierMask = primaryModifier(mask); + this._switcherList = new AppSwitcher(localApps, otherApps, this); + this._items = this._switcherList.icons; - this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent)); - this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent)); + return true; + }, - 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(localApps, otherApps, this); - this.actor.add_actor(this._appSwitcher.actor); - this._appSwitcher.connect('item-activated', Lang.bind(this, this._appActivated)); - this._appSwitcher.connect('item-entered', Lang.bind(this, this._appEntered)); - - this._appIcons = this._appSwitcher.icons; - - // Need to force an allocation so we can figure out whether we - // need to scroll when selecting - this.actor.opacity = 0; - this.actor.show(); - this.actor.get_allocation_box(); - - // Make the initial selection + _initialSelection: function(backward, binding) { if (binding == 'switch-group') { if (backward) { - this._select(0, this._appIcons[0].cachedWindows.length - 1); + this._select(0, this._items[0].cachedWindows.length - 1); } else { - if (this._appIcons[0].cachedWindows.length > 1) + if (this._items[0].cachedWindows.length > 1) this._select(0, 1); else this._select(0, 0); } } else if (binding == 'switch-group-backward') { - this._select(0, this._appIcons[0].cachedWindows.length - 1); + this._select(0, this._items[0].cachedWindows.length - 1); } else if (binding == 'switch-windows-backward') { - this._select(this._appIcons.length - 1); - } else if (this._appIcons.length == 1) { + this._select(this._items.length - 1); + } else if (this._items.length == 1) { this._select(0); } else if (backward) { - this._select(this._appIcons.length - 1); + this._select(this._items.length - 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 [x, y, mods] = global.get_pointer(); - if (!(mods & this._modifierMask)) { - this._finish(); - return false; - } - - // We delay showing the popup so that fast Alt+Tab users aren't - // disturbed by the popup briefly flashing. - this._initialDelayTimeoutId = Mainloop.timeout_add(POPUP_DELAY_TIMEOUT, - Lang.bind(this, function () { - this.actor.opacity = 255; - this._initialDelayTimeoutId = 0; - })); - - return true; - }, - - _nextApp : function() { - return mod(this._currentApp + 1, this._appIcons.length); - }, - _previousApp : function() { - return mod(this._currentApp - 1, this._appIcons.length); }, _nextWindow : function() { // We actually want the second window if we're in the unset state if (this._currentWindow == -1) this._currentWindow = 0; - return mod(this._currentWindow + 1, - this._appIcons[this._currentApp].cachedWindows.length); + return SwitcherPopup.mod(this._currentWindow + 1, + this._items[this._selectedIndex].cachedWindows.length); }, _previousWindow : function() { // Also assume second window here if (this._currentWindow == -1) this._currentWindow = 1; - return mod(this._currentWindow - 1, - this._appIcons[this._currentApp].cachedWindows.length); + return SwitcherPopup.mod(this._currentWindow - 1, + this._items[this._selectedIndex].cachedWindows.length); }, - _keyPressEvent : function(actor, event) { - let keysym = event.get_key_symbol(); - let event_state = event.get_state(); - let backwards = event_state & Clutter.ModifierType.SHIFT_MASK; - let action = global.display.get_keybinding_action(event.get_key_code(), event_state); - - this._disableHover(); - - if (keysym == Clutter.Escape) { - this.destroy(); - } else if (action == Meta.KeyBindingAction.SWITCH_GROUP) { - this._select(this._currentApp, backwards ? this._previousWindow() : this._nextWindow()); + _keyPressHandler: function(keysym, backwards, action) { + if (action == Meta.KeyBindingAction.SWITCH_GROUP) { + this._select(this._selectedIndex, backwards ? this._previousWindow() : this._nextWindow()); } else if (action == Meta.KeyBindingAction.SWITCH_GROUP_BACKWARD) { - this._select(this._currentApp, this._previousWindow()); + this._select(this._selectedIndex, this._previousWindow()); } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS) { - this._select(backwards ? this._previousApp() : this._nextApp()); + this._select(backwards ? this._previous() : this._next()); } else if (action == Meta.KeyBindingAction.SWITCH_WINDOWS_BACKWARD) { - this._select(this._previousApp()); + this._select(this._previous()); } else if (this._thumbnailsFocused) { if (keysym == Clutter.Left) - this._select(this._currentApp, this._previousWindow()); + this._select(this._selectedIndex, this._previousWindow()); else if (keysym == Clutter.Right) - this._select(this._currentApp, this._nextWindow()); + this._select(this._selectedIndex, this._nextWindow()); else if (keysym == Clutter.Up) - this._select(this._currentApp, null, true); + this._select(this._selectedIndex, null, true); } else { if (keysym == Clutter.Left) - this._select(this._previousApp()); + this._select(this._previous()); else if (keysym == Clutter.Right) - this._select(this._nextApp()); + this._select(this._next()); else if (keysym == Clutter.Down) - this._select(this._currentApp, 0); + this._select(this._selectedIndex, 0); } - - return true; }, - _keyReleaseEvent : function(actor, event) { - let [x, y, mods] = global.get_pointer(); - let state = mods & this._modifierMask; - - if (state == 0) - this._finish(); - - return true; - }, - - _onScroll : function(actor, event) { - let direction = event.get_scroll_direction(); + _scrollHandler: function(direction) { if (direction == Clutter.ScrollDirection.UP) { if (this._thumbnailsFocused) { if (this._currentWindow == 0 || this._currentWindow == -1) - this._select(this._previousApp()); + this._select(this._previous()); else - this._select(this._currentApp, this._previousWindow()); + this._select(this._selectedIndex, this._previousWindow()); } else { - let nwindows = this._appIcons[this._currentApp].cachedWindows.length; + let nwindows = this._items[this._selectedIndex].cachedWindows.length; if (nwindows > 1) - this._select(this._currentApp, nwindows - 1); + this._select(this._selectedIndex, nwindows - 1); else - this._select(this._previousApp()); + this._select(this._previous()); } } else if (direction == Clutter.ScrollDirection.DOWN) { if (this._thumbnailsFocused) { - if (this._currentWindow == this._appIcons[this._currentApp].cachedWindows.length - 1) - this._select(this._nextApp()); + if (this._currentWindow == this._items[this._selectedIndex].cachedWindows.length - 1) + this._select(this._next()); else - this._select(this._currentApp, this._nextWindow()); + this._select(this._selectedIndex, this._nextWindow()); } else { - let nwindows = this._appIcons[this._currentApp].cachedWindows.length; + let nwindows = this._items[this._selectedIndex].cachedWindows.length; if (nwindows > 1) - this._select(this._currentApp, 0); + this._select(this._selectedIndex, 0); else - this._select(this._nextApp()); + this._select(this._next()); } } }, - _clickedOutside : function(actor, event) { - this.destroy(); - }, - - _appActivated : function(appSwitcher, n) { + _itemActivatedHandler: function(n) { // If the user clicks on the selected app, activate the // selected window; otherwise (eg, they click on an app while // !mouseActive) activate the clicked-on app. - let window; - if (n == this._currentApp && this._currentWindow >= 0) - window = this._appIcons[n].cachedWindows[this._currentWindow]; + if (n == this._selectedIndex && this._currentWindow >= 0) + this._select(n, this._currentWindow); else - window = null; - this._appIcons[n].app.activate_window(window, global.get_current_time()); - this.destroy(); + this._select(n); }, - _appEntered : function(appSwitcher, n) { - if (!this._mouseActive) - return; - + _itemEnteredHandler: function(n) { this._select(n); }, _windowActivated : function(thumbnailList, n) { - let appIcon = this._appIcons[this._currentApp]; + let appIcon = this._items[this._selectedIndex]; Main.activateWindow(appIcon.cachedWindows[n]); this.destroy(); }, _windowEntered : function(thumbnailList, n) { - if (!this._mouseActive) + if (!this.mouseActive) return; - this._select(this._currentApp, n); - }, - - _disableHover : function() { - this._mouseActive = false; - - if (this._motionTimeoutId != 0) - Mainloop.source_remove(this._motionTimeoutId); - - this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut)); - }, - - _mouseTimedOut : function() { - this._motionTimeoutId = 0; - this._mouseActive = true; + this._select(this._selectedIndex, n); }, _finish : function() { - let app = this._appIcons[this._currentApp]; - if (this._currentWindow >= 0) { - Main.activateWindow(app.cachedWindows[this._currentWindow]); - } else { - app.app.activate_window(null, global.get_current_time()); - } - this.destroy(); - }, + this.parent(); - _popModal: function() { - if (this._haveModal) { - Main.popModal(this.actor); - this._haveModal = false; - } - }, - - destroy : function() { - this._popModal(); - if (this.actor.visible) { - Tweener.addTween(this.actor, - { opacity: 0, - time: POPUP_FADE_OUT_TIME, - transition: 'easeOutQuad', - onComplete: Lang.bind(this, - function() { - this.actor.destroy(); - }) - }); - } else - this.actor.destroy(); + let appIcon = this._items[this._selectedIndex]; + let window; + if (this._currentWindow >= 0) + window = appIcon.cachedWindows[this._currentWindow]; + else + window = null; + appIcon.app.activate_window(window, global.get_current_time()); }, _onDestroy : function() { - this._popModal(); + this.parent(); if (this._thumbnails) this._destroyThumbnails(); - - if (this._motionTimeoutId != 0) - Mainloop.source_remove(this._motionTimeoutId); if (this._thumbnailTimeoutId != 0) Mainloop.source_remove(this._thumbnailTimeoutId); - if (this._initialDelayTimeoutId != 0) - Mainloop.source_remove(this._initialDelayTimeoutId); }, /** @@ -466,7 +283,7 @@ const AltTabPopup = new Lang.Class({ * app list will have the keyboard focus. */ _select : function(app, window, forceAppFocus) { - if (app != this._currentApp || window == null) { + if (app != this._selectedIndex || window == null) { if (this._thumbnails) this._destroyThumbnails(); } @@ -478,16 +295,16 @@ const AltTabPopup = new Lang.Class({ this._thumbnailsFocused = (window != null) && !forceAppFocus; - this._currentApp = app; + this._selectedIndex = app; this._currentWindow = window ? window : -1; - this._appSwitcher.highlight(app, this._thumbnailsFocused); + this._switcherList.highlight(app, this._thumbnailsFocused); if (window != null) { if (!this._thumbnails) this._createThumbnails(); this._currentWindow = window; this._thumbnails.highlight(window, forceAppFocus); - } else if (this._appIcons[this._currentApp].cachedWindows.length > 1 && + } else if (this._items[this._selectedIndex].cachedWindows.length > 1 && !forceAppFocus) { this._thumbnailTimeoutId = Mainloop.timeout_add ( THUMBNAIL_POPUP_TIME, @@ -515,11 +332,11 @@ const AltTabPopup = new Lang.Class({ }) }); this._thumbnails = null; - this._appSwitcher._items[this._currentApp].remove_accessible_state (Atk.StateType.EXPANDED); + this._switcherList._items[this._selectedIndex].remove_accessible_state (Atk.StateType.EXPANDED); }, _createThumbnails : function() { - this._thumbnails = new ThumbnailList (this._appIcons[this._currentApp].cachedWindows); + this._thumbnails = new ThumbnailList (this._items[this._selectedIndex].cachedWindows); this._thumbnails.connect('item-activated', Lang.bind(this, this._windowActivated)); this._thumbnails.connect('item-entered', Lang.bind(this, this._windowEntered)); @@ -537,314 +354,10 @@ const AltTabPopup = new Lang.Class({ onComplete: Lang.bind(this, function () { this.thumbnailsVisible = true; }) }); - this._appSwitcher._items[this._currentApp].add_accessible_state (Atk.StateType.EXPANDED); + this._switcherList._items[this._selectedIndex].add_accessible_state (Atk.StateType.EXPANDED); } }); -const SwitcherList = new Lang.Class({ - Name: 'SwitcherList', - - _init : function(squareItems) { - this.actor = new Shell.GenericContainer({ style_class: 'switcher-list' }); - this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); - this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); - this.actor.connect('allocate', Lang.bind(this, this._allocateTop)); - - // 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({ style_class: 'switcher-list-item-container' }); - this._list.spacing = 0; - this._list.connect('style-changed', Lang.bind(this, function() { - this._list.spacing = this._list.get_theme_node().get_length('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._scrollView = new St.ScrollView({ style_class: 'hfade', - enable_mouse_scrolling: false }); - this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER); - - let scrollBox = new St.BoxLayout(); - scrollBox.add_actor(this._list); - this._scrollView.add_actor(scrollBox); - this.actor.add_actor(this._scrollView); - - // Those arrows indicate whether scrolling in one direction is possible - this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow', - pseudo_class: 'highlighted' }); - this._leftArrow.connect('repaint', Lang.bind(this, - function() { _drawArrow(this._leftArrow, St.Side.LEFT); })); - this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow', - pseudo_class: 'highlighted' }); - this._rightArrow.connect('repaint', Lang.bind(this, - function() { _drawArrow(this._rightArrow, St.Side.RIGHT); })); - - this.actor.add_actor(this._leftArrow); - this.actor.add_actor(this._rightArrow); - - this._items = []; - this._highlighted = -1; - this._separator = null; - this._squareItems = squareItems; - this._minSize = 0; - this._scrollableRight = true; - this._scrollableLeft = false; - }, - - _allocateTop: function(actor, box, flags) { - let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); - let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); - - let childBox = new Clutter.ActorBox(); - let scrollable = this._minSize > box.x2 - box.x1; - - box.y1 -= this.actor.get_theme_node().get_padding(St.Side.TOP); - box.y2 += this.actor.get_theme_node().get_padding(St.Side.BOTTOM); - this._scrollView.allocate(box, flags); - - let arrowWidth = Math.floor(leftPadding / 3); - let arrowHeight = arrowWidth * 2; - childBox.x1 = leftPadding / 2; - childBox.y1 = this.actor.height / 2 - arrowWidth; - childBox.x2 = childBox.x1 + arrowWidth; - childBox.y2 = childBox.y1 + arrowHeight; - this._leftArrow.allocate(childBox, flags); - this._leftArrow.opacity = (this._scrollableLeft && scrollable) ? 255 : 0; - - arrowWidth = Math.floor(rightPadding / 3); - arrowHeight = arrowWidth * 2; - childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2; - childBox.y1 = this.actor.height / 2 - arrowWidth; - childBox.x2 = childBox.x1 + arrowWidth; - childBox.y2 = childBox.y1 + arrowHeight; - this._rightArrow.allocate(childBox, flags); - this._rightArrow.opacity = (this._scrollableRight && scrollable) ? 255 : 0; - }, - - addItem : function(item, label) { - let bbox = new St.Button({ style_class: 'item-box', - reactive: true }); - - bbox.set_child(item); - this._list.add_actor(bbox); - - let n = this._items.length; - bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); })); - bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); })); - - bbox.label_actor = label; - - this._items.push(bbox); - - return bbox; - }, - - _onItemClicked: function (index) { - this._itemActivated(index); - }, - - _onItemEnter: function (index) { - this._itemEntered(index); - }, - - addSeparator: function () { - let box = new St.Bin({ style_class: 'separator' }); - this._separator = box; - this._list.add_actor(box); - }, - - highlight: function(index, justOutline) { - if (this._highlighted != -1) { - this._items[this._highlighted].remove_style_pseudo_class('outlined'); - this._items[this._highlighted].remove_style_pseudo_class('selected'); - } - - this._highlighted = index; - - if (this._highlighted != -1) { - if (justOutline) - this._items[this._highlighted].add_style_pseudo_class('outlined'); - else - this._items[this._highlighted].add_style_pseudo_class('selected'); - } - - let adjustment = this._scrollView.hscroll.adjustment; - let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); - let [absItemX, absItemY] = this._items[index].get_transformed_position(); - let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0); - let [containerWidth, containerHeight] = this.actor.get_transformed_size(); - if (posX + this._items[index].get_width() > containerWidth) - this._scrollToRight(); - else if (this._items[index].allocation.x1 - value < 0) - this._scrollToLeft(); - - }, - - _scrollToLeft : function() { - let adjustment = this._scrollView.hscroll.adjustment; - let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); - - let item = this._items[this._highlighted]; - - if (item.allocation.x1 < value) - value = Math.min(0, item.allocation.x1); - else if (item.allocation.x2 > value + pageSize) - value = Math.max(upper, item.allocation.x2 - pageSize); - - this._scrollableRight = true; - Tweener.addTween(adjustment, - { value: value, - time: POPUP_SCROLL_TIME, - transition: 'easeOutQuad', - onComplete: Lang.bind(this, function () { - if (this._highlighted == 0) { - this._scrollableLeft = false; - this.actor.queue_relayout(); - } - }) - }); - }, - - _scrollToRight : function() { - let adjustment = this._scrollView.hscroll.adjustment; - let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); - - let item = this._items[this._highlighted]; - - if (item.allocation.x1 < value) - value = Math.max(0, item.allocation.x1); - else if (item.allocation.x2 > value + pageSize) - value = Math.min(upper, item.allocation.x2 - pageSize); - - this._scrollableLeft = true; - Tweener.addTween(adjustment, - { value: value, - time: POPUP_SCROLL_TIME, - transition: 'easeOutQuad', - onComplete: Lang.bind(this, function () { - if (this._highlighted == this._items.length - 1) { - this._scrollableRight = false; - this.actor.queue_relayout(); - } - }) - }); - }, - - _itemActivated: function(n) { - this.emit('item-activated', n); - }, - - _itemEntered: function(n) { - this.emit('item-entered', 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.natural_size = alloc.min_size; - this._minSize = alloc.min_size; - }, - - _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 = maxChildMin; - } - - alloc.min_size = maxChildMin; - alloc.natural_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(); - - let primary = Main.layoutManager.primaryMonitor; - let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT); - - 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); - const AppIcon = new Lang.Class({ Name: 'AppIcon', @@ -869,7 +382,7 @@ const AppIcon = new Lang.Class({ const AppSwitcher = new Lang.Class({ Name: 'AppSwitcher', - Extends: SwitcherList, + Extends: SwitcherPopup.SwitcherList, _init : function(localApps, otherApps, altTabPopup) { this.parent(true); @@ -1023,7 +536,7 @@ const AppSwitcher = new Lang.Class({ let n = this._arrows.length; let arrow = new St.DrawingArea({ style_class: 'switcher-arrow' }); - arrow.connect('repaint', function() { _drawArrow(arrow, St.Side.BOTTOM); }); + arrow.connect('repaint', function() { SwitcherPopup.drawArrow(arrow, St.Side.BOTTOM); }); this._list.add_actor(arrow); this._arrows.push(arrow); @@ -1036,7 +549,7 @@ const AppSwitcher = new Lang.Class({ const ThumbnailList = new Lang.Class({ Name: 'ThumbnailList', - Extends: SwitcherList, + Extends: SwitcherPopup.SwitcherList, _init : function(windows) { this.parent(false); @@ -1119,45 +632,3 @@ const ThumbnailList = new Lang.Class({ } }); -function _drawArrow(area, side) { - let themeNode = area.get_theme_node(); - let borderColor = themeNode.get_border_color(side); - let bodyColor = themeNode.get_foreground_color(); - - let [width, height] = area.get_surface_size (); - let cr = area.get_context(); - - cr.setLineWidth(1.0); - Clutter.cairo_set_source_color(cr, borderColor); - - switch (side) { - case St.Side.TOP: - cr.moveTo(0, height); - cr.lineTo(Math.floor(width * 0.5), 0); - cr.lineTo(width, height); - break; - - case St.Side.BOTTOM: - cr.moveTo(width, 0); - cr.lineTo(Math.floor(width * 0.5), height); - cr.lineTo(0, 0); - break; - - case St.Side.LEFT: - cr.moveTo(width, height); - cr.lineTo(0, Math.floor(height * 0.5)); - cr.lineTo(width, 0); - break; - - case St.Side.RIGHT: - cr.moveTo(0, 0); - cr.lineTo(width, Math.floor(height * 0.5)); - cr.lineTo(0, height); - break; - } - - cr.strokePreserve(); - - Clutter.cairo_set_source_color(cr, bodyColor); - cr.fill(); -} diff --git a/js/ui/ctrlAltTab.js b/js/ui/ctrlAltTab.js index 0519c9900..14fba02b2 100644 --- a/js/ui/ctrlAltTab.js +++ b/js/ui/ctrlAltTab.js @@ -1,15 +1,14 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Clutter = imports.gi.Clutter; -const Gdk = imports.gi.Gdk; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; const St = imports.gi.St; -const AltTab = imports.ui.altTab; const Main = imports.ui.main; +const SwitcherPopup = imports.ui.switcherPopup; const Params = imports.misc.params; const Tweener = imports.ui.tweener; @@ -92,7 +91,7 @@ const CtrlAltTabManager = new Lang.Class({ return a.x - b.x; }, - popup: function(backwards, mask) { + popup: function(backward, binding, mask) { // Start with the set of focus groups that are currently mapped let items = this._items.filter(function (item) { return item.proxy.mapped; }); @@ -123,8 +122,8 @@ const CtrlAltTabManager = new Lang.Class({ items.sort(Lang.bind(this, this._sortItems)); if (!this._popup) { - this._popup = new CtrlAltTabPopup(); - this._popup.show(items, backwards, mask); + this._popup = new CtrlAltTabPopup(items); + this._popup.show(backward, binding, mask); this._popup.actor.connect('destroy', Lang.bind(this, function() { @@ -134,176 +133,46 @@ const CtrlAltTabManager = new Lang.Class({ } }); -function mod(a, b) { - return (a + b) % b; -} - const CtrlAltTabPopup = new Lang.Class({ Name: 'CtrlAltTabPopup', + Extends: SwitcherPopup.SwitcherPopup, - _init : function() { - this.actor = new Shell.GenericContainer({ name: 'ctrlAltTabPopup', - reactive: true }); - - this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); - this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); - this.actor.connect('allocate', Lang.bind(this, this._allocate)); - - this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); - - this._haveModal = false; - this._modifierMask = 0; - this._selection = 0; - - Main.uiGroup.add_actor(this.actor); + _createSwitcher: function() { + this._switcherList = new CtrlAltTabSwitcher(this._items); + return true; }, - _getPreferredWidth: function (actor, forHeight, alloc) { - let primary = Main.layoutManager.primaryMonitor; - - alloc.min_size = primary.width; - alloc.natural_size = primary.width; - }, - - _getPreferredHeight: function (actor, forWidth, alloc) { - let primary = Main.layoutManager.primaryMonitor; - - alloc.min_size = primary.height; - alloc.natural_size = primary.height; - }, - - _allocate: function (actor, box, flags) { - let childBox = new Clutter.ActorBox(); - let primary = Main.layoutManager.primaryMonitor; - - let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); - let vPadding = this.actor.get_theme_node().get_vertical_padding(); - let hPadding = this.actor.get_theme_node().get_horizontal_padding(); - - let [childMinHeight, childNaturalHeight] = this._switcher.actor.get_preferred_height(primary.width - hPadding); - let [childMinWidth, childNaturalWidth] = this._switcher.actor.get_preferred_width(childNaturalHeight); - childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2)); - childBox.x2 = Math.min(primary.x + primary.width - hPadding, childBox.x1 + childNaturalWidth); - childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2); - childBox.y2 = childBox.y1 + childNaturalHeight; - this._switcher.actor.allocate(childBox, flags); - }, - - show : function(items, startBackwards, mask) { - if (!Main.pushModal(this.actor)) - return false; - this._haveModal = true; - this._modifierMask = AltTab.primaryModifier(mask); - - this._keyPressEventId = this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent)); - this._keyReleaseEventId = this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent)); - - this._items = items; - this._switcher = new CtrlAltTabSwitcher(items); - this.actor.add_actor(this._switcher.actor); - - if (startBackwards) - this._selection = this._items.length - 1; - this._select(this._selection); - - let [x, y, mods] = global.get_pointer(); - if (!(mods & this._modifierMask)) { - this._finish(); - return false; + _initialSelection: function(backward, binding) { + if (binding == 'switch-panels') { + if (backward) + this._selectedIndex = this._items.length - 1; + } else if (binding == 'switch-panels-backward') { + if (!backward) + this._selectedIndex = this._items.length - 1; } - - this.actor.opacity = 0; - this.actor.show(); - Tweener.addTween(this.actor, - { opacity: 255, - time: POPUP_FADE_TIME, - transition: 'easeOutQuad' - }); - - return true; + this._select(this._selectedIndex); }, - _next : function() { - return mod(this._selection + 1, this._items.length); - }, - - _previous : function() { - return mod(this._selection - 1, this._items.length); - }, - - _keyPressEvent : function(actor, event) { - let keysym = event.get_key_symbol(); - let shift = (event.get_state() & Clutter.ModifierType.SHIFT_MASK); - if (shift && keysym == Clutter.KEY_Tab) - keysym = Clutter.ISO_Left_Tab; - - if (keysym == Clutter.KEY_Escape) - this.destroy(); - else if (keysym == Clutter.KEY_Tab) - this._select(this._next()); - else if (keysym == Clutter.KEY_ISO_Left_Tab) + _keyPressHandler: function(keysym, backwards, action) { + if (action == Meta.KeyBindingAction.SWITCH_PANELS) + this._select(backwards ? this._previous() : this._next()); + else if (action == Meta.KeyBindingAction.SWITCH_PANELS_BACKWARD) + this._select(backwards ? this._next() : this._previous()); + else if (keysym == Clutter.Left) this._select(this._previous()); - else if (keysym == Clutter.KEY_Left) - this._select(this._previous()); - else if (keysym == Clutter.KEY_Right) + else if (keysym == Clutter.Right) this._select(this._next()); - - return true; - }, - - _keyReleaseEvent : function(actor, event) { - let [x, y, mods] = global.get_pointer(); - let state = mods & this._modifierMask; - - if (state == 0) - this._finish(); - - return true; }, _finish : function() { - this.destroy(); - - Main.ctrlAltTabManager.focusGroup(this._items[this._selection]); + this.parent(); + Main.ctrlAltTabManager.focusGroup(this._items[this._selectedIndex]); }, - - _popModal: function() { - if (this._haveModal) { - Main.popModal(this.actor); - this._haveModal = false; - } - }, - - destroy : function() { - this._popModal(); - Tweener.addTween(this.actor, - { opacity: 0, - time: POPUP_FADE_TIME, - transition: 'easeOutQuad', - onComplete: Lang.bind(this, - function() { - this.actor.destroy(); - }) - }); - }, - - _onDestroy : function() { - this._popModal(); - if (this._keyPressEventId) - this.actor.disconnect(this._keyPressEventId); - if (this._keyReleaseEventId) - this.actor.disconnect(this._keyReleaseEventId); - }, - - _select : function(num) { - this._selection = num; - this._switcher.highlight(num); - } }); const CtrlAltTabSwitcher = new Lang.Class({ Name: 'CtrlAltTabSwitcher', - Extends: AltTab.SwitcherList, + Extends: SwitcherPopup.SwitcherList, _init : function(items) { this.parent(true); diff --git a/js/ui/switcherPopup.js b/js/ui/switcherPopup.js new file mode 100644 index 000000000..82ebcbfc8 --- /dev/null +++ b/js/ui/switcherPopup.js @@ -0,0 +1,640 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const Gtk = imports.gi.Gtk; +const Lang = imports.lang; +const Mainloop = imports.mainloop; +const Shell = imports.gi.Shell; +const Signals = imports.signals; +const St = imports.gi.St; + +const Main = imports.ui.main; +const Tweener = imports.ui.tweener; + +const POPUP_DELAY_TIMEOUT = 150; // milliseconds + +const POPUP_SCROLL_TIME = 0.10; // seconds +const POPUP_FADE_OUT_TIME = 0.1; // seconds + +const DISABLE_HOVER_TIMEOUT = 500; // milliseconds + +function mod(a, b) { + return (a + b) % b; +} + +function primaryModifier(mask) { + if (mask == 0) + return 0; + + let primary = 1; + while (mask > 1) { + mask >>= 1; + primary <<= 1; + } + return primary; +} + +const SwitcherPopup = new Lang.Class({ + Name: 'SwitcherPopup', + Abstract: true, + + _init: function(items) { + this._switcherList = null; + + this._items = items || []; + this._selectedIndex = 0; + + this.actor = new Shell.GenericContainer({ style_class: 'switcher-popup', + reactive: true }); + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocate)); + this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); + + Main.uiGroup.add_actor(this.actor); + + this._haveModal = false; + this._modifierMask = 0; + + this._motionTimeoutId = 0; + this._initialDelayTimeoutId = 0; + + // Initially disable hover so we ignore the enter-event if + // the switcher appears underneath the current pointer location + this._disableHover(); + }, + + _getPreferredWidth: function(actor, forHeight, alloc) { + let primary = Main.layoutManager.primaryMonitor; + + alloc.min_size = primary.width; + alloc.natural_size = primary.width; + }, + + _getPreferredHeight: function(actor, forWidth, alloc) { + let primary = Main.layoutManager.primaryMonitor; + + alloc.min_size = primary.height; + alloc.natural_size = primary.height; + }, + + _allocate: function(actor, box, flags) { + let childBox = new Clutter.ActorBox(); + let primary = Main.layoutManager.primaryMonitor; + + let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); + let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); + let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); + let vPadding = this.actor.get_theme_node().get_vertical_padding(); + let hPadding = leftPadding + rightPadding; + + // Allocate the switcherList + // We select a size based on an icon size that does not overflow the screen + let [childMinHeight, childNaturalHeight] = this._switcherList.actor.get_preferred_height(primary.width - hPadding); + let [childMinWidth, childNaturalWidth] = this._switcherList.actor.get_preferred_width(childNaturalHeight); + childBox.x1 = Math.max(primary.x + leftPadding, primary.x + Math.floor((primary.width - childNaturalWidth) / 2)); + childBox.x2 = Math.min(primary.x + primary.width - rightPadding, childBox.x1 + childNaturalWidth); + childBox.y1 = primary.y + Math.floor((primary.height - childNaturalHeight) / 2); + childBox.y2 = childBox.y1 + childNaturalHeight; + this._switcherList.actor.allocate(childBox, flags); + }, + + _createSwitcher: function() { + throw new Error('Not implemented'); + }, + + _initialSelection: function(backward, binding) { + throw new Error('Not implemented'); + }, + + show: function(backward, binding, mask) { + if (!this._createSwitcher()) + return false; + + if (!Main.pushModal(this.actor)) { + // Probably someone else has a pointer grab, try again with keyboard only + if (!Main.pushModal(this.actor, { options: Meta.ModalOptions.POINTER_ALREADY_GRABBED })) { + return false; + } + } + this._haveModal = true; + this._modifierMask = primaryModifier(mask); + + this.actor.connect('key-press-event', Lang.bind(this, this._keyPressEvent)); + this.actor.connect('key-release-event', Lang.bind(this, this._keyReleaseEvent)); + + this.actor.connect('button-press-event', Lang.bind(this, this._clickedOutside)); + this.actor.connect('scroll-event', Lang.bind(this, this._scrollEvent)); + + this.actor.add_actor(this._switcherList.actor); + this._switcherList.connect('item-activated', Lang.bind(this, this._itemActivated)); + this._switcherList.connect('item-entered', Lang.bind(this, this._itemEntered)); + + // Need to force an allocation so we can figure out whether we + // need to scroll when selecting + this.actor.opacity = 0; + this.actor.show(); + this.actor.get_allocation_box(); + + if (this._items.length > 1) + this._selectedIndex = 1; + else + this._selectedIndex = 0; + + this._initialSelection(backward, binding); + + // 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 [x, y, mods] = global.get_pointer(); + if (!(mods & this._modifierMask)) { + this._finish(); + return false; + } + + // We delay showing the popup so that fast Alt+Tab users aren't + // disturbed by the popup briefly flashing. + this._initialDelayTimeoutId = Mainloop.timeout_add(POPUP_DELAY_TIMEOUT, + Lang.bind(this, function () { + this.actor.opacity = 255; + this._initialDelayTimeoutId = 0; + })); + return true; + }, + + _next: function() { + return mod(this._selectedIndex + 1, this._items.length); + }, + + _previous: function() { + return mod(this._selectedIndex - 1, this._items.length); + }, + + _keyPressHandler: function(keysym, backwards, action) { + throw new Error('Not implemented'); + }, + + _keyPressEvent: function(actor, event) { + let keysym = event.get_key_symbol(); + let event_state = event.get_state(); + let backwards = event_state & Clutter.ModifierType.SHIFT_MASK; + let action = global.display.get_keybinding_action(event.get_key_code(), event_state); + + this._disableHover(); + + if (keysym == Clutter.Escape) + this.destroy(); + else + this._keyPressHandler(keysym, backwards, action); + + return true; + }, + + _keyReleaseEvent: function(actor, event) { + let [x, y, mods] = global.get_pointer(); + let state = mods & this._modifierMask; + + if (state == 0) + this._finish(); + + return true; + }, + + _clickedOutside: function(actor, event) { + this.destroy(); + }, + + _scrollHandler: function(direction) { + if (direction == Clutter.ScrollDirection.UP) + this._select(this._previous()); + else if (direction == Clutter.ScrollDirection.DOWN) + this._select(this._next()); + }, + + _scrollEvent: function(actor, event) { + this._scrollHandler(event.get_scroll_direction()); + }, + + _itemActivatedHandler: function(n) { + this._select(n); + }, + + _itemActivated: function(switcher, n) { + this._itemActivatedHandler(n); + this._finish(); + }, + + _itemEnteredHandler: function(n) { + this._select(n); + }, + + _itemEntered: function(switcher, n) { + if (!this.mouseActive) + return; + this._itemEnteredHandler(n); + }, + + _disableHover: function() { + this.mouseActive = false; + + if (this._motionTimeoutId != 0) + Mainloop.source_remove(this._motionTimeoutId); + + this._motionTimeoutId = Mainloop.timeout_add(DISABLE_HOVER_TIMEOUT, Lang.bind(this, this._mouseTimedOut)); + }, + + _mouseTimedOut: function() { + this._motionTimeoutId = 0; + this.mouseActive = true; + }, + + _popModal: function() { + if (this._haveModal) { + Main.popModal(this.actor); + this._haveModal = false; + } + }, + + destroy: function() { + this._popModal(); + if (this.actor.visible) { + Tweener.addTween(this.actor, + { opacity: 0, + time: POPUP_FADE_OUT_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, + function() { + this.actor.destroy(); + }) + }); + } else + this.actor.destroy(); + }, + + _finish: function() { + this.destroy(); + }, + + _onDestroy: function() { + this._popModal(); + + if (this._motionTimeoutId != 0) + Mainloop.source_remove(this._motionTimeoutId); + if (this._initialDelayTimeoutId != 0) + Mainloop.source_remove(this._initialDelayTimeoutId); + }, + + _select: function(num) { + this._selectedIndex = num; + this._switcherList.highlight(num); + } +}); + +const SwitcherList = new Lang.Class({ + Name: 'SwitcherList', + + _init : function(squareItems) { + this.actor = new Shell.GenericContainer({ style_class: 'switcher-list' }); + this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); + this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); + this.actor.connect('allocate', Lang.bind(this, this._allocateTop)); + + // 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({ style_class: 'switcher-list-item-container' }); + this._list.spacing = 0; + this._list.connect('style-changed', Lang.bind(this, function() { + this._list.spacing = this._list.get_theme_node().get_length('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._scrollView = new St.ScrollView({ style_class: 'hfade', + enable_mouse_scrolling: false }); + this._scrollView.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER); + + let scrollBox = new St.BoxLayout(); + scrollBox.add_actor(this._list); + this._scrollView.add_actor(scrollBox); + this.actor.add_actor(this._scrollView); + + // Those arrows indicate whether scrolling in one direction is possible + this._leftArrow = new St.DrawingArea({ style_class: 'switcher-arrow', + pseudo_class: 'highlighted' }); + this._leftArrow.connect('repaint', Lang.bind(this, + function() { drawArrow(this._leftArrow, St.Side.LEFT); })); + this._rightArrow = new St.DrawingArea({ style_class: 'switcher-arrow', + pseudo_class: 'highlighted' }); + this._rightArrow.connect('repaint', Lang.bind(this, + function() { drawArrow(this._rightArrow, St.Side.RIGHT); })); + + this.actor.add_actor(this._leftArrow); + this.actor.add_actor(this._rightArrow); + + this._items = []; + this._highlighted = -1; + this._separator = null; + this._squareItems = squareItems; + this._minSize = 0; + this._scrollableRight = true; + this._scrollableLeft = false; + }, + + _allocateTop: function(actor, box, flags) { + let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); + let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); + + let childBox = new Clutter.ActorBox(); + let scrollable = this._minSize > box.x2 - box.x1; + + box.y1 -= this.actor.get_theme_node().get_padding(St.Side.TOP); + box.y2 += this.actor.get_theme_node().get_padding(St.Side.BOTTOM); + this._scrollView.allocate(box, flags); + + let arrowWidth = Math.floor(leftPadding / 3); + let arrowHeight = arrowWidth * 2; + childBox.x1 = leftPadding / 2; + childBox.y1 = this.actor.height / 2 - arrowWidth; + childBox.x2 = childBox.x1 + arrowWidth; + childBox.y2 = childBox.y1 + arrowHeight; + this._leftArrow.allocate(childBox, flags); + this._leftArrow.opacity = (this._scrollableLeft && scrollable) ? 255 : 0; + + arrowWidth = Math.floor(rightPadding / 3); + arrowHeight = arrowWidth * 2; + childBox.x1 = this.actor.width - arrowWidth - rightPadding / 2; + childBox.y1 = this.actor.height / 2 - arrowWidth; + childBox.x2 = childBox.x1 + arrowWidth; + childBox.y2 = childBox.y1 + arrowHeight; + this._rightArrow.allocate(childBox, flags); + this._rightArrow.opacity = (this._scrollableRight && scrollable) ? 255 : 0; + }, + + addItem : function(item, label) { + let bbox = new St.Button({ style_class: 'item-box', + reactive: true }); + + bbox.set_child(item); + this._list.add_actor(bbox); + + let n = this._items.length; + bbox.connect('clicked', Lang.bind(this, function() { this._onItemClicked(n); })); + bbox.connect('enter-event', Lang.bind(this, function() { this._onItemEnter(n); })); + + bbox.label_actor = label; + + this._items.push(bbox); + + return bbox; + }, + + _onItemClicked: function (index) { + this._itemActivated(index); + }, + + _onItemEnter: function (index) { + this._itemEntered(index); + }, + + addSeparator: function () { + let box = new St.Bin({ style_class: 'separator' }); + this._separator = box; + this._list.add_actor(box); + }, + + highlight: function(index, justOutline) { + if (this._highlighted != -1) { + this._items[this._highlighted].remove_style_pseudo_class('outlined'); + this._items[this._highlighted].remove_style_pseudo_class('selected'); + } + + this._highlighted = index; + + if (this._highlighted != -1) { + if (justOutline) + this._items[this._highlighted].add_style_pseudo_class('outlined'); + else + this._items[this._highlighted].add_style_pseudo_class('selected'); + } + + let adjustment = this._scrollView.hscroll.adjustment; + let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); + let [absItemX, absItemY] = this._items[index].get_transformed_position(); + let [result, posX, posY] = this.actor.transform_stage_point(absItemX, 0); + let [containerWidth, containerHeight] = this.actor.get_transformed_size(); + if (posX + this._items[index].get_width() > containerWidth) + this._scrollToRight(); + else if (this._items[index].allocation.x1 - value < 0) + this._scrollToLeft(); + + }, + + _scrollToLeft : function() { + let adjustment = this._scrollView.hscroll.adjustment; + let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); + + let item = this._items[this._highlighted]; + + if (item.allocation.x1 < value) + value = Math.min(0, item.allocation.x1); + else if (item.allocation.x2 > value + pageSize) + value = Math.max(upper, item.allocation.x2 - pageSize); + + this._scrollableRight = true; + Tweener.addTween(adjustment, + { value: value, + time: POPUP_SCROLL_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function () { + if (this._highlighted == 0) { + this._scrollableLeft = false; + this.actor.queue_relayout(); + } + }) + }); + }, + + _scrollToRight : function() { + let adjustment = this._scrollView.hscroll.adjustment; + let [value, lower, upper, stepIncrement, pageIncrement, pageSize] = adjustment.get_values(); + + let item = this._items[this._highlighted]; + + if (item.allocation.x1 < value) + value = Math.max(0, item.allocation.x1); + else if (item.allocation.x2 > value + pageSize) + value = Math.min(upper, item.allocation.x2 - pageSize); + + this._scrollableLeft = true; + Tweener.addTween(adjustment, + { value: value, + time: POPUP_SCROLL_TIME, + transition: 'easeOutQuad', + onComplete: Lang.bind(this, function () { + if (this._highlighted == this._items.length - 1) { + this._scrollableRight = false; + this.actor.queue_relayout(); + } + }) + }); + }, + + _itemActivated: function(n) { + this.emit('item-activated', n); + }, + + _itemEntered: function(n) { + this.emit('item-entered', 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.natural_size = alloc.min_size; + this._minSize = alloc.min_size; + }, + + _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 = maxChildMin; + } + + alloc.min_size = maxChildMin; + alloc.natural_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(); + + let primary = Main.layoutManager.primaryMonitor; + let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT); + + 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 drawArrow(area, side) { + let themeNode = area.get_theme_node(); + let borderColor = themeNode.get_border_color(side); + let bodyColor = themeNode.get_foreground_color(); + + let [width, height] = area.get_surface_size (); + let cr = area.get_context(); + + cr.setLineWidth(1.0); + Clutter.cairo_set_source_color(cr, borderColor); + + switch (side) { + case St.Side.TOP: + cr.moveTo(0, height); + cr.lineTo(Math.floor(width * 0.5), 0); + cr.lineTo(width, height); + break; + + case St.Side.BOTTOM: + cr.moveTo(width, 0); + cr.lineTo(Math.floor(width * 0.5), height); + cr.lineTo(0, 0); + break; + + case St.Side.LEFT: + cr.moveTo(width, height); + cr.lineTo(0, Math.floor(height * 0.5)); + cr.lineTo(width, 0); + break; + + case St.Side.RIGHT: + cr.moveTo(0, 0); + cr.lineTo(width, Math.floor(height * 0.5)); + cr.lineTo(0, height); + break; + } + + cr.strokePreserve(); + + Clutter.cairo_set_source_color(cr, bodyColor); + cr.fill(); +} + diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js index c4896eea8..42b2e6697 100644 --- a/js/ui/windowManager.js +++ b/js/ui/windowManager.js @@ -621,7 +621,7 @@ const WindowManager = new Lang.Class({ _startA11ySwitcher : function(display, screen, window, binding) { let modifiers = binding.get_modifiers(); let backwards = modifiers & Meta.VirtualModifier.SHIFT_MASK; - Main.ctrlAltTabManager.popup(backwards, binding.get_mask()); + Main.ctrlAltTabManager.popup(backwards, binding.get_name(), binding.get_mask()); }, _openAppMenu : function(display, screen, window, event, binding) {