2010-05-20 11:18:46 -04:00
|
|
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
|
|
|
|
const Cairo = imports.cairo;
|
|
|
|
const Clutter = imports.gi.Clutter;
|
|
|
|
const Gtk = imports.gi.Gtk;
|
|
|
|
const Lang = imports.lang;
|
2010-07-22 08:34:02 -04:00
|
|
|
const Shell = imports.gi.Shell;
|
2010-05-20 11:18:46 -04:00
|
|
|
const Signals = imports.signals;
|
2010-07-22 08:34:02 -04:00
|
|
|
const St = imports.gi.St;
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
const BoxPointer = imports.ui.boxpointer;
|
2010-07-03 19:47:31 -04:00
|
|
|
const Main = imports.ui.main;
|
|
|
|
const Params = imports.misc.params;
|
2010-05-20 11:18:46 -04:00
|
|
|
const Tweener = imports.ui.tweener;
|
|
|
|
|
2010-10-20 16:41:54 -04:00
|
|
|
const SLIDER_SCROLL_STEP = 0.05; /* Slider scrolling step in % */
|
|
|
|
|
2011-07-20 23:17:05 -04:00
|
|
|
function _ensureStyle(actor) {
|
|
|
|
if (actor.get_children) {
|
|
|
|
let children = actor.get_children();
|
|
|
|
for (let i = 0; i < children.length; i++)
|
|
|
|
_ensureStyle(children[i]);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (actor instanceof St.Widget)
|
|
|
|
actor.ensure_style();
|
|
|
|
}
|
|
|
|
|
2010-07-03 19:47:31 -04:00
|
|
|
function PopupBaseMenuItem(params) {
|
|
|
|
this._init(params);
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
PopupBaseMenuItem.prototype = {
|
2010-07-03 19:47:31 -04:00
|
|
|
_init: function (params) {
|
|
|
|
params = Params.parse (params, { reactive: true,
|
|
|
|
activate: true,
|
2011-01-25 16:06:40 -05:00
|
|
|
hover: true,
|
2011-09-15 10:51:19 -04:00
|
|
|
sensitive: true,
|
2011-01-25 16:06:40 -05:00
|
|
|
style_class: null
|
|
|
|
});
|
2010-10-19 13:41:41 -04:00
|
|
|
this.actor = new Shell.GenericContainer({ style_class: 'popup-menu-item',
|
|
|
|
reactive: params.reactive,
|
2010-10-07 14:15:51 -04:00
|
|
|
track_hover: params.reactive,
|
|
|
|
can_focus: params.reactive });
|
2010-10-19 13:41:41 -04:00
|
|
|
this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
|
|
|
|
this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
|
|
|
|
this.actor.connect('allocate', Lang.bind(this, this._allocate));
|
|
|
|
this.actor.connect('style-changed', Lang.bind(this, this._onStyleChanged));
|
2010-05-20 11:18:46 -04:00
|
|
|
this.actor._delegate = this;
|
2010-10-19 13:41:41 -04:00
|
|
|
|
|
|
|
this._children = [];
|
2010-10-20 12:43:22 -04:00
|
|
|
this._dot = null;
|
2010-10-19 13:41:41 -04:00
|
|
|
this._columnWidths = null;
|
|
|
|
this._spacing = 0;
|
2010-05-20 11:18:46 -04:00
|
|
|
this.active = false;
|
2011-09-15 10:51:19 -04:00
|
|
|
this._activatable = params.reactive && params.activate;
|
|
|
|
this.sensitive = this._activatable && params.sensitive;
|
|
|
|
|
|
|
|
this.setSensitive(this.sensitive);
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2011-01-25 16:06:40 -05:00
|
|
|
if (params.style_class)
|
|
|
|
this.actor.add_style_class_name(params.style_class);
|
|
|
|
|
2011-09-15 10:51:19 -04:00
|
|
|
if (this._activatable) {
|
2010-10-07 14:15:51 -04:00
|
|
|
this.actor.connect('button-release-event', Lang.bind(this, this._onButtonReleaseEvent));
|
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
2010-07-03 19:47:31 -04:00
|
|
|
if (params.reactive && params.hover)
|
2010-10-07 14:15:51 -04:00
|
|
|
this.actor.connect('notify::hover', Lang.bind(this, this._onHoverChanged));
|
2011-02-11 14:43:01 -05:00
|
|
|
if (params.reactive) {
|
2010-10-07 14:15:51 -04:00
|
|
|
this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
|
2011-02-11 14:43:01 -05:00
|
|
|
this.actor.connect('key-focus-out', Lang.bind(this, this._onKeyFocusOut));
|
|
|
|
}
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-10-19 13:41:41 -04:00
|
|
|
_onStyleChanged: function (actor) {
|
2011-03-25 18:40:38 -04:00
|
|
|
this._spacing = Math.round(actor.get_theme_node().get_length('spacing'));
|
2010-10-19 13:41:41 -04:00
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
_onButtonReleaseEvent: function (actor, event) {
|
2010-11-01 11:03:28 -04:00
|
|
|
this.activate(event);
|
2010-10-07 14:15:51 -04:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyPressEvent: function (actor, event) {
|
|
|
|
let symbol = event.get_key_symbol();
|
|
|
|
|
|
|
|
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
|
2010-11-01 11:03:28 -04:00
|
|
|
this.activate(event);
|
2010-10-07 14:15:51 -04:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyFocusIn: function (actor) {
|
|
|
|
this.setActive(true);
|
|
|
|
},
|
|
|
|
|
2011-02-11 14:43:01 -05:00
|
|
|
_onKeyFocusOut: function (actor) {
|
|
|
|
this.setActive(false);
|
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
_onHoverChanged: function (actor) {
|
2010-05-20 11:18:46 -04:00
|
|
|
this.setActive(actor.hover);
|
|
|
|
},
|
|
|
|
|
|
|
|
activate: function (event) {
|
|
|
|
this.emit('activate', event);
|
|
|
|
},
|
|
|
|
|
|
|
|
setActive: function (active) {
|
|
|
|
let activeChanged = active != this.active;
|
|
|
|
|
|
|
|
if (activeChanged) {
|
|
|
|
this.active = active;
|
2010-10-07 14:15:51 -04:00
|
|
|
if (active) {
|
2010-05-20 11:18:46 -04:00
|
|
|
this.actor.add_style_pseudo_class('active');
|
2010-10-07 14:15:51 -04:00
|
|
|
this.actor.grab_key_focus();
|
|
|
|
} else
|
2010-05-20 11:18:46 -04:00
|
|
|
this.actor.remove_style_pseudo_class('active');
|
|
|
|
this.emit('active-changed', active);
|
|
|
|
}
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
2011-09-15 10:51:19 -04:00
|
|
|
setSensitive: function(sensitive) {
|
|
|
|
if (!this._activatable)
|
|
|
|
return;
|
|
|
|
if (this.sensitive == sensitive)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.sensitive = sensitive;
|
|
|
|
this.actor.reactive = sensitive;
|
|
|
|
this.actor.can_focus = sensitive;
|
|
|
|
|
|
|
|
if (sensitive)
|
|
|
|
this.actor.remove_style_pseudo_class('insensitive');
|
|
|
|
else
|
|
|
|
this.actor.add_style_pseudo_class('insensitive');
|
|
|
|
this.emit('sensitive-changed', sensitive);
|
|
|
|
},
|
|
|
|
|
2010-07-03 19:47:31 -04:00
|
|
|
destroy: function() {
|
|
|
|
this.actor.destroy();
|
|
|
|
this.emit('destroy');
|
|
|
|
},
|
|
|
|
|
2010-11-17 14:10:12 -05:00
|
|
|
// adds an actor to the menu item; @params can contain %span
|
|
|
|
// (column span; defaults to 1, -1 means "all the remaining width"),
|
|
|
|
// %expand (defaults to #false), and %align (defaults to
|
|
|
|
// #St.Align.START)
|
|
|
|
addActor: function(child, params) {
|
|
|
|
params = Params.parse(params, { span: 1,
|
|
|
|
expand: false,
|
|
|
|
align: St.Align.START });
|
|
|
|
params.actor = child;
|
|
|
|
this._children.push(params);
|
2010-10-28 16:28:17 -04:00
|
|
|
this.actor.connect('destroy', Lang.bind(this, function () { this._removeChild(child); }));
|
2010-10-19 13:41:41 -04:00
|
|
|
this.actor.add_actor(child);
|
|
|
|
},
|
|
|
|
|
2010-10-28 16:28:17 -04:00
|
|
|
_removeChild: function(child) {
|
2010-10-19 13:41:41 -04:00
|
|
|
for (let i = 0; i < this._children.length; i++) {
|
|
|
|
if (this._children[i].actor == child) {
|
|
|
|
this._children.splice(i, 1);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-10-28 16:28:17 -04:00
|
|
|
removeActor: function(child) {
|
|
|
|
this.actor.remove_actor(child);
|
|
|
|
this._removeChild(child);
|
|
|
|
},
|
|
|
|
|
2010-10-20 12:43:22 -04:00
|
|
|
setShowDot: function(show) {
|
|
|
|
if (show) {
|
|
|
|
if (this._dot)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._dot = new St.DrawingArea({ style_class: 'popup-menu-item-dot' });
|
|
|
|
this._dot.connect('repaint', Lang.bind(this, this._onRepaintDot));
|
|
|
|
this.actor.add_actor(this._dot);
|
|
|
|
} else {
|
|
|
|
if (!this._dot)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._dot.destroy();
|
|
|
|
this._dot = null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_onRepaintDot: function(area) {
|
|
|
|
let cr = area.get_context();
|
|
|
|
let [width, height] = area.get_surface_size();
|
2011-02-14 09:20:22 -05:00
|
|
|
let color = area.get_theme_node().get_foreground_color();
|
2010-10-20 12:43:22 -04:00
|
|
|
|
|
|
|
cr.setSourceRGBA (
|
|
|
|
color.red / 255,
|
|
|
|
color.green / 255,
|
|
|
|
color.blue / 255,
|
|
|
|
color.alpha / 255);
|
|
|
|
cr.arc(width / 2, height / 2, width / 3, 0, 2 * Math.PI);
|
|
|
|
cr.fill();
|
|
|
|
},
|
|
|
|
|
2011-03-22 12:57:48 -04:00
|
|
|
// This returns column widths in logical order (i.e. from the dot
|
|
|
|
// to the image), not in visual order (left to right)
|
2010-10-19 13:41:41 -04:00
|
|
|
getColumnWidths: function() {
|
|
|
|
let widths = [];
|
|
|
|
for (let i = 0, col = 0; i < this._children.length; i++) {
|
|
|
|
let child = this._children[i];
|
|
|
|
let [min, natural] = child.actor.get_preferred_width(-1);
|
|
|
|
widths[col++] = natural;
|
|
|
|
if (child.span > 1) {
|
|
|
|
for (let j = 1; j < child.span; j++)
|
|
|
|
widths[col++] = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return widths;
|
|
|
|
},
|
|
|
|
|
|
|
|
setColumnWidths: function(widths) {
|
|
|
|
this._columnWidths = widths;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getPreferredWidth: function(actor, forHeight, alloc) {
|
|
|
|
let width = 0;
|
|
|
|
if (this._columnWidths) {
|
|
|
|
for (let i = 0; i < this._columnWidths.length; i++) {
|
|
|
|
if (i > 0)
|
|
|
|
width += this._spacing;
|
|
|
|
width += this._columnWidths[i];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (let i = 0; i < this._children.length; i++) {
|
|
|
|
let child = this._children[i];
|
|
|
|
if (i > 0)
|
|
|
|
width += this._spacing;
|
2011-08-22 14:29:22 -04:00
|
|
|
let [min, natural] = child.actor.get_preferred_width(-1);
|
2010-10-19 13:41:41 -04:00
|
|
|
width += natural;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
alloc.min_size = alloc.natural_size = width;
|
|
|
|
},
|
|
|
|
|
|
|
|
_getPreferredHeight: function(actor, forWidth, alloc) {
|
2011-09-19 16:46:38 -04:00
|
|
|
let height = 0, x = 0, minWidth, childWidth;
|
2010-10-19 13:41:41 -04:00
|
|
|
for (let i = 0; i < this._children.length; i++) {
|
|
|
|
let child = this._children[i];
|
2011-09-19 16:46:38 -04:00
|
|
|
if (this._columnWidths) {
|
|
|
|
if (child.span == -1) {
|
|
|
|
childWidth = 0;
|
|
|
|
for (let j = i; j < this._columnWidths.length; j++)
|
|
|
|
childWidth += this._columnWidths[j]
|
|
|
|
} else
|
|
|
|
childWidth = this._columnWidths[i];
|
|
|
|
} else {
|
|
|
|
if (child.span == -1)
|
|
|
|
childWidth = forWidth - x;
|
|
|
|
else
|
|
|
|
[minWidth, childWidth] = child.actor.get_preferred_width(-1);
|
|
|
|
}
|
|
|
|
x += childWidth;
|
|
|
|
|
|
|
|
let [min, natural] = child.actor.get_preferred_height(childWidth);
|
2010-10-19 13:41:41 -04:00
|
|
|
if (natural > height)
|
|
|
|
height = natural;
|
|
|
|
}
|
|
|
|
alloc.min_size = alloc.natural_size = height;
|
|
|
|
},
|
|
|
|
|
|
|
|
_allocate: function(actor, box, flags) {
|
2010-10-20 12:43:22 -04:00
|
|
|
let height = box.y2 - box.y1;
|
2011-03-22 12:57:48 -04:00
|
|
|
let direction = this.actor.get_direction();
|
2010-10-20 12:43:22 -04:00
|
|
|
|
|
|
|
if (this._dot) {
|
2011-03-22 12:57:48 -04:00
|
|
|
// The dot is placed outside box
|
|
|
|
// one quarter of padding from the border of the container
|
|
|
|
// (so 3/4 from the inner border)
|
|
|
|
// (padding is box.x1)
|
2010-10-20 12:43:22 -04:00
|
|
|
let dotBox = new Clutter.ActorBox();
|
|
|
|
let dotWidth = Math.round(box.x1 / 2);
|
|
|
|
|
2011-03-22 12:57:48 -04:00
|
|
|
if (direction == St.TextDirection.LTR) {
|
|
|
|
dotBox.x1 = Math.round(box.x1 / 4);
|
|
|
|
dotBox.x2 = dotBox.x1 + dotWidth;
|
|
|
|
} else {
|
|
|
|
dotBox.x2 = box.x2 + 3 * Math.round(box.x1 / 4);
|
|
|
|
dotBox.x1 = dotBox.x2 - dotWidth;
|
|
|
|
}
|
2010-10-20 12:43:22 -04:00
|
|
|
dotBox.y1 = Math.round(box.y1 + (height - dotWidth) / 2);
|
|
|
|
dotBox.y2 = dotBox.y1 + dotWidth;
|
|
|
|
this._dot.allocate(dotBox, flags);
|
|
|
|
}
|
|
|
|
|
2011-03-22 12:57:48 -04:00
|
|
|
let x;
|
|
|
|
if (direction == St.TextDirection.LTR)
|
|
|
|
x = box.x1;
|
|
|
|
else
|
|
|
|
x = box.x2;
|
|
|
|
// if direction is ltr, x is the right edge of the last added
|
|
|
|
// actor, and it's constantly increasing, whereas if rtl, x is
|
|
|
|
// the left edge and it decreases
|
2010-10-19 13:41:41 -04:00
|
|
|
for (let i = 0, col = 0; i < this._children.length; i++) {
|
|
|
|
let child = this._children[i];
|
|
|
|
let childBox = new Clutter.ActorBox();
|
2010-11-17 14:10:12 -05:00
|
|
|
|
|
|
|
let [minWidth, naturalWidth] = child.actor.get_preferred_width(-1);
|
|
|
|
let availWidth, extraWidth;
|
2010-10-19 13:41:41 -04:00
|
|
|
if (this._columnWidths) {
|
2011-03-22 12:57:48 -04:00
|
|
|
if (child.span == -1) {
|
|
|
|
if (direction == St.TextDirection.LTR)
|
|
|
|
availWidth = box.x2 - x;
|
|
|
|
else
|
|
|
|
availWidth = x - box.x1;
|
|
|
|
} else {
|
2010-11-17 14:10:12 -05:00
|
|
|
availWidth = 0;
|
2010-10-19 13:41:41 -04:00
|
|
|
for (let j = 0; j < child.span; j++)
|
2010-11-17 14:10:12 -05:00
|
|
|
availWidth += this._columnWidths[col++];
|
2010-10-19 13:41:41 -04:00
|
|
|
}
|
2010-11-17 14:10:12 -05:00
|
|
|
extraWidth = availWidth - naturalWidth;
|
2010-10-19 13:41:41 -04:00
|
|
|
} else {
|
2011-09-19 16:46:38 -04:00
|
|
|
if (child.span == -1)
|
|
|
|
availWidth = box.x2 - x;
|
|
|
|
else
|
|
|
|
availWidth = naturalWidth;
|
2010-11-17 14:10:12 -05:00
|
|
|
extraWidth = 0;
|
2010-10-19 13:41:41 -04:00
|
|
|
}
|
2010-11-17 14:10:12 -05:00
|
|
|
|
2011-03-22 12:57:48 -04:00
|
|
|
if (direction == St.TextDirection.LTR) {
|
|
|
|
if (child.expand) {
|
|
|
|
childBox.x1 = x;
|
|
|
|
childBox.x2 = x + availWidth;
|
2011-09-19 15:56:49 -04:00
|
|
|
} else if (child.align === St.Align.MIDDLE) {
|
2011-03-22 12:57:48 -04:00
|
|
|
childBox.x1 = x + Math.round(extraWidth / 2);
|
|
|
|
childBox.x2 = childBox.x1 + naturalWidth;
|
|
|
|
} else if (child.align === St.Align.END) {
|
|
|
|
childBox.x2 = x + availWidth;
|
|
|
|
childBox.x1 = childBox.x2 - naturalWidth;
|
|
|
|
} else {
|
|
|
|
childBox.x1 = x;
|
|
|
|
childBox.x2 = x + naturalWidth;
|
|
|
|
}
|
2010-11-17 14:10:12 -05:00
|
|
|
} else {
|
2011-03-22 12:57:48 -04:00
|
|
|
if (child.expand) {
|
|
|
|
childBox.x1 = x - availWidth;
|
|
|
|
childBox.x2 = x;
|
2011-09-19 15:56:49 -04:00
|
|
|
} else if (child.align === St.Align.MIDDLE) {
|
2011-03-22 12:57:48 -04:00
|
|
|
childBox.x1 = x - Math.round(extraWidth / 2);
|
|
|
|
childBox.x2 = childBox.x1 + naturalWidth;
|
|
|
|
} else if (child.align === St.Align.END) {
|
|
|
|
// align to the left
|
|
|
|
childBox.x1 = x - availWidth;
|
|
|
|
childBox.x2 = childBox.x1 + naturalWidth;
|
|
|
|
} else {
|
|
|
|
// align to the right
|
|
|
|
childBox.x2 = x;
|
|
|
|
childBox.x1 = x - naturalWidth;
|
|
|
|
}
|
2010-11-17 14:10:12 -05:00
|
|
|
}
|
|
|
|
|
2011-08-22 14:29:22 -04:00
|
|
|
let [minHeight, naturalHeight] = child.actor.get_preferred_height(childBox.x2 - childBox.x1);
|
2010-11-17 14:10:12 -05:00
|
|
|
childBox.y1 = Math.round(box.y1 + (height - naturalHeight) / 2);
|
|
|
|
childBox.y2 = childBox.y1 + naturalHeight;
|
|
|
|
|
2010-10-19 13:41:41 -04:00
|
|
|
child.actor.allocate(childBox, flags);
|
|
|
|
|
2011-03-22 12:57:48 -04:00
|
|
|
if (direction == St.TextDirection.LTR)
|
|
|
|
x += availWidth + this._spacing;
|
|
|
|
else
|
|
|
|
x -= availWidth + this._spacing;
|
2010-10-19 13:41:41 -04:00
|
|
|
}
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
Signals.addSignalMethods(PopupBaseMenuItem.prototype);
|
|
|
|
|
2010-11-16 09:22:38 -05:00
|
|
|
function PopupMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
PopupMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
2010-11-16 09:22:38 -05:00
|
|
|
_init: function (text, params) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, params);
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
this.label = new St.Label({ text: text });
|
2010-10-19 13:41:41 -04:00
|
|
|
this.addActor(this.label);
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function PopupSeparatorMenuItem() {
|
|
|
|
this._init();
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupSeparatorMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
|
|
|
_init: function () {
|
2010-07-03 19:47:31 -04:00
|
|
|
PopupBaseMenuItem.prototype._init.call(this, { reactive: false });
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
this._drawingArea = new St.DrawingArea({ style_class: 'popup-separator-menu-item' });
|
2010-11-17 14:10:12 -05:00
|
|
|
this.addActor(this._drawingArea, { span: -1, expand: true });
|
2010-05-20 11:18:46 -04:00
|
|
|
this._drawingArea.connect('repaint', Lang.bind(this, this._onRepaint));
|
|
|
|
},
|
|
|
|
|
|
|
|
_onRepaint: function(area) {
|
|
|
|
let cr = area.get_context();
|
|
|
|
let themeNode = area.get_theme_node();
|
|
|
|
let [width, height] = area.get_surface_size();
|
StThemeNode: simplify use of get_color/get_double/get_length
Although within St itself there are situations where the semantics of
these functions (return TRUE or FALSE and return the actual value in
an out parameter) is useful, it's mostly just annoying at the
application level, where you generally know that the CSS property is
going to specified, and there is no especially sane fallback if it's
not.
So rename the current methods to lookup_color, lookup_double, and
lookup_length, and add new get_color, get_double, and get_length
methods that don't take an "inherit" parameter, and return their
values directly. (Well, except for get_color, due to the lack of (out
caller-allocates) in gjs.)
And update the code to use either the old or new methods as appropriate.
https://bugzilla.gnome.org/show_bug.cgi?id=632590
2010-09-26 17:38:36 -04:00
|
|
|
let margin = themeNode.get_length('-margin-horizontal');
|
|
|
|
let gradientHeight = themeNode.get_length('-gradient-height');
|
2011-02-14 09:20:22 -05:00
|
|
|
let startColor = themeNode.get_color('-gradient-start');
|
|
|
|
let endColor = themeNode.get_color('-gradient-end');
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
let gradientWidth = (width - margin * 2);
|
|
|
|
let gradientOffset = (height - gradientHeight) / 2;
|
|
|
|
let pattern = new Cairo.LinearGradient(margin, gradientOffset, width - margin, gradientOffset + gradientHeight);
|
|
|
|
pattern.addColorStopRGBA(0, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
|
|
|
|
pattern.addColorStopRGBA(0.5, endColor.red / 255, endColor.green / 255, endColor.blue / 255, endColor.alpha / 255);
|
|
|
|
pattern.addColorStopRGBA(1, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
|
|
|
|
cr.setSource(pattern);
|
|
|
|
cr.rectangle(margin, gradientOffset, gradientWidth, gradientHeight);
|
|
|
|
cr.fill();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-02-21 15:25:56 -05:00
|
|
|
const PopupAlternatingMenuItemState = {
|
|
|
|
DEFAULT: 0,
|
|
|
|
ALTERNATIVE: 1
|
|
|
|
}
|
|
|
|
|
|
|
|
function PopupAlternatingMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupAlternatingMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
|
|
|
_init: function(text, alternateText, params) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, params);
|
|
|
|
this.actor.add_style_class_name('popup-alternating-menu-item');
|
|
|
|
|
|
|
|
this._text = text;
|
|
|
|
this._alternateText = alternateText;
|
|
|
|
this.label = new St.Label({ text: text });
|
|
|
|
this.state = PopupAlternatingMenuItemState.DEFAULT;
|
|
|
|
this.addActor(this.label);
|
|
|
|
|
|
|
|
this.actor.connect('notify::mapped', Lang.bind(this, this._onMapped));
|
|
|
|
},
|
|
|
|
|
|
|
|
_onMapped: function() {
|
|
|
|
if (this.actor.mapped) {
|
|
|
|
this._capturedEventId = global.stage.connect('captured-event',
|
|
|
|
Lang.bind(this, this._onCapturedEvent));
|
|
|
|
this._updateStateFromModifiers();
|
|
|
|
} else {
|
|
|
|
if (this._capturedEventId != 0) {
|
|
|
|
global.stage.disconnect(this._capturedEventId);
|
|
|
|
this._capturedEventId = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_setState: function(state) {
|
|
|
|
if (this.state != state) {
|
|
|
|
if (state == PopupAlternatingMenuItemState.ALTERNATIVE && !this._canAlternate())
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.state = state;
|
|
|
|
this._updateLabel();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateStateFromModifiers: function() {
|
|
|
|
let [x, y, mods] = global.get_pointer();
|
|
|
|
let state;
|
|
|
|
|
|
|
|
if ((mods & Clutter.ModifierType.MOD1_MASK) == 0) {
|
|
|
|
state = PopupAlternatingMenuItemState.DEFAULT;
|
|
|
|
} else {
|
|
|
|
state = PopupAlternatingMenuItemState.ALTERNATIVE;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._setState(state);
|
|
|
|
},
|
|
|
|
|
|
|
|
_onCapturedEvent: function(actor, event) {
|
|
|
|
if (event.type() != Clutter.EventType.KEY_PRESS &&
|
|
|
|
event.type() != Clutter.EventType.KEY_RELEASE)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
let key = event.get_key_symbol();
|
|
|
|
|
|
|
|
if (key == Clutter.KEY_Alt_L || key == Clutter.KEY_Alt_R)
|
|
|
|
this._updateStateFromModifiers();
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
_updateLabel: function() {
|
|
|
|
if (this.state == PopupAlternatingMenuItemState.ALTERNATIVE) {
|
|
|
|
this.actor.add_style_pseudo_class('alternate');
|
|
|
|
this.label.set_text(this._alternateText);
|
|
|
|
} else {
|
|
|
|
this.actor.remove_style_pseudo_class('alternate');
|
|
|
|
this.label.set_text(this._text);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_canAlternate: function() {
|
|
|
|
if (this.state == PopupAlternatingMenuItemState.DEFAULT && !this._alternateText)
|
|
|
|
return false;
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
updateText: function(text, alternateText) {
|
|
|
|
this._text = text;
|
|
|
|
this._alternateText = alternateText;
|
|
|
|
|
|
|
|
if (!this._canAlternate())
|
|
|
|
this._setState(PopupAlternatingMenuItemState.DEFAULT);
|
|
|
|
|
|
|
|
this._updateLabel();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-07-22 08:34:02 -04:00
|
|
|
function PopupSliderMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupSliderMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
2011-07-19 19:45:29 -04:00
|
|
|
_init: function(value) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, { activate: false });
|
2010-07-22 08:34:02 -04:00
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
|
|
|
2010-07-22 08:34:02 -04:00
|
|
|
if (isNaN(value))
|
|
|
|
// Avoid spreading NaNs around
|
|
|
|
throw TypeError('The slider value must be a number');
|
2010-07-22 20:39:44 -04:00
|
|
|
this._value = Math.max(Math.min(value, 1), 0);
|
2010-07-22 08:34:02 -04:00
|
|
|
|
|
|
|
this._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
|
2010-11-17 14:10:12 -05:00
|
|
|
this.addActor(this._slider, { span: -1, expand: true });
|
2010-07-22 08:34:02 -04:00
|
|
|
this._slider.connect('repaint', Lang.bind(this, this._sliderRepaint));
|
2011-04-21 11:49:41 -04:00
|
|
|
this.actor.connect('button-press-event', Lang.bind(this, this._startDragging));
|
2010-11-01 08:26:49 -04:00
|
|
|
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
|
2010-07-22 08:34:02 -04:00
|
|
|
|
|
|
|
this._releaseId = this._motionId = 0;
|
|
|
|
this._dragging = false;
|
|
|
|
},
|
|
|
|
|
|
|
|
setValue: function(value) {
|
|
|
|
if (isNaN(value))
|
|
|
|
throw TypeError('The slider value must be a number');
|
|
|
|
|
2010-07-22 20:39:44 -04:00
|
|
|
this._value = Math.max(Math.min(value, 1), 0);
|
2010-07-22 08:34:02 -04:00
|
|
|
this._slider.queue_repaint();
|
|
|
|
},
|
|
|
|
|
|
|
|
_sliderRepaint: function(area) {
|
|
|
|
let cr = area.get_context();
|
|
|
|
let themeNode = area.get_theme_node();
|
|
|
|
let [width, height] = area.get_surface_size();
|
|
|
|
|
StThemeNode: simplify use of get_color/get_double/get_length
Although within St itself there are situations where the semantics of
these functions (return TRUE or FALSE and return the actual value in
an out parameter) is useful, it's mostly just annoying at the
application level, where you generally know that the CSS property is
going to specified, and there is no especially sane fallback if it's
not.
So rename the current methods to lookup_color, lookup_double, and
lookup_length, and add new get_color, get_double, and get_length
methods that don't take an "inherit" parameter, and return their
values directly. (Well, except for get_color, due to the lack of (out
caller-allocates) in gjs.)
And update the code to use either the old or new methods as appropriate.
https://bugzilla.gnome.org/show_bug.cgi?id=632590
2010-09-26 17:38:36 -04:00
|
|
|
let handleRadius = themeNode.get_length('-slider-handle-radius');
|
2010-07-22 08:34:02 -04:00
|
|
|
|
|
|
|
let sliderWidth = width - 2 * handleRadius;
|
StThemeNode: simplify use of get_color/get_double/get_length
Although within St itself there are situations where the semantics of
these functions (return TRUE or FALSE and return the actual value in
an out parameter) is useful, it's mostly just annoying at the
application level, where you generally know that the CSS property is
going to specified, and there is no especially sane fallback if it's
not.
So rename the current methods to lookup_color, lookup_double, and
lookup_length, and add new get_color, get_double, and get_length
methods that don't take an "inherit" parameter, and return their
values directly. (Well, except for get_color, due to the lack of (out
caller-allocates) in gjs.)
And update the code to use either the old or new methods as appropriate.
https://bugzilla.gnome.org/show_bug.cgi?id=632590
2010-09-26 17:38:36 -04:00
|
|
|
let sliderHeight = themeNode.get_length('-slider-height');
|
2010-07-22 08:34:02 -04:00
|
|
|
|
StThemeNode: simplify use of get_color/get_double/get_length
Although within St itself there are situations where the semantics of
these functions (return TRUE or FALSE and return the actual value in
an out parameter) is useful, it's mostly just annoying at the
application level, where you generally know that the CSS property is
going to specified, and there is no especially sane fallback if it's
not.
So rename the current methods to lookup_color, lookup_double, and
lookup_length, and add new get_color, get_double, and get_length
methods that don't take an "inherit" parameter, and return their
values directly. (Well, except for get_color, due to the lack of (out
caller-allocates) in gjs.)
And update the code to use either the old or new methods as appropriate.
https://bugzilla.gnome.org/show_bug.cgi?id=632590
2010-09-26 17:38:36 -04:00
|
|
|
let sliderBorderWidth = themeNode.get_length('-slider-border-width');
|
2010-07-22 08:34:02 -04:00
|
|
|
|
2011-02-14 09:20:22 -05:00
|
|
|
let sliderBorderColor = themeNode.get_color('-slider-border-color');
|
|
|
|
let sliderColor = themeNode.get_color('-slider-background-color');
|
2010-07-22 08:34:02 -04:00
|
|
|
|
2011-03-12 17:49:03 -05:00
|
|
|
let sliderActiveBorderColor = themeNode.get_color('-slider-active-border-color');
|
|
|
|
let sliderActiveColor = themeNode.get_color('-slider-active-background-color');
|
|
|
|
|
|
|
|
cr.setSourceRGBA (
|
|
|
|
sliderActiveColor.red / 255,
|
|
|
|
sliderActiveColor.green / 255,
|
|
|
|
sliderActiveColor.blue / 255,
|
|
|
|
sliderActiveColor.alpha / 255);
|
|
|
|
cr.rectangle(handleRadius, (height - sliderHeight) / 2, sliderWidth * this._value, sliderHeight);
|
|
|
|
cr.fillPreserve();
|
|
|
|
cr.setSourceRGBA (
|
|
|
|
sliderActiveBorderColor.red / 255,
|
|
|
|
sliderActiveBorderColor.green / 255,
|
|
|
|
sliderActiveBorderColor.blue / 255,
|
|
|
|
sliderActiveBorderColor.alpha / 255);
|
|
|
|
cr.setLineWidth(sliderBorderWidth);
|
|
|
|
cr.stroke();
|
|
|
|
|
2010-07-22 08:34:02 -04:00
|
|
|
cr.setSourceRGBA (
|
|
|
|
sliderColor.red / 255,
|
|
|
|
sliderColor.green / 255,
|
|
|
|
sliderColor.blue / 255,
|
|
|
|
sliderColor.alpha / 255);
|
2011-03-12 17:49:03 -05:00
|
|
|
cr.rectangle(handleRadius + sliderWidth * this._value, (height - sliderHeight) / 2, sliderWidth * (1 - this._value), sliderHeight);
|
2010-07-22 08:34:02 -04:00
|
|
|
cr.fillPreserve();
|
|
|
|
cr.setSourceRGBA (
|
|
|
|
sliderBorderColor.red / 255,
|
|
|
|
sliderBorderColor.green / 255,
|
|
|
|
sliderBorderColor.blue / 255,
|
|
|
|
sliderBorderColor.alpha / 255);
|
|
|
|
cr.setLineWidth(sliderBorderWidth);
|
|
|
|
cr.stroke();
|
|
|
|
|
|
|
|
let handleY = height / 2;
|
2010-07-22 20:39:44 -04:00
|
|
|
let handleX = handleRadius + (width - 2 * handleRadius) * this._value;
|
2010-07-22 08:34:02 -04:00
|
|
|
|
2011-02-14 09:20:22 -05:00
|
|
|
let color = themeNode.get_foreground_color();
|
2010-07-22 08:34:02 -04:00
|
|
|
cr.setSourceRGBA (
|
|
|
|
color.red / 255,
|
|
|
|
color.green / 255,
|
|
|
|
color.blue / 255,
|
|
|
|
color.alpha / 255);
|
|
|
|
cr.arc(handleX, handleY, handleRadius, 0, 2 * Math.PI);
|
|
|
|
cr.fill();
|
|
|
|
},
|
|
|
|
|
|
|
|
_startDragging: function(actor, event) {
|
|
|
|
if (this._dragging) // don't allow two drags at the same time
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._dragging = true;
|
|
|
|
|
|
|
|
// FIXME: we should only grab the specific device that originated
|
|
|
|
// the event, but for some weird reason events are still delivered
|
|
|
|
// outside the slider if using clutter_grab_pointer_for_device
|
|
|
|
Clutter.grab_pointer(this._slider);
|
|
|
|
this._releaseId = this._slider.connect('button-release-event', Lang.bind(this, this._endDragging));
|
|
|
|
this._motionId = this._slider.connect('motion-event', Lang.bind(this, this._motionEvent));
|
|
|
|
let absX, absY;
|
|
|
|
[absX, absY] = event.get_coords();
|
|
|
|
this._moveHandle(absX, absY);
|
|
|
|
},
|
|
|
|
|
|
|
|
_endDragging: function() {
|
|
|
|
if (this._dragging) {
|
|
|
|
this._slider.disconnect(this._releaseId);
|
|
|
|
this._slider.disconnect(this._motionId);
|
|
|
|
|
|
|
|
Clutter.ungrab_pointer();
|
|
|
|
this._dragging = false;
|
|
|
|
|
2010-07-22 20:39:44 -04:00
|
|
|
this.emit('drag-end');
|
2010-07-22 08:34:02 -04:00
|
|
|
}
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2010-10-20 16:41:54 -04:00
|
|
|
_onScrollEvent: function (actor, event) {
|
|
|
|
let direction = event.get_scroll_direction();
|
|
|
|
|
|
|
|
if (direction == Clutter.ScrollDirection.DOWN) {
|
|
|
|
this._value = Math.max(0, this._value - SLIDER_SCROLL_STEP);
|
|
|
|
}
|
|
|
|
else if (direction == Clutter.ScrollDirection.UP) {
|
|
|
|
this._value = Math.min(1, this._value + SLIDER_SCROLL_STEP);
|
|
|
|
}
|
|
|
|
|
|
|
|
this._slider.queue_repaint();
|
|
|
|
this.emit('value-changed', this._value);
|
|
|
|
},
|
|
|
|
|
2010-07-22 08:34:02 -04:00
|
|
|
_motionEvent: function(actor, event) {
|
|
|
|
let absX, absY;
|
|
|
|
[absX, absY] = event.get_coords();
|
2011-02-08 14:19:04 -05:00
|
|
|
this._moveHandle(absX, absY);
|
2010-07-22 08:34:02 -04:00
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
_moveHandle: function(absX, absY) {
|
|
|
|
let relX, relY, sliderX, sliderY;
|
|
|
|
[sliderX, sliderY] = this._slider.get_transformed_position();
|
|
|
|
relX = absX - sliderX;
|
|
|
|
relY = absY - sliderY;
|
|
|
|
|
|
|
|
let width = this._slider.width;
|
StThemeNode: simplify use of get_color/get_double/get_length
Although within St itself there are situations where the semantics of
these functions (return TRUE or FALSE and return the actual value in
an out parameter) is useful, it's mostly just annoying at the
application level, where you generally know that the CSS property is
going to specified, and there is no especially sane fallback if it's
not.
So rename the current methods to lookup_color, lookup_double, and
lookup_length, and add new get_color, get_double, and get_length
methods that don't take an "inherit" parameter, and return their
values directly. (Well, except for get_color, due to the lack of (out
caller-allocates) in gjs.)
And update the code to use either the old or new methods as appropriate.
https://bugzilla.gnome.org/show_bug.cgi?id=632590
2010-09-26 17:38:36 -04:00
|
|
|
let handleRadius = this._slider.get_theme_node().get_length('-slider-handle-radius');
|
2010-07-22 08:34:02 -04:00
|
|
|
|
|
|
|
let newvalue;
|
|
|
|
if (relX < handleRadius)
|
|
|
|
newvalue = 0;
|
|
|
|
else if (relX > width - handleRadius)
|
|
|
|
newvalue = 1;
|
|
|
|
else
|
|
|
|
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
|
2010-07-22 20:39:44 -04:00
|
|
|
this._value = newvalue;
|
2010-07-22 08:34:02 -04:00
|
|
|
this._slider.queue_repaint();
|
2010-07-22 20:39:44 -04:00
|
|
|
this.emit('value-changed', this._value);
|
2010-07-22 08:34:02 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
get value() {
|
|
|
|
return this._value;
|
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
_onKeyPressEvent: function (actor, event) {
|
2010-07-22 08:34:02 -04:00
|
|
|
let key = event.get_key_symbol();
|
2010-10-07 14:15:51 -04:00
|
|
|
if (key == Clutter.KEY_Right || key == Clutter.KEY_Left) {
|
|
|
|
let delta = key == Clutter.KEY_Right ? 0.1 : -0.1;
|
2010-07-22 20:39:44 -04:00
|
|
|
this._value = Math.max(0, Math.min(this._value + delta, 1));
|
2010-07-22 08:34:02 -04:00
|
|
|
this._slider.queue_repaint();
|
|
|
|
this.emit('value-changed', this._value);
|
2010-07-22 20:39:44 -04:00
|
|
|
this.emit('drag-end');
|
2010-07-22 08:34:02 -04:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
2011-02-08 14:19:04 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
function Switch() {
|
|
|
|
this._init.apply(this, arguments);
|
2010-07-22 08:34:02 -04:00
|
|
|
}
|
|
|
|
|
2011-02-08 14:19:04 -05:00
|
|
|
Switch.prototype = {
|
|
|
|
_init: function(state) {
|
|
|
|
this.actor = new St.Bin({ style_class: 'toggle-switch' });
|
|
|
|
// Translators: this MUST be either "toggle-switch-us"
|
|
|
|
// (for toggle switches containing the English words
|
|
|
|
// "ON" and "OFF") or "toggle-switch-intl" (for toggle
|
|
|
|
// switches containing "◯" and "|"). Other values will
|
|
|
|
// simply result in invisible toggle switches.
|
|
|
|
this.actor.add_style_class_name(_("toggle-switch-us"));
|
|
|
|
this.setToggleState(state);
|
|
|
|
},
|
|
|
|
|
|
|
|
setToggleState: function(state) {
|
|
|
|
if (state)
|
|
|
|
this.actor.add_style_pseudo_class('checked');
|
|
|
|
else
|
|
|
|
this.actor.remove_style_pseudo_class('checked');
|
|
|
|
this.state = state;
|
|
|
|
},
|
|
|
|
|
|
|
|
toggle: function() {
|
|
|
|
this.setToggleState(!this.state);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-06-17 08:17:01 -04:00
|
|
|
function PopupSwitchMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupSwitchMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
2011-01-25 16:06:40 -05:00
|
|
|
_init: function(text, active, params) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, params);
|
2010-06-17 08:17:01 -04:00
|
|
|
|
|
|
|
this.label = new St.Label({ text: text });
|
2010-10-13 14:10:38 -04:00
|
|
|
this._switch = new Switch(active);
|
2010-06-17 08:17:01 -04:00
|
|
|
|
2010-10-19 13:41:41 -04:00
|
|
|
this.addActor(this.label);
|
2010-06-17 08:17:01 -04:00
|
|
|
|
2011-04-21 10:44:28 -04:00
|
|
|
this._statusBin = new St.Bin({ x_align: St.Align.END });
|
2011-07-28 10:59:10 -04:00
|
|
|
this.addActor(this._statusBin,
|
|
|
|
{ expand: true, span: -1, align: St.Align.END });
|
2011-04-21 10:44:28 -04:00
|
|
|
|
|
|
|
this._statusLabel = new St.Label({ text: '',
|
|
|
|
style_class: 'popup-inactive-menu-item'
|
|
|
|
});
|
|
|
|
this._statusBin.child = this._switch.actor;
|
|
|
|
},
|
|
|
|
|
|
|
|
setStatus: function(text) {
|
|
|
|
if (text != null) {
|
|
|
|
this._statusLabel.text = text;
|
|
|
|
this._statusBin.child = this._statusLabel;
|
|
|
|
this.actor.reactive = false;
|
|
|
|
this.actor.can_focus = false;
|
|
|
|
} else {
|
|
|
|
this._statusBin.child = this._switch.actor;
|
|
|
|
this.actor.reactive = true;
|
|
|
|
this.actor.can_focus = true;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
activate: function(event) {
|
|
|
|
if (this._switch.actor.mapped) {
|
2010-06-17 08:17:01 -04:00
|
|
|
this.toggle();
|
2011-04-21 10:44:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
PopupBaseMenuItem.prototype.activate.call(this, event);
|
2010-06-17 08:17:01 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
toggle: function() {
|
|
|
|
this._switch.toggle();
|
|
|
|
this.emit('toggled', this._switch.state);
|
|
|
|
},
|
|
|
|
|
|
|
|
get state() {
|
|
|
|
return this._switch.state;
|
|
|
|
},
|
|
|
|
|
|
|
|
setToggleState: function(state) {
|
|
|
|
this._switch.setToggleState(state);
|
|
|
|
}
|
2011-02-08 14:19:04 -05:00
|
|
|
};
|
2010-06-17 08:17:01 -04:00
|
|
|
|
2011-01-25 16:06:40 -05:00
|
|
|
function PopupImageMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
PopupImageMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
2011-01-25 16:06:40 -05:00
|
|
|
_init: function (text, iconName, params) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, params);
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-07-06 03:51:53 -04:00
|
|
|
this.label = new St.Label({ text: text });
|
2010-10-19 13:41:41 -04:00
|
|
|
this.addActor(this.label);
|
2010-11-02 18:33:22 -04:00
|
|
|
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
|
2010-11-17 14:10:12 -05:00
|
|
|
this.addActor(this._icon, { align: St.Align.END });
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-10-19 11:59:23 -04:00
|
|
|
this.setIcon(iconName);
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-07-06 03:51:53 -04:00
|
|
|
setIcon: function(name) {
|
2010-11-02 18:33:22 -04:00
|
|
|
this._icon.icon_name = name;
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
function PopupMenuBase() {
|
|
|
|
throw new TypeError('Trying to instantiate abstract class PopupMenuBase');
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
PopupMenuBase.prototype = {
|
|
|
|
_init: function(sourceActor, styleClass) {
|
2010-05-20 11:18:46 -04:00
|
|
|
this.sourceActor = sourceActor;
|
|
|
|
|
2011-01-25 16:06:40 -05:00
|
|
|
if (styleClass !== undefined) {
|
|
|
|
this.box = new St.BoxLayout({ style_class: styleClass,
|
|
|
|
vertical: true });
|
|
|
|
} else {
|
|
|
|
this.box = new St.BoxLayout({ vertical: true });
|
|
|
|
}
|
2011-03-25 10:14:55 -04:00
|
|
|
this.box.connect_after('queue-relayout', Lang.bind(this, this._menuQueueRelayout));
|
2011-03-28 15:27:03 -04:00
|
|
|
this.length = 0;
|
2010-10-07 14:15:51 -04:00
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
this.isOpen = false;
|
2011-03-22 10:29:32 -04:00
|
|
|
|
|
|
|
// If set, we don't send events (including crossing events) to the source actor
|
|
|
|
// for the menu which causes its prelight state to freeze
|
|
|
|
this.blockSourceEvents = false;
|
|
|
|
|
2011-04-02 12:35:03 -04:00
|
|
|
// Can be set while a menu is up to let all events through without special
|
|
|
|
// menu handling useful for scrollbars in menus, and probably not otherwise.
|
|
|
|
this.passEvents = false;
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
this._activeMenuItem = null;
|
2011-07-12 15:47:37 -04:00
|
|
|
this._childMenus = [];
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
addAction: function(title, callback) {
|
2011-08-23 10:14:55 -04:00
|
|
|
let menuItem = new PopupMenuItem(title);
|
2010-05-20 11:18:46 -04:00
|
|
|
this.addMenuItem(menuItem);
|
|
|
|
menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
|
|
|
|
callback(event);
|
|
|
|
}));
|
2011-08-23 10:14:55 -04:00
|
|
|
|
|
|
|
return menuItem;
|
|
|
|
},
|
|
|
|
|
|
|
|
addSettingsAction: function(title, desktopFile) {
|
2011-08-28 22:15:53 -04:00
|
|
|
// Don't allow user settings to get edited unless we're in a user session
|
|
|
|
if (global.session_type != Shell.SessionType.USER)
|
|
|
|
return null;
|
|
|
|
|
2011-08-23 10:14:55 -04:00
|
|
|
let menuItem = this.addAction(title, function() {
|
|
|
|
let app = Shell.AppSystem.get_default().lookup_setting(desktopFile);
|
|
|
|
|
|
|
|
if (!app) {
|
|
|
|
log('Settings panel for desktop file ' + desktopFile + ' could not be loaded!');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
Main.overview.hide();
|
|
|
|
app.activate();
|
|
|
|
});
|
|
|
|
return menuItem;
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-07-12 15:47:37 -04:00
|
|
|
isChildMenu: function(menu) {
|
|
|
|
return this._childMenus.indexOf(menu) != -1;
|
|
|
|
},
|
|
|
|
|
|
|
|
addChildMenu: function(menu) {
|
|
|
|
if (this.isChildMenu(menu))
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._childMenus.push(menu);
|
|
|
|
this.emit('child-menu-added', menu);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeChildMenu: function(menu) {
|
|
|
|
let index = this._childMenus.indexOf(menu);
|
|
|
|
|
|
|
|
if (index == -1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this._childMenus.splice(index, 1);
|
|
|
|
this.emit('child-menu-removed', menu);
|
|
|
|
},
|
|
|
|
|
2011-01-25 16:04:57 -05:00
|
|
|
/**
|
|
|
|
* _connectSubMenuSignals:
|
|
|
|
* @object: a menu item, or a menu section
|
|
|
|
* @menu: a sub menu, or a menu section
|
|
|
|
*
|
|
|
|
* Connects to signals on @menu that are necessary for
|
|
|
|
* operating the submenu, and stores the ids on @object.
|
|
|
|
*/
|
|
|
|
_connectSubMenuSignals: function(object, menu) {
|
|
|
|
object._subMenuActivateId = menu.connect('activate', Lang.bind(this, function() {
|
|
|
|
this.emit('activate');
|
|
|
|
this.close(true);
|
|
|
|
}));
|
|
|
|
object._subMenuActiveChangeId = menu.connect('active-changed', Lang.bind(this, function(submenu, submenuItem) {
|
|
|
|
if (this._activeMenuItem && this._activeMenuItem != submenuItem)
|
|
|
|
this._activeMenuItem.setActive(false);
|
|
|
|
this._activeMenuItem = submenuItem;
|
|
|
|
this.emit('active-changed', submenuItem);
|
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
|
|
|
_connectItemSignals: function(menuItem) {
|
2010-05-20 11:18:46 -04:00
|
|
|
menuItem._activeChangeId = menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) {
|
|
|
|
if (active && this._activeMenuItem != menuItem) {
|
|
|
|
if (this._activeMenuItem)
|
|
|
|
this._activeMenuItem.setActive(false);
|
|
|
|
this._activeMenuItem = menuItem;
|
|
|
|
this.emit('active-changed', menuItem);
|
|
|
|
} else if (!active && this._activeMenuItem == menuItem) {
|
|
|
|
this._activeMenuItem = null;
|
|
|
|
this.emit('active-changed', null);
|
|
|
|
}
|
|
|
|
}));
|
2011-09-15 10:51:19 -04:00
|
|
|
menuItem._sensitiveChangeId = menuItem.connect('sensitive-changed', Lang.bind(this, function(menuItem, sensitive) {
|
|
|
|
if (!sensitive && this._activeMenuItem == menuItem) {
|
|
|
|
if (!this.actor.navigate_focus(menuItem.actor,
|
|
|
|
Gtk.DirectionType.TAB_FORWARD,
|
|
|
|
true))
|
|
|
|
this.actor.grab_key_focus();
|
|
|
|
} else if (sensitive && this._activeMenuItem == null) {
|
|
|
|
if (global.stage.get_key_focus() == this.actor)
|
|
|
|
menuItem.actor.grab_key_focus();
|
|
|
|
}
|
|
|
|
}));
|
2010-05-20 11:18:46 -04:00
|
|
|
menuItem._activateId = menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
|
|
|
|
this.emit('activate', menuItem);
|
2010-11-18 16:18:54 -05:00
|
|
|
this.close(true);
|
2010-05-20 11:18:46 -04:00
|
|
|
}));
|
2010-07-03 19:47:31 -04:00
|
|
|
menuItem.connect('destroy', Lang.bind(this, function(emitter) {
|
|
|
|
menuItem.disconnect(menuItem._activateId);
|
|
|
|
menuItem.disconnect(menuItem._activeChangeId);
|
2011-09-15 10:51:19 -04:00
|
|
|
menuItem.disconnect(menuItem._sensitiveChangeId);
|
2010-11-01 11:03:28 -04:00
|
|
|
if (menuItem.menu) {
|
|
|
|
menuItem.menu.disconnect(menuItem._subMenuActivateId);
|
|
|
|
menuItem.menu.disconnect(menuItem._subMenuActiveChangeId);
|
|
|
|
this.disconnect(menuItem._closingId);
|
|
|
|
}
|
2010-07-03 19:47:31 -04:00
|
|
|
if (menuItem == this._activeMenuItem)
|
|
|
|
this._activeMenuItem = null;
|
|
|
|
}));
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-08-19 14:42:20 -04:00
|
|
|
_updateSeparatorVisibility: function(menuItem) {
|
|
|
|
let children = this.box.get_children();
|
|
|
|
|
|
|
|
let index = children.indexOf(menuItem.actor);
|
|
|
|
|
|
|
|
if (index < 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let childBeforeIndex = index - 1;
|
|
|
|
|
|
|
|
while (childBeforeIndex >= 0 && !children[childBeforeIndex].visible)
|
|
|
|
childBeforeIndex--;
|
|
|
|
|
|
|
|
if (childBeforeIndex < 0
|
|
|
|
|| children[childBeforeIndex]._delegate instanceof PopupSeparatorMenuItem) {
|
|
|
|
menuItem.actor.hide();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
let childAfterIndex = index + 1;
|
|
|
|
|
|
|
|
while (childAfterIndex < children.length && !children[childAfterIndex].visible)
|
|
|
|
childAfterIndex++;
|
|
|
|
|
|
|
|
if (childAfterIndex >= children.length
|
|
|
|
|| children[childAfterIndex]._delegate instanceof PopupSeparatorMenuItem) {
|
|
|
|
menuItem.actor.hide();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
menuItem.actor.show();
|
|
|
|
},
|
|
|
|
|
2011-01-25 16:04:57 -05:00
|
|
|
addMenuItem: function(menuItem, position) {
|
|
|
|
let before_item = null;
|
|
|
|
if (position == undefined) {
|
|
|
|
this.box.add(menuItem.actor);
|
|
|
|
} else {
|
|
|
|
let items = this._getMenuItems();
|
|
|
|
if (position < items.length) {
|
|
|
|
before_item = items[position].actor;
|
|
|
|
this.box.insert_before(menuItem.actor, before_item);
|
|
|
|
} else
|
|
|
|
this.box.add(menuItem.actor);
|
|
|
|
}
|
|
|
|
if (menuItem instanceof PopupMenuSection) {
|
|
|
|
this._connectSubMenuSignals(menuItem, menuItem);
|
|
|
|
menuItem.connect('destroy', Lang.bind(this, function() {
|
|
|
|
menuItem.disconnect(menuItem._subMenuActivateId);
|
|
|
|
menuItem.disconnect(menuItem._subMenuActiveChangeId);
|
2011-03-28 15:27:03 -04:00
|
|
|
|
|
|
|
this.length--;
|
2011-01-25 16:04:57 -05:00
|
|
|
}));
|
|
|
|
} else if (menuItem instanceof PopupSubMenuMenuItem) {
|
|
|
|
if (before_item == null)
|
|
|
|
this.box.add(menuItem.menu.actor);
|
|
|
|
else
|
|
|
|
this.box.insert_before(menuItem.menu.actor, before_item);
|
|
|
|
this._connectSubMenuSignals(menuItem, menuItem.menu);
|
|
|
|
this._connectItemSignals(menuItem);
|
|
|
|
menuItem._closingId = this.connect('open-state-changed', function(self, open) {
|
|
|
|
if (!open)
|
|
|
|
menuItem.menu.close(false);
|
|
|
|
});
|
2011-08-19 14:42:20 -04:00
|
|
|
} else if (menuItem instanceof PopupSeparatorMenuItem) {
|
|
|
|
this._connectItemSignals(menuItem);
|
|
|
|
|
|
|
|
// updateSeparatorVisibility needs to get called any time the
|
|
|
|
// separator's adjacent siblings change visibility or position.
|
|
|
|
// open-state-changed isn't exactly that, but doing it in more
|
|
|
|
// precise ways would require a lot more bookkeeping.
|
|
|
|
this.connect('open-state-changed', Lang.bind(this, function() { this._updateSeparatorVisibility(menuItem); }));
|
2011-01-25 16:04:57 -05:00
|
|
|
} else if (menuItem instanceof PopupBaseMenuItem)
|
|
|
|
this._connectItemSignals(menuItem);
|
|
|
|
else
|
|
|
|
throw TypeError("Invalid argument to PopupMenuBase.addMenuItem()");
|
2011-03-28 15:27:03 -04:00
|
|
|
|
|
|
|
this.length++;
|
2011-01-25 16:04:57 -05:00
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
getColumnWidths: function() {
|
|
|
|
let columnWidths = [];
|
|
|
|
let items = this.box.get_children();
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
2011-03-10 11:59:50 -05:00
|
|
|
if (!items[i].visible)
|
|
|
|
continue;
|
2010-11-01 11:03:28 -04:00
|
|
|
if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase) {
|
|
|
|
let itemColumnWidths = items[i]._delegate.getColumnWidths();
|
|
|
|
for (let j = 0; j < itemColumnWidths.length; j++) {
|
|
|
|
if (j >= columnWidths.length || itemColumnWidths[j] > columnWidths[j])
|
|
|
|
columnWidths[j] = itemColumnWidths[j];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return columnWidths;
|
|
|
|
},
|
|
|
|
|
|
|
|
setColumnWidths: function(widths) {
|
|
|
|
let items = this.box.get_children();
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase)
|
|
|
|
items[i]._delegate.setColumnWidths(widths);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2011-03-25 10:14:55 -04:00
|
|
|
// Because of the above column-width funniness, we need to do a
|
|
|
|
// queue-relayout on every item whenever the menu itself changes
|
|
|
|
// size, to force clutter to drop its cached size requests. (The
|
|
|
|
// menuitems will in turn call queue_relayout on their parent, the
|
|
|
|
// menu, but that call will be a no-op since the menu already
|
|
|
|
// has a relayout queued, so we won't get stuck in a loop.
|
|
|
|
_menuQueueRelayout: function() {
|
|
|
|
this.box.get_children().map(function (actor) { actor.queue_relayout(); });
|
|
|
|
},
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
addActor: function(actor) {
|
2010-11-01 11:03:28 -04:00
|
|
|
this.box.add(actor);
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-01-25 16:04:57 -05:00
|
|
|
_getMenuItems: function() {
|
|
|
|
return this.box.get_children().map(function (actor) {
|
|
|
|
return actor._delegate;
|
|
|
|
}).filter(function(item) {
|
|
|
|
return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection;
|
|
|
|
});
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-06-27 11:45:24 -04:00
|
|
|
get firstMenuItem() {
|
|
|
|
let items = this._getMenuItems();
|
|
|
|
if (items.length)
|
|
|
|
return items[0];
|
|
|
|
else
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2011-07-20 23:17:05 -04:00
|
|
|
get numMenuItems() {
|
|
|
|
return this._getMenuItems().length;
|
|
|
|
},
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
removeAll: function() {
|
2011-01-25 16:04:57 -05:00
|
|
|
let children = this._getMenuItems();
|
2010-05-20 11:18:46 -04:00
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
let item = children[i];
|
2010-07-03 19:47:31 -04:00
|
|
|
item.destroy();
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
toggle: function() {
|
2010-05-20 11:18:46 -04:00
|
|
|
if (this.isOpen)
|
2010-11-18 16:18:54 -05:00
|
|
|
this.close(true);
|
2010-11-01 11:03:28 -04:00
|
|
|
else
|
2010-11-18 16:18:54 -05:00
|
|
|
this.open(true);
|
2010-11-01 11:03:28 -04:00
|
|
|
},
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
destroy: function() {
|
|
|
|
this.removeAll();
|
|
|
|
this.actor.destroy();
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
this.emit('destroy');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Signals.addSignalMethods(PopupMenuBase.prototype);
|
2010-06-12 12:13:04 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
function PopupMenu() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
PopupMenu.prototype = {
|
|
|
|
__proto__: PopupMenuBase.prototype,
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2011-07-30 13:24:24 -04:00
|
|
|
_init: function(sourceActor, alignment, arrowSide) {
|
2010-11-01 11:03:28 -04:00
|
|
|
PopupMenuBase.prototype._init.call (this, sourceActor, 'popup-menu-content');
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
this._alignment = alignment;
|
|
|
|
this._arrowSide = arrowSide;
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
this._boxPointer = new BoxPointer.BoxPointer(arrowSide,
|
|
|
|
{ x_fill: true,
|
|
|
|
y_fill: true,
|
|
|
|
x_align: St.Align.START });
|
|
|
|
this.actor = this._boxPointer.actor;
|
|
|
|
this.actor._delegate = this;
|
|
|
|
this.actor.style_class = 'popup-menu-boxpointer';
|
2011-02-08 14:53:43 -05:00
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
this._boxWrapper = new Shell.GenericContainer();
|
|
|
|
this._boxWrapper.connect('get-preferred-width', Lang.bind(this, this._boxGetPreferredWidth));
|
|
|
|
this._boxWrapper.connect('get-preferred-height', Lang.bind(this, this._boxGetPreferredHeight));
|
|
|
|
this._boxWrapper.connect('allocate', Lang.bind(this, this._boxAllocate));
|
|
|
|
this._boxPointer.bin.set_child(this._boxWrapper);
|
|
|
|
this._boxWrapper.add_actor(this.box);
|
|
|
|
this.actor.add_style_class_name('popup-menu');
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
global.focus_manager.add_group(this.actor);
|
2010-05-20 11:18:46 -04:00
|
|
|
this.actor.reactive = true;
|
2010-11-01 11:03:28 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_boxGetPreferredWidth: function (actor, forHeight, alloc) {
|
|
|
|
let columnWidths = this.getColumnWidths();
|
|
|
|
this.setColumnWidths(columnWidths);
|
|
|
|
|
|
|
|
// Now they will request the right sizes
|
|
|
|
[alloc.min_size, alloc.natural_size] = this.box.get_preferred_width(forHeight);
|
|
|
|
},
|
|
|
|
|
|
|
|
_boxGetPreferredHeight: function (actor, forWidth, alloc) {
|
|
|
|
[alloc.min_size, alloc.natural_size] = this.box.get_preferred_height(forWidth);
|
|
|
|
},
|
|
|
|
|
|
|
|
_boxAllocate: function (actor, box, flags) {
|
|
|
|
this.box.allocate(box, flags);
|
|
|
|
},
|
|
|
|
|
2011-02-08 14:53:43 -05:00
|
|
|
_onKeyPressEvent: function(actor, event) {
|
|
|
|
if (event.get_key_symbol() == Clutter.Escape) {
|
|
|
|
this.close(true);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
setArrowOrigin: function(origin) {
|
|
|
|
this._boxPointer.setArrowOrigin(origin);
|
|
|
|
},
|
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
open: function(animate) {
|
2010-11-01 11:03:28 -04:00
|
|
|
if (this.isOpen)
|
|
|
|
return;
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
this.isOpen = true;
|
2010-11-01 11:03:28 -04:00
|
|
|
|
2011-07-30 13:24:24 -04:00
|
|
|
this._boxPointer.setPosition(this.sourceActor, this._alignment);
|
2010-11-18 16:18:54 -05:00
|
|
|
this._boxPointer.show(animate);
|
2010-11-01 11:03:28 -04:00
|
|
|
|
2011-08-29 12:04:17 -04:00
|
|
|
this.actor.raise_top();
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
this.emit('open-state-changed', true);
|
|
|
|
},
|
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
close: function(animate) {
|
2010-05-20 11:18:46 -04:00
|
|
|
if (!this.isOpen)
|
|
|
|
return;
|
|
|
|
|
2010-07-03 19:47:31 -04:00
|
|
|
if (this._activeMenuItem)
|
|
|
|
this._activeMenuItem.setActive(false);
|
2010-11-01 11:03:28 -04:00
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
this._boxPointer.hide(animate);
|
2010-11-01 11:03:28 -04:00
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
this.isOpen = false;
|
|
|
|
this.emit('open-state-changed', false);
|
2010-11-01 11:03:28 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function PopupSubMenu() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupSubMenu.prototype = {
|
|
|
|
__proto__: PopupMenuBase.prototype,
|
|
|
|
|
|
|
|
_init: function(sourceActor, sourceArrow) {
|
2011-04-04 08:55:15 -04:00
|
|
|
PopupMenuBase.prototype._init.call(this, sourceActor);
|
2010-11-01 11:03:28 -04:00
|
|
|
|
|
|
|
this._arrow = sourceArrow;
|
|
|
|
this._arrow.rotation_center_z_gravity = Clutter.Gravity.CENTER;
|
|
|
|
|
2011-04-02 12:35:03 -04:00
|
|
|
// Since a function of a submenu might be to provide a "More.." expander
|
|
|
|
// with long content, we make it scrollable - the scrollbar will only take
|
|
|
|
// effect if a CSS max-height is set on the top menu.
|
2011-04-04 08:55:15 -04:00
|
|
|
this.actor = new St.ScrollView({ style_class: 'popup-sub-menu',
|
|
|
|
hscrollbar_policy: Gtk.PolicyType.NEVER,
|
2011-04-02 12:35:03 -04:00
|
|
|
vscrollbar_policy: Gtk.PolicyType.NEVER });
|
|
|
|
|
|
|
|
// StScrollbar plays dirty tricks with events, calling
|
|
|
|
// clutter_set_motion_events_enabled (FALSE) during the scroll; this
|
|
|
|
// confuses our event tracking, so we just turn it off during the
|
|
|
|
// scroll.
|
|
|
|
let vscroll = this.actor.get_vscroll_bar();
|
|
|
|
vscroll.connect('scroll-start',
|
|
|
|
Lang.bind(this, function() {
|
|
|
|
let topMenu = this._getTopMenu();
|
|
|
|
if (topMenu)
|
|
|
|
topMenu.passEvents = true;
|
|
|
|
}));
|
|
|
|
vscroll.connect('scroll-stop',
|
|
|
|
Lang.bind(this, function() {
|
|
|
|
let topMenu = this._getTopMenu();
|
|
|
|
if (topMenu)
|
|
|
|
topMenu.passEvents = false;
|
|
|
|
}));
|
|
|
|
|
|
|
|
this.actor.add_actor(this.box);
|
2010-11-01 11:03:28 -04:00
|
|
|
this.actor._delegate = this;
|
|
|
|
this.actor.clip_to_allocation = true;
|
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
|
|
this.actor.hide();
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-04-02 12:35:03 -04:00
|
|
|
_getTopMenu: function() {
|
|
|
|
let actor = this.actor.get_parent();
|
|
|
|
while (actor) {
|
|
|
|
if (actor._delegate && actor._delegate instanceof PopupMenu)
|
|
|
|
return actor._delegate;
|
|
|
|
|
|
|
|
actor = actor.get_parent();
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
|
|
|
_needsScrollbar: function() {
|
|
|
|
let topMenu = this._getTopMenu();
|
|
|
|
let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1);
|
|
|
|
let topThemeNode = topMenu.actor.get_theme_node();
|
|
|
|
|
|
|
|
let topMaxHeight = topThemeNode.get_max_height();
|
|
|
|
return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight;
|
|
|
|
},
|
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
open: function(animate) {
|
2010-05-20 11:18:46 -04:00
|
|
|
if (this.isOpen)
|
2010-11-01 11:03:28 -04:00
|
|
|
return;
|
|
|
|
|
|
|
|
this.isOpen = true;
|
|
|
|
|
2011-03-31 15:51:38 -04:00
|
|
|
this.actor.show();
|
2011-04-02 12:35:03 -04:00
|
|
|
|
|
|
|
let needsScrollbar = this._needsScrollbar();
|
|
|
|
|
|
|
|
// St.ScrollView always requests space horizontally for a possible vertical
|
|
|
|
// scrollbar if in AUTOMATIC mode. Doing better would require implementation
|
|
|
|
// of width-for-height in St.BoxLayout and St.ScrollView. This looks bad
|
|
|
|
// when we *don't* need it, so turn off the scrollbar when that's true.
|
|
|
|
// Dynamic changes in whether we need it aren't handled properly.
|
|
|
|
this.actor.vscrollbar_policy =
|
|
|
|
needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
|
|
|
|
|
|
|
|
// It looks funny if we animate with a scrollbar (at what point is
|
|
|
|
// the scrollbar added?) so just skip that case
|
|
|
|
if (animate && needsScrollbar)
|
|
|
|
animate = false;
|
|
|
|
|
|
|
|
if (animate) {
|
|
|
|
let [minHeight, naturalHeight] = this.actor.get_preferred_height(-1);
|
|
|
|
this.actor.height = 0;
|
|
|
|
this.actor._arrow_rotation = this._arrow.rotation_angle_z;
|
|
|
|
Tweener.addTween(this.actor,
|
|
|
|
{ _arrow_rotation: 90,
|
|
|
|
height: naturalHeight,
|
|
|
|
time: 0.25,
|
|
|
|
onUpdateScope: this,
|
|
|
|
onUpdate: function() {
|
|
|
|
this._arrow.rotation_angle_z = this.actor._arrow_rotation;
|
|
|
|
},
|
|
|
|
onCompleteScope: this,
|
|
|
|
onComplete: function() {
|
|
|
|
this.actor.set_height(-1);
|
|
|
|
this.emit('open-state-changed', true);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this._arrow.rotation_angle_z = 90;
|
|
|
|
this.emit('open-state-changed', true);
|
|
|
|
}
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
close: function(animate) {
|
2010-11-01 11:03:28 -04:00
|
|
|
if (!this.isOpen)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.isOpen = false;
|
|
|
|
|
|
|
|
if (this._activeMenuItem)
|
2010-10-07 14:15:51 -04:00
|
|
|
this._activeMenuItem.setActive(false);
|
2010-05-20 11:18:46 -04:00
|
|
|
|
2011-04-02 12:35:03 -04:00
|
|
|
if (animate && this._needsScrollbar())
|
|
|
|
animate = false;
|
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
if (animate) {
|
|
|
|
this.actor._arrow_rotation = this._arrow.rotation_angle_z;
|
|
|
|
Tweener.addTween(this.actor,
|
|
|
|
{ _arrow_rotation: 0,
|
|
|
|
height: 0,
|
|
|
|
time: 0.25,
|
|
|
|
onCompleteScope: this,
|
|
|
|
onComplete: function() {
|
|
|
|
this.actor.hide();
|
|
|
|
this.actor.set_height(-1);
|
|
|
|
|
|
|
|
this.emit('open-state-changed', false);
|
|
|
|
},
|
|
|
|
onUpdateScope: this,
|
|
|
|
onUpdate: function() {
|
|
|
|
this._arrow.rotation_angle_z = this.actor._arrow_rotation;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this._arrow.rotation_angle_z = 0;
|
|
|
|
this.actor.hide();
|
2010-11-01 11:03:28 -04:00
|
|
|
|
2010-11-18 16:18:54 -05:00
|
|
|
this.isOpen = false;
|
|
|
|
this.emit('open-state-changed', false);
|
|
|
|
}
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
_onKeyPressEvent: function(actor, event) {
|
|
|
|
// Move focus back to parent menu if the user types Left.
|
2010-07-03 19:47:31 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
if (this.isOpen && event.get_key_symbol() == Clutter.KEY_Left) {
|
2010-11-18 16:18:54 -05:00
|
|
|
this.close(true);
|
2010-11-01 11:03:28 -04:00
|
|
|
this.sourceActor._delegate.setActive(true);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-01-25 16:04:57 -05:00
|
|
|
/**
|
|
|
|
* PopupMenuSection:
|
|
|
|
*
|
|
|
|
* A section of a PopupMenu which is handled like a submenu
|
|
|
|
* (you can add and remove items, you can destroy it, you
|
|
|
|
* can add it to another menu), but is completely transparent
|
|
|
|
* to the user
|
|
|
|
*/
|
|
|
|
function PopupMenuSection() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupMenuSection.prototype = {
|
|
|
|
__proto__: PopupMenuBase.prototype,
|
|
|
|
|
|
|
|
_init: function() {
|
|
|
|
PopupMenuBase.prototype._init.call(this);
|
|
|
|
|
|
|
|
this.actor = this.box;
|
|
|
|
this.actor._delegate = this;
|
|
|
|
this.isOpen = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
// deliberately ignore any attempt to open() or close()
|
|
|
|
open: function(animate) { },
|
|
|
|
close: function() { },
|
|
|
|
}
|
|
|
|
|
2010-07-03 19:47:31 -04:00
|
|
|
function PopupSubMenuMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupSubMenuMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
|
|
|
_init: function(text) {
|
2010-11-01 11:03:28 -04:00
|
|
|
PopupBaseMenuItem.prototype._init.call(this);
|
|
|
|
|
|
|
|
this.actor.add_style_class_name('popup-submenu-menu-item');
|
2010-07-03 19:47:31 -04:00
|
|
|
|
|
|
|
this.label = new St.Label({ text: text });
|
2010-10-19 13:41:41 -04:00
|
|
|
this.addActor(this.label);
|
2010-11-01 11:03:28 -04:00
|
|
|
this._triangle = new St.Label({ text: '\u25B8' });
|
|
|
|
this.addActor(this._triangle, { align: St.Align.END });
|
2010-07-03 19:47:31 -04:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
this.menu = new PopupSubMenu(this.actor, this._triangle);
|
|
|
|
this.menu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_subMenuOpenStateChanged: function(menu, open) {
|
2010-11-01 11:03:28 -04:00
|
|
|
if (open)
|
|
|
|
this.actor.add_style_pseudo_class('open');
|
|
|
|
else
|
|
|
|
this.actor.remove_style_pseudo_class('open');
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
2010-11-01 11:03:28 -04:00
|
|
|
this.menu.destroy();
|
2010-07-03 19:47:31 -04:00
|
|
|
PopupBaseMenuItem.prototype.destroy.call(this);
|
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
_onKeyPressEvent: function(actor, event) {
|
2011-02-08 14:53:43 -05:00
|
|
|
let symbol = event.get_key_symbol();
|
|
|
|
|
|
|
|
if (symbol == Clutter.KEY_Right) {
|
2010-11-18 16:18:54 -05:00
|
|
|
this.menu.open(true);
|
2011-02-08 14:53:43 -05:00
|
|
|
this.menu.actor.navigate_focus(null, Gtk.DirectionType.DOWN, false);
|
|
|
|
return true;
|
|
|
|
} else if (symbol == Clutter.KEY_Left && this.menu.isOpen) {
|
|
|
|
this.menu.close();
|
2010-07-03 19:47:31 -04:00
|
|
|
return true;
|
|
|
|
}
|
2011-02-08 14:53:43 -05:00
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
return PopupBaseMenuItem.prototype._onKeyPressEvent.call(this, actor, event);
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
activate: function(event) {
|
2010-11-18 16:18:54 -05:00
|
|
|
this.menu.open(true);
|
2010-07-03 19:47:31 -04:00
|
|
|
},
|
|
|
|
|
2010-11-01 11:03:28 -04:00
|
|
|
_onButtonReleaseEvent: function(actor) {
|
|
|
|
this.menu.toggle();
|
2010-07-03 19:47:31 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2011-07-20 23:17:05 -04:00
|
|
|
function PopupComboMenu() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupComboMenu.prototype = {
|
|
|
|
__proto__: PopupMenuBase.prototype,
|
|
|
|
|
|
|
|
_init: function(sourceActor) {
|
|
|
|
PopupMenuBase.prototype._init.call(this,
|
|
|
|
sourceActor, 'popup-combo-menu');
|
|
|
|
this.actor = this.box;
|
|
|
|
this.actor._delegate = this;
|
|
|
|
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
|
|
|
|
this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
|
|
|
|
this._activeItemPos = -1;
|
|
|
|
global.focus_manager.add_group(this.actor);
|
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyPressEvent: function(actor, event) {
|
|
|
|
if (event.get_key_symbol() == Clutter.Escape) {
|
|
|
|
this.close(true);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
_onKeyFocusIn: function(actor) {
|
|
|
|
let items = this._getMenuItems();
|
|
|
|
let activeItem = items[this._activeItemPos];
|
|
|
|
activeItem.actor.grab_key_focus();
|
|
|
|
},
|
|
|
|
|
|
|
|
open: function() {
|
|
|
|
if (this.isOpen)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.isOpen = true;
|
|
|
|
|
|
|
|
let [sourceX, sourceY] = this.sourceActor.get_transformed_position();
|
|
|
|
let items = this._getMenuItems();
|
|
|
|
let activeItem = items[this._activeItemPos];
|
|
|
|
|
|
|
|
this.actor.set_position(sourceX, sourceY - activeItem.actor.y);
|
|
|
|
this.actor.width = Math.max(this.actor.width, this.sourceActor.width);
|
|
|
|
this.actor.raise_top();
|
|
|
|
|
|
|
|
this.actor.opacity = 0;
|
|
|
|
this.actor.show();
|
|
|
|
|
|
|
|
Tweener.addTween(this.actor,
|
|
|
|
{ opacity: 255,
|
|
|
|
transition: 'linear',
|
|
|
|
time: BoxPointer.POPUP_ANIMATION_TIME });
|
|
|
|
|
|
|
|
this.emit('open-state-changed', true);
|
|
|
|
},
|
|
|
|
|
|
|
|
close: function() {
|
|
|
|
if (!this.isOpen)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.isOpen = false;
|
|
|
|
Tweener.addTween(this.actor,
|
|
|
|
{ opacity: 0,
|
|
|
|
transition: 'linear',
|
|
|
|
time: BoxPointer.POPUP_ANIMATION_TIME,
|
|
|
|
onComplete: Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
this.actor.hide();
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
this.emit('open-state-changed', false);
|
|
|
|
},
|
|
|
|
|
|
|
|
setActiveItem: function(position) {
|
|
|
|
this._activeItemPos = position;
|
|
|
|
},
|
|
|
|
|
|
|
|
setItemVisible: function(position, visible) {
|
|
|
|
if (!visible && position == this._activeItemPos) {
|
|
|
|
log('Trying to hide the active menu item.');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._getMenuItems()[position].actor.visible = visible;
|
2011-09-01 13:33:31 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
getItemVisible: function(position) {
|
|
|
|
return this._getMenuItems()[position].actor.visible;
|
2011-07-20 23:17:05 -04:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
function PopupComboBoxMenuItem() {
|
|
|
|
this._init.apply(this, arguments);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupComboBoxMenuItem.prototype = {
|
|
|
|
__proto__: PopupBaseMenuItem.prototype,
|
|
|
|
|
|
|
|
_init: function (params) {
|
|
|
|
PopupBaseMenuItem.prototype._init.call(this, params);
|
|
|
|
|
|
|
|
this._itemBox = new Shell.Stack();
|
|
|
|
this.addActor(this._itemBox);
|
|
|
|
|
|
|
|
let expander = new St.Label({ text: '\u2304' });
|
2011-08-23 13:15:31 -04:00
|
|
|
this.addActor(expander, { align: St.Align.END,
|
|
|
|
span: -1 });
|
2011-07-20 23:17:05 -04:00
|
|
|
|
|
|
|
this._menu = new PopupComboMenu(this.actor);
|
|
|
|
Main.uiGroup.add_actor(this._menu.actor);
|
|
|
|
this._menu.actor.hide();
|
|
|
|
|
|
|
|
if (params.style_class)
|
|
|
|
this._menu.actor.add_style_class_name(params.style_class);
|
|
|
|
|
2011-09-01 13:33:31 -04:00
|
|
|
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
|
|
|
|
|
2011-07-20 23:17:05 -04:00
|
|
|
this._activeItemPos = -1;
|
|
|
|
this._items = [];
|
|
|
|
},
|
|
|
|
|
|
|
|
_getTopMenu: function() {
|
|
|
|
let actor = this.actor.get_parent();
|
|
|
|
while (actor) {
|
|
|
|
if (actor._delegate &&
|
|
|
|
(actor._delegate instanceof PopupMenu ||
|
|
|
|
actor._delegate instanceof PopupComboMenu))
|
|
|
|
return actor._delegate;
|
|
|
|
|
|
|
|
actor = actor.get_parent();
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
|
2011-09-01 13:33:31 -04:00
|
|
|
_onScrollEvent: function(actor, event) {
|
|
|
|
if (this._activeItemPos == -1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
let position = this._activeItemPos;
|
|
|
|
let direction = event.get_scroll_direction();
|
|
|
|
if (direction == Clutter.ScrollDirection.DOWN) {
|
|
|
|
while (position < this._items.length - 1) {
|
|
|
|
position++;
|
|
|
|
if (this._menu.getItemVisible(position))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else if (direction == Clutter.ScrollDirection.UP) {
|
|
|
|
while (position > 0) {
|
|
|
|
position--;
|
|
|
|
if (this._menu.getItemVisible(position))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (position == this._activeItemPos)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.setActiveItem(position);
|
|
|
|
this.emit('active-item-changed', position);
|
|
|
|
},
|
|
|
|
|
2011-07-20 23:17:05 -04:00
|
|
|
activate: function(event) {
|
|
|
|
let topMenu = this._getTopMenu();
|
|
|
|
if (!topMenu)
|
|
|
|
return;
|
|
|
|
|
|
|
|
topMenu.addChildMenu(this._menu);
|
|
|
|
this._menu.toggle();
|
|
|
|
},
|
|
|
|
|
|
|
|
addMenuItem: function(menuItem, position) {
|
|
|
|
if (position === undefined)
|
|
|
|
position = this._menu.numMenuItems;
|
|
|
|
|
|
|
|
this._menu.addMenuItem(menuItem, position);
|
|
|
|
_ensureStyle(this._menu.actor);
|
|
|
|
|
|
|
|
let item = new St.BoxLayout({ style_class: 'popup-combobox-item' });
|
|
|
|
|
|
|
|
let children = menuItem.actor.get_children();
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
let clone = new Clutter.Clone({ source: children[i] });
|
|
|
|
item.add(clone, { y_fill: false });
|
|
|
|
}
|
|
|
|
|
|
|
|
let oldItem = this._items[position];
|
|
|
|
if (oldItem)
|
|
|
|
this._itemBox.remove_actor(oldItem);
|
|
|
|
|
|
|
|
this._items[position] = item;
|
|
|
|
this._itemBox.add_actor(item);
|
|
|
|
|
|
|
|
menuItem.connect('activate',
|
|
|
|
Lang.bind(this, this._itemActivated, position));
|
|
|
|
},
|
|
|
|
|
|
|
|
setActiveItem: function(position) {
|
|
|
|
let item = this._items[position];
|
|
|
|
if (!item)
|
|
|
|
return;
|
|
|
|
if (this._activeItemPos == position)
|
|
|
|
return;
|
|
|
|
this._menu.setActiveItem(position);
|
|
|
|
this._activeItemPos = position;
|
|
|
|
for (let i = 0; i < this._items.length; i++)
|
|
|
|
this._items[i].visible = (i == this._activeItemPos);
|
|
|
|
},
|
|
|
|
|
|
|
|
setItemVisible: function(position, visible) {
|
|
|
|
this._menu.setItemVisible(position, visible);
|
|
|
|
},
|
|
|
|
|
|
|
|
_itemActivated: function(menuItem, event, position) {
|
|
|
|
this.setActiveItem(position);
|
|
|
|
this.emit('active-item-changed', position);
|
|
|
|
}
|
|
|
|
};
|
2010-07-03 19:47:31 -04:00
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
/* Basic implementation of a menu manager.
|
|
|
|
* Call addMenu to add menus
|
|
|
|
*/
|
|
|
|
function PopupMenuManager(owner) {
|
|
|
|
this._init(owner);
|
|
|
|
}
|
|
|
|
|
|
|
|
PopupMenuManager.prototype = {
|
|
|
|
_init: function(owner) {
|
|
|
|
this._owner = owner;
|
|
|
|
this.grabbed = false;
|
|
|
|
|
|
|
|
this._eventCaptureId = 0;
|
|
|
|
this._enterEventId = 0;
|
|
|
|
this._leaveEventId = 0;
|
2011-02-08 14:53:43 -05:00
|
|
|
this._keyFocusNotifyId = 0;
|
2010-05-20 11:18:46 -04:00
|
|
|
this._activeMenu = null;
|
|
|
|
this._menus = [];
|
2011-07-12 15:47:37 -04:00
|
|
|
this._menuStack = [];
|
2010-11-03 13:30:08 -04:00
|
|
|
this._preGrabInputMode = null;
|
2011-02-08 14:53:43 -05:00
|
|
|
this._grabbedFromKeynav = false;
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
addMenu: function(menu, position) {
|
2010-06-25 08:55:03 -04:00
|
|
|
let menudata = {
|
|
|
|
menu: menu,
|
|
|
|
openStateChangeId: menu.connect('open-state-changed', Lang.bind(this, this._onMenuOpenState)),
|
2011-07-12 15:47:37 -04:00
|
|
|
childMenuAddedId: menu.connect('child-menu-added', Lang.bind(this, this._onChildMenuAdded)),
|
|
|
|
childMenuRemovedId: menu.connect('child-menu-removed', Lang.bind(this, this._onChildMenuRemoved)),
|
2010-09-29 12:12:38 -04:00
|
|
|
destroyId: menu.connect('destroy', Lang.bind(this, this._onMenuDestroy)),
|
2010-06-25 08:55:03 -04:00
|
|
|
enterId: 0,
|
2011-02-08 14:53:43 -05:00
|
|
|
focusInId: 0
|
2010-06-25 08:55:03 -04:00
|
|
|
};
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
let source = menu.sourceActor;
|
|
|
|
if (source) {
|
2010-10-07 14:15:51 -04:00
|
|
|
menudata.enterId = source.connect('enter-event', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
|
2010-11-03 13:30:08 -04:00
|
|
|
menudata.focusInId = source.connect('key-focus-in', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
2010-06-25 08:55:03 -04:00
|
|
|
|
|
|
|
if (position == undefined)
|
|
|
|
this._menus.push(menudata);
|
|
|
|
else
|
|
|
|
this._menus.splice(position, 0, menudata);
|
|
|
|
},
|
|
|
|
|
|
|
|
removeMenu: function(menu) {
|
|
|
|
if (menu == this._activeMenu)
|
|
|
|
this._closeMenu();
|
2010-07-03 19:47:31 -04:00
|
|
|
|
2010-06-25 08:55:03 -04:00
|
|
|
let position = this._findMenu(menu);
|
|
|
|
if (position == -1) // not a menu we manage
|
|
|
|
return;
|
|
|
|
|
|
|
|
let menudata = this._menus[position];
|
|
|
|
menu.disconnect(menudata.openStateChangeId);
|
2011-07-12 15:47:37 -04:00
|
|
|
menu.disconnect(menudata.childMenuAddedId);
|
|
|
|
menu.disconnect(menudata.childMenuRemovedId);
|
2010-07-03 19:47:31 -04:00
|
|
|
menu.disconnect(menudata.destroyId);
|
2010-06-25 08:55:03 -04:00
|
|
|
|
|
|
|
if (menudata.enterId)
|
|
|
|
menu.sourceActor.disconnect(menudata.enterId);
|
2010-11-03 13:30:08 -04:00
|
|
|
if (menudata.focusInId)
|
|
|
|
menu.sourceActor.disconnect(menudata.focusInId);
|
2010-06-25 08:55:03 -04:00
|
|
|
|
|
|
|
this._menus.splice(position, 1);
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-11-03 13:30:08 -04:00
|
|
|
_grab: function() {
|
2010-05-20 11:18:46 -04:00
|
|
|
Main.pushModal(this._owner.actor);
|
|
|
|
|
|
|
|
this._eventCaptureId = global.stage.connect('captured-event', Lang.bind(this, this._onEventCapture));
|
|
|
|
// captured-event doesn't see enter/leave events
|
|
|
|
this._enterEventId = global.stage.connect('enter-event', Lang.bind(this, this._onEventCapture));
|
|
|
|
this._leaveEventId = global.stage.connect('leave-event', Lang.bind(this, this._onEventCapture));
|
2011-02-08 14:53:43 -05:00
|
|
|
this._keyFocusNotifyId = global.stage.connect('notify::key-focus', Lang.bind(this, this._onKeyFocusChanged));
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
this.grabbed = true;
|
|
|
|
},
|
|
|
|
|
2010-11-03 13:30:08 -04:00
|
|
|
_ungrab: function() {
|
2010-05-20 11:18:46 -04:00
|
|
|
global.stage.disconnect(this._eventCaptureId);
|
|
|
|
this._eventCaptureId = 0;
|
|
|
|
global.stage.disconnect(this._enterEventId);
|
|
|
|
this._enterEventId = 0;
|
|
|
|
global.stage.disconnect(this._leaveEventId);
|
|
|
|
this._leaveEventId = 0;
|
2011-02-08 14:53:43 -05:00
|
|
|
global.stage.disconnect(this._keyFocusNotifyId);
|
|
|
|
this._keyFocusNotifyId = 0;
|
2010-05-20 11:18:46 -04:00
|
|
|
|
|
|
|
this.grabbed = false;
|
2010-10-07 14:15:51 -04:00
|
|
|
Main.popModal(this._owner.actor);
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
|
|
|
_onMenuOpenState: function(menu, open) {
|
2011-07-12 15:47:37 -04:00
|
|
|
if (open) {
|
|
|
|
if (this._activeMenu && this._activeMenu.isChildMenu(menu)) {
|
|
|
|
this._menuStack.push(this._activeMenu);
|
|
|
|
menu.actor.grab_key_focus();
|
|
|
|
}
|
2011-02-08 14:53:43 -05:00
|
|
|
this._activeMenu = menu;
|
2011-07-12 15:47:37 -04:00
|
|
|
} else {
|
|
|
|
if (this._menuStack.length > 0) {
|
|
|
|
this._activeMenu = this._menuStack.pop();
|
|
|
|
if (menu.sourceActor)
|
|
|
|
menu.sourceActor.grab_key_focus();
|
|
|
|
this._didPop = true;
|
|
|
|
}
|
|
|
|
}
|
2011-02-08 14:53:43 -05:00
|
|
|
|
|
|
|
// Check what the focus was before calling pushModal/popModal
|
|
|
|
let focus = global.stage.key_focus;
|
|
|
|
let hadFocus = focus && this._activeMenuContains(focus);
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
if (open) {
|
2010-11-03 13:30:08 -04:00
|
|
|
if (!this.grabbed) {
|
|
|
|
this._preGrabInputMode = global.stage_input_mode;
|
2011-02-08 14:53:43 -05:00
|
|
|
this._grabbedFromKeynav = hadFocus;
|
2010-11-03 13:30:08 -04:00
|
|
|
this._grab();
|
|
|
|
}
|
|
|
|
|
2011-02-08 14:53:43 -05:00
|
|
|
if (hadFocus)
|
|
|
|
focus.grab_key_focus();
|
|
|
|
else
|
|
|
|
menu.actor.grab_key_focus();
|
2010-10-07 14:15:51 -04:00
|
|
|
} else if (menu == this._activeMenu) {
|
|
|
|
if (this.grabbed)
|
2010-11-03 13:30:08 -04:00
|
|
|
this._ungrab();
|
|
|
|
this._activeMenu = null;
|
|
|
|
|
2011-02-08 14:53:43 -05:00
|
|
|
if (this._grabbedFromKeynav) {
|
|
|
|
if (this._preGrabInputMode == Shell.StageInputMode.FOCUSED)
|
|
|
|
global.stage_input_mode = Shell.StageInputMode.FOCUSED;
|
|
|
|
if (hadFocus && menu.sourceActor)
|
2010-11-03 13:30:08 -04:00
|
|
|
menu.sourceActor.grab_key_focus();
|
2011-02-08 14:53:43 -05:00
|
|
|
else if (focus)
|
2010-11-03 13:30:08 -04:00
|
|
|
focus.grab_key_focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2011-07-12 15:47:37 -04:00
|
|
|
_onChildMenuAdded: function(menu, childMenu) {
|
|
|
|
this.addMenu(childMenu);
|
|
|
|
},
|
|
|
|
|
|
|
|
_onChildMenuRemoved: function(menu, childMenu) {
|
|
|
|
this.removeMenu(childMenu);
|
|
|
|
},
|
|
|
|
|
2010-11-03 13:30:08 -04:00
|
|
|
// change the currently-open menu without dropping grab
|
|
|
|
_changeMenu: function(newMenu) {
|
|
|
|
if (this._activeMenu) {
|
|
|
|
// _onOpenMenuState will drop the grab if it sees
|
|
|
|
// this._activeMenu being closed; so clear _activeMenu
|
|
|
|
// before closing it to keep that from happening
|
|
|
|
let oldMenu = this._activeMenu;
|
|
|
|
this._activeMenu = null;
|
2011-07-12 15:47:37 -04:00
|
|
|
for (let i = this._menuStack.length - 1; i >= 0; i--)
|
|
|
|
this._menuStack[i].close(false);
|
2010-11-18 16:18:54 -05:00
|
|
|
oldMenu.close(false);
|
|
|
|
newMenu.open(false);
|
|
|
|
} else
|
|
|
|
newMenu.open(true);
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-10-07 14:15:51 -04:00
|
|
|
_onMenuSourceEnter: function(menu) {
|
2010-05-20 11:18:46 -04:00
|
|
|
if (!this.grabbed || menu == this._activeMenu)
|
|
|
|
return false;
|
|
|
|
|
2011-07-12 15:47:37 -04:00
|
|
|
if (this._activeMenu && this._activeMenu.isChildMenu(menu))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (this._menuStack.indexOf(menu) != -1)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (this._menuStack.length > 0 && this._menuStack[0].isChildMenu(menu))
|
|
|
|
return false;
|
|
|
|
|
2010-11-03 13:30:08 -04:00
|
|
|
this._changeMenu(menu);
|
2010-05-20 11:18:46 -04:00
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
2011-02-08 14:53:43 -05:00
|
|
|
_onKeyFocusChanged: function() {
|
|
|
|
if (!this.grabbed || !this._activeMenu)
|
2010-11-03 13:30:08 -04:00
|
|
|
return;
|
|
|
|
|
2011-02-08 14:53:43 -05:00
|
|
|
let focus = global.stage.key_focus;
|
|
|
|
if (focus) {
|
|
|
|
if (this._activeMenuContains(focus))
|
|
|
|
return;
|
2011-07-12 15:47:37 -04:00
|
|
|
if (this._menuStack.length > 0)
|
|
|
|
return;
|
2011-02-08 14:53:43 -05:00
|
|
|
if (focus._delegate && focus._delegate.menu &&
|
|
|
|
this._findMenu(focus._delegate.menu) != -1)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this._closeMenu();
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-07-03 19:47:31 -04:00
|
|
|
_onMenuDestroy: function(menu) {
|
|
|
|
this.removeMenu(menu);
|
|
|
|
},
|
|
|
|
|
2010-11-03 13:30:08 -04:00
|
|
|
_activeMenuContains: function(actor) {
|
2010-05-20 11:18:46 -04:00
|
|
|
return this._activeMenu != null
|
2010-11-03 13:30:08 -04:00
|
|
|
&& (this._activeMenu.actor.contains(actor) ||
|
|
|
|
(this._activeMenu.sourceActor && this._activeMenu.sourceActor.contains(actor)));
|
|
|
|
},
|
|
|
|
|
|
|
|
_eventIsOnActiveMenu: function(event) {
|
|
|
|
return this._activeMenuContains(event.get_source());
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2011-03-22 10:29:32 -04:00
|
|
|
_shouldBlockEvent: function(event) {
|
2010-05-20 11:18:46 -04:00
|
|
|
let src = event.get_source();
|
2011-03-22 10:29:32 -04:00
|
|
|
|
|
|
|
if (this._activeMenu != null && this._activeMenu.actor.contains(src))
|
|
|
|
return false;
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
for (let i = 0; i < this._menus.length; i++) {
|
2010-06-25 08:55:03 -04:00
|
|
|
let menu = this._menus[i].menu;
|
2011-03-22 10:29:32 -04:00
|
|
|
if (menu.sourceActor && !menu.blockSourceEvents && menu.sourceActor.contains(src)) {
|
|
|
|
return false;
|
|
|
|
}
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
2011-03-22 10:29:32 -04:00
|
|
|
|
|
|
|
return true;
|
2010-05-20 11:18:46 -04:00
|
|
|
},
|
|
|
|
|
2010-06-25 08:55:03 -04:00
|
|
|
_findMenu: function(item) {
|
|
|
|
for (let i = 0; i < this._menus.length; i++) {
|
|
|
|
let menudata = this._menus[i];
|
|
|
|
if (item == menudata.menu)
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
},
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
_onEventCapture: function(actor, event) {
|
|
|
|
if (!this.grabbed)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
if (this._owner.menuEventFilter &&
|
|
|
|
this._owner.menuEventFilter(event))
|
|
|
|
return true;
|
|
|
|
|
2011-04-02 12:35:03 -04:00
|
|
|
if (this._activeMenu != null && this._activeMenu.passEvents)
|
|
|
|
return false;
|
|
|
|
|
2011-07-12 15:47:37 -04:00
|
|
|
if (this._didPop) {
|
|
|
|
this._didPop = false;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
let activeMenuContains = this._eventIsOnActiveMenu(event);
|
|
|
|
let eventType = event.type();
|
2011-04-02 12:35:03 -04:00
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
if (eventType == Clutter.EventType.BUTTON_RELEASE) {
|
|
|
|
if (activeMenuContains) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
this._closeMenu();
|
|
|
|
return true;
|
|
|
|
}
|
2011-02-08 14:53:43 -05:00
|
|
|
} else if (eventType == Clutter.EventType.BUTTON_PRESS && !activeMenuContains) {
|
2010-05-20 11:18:46 -04:00
|
|
|
this._closeMenu();
|
|
|
|
return true;
|
2011-03-22 10:29:32 -04:00
|
|
|
} else if (!this._shouldBlockEvent(event)) {
|
2010-10-07 14:15:51 -04:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
2010-05-20 11:18:46 -04:00
|
|
|
_closeMenu: function() {
|
|
|
|
if (this._activeMenu != null)
|
2010-11-18 16:18:54 -05:00
|
|
|
this._activeMenu.close(true);
|
2010-05-20 11:18:46 -04:00
|
|
|
}
|
|
|
|
};
|