diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index 0d92e4484..af5dca715 100644 --- a/js/ui/Makefile.am +++ b/js/ui/Makefile.am @@ -10,6 +10,7 @@ dist_jsui_DATA = \ dnd.js \ docDisplay.js \ genericDisplay.js \ + lightbox.js \ link.js \ lookingGlass.js \ main.js \ diff --git a/js/ui/altTab.js b/js/ui/altTab.js index 206292a4c..fbc1268a7 100644 --- a/js/ui/altTab.js +++ b/js/ui/altTab.js @@ -7,6 +7,7 @@ const Meta = imports.gi.Meta; const Pango = imports.gi.Pango; const Shell = imports.gi.Shell; +const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Tweener = imports.ui.tweener; @@ -24,10 +25,6 @@ const POPUP_NUM_COLUMNS = 5; const POPUP_LABEL_MAX_WIDTH = POPUP_NUM_COLUMNS * (POPUP_ICON_SIZE + POPUP_GRID_SPACING); -const OVERLAY_COLOR = new Clutter.Color(); -OVERLAY_COLOR.from_pixel(0x00000044); - -const SHOW_TIME = 0.05; const SWITCH_TIME = 0.1; function AltTabPopup() { @@ -73,19 +70,8 @@ AltTabPopup.prototype = { this.actor.append(this._indicator, Big.BoxPackFlags.FIXED); this._items = []; - this._toplevels = global.window_group.get_children(); global.stage.add_actor(this.actor); - - // Dark translucent window used to cover all but the - // currently-selected window while Alt-Tabbing. - this._overlay = new Clutter.Rectangle({ color: OVERLAY_COLOR, - x: 0, - y: 0, - width: global.screen_width, - height: global.screen_height, - border_width: 0, - reactive: true }); }, addWindow : function(win) { @@ -101,14 +87,6 @@ AltTabPopup.prototype = { item.box = new Big.Box({ padding: POPUP_INDICATOR_WIDTH * 2 }); item.box.append(item.icon, Big.BoxPackFlags.NONE); - item.above = null; - for (let i = 1; i < this._toplevels.length; i++) { - if (this._toplevels[i] == win) { - item.above = this._toplevels[i - 1]; - break; - } - } - item.n = this._items.length; this._items.push(item); @@ -122,13 +100,11 @@ AltTabPopup.prototype = { }, show : function(initialSelection) { - global.window_group.add_actor(this._overlay); - this._overlay.raise_top(); - this._overlay.show(); - this.actor.opacity = 0; - Tweener.addTween(this.actor, { opacity: 255, - time: SHOW_TIME, - transition: "easeOutQuad" }); + // 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.actor.x = Math.floor((global.screen_width - this.actor.width) / 2); @@ -139,7 +115,7 @@ AltTabPopup.prototype = { destroy : function() { this.actor.destroy(); - this._overlay.destroy(); + this._lightbox.destroy(); }, select : function(n) { @@ -150,11 +126,6 @@ AltTabPopup.prototype = { this._selected.box.disconnect(this._allocationChangedId); delete this._allocationChangedId; } - - if (this._selected.above) - this._selected.window.raise(this._selected.above); - else - this._selected.window.lower_bottom(); } let item = this._items[n]; @@ -193,8 +164,7 @@ AltTabPopup.prototype = { } this._indicator.show(); - if (this._overlay.visible) - this._selected.window.raise(this._overlay); + this._lightbox.highlight(this._selected.window); this._allocationChangedId = this._selected.box.connect('notify::allocation', @@ -202,6 +172,7 @@ AltTabPopup.prototype = { } else { this._label.text = ""; this._indicator.hide(); + this._lightbox.highlight(null); } }, diff --git a/js/ui/lightbox.js b/js/ui/lightbox.js new file mode 100644 index 000000000..17ae9a7f3 --- /dev/null +++ b/js/ui/lightbox.js @@ -0,0 +1,151 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Clutter = imports.gi.Clutter; +const Lang = imports.lang; + +const Main = imports.ui.main; +const Tweener = imports.ui.tweener; + +const SHADE_COLOR = new Clutter.Color(); +SHADE_COLOR.from_pixel(0x00000044); + +/** + * Lightbox: + * @container: parent Clutter.Container + * @width: (optional) shade actor width + * @height: (optional) shade actor height + * + * Lightbox creates a dark translucent "shade" actor to hide the + * contents of @container, and allows you to specify particular actors + * in @container to highlight by bringing them above the shade. It + * tracks added and removed actors in @container while the lightboxing + * is active, and ensures that all actors are returned to their + * original stacking order when the lightboxing is removed. (However, + * if actors are restacked by outside code while the lightboxing is + * active, the lightbox may later revert them back to their original + * order.) + * + * By default, the shade window will have the height and width of + * @container and will track any changes in its size. You can override + * this by passing an explicit width and height + */ +function Lightbox(container, width, height) { + this._init(container, width, height); +} + +Lightbox.prototype = { + _init : function(container, width, height) { + this._container = container; + this._children = container.get_children(); + this.actor = new Clutter.Rectangle({ color: SHADE_COLOR, + x: 0, + y: 0, + border_width: 0, + reactive: true }); + + container.add_actor(this.actor); + this.actor.raise_top(); + + this._destroySignalId = this.actor.connect('destroy', Lang.bind(this, this.destroy)); + + if (width && height) { + this.actor.width = width; + this.actor.height = height; + this._allocationChangedSignalId = 0; + } else { + this.actor.width = container.width; + this.actor.height = container.height; + this._allocationChangedSignalId = container.connect('allocation-changed', Lang.bind(this, this._allocationChanged)); + } + + this._actorAddedSignalId = container.connect('actor-added', Lang.bind(this, this._actorAdded)); + this._actorRemovedSignalId = container.connect('actor-removed', Lang.bind(this, this._actorRemoved)); + + this._highlighted = null; + }, + + _allocationChanged : function(container, box, flags) { + this.actor.width = this._container.width; + this.actor.height = this._container.height; + }, + + _actorAdded : function(container, newChild) { + let children = this._container.get_children(); + let myIndex = children.indexOf(this.actor); + let newChildIndex = children.indexOf(newChild); + + if (newChildIndex < myIndex) { + // The child was added above the shade (presumably it was + // made the new top-most child). Move it below the shade, + // and add it to this._children as the new topmost actor. + newChild.lower(this.actor); + this._children.unshift(newChild); + } else if (newChildIndex == children.length - 1) { + // Bottom of stack + this._children.push(newChild); + } else { + // Somewhere else; insert it into the correct spot + let nextChild = this._children.indexOf(children[newChildIndex + 1]); + if (nextChild != -1) // paranoia + this._children.splice(nextChild, 0, newChild); + } + }, + + _actorRemoved : function(container, child) { + let index = this._children.indexOf(child); + if (index != -1) // paranoia + this._children.splice(index, 1); + + if (child == this._highlighted) + this._highlighted = null; + }, + + /** + * highlight: + * @window: actor to highlight + * + * Highlights the indicated actor and unhighlights any other + * currently-highlighted actor. With no arguments or a false/null + * argument, all actors will be unhighlighted. + */ + highlight : function(window) { + if (this._highlighted == window) + return; + + // Walk this._children raising and lowering actors as needed. + // Things get a little tricky if the to-be-raised and + // to-be-lowered actors were originally adjacent, in which + // case we may need to indicate some *other* actor as the new + // sibling of the to-be-lowered one. + + let below = this.actor; + for (let i = 0; i < this._children.length; i++) { + if (this._children[i] == window) + this._children[i].raise_top(); + else if (this._children[i] == this._highlighted) + this._children[i].lower(below); + else + below = this._children[i]; + } + + this._highlighted = window; + }, + + /** + * destroy: + * + * Destroys the lightbox. This is called automatically if the + * lightbox's container is destroyed. + */ + destroy : function() { + if (this._allocationChangedSignalId != 0) + this._container.disconnect(this._allocationChangedSignalId); + this._container.disconnect(this._actorAddedSignalId); + this._container.disconnect(this._actorRemovedSignalId); + + this.actor.disconnect(this._destroySignalId); + + this.highlight(null); + this.actor.destroy(); + } +}; diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js index ca9f06eb1..d6d7994ad 100644 --- a/js/ui/runDialog.js +++ b/js/ui/runDialog.js @@ -11,11 +11,9 @@ const Signals = imports.signals; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; +const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; -const OVERLAY_COLOR = new Clutter.Color(); -OVERLAY_COLOR.from_pixel(0x00000044); - const BOX_BACKGROUND_COLOR = new Clutter.Color(); BOX_BACKGROUND_COLOR.from_pixel(0x000000cc); @@ -66,12 +64,7 @@ RunDialog.prototype = { this._group = new Clutter.Group({ visible: false }); global.stage.add_actor(this._group); - this._overlay = new Clutter.Rectangle({ color: OVERLAY_COLOR, - width: global.screen_width, - height: global.screen_height, - border_width: 0, - reactive: true }); - this._group.add_actor(this._overlay); + this._lightbox = new Lightbox.Lightbox(this._group); let boxH = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL, x_align: Big.BoxAlignment.CENTER, @@ -80,6 +73,7 @@ RunDialog.prototype = { height: global.screen_height }); this._group.add_actor(boxH); + this._lightbox.highlight(boxH); let boxV = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, y_align: Big.BoxAlignment.CENTER }); diff --git a/js/ui/workspaces.js b/js/ui/workspaces.js index de9e60f24..cd7f6b0c9 100644 --- a/js/ui/workspaces.js +++ b/js/ui/workspaces.js @@ -12,6 +12,7 @@ const Shell = imports.gi.Shell; const Signals = imports.signals; const DND = imports.ui.dnd; +const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Overview = imports.ui.overview; const Panel = imports.ui.panel; @@ -25,8 +26,6 @@ const WINDOWCLONE_TITLE_COLOR = new Clutter.Color(); WINDOWCLONE_TITLE_COLOR.from_pixel(0xffffffff); const FRAME_COLOR = new Clutter.Color(); FRAME_COLOR.from_pixel(0xffffffff); -const LIGHTBOX_COLOR = new Clutter.Color(); -LIGHTBOX_COLOR.from_pixel(0x00000044); const SCROLL_SCALE_AMOUNT = 100 / 5; @@ -206,21 +205,7 @@ WindowClone.prototype = { }, _zoomStart : function () { - this._zoomOverlay = new Clutter.Rectangle({ reactive: true, - color: LIGHTBOX_COLOR, - border_width: 0, - x: 0, - y: 0, - width: global.screen_width, - height: global.screen_height, - opacity: 0 }); - this._zoomOverlay.show(); - global.stage.add_actor(this._zoomOverlay); - Tweener.addTween(this._zoomOverlay, - { opacity: 255, - time: ZOOM_OVERLAY_FADE_TIME, - transition: "easeOutQuad" - }); + this._zoomLightbox = new Lightbox.Lightbox(global.stage); this._zoomLocalOrig = new ScaledPoint(this.actor.x, this.actor.y, this.actor.scale_x, this.actor.scale_y); this._zoomGlobalOrig = new ScaledPoint(); @@ -229,10 +214,8 @@ WindowClone.prototype = { this._zoomGlobalOrig.setPosition.apply(this._zoomGlobalOrig, this.actor.get_transformed_position()); this._zoomGlobalOrig.setScale(width / this.actor.width, height / this.actor.height); - this._zoomOverlay.raise_top(); - this._zoomOverlay.show(); - this.actor.reparent(global.stage); + this._zoomLightbox.highlight(this.actor); [this.actor.x, this.actor.y] = this._zoomGlobalOrig.getPosition(); [this.actor.scale_x, this.actor.scale_y] = this._zoomGlobalOrig.getScale(); @@ -255,7 +238,7 @@ WindowClone.prototype = { this._adjustTitle(); - this._zoomOverlay.destroy(); + this._zoomLightbox.destroy(); Main.overview.disconnect(this._hideEventId); this._zoomLocalPosition = undefined; @@ -263,8 +246,8 @@ WindowClone.prototype = { this._zoomGlobalPosition = undefined; this._zoomGlobalScale = undefined; this._zoomTargetPosition = undefined; - this._zoomStep = undefined; - this._zoomOverlay = undefined; + this._zoomStep = undefined; + this._zoomLightbox = undefined; }, _onButtonRelease : function (actor, event) { @@ -431,14 +414,6 @@ Workspace.prototype = { this.actor.height = global.screen_height; this.scale = 1.0; - this._lightbox = new Clutter.Rectangle({ color: LIGHTBOX_COLOR }); - this.actor.connect('notify::allocation', Lang.bind(this, function () { - let [width, height] = this.actor.get_size(); - this._lightbox.set_size(width, height); - })); - this.actor.add_actor(this._lightbox); - this._lightbox.hide(); - let windows = global.get_windows().filter(this._isMyWindow, this); // Find the desktop window @@ -573,10 +548,10 @@ Workspace.prototype = { */ setLightboxMode: function (showLightbox) { if (showLightbox) { - this.setHighlightWindow(null); - this._lightbox.show(); + this._lightbox = new Lightbox.Lightbox(this.actor); } else { - this._lightbox.hide(); + this._lightbox.destroy(); + this._lightbox = null; } }, @@ -587,13 +562,12 @@ Workspace.prototype = { * Draw the user's attention to the given window @metaWindow. */ setHighlightWindow: function (metaWindow) { - for (let i = 0; i < this._windows.length; i++) { - this._windows[i].actor.lower(this._lightbox); - } + let actor; if (metaWindow != null) { let clone = this.lookupCloneForMetaWindow(metaWindow); - clone.actor.raise(this._lightbox); + actor = clone.actor; } + this._lightbox.highlight(actor); }, _adjustRemoveButton : function() {