gnome-shell/js/ui/lightbox.js

206 lines
7.3 KiB
JavaScript
Raw Permalink Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;
const Meta = imports.gi.Meta;
const Signals = imports.signals;
const St = imports.gi.St;
const Params = imports.misc.params;
const Tweener = imports.ui.tweener;
const DEFAULT_FADE_FACTOR = 0.4;
/**
* Lightbox:
* @container: parent Clutter.Container
* @params: (optional) additional parameters:
* - inhibitEvents: whether to inhibit events for @container
* - width: shade actor width
* - height: shade actor height
* - fadeInTime: seconds used to fade in
* - fadeOutTime: seconds used to fade 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,
fadeInTime: null,
fadeOutTime: null,
fadeFactor: DEFAULT_FADE_FACTOR
});
this._container = container;
this._children = container.get_children();
this._fadeInTime = params.fadeInTime;
this._fadeOutTime = params.fadeOutTime;
this._fadeFactor = params.fadeFactor;
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.shown = false;
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
if (params.width && params.height) {
this.actor.width = params.width;
this.actor.height = params.height;
} else {
let constraint = new Clutter.BindConstraint({ source: container,
coordinate: Clutter.BindCoordinate.ALL });
this.actor.add_constraint(constraint);
}
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;
},
_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() {
Tweener.removeTweens(this.actor);
if (this._fadeInTime) {
this.shown = false;
this.actor.opacity = 0;
Tweener.addTween(this.actor,
{ opacity: 255 * this._fadeFactor,
time: this._fadeInTime,
transition: 'easeOutQuad',
onComplete: Lang.bind(this, function() {
this.shown = true;
this.emit('shown');
})
});
} else {
this.actor.opacity = 255 * this._fadeFactor;
this.shown = true;
this.emit('shown');
}
this.actor.show();
},
hide: function() {
this.shown = false;
Tweener.removeTweens(this.actor);
if (this._fadeOutTime) {
Tweener.addTween(this.actor,
{ opacity: 0,
time: this._fadeOutTime,
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() {
this._container.disconnect(this._actorAddedSignalId);
this._container.disconnect(this._actorRemovedSignalId);
this.highlight(null);
}
});
Signals.addSignalMethods(Lightbox.prototype);