PageRenderTime 31ms CodeModel.GetById 18ms 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

Large files files are truncated, but you can click here to view the full file

  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);

Large files files are truncated, but you can click here to view the full file