// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- const Lang = imports.lang; const Meta = imports.gi.Meta; const St = imports.gi.St; const Params = imports.misc.params; const Tweener = imports.ui.tweener; /** * Lightbox: * @container: parent Clutter.Container * @params: (optional) additional parameters: * - inhibitEvents: whether to inhibit events for @container * - width: shade actor width * - height: shade actor height * - fadeTime: seconds used to fade in/out * * 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 in @params. */ const Lightbox = new Lang.Class({ Name: 'Lightbox', _init : function(container, params) { params = Params.parse(params, { inhibitEvents: false, width: null, height: null, fadeTime: null }); this._container = container; this._children = container.get_children(); this._fadeTime = params.fadeTime; this.actor = new St.Bin({ x: 0, y: 0, style_class: 'lightbox', reactive: params.inhibitEvents }); container.add_actor(this.actor); this.actor.raise_top(); this.actor.hide(); this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); if (params.width && params.height) { this.actor.width = params.width; this.actor.height = params.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) { Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { this.actor.width = this.width; this.actor.height = this.height; return false; })); this.width = this._container.width; this.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.push(newChild); } else if (newChildIndex == 0) { // Bottom of stack this._children.unshift(newChild); } else { // Somewhere else; insert it into the correct spot let prevChild = this._children.indexOf(children[newChildIndex - 1]); if (prevChild != -1) // paranoia this._children.splice(prevChild + 1, 0, newChild); } }, show: function() { if (this._fadeTime) { this.actor.opacity = 0; Tweener.addTween(this.actor, { opacity: 255, time: this._fadeTime, transition: 'easeOutQuad' }); } else { this.actor.opacity = 255; } this.actor.show(); }, hide: function() { if (this._fadeTime) { Tweener.addTween(this.actor, { opacity: 0, time: this._fadeTime, transition: 'easeOutQuad', onComplete: Lang.bind(this, function() { this.actor.hide(); }) }); } else { this.actor.hide(); } }, _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 = this._children.length - 1; i >= 0; 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. */ destroy : function() { this.actor.destroy(); }, /** * _onDestroy: * * This is called when the lightbox' actor is destroyed, either * by destroying its container or by explicitly calling this.destroy(). */ _onDestroy: function() { if (this._allocationChangedSignalId != 0) this._container.disconnect(this._allocationChangedSignalId); this._container.disconnect(this._actorAddedSignalId); this._container.disconnect(this._actorRemovedSignalId); this.highlight(null); } });