1
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
3
const Clutter = imports.gi.Clutter;
4
const GLib = imports.gi.GLib;
5
const Gtk = imports.gi.Gtk;
6
const Gio = imports.gi.Gio;
7
const Lang = imports.lang;
8
const Shell = imports.gi.Shell;
9
const Signals = imports.signals;
10
const St = imports.gi.St;
11
const Atk = imports.gi.Atk;
13
const BoxPointer = imports.ui.boxpointer;
14
const GrabHelper = imports.ui.grabHelper;
15
const Main = imports.ui.main;
16
const Params = imports.misc.params;
17
const Separator = imports.ui.separator;
18
const Tweener = imports.ui.tweener;
20
const SLIDER_SCROLL_STEP = 0.05; /* Slider scrolling step in % */
22
function _ensureStyle(actor) {
23
if (actor.get_children) {
24
let children = actor.get_children();
25
for (let i = 0; i < children.length; i++)
26
_ensureStyle(children[i]);
29
if (actor instanceof St.Widget)
33
const PopupBaseMenuItem = new Lang.Class({
34
Name: 'PopupBaseMenuItem',
36
_init: function (params) {
37
params = Params.parse (params, { reactive: true,
44
this.actor = new Shell.GenericContainer({ style_class: 'popup-menu-item',
45
reactive: params.reactive,
46
track_hover: params.reactive,
47
can_focus: params.can_focus,
48
accessible_role: Atk.Role.MENU_ITEM});
49
this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
50
this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
51
this.actor.connect('allocate', Lang.bind(this, this._allocate));
52
this.actor.connect('style-changed', Lang.bind(this, this._onStyleChanged));
53
this.actor._delegate = this;
57
this._columnWidths = null;
60
this._activatable = params.reactive && params.activate;
61
this.sensitive = this._activatable && params.sensitive;
63
this.setSensitive(this.sensitive);
65
if (!this._activatable)
66
this.actor.add_style_class_name('popup-inactive-menu-item');
68
if (params.style_class)
69
this.actor.add_style_class_name(params.style_class);
71
if (this._activatable) {
72
this.actor.connect('button-release-event', Lang.bind(this, this._onButtonReleaseEvent));
73
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
75
if (params.reactive && params.hover)
76
this.actor.connect('notify::hover', Lang.bind(this, this._onHoverChanged));
78
this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
79
this.actor.connect('key-focus-out', Lang.bind(this, this._onKeyFocusOut));
82
_onStyleChanged: function (actor) {
83
this._spacing = Math.round(actor.get_theme_node().get_length('spacing'));
86
_onButtonReleaseEvent: function (actor, event) {
91
_onKeyPressEvent: function (actor, event) {
92
let symbol = event.get_key_symbol();
94
if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
101
_onKeyFocusIn: function (actor) {
102
this.setActive(true);
105
_onKeyFocusOut: function (actor) {
106
this.setActive(false);
109
_onHoverChanged: function (actor) {
110
this.setActive(actor.hover);
113
activate: function (event) {
114
this.emit('activate', event);
117
setActive: function (active, params) {
118
let activeChanged = active != this.active;
119
params = Params.parse (params, { grabKeyboard: true });
122
this.active = active;
124
this.actor.add_style_pseudo_class('active');
125
if (params.grabKeyboard)
126
this.actor.grab_key_focus();
128
this.actor.remove_style_pseudo_class('active');
129
this.emit('active-changed', active);
133
setSensitive: function(sensitive) {
134
if (!this._activatable)
136
if (this.sensitive == sensitive)
139
this.sensitive = sensitive;
140
this.actor.reactive = sensitive;
141
this.actor.can_focus = sensitive;
143
this.emit('sensitive-changed', sensitive);
146
destroy: function() {
147
this.actor.destroy();
148
this.emit('destroy');
151
// adds an actor to the menu item; @params can contain %span
152
// (column span; defaults to 1, -1 means "all the remaining width"),
153
// %expand (defaults to #false), and %align (defaults to
155
addActor: function(child, params) {
156
params = Params.parse(params, { span: 1,
158
align: St.Align.START });
159
params.actor = child;
160
this._children.push(params);
161
this.actor.connect('destroy', Lang.bind(this, function () { this._removeChild(child); }));
162
this.actor.add_actor(child);
165
_removeChild: function(child) {
166
for (let i = 0; i < this._children.length; i++) {
167
if (this._children[i].actor == child) {
168
this._children.splice(i, 1);
174
removeActor: function(child) {
175
this.actor.remove_actor(child);
176
this._removeChild(child);
179
setShowDot: function(show) {
184
this._dot = new St.DrawingArea({ style_class: 'popup-menu-item-dot' });
185
this._dot.connect('repaint', Lang.bind(this, this._onRepaintDot));
186
this.actor.add_actor(this._dot);
187
this.actor.add_accessible_state (Atk.StateType.CHECKED);
194
this.actor.remove_accessible_state (Atk.StateType.CHECKED);
198
_onRepaintDot: function(area) {
199
let cr = area.get_context();
200
let [width, height] = area.get_surface_size();
201
let color = area.get_theme_node().get_foreground_color();
208
cr.arc(width / 2, height / 2, width / 3, 0, 2 * Math.PI);
213
// This returns column widths in logical order (i.e. from the dot
214
// to the image), not in visual order (left to right)
215
getColumnWidths: function() {
217
for (let i = 0, col = 0; i < this._children.length; i++) {
218
let child = this._children[i];
219
let [min, natural] = child.actor.get_preferred_width(-1);
220
widths[col++] = natural;
221
if (child.span > 1) {
222
for (let j = 1; j < child.span; j++)
229
setColumnWidths: function(widths) {
230
this._columnWidths = widths;
233
_getPreferredWidth: function(actor, forHeight, alloc) {
235
if (this._columnWidths) {
236
for (let i = 0; i < this._columnWidths.length; i++) {
238
width += this._spacing;
239
width += this._columnWidths[i];
242
for (let i = 0; i < this._children.length; i++) {
243
let child = this._children[i];
245
width += this._spacing;
246
let [min, natural] = child.actor.get_preferred_width(-1);
250
alloc.min_size = alloc.natural_size = width;
253
_getPreferredHeight: function(actor, forWidth, alloc) {
254
let height = 0, x = 0, minWidth, childWidth;
255
for (let i = 0; i < this._children.length; i++) {
256
let child = this._children[i];
257
if (this._columnWidths) {
258
if (child.span == -1) {
260
for (let j = i; j < this._columnWidths.length; j++)
261
childWidth += this._columnWidths[j]
263
childWidth = this._columnWidths[i];
265
if (child.span == -1)
266
childWidth = forWidth - x;
268
[minWidth, childWidth] = child.actor.get_preferred_width(-1);
272
let [min, natural] = child.actor.get_preferred_height(childWidth);
273
if (natural > height)
276
alloc.min_size = alloc.natural_size = height;
279
_allocate: function(actor, box, flags) {
280
let height = box.y2 - box.y1;
281
let direction = this.actor.get_text_direction();
284
// The dot is placed outside box
285
// one quarter of padding from the border of the container
286
// (so 3/4 from the inner border)
287
// (padding is box.x1)
288
let dotBox = new Clutter.ActorBox();
289
let dotWidth = Math.round(box.x1 / 2);
291
if (direction == Clutter.TextDirection.LTR) {
292
dotBox.x1 = Math.round(box.x1 / 4);
293
dotBox.x2 = dotBox.x1 + dotWidth;
295
dotBox.x2 = box.x2 + 3 * Math.round(box.x1 / 4);
296
dotBox.x1 = dotBox.x2 - dotWidth;
298
dotBox.y1 = Math.round(box.y1 + (height - dotWidth) / 2);
299
dotBox.y2 = dotBox.y1 + dotWidth;
300
this._dot.allocate(dotBox, flags);
304
if (direction == Clutter.TextDirection.LTR)
308
// if direction is ltr, x is the right edge of the last added
309
// actor, and it's constantly increasing, whereas if rtl, x is
310
// the left edge and it decreases
311
for (let i = 0, col = 0; i < this._children.length; i++) {
312
let child = this._children[i];
313
let childBox = new Clutter.ActorBox();
315
let [minWidth, naturalWidth] = child.actor.get_preferred_width(-1);
316
let availWidth, extraWidth;
317
if (this._columnWidths) {
318
if (child.span == -1) {
319
if (direction == Clutter.TextDirection.LTR)
320
availWidth = box.x2 - x;
322
availWidth = x - box.x1;
325
for (let j = 0; j < child.span; j++)
326
availWidth += this._columnWidths[col++];
328
extraWidth = availWidth - naturalWidth;
330
if (child.span == -1) {
331
if (direction == Clutter.TextDirection.LTR)
332
availWidth = box.x2 - x;
334
availWidth = x - box.x1;
336
availWidth = naturalWidth;
341
if (direction == Clutter.TextDirection.LTR) {
344
childBox.x2 = x + availWidth;
345
} else if (child.align === St.Align.MIDDLE) {
346
childBox.x1 = x + Math.round(extraWidth / 2);
347
childBox.x2 = childBox.x1 + naturalWidth;
348
} else if (child.align === St.Align.END) {
349
childBox.x2 = x + availWidth;
350
childBox.x1 = childBox.x2 - naturalWidth;
353
childBox.x2 = x + naturalWidth;
357
childBox.x1 = x - availWidth;
359
} else if (child.align === St.Align.MIDDLE) {
360
childBox.x1 = x - Math.round(extraWidth / 2);
361
childBox.x2 = childBox.x1 + naturalWidth;
362
} else if (child.align === St.Align.END) {
364
childBox.x1 = x - availWidth;
365
childBox.x2 = childBox.x1 + naturalWidth;
367
// align to the right
369
childBox.x1 = x - naturalWidth;
373
let [minHeight, naturalHeight] = child.actor.get_preferred_height(childBox.x2 - childBox.x1);
374
childBox.y1 = Math.round(box.y1 + (height - naturalHeight) / 2);
375
childBox.y2 = childBox.y1 + naturalHeight;
377
child.actor.allocate(childBox, flags);
379
if (direction == Clutter.TextDirection.LTR)
380
x += availWidth + this._spacing;
382
x -= availWidth + this._spacing;
386
Signals.addSignalMethods(PopupBaseMenuItem.prototype);
388
const PopupMenuItem = new Lang.Class({
389
Name: 'PopupMenuItem',
390
Extends: PopupBaseMenuItem,
392
_init: function (text, params) {
395
this.label = new St.Label({ text: text });
396
this.addActor(this.label);
397
this.actor.label_actor = this.label
401
const PopupSeparatorMenuItem = new Lang.Class({
402
Name: 'PopupSeparatorMenuItem',
403
Extends: PopupBaseMenuItem,
406
this.parent({ reactive: false,
409
this._separator = new Separator.HorizontalSeparator({ style_class: 'popup-separator-menu-item' });
410
this.addActor(this._separator.actor, { span: -1, expand: true });
414
const PopupAlternatingMenuItemState = {
419
const PopupAlternatingMenuItem = new Lang.Class({
420
Name: 'PopupAlternatingMenuItem',
421
Extends: PopupBaseMenuItem,
423
_init: function(text, alternateText, params) {
425
this.actor.add_style_class_name('popup-alternating-menu-item');
428
this._alternateText = alternateText;
429
this.label = new St.Label({ text: text });
430
this.state = PopupAlternatingMenuItemState.DEFAULT;
431
this.addActor(this.label);
432
this.actor.label_actor = this.label;
434
this.actor.connect('notify::mapped', Lang.bind(this, this._onMapped));
437
_onMapped: function() {
438
if (this.actor.mapped) {
439
this._capturedEventId = global.stage.connect('captured-event',
440
Lang.bind(this, this._onCapturedEvent));
441
this._updateStateFromModifiers();
443
if (this._capturedEventId != 0) {
444
global.stage.disconnect(this._capturedEventId);
445
this._capturedEventId = 0;
450
_setState: function(state) {
451
if (this.state != state) {
452
if (state == PopupAlternatingMenuItemState.ALTERNATIVE && !this._canAlternate())
460
_updateStateFromModifiers: function() {
461
let [x, y, mods] = global.get_pointer();
464
if ((mods & Clutter.ModifierType.MOD1_MASK) == 0) {
465
state = PopupAlternatingMenuItemState.DEFAULT;
467
state = PopupAlternatingMenuItemState.ALTERNATIVE;
470
this._setState(state);
473
_onCapturedEvent: function(actor, event) {
474
if (event.type() != Clutter.EventType.KEY_PRESS &&
475
event.type() != Clutter.EventType.KEY_RELEASE)
478
let key = event.get_key_symbol();
480
if (key == Clutter.KEY_Alt_L || key == Clutter.KEY_Alt_R)
481
this._updateStateFromModifiers();
486
_updateLabel: function() {
487
if (this.state == PopupAlternatingMenuItemState.ALTERNATIVE) {
488
this.actor.add_style_pseudo_class('alternate');
489
this.label.set_text(this._alternateText);
491
this.actor.remove_style_pseudo_class('alternate');
492
this.label.set_text(this._text);
496
_canAlternate: function() {
497
if (this.state == PopupAlternatingMenuItemState.DEFAULT && !this._alternateText)
502
updateText: function(text, alternateText) {
504
this._alternateText = alternateText;
506
if (!this._canAlternate())
507
this._setState(PopupAlternatingMenuItemState.DEFAULT);
513
const PopupSliderMenuItem = new Lang.Class({
514
Name: 'PopupSliderMenuItem',
515
Extends: PopupBaseMenuItem,
517
_init: function(value) {
518
this.parent({ activate: false });
520
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
523
// Avoid spreading NaNs around
524
throw TypeError('The slider value must be a number');
525
this._value = Math.max(Math.min(value, 1), 0);
527
this._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
528
this.addActor(this._slider, { span: -1, expand: true });
529
this._slider.connect('repaint', Lang.bind(this, this._sliderRepaint));
530
this.actor.connect('button-press-event', Lang.bind(this, this._startDragging));
531
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
532
this.actor.connect('notify::mapped', Lang.bind(this, function() {
533
if (!this.actor.mapped)
537
this._releaseId = this._motionId = 0;
538
this._dragging = false;
541
setValue: function(value) {
543
throw TypeError('The slider value must be a number');
545
this._value = Math.max(Math.min(value, 1), 0);
546
this._slider.queue_repaint();
549
_sliderRepaint: function(area) {
550
let cr = area.get_context();
551
let themeNode = area.get_theme_node();
552
let [width, height] = area.get_surface_size();
554
let handleRadius = themeNode.get_length('-slider-handle-radius');
556
let sliderWidth = width - 2 * handleRadius;
557
let sliderHeight = themeNode.get_length('-slider-height');
559
let sliderBorderWidth = themeNode.get_length('-slider-border-width');
561
let sliderBorderColor = themeNode.get_color('-slider-border-color');
562
let sliderColor = themeNode.get_color('-slider-background-color');
564
let sliderActiveBorderColor = themeNode.get_color('-slider-active-border-color');
565
let sliderActiveColor = themeNode.get_color('-slider-active-background-color');
568
sliderActiveColor.red / 255,
569
sliderActiveColor.green / 255,
570
sliderActiveColor.blue / 255,
571
sliderActiveColor.alpha / 255);
572
cr.rectangle(handleRadius, (height - sliderHeight) / 2, sliderWidth * this._value, sliderHeight);
575
sliderActiveBorderColor.red / 255,
576
sliderActiveBorderColor.green / 255,
577
sliderActiveBorderColor.blue / 255,
578
sliderActiveBorderColor.alpha / 255);
579
cr.setLineWidth(sliderBorderWidth);
583
sliderColor.red / 255,
584
sliderColor.green / 255,
585
sliderColor.blue / 255,
586
sliderColor.alpha / 255);
587
cr.rectangle(handleRadius + sliderWidth * this._value, (height - sliderHeight) / 2, sliderWidth * (1 - this._value), sliderHeight);
590
sliderBorderColor.red / 255,
591
sliderBorderColor.green / 255,
592
sliderBorderColor.blue / 255,
593
sliderBorderColor.alpha / 255);
594
cr.setLineWidth(sliderBorderWidth);
597
let handleY = height / 2;
598
let handleX = handleRadius + (width - 2 * handleRadius) * this._value;
600
let color = themeNode.get_foreground_color();
606
cr.arc(handleX, handleY, handleRadius, 0, 2 * Math.PI);
611
_startDragging: function(actor, event) {
612
if (this._dragging) // don't allow two drags at the same time
615
this._dragging = true;
617
// FIXME: we should only grab the specific device that originated
618
// the event, but for some weird reason events are still delivered
619
// outside the slider if using clutter_grab_pointer_for_device
620
Clutter.grab_pointer(this._slider);
621
this._releaseId = this._slider.connect('button-release-event', Lang.bind(this, this._endDragging));
622
this._motionId = this._slider.connect('motion-event', Lang.bind(this, this._motionEvent));
624
[absX, absY] = event.get_coords();
625
this._moveHandle(absX, absY);
628
_endDragging: function() {
629
if (this._dragging) {
630
this._slider.disconnect(this._releaseId);
631
this._slider.disconnect(this._motionId);
633
Clutter.ungrab_pointer();
634
this._dragging = false;
636
this.emit('drag-end');
641
scroll: function(event) {
642
let direction = event.get_scroll_direction();
645
if (event.is_pointer_emulated())
648
if (direction == Clutter.ScrollDirection.DOWN) {
649
delta = -SLIDER_SCROLL_STEP;
650
} else if (direction == Clutter.ScrollDirection.UP) {
651
delta = +SLIDER_SCROLL_STEP;
652
} else if (direction == Clutter.ScrollDirection.SMOOTH) {
653
let [dx, dy] = event.get_scroll_delta();
654
// Even though the slider is horizontal, use dy to match
655
// the UP/DOWN above.
659
this._value = Math.min(Math.max(0, this._value + delta), 1);
661
this._slider.queue_repaint();
662
this.emit('value-changed', this._value);
665
_onScrollEvent: function(actor, event) {
669
_motionEvent: function(actor, event) {
671
[absX, absY] = event.get_coords();
672
this._moveHandle(absX, absY);
676
_moveHandle: function(absX, absY) {
677
let relX, relY, sliderX, sliderY;
678
[sliderX, sliderY] = this._slider.get_transformed_position();
679
relX = absX - sliderX;
680
relY = absY - sliderY;
682
let width = this._slider.width;
683
let handleRadius = this._slider.get_theme_node().get_length('-slider-handle-radius');
686
if (relX < handleRadius)
688
else if (relX > width - handleRadius)
691
newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
692
this._value = newvalue;
693
this._slider.queue_repaint();
694
this.emit('value-changed', this._value);
701
_onKeyPressEvent: function (actor, event) {
702
let key = event.get_key_symbol();
703
if (key == Clutter.KEY_Right || key == Clutter.KEY_Left) {
704
let delta = key == Clutter.KEY_Right ? 0.1 : -0.1;
705
this._value = Math.max(0, Math.min(this._value + delta, 1));
706
this._slider.queue_repaint();
707
this.emit('value-changed', this._value);
708
this.emit('drag-end');
715
const Switch = new Lang.Class({
718
_init: function(state) {
719
this.actor = new St.Bin({ style_class: 'toggle-switch',
720
accessible_role: Atk.Role.CHECK_BOX,
722
// Translators: this MUST be either "toggle-switch-us"
723
// (for toggle switches containing the English words
724
// "ON" and "OFF") or "toggle-switch-intl" (for toggle
725
// switches containing "◯" and "|"). Other values will
726
// simply result in invisible toggle switches.
727
this.actor.add_style_class_name(_("toggle-switch-us"));
728
this.setToggleState(state);
731
setToggleState: function(state) {
733
this.actor.add_style_pseudo_class('checked');
735
this.actor.remove_style_pseudo_class('checked');
740
this.setToggleState(!this.state);
744
const PopupSwitchMenuItem = new Lang.Class({
745
Name: 'PopupSwitchMenuItem',
746
Extends: PopupBaseMenuItem,
748
_init: function(text, active, params) {
751
this.label = new St.Label({ text: text });
752
this._switch = new Switch(active);
754
this.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM;
755
this.checkAccessibleState();
756
this.actor.label_actor = this.label;
758
this.addActor(this.label);
760
this._statusBin = new St.Bin({ x_align: St.Align.END });
761
this.addActor(this._statusBin,
762
{ expand: true, span: -1, align: St.Align.END });
764
this._statusLabel = new St.Label({ text: '',
765
style_class: 'popup-status-menu-item'
767
this._statusBin.child = this._switch.actor;
770
setStatus: function(text) {
772
this._statusLabel.text = text;
773
this._statusBin.child = this._statusLabel;
774
this.actor.reactive = false;
775
this.actor.accessible_role = Atk.Role.MENU_ITEM;
777
this._statusBin.child = this._switch.actor;
778
this.actor.reactive = true;
779
this.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM;
781
this.checkAccessibleState();
784
activate: function(event) {
785
if (this._switch.actor.mapped) {
789
// we allow pressing space to toggle the switch
790
// without closing the menu
791
if (event.type() == Clutter.EventType.KEY_PRESS &&
792
event.get_key_symbol() == Clutter.KEY_space)
799
this._switch.toggle();
800
this.emit('toggled', this._switch.state);
801
this.checkAccessibleState();
805
return this._switch.state;
808
setToggleState: function(state) {
809
this._switch.setToggleState(state);
810
this.checkAccessibleState();
813
checkAccessibleState: function() {
814
switch (this.actor.accessible_role) {
815
case Atk.Role.CHECK_MENU_ITEM:
816
if (this._switch.state)
817
this.actor.add_accessible_state (Atk.StateType.CHECKED);
819
this.actor.remove_accessible_state (Atk.StateType.CHECKED);
822
this.actor.remove_accessible_state (Atk.StateType.CHECKED);
827
const PopupImageMenuItem = new Lang.Class({
828
Name: 'PopupImageMenuItem',
829
Extends: PopupBaseMenuItem,
831
_init: function (text, iconName, params) {
834
this.label = new St.Label({ text: text });
835
this.addActor(this.label);
836
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
837
this.addActor(this._icon, { align: St.Align.END });
839
this.setIcon(iconName);
842
setIcon: function(name) {
843
this._icon.icon_name = name;
847
const PopupMenuBase = new Lang.Class({
848
Name: 'PopupMenuBase',
851
_init: function(sourceActor, styleClass) {
852
this.sourceActor = sourceActor;
854
if (styleClass !== undefined) {
855
this.box = new St.BoxLayout({ style_class: styleClass,
858
this.box = new St.BoxLayout({ vertical: true });
860
this.box.connect_after('queue-relayout', Lang.bind(this, this._menuQueueRelayout));
865
// If set, we don't send events (including crossing events) to the source actor
866
// for the menu which causes its prelight state to freeze
867
this.blockSourceEvents = false;
869
this._activeMenuItem = null;
870
this._settingsActions = { };
872
this._sessionUpdatedId = Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
875
_sessionUpdated: function() {
876
this._setSettingsVisibility(Main.sessionMode.allowSettings);
879
addAction: function(title, callback) {
880
let menuItem = new PopupMenuItem(title);
881
this.addMenuItem(menuItem);
882
menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
889
addSettingsAction: function(title, desktopFile) {
890
let menuItem = this.addAction(title, function() {
891
let app = Shell.AppSystem.get_default().lookup_app(desktopFile);
894
log('Settings panel for desktop file ' + desktopFile + ' could not be loaded!');
898
Main.overview.hide();
902
menuItem.actor.visible = Main.sessionMode.allowSettings;
903
this._settingsActions[desktopFile] = menuItem;
908
_setSettingsVisibility: function(visible) {
909
for (let id in this._settingsActions) {
910
let item = this._settingsActions[id];
911
item.actor.visible = visible;
915
isEmpty: function() {
916
let hasVisibleChildren = this.box.get_children().some(function(child) {
917
return child.visible;
920
return !hasVisibleChildren;
923
isChildMenu: function() {
928
* _connectSubMenuSignals:
929
* @object: a menu item, or a menu section
930
* @menu: a sub menu, or a menu section
932
* Connects to signals on @menu that are necessary for
933
* operating the submenu, and stores the ids on @object.
935
_connectSubMenuSignals: function(object, menu) {
936
object._subMenuActivateId = menu.connect('activate', Lang.bind(this, function() {
937
this.emit('activate');
938
this.close(BoxPointer.PopupAnimation.FULL);
940
object._subMenuActiveChangeId = menu.connect('active-changed', Lang.bind(this, function(submenu, submenuItem) {
941
if (this._activeMenuItem && this._activeMenuItem != submenuItem)
942
this._activeMenuItem.setActive(false);
943
this._activeMenuItem = submenuItem;
944
this.emit('active-changed', submenuItem);
948
_connectItemSignals: function(menuItem) {
949
menuItem._activeChangeId = menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) {
950
if (active && this._activeMenuItem != menuItem) {
951
if (this._activeMenuItem)
952
this._activeMenuItem.setActive(false);
953
this._activeMenuItem = menuItem;
954
this.emit('active-changed', menuItem);
955
} else if (!active && this._activeMenuItem == menuItem) {
956
this._activeMenuItem = null;
957
this.emit('active-changed', null);
960
menuItem._sensitiveChangeId = menuItem.connect('sensitive-changed', Lang.bind(this, function(menuItem, sensitive) {
961
if (!sensitive && this._activeMenuItem == menuItem) {
962
if (!this.actor.navigate_focus(menuItem.actor,
963
Gtk.DirectionType.TAB_FORWARD,
965
this.actor.grab_key_focus();
966
} else if (sensitive && this._activeMenuItem == null) {
967
if (global.stage.get_key_focus() == this.actor)
968
menuItem.actor.grab_key_focus();
971
menuItem._activateId = menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
972
this.emit('activate', menuItem);
973
this.close(BoxPointer.PopupAnimation.FULL);
975
// the weird name is to avoid a conflict with some random property
976
// the menuItem may have, called destroyId
977
// (FIXME: in the future it may make sense to have container objects
978
// like PopupMenuManager does)
979
menuItem._popupMenuDestroyId = menuItem.connect('destroy', Lang.bind(this, function(menuItem) {
980
menuItem.disconnect(menuItem._popupMenuDestroyId);
981
menuItem.disconnect(menuItem._activateId);
982
menuItem.disconnect(menuItem._activeChangeId);
983
menuItem.disconnect(menuItem._sensitiveChangeId);
985
menuItem.menu.disconnect(menuItem._subMenuActivateId);
986
menuItem.menu.disconnect(menuItem._subMenuActiveChangeId);
987
this.disconnect(menuItem._closingId);
989
if (menuItem == this._activeMenuItem)
990
this._activeMenuItem = null;
994
_updateSeparatorVisibility: function(menuItem) {
995
let children = this.box.get_children();
997
let index = children.indexOf(menuItem.actor);
1002
let childBeforeIndex = index - 1;
1004
while (childBeforeIndex >= 0 && !children[childBeforeIndex].visible)
1007
if (childBeforeIndex < 0
1008
|| children[childBeforeIndex]._delegate instanceof PopupSeparatorMenuItem) {
1009
menuItem.actor.hide();
1013
let childAfterIndex = index + 1;
1015
while (childAfterIndex < children.length && !children[childAfterIndex].visible)
1018
if (childAfterIndex >= children.length
1019
|| children[childAfterIndex]._delegate instanceof PopupSeparatorMenuItem) {
1020
menuItem.actor.hide();
1024
menuItem.actor.show();
1027
addMenuItem: function(menuItem, position) {
1028
let before_item = null;
1029
if (position == undefined) {
1030
this.box.add(menuItem.actor);
1032
let items = this._getMenuItems();
1033
if (position < items.length) {
1034
before_item = items[position].actor;
1035
this.box.insert_child_below(menuItem.actor, before_item);
1037
this.box.add(menuItem.actor);
1041
if (menuItem instanceof PopupMenuSection) {
1042
this._connectSubMenuSignals(menuItem, menuItem);
1043
menuItem._parentOpenStateChangedId = this.connect('open-state-changed',
1044
function(self, open) {
1050
menuItem.connect('destroy', Lang.bind(this, function() {
1051
menuItem.disconnect(menuItem._subMenuActivateId);
1052
menuItem.disconnect(menuItem._subMenuActiveChangeId);
1053
this.disconnect(menuItem._parentOpenStateChangedId);
1057
} else if (menuItem instanceof PopupSubMenuMenuItem) {
1058
if (before_item == null)
1059
this.box.add(menuItem.menu.actor);
1061
this.box.insert_child_below(menuItem.menu.actor, before_item);
1062
this._connectSubMenuSignals(menuItem, menuItem.menu);
1063
this._connectItemSignals(menuItem);
1064
menuItem._closingId = this.connect('open-state-changed', function(self, open) {
1066
menuItem.menu.close(BoxPointer.PopupAnimation.FADE);
1068
} else if (menuItem instanceof PopupSeparatorMenuItem) {
1069
this._connectItemSignals(menuItem);
1071
// updateSeparatorVisibility needs to get called any time the
1072
// separator's adjacent siblings change visibility or position.
1073
// open-state-changed isn't exactly that, but doing it in more
1074
// precise ways would require a lot more bookkeeping.
1075
this.connect('open-state-changed', Lang.bind(this, function() { this._updateSeparatorVisibility(menuItem); }));
1076
} else if (menuItem instanceof PopupBaseMenuItem)
1077
this._connectItemSignals(menuItem);
1079
throw TypeError("Invalid argument to PopupMenuBase.addMenuItem()");
1084
getColumnWidths: function() {
1085
let columnWidths = [];
1086
let items = this.box.get_children();
1087
for (let i = 0; i < items.length; i++) {
1088
if (!items[i].visible &&
1089
!(items[i]._delegate instanceof PopupSubMenu && items[i-1].visible))
1091
if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase) {
1092
let itemColumnWidths = items[i]._delegate.getColumnWidths();
1093
for (let j = 0; j < itemColumnWidths.length; j++) {
1094
if (j >= columnWidths.length || itemColumnWidths[j] > columnWidths[j])
1095
columnWidths[j] = itemColumnWidths[j];
1099
return columnWidths;
1102
setColumnWidths: function(widths) {
1103
let items = this.box.get_children();
1104
for (let i = 0; i < items.length; i++) {
1105
if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase)
1106
items[i]._delegate.setColumnWidths(widths);
1110
// Because of the above column-width funniness, we need to do a
1111
// queue-relayout on every item whenever the menu itself changes
1112
// size, to force clutter to drop its cached size requests. (The
1113
// menuitems will in turn call queue_relayout on their parent, the
1114
// menu, but that call will be a no-op since the menu already
1115
// has a relayout queued, so we won't get stuck in a loop.
1116
_menuQueueRelayout: function() {
1117
this.box.get_children().map(function (actor) { actor.queue_relayout(); });
1120
addActor: function(actor) {
1121
this.box.add(actor);
1124
_getMenuItems: function() {
1125
return this.box.get_children().map(function (actor) {
1126
return actor._delegate;
1127
}).filter(function(item) {
1128
return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection;
1132
get firstMenuItem() {
1133
let items = this._getMenuItems();
1140
get numMenuItems() {
1141
return this._getMenuItems().length;
1144
removeAll: function() {
1145
let children = this._getMenuItems();
1146
for (let i = 0; i < children.length; i++) {
1147
let item = children[i];
1152
toggle: function() {
1154
this.close(BoxPointer.PopupAnimation.FULL);
1156
this.open(BoxPointer.PopupAnimation.FULL);
1159
destroy: function() {
1162
this.actor.destroy();
1164
this.emit('destroy');
1166
Main.sessionMode.disconnect(this._sessionUpdatedId);
1167
this._sessionUpdatedId = 0;
1170
Signals.addSignalMethods(PopupMenuBase.prototype);
1172
const PopupMenu = new Lang.Class({
1174
Extends: PopupMenuBase,
1176
_init: function(sourceActor, arrowAlignment, arrowSide) {
1177
this.parent(sourceActor, 'popup-menu-content');
1179
this._arrowAlignment = arrowAlignment;
1180
this._arrowSide = arrowSide;
1182
this._boxPointer = new BoxPointer.BoxPointer(arrowSide,
1185
x_align: St.Align.START });
1186
this.actor = this._boxPointer.actor;
1187
this.actor._delegate = this;
1188
this.actor.style_class = 'popup-menu-boxpointer';
1190
this._boxWrapper = new Shell.GenericContainer();
1191
this._boxWrapper.connect('get-preferred-width', Lang.bind(this, this._boxGetPreferredWidth));
1192
this._boxWrapper.connect('get-preferred-height', Lang.bind(this, this._boxGetPreferredHeight));
1193
this._boxWrapper.connect('allocate', Lang.bind(this, this._boxAllocate));
1194
this._boxPointer.bin.set_child(this._boxWrapper);
1195
this._boxWrapper.add_actor(this.box);
1196
this.actor.add_style_class_name('popup-menu');
1198
global.focus_manager.add_group(this.actor);
1199
this.actor.reactive = true;
1201
this._childMenus = [];
1204
_boxGetPreferredWidth: function (actor, forHeight, alloc) {
1205
let columnWidths = this.getColumnWidths();
1206
this.setColumnWidths(columnWidths);
1208
// Now they will request the right sizes
1209
[alloc.min_size, alloc.natural_size] = this.box.get_preferred_width(forHeight);
1212
_boxGetPreferredHeight: function (actor, forWidth, alloc) {
1213
[alloc.min_size, alloc.natural_size] = this.box.get_preferred_height(forWidth);
1216
_boxAllocate: function (actor, box, flags) {
1217
this.box.allocate(box, flags);
1220
setArrowOrigin: function(origin) {
1221
this._boxPointer.setArrowOrigin(origin);
1224
setSourceAlignment: function(alignment) {
1225
this._boxPointer.setSourceAlignment(alignment);
1228
isChildMenu: function(menu) {
1229
return this._childMenus.indexOf(menu) != -1;
1232
addChildMenu: function(menu) {
1233
if (this.isChildMenu(menu))
1236
this._childMenus.push(menu);
1237
this.emit('child-menu-added', menu);
1240
removeChildMenu: function(menu) {
1241
let index = this._childMenus.indexOf(menu);
1246
this._childMenus.splice(index, 1);
1247
this.emit('child-menu-removed', menu);
1250
open: function(animate) {
1259
this._boxPointer.setPosition(this.sourceActor, this._arrowAlignment);
1260
this._boxPointer.show(animate);
1262
this.actor.raise_top();
1264
this.emit('open-state-changed', true);
1267
close: function(animate) {
1268
if (this._activeMenuItem)
1269
this._activeMenuItem.setActive(false);
1271
this._childMenus.forEach(function(childMenu) {
1275
if (this._boxPointer.actor.visible)
1276
this._boxPointer.hide(animate);
1281
this.isOpen = false;
1282
this.emit('open-state-changed', false);
1286
const PopupDummyMenu = new Lang.Class({
1287
Name: 'PopupDummyMenu',
1289
_init: function(sourceActor) {
1290
this.sourceActor = sourceActor;
1291
this.actor = sourceActor;
1292
this.actor._delegate = this;
1295
isChildMenu: function() {
1299
open: function() { this.emit('open-state-changed', true); },
1300
close: function() { this.emit('open-state-changed', false); },
1301
toggle: function() {},
1302
destroy: function() {
1303
this.emit('destroy');
1306
Signals.addSignalMethods(PopupDummyMenu.prototype);
1308
const PopupSubMenu = new Lang.Class({
1309
Name: 'PopupSubMenu',
1310
Extends: PopupMenuBase,
1312
_init: function(sourceActor, sourceArrow) {
1313
this.parent(sourceActor);
1315
this._arrow = sourceArrow;
1316
this._arrow.rotation_center_z_gravity = Clutter.Gravity.CENTER;
1318
// Since a function of a submenu might be to provide a "More.." expander
1319
// with long content, we make it scrollable - the scrollbar will only take
1320
// effect if a CSS max-height is set on the top menu.
1321
this.actor = new St.ScrollView({ style_class: 'popup-sub-menu',
1322
hscrollbar_policy: Gtk.PolicyType.NEVER,
1323
vscrollbar_policy: Gtk.PolicyType.NEVER });
1325
this.actor.add_actor(this.box);
1326
this.actor._delegate = this;
1327
this.actor.clip_to_allocation = true;
1328
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
1332
_getTopMenu: function() {
1333
let actor = this.actor.get_parent();
1335
if (actor._delegate && actor._delegate instanceof PopupMenu)
1336
return actor._delegate;
1338
actor = actor.get_parent();
1344
_needsScrollbar: function() {
1345
let topMenu = this._getTopMenu();
1346
let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1);
1347
let topThemeNode = topMenu.actor.get_theme_node();
1349
let topMaxHeight = topThemeNode.get_max_height();
1350
return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight;
1353
open: function(animate) {
1364
let needsScrollbar = this._needsScrollbar();
1366
// St.ScrollView always requests space horizontally for a possible vertical
1367
// scrollbar if in AUTOMATIC mode. Doing better would require implementation
1368
// of width-for-height in St.BoxLayout and St.ScrollView. This looks bad
1369
// when we *don't* need it, so turn off the scrollbar when that's true.
1370
// Dynamic changes in whether we need it aren't handled properly.
1371
this.actor.vscrollbar_policy =
1372
needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
1375
this.actor.add_style_pseudo_class('scrolled');
1377
this.actor.remove_style_pseudo_class('scrolled');
1379
// It looks funny if we animate with a scrollbar (at what point is
1380
// the scrollbar added?) so just skip that case
1381
if (animate && needsScrollbar)
1385
let [minHeight, naturalHeight] = this.actor.get_preferred_height(-1);
1386
this.actor.height = 0;
1387
this.actor._arrow_rotation = this._arrow.rotation_angle_z;
1388
Tweener.addTween(this.actor,
1389
{ _arrow_rotation: 90,
1390
height: naturalHeight,
1392
onUpdateScope: this,
1393
onUpdate: function() {
1394
this._arrow.rotation_angle_z = this.actor._arrow_rotation;
1396
onCompleteScope: this,
1397
onComplete: function() {
1398
this.actor.set_height(-1);
1399
this.emit('open-state-changed', true);
1403
this._arrow.rotation_angle_z = 90;
1404
this.emit('open-state-changed', true);
1408
close: function(animate) {
1412
this.isOpen = false;
1414
if (this._activeMenuItem)
1415
this._activeMenuItem.setActive(false);
1417
if (animate && this._needsScrollbar())
1421
this.actor._arrow_rotation = this._arrow.rotation_angle_z;
1422
Tweener.addTween(this.actor,
1423
{ _arrow_rotation: 0,
1426
onCompleteScope: this,
1427
onComplete: function() {
1429
this.actor.set_height(-1);
1431
this.emit('open-state-changed', false);
1433
onUpdateScope: this,
1434
onUpdate: function() {
1435
this._arrow.rotation_angle_z = this.actor._arrow_rotation;
1439
this._arrow.rotation_angle_z = 0;
1442
this.isOpen = false;
1443
this.emit('open-state-changed', false);
1447
_onKeyPressEvent: function(actor, event) {
1448
// Move focus back to parent menu if the user types Left.
1450
if (this.isOpen && event.get_key_symbol() == Clutter.KEY_Left) {
1451
this.close(BoxPointer.PopupAnimation.FULL);
1452
this.sourceActor._delegate.setActive(true);
1463
* A section of a PopupMenu which is handled like a submenu
1464
* (you can add and remove items, you can destroy it, you
1465
* can add it to another menu), but is completely transparent
1468
const PopupMenuSection = new Lang.Class({
1469
Name: 'PopupMenuSection',
1470
Extends: PopupMenuBase,
1475
this.actor = this.box;
1476
this.actor._delegate = this;
1479
// an array of externally managed separators
1480
this.separators = [];
1483
// deliberately ignore any attempt to open() or close(), but emit the
1484
// corresponding signal so children can still pick it up
1485
open: function() { this.emit('open-state-changed', true); },
1486
close: function() { this.emit('open-state-changed', false); },
1488
destroy: function() {
1489
for (let i = 0; i < this.separators.length; i++)
1490
this.separators[i].destroy();
1491
this.separators = [];
1497
const PopupSubMenuMenuItem = new Lang.Class({
1498
Name: 'PopupSubMenuMenuItem',
1499
Extends: PopupBaseMenuItem,
1501
_init: function(text) {
1504
this.actor.add_style_class_name('popup-submenu-menu-item');
1506
this.label = new St.Label({ text: text });
1507
this.addActor(this.label);
1508
this.actor.label_actor = this.label;
1509
this._triangle = new St.Label({ text: '\u25B8' });
1510
this.addActor(this._triangle, { align: St.Align.END });
1512
this.menu = new PopupSubMenu(this.actor, this._triangle);
1513
this.menu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
1516
_subMenuOpenStateChanged: function(menu, open) {
1518
this.actor.add_style_pseudo_class('open');
1520
this.actor.remove_style_pseudo_class('open');
1523
destroy: function() {
1524
this.menu.destroy();
1529
_onKeyPressEvent: function(actor, event) {
1530
let symbol = event.get_key_symbol();
1532
if (symbol == Clutter.KEY_Right) {
1533
this.menu.open(BoxPointer.PopupAnimation.FULL);
1534
this.menu.actor.navigate_focus(null, Gtk.DirectionType.DOWN, false);
1536
} else if (symbol == Clutter.KEY_Left && this.menu.isOpen) {
1541
return this.parent(actor, event);
1544
activate: function(event) {
1545
this.menu.open(BoxPointer.PopupAnimation.FULL);
1548
_onButtonReleaseEvent: function(actor) {
1553
const PopupComboMenu = new Lang.Class({
1554
Name: 'PopupComboMenu',
1555
Extends: PopupMenuBase,
1557
_init: function(sourceActor) {
1558
this.parent(sourceActor, 'popup-combo-menu');
1560
this.actor = this.box;
1561
this.actor._delegate = this;
1562
this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
1563
sourceActor.connect('style-changed',
1564
Lang.bind(this, this._onSourceActorStyleChanged));
1565
this._activeItemPos = -1;
1566
global.focus_manager.add_group(this.actor);
1569
_onKeyFocusIn: function(actor) {
1570
let items = this._getMenuItems();
1571
let activeItem = items[this._activeItemPos];
1572
activeItem.actor.grab_key_focus();
1575
_onSourceActorStyleChanged: function() {
1576
// PopupComboBoxMenuItem clones the active item's actors
1577
// to work with arbitrary items in the menu; this means
1578
// that we need to propagate some style information and
1579
// enforce style updates even when the menu is closed
1580
let activeItem = this._getMenuItems()[this._activeItemPos];
1581
if (this.sourceActor.has_style_pseudo_class('insensitive'))
1582
activeItem.actor.add_style_pseudo_class('insensitive');
1584
activeItem.actor.remove_style_pseudo_class('insensitive');
1586
// To propagate the :active style, we need to make sure that the
1587
// internal state of the PopupComboMenu is updated as well, but
1588
// we must not move the keyboard grab
1589
activeItem.setActive(this.sourceActor.has_style_pseudo_class('active'),
1590
{ grabKeyboard: false });
1592
_ensureStyle(this.actor);
1604
let [sourceX, sourceY] = this.sourceActor.get_transformed_position();
1605
let items = this._getMenuItems();
1606
let activeItem = items[this._activeItemPos];
1608
this.actor.set_position(sourceX, sourceY - activeItem.actor.y);
1609
this.actor.width = Math.max(this.actor.width, this.sourceActor.width);
1610
this.actor.raise_top();
1612
this.actor.opacity = 0;
1615
Tweener.addTween(this.actor,
1617
transition: 'linear',
1618
time: BoxPointer.POPUP_ANIMATION_TIME });
1620
this.emit('open-state-changed', true);
1627
this.isOpen = false;
1628
Tweener.addTween(this.actor,
1630
transition: 'linear',
1631
time: BoxPointer.POPUP_ANIMATION_TIME,
1632
onComplete: Lang.bind(this,
1638
this.emit('open-state-changed', false);
1641
setActiveItem: function(position) {
1642
this._activeItemPos = position;
1645
getActiveItem: function() {
1646
return this._getMenuItems()[this._activeItemPos];
1649
setItemVisible: function(position, visible) {
1650
if (!visible && position == this._activeItemPos) {
1651
log('Trying to hide the active menu item.');
1655
this._getMenuItems()[position].actor.visible = visible;
1658
getItemVisible: function(position) {
1659
return this._getMenuItems()[position].actor.visible;
1663
const PopupComboBoxMenuItem = new Lang.Class({
1664
Name: 'PopupComboBoxMenuItem',
1665
Extends: PopupBaseMenuItem,
1667
_init: function (params) {
1668
this.parent(params);
1670
this.actor.accessible_role = Atk.Role.COMBO_BOX;
1672
this._itemBox = new Shell.Stack();
1673
this.addActor(this._itemBox);
1675
let expander = new St.Label({ text: '\u2304' });
1676
this.addActor(expander, { align: St.Align.END,
1679
this._menu = new PopupComboMenu(this.actor);
1680
Main.uiGroup.add_actor(this._menu.actor);
1681
this._menu.actor.hide();
1683
if (params.style_class)
1684
this._menu.actor.add_style_class_name(params.style_class);
1686
this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
1688
this._activeItemPos = -1;
1692
_getTopMenu: function() {
1693
let actor = this.actor.get_parent();
1695
if (actor._delegate && actor._delegate instanceof PopupMenu)
1696
return actor._delegate;
1698
actor = actor.get_parent();
1704
_onScrollEvent: function(actor, event) {
1705
if (this._activeItemPos == -1)
1708
let position = this._activeItemPos;
1709
let direction = event.get_scroll_direction();
1710
if (direction == Clutter.ScrollDirection.DOWN) {
1711
while (position < this._items.length - 1) {
1713
if (this._menu.getItemVisible(position))
1716
} else if (direction == Clutter.ScrollDirection.UP) {
1717
while (position > 0) {
1719
if (this._menu.getItemVisible(position))
1724
if (position == this._activeItemPos)
1727
this.setActiveItem(position);
1728
this.emit('active-item-changed', position);
1731
activate: function(event) {
1732
let topMenu = this._getTopMenu();
1736
topMenu.addChildMenu(this._menu);
1737
this._menu.toggle();
1740
addMenuItem: function(menuItem, position) {
1741
if (position === undefined)
1742
position = this._menu.numMenuItems;
1744
this._menu.addMenuItem(menuItem, position);
1745
_ensureStyle(this._menu.actor);
1747
let item = new St.BoxLayout({ style_class: 'popup-combobox-item' });
1749
let children = menuItem.actor.get_children();
1750
for (let i = 0; i < children.length; i++) {
1751
let clone = new Clutter.Clone({ source: children[i] });
1752
item.add(clone, { y_fill: false });
1755
let oldItem = this._items[position];
1757
this._itemBox.remove_actor(oldItem);
1759
this._items[position] = item;
1760
this._itemBox.add_actor(item);
1762
menuItem.connect('activate',
1763
Lang.bind(this, this._itemActivated, position));
1766
checkAccessibleLabel: function() {
1767
let activeItem = this._menu.getActiveItem();
1768
this.actor.label_actor = activeItem.label;
1771
setActiveItem: function(position) {
1772
let item = this._items[position];
1775
if (this._activeItemPos == position)
1777
this._menu.setActiveItem(position);
1778
this._activeItemPos = position;
1779
for (let i = 0; i < this._items.length; i++)
1780
this._items[i].visible = (i == this._activeItemPos);
1782
this.checkAccessibleLabel();
1785
setItemVisible: function(position, visible) {
1786
this._menu.setItemVisible(position, visible);
1789
_itemActivated: function(menuItem, event, position) {
1790
this.setActiveItem(position);
1791
this.emit('active-item-changed', position);
1798
* A PopupMenu that tracks a GMenuModel and shows its actions
1799
* (exposed by GApplication/GActionGroup)
1801
const RemoteMenu = new Lang.Class({
1805
_init: function(sourceActor, model, actionGroup) {
1806
this.parent(sourceActor, 0.0, St.Side.TOP);
1809
this.actionGroup = actionGroup;
1811
this._actions = { };
1812
this._modelChanged(this.model, 0, 0, this.model.get_n_items(), this);
1814
this._actionStateChangeId = this.actionGroup.connect('action-state-changed', Lang.bind(this, this._actionStateChanged));
1815
this._actionEnableChangeId = this.actionGroup.connect('action-enabled-changed', Lang.bind(this, this._actionEnabledChanged));
1818
destroy: function() {
1819
if (this._actionStateChangeId) {
1820
this.actionGroup.disconnect(this._actionStateChangeId);
1821
this._actionStateChangeId = 0;
1824
if (this._actionEnableChangeId) {
1825
this.actionGroup.disconnect(this._actionEnableChangeId);
1826
this._actionEnableChangeId = 0;
1832
_createMenuItem: function(model, index) {
1833
let labelValue = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null);
1834
let label = labelValue ? labelValue.deep_unpack() : '';
1835
// remove all underscores that are not followed by another underscore
1836
label = label.replace(/_([^_])/, '$1');
1838
let section_link = model.get_item_link(index, Gio.MENU_LINK_SECTION);
1840
let item = new PopupMenuSection();
1842
let title = new PopupMenuItem(label, { reactive: false,
1843
style_class: 'popup-subtitle-menu-item' });
1844
item._titleMenuItem = title;
1845
title._ignored = true;
1846
item.addMenuItem(title);
1848
this._modelChanged(section_link, 0, 0, section_link.get_n_items(), item);
1849
return [item, true, ''];
1852
let submenu_link = model.get_item_link(index, Gio.MENU_LINK_SUBMENU);
1855
let item = new PopupSubMenuMenuItem(label);
1856
this._modelChanged(submenu_link, 0, 0, submenu_link.get_n_items(), item.menu);
1857
return [item, false, ''];
1860
let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack();
1861
if (!this.actionGroup.has_action(action_id)) {
1862
// the action may not be there yet, wait for action-added
1863
return [null, false, 'action-added'];
1866
if (!this._actions[action_id])
1867
this._actions[action_id] = { enabled: this.actionGroup.get_action_enabled(action_id),
1868
state: this.actionGroup.get_action_state(action_id),
1871
let action = this._actions[action_id];
1872
let item, target, destroyId, specificSignalId;
1875
// Docs have get_state_hint(), except that the DBus protocol
1876
// has no provision for it (so ShellApp does not implement it,
1877
// and neither GApplication), and g_action_get_state_hint()
1878
// always returns null
1881
switch (String.fromCharCode(action.state.classify())) {
1883
item = new PopupSwitchMenuItem(label, action.state.get_boolean());
1884
action.items.push(item);
1885
specificSignalId = item.connect('toggled', Lang.bind(this, function(item) {
1886
this.actionGroup.activate_action(action_id, null);
1890
item = new PopupMenuItem(label);
1891
item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null).deep_unpack();
1892
action.items.push(item);
1893
item.setShowDot(action.state.deep_unpack() == item._remoteTarget);
1894
specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
1895
this.actionGroup.activate_action(action_id, GLib.Variant.new_string(item._remoteTarget));
1899
log('Action "%s" has state of type %s, which is not supported'.format(action_id, action.state.get_type_string()));
1900
return [null, false, 'action-state-changed'];
1903
target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null);
1904
item = new PopupMenuItem(label);
1905
action.items.push(item);
1906
specificSignalId = item.connect('activate', Lang.bind(this, function() {
1907
this.actionGroup.activate_action(action_id, target);
1911
item.actor.reactive = item.actor.can_focus = action.enabled;
1913
destroyId = item.connect('destroy', Lang.bind(this, function() {
1914
item.disconnect(destroyId);
1915
item.disconnect(specificSignalId);
1917
let pos = action.items.indexOf(item);
1919
action.items.splice(pos, 1);
1922
return [item, false, ''];
1925
_modelChanged: function(model, position, removed, added, target) {
1929
let currentItems = target._getMenuItems();
1932
// skip ignored items at the beginning
1933
while (k0 < currentItems.length && currentItems[k0]._ignored)
1935
// find the right menu item matching the model item
1936
for (j0 = 0; k0 < currentItems.length && j0 < position; j0++, k0++) {
1937
if (currentItems[k0]._ignored)
1941
if (removed == -1) {
1942
// special flag to indicate we should destroy everything
1943
for (k = k0; k < currentItems.length; k++)
1944
currentItems[k].destroy();
1946
for (j = j0, k = k0; k < currentItems.length && j < j0 + removed; j++, k++) {
1947
currentItems[k].destroy();
1949
if (currentItems[k]._ignored)
1954
for (j = j0, k = k0; j < j0 + added; j++, k++) {
1955
let [item, addSeparator, changeSignal] = this._createMenuItem(model, j);
1958
// separators must be added in the parent to make autohiding work
1960
let separator = new PopupSeparatorMenuItem();
1961
item.separators.push(separator);
1962
separator._ignored = true;
1963
target.addMenuItem(separator, k+1);
1967
target.addMenuItem(item, k);
1970
let separator = new PopupSeparatorMenuItem();
1971
item.separators.push(separator);
1972
separator._ignored = true;
1973
target.addMenuItem(separator, k+1);
1976
} else if (changeSignal) {
1977
let signalId = this.actionGroup.connect(changeSignal, Lang.bind(this, function(actionGroup, actionName) {
1978
actionGroup.disconnect(signalId);
1979
if (this._actions[actionName]) return;
1981
// force a full update
1982
this._modelChanged(model, 0, -1, model.get_n_items(), target);
1987
if (!model._changedId) {
1988
model._changedId = model.connect('items-changed', Lang.bind(this, this._modelChanged, target));
1989
model._destroyId = target.connect('destroy', function() {
1990
if (model._changedId)
1991
model.disconnect(model._changedId);
1992
if (model._destroyId)
1993
target.disconnect(model._destroyId);
1994
model._changedId = 0;
1995
model._destroyId = 0;
1999
if (target instanceof PopupMenuSection) {
2000
if (target._titleMenuItem)
2001
target.actor.visible = target.numMenuItems != 1;
2003
target.actor.visible = target.numMenuItems != 0;
2005
let sourceItem = target.sourceActor._delegate;
2006
if (sourceItem instanceof PopupSubMenuMenuItem)
2007
sourceItem.actor.visible = target.numMenuItems != 0;
2011
_actionStateChanged: function(actionGroup, action_id) {
2012
let action = this._actions[action_id];
2016
action.state = actionGroup.get_action_state(action_id);
2017
if (action.items.length) {
2018
switch (String.fromCharCode(action.state.classify())) {
2020
for (let i = 0; i < action.items.length; i++)
2021
action.items[i].setToggleState(action.state.get_boolean());
2024
for (let i = 0; i < action.items.length; i++)
2025
action.items[i].setValue(action.state.get_double());
2028
for (let i = 0; i < action.items.length; i++)
2029
action.items[i].setShowDot(action.items[i]._remoteTarget == action.state.deep_unpack());
2034
_actionEnabledChanged: function(actionGroup, action_id) {
2035
let action = this._actions[action_id];
2039
action.enabled = actionGroup.get_action_enabled(action_id);
2040
if (action.items.length) {
2041
for (let i = 0; i < action.items.length; i++) {
2042
let item = action.items[i];
2043
item.actor.reactive = item.actor.can_focus = action.enabled;
2049
/* Basic implementation of a menu manager.
2050
* Call addMenu to add menus
2052
const PopupMenuManager = new Lang.Class({
2053
Name: 'PopupMenuManager',
2055
_init: function(owner, grabParams) {
2056
this._owner = owner;
2057
this._grabHelper = new GrabHelper.GrabHelper(owner.actor, grabParams);
2061
addMenu: function(menu, position) {
2062
if (this._findMenu(menu) > -1)
2067
openStateChangeId: menu.connect('open-state-changed', Lang.bind(this, this._onMenuOpenState)),
2068
childMenuAddedId: menu.connect('child-menu-added', Lang.bind(this, this._onChildMenuAdded)),
2069
childMenuRemovedId: menu.connect('child-menu-removed', Lang.bind(this, this._onChildMenuRemoved)),
2070
destroyId: menu.connect('destroy', Lang.bind(this, this._onMenuDestroy)),
2075
let source = menu.sourceActor;
2077
if (!menu.blockSourceEvents)
2078
this._grabHelper.addActor(source);
2079
menudata.enterId = source.connect('enter-event', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
2080
menudata.focusInId = source.connect('key-focus-in', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
2083
if (position == undefined)
2084
this._menus.push(menudata);
2086
this._menus.splice(position, 0, menudata);
2089
removeMenu: function(menu) {
2090
if (menu == this.activeMenu)
2091
this._closeMenu(menu);
2093
let position = this._findMenu(menu);
2094
if (position == -1) // not a menu we manage
2097
let menudata = this._menus[position];
2098
menu.disconnect(menudata.openStateChangeId);
2099
menu.disconnect(menudata.childMenuAddedId);
2100
menu.disconnect(menudata.childMenuRemovedId);
2101
menu.disconnect(menudata.destroyId);
2103
if (menudata.enterId)
2104
menu.sourceActor.disconnect(menudata.enterId);
2105
if (menudata.focusInId)
2106
menu.sourceActor.disconnect(menudata.focusInId);
2108
if (menu.sourceActor)
2109
this._grabHelper.removeActor(menu.sourceActor);
2110
this._menus.splice(position, 1);
2114
let firstGrab = this._grabHelper.grabStack[0];
2116
return firstGrab.actor._delegate;
2121
ignoreRelease: function() {
2122
return this._grabHelper.ignoreRelease();
2125
_onMenuOpenState: function(menu, open) {
2127
if (this.activeMenu)
2128
this.activeMenu.close(BoxPointer.PopupAnimation.FADE);
2129
this._grabHelper.grab({ actor: menu.actor, modal: true, focus: menu.sourceActor,
2130
onUngrab: Lang.bind(this, this._closeMenu, menu) });
2132
this._grabHelper.ungrab({ actor: menu.actor });
2136
_onChildMenuAdded: function(menu, childMenu) {
2137
this.addMenu(childMenu);
2140
_onChildMenuRemoved: function(menu, childMenu) {
2141
this.removeMenu(childMenu);
2144
_changeMenu: function(newMenu) {
2145
newMenu.open(this.activeMenu ? BoxPointer.PopupAnimation.FADE
2146
: BoxPointer.PopupAnimation.FULL);
2149
_onMenuSourceEnter: function(menu) {
2150
if (!this._grabHelper.grabbed)
2153
if (this._grabHelper.isActorGrabbed(menu.actor))
2156
let isChildMenu = this._grabHelper.grabStack.some(function(grab) {
2157
let existingMenu = grab.actor._delegate;
2158
return existingMenu.isChildMenu(menu);
2163
this._changeMenu(menu);
2167
_onMenuDestroy: function(menu) {
2168
this.removeMenu(menu);
2171
_findMenu: function(item) {
2172
for (let i = 0; i < this._menus.length; i++) {
2173
let menudata = this._menus[i];
2174
if (item == menudata.menu)
2180
_closeMenu: function(isUser, menu) {
2181
// If this isn't a user action, we called close()
2182
// on the BoxPointer ourselves, so we shouldn't
2185
menu.close(BoxPointer.PopupAnimation.FULL);