PageRenderTime 37ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/gnome-shell-3.5.4/js/ui/popupMenu.js

#
JavaScript | 2318 lines | 1811 code | 393 blank | 114 comment | 414 complexity | e24d610d6c25937aca95242bd84592cf MD5 | raw file
Possible License(s): GPL-2.0
  1. // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2. const Cairo = imports.cairo;
  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;
  12. const BoxPointer = imports.ui.boxpointer;
  13. const Main = imports.ui.main;
  14. const Params = imports.misc.params;
  15. const Tweener = imports.ui.tweener;
  16. const SLIDER_SCROLL_STEP = 0.05; /* Slider scrolling step in % */
  17. function _ensureStyle(actor) {
  18. if (actor.get_children) {
  19. let children = actor.get_children();
  20. for (let i = 0; i < children.length; i++)
  21. _ensureStyle(children[i]);
  22. }
  23. if (actor instanceof St.Widget)
  24. actor.ensure_style();
  25. }
  26. const PopupBaseMenuItem = new Lang.Class({
  27. Name: 'PopupBaseMenuItem',
  28. _init: function (params) {
  29. params = Params.parse (params, { reactive: true,
  30. activate: true,
  31. hover: true,
  32. sensitive: true,
  33. style_class: null
  34. });
  35. this.actor = new Shell.GenericContainer({ style_class: 'popup-menu-item',
  36. reactive: params.reactive,
  37. track_hover: params.reactive,
  38. can_focus: params.reactive,
  39. accessible_role: Atk.Role.MENU_ITEM});
  40. this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
  41. this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
  42. this.actor.connect('allocate', Lang.bind(this, this._allocate));
  43. this.actor.connect('style-changed', Lang.bind(this, this._onStyleChanged));
  44. this.actor._delegate = this;
  45. this._children = [];
  46. this._dot = null;
  47. this._columnWidths = null;
  48. this._spacing = 0;
  49. this.active = false;
  50. this._activatable = params.reactive && params.activate;
  51. this.sensitive = this._activatable && params.sensitive;
  52. this.setSensitive(this.sensitive);
  53. if (params.style_class)
  54. this.actor.add_style_class_name(params.style_class);
  55. if (this._activatable) {
  56. this.actor.connect('button-release-event', Lang.bind(this, this._onButtonReleaseEvent));
  57. this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
  58. }
  59. if (params.reactive && params.hover)
  60. this.actor.connect('notify::hover', Lang.bind(this, this._onHoverChanged));
  61. if (params.reactive) {
  62. this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
  63. this.actor.connect('key-focus-out', Lang.bind(this, this._onKeyFocusOut));
  64. }
  65. },
  66. _onStyleChanged: function (actor) {
  67. this._spacing = Math.round(actor.get_theme_node().get_length('spacing'));
  68. },
  69. _onButtonReleaseEvent: function (actor, event) {
  70. this.activate(event);
  71. return true;
  72. },
  73. _onKeyPressEvent: function (actor, event) {
  74. let symbol = event.get_key_symbol();
  75. if (symbol == Clutter.KEY_space || symbol == Clutter.KEY_Return) {
  76. this.activate(event);
  77. return true;
  78. }
  79. return false;
  80. },
  81. _onKeyFocusIn: function (actor) {
  82. this.setActive(true);
  83. },
  84. _onKeyFocusOut: function (actor) {
  85. this.setActive(false);
  86. },
  87. _onHoverChanged: function (actor) {
  88. this.setActive(actor.hover);
  89. },
  90. activate: function (event) {
  91. this.emit('activate', event);
  92. },
  93. setActive: function (active, params) {
  94. let activeChanged = active != this.active;
  95. params = Params.parse (params, { grabKeyboard: true });
  96. if (activeChanged) {
  97. this.active = active;
  98. if (active) {
  99. this.actor.add_style_pseudo_class('active');
  100. if (params.grabKeyboard)
  101. this.actor.grab_key_focus();
  102. } else
  103. this.actor.remove_style_pseudo_class('active');
  104. this.emit('active-changed', active);
  105. }
  106. },
  107. setSensitive: function(sensitive) {
  108. if (!this._activatable)
  109. return;
  110. if (this.sensitive == sensitive)
  111. return;
  112. this.sensitive = sensitive;
  113. this.actor.reactive = sensitive;
  114. this.actor.can_focus = sensitive;
  115. if (sensitive)
  116. this.actor.remove_style_pseudo_class('insensitive');
  117. else
  118. this.actor.add_style_pseudo_class('insensitive');
  119. this.emit('sensitive-changed', sensitive);
  120. },
  121. destroy: function() {
  122. this.actor.destroy();
  123. this.emit('destroy');
  124. },
  125. // adds an actor to the menu item; @params can contain %span
  126. // (column span; defaults to 1, -1 means "all the remaining width"),
  127. // %expand (defaults to #false), and %align (defaults to
  128. // #St.Align.START)
  129. addActor: function(child, params) {
  130. params = Params.parse(params, { span: 1,
  131. expand: false,
  132. align: St.Align.START });
  133. params.actor = child;
  134. this._children.push(params);
  135. this.actor.connect('destroy', Lang.bind(this, function () { this._removeChild(child); }));
  136. this.actor.add_actor(child);
  137. },
  138. _removeChild: function(child) {
  139. for (let i = 0; i < this._children.length; i++) {
  140. if (this._children[i].actor == child) {
  141. this._children.splice(i, 1);
  142. return;
  143. }
  144. }
  145. },
  146. removeActor: function(child) {
  147. this.actor.remove_actor(child);
  148. this._removeChild(child);
  149. },
  150. setShowDot: function(show) {
  151. if (show) {
  152. if (this._dot)
  153. return;
  154. this._dot = new St.DrawingArea({ style_class: 'popup-menu-item-dot' });
  155. this._dot.connect('repaint', Lang.bind(this, this._onRepaintDot));
  156. this.actor.add_actor(this._dot);
  157. } else {
  158. if (!this._dot)
  159. return;
  160. this._dot.destroy();
  161. this._dot = null;
  162. }
  163. },
  164. _onRepaintDot: function(area) {
  165. let cr = area.get_context();
  166. let [width, height] = area.get_surface_size();
  167. let color = area.get_theme_node().get_foreground_color();
  168. cr.setSourceRGBA (
  169. color.red / 255,
  170. color.green / 255,
  171. color.blue / 255,
  172. color.alpha / 255);
  173. cr.arc(width / 2, height / 2, width / 3, 0, 2 * Math.PI);
  174. cr.fill();
  175. },
  176. // This returns column widths in logical order (i.e. from the dot
  177. // to the image), not in visual order (left to right)
  178. getColumnWidths: function() {
  179. let widths = [];
  180. for (let i = 0, col = 0; i < this._children.length; i++) {
  181. let child = this._children[i];
  182. let [min, natural] = child.actor.get_preferred_width(-1);
  183. widths[col++] = natural;
  184. if (child.span > 1) {
  185. for (let j = 1; j < child.span; j++)
  186. widths[col++] = 0;
  187. }
  188. }
  189. return widths;
  190. },
  191. setColumnWidths: function(widths) {
  192. this._columnWidths = widths;
  193. },
  194. _getPreferredWidth: function(actor, forHeight, alloc) {
  195. let width = 0;
  196. if (this._columnWidths) {
  197. for (let i = 0; i < this._columnWidths.length; i++) {
  198. if (i > 0)
  199. width += this._spacing;
  200. width += this._columnWidths[i];
  201. }
  202. } else {
  203. for (let i = 0; i < this._children.length; i++) {
  204. let child = this._children[i];
  205. if (i > 0)
  206. width += this._spacing;
  207. let [min, natural] = child.actor.get_preferred_width(-1);
  208. width += natural;
  209. }
  210. }
  211. alloc.min_size = alloc.natural_size = width;
  212. },
  213. _getPreferredHeight: function(actor, forWidth, alloc) {
  214. let height = 0, x = 0, minWidth, childWidth;
  215. for (let i = 0; i < this._children.length; i++) {
  216. let child = this._children[i];
  217. if (this._columnWidths) {
  218. if (child.span == -1) {
  219. childWidth = 0;
  220. for (let j = i; j < this._columnWidths.length; j++)
  221. childWidth += this._columnWidths[j]
  222. } else
  223. childWidth = this._columnWidths[i];
  224. } else {
  225. if (child.span == -1)
  226. childWidth = forWidth - x;
  227. else
  228. [minWidth, childWidth] = child.actor.get_preferred_width(-1);
  229. }
  230. x += childWidth;
  231. let [min, natural] = child.actor.get_preferred_height(childWidth);
  232. if (natural > height)
  233. height = natural;
  234. }
  235. alloc.min_size = alloc.natural_size = height;
  236. },
  237. _allocate: function(actor, box, flags) {
  238. let height = box.y2 - box.y1;
  239. let direction = this.actor.get_text_direction();
  240. if (this._dot) {
  241. // The dot is placed outside box
  242. // one quarter of padding from the border of the container
  243. // (so 3/4 from the inner border)
  244. // (padding is box.x1)
  245. let dotBox = new Clutter.ActorBox();
  246. let dotWidth = Math.round(box.x1 / 2);
  247. if (direction == Clutter.TextDirection.LTR) {
  248. dotBox.x1 = Math.round(box.x1 / 4);
  249. dotBox.x2 = dotBox.x1 + dotWidth;
  250. } else {
  251. dotBox.x2 = box.x2 + 3 * Math.round(box.x1 / 4);
  252. dotBox.x1 = dotBox.x2 - dotWidth;
  253. }
  254. dotBox.y1 = Math.round(box.y1 + (height - dotWidth) / 2);
  255. dotBox.y2 = dotBox.y1 + dotWidth;
  256. this._dot.allocate(dotBox, flags);
  257. }
  258. let x;
  259. if (direction == Clutter.TextDirection.LTR)
  260. x = box.x1;
  261. else
  262. x = box.x2;
  263. // if direction is ltr, x is the right edge of the last added
  264. // actor, and it's constantly increasing, whereas if rtl, x is
  265. // the left edge and it decreases
  266. for (let i = 0, col = 0; i < this._children.length; i++) {
  267. let child = this._children[i];
  268. let childBox = new Clutter.ActorBox();
  269. let [minWidth, naturalWidth] = child.actor.get_preferred_width(-1);
  270. let availWidth, extraWidth;
  271. if (this._columnWidths) {
  272. if (child.span == -1) {
  273. if (direction == Clutter.TextDirection.LTR)
  274. availWidth = box.x2 - x;
  275. else
  276. availWidth = x - box.x1;
  277. } else {
  278. availWidth = 0;
  279. for (let j = 0; j < child.span; j++)
  280. availWidth += this._columnWidths[col++];
  281. }
  282. extraWidth = availWidth - naturalWidth;
  283. } else {
  284. if (child.span == -1) {
  285. if (direction == Clutter.TextDirection.LTR)
  286. availWidth = box.x2 - x;
  287. else
  288. availWidth = x - box.x1;
  289. } else {
  290. availWidth = naturalWidth;
  291. }
  292. extraWidth = 0;
  293. }
  294. if (direction == Clutter.TextDirection.LTR) {
  295. if (child.expand) {
  296. childBox.x1 = x;
  297. childBox.x2 = x + availWidth;
  298. } else if (child.align === St.Align.MIDDLE) {
  299. childBox.x1 = x + Math.round(extraWidth / 2);
  300. childBox.x2 = childBox.x1 + naturalWidth;
  301. } else if (child.align === St.Align.END) {
  302. childBox.x2 = x + availWidth;
  303. childBox.x1 = childBox.x2 - naturalWidth;
  304. } else {
  305. childBox.x1 = x;
  306. childBox.x2 = x + naturalWidth;
  307. }
  308. } else {
  309. if (child.expand) {
  310. childBox.x1 = x - availWidth;
  311. childBox.x2 = x;
  312. } else if (child.align === St.Align.MIDDLE) {
  313. childBox.x1 = x - Math.round(extraWidth / 2);
  314. childBox.x2 = childBox.x1 + naturalWidth;
  315. } else if (child.align === St.Align.END) {
  316. // align to the left
  317. childBox.x1 = x - availWidth;
  318. childBox.x2 = childBox.x1 + naturalWidth;
  319. } else {
  320. // align to the right
  321. childBox.x2 = x;
  322. childBox.x1 = x - naturalWidth;
  323. }
  324. }
  325. let [minHeight, naturalHeight] = child.actor.get_preferred_height(childBox.x2 - childBox.x1);
  326. childBox.y1 = Math.round(box.y1 + (height - naturalHeight) / 2);
  327. childBox.y2 = childBox.y1 + naturalHeight;
  328. child.actor.allocate(childBox, flags);
  329. if (direction == Clutter.TextDirection.LTR)
  330. x += availWidth + this._spacing;
  331. else
  332. x -= availWidth + this._spacing;
  333. }
  334. }
  335. });
  336. Signals.addSignalMethods(PopupBaseMenuItem.prototype);
  337. const PopupMenuItem = new Lang.Class({
  338. Name: 'PopupMenuItem',
  339. Extends: PopupBaseMenuItem,
  340. _init: function (text, params) {
  341. this.parent(params);
  342. this.label = new St.Label({ text: text });
  343. this.addActor(this.label);
  344. this.actor.label_actor = this.label
  345. }
  346. });
  347. const PopupSeparatorMenuItem = new Lang.Class({
  348. Name: 'PopupSeparatorMenuItem',
  349. Extends: PopupBaseMenuItem,
  350. _init: function () {
  351. this.parent({ reactive: false });
  352. this._drawingArea = new St.DrawingArea({ style_class: 'popup-separator-menu-item' });
  353. this.addActor(this._drawingArea, { span: -1, expand: true });
  354. this._drawingArea.connect('repaint', Lang.bind(this, this._onRepaint));
  355. },
  356. _onRepaint: function(area) {
  357. let cr = area.get_context();
  358. let themeNode = area.get_theme_node();
  359. let [width, height] = area.get_surface_size();
  360. let margin = themeNode.get_length('-margin-horizontal');
  361. let gradientHeight = themeNode.get_length('-gradient-height');
  362. let startColor = themeNode.get_color('-gradient-start');
  363. let endColor = themeNode.get_color('-gradient-end');
  364. let gradientWidth = (width - margin * 2);
  365. let gradientOffset = (height - gradientHeight) / 2;
  366. let pattern = new Cairo.LinearGradient(margin, gradientOffset, width - margin, gradientOffset + gradientHeight);
  367. pattern.addColorStopRGBA(0, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
  368. pattern.addColorStopRGBA(0.5, endColor.red / 255, endColor.green / 255, endColor.blue / 255, endColor.alpha / 255);
  369. pattern.addColorStopRGBA(1, startColor.red / 255, startColor.green / 255, startColor.blue / 255, startColor.alpha / 255);
  370. cr.setSource(pattern);
  371. cr.rectangle(margin, gradientOffset, gradientWidth, gradientHeight);
  372. cr.fill();
  373. }
  374. });
  375. const PopupAlternatingMenuItemState = {
  376. DEFAULT: 0,
  377. ALTERNATIVE: 1
  378. }
  379. const PopupAlternatingMenuItem = new Lang.Class({
  380. Name: 'PopupAlternatingMenuItem',
  381. Extends: PopupBaseMenuItem,
  382. _init: function(text, alternateText, params) {
  383. this.parent(params);
  384. this.actor.add_style_class_name('popup-alternating-menu-item');
  385. this._text = text;
  386. this._alternateText = alternateText;
  387. this.label = new St.Label({ text: text });
  388. this.state = PopupAlternatingMenuItemState.DEFAULT;
  389. this.addActor(this.label);
  390. this.actor.label_actor = this.label;
  391. this.actor.connect('notify::mapped', Lang.bind(this, this._onMapped));
  392. },
  393. _onMapped: function() {
  394. if (this.actor.mapped) {
  395. this._capturedEventId = global.stage.connect('captured-event',
  396. Lang.bind(this, this._onCapturedEvent));
  397. this._updateStateFromModifiers();
  398. } else {
  399. if (this._capturedEventId != 0) {
  400. global.stage.disconnect(this._capturedEventId);
  401. this._capturedEventId = 0;
  402. }
  403. }
  404. },
  405. _setState: function(state) {
  406. if (this.state != state) {
  407. if (state == PopupAlternatingMenuItemState.ALTERNATIVE && !this._canAlternate())
  408. return;
  409. this.state = state;
  410. this._updateLabel();
  411. }
  412. },
  413. _updateStateFromModifiers: function() {
  414. let [x, y, mods] = global.get_pointer();
  415. let state;
  416. if ((mods & Clutter.ModifierType.MOD1_MASK) == 0) {
  417. state = PopupAlternatingMenuItemState.DEFAULT;
  418. } else {
  419. state = PopupAlternatingMenuItemState.ALTERNATIVE;
  420. }
  421. this._setState(state);
  422. },
  423. _onCapturedEvent: function(actor, event) {
  424. if (event.type() != Clutter.EventType.KEY_PRESS &&
  425. event.type() != Clutter.EventType.KEY_RELEASE)
  426. return false;
  427. let key = event.get_key_symbol();
  428. if (key == Clutter.KEY_Alt_L || key == Clutter.KEY_Alt_R)
  429. this._updateStateFromModifiers();
  430. return false;
  431. },
  432. _updateLabel: function() {
  433. if (this.state == PopupAlternatingMenuItemState.ALTERNATIVE) {
  434. this.actor.add_style_pseudo_class('alternate');
  435. this.label.set_text(this._alternateText);
  436. } else {
  437. this.actor.remove_style_pseudo_class('alternate');
  438. this.label.set_text(this._text);
  439. }
  440. },
  441. _canAlternate: function() {
  442. if (this.state == PopupAlternatingMenuItemState.DEFAULT && !this._alternateText)
  443. return false;
  444. return true;
  445. },
  446. updateText: function(text, alternateText) {
  447. this._text = text;
  448. this._alternateText = alternateText;
  449. if (!this._canAlternate())
  450. this._setState(PopupAlternatingMenuItemState.DEFAULT);
  451. this._updateLabel();
  452. }
  453. });
  454. const PopupSliderMenuItem = new Lang.Class({
  455. Name: 'PopupSliderMenuItem',
  456. Extends: PopupBaseMenuItem,
  457. _init: function(value) {
  458. this.parent({ activate: false });
  459. this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
  460. if (isNaN(value))
  461. // Avoid spreading NaNs around
  462. throw TypeError('The slider value must be a number');
  463. this._value = Math.max(Math.min(value, 1), 0);
  464. this._slider = new St.DrawingArea({ style_class: 'popup-slider-menu-item', reactive: true });
  465. this.addActor(this._slider, { span: -1, expand: true });
  466. this._slider.connect('repaint', Lang.bind(this, this._sliderRepaint));
  467. this.actor.connect('button-press-event', Lang.bind(this, this._startDragging));
  468. this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
  469. this.actor.connect('notify::mapped', Lang.bind(this, function() {
  470. if (!this.actor.mapped)
  471. this._endDragging();
  472. }));
  473. this._releaseId = this._motionId = 0;
  474. this._dragging = false;
  475. },
  476. setValue: function(value) {
  477. if (isNaN(value))
  478. throw TypeError('The slider value must be a number');
  479. this._value = Math.max(Math.min(value, 1), 0);
  480. this._slider.queue_repaint();
  481. },
  482. _sliderRepaint: function(area) {
  483. let cr = area.get_context();
  484. let themeNode = area.get_theme_node();
  485. let [width, height] = area.get_surface_size();
  486. let handleRadius = themeNode.get_length('-slider-handle-radius');
  487. let sliderWidth = width - 2 * handleRadius;
  488. let sliderHeight = themeNode.get_length('-slider-height');
  489. let sliderBorderWidth = themeNode.get_length('-slider-border-width');
  490. let sliderBorderColor = themeNode.get_color('-slider-border-color');
  491. let sliderColor = themeNode.get_color('-slider-background-color');
  492. let sliderActiveBorderColor = themeNode.get_color('-slider-active-border-color');
  493. let sliderActiveColor = themeNode.get_color('-slider-active-background-color');
  494. cr.setSourceRGBA (
  495. sliderActiveColor.red / 255,
  496. sliderActiveColor.green / 255,
  497. sliderActiveColor.blue / 255,
  498. sliderActiveColor.alpha / 255);
  499. cr.rectangle(handleRadius, (height - sliderHeight) / 2, sliderWidth * this._value, sliderHeight);
  500. cr.fillPreserve();
  501. cr.setSourceRGBA (
  502. sliderActiveBorderColor.red / 255,
  503. sliderActiveBorderColor.green / 255,
  504. sliderActiveBorderColor.blue / 255,
  505. sliderActiveBorderColor.alpha / 255);
  506. cr.setLineWidth(sliderBorderWidth);
  507. cr.stroke();
  508. cr.setSourceRGBA (
  509. sliderColor.red / 255,
  510. sliderColor.green / 255,
  511. sliderColor.blue / 255,
  512. sliderColor.alpha / 255);
  513. cr.rectangle(handleRadius + sliderWidth * this._value, (height - sliderHeight) / 2, sliderWidth * (1 - this._value), sliderHeight);
  514. cr.fillPreserve();
  515. cr.setSourceRGBA (
  516. sliderBorderColor.red / 255,
  517. sliderBorderColor.green / 255,
  518. sliderBorderColor.blue / 255,
  519. sliderBorderColor.alpha / 255);
  520. cr.setLineWidth(sliderBorderWidth);
  521. cr.stroke();
  522. let handleY = height / 2;
  523. let handleX = handleRadius + (width - 2 * handleRadius) * this._value;
  524. let color = themeNode.get_foreground_color();
  525. cr.setSourceRGBA (
  526. color.red / 255,
  527. color.green / 255,
  528. color.blue / 255,
  529. color.alpha / 255);
  530. cr.arc(handleX, handleY, handleRadius, 0, 2 * Math.PI);
  531. cr.fill();
  532. },
  533. _startDragging: function(actor, event) {
  534. if (this._dragging) // don't allow two drags at the same time
  535. return;
  536. this._dragging = true;
  537. // FIXME: we should only grab the specific device that originated
  538. // the event, but for some weird reason events are still delivered
  539. // outside the slider if using clutter_grab_pointer_for_device
  540. Clutter.grab_pointer(this._slider);
  541. this._releaseId = this._slider.connect('button-release-event', Lang.bind(this, this._endDragging));
  542. this._motionId = this._slider.connect('motion-event', Lang.bind(this, this._motionEvent));
  543. let absX, absY;
  544. [absX, absY] = event.get_coords();
  545. this._moveHandle(absX, absY);
  546. },
  547. _endDragging: function() {
  548. if (this._dragging) {
  549. this._slider.disconnect(this._releaseId);
  550. this._slider.disconnect(this._motionId);
  551. Clutter.ungrab_pointer();
  552. this._dragging = false;
  553. this.emit('drag-end');
  554. }
  555. return true;
  556. },
  557. _onScrollEvent: function (actor, event) {
  558. let direction = event.get_scroll_direction();
  559. if (direction == Clutter.ScrollDirection.DOWN) {
  560. this._value = Math.max(0, this._value - SLIDER_SCROLL_STEP);
  561. }
  562. else if (direction == Clutter.ScrollDirection.UP) {
  563. this._value = Math.min(1, this._value + SLIDER_SCROLL_STEP);
  564. }
  565. this._slider.queue_repaint();
  566. this.emit('value-changed', this._value);
  567. },
  568. _motionEvent: function(actor, event) {
  569. let absX, absY;
  570. [absX, absY] = event.get_coords();
  571. this._moveHandle(absX, absY);
  572. return true;
  573. },
  574. _moveHandle: function(absX, absY) {
  575. let relX, relY, sliderX, sliderY;
  576. [sliderX, sliderY] = this._slider.get_transformed_position();
  577. relX = absX - sliderX;
  578. relY = absY - sliderY;
  579. let width = this._slider.width;
  580. let handleRadius = this._slider.get_theme_node().get_length('-slider-handle-radius');
  581. let newvalue;
  582. if (relX < handleRadius)
  583. newvalue = 0;
  584. else if (relX > width - handleRadius)
  585. newvalue = 1;
  586. else
  587. newvalue = (relX - handleRadius) / (width - 2 * handleRadius);
  588. this._value = newvalue;
  589. this._slider.queue_repaint();
  590. this.emit('value-changed', this._value);
  591. },
  592. get value() {
  593. return this._value;
  594. },
  595. _onKeyPressEvent: function (actor, event) {
  596. let key = event.get_key_symbol();
  597. if (key == Clutter.KEY_Right || key == Clutter.KEY_Left) {
  598. let delta = key == Clutter.KEY_Right ? 0.1 : -0.1;
  599. this._value = Math.max(0, Math.min(this._value + delta, 1));
  600. this._slider.queue_repaint();
  601. this.emit('value-changed', this._value);
  602. this.emit('drag-end');
  603. return true;
  604. }
  605. return false;
  606. }
  607. });
  608. const Switch = new Lang.Class({
  609. Name: 'Switch',
  610. _init: function(state) {
  611. this.actor = new St.Bin({ style_class: 'toggle-switch',
  612. accessible_role: Atk.Role.CHECK_BOX});
  613. // Translators: this MUST be either "toggle-switch-us"
  614. // (for toggle switches containing the English words
  615. // "ON" and "OFF") or "toggle-switch-intl" (for toggle
  616. // switches containing "â&#x2014;?" and "|"). Other values will
  617. // simply result in invisible toggle switches.
  618. this.actor.add_style_class_name(_("toggle-switch-us"));
  619. this.setToggleState(state);
  620. },
  621. setToggleState: function(state) {
  622. if (state)
  623. this.actor.add_style_pseudo_class('checked');
  624. else
  625. this.actor.remove_style_pseudo_class('checked');
  626. this.state = state;
  627. },
  628. toggle: function() {
  629. this.setToggleState(!this.state);
  630. }
  631. });
  632. const PopupSwitchMenuItem = new Lang.Class({
  633. Name: 'PopupSwitchMenuItem',
  634. Extends: PopupBaseMenuItem,
  635. _init: function(text, active, params) {
  636. this.parent(params);
  637. this.label = new St.Label({ text: text });
  638. this._switch = new Switch(active);
  639. this.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM;
  640. this.checkAccessibleState();
  641. this.actor.label_actor = this.label;
  642. this.addActor(this.label);
  643. this._statusBin = new St.Bin({ x_align: St.Align.END });
  644. this.addActor(this._statusBin,
  645. { expand: true, span: -1, align: St.Align.END });
  646. this._statusLabel = new St.Label({ text: '',
  647. style_class: 'popup-inactive-menu-item'
  648. });
  649. this._statusBin.child = this._switch.actor;
  650. },
  651. setStatus: function(text) {
  652. if (text != null) {
  653. this._statusLabel.text = text;
  654. this._statusBin.child = this._statusLabel;
  655. this.actor.reactive = false;
  656. this.actor.can_focus = false;
  657. this.actor.accessible_role = Atk.Role.MENU_ITEM;
  658. } else {
  659. this._statusBin.child = this._switch.actor;
  660. this.actor.reactive = true;
  661. this.actor.can_focus = true;
  662. this.actor.accessible_role = Atk.Role.CHECK_MENU_ITEM;
  663. }
  664. this.checkAccessibleState();
  665. },
  666. activate: function(event) {
  667. if (this._switch.actor.mapped) {
  668. this.toggle();
  669. }
  670. // we allow pressing space to toggle the switch
  671. // without closing the menu
  672. if (event.type() == Clutter.EventType.KEY_PRESS &&
  673. event.get_key_symbol() == Clutter.KEY_space)
  674. return;
  675. this.parent(event);
  676. },
  677. toggle: function() {
  678. this._switch.toggle();
  679. this.emit('toggled', this._switch.state);
  680. this.checkAccessibleState();
  681. },
  682. get state() {
  683. return this._switch.state;
  684. },
  685. setToggleState: function(state) {
  686. this._switch.setToggleState(state);
  687. this.checkAccessibleState();
  688. },
  689. checkAccessibleState: function() {
  690. switch (this.actor.accessible_role) {
  691. case Atk.Role.CHECK_MENU_ITEM:
  692. if (this._switch.state)
  693. this.actor.add_accessible_state (Atk.StateType.CHECKED);
  694. else
  695. this.actor.remove_accessible_state (Atk.StateType.CHECKED);
  696. break;
  697. default:
  698. this.actor.remove_accessible_state (Atk.StateType.CHECKED);
  699. }
  700. }
  701. });
  702. const PopupImageMenuItem = new Lang.Class({
  703. Name: 'PopupImageMenuItem',
  704. Extends: PopupBaseMenuItem,
  705. _init: function (text, iconName, params) {
  706. this.parent(params);
  707. this.label = new St.Label({ text: text });
  708. this.addActor(this.label);
  709. this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
  710. this.addActor(this._icon, { align: St.Align.END });
  711. this.setIcon(iconName);
  712. },
  713. setIcon: function(name) {
  714. this._icon.icon_name = name;
  715. }
  716. });
  717. const PopupMenuBase = new Lang.Class({
  718. Name: 'PopupMenuBase',
  719. Abstract: true,
  720. _init: function(sourceActor, styleClass) {
  721. this.sourceActor = sourceActor;
  722. if (styleClass !== undefined) {
  723. this.box = new St.BoxLayout({ style_class: styleClass,
  724. vertical: true });
  725. } else {
  726. this.box = new St.BoxLayout({ vertical: true });
  727. }
  728. this.box.connect_after('queue-relayout', Lang.bind(this, this._menuQueueRelayout));
  729. this.length = 0;
  730. this.isOpen = false;
  731. // If set, we don't send events (including crossing events) to the source actor
  732. // for the menu which causes its prelight state to freeze
  733. this.blockSourceEvents = false;
  734. // Can be set while a menu is up to let all events through without special
  735. // menu handling useful for scrollbars in menus, and probably not otherwise.
  736. this.passEvents = false;
  737. this._activeMenuItem = null;
  738. this._childMenus = [];
  739. },
  740. addAction: function(title, callback) {
  741. let menuItem = new PopupMenuItem(title);
  742. this.addMenuItem(menuItem);
  743. menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
  744. callback(event);
  745. }));
  746. return menuItem;
  747. },
  748. addSettingsAction: function(title, desktopFile) {
  749. if (!Main.sessionMode.allowSettings)
  750. return null;
  751. let menuItem = this.addAction(title, function() {
  752. let app = Shell.AppSystem.get_default().lookup_setting(desktopFile);
  753. if (!app) {
  754. log('Settings panel for desktop file ' + desktopFile + ' could not be loaded!');
  755. return;
  756. }
  757. Main.overview.hide();
  758. app.activate();
  759. });
  760. return menuItem;
  761. },
  762. isEmpty: function() {
  763. return this.box.get_n_children() == 0;
  764. },
  765. isChildMenu: function(menu) {
  766. return this._childMenus.indexOf(menu) != -1;
  767. },
  768. addChildMenu: function(menu) {
  769. if (this.isChildMenu(menu))
  770. return;
  771. this._childMenus.push(menu);
  772. this.emit('child-menu-added', menu);
  773. },
  774. removeChildMenu: function(menu) {
  775. let index = this._childMenus.indexOf(menu);
  776. if (index == -1)
  777. return;
  778. this._childMenus.splice(index, 1);
  779. this.emit('child-menu-removed', menu);
  780. },
  781. /**
  782. * _connectSubMenuSignals:
  783. * @object: a menu item, or a menu section
  784. * @menu: a sub menu, or a menu section
  785. *
  786. * Connects to signals on @menu that are necessary for
  787. * operating the submenu, and stores the ids on @object.
  788. */
  789. _connectSubMenuSignals: function(object, menu) {
  790. object._subMenuActivateId = menu.connect('activate', Lang.bind(this, function() {
  791. this.emit('activate');
  792. this.close(BoxPointer.PopupAnimation.FULL);
  793. }));
  794. object._subMenuActiveChangeId = menu.connect('active-changed', Lang.bind(this, function(submenu, submenuItem) {
  795. if (this._activeMenuItem && this._activeMenuItem != submenuItem)
  796. this._activeMenuItem.setActive(false);
  797. this._activeMenuItem = submenuItem;
  798. this.emit('active-changed', submenuItem);
  799. }));
  800. },
  801. _connectItemSignals: function(menuItem) {
  802. menuItem._activeChangeId = menuItem.connect('active-changed', Lang.bind(this, function (menuItem, active) {
  803. if (active && this._activeMenuItem != menuItem) {
  804. if (this._activeMenuItem)
  805. this._activeMenuItem.setActive(false);
  806. this._activeMenuItem = menuItem;
  807. this.emit('active-changed', menuItem);
  808. } else if (!active && this._activeMenuItem == menuItem) {
  809. this._activeMenuItem = null;
  810. this.emit('active-changed', null);
  811. }
  812. }));
  813. menuItem._sensitiveChangeId = menuItem.connect('sensitive-changed', Lang.bind(this, function(menuItem, sensitive) {
  814. if (!sensitive && this._activeMenuItem == menuItem) {
  815. if (!this.actor.navigate_focus(menuItem.actor,
  816. Gtk.DirectionType.TAB_FORWARD,
  817. true))
  818. this.actor.grab_key_focus();
  819. } else if (sensitive && this._activeMenuItem == null) {
  820. if (global.stage.get_key_focus() == this.actor)
  821. menuItem.actor.grab_key_focus();
  822. }
  823. }));
  824. menuItem._activateId = menuItem.connect('activate', Lang.bind(this, function (menuItem, event) {
  825. this.emit('activate', menuItem);
  826. this.close(BoxPointer.PopupAnimation.FULL);
  827. }));
  828. // the weird name is to avoid a conflict with some random property
  829. // the menuItem may have, called destroyId
  830. // (FIXME: in the future it may make sense to have container objects
  831. // like PopupMenuManager does)
  832. menuItem._popupMenuDestroyId = menuItem.connect('destroy', Lang.bind(this, function(menuItem) {
  833. menuItem.disconnect(menuItem._popupMenuDestroyId);
  834. menuItem.disconnect(menuItem._activateId);
  835. menuItem.disconnect(menuItem._activeChangeId);
  836. menuItem.disconnect(menuItem._sensitiveChangeId);
  837. if (menuItem.menu) {
  838. menuItem.menu.disconnect(menuItem._subMenuActivateId);
  839. menuItem.menu.disconnect(menuItem._subMenuActiveChangeId);
  840. this.disconnect(menuItem._closingId);
  841. }
  842. if (menuItem == this._activeMenuItem)
  843. this._activeMenuItem = null;
  844. }));
  845. },
  846. _updateSeparatorVisibility: function(menuItem) {
  847. let children = this.box.get_children();
  848. let index = children.indexOf(menuItem.actor);
  849. if (index < 0)
  850. return;
  851. let childBeforeIndex = index - 1;
  852. while (childBeforeIndex >= 0 && !children[childBeforeIndex].visible)
  853. childBeforeIndex--;
  854. if (childBeforeIndex < 0
  855. || children[childBeforeIndex]._delegate instanceof PopupSeparatorMenuItem) {
  856. menuItem.actor.hide();
  857. return;
  858. }
  859. let childAfterIndex = index + 1;
  860. while (childAfterIndex < children.length && !children[childAfterIndex].visible)
  861. childAfterIndex++;
  862. if (childAfterIndex >= children.length
  863. || children[childAfterIndex]._delegate instanceof PopupSeparatorMenuItem) {
  864. menuItem.actor.hide();
  865. return;
  866. }
  867. menuItem.actor.show();
  868. },
  869. addMenuItem: function(menuItem, position) {
  870. let before_item = null;
  871. if (position == undefined) {
  872. this.box.add(menuItem.actor);
  873. } else {
  874. let items = this._getMenuItems();
  875. if (position < items.length) {
  876. before_item = items[position].actor;
  877. this.box.insert_child_below(menuItem.actor, before_item);
  878. } else {
  879. this.box.add(menuItem.actor);
  880. }
  881. }
  882. if (menuItem instanceof PopupMenuSection) {
  883. this._connectSubMenuSignals(menuItem, menuItem);
  884. menuItem._closingId = this.connect('open-state-changed',
  885. function(self, open) {
  886. if (!open)
  887. menuItem.close(BoxPointer.PopupAnimation.FADE);
  888. });
  889. menuItem.connect('destroy', Lang.bind(this, function() {
  890. menuItem.disconnect(menuItem._subMenuActivateId);
  891. menuItem.disconnect(menuItem._subMenuActiveChangeId);
  892. this.length--;
  893. }));
  894. } else if (menuItem instanceof PopupSubMenuMenuItem) {
  895. if (before_item == null)
  896. this.box.add(menuItem.menu.actor);
  897. else
  898. this.box.insert_child_below(menuItem.menu.actor, before_item);
  899. this._connectSubMenuSignals(menuItem, menuItem.menu);
  900. this._connectItemSignals(menuItem);
  901. menuItem._closingId = this.connect('open-state-changed', function(self, open) {
  902. if (!open)
  903. menuItem.menu.close(BoxPointer.PopupAnimation.FADE);
  904. });
  905. } else if (menuItem instanceof PopupSeparatorMenuItem) {
  906. this._connectItemSignals(menuItem);
  907. // updateSeparatorVisibility needs to get called any time the
  908. // separator's adjacent siblings change visibility or position.
  909. // open-state-changed isn't exactly that, but doing it in more
  910. // precise ways would require a lot more bookkeeping.
  911. this.connect('open-state-changed', Lang.bind(this, function() { this._updateSeparatorVisibility(menuItem); }));
  912. } else if (menuItem instanceof PopupBaseMenuItem)
  913. this._connectItemSignals(menuItem);
  914. else
  915. throw TypeError("Invalid argument to PopupMenuBase.addMenuItem()");
  916. this.length++;
  917. },
  918. getColumnWidths: function() {
  919. let columnWidths = [];
  920. let items = this.box.get_children();
  921. for (let i = 0; i < items.length; i++) {
  922. if (!items[i].visible)
  923. continue;
  924. if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase) {
  925. let itemColumnWidths = items[i]._delegate.getColumnWidths();
  926. for (let j = 0; j < itemColumnWidths.length; j++) {
  927. if (j >= columnWidths.length || itemColumnWidths[j] > columnWidths[j])
  928. columnWidths[j] = itemColumnWidths[j];
  929. }
  930. }
  931. }
  932. return columnWidths;
  933. },
  934. setColumnWidths: function(widths) {
  935. let items = this.box.get_children();
  936. for (let i = 0; i < items.length; i++) {
  937. if (items[i]._delegate instanceof PopupBaseMenuItem || items[i]._delegate instanceof PopupMenuBase)
  938. items[i]._delegate.setColumnWidths(widths);
  939. }
  940. },
  941. // Because of the above column-width funniness, we need to do a
  942. // queue-relayout on every item whenever the menu itself changes
  943. // size, to force clutter to drop its cached size requests. (The
  944. // menuitems will in turn call queue_relayout on their parent, the
  945. // menu, but that call will be a no-op since the menu already
  946. // has a relayout queued, so we won't get stuck in a loop.
  947. _menuQueueRelayout: function() {
  948. this.box.get_children().map(function (actor) { actor.queue_relayout(); });
  949. },
  950. addActor: function(actor) {
  951. this.box.add(actor);
  952. },
  953. _getMenuItems: function() {
  954. return this.box.get_children().map(function (actor) {
  955. return actor._delegate;
  956. }).filter(function(item) {
  957. return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection;
  958. });
  959. },
  960. get firstMenuItem() {
  961. let items = this._getMenuItems();
  962. if (items.length)
  963. return items[0];
  964. else
  965. return null;
  966. },
  967. get numMenuItems() {
  968. return this._getMenuItems().length;
  969. },
  970. removeAll: function() {
  971. let children = this._getMenuItems();
  972. for (let i = 0; i < children.length; i++) {
  973. let item = children[i];
  974. item.destroy();
  975. }
  976. },
  977. toggle: function() {
  978. if (this.isOpen)
  979. this.close(BoxPointer.PopupAnimation.FULL);
  980. else
  981. this.open(BoxPointer.PopupAnimation.FULL);
  982. },
  983. destroy: function() {
  984. this.removeAll();
  985. this.actor.destroy();
  986. this.emit('destroy');
  987. }
  988. });
  989. Signals.addSignalMethods(PopupMenuBase.prototype);
  990. const PopupMenu = new Lang.Class({
  991. Name: 'PopupMenu',
  992. Extends: PopupMenuBase,
  993. _init: function(sourceActor, arrowAlignment, arrowSide) {
  994. this.parent(sourceActor, 'popup-menu-content');
  995. this._arrowAlignment = arrowAlignment;
  996. this._arrowSide = arrowSide;
  997. this._boxPointer = new BoxPointer.BoxPointer(arrowSide,
  998. { x_fill: true,
  999. y_fill: true,
  1000. x_align: St.Align.START });
  1001. this.actor = this._boxPointer.actor;
  1002. this.actor._delegate = this;
  1003. this.actor.style_class = 'popup-menu-boxpointer';
  1004. this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
  1005. this._boxWrapper = new Shell.GenericContainer();
  1006. this._boxWrapper.connect('get-preferred-width', Lang.bind(this, this._boxGetPreferredWidth));
  1007. this._boxWrapper.connect('get-preferred-height', Lang.bind(this, this._boxGetPreferredHeight));
  1008. this._boxWrapper.connect('allocate', Lang.bind(this, this._boxAllocate));
  1009. this._boxPointer.bin.set_child(this._boxWrapper);
  1010. this._boxWrapper.add_actor(this.box);
  1011. this.actor.add_style_class_name('popup-menu');
  1012. global.focus_manager.add_group(this.actor);
  1013. this.actor.reactive = true;
  1014. },
  1015. _boxGetPreferredWidth: function (actor, forHeight, alloc) {
  1016. let columnWidths = this.getColumnWidths();
  1017. this.setColumnWidths(columnWidths);
  1018. // Now they will request the right sizes
  1019. [alloc.min_size, alloc.natural_size] = this.box.get_preferred_width(forHeight);
  1020. },
  1021. _boxGetPreferredHeight: function (actor, forWidth, alloc) {
  1022. [alloc.min_size, alloc.natural_size] = this.box.get_preferred_height(forWidth);
  1023. },
  1024. _boxAllocate: function (actor, box, flags) {
  1025. this.box.allocate(box, flags);
  1026. },
  1027. _onKeyPressEvent: function(actor, event) {
  1028. if (event.get_key_symbol() == Clutter.Escape) {
  1029. this.close(BoxPointer.PopupAnimation.FULL);
  1030. return true;
  1031. }
  1032. return false;
  1033. },
  1034. setArrowOrigin: function(origin) {
  1035. this._boxPointer.setArrowOrigin(origin);
  1036. },
  1037. setSourceAlignment: function(alignment) {
  1038. this._boxPointer.setSourceAlignment(alignment);
  1039. },
  1040. open: function(animate) {
  1041. if (this.isOpen)
  1042. return;
  1043. if (this.isEmpty())
  1044. return;
  1045. this.isOpen = true;
  1046. this._boxPointer.setPosition(this.sourceActor, this._arrowAlignment);
  1047. this._boxPointer.show(animate);
  1048. this.actor.raise_top();
  1049. this.emit('open-state-changed', true);
  1050. },
  1051. close: function(animate) {
  1052. if (!this.isOpen)
  1053. return;
  1054. if (this._activeMenuItem)
  1055. this._activeMenuItem.setActive(false);
  1056. this._boxPointer.hide(animate);
  1057. this.isOpen = false;
  1058. this.emit('open-state-changed', false);
  1059. }
  1060. });
  1061. const PopupSubMenu = new Lang.Class({
  1062. Name: 'PopupSubMenu',
  1063. Extends: PopupMenuBase,
  1064. _init: function(sourceActor, sourceArrow) {
  1065. this.parent(sourceActor);
  1066. this._arrow = sourceArrow;
  1067. this._arrow.rotation_center_z_gravity = Clutter.Gravity.CENTER;
  1068. // Since a function of a submenu might be to provide a "More.." expander
  1069. // with long content, we make it scrollable - the scrollbar will only take
  1070. // effect if a CSS max-height is set on the top menu.
  1071. this.actor = new St.ScrollView({ style_class: 'popup-sub-menu',
  1072. hscrollbar_policy: Gtk.PolicyType.NEVER,
  1073. vscrollbar_policy: Gtk.PolicyType.NEVER });
  1074. // StScrollbar plays dirty tricks with events, calling
  1075. // clutter_set_motion_events_enabled (FALSE) during the scroll; this
  1076. // confuses our event tracking, so we just turn it off during the
  1077. // scroll.
  1078. let vscroll = this.actor.get_vscroll_bar();
  1079. vscroll.connect('scroll-start',
  1080. Lang.bind(this, function() {
  1081. let topMenu = this._getTopMenu();
  1082. if (topMenu)
  1083. topMenu.passEvents = true;
  1084. }));
  1085. vscroll.connect('scroll-stop',
  1086. Lang.bind(this, function() {
  1087. let topMenu = this._getTopMenu();
  1088. if (topMenu)
  1089. topMenu.passEvents = false;
  1090. }));
  1091. this.actor.add_actor(this.box);
  1092. this.actor._delegate = this;
  1093. this.actor.clip_to_allocation = true;
  1094. this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
  1095. this.actor.hide();
  1096. },
  1097. _getTopMenu: function() {
  1098. let actor = this.actor.get_parent();
  1099. while (actor) {
  1100. if (actor._delegate && actor._delegate instanceof PopupMenu)
  1101. return actor._delegate;
  1102. actor = actor.get_parent();
  1103. }
  1104. return null;
  1105. },
  1106. _needsScrollbar: function() {
  1107. let topMenu = this._getTopMenu();
  1108. let [topMinHeight, topNaturalHeight] = topMenu.actor.get_preferred_height(-1);
  1109. let topThemeNode = topMenu.actor.get_theme_node();
  1110. let topMaxHeight = topThemeNode.get_max_height();
  1111. return topMaxHeight >= 0 && topNaturalHeight >= topMaxHeight;
  1112. },
  1113. open: function(animate) {
  1114. if (this.isOpen)
  1115. return;
  1116. if (this.isEmpty())
  1117. return;
  1118. this.isOpen = true;
  1119. this.actor.show();
  1120. let needsScrollbar = this._needsScrollbar();
  1121. // St.ScrollView always requests space horizontally for a possible vertical
  1122. // scrollbar if in AUTOMATIC mode. Doing better would require implementation
  1123. // of width-for-height in St.BoxLayout and St.ScrollView. This looks bad
  1124. // when we *don't* need it, so turn off the scrollbar when that's true.
  1125. // Dynamic changes in whether we need it aren't handled properly.
  1126. this.actor.vscrollbar_policy =
  1127. needsScrollbar ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
  1128. // It looks funny if we animate with a scrollbar (at what point is
  1129. // the scrollbar added?) so just skip that case
  1130. if (animate && needsScrollbar)
  1131. animate = false;
  1132. if (animate) {
  1133. let [minHeight, naturalHeight] = this.actor.get_preferred_height(-1);
  1134. this.actor.height = 0;
  1135. this.actor._arrow_rotation = this._arrow.rotation_angle_z;
  1136. Tweener.addTween(this.actor,
  1137. { _arrow_rotation: 90,
  1138. height: naturalHeight,
  1139. time: 0.25,
  1140. onUpdateScope: this,
  1141. onUpdate: function() {
  1142. this._arrow.rotation_angle_z = this.actor._arrow_rotation;
  1143. },
  1144. onCompleteScope: this,
  1145. onComplete: function() {
  1146. this.actor.set_height(-1);
  1147. this.emit('open-state-changed', true);
  1148. }
  1149. });
  1150. } else {
  1151. this._arrow.rotation_angle_z = 90;
  1152. this.emit('open-state-changed', true);
  1153. }
  1154. },
  1155. close: function(animate) {
  1156. if (!this.isOpen)
  1157. return;
  1158. this.isOpen = false;
  1159. if (this._activeMenuItem)
  1160. this._activeMenuItem.setActive(false);
  1161. if (animate && this._needsScrollbar())
  1162. animate = false;
  1163. if (animate) {
  1164. this.actor._arrow_rotation = this._arrow.rotation_angle_z;
  1165. Tweener.addTween(this.actor,
  1166. { _arrow_rotation: 0,
  1167. height: 0,
  1168. time: 0.25,
  1169. onCompleteScope: this,
  1170. onComplete: function() {
  1171. this.actor.hide();
  1172. this.actor.set_height(-1);
  1173. this.emit('open-state-changed', false);
  1174. },
  1175. onUpdateScope: this,
  1176. onUpdate: function() {
  1177. this._arrow.rotation_angle_z = this.actor._arrow_rotation;
  1178. }
  1179. });
  1180. } else {
  1181. this._arrow.rotation_angle_z = 0;
  1182. this.actor.hide();
  1183. this.isOpen = false;
  1184. this.emit('open-state-changed', false);
  1185. }
  1186. },
  1187. _onKeyPressEvent: function(actor, event) {
  1188. // Move focus back to parent menu if the user types Left.
  1189. if (this.isOpen && event.get_key_symbol() == Clutter.KEY_Left) {
  1190. this.close(BoxPointer.PopupAnimation.FULL);
  1191. this.sourceActor._delegate.setActive(true);
  1192. return true;
  1193. }
  1194. return false;
  1195. }
  1196. });
  1197. /**
  1198. * PopupMenuSection:
  1199. *
  1200. * A section of a PopupMenu which is handled like a submenu
  1201. * (you can add and remove items, you can destroy it, you
  1202. * can add it to another menu), but is completely transparent
  1203. * to the user
  1204. */
  1205. const PopupMenuSection = new Lang.Class({
  1206. Name: 'PopupMenuSection',
  1207. Extends: PopupMenuBase,
  1208. _init: function() {
  1209. this.parent();
  1210. this.actor = this.box;
  1211. this.actor._delegate = this;
  1212. this.isOpen = true;
  1213. // an array of externally managed separators
  1214. this.separators = [];
  1215. },
  1216. // deliberately ignore any attempt to open() or close(), but emit the
  1217. // corresponding signal so children can still pick it up
  1218. open: function(animate) { this.emit('open-state-changed', true); },
  1219. close: function() { this.emit('open-state-changed', false); },
  1220. destroy: function() {
  1221. for (let i = 0; i < this.separators.length; i++)
  1222. this.separators[i].destroy();
  1223. this.separators = [];
  1224. this.parent();
  1225. }
  1226. });
  1227. const PopupSubMenuMenuItem = new Lang.Class({
  1228. Name: 'PopupSubMenuMenuItem',
  1229. Extends: PopupBaseMenuItem,
  1230. _init: function(text) {
  1231. this.parent();
  1232. this.actor.add_style_class_name('popup-submenu-menu-item');
  1233. this.label = new St.Label({ text: text });
  1234. this.addActor(this.label);
  1235. this.actor.label_actor = this.label;
  1236. this._triangle = new St.Label({ text: '\u25B8' });
  1237. this.addActor(this._triangle, { align: St.Align.END });
  1238. this.menu = new PopupSubMenu(this.actor, this._triangle);
  1239. this.menu.connect('open-state-changed', Lang.bind(this, this._subMenuOpenStateChanged));
  1240. },
  1241. _subMenuOpenStateChanged: function(menu, open) {
  1242. if (open)
  1243. this.actor.add_style_pseudo_class('open');
  1244. else
  1245. this.actor.remove_style_pseudo_class('open');
  1246. },
  1247. destroy: function() {
  1248. this.menu.destroy();
  1249. this.parent();
  1250. },
  1251. _onKeyPressEvent: function(actor, event) {
  1252. let symbol = event.get_key_symbol();
  1253. if (symbol == Clutter.KEY_Right) {
  1254. this.menu.open(BoxPointer.PopupAnimation.FULL);
  1255. this.menu.actor.navigate_focus(null, Gtk.DirectionType.DOWN, false);
  1256. return true;
  1257. } else if (symbol == Clutter.KEY_Left && this.menu.isOpen) {
  1258. this.menu.close();
  1259. return true;
  1260. }
  1261. return this.parent(actor, event);
  1262. },
  1263. activate: function(event) {
  1264. this.menu.open(BoxPointer.PopupAnimation.FULL);
  1265. },
  1266. _onButtonReleaseEvent: function(actor) {
  1267. this.menu.toggle();
  1268. }
  1269. });
  1270. const PopupComboMenu = new Lang.Class({
  1271. Name: 'PopupComboMenu',
  1272. Extends: PopupMenuBase,
  1273. _init: function(sourceActor) {
  1274. this.parent(sourceActor, 'popup-combo-menu');
  1275. this.actor = this.box;
  1276. this.actor._delegate = this;
  1277. this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPressEvent));
  1278. this.actor.connect('key-focus-in', Lang.bind(this, this._onKeyFocusIn));
  1279. sourceActor.connect('style-changed',
  1280. Lang.bind(this, this._onSourceActorStyleChanged));
  1281. this._activeItemPos = -1;
  1282. global.focus_manager.add_group(this.actor);
  1283. },
  1284. _onKeyPressEvent: function(actor, event) {
  1285. if (event.get_key_symbol() == Clutter.Escape) {
  1286. this.close(BoxPointer.PopupAnimation.FULL);
  1287. return true;
  1288. }
  1289. return false;
  1290. },
  1291. _onKeyFocusIn: function(actor) {
  1292. let items = this._getMenuItems();
  1293. let activeItem = items[this._activeItemPos];
  1294. activeItem.actor.grab_key_focus();
  1295. },
  1296. _onSourceActorStyleChanged: function() {
  1297. // PopupComboBoxMenuItem clones the active item's actors
  1298. // to work with arbitrary items in the menu; this means
  1299. // that we need to propagate some style information and
  1300. // enforce style updates even when the menu is closed
  1301. let activeItem = this._getMenuItems()[this._activeItemPos];
  1302. if (this.sourceActor.has_style_pseudo_class('insensitive'))
  1303. activeItem.actor.add_style_pseudo_class('insensitive');
  1304. else
  1305. activeItem.actor.remove_style_pseudo_class('insensitive');
  1306. // To propagate the :active style, we need to make sure that the
  1307. // internal state of the PopupComboMenu is updated as well, but
  1308. // we must not move the keyboard grab
  1309. activeItem.setActive(this.sourceActor.has_style_pseudo_class('active'),
  1310. { grabKeyboard: false });
  1311. _ensureStyle(this.actor);
  1312. },
  1313. open: function() {
  1314. if (this.isOpen)
  1315. return;
  1316. if (this.isEmpty())
  1317. return;
  1318. this.isOpen = true;
  1319. let [sourceX, sourceY] = this.sourceActor.get_transformed_position();
  1320. let items = this._getMenuItems();
  1321. let activeItem = items[this._activeItemPos];
  1322. this.actor.set_position(sourceX, sourceY - activeItem.actor.y);
  1323. this.actor.width = Math.max(this.actor.width, this.sourceActor.width);
  1324. this.actor.raise_top();
  1325. this.actor.opacity = 0;
  1326. this.actor.show();
  1327. Tweener.addTween(this.actor,
  1328. { opacity: 255,
  1329. transition: 'linear',
  1330. time: BoxPointer.POPUP_ANIMATION_TIME });
  1331. this.emit('open-state-changed', true);
  1332. },
  1333. close: function() {
  1334. if (!this.isOpen)
  1335. return;
  1336. this.isOpen = false;
  1337. Tweener.addTween(this.actor,
  1338. { opacity: 0,
  1339. transition: 'linear',
  1340. time: BoxPointer.POPUP_ANIMATION_TIME,
  1341. onComplete: Lang.bind(this,
  1342. function() {
  1343. this.actor.hide();
  1344. })
  1345. });
  1346. this.emit('open-state-changed', false);
  1347. },
  1348. setActiveItem: function(position) {
  1349. this._activeItemPos = position;
  1350. },
  1351. getActiveItem: function() {
  1352. return this._getMenuItems()[this._activeItemPos];
  1353. },
  1354. setItemVisible: function(position, visible) {
  1355. if (!visible && position == this._activeItemPos) {
  1356. log('Trying to hide the active menu item.');
  1357. return;
  1358. }
  1359. this._getMenuItems()[position].actor.visible = visible;
  1360. },
  1361. getItemVisible: function(position) {
  1362. return this._getMenuItems()[position].actor.visible;
  1363. }
  1364. });
  1365. const PopupComboBoxMenuItem = new Lang.Class({
  1366. Name: 'PopupComboBoxMenuItem',
  1367. Extends: PopupBaseMenuItem,
  1368. _init: function (params) {
  1369. this.parent(params);
  1370. this.actor.accessible_role = Atk.Role.COMBO_BOX;
  1371. this._itemBox = new Shell.Stack();
  1372. this.addActor(this._itemBox);
  1373. let expander = new St.Label({ text: '\u2304' });
  1374. this.addActor(expander, { align: St.Align.END,
  1375. span: -1 });
  1376. this._menu = new PopupComboMenu(this.actor);
  1377. Main.uiGroup.add_actor(this._menu.actor);
  1378. this._menu.actor.hide();
  1379. if (params.style_class)
  1380. this._menu.actor.add_style_class_name(params.style_class);
  1381. this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent));
  1382. this._activeItemPos = -1;
  1383. this._items = [];
  1384. },
  1385. _getTopMenu: function() {
  1386. let actor = this.actor.get_parent();
  1387. while (actor) {
  1388. if (actor._delegate &&
  1389. (actor._delegate instanceof PopupMenu ||
  1390. actor._delegate instanceof PopupComboMenu))
  1391. return actor._delegate;
  1392. actor = actor.get_parent();
  1393. }
  1394. return null;
  1395. },
  1396. _onScrollEvent: function(actor, event) {
  1397. if (this._activeItemPos == -1)
  1398. return;
  1399. let position = this._activeItemPos;
  1400. let direction = event.get_scroll_direction();
  1401. if (direction == Clutter.ScrollDirection.DOWN) {
  1402. while (position < this._items.length - 1) {
  1403. position++;
  1404. if (this._menu.getItemVisible(position))
  1405. break;
  1406. }
  1407. } else if (direction == Clutter.ScrollDirection.UP) {
  1408. while (position > 0) {
  1409. position--;
  1410. if (this._menu.getItemVisible(position))
  1411. break;
  1412. }
  1413. }
  1414. if (position == this._activeItemPos)
  1415. return;
  1416. this.setActiveItem(position);
  1417. this.emit('active-item-changed', position);
  1418. },
  1419. activate: function(event) {
  1420. let topMenu = this._getTopMenu();
  1421. if (!topMenu)
  1422. return;
  1423. topMenu.addChildMenu(this._menu);
  1424. this._menu.toggle();
  1425. },
  1426. addMenuItem: function(menuItem, position) {
  1427. if (position === undefined)
  1428. position = this._menu.numMenuItems;
  1429. this._menu.addMenuItem(menuItem, position);
  1430. _ensureStyle(this._menu.actor);
  1431. let item = new St.BoxLayout({ style_class: 'popup-combobox-item' });
  1432. let children = menuItem.actor.get_children();
  1433. for (let i = 0; i < children.length; i++) {
  1434. let clone = new Clutter.Clone({ source: children[i] });
  1435. item.add(clone, { y_fill: false });
  1436. }
  1437. let oldItem = this._items[position];
  1438. if (oldItem)
  1439. this._itemBox.remove_actor(oldItem);
  1440. this._items[position] = item;
  1441. this._itemBox.add_actor(item);
  1442. menuItem.connect('activate',
  1443. Lang.bind(this, this._itemActivated, position));
  1444. },
  1445. checkAccessibleLabel: function() {
  1446. let activeItem = this._menu.getActiveItem();
  1447. this.actor.label_actor = activeItem.label;
  1448. },
  1449. setActiveItem: function(position) {
  1450. let item = this._items[position];
  1451. if (!item)
  1452. return;
  1453. if (this._activeItemPos == position)
  1454. return;
  1455. this._menu.setActiveItem(position);
  1456. this._activeItemPos = position;
  1457. for (let i = 0; i < this._items.length; i++)
  1458. this._items[i].visible = (i == this._activeItemPos);
  1459. this.checkAccessibleLabel();
  1460. },
  1461. setItemVisible: function(position, visible) {
  1462. this._menu.setItemVisible(position, visible);
  1463. },
  1464. _itemActivated: function(menuItem, event, position) {
  1465. this.setActiveItem(position);
  1466. this.emit('active-item-changed', position);
  1467. }
  1468. });
  1469. /**
  1470. * RemoteMenu:
  1471. *
  1472. * A PopupMenu that tracks a GMenuModel and shows its actions
  1473. * (exposed by GApplication/GActionGroup)
  1474. */
  1475. const RemoteMenu = new Lang.Class({
  1476. Name: 'RemoteMenu',
  1477. Extends: PopupMenu,
  1478. _init: function(sourceActor, model, actionGroup) {
  1479. this.parent(sourceActor, 0.0, St.Side.TOP);
  1480. this.model = model;
  1481. this.actionGroup = actionGroup;
  1482. this._actions = { };
  1483. this._modelChanged(this.model, 0, 0, this.model.get_n_items(), this);
  1484. this._actionStateChangeId = this.actionGroup.connect('action-state-changed', Lang.bind(this, this._actionStateChanged));
  1485. this._actionEnableChangeId = this.actionGroup.connect('action-enabled-changed', Lang.bind(this, this._actionEnabledChanged));
  1486. },
  1487. destroy: function() {
  1488. if (this._actionStateChangeId) {
  1489. this.actionGroup.disconnect(this._actionStateChangeId);
  1490. this._actionStateChangeId = 0;
  1491. }
  1492. if (this._actionEnableChangeId) {
  1493. this.actionGroup.disconnect(this._actionEnableChangeId);
  1494. this._actionEnableChangeId = 0;
  1495. }
  1496. this.parent();
  1497. },
  1498. _createMenuItem: function(model, index) {
  1499. let labelValue = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_LABEL, null);
  1500. let label = labelValue ? labelValue.deep_unpack() : '';
  1501. // remove all underscores that are not followed by another underscore
  1502. label = label.replace(/_([^_])/, '$1');
  1503. let section_link = model.get_item_link(index, Gio.MENU_LINK_SECTION);
  1504. if (section_link) {
  1505. let item = new PopupMenuSection();
  1506. if (label) {
  1507. let title = new PopupMenuItem(label, { reactive: false,
  1508. style_class: 'popup-subtitle-menu-item' });
  1509. item._titleMenuItem = title;
  1510. title._ignored = true;
  1511. item.addMenuItem(title);
  1512. }
  1513. this._modelChanged(section_link, 0, 0, section_link.get_n_items(), item);
  1514. return [item, true, ''];
  1515. }
  1516. let submenu_link = model.get_item_link(index, Gio.MENU_LINK_SUBMENU);
  1517. if (submenu_link) {
  1518. let item = new PopupSubMenuMenuItem(label);
  1519. this._modelChanged(submenu_link, 0, 0, submenu_link.get_n_items(), item.menu);
  1520. return [item, false, ''];
  1521. }
  1522. let action_id = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_ACTION, null).deep_unpack();
  1523. if (!this.actionGroup.has_action(action_id)) {
  1524. // the action may not be there yet, wait for action-added
  1525. return [null, false, 'action-added'];
  1526. }
  1527. if (!this._actions[action_id])
  1528. this._actions[action_id] = { enabled: this.actionGroup.get_action_enabled(action_id),
  1529. state: this.actionGroup.get_action_state(action_id),
  1530. items: [ ],
  1531. };
  1532. let action = this._actions[action_id];
  1533. let item, target, destroyId, specificSignalId;
  1534. if (action.state) {
  1535. // Docs have get_state_hint(), except that the DBus protocol
  1536. // has no provision for it (so ShellApp does not implement it,
  1537. // and neither GApplication), and g_action_get_state_hint()
  1538. // always returns null
  1539. // Funny :)
  1540. switch (String.fromCharCode(action.state.classify())) {
  1541. case 'b':
  1542. item = new PopupSwitchMenuItem(label, action.state.get_boolean());
  1543. action.items.push(item);
  1544. specificSignalId = item.connect('toggled', Lang.bind(this, function(item) {
  1545. this.actionGroup.activate_action(action_id, null);
  1546. }));
  1547. break;
  1548. case 's':
  1549. item = new PopupMenuItem(label);
  1550. item._remoteTarget = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null).deep_unpack();
  1551. action.items.push(item);
  1552. item.setShowDot(action.state.deep_unpack() == item._remoteTarget);
  1553. specificSignalId = item.connect('activate', Lang.bind(this, function(item) {
  1554. this.actionGroup.activate_action(action_id, GLib.Variant.new_string(item._remoteTarget));
  1555. }));
  1556. break;
  1557. default:
  1558. log('Action "%s" has state of type %s, which is not supported'.format(action_id, action.state.get_type_string()));
  1559. return [null, false, 'action-state-changed'];
  1560. }
  1561. } else {
  1562. target = model.get_item_attribute_value(index, Gio.MENU_ATTRIBUTE_TARGET, null);
  1563. item = new PopupMenuItem(label);
  1564. action.items.push(item);
  1565. specificSignalId = item.connect('activate', Lang.bind(this, function() {
  1566. this.actionGroup.activate_action(action_id, target);
  1567. }));
  1568. }
  1569. item.actor.reactive = item.actor.can_focus = action.enabled;
  1570. if (action.enabled)
  1571. item.actor.remove_style_pseudo_class('insensitive');
  1572. else
  1573. item.actor.add_style_pseudo_class('insensitive');
  1574. destroyId = item.connect('destroy', Lang.bind(this, function() {
  1575. item.disconnect(destroyId);
  1576. item.disconnect(specificSignalId);
  1577. let pos = action.items.indexOf(item);
  1578. if (pos != -1)
  1579. action.items.splice(pos, 1);
  1580. }));
  1581. return [item, false, ''];
  1582. },
  1583. _modelChanged: function(model, position, removed, added, target) {
  1584. let j, k;
  1585. let j0, k0;
  1586. let currentItems = target._getMenuItems();
  1587. k0 = 0;
  1588. // skip ignored items at the beginning
  1589. while (k0 < currentItems.length && currentItems[k0]._ignored)
  1590. k0++;
  1591. // find the right menu item matching the model item
  1592. for (j0 = 0; k0 < currentItems.length && j0 < position; j0++, k0++) {
  1593. if (currentItems[k0]._ignored)
  1594. k0++;
  1595. }
  1596. if (removed == -1) {
  1597. // special flag to indicate we should destroy everything
  1598. for (k = k0; k < currentItems.length; k++)
  1599. currentItems[k].destroy();
  1600. } else {
  1601. for (j = j0, k = k0; k < currentItems.length && j < j0 + removed; j++, k++) {
  1602. currentItems[k].destroy();
  1603. if (currentItems[k]._ignored)
  1604. j--;
  1605. }
  1606. }
  1607. for (j = j0, k = k0; j < j0 + added; j++, k++) {
  1608. let [item, addSeparator, changeSignal] = this._createMenuItem(model, j);
  1609. if (item) {
  1610. // separators must be added in the parent to make autohiding work
  1611. if (addSeparator) {
  1612. let separator = new PopupSeparatorMenuItem();
  1613. item.separators.push(separator);
  1614. separator._ignored = true;
  1615. target.addMenuItem(separator, k+1);
  1616. k++;
  1617. }
  1618. target.addMenuItem(item, k);
  1619. if (addSeparator) {
  1620. let separator = new PopupSeparatorMenuItem();
  1621. item.separators.push(separator);
  1622. separator._ignored = true;
  1623. target.addMenuItem(separator, k+1);
  1624. k++;
  1625. }
  1626. } else if (changeSignal) {
  1627. let signalId = this.actionGroup.connect(changeSignal, Lang.bind(this, function(actionGroup, actionName) {
  1628. actionGroup.disconnect(signalId);
  1629. if (this._actions[actionName]) return;
  1630. // force a full update
  1631. this._modelChanged(model, 0, -1, model.get_n_items(), target);
  1632. }));
  1633. }
  1634. }
  1635. if (!model._changedId) {
  1636. model._changedId = model.connect('items-changed', Lang.bind(this, this._modelChanged, target));
  1637. model._destroyId = target.connect('destroy', function() {
  1638. if (model._changedId)
  1639. model.disconnect(model._changedId);
  1640. if (model._destroyId)
  1641. target.disconnect(model._destroyId);
  1642. model._changedId = 0;
  1643. model._destroyId = 0;
  1644. });
  1645. }
  1646. if (target instanceof PopupMenuSection) {
  1647. if (target._titleMenuItem)
  1648. target.actor.visible = target.numMenuItems != 1;
  1649. else
  1650. target.actor.visible = target.numMenuItems != 0;
  1651. } else {
  1652. let sourceItem = target.sourceActor._delegate;
  1653. if (sourceItem instanceof PopupSubMenuMenuItem)
  1654. sourceItem.actor.visible = target.numMenuItems != 0;
  1655. }
  1656. },
  1657. _actionStateChanged: function(actionGroup, action_id) {
  1658. let action = this._actions[action_id];
  1659. if (!action)
  1660. return;
  1661. action.state = actionGroup.get_action_state(action_id);
  1662. if (action.items.length) {
  1663. switch (String.fromCharCode(action.state.classify())) {
  1664. case 'b':
  1665. for (let i = 0; i < action.items.length; i++)
  1666. action.items[i].setToggleState(action.state.get_boolean());
  1667. break;
  1668. case 'd':
  1669. for (let i = 0; i < action.items.length; i++)
  1670. action.items[i].setValue(action.state.get_double());
  1671. break;
  1672. case 's':
  1673. for (let i = 0; i < action.items.length; i++)
  1674. action.items[i].setShowDot(action.items[i]._remoteTarget == action.state.deep_unpack());
  1675. }
  1676. }
  1677. },
  1678. _actionEnabledChanged: function(actionGroup, action_id) {
  1679. let action = this._actions[action_id];
  1680. if (!action)
  1681. return;
  1682. action.enabled = actionGroup.get_action_enabled(action_id);
  1683. if (action.items.length) {
  1684. for (let i = 0; i < action.items.length; i++) {
  1685. let item = action.items[i];
  1686. item.actor.reactive = item.actor.can_focus = action.enabled;
  1687. if (action.enabled)
  1688. item.actor.remove_style_pseudo_class('insensitive');
  1689. else
  1690. item.actor.add_style_pseudo_class('insensitive');
  1691. }
  1692. }
  1693. }
  1694. });
  1695. /* Basic implementation of a menu manager.
  1696. * Call addMenu to add menus
  1697. */
  1698. const PopupMenuManager = new Lang.Class({
  1699. Name: 'PopupMenuManager',
  1700. _init: function(owner) {
  1701. this._owner = owner;
  1702. this.grabbed = false;
  1703. this._eventCaptureId = 0;
  1704. this._enterEventId = 0;
  1705. this._leaveEventId = 0;
  1706. this._keyFocusNotifyId = 0;
  1707. this._activeMenu = null;
  1708. this._menus = [];
  1709. this._menuStack = [];
  1710. this._preGrabInputMode = null;
  1711. this._grabbedFromKeynav = false;
  1712. },
  1713. addMenu: function(menu, position) {
  1714. let menudata = {
  1715. menu: menu,
  1716. openStateChangeId: menu.connect('open-state-changed', Lang.bind(this, this._onMenuOpenState)),
  1717. childMenuAddedId: menu.connect('child-menu-added', Lang.bind(this, this._onChildMenuAdded)),
  1718. childMenuRemovedId: menu.connect('child-menu-removed', Lang.bind(this, this._onChildMenuRemoved)),
  1719. destroyId: menu.connect('destroy', Lang.bind(this, this._onMenuDestroy)),
  1720. enterId: 0,
  1721. focusInId: 0
  1722. };
  1723. let source = menu.sourceActor;
  1724. if (source) {
  1725. menudata.enterId = source.connect('enter-event', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
  1726. menudata.focusInId = source.connect('key-focus-in', Lang.bind(this, function() { this._onMenuSourceEnter(menu); }));
  1727. }
  1728. if (position == undefined)
  1729. this._menus.push(menudata);
  1730. else
  1731. this._menus.splice(position, 0, menudata);
  1732. },
  1733. removeMenu: function(menu) {
  1734. if (menu == this._activeMenu)
  1735. this._closeMenu();
  1736. let position = this._findMenu(menu);
  1737. if (position == -1) // not a menu we manage
  1738. return;
  1739. let menudata = this._menus[position];
  1740. menu.disconnect(menudata.openStateChangeId);
  1741. menu.disconnect(menudata.childMenuAddedId);
  1742. menu.disconnect(menudata.childMenuRemovedId);
  1743. menu.disconnect(menudata.destroyId);
  1744. if (menudata.enterId)
  1745. menu.sourceActor.disconnect(menudata.enterId);
  1746. if (menudata.focusInId)
  1747. menu.sourceActor.disconnect(menudata.focusInId);
  1748. this._menus.splice(position, 1);
  1749. },
  1750. _grab: function() {
  1751. Main.pushModal(this._owner.actor);
  1752. this._eventCaptureId = global.stage.connect('captured-event', Lang.bind(this, this._onEventCapture));
  1753. // captured-event doesn't see enter/leave events
  1754. this._enterEventId = global.stage.connect('enter-event', Lang.bind(this, this._onEventCapture));
  1755. this._leaveEventId = global.stage.connect('leave-event', Lang.bind(this, this._onEventCapture));
  1756. this._keyFocusNotifyId = global.stage.connect('notify::key-focus', Lang.bind(this, this._onKeyFocusChanged));
  1757. this.grabbed = true;
  1758. },
  1759. _ungrab: function() {
  1760. global.stage.disconnect(this._eventCaptureId);
  1761. this._eventCaptureId = 0;
  1762. global.stage.disconnect(this._enterEventId);
  1763. this._enterEventId = 0;
  1764. global.stage.disconnect(this._leaveEventId);
  1765. this._leaveEventId = 0;
  1766. global.stage.disconnect(this._keyFocusNotifyId);
  1767. this._keyFocusNotifyId = 0;
  1768. this.grabbed = false;
  1769. Main.popModal(this._owner.actor);
  1770. },
  1771. _onMenuOpenState: function(menu, open) {
  1772. if (open) {
  1773. if (this._activeMenu && this._activeMenu.isChildMenu(menu)) {
  1774. this._menuStack.push(this._activeMenu);
  1775. menu.actor.grab_key_focus();
  1776. }
  1777. this._activeMenu = menu;
  1778. } else {
  1779. if (this._menuStack.length > 0) {
  1780. this._activeMenu = this._menuStack.pop();
  1781. if (menu.sourceActor)
  1782. menu.sourceActor.grab_key_focus();
  1783. this._didPop = true;
  1784. }
  1785. }
  1786. // Check what the focus was before calling pushModal/popModal
  1787. let focus = global.stage.key_focus;
  1788. let hadFocus = focus && this._activeMenuContains(focus);
  1789. if (open) {
  1790. if (!this.grabbed) {
  1791. this._preGrabInputMode = global.stage_input_mode;
  1792. this._grabbedFromKeynav = hadFocus;
  1793. this._grab();
  1794. }
  1795. if (hadFocus)
  1796. focus.grab_key_focus();
  1797. else
  1798. menu.actor.grab_key_focus();
  1799. } else if (menu == this._activeMenu) {
  1800. if (this.grabbed)
  1801. this._ungrab();
  1802. this._activeMenu = null;
  1803. if (this._grabbedFromKeynav) {
  1804. if (this._preGrabInputMode == Shell.StageInputMode.FOCUSED)
  1805. global.stage_input_mode = Shell.StageInputMode.FOCUSED;
  1806. if (hadFocus && menu.sourceActor)
  1807. menu.sourceActor.grab_key_focus();
  1808. else if (focus)
  1809. focus.grab_key_focus();
  1810. }
  1811. }
  1812. },
  1813. _onChildMenuAdded: function(menu, childMenu) {
  1814. this.addMenu(childMenu);
  1815. },
  1816. _onChildMenuRemoved: function(menu, childMenu) {
  1817. this.removeMenu(childMenu);
  1818. },
  1819. // change the currently-open menu without dropping grab
  1820. _changeMenu: function(newMenu) {
  1821. if (this._activeMenu) {
  1822. // _onOpenMenuState will drop the grab if it sees
  1823. // this._activeMenu being closed; so clear _activeMenu
  1824. // before closing it to keep that from happening
  1825. let oldMenu = this._activeMenu;
  1826. this._activeMenu = null;
  1827. for (let i = this._menuStack.length - 1; i >= 0; i--)
  1828. this._menuStack[i].close(BoxPointer.PopupAnimation.FADE);
  1829. oldMenu.close(BoxPointer.PopupAnimation.FADE);
  1830. newMenu.open(BoxPointer.PopupAnimation.FADE);
  1831. } else
  1832. newMenu.open(BoxPointer.PopupAnimation.FULL);
  1833. },
  1834. _onMenuSourceEnter: function(menu) {
  1835. if (!this.grabbed || menu == this._activeMenu)
  1836. return false;
  1837. if (this._activeMenu && this._activeMenu.isChildMenu(menu))
  1838. return false;
  1839. if (this._menuStack.indexOf(menu) != -1)
  1840. return false;
  1841. if (this._menuStack.length > 0 && this._menuStack[0].isChildMenu(menu))
  1842. return false;
  1843. this._changeMenu(menu);
  1844. return false;
  1845. },
  1846. _onKeyFocusChanged: function() {
  1847. if (!this.grabbed || !this._activeMenu)
  1848. return;
  1849. let focus = global.stage.key_focus;
  1850. if (focus) {
  1851. if (this._activeMenuContains(focus))
  1852. return;
  1853. if (this._menuStack.length > 0)
  1854. return;
  1855. if (focus._delegate && focus._delegate.menu &&
  1856. this._findMenu(focus._delegate.menu) != -1)
  1857. return;
  1858. }
  1859. this._closeMenu();
  1860. },
  1861. _onMenuDestroy: function(menu) {
  1862. this.removeMenu(menu);
  1863. },
  1864. _activeMenuContains: function(actor) {
  1865. return this._activeMenu != null
  1866. && (this._activeMenu.actor.contains(actor) ||
  1867. (this._activeMenu.sourceActor && this._activeMenu.sourceActor.contains(actor)));
  1868. },
  1869. _eventIsOnActiveMenu: function(event) {
  1870. return this._activeMenuContains(event.get_source());
  1871. },
  1872. _shouldBlockEvent: function(event) {
  1873. let src = event.get_source();
  1874. if (this._activeMenu != null && this._activeMenu.actor.contains(src))
  1875. return false;
  1876. for (let i = 0; i < this._menus.length; i++) {
  1877. let menu = this._menus[i].menu;
  1878. if (menu.sourceActor && !menu.blockSourceEvents && menu.sourceActor.contains(src)) {
  1879. return false;
  1880. }
  1881. }
  1882. return true;
  1883. },
  1884. _findMenu: function(item) {
  1885. for (let i = 0; i < this._menus.length; i++) {
  1886. let menudata = this._menus[i];
  1887. if (item == menudata.menu)
  1888. return i;
  1889. }
  1890. return -1;
  1891. },
  1892. _onEventCapture: function(actor, event) {
  1893. if (!this.grabbed)
  1894. return false;
  1895. if (this._owner.menuEventFilter &&
  1896. this._owner.menuEventFilter(event))
  1897. return true;
  1898. if (this._activeMenu != null && this._activeMenu.passEvents)
  1899. return false;
  1900. if (this._didPop) {
  1901. this._didPop = false;
  1902. return true;
  1903. }
  1904. let activeMenuContains = this._eventIsOnActiveMenu(event);
  1905. let eventType = event.type();
  1906. if (eventType == Clutter.EventType.BUTTON_RELEASE) {
  1907. if (activeMenuContains) {
  1908. return false;
  1909. } else {
  1910. this._closeMenu();
  1911. return true;
  1912. }
  1913. } else if (eventType == Clutter.EventType.BUTTON_PRESS && !activeMenuContains) {
  1914. this._closeMenu();
  1915. return true;
  1916. } else if (!this._shouldBlockEvent(event)) {
  1917. return false;
  1918. }
  1919. return true;
  1920. },
  1921. _closeMenu: function() {
  1922. if (this._activeMenu != null)
  1923. this._activeMenu.close(BoxPointer.PopupAnimation.FULL);
  1924. }
  1925. });