360 lines
14 KiB
JavaScript
360 lines
14 KiB
JavaScript
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||
|
|
||
|
const Big = imports.gi.Big;
|
||
|
const Clutter = imports.gi.Clutter;
|
||
|
const Shell = imports.gi.Shell;
|
||
|
const Lang = imports.lang;
|
||
|
const Mainloop = imports.mainloop;
|
||
|
|
||
|
const Main = imports.ui.main;
|
||
|
const Tweener = imports.ui.tweener;
|
||
|
const Widget = imports.ui.widget;
|
||
|
|
||
|
const WIDGETBOX_BG_COLOR = new Clutter.Color();
|
||
|
WIDGETBOX_BG_COLOR.from_pixel(0xf0f0f0ff);
|
||
|
const BLACK = new Clutter.Color();
|
||
|
BLACK.from_pixel(0x000000ff);
|
||
|
|
||
|
const WIDGETBOX_PADDING = 4;
|
||
|
const ANIMATION_TIME = 0.5;
|
||
|
const POP_IN_LAG = 250; /* milliseconds */
|
||
|
|
||
|
function WidgetBox(widget) {
|
||
|
this._init(widget);
|
||
|
}
|
||
|
|
||
|
WidgetBox.prototype = {
|
||
|
_init: function(widget) {
|
||
|
if (widget instanceof Widget.Widget)
|
||
|
this._widget = widget;
|
||
|
else {
|
||
|
let ctor = this._ctorFromName(widget);
|
||
|
this._widget = new ctor();
|
||
|
}
|
||
|
|
||
|
if (!this._widget.actor)
|
||
|
throw new Error("widget has no actor");
|
||
|
else if (!this._widget.title && !this._widget.collapsedActor)
|
||
|
throw new Error("widget has neither title nor collapsedActor");
|
||
|
|
||
|
this.state = this._widget.state = Widget.STATE_EXPANDED;
|
||
|
|
||
|
// The structure of a WidgetBox:
|
||
|
//
|
||
|
// The top level is a Clutter.Group, which exists to make
|
||
|
// pop-out work correctly; when another widget pops out, its
|
||
|
// width will increase, which will in turn cause the sidebar's
|
||
|
// width to increase, which will cause the sidebar to increase
|
||
|
// the width of each of its children (the WidgetBoxes). But we
|
||
|
// don't want the non-popped-out widgets to expand, so we make
|
||
|
// the top-level actor be a Clutter.Group, which will accept
|
||
|
// the new width from the Sidebar, but not impose it on its
|
||
|
// own child.
|
||
|
//
|
||
|
// Inside the toplevel group is a horizontal Big.Box
|
||
|
// containing 2 Clutter.Groups; one for the collapsed state
|
||
|
// (cgroup) and one for the expanded state (egroup). Each
|
||
|
// group contains a single vertical Big.Box (cbox and ebox
|
||
|
// respectively), which have the appropriate fixed width. The
|
||
|
// cbox contains either the collapsed widget actor or else the
|
||
|
// rotated title. The ebox contains the horizontal title (if
|
||
|
// any), separator line, and the expanded widget actor. (If
|
||
|
// the widget doesn't have a collapsed actor, and therefore
|
||
|
// supports pop-out, then it will also have a vertical line
|
||
|
// between the two groups, which will only be shown during
|
||
|
// pop-out.)
|
||
|
//
|
||
|
// In the expanded view, cgroup is hidden and egroup is shown.
|
||
|
// When animating to the collapsed view, first the ebox is
|
||
|
// slid offscreen by giving it increasingly negative x
|
||
|
// coordinates within egroup. Then once it's fully offscreen,
|
||
|
// we hide egroup, show cgroup, and slide cbox back in in the
|
||
|
// same way.
|
||
|
//
|
||
|
// The pop-out view works similarly to the second half of the
|
||
|
// collapsed-to-expanded transition, except that the
|
||
|
// horizontal title gets hidden to avoid duplication.
|
||
|
|
||
|
this.actor = new Clutter.Group();
|
||
|
this._hbox = new Big.Box({ background_color: WIDGETBOX_BG_COLOR,
|
||
|
padding: WIDGETBOX_PADDING,
|
||
|
spacing: WIDGETBOX_PADDING,
|
||
|
corner_radius: WIDGETBOX_PADDING / 2,
|
||
|
orientation: Big.BoxOrientation.HORIZONTAL,
|
||
|
reactive: true });
|
||
|
this.actor.add_actor(this._hbox);
|
||
|
|
||
|
this._cgroup = new Clutter.Group({ clip_to_allocation: true });
|
||
|
this._hbox.append(this._cgroup, Big.BoxPackFlags.NONE);
|
||
|
|
||
|
this._cbox = new Big.Box({ width: Widget.COLLAPSED_WIDTH,
|
||
|
clip_to_allocation: true });
|
||
|
this._cgroup.add_actor(this._cbox);
|
||
|
|
||
|
if (this._widget.collapsedActor) {
|
||
|
if (this._widget.collapsedActor == this._widget.actor)
|
||
|
this._singleActor = true;
|
||
|
else {
|
||
|
this._cbox.append(this._widget.collapsedActor,
|
||
|
Big.BoxPackFlags.NONE);
|
||
|
}
|
||
|
} else {
|
||
|
let vtitle = new Clutter.Text({ font_name: "Sans 16px",
|
||
|
text: this._widget.title,
|
||
|
rotation_angle_z: -90.0 });
|
||
|
let signalId = vtitle.connect('notify::allocation',
|
||
|
function () {
|
||
|
vtitle.disconnect(signalId);
|
||
|
vtitle.set_anchor_point(vtitle.natural_width, 0);
|
||
|
vtitle.set_size(vtitle.natural_height,
|
||
|
vtitle.natural_width);
|
||
|
});
|
||
|
this._vtitle = vtitle;
|
||
|
this._cbox.append(this._vtitle, Big.BoxPackFlags.NONE);
|
||
|
|
||
|
this._vline = new Clutter.Rectangle({ color: BLACK, width: 1 });
|
||
|
this._hbox.append(this._vline, Big.BoxPackFlags.NONE);
|
||
|
this._vline.hide();
|
||
|
|
||
|
// Set up pop-out
|
||
|
this._eventHandler = this._hbox.connect('captured-event',
|
||
|
Lang.bind(this, this._popEventHandler));
|
||
|
this._activationHandler = this._widget.connect('activated',
|
||
|
Lang.bind(this, this._activationHandler));
|
||
|
}
|
||
|
this._cgroup.hide();
|
||
|
|
||
|
this._egroup = new Clutter.Group({ clip_to_allocation: true });
|
||
|
this._hbox.append(this._egroup, Big.BoxPackFlags.NONE);
|
||
|
|
||
|
this._ebox = new Big.Box({ spacing: WIDGETBOX_PADDING,
|
||
|
width: Widget.EXPANDED_WIDTH,
|
||
|
clip_to_allocation: true });
|
||
|
this._egroup.add_actor(this._ebox);
|
||
|
|
||
|
if (this._widget.title) {
|
||
|
this._htitle = new Clutter.Text({ font_name: "Sans 16px",
|
||
|
text: this._widget.title });
|
||
|
this._ebox.append(this._htitle, Big.BoxPackFlags.NONE);
|
||
|
|
||
|
this._hline = new Clutter.Rectangle({ color: BLACK, height: 1 });
|
||
|
this._ebox.append(this._hline, Big.BoxPackFlags.NONE);
|
||
|
}
|
||
|
|
||
|
this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);
|
||
|
},
|
||
|
|
||
|
// Given a name like "imports.ui.widget.ClockWidget", turn that
|
||
|
// into a constructor function
|
||
|
_ctorFromName: function(name) {
|
||
|
// Make sure it's a valid import
|
||
|
if (!name.match(/^imports(\.[a-zA-Z0-9_]+)+$/))
|
||
|
throw new Error("widget name must start with 'imports.'");
|
||
|
if (name.match(/^imports\.gi\./))
|
||
|
throw new Error("cannot import widget from GIR");
|
||
|
|
||
|
let ctor = eval(name);
|
||
|
|
||
|
// Make sure it's really a constructor
|
||
|
if (!ctor || typeof(ctor) != "function")
|
||
|
throw new Error("widget name is not a constructor");
|
||
|
|
||
|
// Make sure it's a widget
|
||
|
let proto = ctor.prototype;
|
||
|
while (proto && proto != Widget.Widget.prototype)
|
||
|
proto = proto.__proto__;
|
||
|
if (!proto)
|
||
|
throw new Error("widget does not inherit from Widget prototype");
|
||
|
|
||
|
return ctor;
|
||
|
},
|
||
|
|
||
|
expand: function() {
|
||
|
Tweener.addTween(this._cbox, { x: -Widget.COLLAPSED_WIDTH,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._expandPart1Complete,
|
||
|
onCompleteScope: this });
|
||
|
this.state = this._widget.state = Widget.STATE_EXPANDING;
|
||
|
},
|
||
|
|
||
|
_expandPart1Complete: function() {
|
||
|
this._cgroup.hide();
|
||
|
this._cbox.x = 0;
|
||
|
|
||
|
if (this._singleActor) {
|
||
|
log(this._widget.actor);
|
||
|
this._widget.actor.unparent();
|
||
|
this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);
|
||
|
}
|
||
|
|
||
|
if (this._widget.expand) {
|
||
|
try {
|
||
|
this._widget.expand();
|
||
|
} catch (e) {
|
||
|
logError(e, 'Widget failed to expand');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this._egroup.show();
|
||
|
if (this._htitle) {
|
||
|
this._htitle.show();
|
||
|
this._hline.show();
|
||
|
}
|
||
|
this._ebox.x = -Widget.EXPANDED_WIDTH;
|
||
|
Tweener.addTween(this._ebox, { x: 0,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._expandComplete,
|
||
|
onCompleteScope: this });
|
||
|
},
|
||
|
|
||
|
_expandComplete: function() {
|
||
|
this.state = this._widget.state = Widget.STATE_EXPANDED;
|
||
|
},
|
||
|
|
||
|
collapse: function() {
|
||
|
Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._collapsePart1Complete,
|
||
|
onCompleteScope: this });
|
||
|
this.state = this._widget.state = Widget.STATE_COLLAPSING;
|
||
|
},
|
||
|
|
||
|
_collapsePart1Complete: function() {
|
||
|
this._egroup.hide();
|
||
|
this._ebox.x = 0;
|
||
|
if (this._htitle) {
|
||
|
this._htitle.hide();
|
||
|
this._hline.hide();
|
||
|
}
|
||
|
|
||
|
if (this._singleActor) {
|
||
|
log(this._widget.actor);
|
||
|
this._widget.actor.unparent();
|
||
|
this._cbox.append(this._widget.actor, Big.BoxPackFlags.NONE);
|
||
|
}
|
||
|
|
||
|
if (this._widget.collapse) {
|
||
|
try {
|
||
|
this._widget.collapse();
|
||
|
} catch (e) {
|
||
|
logError(e, 'Widget failed to collapse');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this._cgroup.show();
|
||
|
this._cbox.x = -Widget.COLLAPSED_WIDTH;
|
||
|
if (this._vtitle)
|
||
|
this._cbox.height = this._ebox.height;
|
||
|
Tweener.addTween(this._cbox, { x: 0,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._collapseComplete,
|
||
|
onCompleteScope: this });
|
||
|
},
|
||
|
|
||
|
_collapseComplete: function() {
|
||
|
this.state = this._widget.state = Widget.STATE_COLLAPSED;
|
||
|
},
|
||
|
|
||
|
_popEventHandler: function(actor, event) {
|
||
|
let type = event.type();
|
||
|
|
||
|
if (type == Clutter.EventType.ENTER) {
|
||
|
this._clearPopInTimeout();
|
||
|
if (this.state == Widget.STATE_COLLAPSED ||
|
||
|
this.state == Widget.STATE_COLLAPSING) {
|
||
|
this._popOut();
|
||
|
return false;
|
||
|
}
|
||
|
} else if (type == Clutter.EventType.LEAVE &&
|
||
|
(this.state == Widget.STATE_POPPED_OUT ||
|
||
|
this.state == Widget.STATE_POPPING_OUT)) {
|
||
|
// If moving into another actor within this._hbox, let the
|
||
|
// event be propagated
|
||
|
let into = Shell.get_event_related(event);
|
||
|
while (into) {
|
||
|
if (into == this._hbox)
|
||
|
return false;
|
||
|
into = into.get_parent();
|
||
|
}
|
||
|
|
||
|
// Else, moving out of this._hbox
|
||
|
this._setPopInTimeout();
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
},
|
||
|
|
||
|
_activationHandler: function() {
|
||
|
if (this.state == Widget.STATE_POPPED_OUT)
|
||
|
this._popIn();
|
||
|
},
|
||
|
|
||
|
_popOut: function() {
|
||
|
if (this.state != Widget.STATE_COLLAPSED &&
|
||
|
this.state != Widget.STATE_COLLAPSING)
|
||
|
return;
|
||
|
|
||
|
this._vline.show();
|
||
|
this._egroup.show();
|
||
|
this._ebox.x = -Widget.EXPANDED_WIDTH;
|
||
|
Tweener.addTween(this._ebox, { x: 0,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._popOutComplete,
|
||
|
onCompleteScope: this });
|
||
|
this.state = this._widget.state = Widget.STATE_POPPING_OUT;
|
||
|
|
||
|
Main.chrome.addInputRegionActor(this._hbox);
|
||
|
},
|
||
|
|
||
|
_popOutComplete: function() {
|
||
|
this.state = this._widget.state = Widget.STATE_POPPED_OUT;
|
||
|
},
|
||
|
|
||
|
_setPopInTimeout: function() {
|
||
|
this._clearPopInTimeout();
|
||
|
this._popInTimeout = Mainloop.timeout_add(POP_IN_LAG, Lang.bind(this, function () { this._popIn(); return false; }));
|
||
|
},
|
||
|
|
||
|
_clearPopInTimeout: function() {
|
||
|
if (this._popInTimeout) {
|
||
|
Mainloop.source_remove(this._popInTimeout);
|
||
|
delete this._popInTimeout;
|
||
|
}
|
||
|
},
|
||
|
|
||
|
_popIn: function() {
|
||
|
this._clearPopInTimeout();
|
||
|
|
||
|
if (this.state != Widget.STATE_POPPED_OUT &&
|
||
|
this.state != Widget.STATE_POPPING_OUT)
|
||
|
return;
|
||
|
|
||
|
Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
|
||
|
time: ANIMATION_TIME / 2,
|
||
|
transition: "easeOutQuad",
|
||
|
onComplete: this._popInComplete,
|
||
|
onCompleteScope: this });
|
||
|
},
|
||
|
|
||
|
_popInComplete: function() {
|
||
|
this.state = this._widget.state = Widget.STATE_COLLAPSED;
|
||
|
this._vline.hide();
|
||
|
this._egroup.hide();
|
||
|
this._ebox.x = 0;
|
||
|
|
||
|
Main.chrome.removeInputRegionActor(this._hbox);
|
||
|
},
|
||
|
|
||
|
destroy: function() {
|
||
|
if (this._widget.destroy)
|
||
|
this._widget.destroy();
|
||
|
}
|
||
|
};
|
||
|
|