2011-09-28 09:16:26 -04:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
|
|
const Gdk = imports.gi.Gdk;
|
|
|
|
const Gio = imports.gi.Gio;
|
|
|
|
const GLib = imports.gi.GLib;
|
|
|
|
const Lang = imports.lang;
|
|
|
|
const Meta = imports.gi.Meta;
|
|
|
|
const Pango = imports.gi.Pango;
|
|
|
|
const St = imports.gi.St;
|
|
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const Signals = imports.signals;
|
|
|
|
|
|
|
|
const Params = imports.misc.params;
|
|
|
|
|
|
|
|
const Lightbox = imports.ui.lightbox;
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
const Tweener = imports.ui.tweener;
|
|
|
|
|
|
|
|
const OPEN_AND_CLOSE_TIME = 0.1;
|
2011-08-27 22:51:54 -04:00
|
|
|
const FADE_IN_BUTTONS_TIME = 0.33;
|
2010-10-20 20:46:38 -04:00
|
|
|
const FADE_OUT_DIALOG_TIME = 1.0;
|
|
|
|
|
|
|
|
const State = {
|
|
|
|
OPENED: 0,
|
|
|
|
CLOSED: 1,
|
|
|
|
OPENING: 2,
|
|
|
|
CLOSING: 3,
|
|
|
|
FADED_OUT: 4
|
|
|
|
};
|
|
|
|
|
|
|
|
function ModalDialog() {
|
|
|
|
this._init();
|
|
|
|
}
|
|
|
|
|
|
|
|
ModalDialog.prototype = {
|
|
|
|
_init: function(params) {
|
2011-08-27 23:04:56 -04:00
|
|
|
params = Params.parse(params, { shellReactive: false,
|
|
|
|
styleClass: null });
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
this.state = State.CLOSED;
|
2011-03-18 11:28:18 -04:00
|
|
|
this._hasModal = false;
|
2011-08-27 23:04:56 -04:00
|
|
|
this._shellReactive = params.shellReactive;
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
this._group = new St.Group({ visible: false,
|
|
|
|
x: 0,
|
|
|
|
y: 0 });
|
|
|
|
Main.uiGroup.add_actor(this._group);
|
2011-03-16 18:57:05 -04:00
|
|
|
|
|
|
|
let constraint = new Clutter.BindConstraint({ source: global.stage,
|
|
|
|
coordinate: Clutter.BindCoordinate.POSITION | Clutter.BindCoordinate.SIZE });
|
|
|
|
this._group.add_constraint(constraint);
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
this._group.connect('destroy', Lang.bind(this, this._onGroupDestroy));
|
|
|
|
|
|
|
|
this._actionKeys = {};
|
|
|
|
this._group.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
|
|
|
|
|
|
this._backgroundBin = new St.Bin();
|
|
|
|
this._group.add_actor(this._backgroundBin);
|
2011-03-18 11:28:18 -04:00
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
this._dialogLayout = new St.BoxLayout({ style_class: 'modal-dialog',
|
|
|
|
vertical: true });
|
|
|
|
if (params.styleClass != null) {
|
|
|
|
this._dialogLayout.add_style_class_name(params.styleClass);
|
|
|
|
}
|
2011-08-27 23:04:56 -04:00
|
|
|
|
|
|
|
if (!this._shellReactive) {
|
|
|
|
this._lightbox = new Lightbox.Lightbox(this._group,
|
|
|
|
{ inhibitEvents: true });
|
|
|
|
this._lightbox.highlight(this._backgroundBin);
|
|
|
|
|
|
|
|
let stack = new Shell.Stack();
|
|
|
|
this._backgroundBin.child = stack;
|
|
|
|
|
|
|
|
this._eventBlocker = new Clutter.Group({ reactive: true });
|
|
|
|
stack.add_actor(this._eventBlocker);
|
|
|
|
stack.add_actor(this._dialogLayout);
|
|
|
|
} else {
|
|
|
|
this._backgroundBin.child = this._dialogLayout;
|
|
|
|
}
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
this.contentLayout = new St.BoxLayout({ vertical: true });
|
|
|
|
this._dialogLayout.add(this.contentLayout,
|
|
|
|
{ x_fill: true,
|
|
|
|
y_fill: true,
|
|
|
|
x_align: St.Align.MIDDLE,
|
|
|
|
y_align: St.Align.START });
|
|
|
|
|
2011-03-21 16:07:22 -04:00
|
|
|
this._buttonLayout = new St.BoxLayout({ style_class: 'modal-dialog-button-box',
|
|
|
|
vertical: false });
|
2010-10-20 20:46:38 -04:00
|
|
|
this._dialogLayout.add(this._buttonLayout,
|
|
|
|
{ expand: true,
|
|
|
|
x_align: St.Align.MIDDLE,
|
|
|
|
y_align: St.Align.END });
|
2011-04-06 12:54:47 -04:00
|
|
|
|
|
|
|
global.focus_manager.add_group(this._dialogLayout);
|
|
|
|
this._initialKeyFocus = this._dialogLayout;
|
|
|
|
this._savedKeyFocus = null;
|
2010-10-20 20:46:38 -04:00
|
|
|
},
|
|
|
|
|
2011-08-19 14:29:39 -04:00
|
|
|
destroy: function() {
|
|
|
|
this._group.destroy();
|
|
|
|
},
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
setButtons: function(buttons) {
|
2011-08-27 22:51:54 -04:00
|
|
|
let hadChildren = this._buttonLayout.get_children() > 0;
|
|
|
|
|
2011-01-31 18:25:35 -05:00
|
|
|
this._buttonLayout.destroy_children();
|
2011-01-30 18:15:19 -05:00
|
|
|
this._actionKeys = {};
|
|
|
|
|
2011-10-20 16:40:22 -04:00
|
|
|
for (let i = 0; i < buttons.length; i++) {
|
2011-09-18 20:40:21 -04:00
|
|
|
let buttonInfo = buttons[i];
|
2010-10-20 20:46:38 -04:00
|
|
|
let label = buttonInfo['label'];
|
|
|
|
let action = buttonInfo['action'];
|
|
|
|
let key = buttonInfo['key'];
|
|
|
|
|
2011-08-19 14:29:39 -04:00
|
|
|
buttonInfo.button = new St.Button({ style_class: 'modal-dialog-button',
|
|
|
|
reactive: true,
|
|
|
|
can_focus: true,
|
|
|
|
label: label });
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
let x_alignment;
|
|
|
|
if (buttons.length == 1)
|
|
|
|
x_alignment = St.Align.END;
|
|
|
|
else if (i == 0)
|
|
|
|
x_alignment = St.Align.START;
|
|
|
|
else if (i == buttons.length - 1)
|
|
|
|
x_alignment = St.Align.END;
|
|
|
|
else
|
|
|
|
x_alignment = St.Align.MIDDLE;
|
|
|
|
|
2011-09-15 08:02:42 -04:00
|
|
|
if (this._initialKeyFocus == this._dialogLayout ||
|
|
|
|
this._buttonLayout.contains(this._initialKeyFocus))
|
|
|
|
this._initialKeyFocus = buttonInfo.button;
|
2011-08-19 14:29:39 -04:00
|
|
|
this._buttonLayout.add(buttonInfo.button,
|
2010-10-20 20:46:38 -04:00
|
|
|
{ expand: true,
|
|
|
|
x_fill: false,
|
|
|
|
y_fill: false,
|
|
|
|
x_align: x_alignment,
|
|
|
|
y_align: St.Align.MIDDLE });
|
|
|
|
|
2011-08-19 14:29:39 -04:00
|
|
|
buttonInfo.button.connect('clicked', action);
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
if (key)
|
|
|
|
this._actionKeys[key] = action;
|
|
|
|
}
|
2011-08-27 22:51:54 -04:00
|
|
|
|
|
|
|
// Fade in buttons if there weren't any before
|
2011-09-18 20:40:21 -04:00
|
|
|
if (!hadChildren && buttons.length > 0) {
|
2011-08-27 22:51:54 -04:00
|
|
|
this._buttonLayout.opacity = 0;
|
|
|
|
Tweener.addTween(this._buttonLayout,
|
|
|
|
{ opacity: 255,
|
|
|
|
time: FADE_IN_BUTTONS_TIME,
|
|
|
|
transition: 'easeOutQuad',
|
|
|
|
onComplete: Lang.bind(this, function() {
|
|
|
|
this.emit('buttons-set');
|
|
|
|
})
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.emit('buttons-set');
|
|
|
|
}
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyPressEvent: function(object, keyPressEvent) {
|
|
|
|
let symbol = keyPressEvent.get_key_symbol();
|
|
|
|
let action = this._actionKeys[symbol];
|
|
|
|
|
|
|
|
if (action)
|
|
|
|
action();
|
|
|
|
},
|
|
|
|
|
|
|
|
_onGroupDestroy: function() {
|
|
|
|
this.emit('destroy');
|
|
|
|
},
|
|
|
|
|
|
|
|
_fadeOpen: function() {
|
2011-06-13 09:54:05 -04:00
|
|
|
let monitor = Main.layoutManager.focusMonitor;
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
this._backgroundBin.set_position(monitor.x, monitor.y);
|
|
|
|
this._backgroundBin.set_size(monitor.width, monitor.height);
|
|
|
|
|
|
|
|
this.state = State.OPENING;
|
|
|
|
|
|
|
|
this._dialogLayout.opacity = 255;
|
2011-08-27 23:04:56 -04:00
|
|
|
if (this._lightbox)
|
|
|
|
this._lightbox.show();
|
2010-10-20 20:46:38 -04:00
|
|
|
this._group.opacity = 0;
|
|
|
|
this._group.show();
|
|
|
|
Tweener.addTween(this._group,
|
|
|
|
{ opacity: 255,
|
|
|
|
time: OPEN_AND_CLOSE_TIME,
|
|
|
|
transition: 'easeOutQuad',
|
|
|
|
onComplete: Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
this.state = State.OPENED;
|
|
|
|
this.emit('opened');
|
2011-03-15 16:05:40 -04:00
|
|
|
})
|
2010-10-20 20:46:38 -04:00
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2011-03-15 16:05:40 -04:00
|
|
|
setInitialKeyFocus: function(actor) {
|
|
|
|
this._initialKeyFocus = actor;
|
|
|
|
},
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
open: function(timestamp) {
|
|
|
|
if (this.state == State.OPENED || this.state == State.OPENING)
|
|
|
|
return true;
|
|
|
|
|
2011-03-18 11:28:18 -04:00
|
|
|
if (!this.pushModal(timestamp))
|
2010-10-20 20:46:38 -04:00
|
|
|
return false;
|
|
|
|
|
|
|
|
this._fadeOpen();
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
close: function(timestamp) {
|
|
|
|
if (this.state == State.CLOSED || this.state == State.CLOSING)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.state = State.CLOSING;
|
2011-03-18 11:28:18 -04:00
|
|
|
this.popModal(timestamp);
|
2011-04-06 12:54:47 -04:00
|
|
|
this._savedKeyFocus = null;
|
2010-10-20 20:46:38 -04:00
|
|
|
|
|
|
|
Tweener.addTween(this._group,
|
|
|
|
{ opacity: 0,
|
|
|
|
time: OPEN_AND_CLOSE_TIME,
|
|
|
|
transition: 'easeOutQuad',
|
|
|
|
onComplete: Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
this.state = State.CLOSED;
|
|
|
|
this._group.hide();
|
|
|
|
})
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
2011-03-18 11:28:18 -04:00
|
|
|
// Drop modal status without closing the dialog; this makes the
|
|
|
|
// dialog insensitive as well, so it needs to be followed shortly
|
|
|
|
// by either a close() or a pushModal()
|
|
|
|
popModal: function(timestamp) {
|
|
|
|
if (!this._hasModal)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let focus = global.stage.key_focus;
|
|
|
|
if (focus && this._group.contains(focus))
|
|
|
|
this._savedKeyFocus = focus;
|
|
|
|
else
|
|
|
|
this._savedKeyFocus = null;
|
|
|
|
Main.popModal(this._group, timestamp);
|
|
|
|
global.gdk_screen.get_display().sync();
|
|
|
|
this._hasModal = false;
|
|
|
|
|
2011-08-27 23:04:56 -04:00
|
|
|
if (!this._shellReactive)
|
|
|
|
this._eventBlocker.raise_top();
|
2011-03-18 11:28:18 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
pushModal: function (timestamp) {
|
|
|
|
if (this._hasModal)
|
|
|
|
return true;
|
|
|
|
if (!Main.pushModal(this._group, timestamp))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
this._hasModal = true;
|
|
|
|
if (this._savedKeyFocus) {
|
|
|
|
this._savedKeyFocus.grab_key_focus();
|
|
|
|
this._savedKeyFocus = null;
|
|
|
|
} else
|
|
|
|
this._initialKeyFocus.grab_key_focus();
|
|
|
|
|
2011-08-27 23:04:56 -04:00
|
|
|
if (!this._shellReactive)
|
|
|
|
this._eventBlocker.lower_bottom();
|
2011-03-18 11:28:18 -04:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2010-10-20 20:46:38 -04:00
|
|
|
// This method is like close, but fades the dialog out much slower,
|
|
|
|
// and leaves the lightbox in place. Once in the faded out state,
|
|
|
|
// the dialog can be brought back by an open call, or the lightbox
|
|
|
|
// can be dismissed by a close call.
|
|
|
|
//
|
|
|
|
// The main point of this method is to give some indication to the user
|
|
|
|
// that the dialog reponse has been acknowledged but will take a few
|
|
|
|
// moments before being processed.
|
|
|
|
// e.g., if a user clicked "Log Out" then the dialog should go away
|
|
|
|
// imediately, but the lightbox should remain until the logout is
|
|
|
|
// complete.
|
|
|
|
_fadeOutDialog: function(timestamp) {
|
|
|
|
if (this.state == State.CLOSED || this.state == State.CLOSING)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (this.state == State.FADED_OUT)
|
|
|
|
return;
|
|
|
|
|
2011-03-18 11:28:18 -04:00
|
|
|
this.popModal(timestamp);
|
2010-10-20 20:46:38 -04:00
|
|
|
Tweener.addTween(this._dialogLayout,
|
|
|
|
{ opacity: 0,
|
|
|
|
time: FADE_OUT_DIALOG_TIME,
|
|
|
|
transition: 'easeOutQuad',
|
|
|
|
onComplete: Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
this.state = State.FADED_OUT;
|
|
|
|
})
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Signals.addSignalMethods(ModalDialog.prototype);
|