Advertisement
Guest User

Untitled

a guest
Sep 28th, 2015
186
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 73.39 KB | None | 0 0
  1. // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
  2.  
  3. const Clutter = imports.gi.Clutter;
  4. const GLib = imports.gi.GLib;
  5. const Gio = imports.gi.Gio;
  6. const Gtk = imports.gi.Gtk;
  7. const Atk = imports.gi.Atk;
  8. const Lang = imports.lang;
  9. const Mainloop = imports.mainloop;
  10. const Meta = imports.gi.Meta;
  11. const Pango = imports.gi.Pango;
  12. const Cinnamon = imports.gi.Cinnamon;
  13. const Signals = imports.signals;
  14. const St = imports.gi.St;
  15.  
  16. const GnomeSession = imports.misc.gnomeSession;
  17. const Main = imports.ui.main;
  18. const PopupMenu = imports.ui.popupMenu;
  19. const Params = imports.misc.params;
  20. const Tweener = imports.ui.tweener;
  21. const Util = imports.misc.util;
  22. const AppletManager = imports.ui.appletManager;
  23.  
  24. const ANIMATION_TIME = .2;
  25. const NOTIFICATION_TIMEOUT = 4;
  26. const NOTIFICATION_CRITICAL_TIMEOUT_WITH_APPLET = 10;
  27. const SUMMARY_TIMEOUT = 1;
  28. const LONGER_SUMMARY_TIMEOUT = 4;
  29.  
  30. const HIDE_TIMEOUT = 0.2;
  31. const LONGER_HIDE_TIMEOUT = 0.6;
  32.  
  33. const MAX_SOURCE_TITLE_WIDTH = 180;
  34.  
  35.  
  36. // We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
  37. // range from the point where it left the tray.
  38. const MOUSE_LEFT_ACTOR_THRESHOLD = 20;
  39.  
  40. const State = {
  41. HIDDEN: 0,
  42. SHOWING: 1,
  43. SHOWN: 2,
  44. HIDING: 3
  45. };
  46.  
  47. // These reasons are useful when we destroy the notifications received through
  48. // the notification daemon. We use EXPIRED for transient notifications that the
  49. // user did not interact with, DISMISSED for all other notifications that were
  50. // destroyed as a result of a user action, and SOURCE_CLOSED for the notifications
  51. // that were requested to be destroyed by the associated source.
  52. const NotificationDestroyedReason = {
  53. EXPIRED: 1,
  54. DISMISSED: 2,
  55. SOURCE_CLOSED: 3
  56. };
  57.  
  58. // Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
  59. // urgency values map to the corresponding values for the notifications received
  60. // through the notification daemon. HIGH urgency value is used for chats received
  61. // through the Telepathy client.
  62. const Urgency = {
  63. LOW: 0,
  64. NORMAL: 1,
  65. HIGH: 2,
  66. CRITICAL: 3
  67. }
  68.  
  69. function _fixMarkup(text, allowMarkup) {
  70. if (allowMarkup) {
  71. // Support &, ", ', < and >, escape all other
  72. // occurrences of '&'.
  73. let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&');
  74.  
  75. // Support <b>, <i>, and <u>, escape anything else
  76. // so it displays as raw markup.
  77. _text = _text.replace(/<(?!\/?[biu]>)/g, '&lt;');
  78.  
  79. try {
  80. Pango.parse_markup(_text, -1, '');
  81. return _text;
  82. } catch (e) {}
  83. }
  84.  
  85. // !allowMarkup, or invalid markup
  86. return GLib.markup_escape_text(text, -1);
  87. }
  88.  
  89. function URLHighlighter(text, lineWrap, allowMarkup) {
  90. this._init(text, lineWrap, allowMarkup);
  91. }
  92.  
  93. URLHighlighter.prototype = {
  94. _init: function(text, lineWrap, allowMarkup) {
  95. if (!text)
  96. text = '';
  97. this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' });
  98. this._linkColor = '#ccccff';
  99. this.actor.connect('style-changed', Lang.bind(this, function() {
  100. let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false);
  101. if (hasColor) {
  102. let linkColor = color.to_string().substr(0, 7);
  103. if (linkColor != this._linkColor) {
  104. this._linkColor = linkColor;
  105. this._highlightUrls();
  106. }
  107. }
  108. }));
  109. if (lineWrap) {
  110. this.actor.clutter_text.line_wrap = true;
  111. this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
  112. this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
  113. }
  114.  
  115. this.setMarkup(text, allowMarkup);
  116. this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) {
  117. // Don't try to URL highlight when invisible.
  118. // The MessageTray doesn't actually hide us, so
  119. // we need to check for paint opacities as well.
  120. if (!actor.visible || actor.get_paint_opacity() == 0)
  121. return false;
  122.  
  123. // Keep Notification.actor from seeing this and taking
  124. // a pointer grab, which would block our button-release-event
  125. // handler, if an URL is clicked
  126. return this._findUrlAtPos(event) != -1;
  127. }));
  128. this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) {
  129. if (!actor.visible || actor.get_paint_opacity() == 0)
  130. return false;
  131.  
  132. let urlId = this._findUrlAtPos(event);
  133. if (urlId != -1) {
  134. let url = this._urls[urlId].url;
  135. if (url.indexOf(':') == -1)
  136. url = 'http://' + url;
  137. try {
  138. Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context());
  139. return true;
  140. } catch (e) {
  141. // TODO: remove this after gnome 3 release
  142. Util.spawn(['gvfs-open', url]);
  143. return true;
  144. }
  145. }
  146. return false;
  147. }));
  148. this.actor.connect('motion-event', Lang.bind(this, function(actor, event) {
  149. if (!actor.visible || actor.get_paint_opacity() == 0)
  150. return false;
  151.  
  152. let urlId = this._findUrlAtPos(event);
  153. if (urlId != -1 && !this._cursorChanged) {
  154. global.set_cursor(Cinnamon.Cursor.POINTING_HAND);
  155. this._cursorChanged = true;
  156. } else if (urlId == -1) {
  157. global.unset_cursor();
  158. this._cursorChanged = false;
  159. }
  160. return false;
  161. }));
  162. this.actor.connect('leave-event', Lang.bind(this, function() {
  163. if (!this.actor.visible || this.actor.get_paint_opacity() == 0)
  164. return;
  165.  
  166. if (this._cursorChanged) {
  167. this._cursorChanged = false;
  168. global.unset_cursor();
  169. }
  170. }));
  171. },
  172.  
  173. setMarkup: function(text, allowMarkup) {
  174. text = text ? _fixMarkup(text, allowMarkup) : '';
  175. this._text = text;
  176.  
  177. this.actor.clutter_text.set_markup(text);
  178. /* clutter_text.text contain text without markup */
  179. this._urls = Util.findUrls(this.actor.clutter_text.text);
  180. this._highlightUrls();
  181. },
  182.  
  183. _highlightUrls: function() {
  184. // text here contain markup
  185. let urls = Util.findUrls(this._text);
  186. let markup = '';
  187. let pos = 0;
  188. for (let i = 0; i < urls.length; i++) {
  189. let url = urls[i];
  190. let str = this._text.substr(pos, url.pos - pos);
  191. markup += str + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>';
  192. pos = url.pos + url.url.length;
  193. }
  194. markup += this._text.substr(pos);
  195. this.actor.clutter_text.set_markup(markup);
  196. },
  197.  
  198. _findUrlAtPos: function(event) {
  199. let success;
  200. let [x, y] = event.get_coords();
  201. [success, x, y] = this.actor.transform_stage_point(x, y);
  202. let find_pos = -1;
  203. for (let i = 0; i < this.actor.clutter_text.text.length; i++) {
  204. let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i);
  205. if (py > y || py + line_height < y || x < px)
  206. continue;
  207. find_pos = i;
  208. }
  209. if (find_pos != -1) {
  210. for (let i = 0; i < this._urls.length; i++)
  211. if (find_pos >= this._urls[i].pos &&
  212. this._urls[i].pos + this._urls[i].url.length > find_pos)
  213. return i;
  214. }
  215. return -1;
  216. }
  217. };
  218.  
  219. function FocusGrabber() {
  220. this._init();
  221. }
  222.  
  223. FocusGrabber.prototype = {
  224. _init: function() {
  225. this.actor = null;
  226.  
  227. this._hasFocus = false;
  228. // We use this._prevFocusedWindow and this._prevKeyFocusActor to return the
  229. // focus where it previously belonged after a focus grab, unless the user
  230. // has explicitly changed that.
  231. this._prevFocusedWindow = null;
  232. this._prevKeyFocusActor = null;
  233.  
  234. this._focusActorChangedId = 0;
  235. this._stageInputModeChangedId = 0;
  236. this._capturedEventId = 0;
  237. this._togglingFocusGrabMode = false;
  238.  
  239. Main.overview.connect('showing', Lang.bind(this,
  240. function() {
  241. this._toggleFocusGrabMode();
  242. }));
  243. Main.overview.connect('hidden', Lang.bind(this,
  244. function() {
  245. this._toggleFocusGrabMode();
  246. }));
  247. Main.expo.connect('showing', Lang.bind(this,
  248. function() {
  249. this._toggleFocusGrabMode();
  250. }));
  251. Main.expo.connect('hidden', Lang.bind(this,
  252. function() {
  253. this._toggleFocusGrabMode();
  254. }));
  255. },
  256.  
  257. grabFocus: function(actor) {
  258. if (this._hasFocus)
  259. return;
  260.  
  261. this.actor = actor;
  262.  
  263. this._prevFocusedWindow = global.display.focus_window;
  264. this._prevKeyFocusActor = global.stage.get_key_focus();
  265.  
  266. if (global.stage_input_mode == Cinnamon.StageInputMode.NONREACTIVE ||
  267. global.stage_input_mode == Cinnamon.StageInputMode.NORMAL)
  268. global.set_stage_input_mode(Cinnamon.StageInputMode.FOCUSED);
  269.  
  270. // Use captured-event to notice clicks outside the focused actor
  271. // without consuming them.
  272. this._capturedEventId = global.stage.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
  273.  
  274. this._stageInputModeChangedId = global.connect('notify::stage-input-mode', Lang.bind(this, this._stageInputModeChanged));
  275. this._focusActorChangedId = global.stage.connect('notify::key-focus', Lang.bind(this, this._focusActorChanged));
  276.  
  277. this._hasFocus = true;
  278.  
  279. this.actor.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
  280. this.emit('focus-grabbed');
  281. },
  282.  
  283. _focusActorChanged: function() {
  284. let focusedActor = global.stage.get_key_focus();
  285. if (!focusedActor || !this.actor.contains(focusedActor)) {
  286. this._prevKeyFocusActor = null;
  287. this.ungrabFocus();
  288. }
  289. },
  290.  
  291. _stageInputModeChanged: function() {
  292. this.ungrabFocus();
  293. },
  294.  
  295. _onCapturedEvent: function(actor, event) {
  296. let source = event.get_source();
  297. switch (event.type()) {
  298. case Clutter.EventType.BUTTON_PRESS:
  299. if (!this.actor.contains(source) &&
  300. !Main.layoutManager.keyboardBox.contains(source))
  301. this.emit('button-pressed', source);
  302. break;
  303. case Clutter.EventType.KEY_PRESS:
  304. let symbol = event.get_key_symbol();
  305. if (symbol == Clutter.Escape) {
  306. this.emit('escape-pressed');
  307. return true;
  308. }
  309. break;
  310. }
  311.  
  312. return false;
  313. },
  314.  
  315. ungrabFocus: function() {
  316. if (!this._hasFocus)
  317. return;
  318.  
  319. if (this._focusActorChangedId > 0) {
  320. global.stage.disconnect(this._focusActorChangedId);
  321. this._focusActorChangedId = 0;
  322. }
  323.  
  324. if (this._stageInputModeChangedId) {
  325. global.disconnect(this._stageInputModeChangedId);
  326. this._stageInputModeChangedId = 0;
  327. }
  328.  
  329. if (this._capturedEventId > 0) {
  330. global.stage.disconnect(this._capturedEventId);
  331. this._capturedEventId = 0;
  332. }
  333.  
  334. this._hasFocus = false;
  335. this.emit('focus-ungrabbed');
  336.  
  337. if (this._prevFocusedWindow && !global.display.focus_window) {
  338. global.display.set_input_focus_window(this._prevFocusedWindow, false, global.get_current_time());
  339. this._prevFocusedWindow = null;
  340. }
  341. if (this._prevKeyFocusActor) {
  342. global.stage.set_key_focus(this._prevKeyFocusActor);
  343. this._prevKeyFocusActor = null;
  344. } else {
  345. // We don't want to keep any actor inside the previously focused actor focused.
  346. let focusedActor = global.stage.get_key_focus();
  347. if (focusedActor && this.actor.contains(focusedActor))
  348. global.stage.set_key_focus(null);
  349. }
  350. if (!this._togglingFocusGrabMode)
  351. this.actor = null;
  352. },
  353.  
  354. // Because we grab focus differently in the overview
  355. // and in the main view, we need to change how it is
  356. // done when we move between the two.
  357. _toggleFocusGrabMode: function() {
  358. if (this._hasFocus) {
  359. this._togglingFocusGrabMode = true;
  360. this.ungrabFocus();
  361. this.grabFocus(this.actor);
  362. this._togglingFocusGrabMode = false;
  363. }
  364. }
  365. }
  366. Signals.addSignalMethods(FocusGrabber.prototype);
  367.  
  368. // Notification:
  369. // @source: the notification's Source
  370. // @title: the title
  371. // @banner: the banner text
  372. // @params: optional additional params
  373. //
  374. // Creates a notification. In the banner mode, the notification
  375. // will show an icon, @title (in bold) and @banner, all on a single
  376. // line (with @banner ellipsized if necessary).
  377. //
  378. // The notification will be expandable if either it has additional
  379. // elements that were added to it or if the @banner text did not
  380. // fit fully in the banner mode. When the notification is expanded,
  381. // the @banner text from the top line is always removed. The complete
  382. // @banner text is added as the first element in the content section,
  383. // unless 'customContent' parameter with the value 'true' is specified
  384. // in @params.
  385. //
  386. // Additional notification content can be added with addActor() and
  387. // addBody() methods. The notification content is put inside a
  388. // scrollview, so if it gets too tall, the notification will scroll
  389. // rather than continue to grow. In addition to this main content
  390. // area, there is also a single-row action area, which is not
  391. // scrolled and can contain a single actor. The action area can
  392. // be set by calling setActionArea() method. There is also a
  393. // convenience method addButton() for adding a button to the action
  394. // area.
  395. //
  396. // @params can contain values for 'customContent', 'body', 'icon',
  397. // 'titleMarkup', 'bannerMarkup', 'bodyMarkup', and 'clear'
  398. // parameters.
  399. //
  400. // If @params contains a 'customContent' parameter with the value %true,
  401. // then @banner will not be shown in the body of the notification when the
  402. // notification is expanded and calls to update() will not clear the content
  403. // unless 'clear' parameter with value %true is explicitly specified.
  404. //
  405. // If @params contains a 'body' parameter, then that text will be added to
  406. // the content area (as with addBody()).
  407. //
  408. // By default, the icon shown is created by calling
  409. // source.createNotificationIcon(). However, if @params contains an 'icon'
  410. // parameter, the passed in icon will be used.
  411. //
  412. // If @params contains a 'titleMarkup', 'bannerMarkup', or
  413. // 'bodyMarkup' parameter with the value %true, then the corresponding
  414. // element is assumed to use pango markup. If the parameter is not
  415. // present for an element, then anything that looks like markup in
  416. // that element will appear literally in the output.
  417. //
  418. // If @params contains a 'clear' parameter with the value %true, then
  419. // the content and the action area of the notification will be cleared.
  420. // The content area is also always cleared if 'customContent' is false
  421. // because it might contain the @banner that didn't fit in the banner mode.
  422. function Notification(source, title, banner, params) {
  423. this._init(source, title, banner, params);
  424. }
  425.  
  426. Notification.prototype = {
  427. IMAGE_SIZE: 125,
  428.  
  429. _init: function(source, title, banner, params) {
  430. this.source = source;
  431. this.title = title;
  432. this.urgency = Urgency.NORMAL;
  433. this.resident = false;
  434. // 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
  435. this.isTransient = false;
  436. this.expanded = false;
  437. this._destroyed = false;
  438. this._useActionIcons = false;
  439. this._customContent = false;
  440. this._bannerBodyText = null;
  441. this._bannerBodyMarkup = false;
  442. this._titleFitsInBannerMode = true;
  443. this._titleDirection = St.TextDirection.NONE;
  444. this._spacing = 0;
  445.  
  446. this._imageBin = null;
  447. this._timestamp = new Date();
  448. this._inNotificationBin = false;
  449. this.dateFormat = _("%l:%M %p");
  450.  
  451. this.enter_id = 0;
  452. this.leave_id = 0;
  453.  
  454. source.connect('destroy', Lang.bind(this,
  455. function (source, reason) {
  456. this.destroy(reason);
  457. }));
  458.  
  459. this.actor = new St.Button({ accessible_role: Atk.Role.NOTIFICATION });
  460. this.actor._delegate = this;
  461. this.actor._parent_container = null;
  462. this.actor.connect('clicked', Lang.bind(this, this._onClicked));
  463. this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
  464. // Transparency on mouse over?
  465. if (Main.messageTray.fadeOnMouseover) {
  466. // Register to every notification as we intend to support multiple notifications on screen.
  467. this.enter_id = this.actor.connect('enter-event', Lang.bind(this, function() {
  468. Tweener.addTween(this.actor, {
  469. opacity: ((Main.messageTray.fadeOpacity / 100) * 255).clamp(0, 255),
  470. time: ANIMATION_TIME,
  471. transition: 'easeOutQuad'
  472. });
  473. }));
  474. this.leave_id = this.actor.connect('leave-event', Lang.bind(this, function() {
  475. Tweener.addTween(this.actor, {
  476. opacity: (this._table.get_theme_node().get_length('opacity') / global.ui_scale) || 255,
  477. time: ANIMATION_TIME,
  478. transition: 'easeOutQuad'
  479. });
  480. }));
  481. }
  482.  
  483. this._table = new St.Table({ name: 'notification',
  484. reactive: true });
  485. this._table.connect('style-changed', Lang.bind(this, this._styleChanged));
  486. this.actor.set_child(this._table);
  487.  
  488. this._buttonFocusManager = St.FocusManager.get_for_stage(global.stage);
  489.  
  490. // The first line should have the title, followed by the
  491. // banner text, but ellipsized if they won't both fit. We can't
  492. // make St.Table or St.BoxLayout do this the way we want (don't
  493. // show banner at all if title needs to be ellipsized), so we
  494. // use Cinnamon.GenericContainer.
  495. this._bannerBox = new Cinnamon.GenericContainer();
  496. this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
  497. this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
  498. this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
  499. this._table.add(this._bannerBox, { row: 0,
  500. col: 1,
  501. col_span: 2,
  502. x_expand: false,
  503. y_expand: false,
  504. y_fill: false });
  505.  
  506. // This is an empty cell that overlaps with this._bannerBox cell to ensure
  507. // that this._bannerBox cell expands horizontally, while not forcing the
  508. // this._imageBin that is also in col: 2 to expand horizontally.
  509. this._table.add(new St.Bin(), { row: 0,
  510. col: 2,
  511. y_expand: false,
  512. y_fill: false });
  513. this._timeLabel = new St.Label();
  514. this._titleLabel = new St.Label();
  515. this._bannerBox.add_actor(this._titleLabel);
  516. this._bannerBox.add_actor(this._timeLabel);
  517. this._timeLabel.hide();
  518. this._bannerUrlHighlighter = new URLHighlighter();
  519. this._bannerLabel = this._bannerUrlHighlighter.actor;
  520. this._bannerBox.add_actor(this._bannerLabel);
  521.  
  522. this.update(title, banner, params);
  523. },
  524.  
  525. // update:
  526. // @title: the new title
  527. // @banner: the new banner
  528. // @params: as in the Notification constructor
  529. //
  530. // Updates the notification by regenerating its icon and updating
  531. // the title/banner. If @params.clear is %true, it will also
  532. // remove any additional actors/action buttons previously added.
  533. update: function(title, banner, params) {
  534. this._timestamp = new Date();
  535. this._inNotificationBin = false;
  536. params = Params.parse(params, { customContent: false,
  537. body: null,
  538. icon: null,
  539. titleMarkup: false,
  540. bannerMarkup: false,
  541. bodyMarkup: false,
  542. clear: false });
  543.  
  544. this._customContent = params.customContent;
  545.  
  546. let oldFocus = global.stage.key_focus;
  547.  
  548. if (this._icon && (params.icon || params.clear)) {
  549. this._icon.destroy();
  550. this._icon = null;
  551. }
  552.  
  553. // We always clear the content area if we don't have custom
  554. // content because it might contain the @banner that didn't
  555. // fit in the banner mode.
  556. if (this._scrollArea && (!this._customContent || params.clear)) {
  557. if (oldFocus && this._scrollArea.contains(oldFocus))
  558. this.actor.grab_key_focus();
  559.  
  560. this._scrollArea.destroy();
  561. this._scrollArea = null;
  562. this._contentArea = null;
  563. }
  564. if (this._actionArea && params.clear) {
  565. if (oldFocus && this._actionArea.contains(oldFocus))
  566. this.actor.grab_key_focus();
  567.  
  568. this._actionArea.destroy();
  569. this._actionArea = null;
  570. this._buttonBox = null;
  571. }
  572. if (this._imageBin && params.clear)
  573. this.unsetImage();
  574.  
  575. if (!this._scrollArea && !this._actionArea && !this._imageBin)
  576. this._table.remove_style_class_name('multi-line-notification');
  577.  
  578. if (!this._icon) {
  579. this._icon = params.icon || this.source.createNotificationIcon();
  580. this._table.add(this._icon, { row: 0,
  581. col: 0,
  582. x_expand: false,
  583. y_expand: false,
  584. y_fill: false,
  585. y_align: St.Align.START });
  586. }
  587.  
  588. this.title = title;
  589. title = title ? _fixMarkup(title.replace(/\n/g, ' '), params.titleMarkup) : '';
  590. this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>');
  591. this._timeLabel.clutter_text.set_markup(this._timestamp.toLocaleTimeString(this.dateFormat));
  592. this._timeLabel.hide();
  593. if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL)
  594. this._titleDirection = St.TextDirection.RTL;
  595. else
  596. this._titleDirection = St.TextDirection.LTR;
  597.  
  598. // Let the title's text direction control the overall direction
  599. // of the notification - in case where different scripts are used
  600. // in the notification, this is the right thing for the icon, and
  601. // arguably for action buttons as well. Labels other than the title
  602. // will be allocated at the available width, so that their alignment
  603. // is done correctly automatically.
  604. this._table.set_direction(this._titleDirection);
  605.  
  606. // Unless the notification has custom content, we save this._bannerBodyText
  607. // to add it to the content of the notification if the notification is
  608. // expandable due to other elements in its content area or due to the banner
  609. // not fitting fully in the single-line mode.
  610. this._bannerBodyText = this._customContent ? null : banner;
  611. this._bannerBodyMarkup = params.bannerMarkup;
  612.  
  613. banner = banner ? banner.replace(/\n/g, ' ') : '';
  614.  
  615. this._bannerUrlHighlighter.setMarkup(banner, params.bannerMarkup);
  616. this._bannerLabel.queue_relayout();
  617.  
  618. // Add the bannerBody now if we know for sure we'll need it
  619. if (this._bannerBodyText && this._bannerBodyText.indexOf('\n') > -1)
  620. this._addBannerBody();
  621.  
  622. if (params.body)
  623. this.addBody(params.body, params.bodyMarkup);
  624. this._updated();
  625. },
  626.  
  627. setIconVisible: function(visible) {
  628. this._icon.visible = visible;
  629. },
  630.  
  631. _createScrollArea: function() {
  632. this._table.add_style_class_name('multi-line-notification');
  633. this._scrollArea = new St.ScrollView({ name: 'notification-scrollview',
  634. vscrollbar_policy: Gtk.PolicyType.NEVER,
  635. hscrollbar_policy: Gtk.PolicyType.NEVER,
  636. style_class: 'vfade' });
  637. this._table.add(this._scrollArea, { row: 1,
  638. col: 2 });
  639. this._updateLastColumnSettings();
  640. this._contentArea = new St.BoxLayout({ name: 'notification-body',
  641. vertical: true });
  642. this._scrollArea.add_actor(this._contentArea);
  643. // If we know the notification will be expandable, we need to add
  644. // the banner text to the body as the first element.
  645. this._addBannerBody();
  646. },
  647.  
  648. // addActor:
  649. // @actor: actor to add to the body of the notification
  650. //
  651. // Appends @actor to the notification's body
  652. addActor: function(actor, style) {
  653. if (!this._scrollArea) {
  654. this._createScrollArea();
  655. }
  656.  
  657. this._contentArea.add(actor, style ? style : {});
  658. this._updated();
  659. },
  660.  
  661. // addBody:
  662. // @text: the text
  663. // @markup: %true if @text contains pango markup
  664. // @style: style to use when adding the actor containing the text
  665. //
  666. // Adds a multi-line label containing @text to the notification.
  667. //
  668. // Return value: the newly-added label
  669. addBody: function(text, markup, style) {
  670. let label = new URLHighlighter(text, true, markup);
  671.  
  672. this.addActor(label.actor, style);
  673. return label.actor;
  674. },
  675.  
  676. _addBannerBody: function() {
  677. if (this._bannerBodyText) {
  678. let text = this._bannerBodyText;
  679. this._bannerBodyText = null;
  680. this.addBody(text, this._bannerBodyMarkup);
  681. }
  682. },
  683.  
  684. // scrollTo:
  685. // @side: St.Side.TOP or St.Side.BOTTOM
  686. //
  687. // Scrolls the content area (if scrollable) to the indicated edge
  688. scrollTo: function(side) {
  689. let adjustment = this._scrollArea.vscroll.adjustment;
  690. if (side == St.Side.TOP)
  691. adjustment.value = adjustment.lower;
  692. else if (side == St.Side.BOTTOM)
  693. adjustment.value = adjustment.upper;
  694. },
  695.  
  696. // setActionArea:
  697. // @actor: the actor
  698. // @props: (option) St.Table child properties
  699. //
  700. // Puts @actor into the action area of the notification, replacing
  701. // the previous contents
  702. setActionArea: function(actor, props) {
  703. if (this._actionArea) {
  704. this._actionArea.destroy();
  705. this._actionArea = null;
  706. if (this._buttonBox)
  707. this._buttonBox = null;
  708. } else {
  709. this._addBannerBody();
  710. }
  711. this._actionArea = actor;
  712.  
  713. if (!props)
  714. props = {};
  715. props.row = 2;
  716. props.col = 2;
  717.  
  718. this._table.add_style_class_name('multi-line-notification');
  719. this._table.add(this._actionArea, props);
  720. this._updateLastColumnSettings();
  721. this._updated();
  722. },
  723.  
  724. _updateLastColumnSettings: function() {
  725. if (this._scrollArea)
  726. this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1,
  727. col_span: this._imageBin ? 1 : 2 });
  728. if (this._actionArea)
  729. this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1,
  730. col_span: this._imageBin ? 1 : 2 });
  731. },
  732.  
  733. setImage: function(image) {
  734. if (this._imageBin)
  735. this.unsetImage();
  736. this._imageBin = new St.Bin();
  737. this._imageBin.child = image;
  738. this._imageBin.opacity = 230;
  739. this._table.add_style_class_name('multi-line-notification');
  740. this._table.add_style_class_name('notification-with-image');
  741. this._addBannerBody();
  742. this._updateLastColumnSettings();
  743. this._table.add(this._imageBin, { row: 1,
  744. col: 1,
  745. row_span: 2,
  746. x_expand: false,
  747. y_expand: false,
  748. x_fill: false,
  749. y_fill: false });
  750. },
  751.  
  752. unsetImage: function() {
  753. if (this._imageBin) {
  754. this._table.remove_style_class_name('notification-with-image');
  755. this._table.remove_actor(this._imageBin);
  756. this._imageBin = null;
  757. this._updateLastColumnSettings();
  758. if (!this._scrollArea && !this._actionArea)
  759. this._table.remove_style_class_name('multi-line-notification');
  760. }
  761. },
  762.  
  763. // addButton:
  764. // @id: the action ID
  765. // @label: the label for the action's button
  766. //
  767. // Adds a button with the given @label to the notification. All
  768. // action buttons will appear in a single row at the bottom of
  769. // the notification.
  770. //
  771. // If the button is clicked, the notification will emit the
  772. // %action-invoked signal with @id as a parameter
  773. addButton: function(id, label) {
  774. if (!this._buttonBox) {
  775.  
  776. let box = new St.BoxLayout({ name: 'notification-actions' });
  777. this.setActionArea(box, { x_expand: true,
  778. y_expand: false,
  779. x_fill: true,
  780. y_fill: false,
  781. x_align: St.Align.START });
  782. this._buttonBox = box;
  783. }
  784.  
  785. let button = new St.Button({ can_focus: true });
  786.  
  787. if (this._useActionIcons && Gtk.IconTheme.get_default().has_icon(id)) {
  788. button.add_style_class_name('notification-icon-button');
  789. button.child = new St.Icon({ icon_name: id });
  790. } else {
  791. button.add_style_class_name('notification-button');
  792. button.label = label;
  793. }
  794.  
  795. if (this._buttonBox.get_children().length > 0)
  796. this._buttonFocusManager.remove_group(this._buttonBox);
  797.  
  798. this._buttonBox.add(button);
  799. this._buttonFocusManager.add_group(this._buttonBox);
  800. button.connect('clicked', Lang.bind(this, this._onActionInvoked, id));
  801.  
  802. this._updated();
  803. },
  804.  
  805. setUrgency: function(urgency) {
  806. this.urgency = urgency;
  807. },
  808.  
  809. setResident: function(resident) {
  810. this.resident = resident;
  811. },
  812.  
  813. setTransient: function(isTransient) {
  814. this.isTransient = isTransient;
  815. },
  816.  
  817. setUseActionIcons: function(useIcons) {
  818. this._useActionIcons = useIcons;
  819. },
  820.  
  821. _styleChanged: function() {
  822. this._spacing = this._table.get_theme_node().get_length('spacing-columns');
  823. },
  824.  
  825. _bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
  826. let [titleMin, titleNat] = this._titleLabel.get_preferred_width(forHeight);
  827. let [bannerMin, bannerNat] = this._bannerLabel.get_preferred_width(forHeight);
  828. let [timeMin, timeNat] = this._timeLabel.get_preferred_width(forHeight);
  829. if (this._inNotificationBin) {
  830. alloc.min_size = Math.max(titleMin, timeMin);
  831. alloc.natural_size = Math.max(titleNat, timeNat) + this._spacing + bannerNat;
  832. } else {
  833. alloc.min_size = titleMin;
  834. alloc.natural_size = titleNat + this._spacing + bannerNat;
  835. }
  836. },
  837.  
  838. _bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
  839. if (this._inNotificationBin) {
  840. let [titleMin, titleNat] = this._titleLabel.get_preferred_height(forWidth);
  841. let [timeMin, timeNat] = this._timeLabel.get_preferred_height(forWidth);
  842. alloc.min_size = titleMin + timeMin;
  843. alloc.natural_size = titleNat + timeNat;
  844. } else {
  845. [alloc.min_size, alloc.natural_size] =
  846. this._titleLabel.get_preferred_height(forWidth);
  847. }
  848. },
  849.  
  850. _bannerBoxAllocate: function(actor, box, flags) {
  851. let availWidth = box.x2 - box.x1;
  852.  
  853. let [titleMinW, titleNatW] = this._titleLabel.get_preferred_width(-1);
  854. let [titleMinH, titleNatH] = this._titleLabel.get_preferred_height(availWidth);
  855.  
  856. let [timeMinW, timeNatW] = this._timeLabel.get_preferred_width(-1);
  857. let [timeMinH, timeNatH] = this._timeLabel.get_preferred_height(availWidth);
  858.  
  859. let [bannerMinW, bannerNatW] = this._bannerLabel.get_preferred_width(availWidth);
  860.  
  861. let titleBox = new Clutter.ActorBox();
  862. let timeBox = new Clutter.ActorBox();
  863. let titleBoxW = Math.min(titleNatW, availWidth);
  864. let timeBoxW = Math.min(timeNatW, availWidth);
  865. if (this._titleDirection == St.TextDirection.RTL) {
  866. titleBox.x1 = availWidth - titleBoxW;
  867. titleBox.x2 = availWidth;
  868. timeBox.x1 = availWidth - timeBoxW;
  869. timeBox.x2 = availWidth;
  870. } else {
  871. titleBox.x1 = 0;
  872. timeBox.x1 = 0;
  873. titleBox.x2 = titleBoxW;
  874. timeBox.x2 = timeBoxW;
  875. }
  876. if (this._inNotificationBin) {
  877. timeBox.y1 = 0;
  878. timeBox.y2 = timeNatH;
  879. titleBox.y1 = timeNatH;
  880. titleBox.y2 = timeNatH + titleNatH;
  881. } else {
  882. titleBox.y1 = 0;
  883. titleBox.y2 = titleNatH;
  884. }
  885.  
  886. this._titleLabel.allocate(titleBox, flags);
  887. if (this._inNotificationBin) {
  888. this._timeLabel.allocate(timeBox, flags);
  889. }
  890. this._titleFitsInBannerMode = (titleNatW <= availWidth);
  891.  
  892. let bannerFits = true;
  893.  
  894. if (titleBoxW + this._spacing > availWidth) {
  895. this._bannerLabel.opacity = 0;
  896. bannerFits = false;
  897. } else {
  898. let bannerBox = new Clutter.ActorBox();
  899.  
  900. if (this._titleDirection == St.TextDirection.RTL) {
  901. bannerBox.x1 = 0;
  902. bannerBox.x2 = titleBox.x1 - this._spacing;
  903.  
  904. bannerFits = (bannerBox.x2 - bannerNatW >= 0);
  905. } else {
  906. bannerBox.x1 = titleBox.x2 + this._spacing;
  907. bannerBox.x2 = availWidth;
  908.  
  909. bannerFits = (bannerBox.x1 + bannerNatW <= availWidth);
  910. }
  911. if (this._inNotificationBin) {
  912. bannerBox.y1 = timeNatH;
  913. bannerBox.y2 = timeNatH + titleNatH;
  914. } else {
  915. bannerBox.y1 = 0;
  916. bannerBox.y2 = titleNatH;
  917. }
  918. this._bannerLabel.allocate(bannerBox, flags);
  919.  
  920. // Make _bannerLabel visible if the entire notification
  921. // fits on one line, or if the notification is currently
  922. // unexpanded and only showing one line anyway.
  923. if (!this.expanded || (bannerFits && this._table.row_count == 1))
  924. this._bannerLabel.opacity = 255;
  925. }
  926.  
  927. // If the banner doesn't fully fit in the banner box, we possibly need to add the
  928. // banner to the body. We can't do that from here though since that will force a
  929. // relayout, so we add it to the main loop.
  930. if (!bannerFits && this._canExpandContent())
  931. Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
  932. Lang.bind(this,
  933. function() {
  934. if (this._canExpandContent()) {
  935. this._addBannerBody();
  936. this._table.add_style_class_name('multi-line-notification');
  937. this._updated();
  938. }
  939. return false;
  940. }));
  941. },
  942.  
  943. _canExpandContent: function() {
  944. return this._bannerBodyText ||
  945. (!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification'));
  946. },
  947.  
  948. _updated: function() {
  949. if (this.expanded)
  950. this.expand(false);
  951. },
  952.  
  953. expand: function(animate) {
  954. this.expanded = true;
  955. // The banner is never shown when the title did not fit, so this
  956. // can be an if-else statement.
  957. if (!this._titleFitsInBannerMode) {
  958. // Remove ellipsization from the title label and make it wrap so that
  959. // we show the full title when the notification is expanded.
  960. this._titleLabel.clutter_text.line_wrap = true;
  961. this._titleLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
  962. this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
  963. } else if (this._table.row_count > 1 && this._bannerLabel.opacity != 0) {
  964. // We always hide the banner if the notification has additional content.
  965. //
  966. // We don't need to wrap the banner that doesn't fit the way we wrap the
  967. // title that doesn't fit because we won't have a notification with
  968. // row_count=1 that has a banner that doesn't fully fit. We'll either add
  969. // that banner to the content of the notification in _bannerBoxAllocate()
  970. // or the notification will have custom content.
  971. if (animate)
  972. Tweener.addTween(this._bannerLabel,
  973. { opacity: 0,
  974. time: ANIMATION_TIME,
  975. transition: 'easeOutQuad' });
  976. else
  977. this._bannerLabel.opacity = 0;
  978. }
  979. this.emit('expanded');
  980. },
  981.  
  982. collapseCompleted: function() {
  983. if (this._destroyed)
  984. return;
  985. this.expanded = false;
  986. // Make sure we don't line wrap the title, and ellipsize it instead.
  987. this._titleLabel.clutter_text.line_wrap = false;
  988. this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
  989. // Restore banner opacity in case the notification is shown in the
  990. // banner mode again on update.
  991. this._bannerLabel.opacity = 255;
  992. this.emit('collapsed');
  993. },
  994.  
  995. _onActionInvoked: function(actor, mouseButtonClicked, id) {
  996. this.emit('action-invoked', id);
  997. if (!this.resident) {
  998. // We don't hide a resident notification when the user invokes one of its actions,
  999. // because it is common for such notifications to update themselves with new
  1000. // information based on the action. We'd like to display the updated information
  1001. // in place, rather than pop-up a new notification.
  1002. this.emit('done-displaying');
  1003. this.destroy();
  1004. }
  1005. },
  1006.  
  1007. _onClicked: function() {
  1008. this.emit('clicked');
  1009. // We hide all types of notifications once the user clicks on them because the common
  1010. // outcome of clicking should be the relevant window being brought forward and the user's
  1011. // attention switching to the window.
  1012. this.emit('done-displaying');
  1013. if (!this.resident)
  1014. this.destroy();
  1015. },
  1016.  
  1017. _onDestroy: function() {
  1018. if (this._destroyed)
  1019. return;
  1020. this._destroyed = true;
  1021. if (!this._destroyedReason)
  1022. this._destroyedReason = NotificationDestroyedReason.DISMISSED;
  1023. this.emit('destroy', this._destroyedReason);
  1024. },
  1025.  
  1026. destroy: function(reason) {
  1027. this._destroyedReason = reason;
  1028. this.actor.destroy();
  1029. this.actor._delegate = null;
  1030. }
  1031. };
  1032. Signals.addSignalMethods(Notification.prototype);
  1033.  
  1034. function Source(title) {
  1035. this._init(title);
  1036. }
  1037.  
  1038. Source.prototype = {
  1039. ICON_SIZE: 24,
  1040.  
  1041. _init: function(title) {
  1042. this.title = title;
  1043.  
  1044. this.actor = new Cinnamon.GenericContainer();
  1045. this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
  1046. this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
  1047. this.actor.connect('allocate', Lang.bind(this, this._allocate));
  1048. this.actor.connect('destroy', Lang.bind(this,
  1049. function() {
  1050. this._actorDestroyed = true;
  1051. }));
  1052. this._actorDestroyed = false;
  1053.  
  1054. this._counterLabel = new St.Label();
  1055. this._counterBin = new St.Bin({ style_class: 'summary-source-counter',
  1056. child: this._counterLabel });
  1057. this._counterBin.hide();
  1058.  
  1059. this._iconBin = new St.Bin({ x_fill: true,
  1060. y_fill: true });
  1061.  
  1062. this.actor.add_actor(this._iconBin);
  1063. this.actor.add_actor(this._counterBin);
  1064.  
  1065. this.isTransient = false;
  1066. this.isChat = false;
  1067.  
  1068. this.notifications = [];
  1069. },
  1070.  
  1071. _getPreferredWidth: function (actor, forHeight, alloc) {
  1072. let [min, nat] = this._iconBin.get_preferred_width(forHeight);
  1073. alloc.min_size = min; alloc.nat_size = nat;
  1074. },
  1075.  
  1076. _getPreferredHeight: function (actor, forWidth, alloc) {
  1077. let [min, nat] = this._iconBin.get_preferred_height(forWidth);
  1078. alloc.min_size = min; alloc.nat_size = nat;
  1079. },
  1080.  
  1081. _allocate: function(actor, box, flags) {
  1082. // the iconBin should fill our entire box
  1083. this._iconBin.allocate(box, flags);
  1084.  
  1085. let childBox = new Clutter.ActorBox();
  1086.  
  1087. let [minWidth, minHeight, naturalWidth, naturalHeight] = this._counterBin.get_preferred_size();
  1088. let direction = this.actor.get_direction();
  1089.  
  1090. if (direction == St.TextDirection.LTR) {
  1091. // allocate on the right in LTR
  1092. childBox.x1 = box.x2 - naturalWidth;
  1093. childBox.x2 = box.x2;
  1094. } else {
  1095. // allocate on the left in RTL
  1096. childBox.x1 = 0;
  1097. childBox.x2 = naturalWidth;
  1098. }
  1099.  
  1100. childBox.y1 = box.y2 - naturalHeight;
  1101. childBox.y2 = box.y2;
  1102.  
  1103. this._counterBin.allocate(childBox, flags);
  1104. },
  1105.  
  1106. _setCount: function(count, visible) {
  1107. if (isNaN(parseInt(count)))
  1108. throw new Error("Invalid notification count: " + count);
  1109.  
  1110. if (this._actorDestroyed)
  1111. return;
  1112.  
  1113. this._counterBin.visible = visible;
  1114. this._counterLabel.set_text(count.toString());
  1115. },
  1116.  
  1117. _updateCount: function() {
  1118. let count = this.notifications.length;
  1119. this._setCount(count, count > 1);
  1120. },
  1121.  
  1122. setTransient: function(isTransient) {
  1123. this.isTransient = isTransient;
  1124. },
  1125.  
  1126. setTitle: function(newTitle) {
  1127. this.title = newTitle;
  1128. this.emit('title-changed');
  1129. },
  1130.  
  1131. // Called to create a new icon actor (of size this.ICON_SIZE).
  1132. // Must be overridden by the subclass if you do not pass icons
  1133. // explicitly to the Notification() constructor.
  1134. createNotificationIcon: function() {
  1135. throw new Error('no implementation of createNotificationIcon in ' + this);
  1136. },
  1137.  
  1138. // Unlike createNotificationIcon, this always returns the same actor;
  1139. // there is only one summary icon actor for a Source.
  1140. getSummaryIcon: function() {
  1141. return this.actor;
  1142. },
  1143.  
  1144. pushNotification: function(notification) {
  1145. if (this.notifications.indexOf(notification) < 0) {
  1146. this.notifications.push(notification);
  1147. this.emit('notification-added', notification);
  1148. }
  1149.  
  1150. notification.connect('clicked', Lang.bind(this, this.open));
  1151. notification.connect('destroy', Lang.bind(this,
  1152. function () {
  1153. let index = this.notifications.indexOf(notification);
  1154. if (index < 0)
  1155. return;
  1156.  
  1157. this.notifications.splice(index, 1);
  1158. if (this.notifications.length == 0)
  1159. this._lastNotificationRemoved();
  1160.  
  1161. this._updateCount();
  1162. }));
  1163.  
  1164. this._updateCount();
  1165. },
  1166.  
  1167. notify: function(notification) {
  1168. this.pushNotification(notification);
  1169. this.emit('notify', notification);
  1170. },
  1171.  
  1172. destroy: function(reason) {
  1173. this.emit('destroy', reason);
  1174. },
  1175.  
  1176. // A subclass can redefine this to "steal" clicks from the
  1177. // summaryitem; Use Clutter.get_current_event() to get the
  1178. // details, return true to prevent the default handling from
  1179. // ocurring.
  1180. handleSummaryClick: function() {
  1181. return false;
  1182. },
  1183.  
  1184. //// Protected methods ////
  1185.  
  1186. // The subclass must call this at least once to set the summary icon.
  1187. _setSummaryIcon: function(icon) {
  1188. if (this._iconBin.child)
  1189. this._iconBin.child.destroy();
  1190. this._iconBin.child = icon;
  1191. },
  1192.  
  1193. // Default implementation is to do nothing, but subclasses can override
  1194. open: function(notification) {
  1195. },
  1196.  
  1197. destroyNonResidentNotifications: function() {
  1198. for (let i = this.notifications.length - 1; i >= 0; i--)
  1199. if (!this.notifications[i].resident)
  1200. this.notifications[i].destroy();
  1201.  
  1202. this._updateCount();
  1203. },
  1204.  
  1205. // Default implementation is to destroy this source, but subclasses can override
  1206. _lastNotificationRemoved: function() {
  1207. this.destroy();
  1208. }
  1209. };
  1210. Signals.addSignalMethods(Source.prototype);
  1211.  
  1212. function SummaryItem(source) {
  1213. this._init(source);
  1214. }
  1215.  
  1216. SummaryItem.prototype = {
  1217. _init: function(source) {
  1218. this.source = source;
  1219. this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource));
  1220.  
  1221. this.actor = new St.Button({ style_class: 'summary-source-button',
  1222. y_fill: true,
  1223. reactive: true,
  1224. button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE,
  1225. track_hover: true });
  1226.  
  1227. this._sourceBox = new St.BoxLayout({ style_class: 'summary-source' });
  1228.  
  1229. this._sourceIcon = source.getSummaryIcon();
  1230. this._sourceTitleBin = new St.Bin({ y_align: St.Align.MIDDLE,
  1231. x_fill: true,
  1232. clip_to_allocation: true });
  1233. this._sourceTitle = new St.Label({ style_class: 'source-title',
  1234. text: source.title });
  1235. this._sourceTitle.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
  1236. this._sourceTitleBin.child = this._sourceTitle;
  1237. this._sourceTitleBin.width = 0;
  1238.  
  1239. this.source.connect('title-changed',
  1240. Lang.bind(this, function() {
  1241. this._sourceTitle.text = source.title;
  1242. }));
  1243.  
  1244. this._sourceBox.add(this._sourceIcon, { y_fill: false });
  1245. this._sourceBox.add(this._sourceTitleBin, { expand: true, y_fill: false });
  1246. this.actor.child = this._sourceBox;
  1247.  
  1248. this.notificationStackView = new St.ScrollView({ name: source.isChat ? '' : 'summary-notification-stack-scrollview',
  1249. vscrollbar_policy: source.isChat ? Gtk.PolicyType.NEVER : Gtk.PolicyType.AUTOMATIC,
  1250. hscrollbar_policy: Gtk.PolicyType.NEVER,
  1251. style_class: 'vfade' });
  1252. this.notificationStack = new St.BoxLayout({ name: 'summary-notification-stack',
  1253. vertical: true });
  1254. this.notificationStackView.add_actor(this.notificationStack);
  1255. this._stackedNotifications = [];
  1256.  
  1257. this._oldMaxScrollAdjustment = 0;
  1258.  
  1259. this.notificationStackView.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
  1260. let currentValue = adjustment.value + adjustment.page_size;
  1261. if (currentValue == this._oldMaxScrollAdjustment)
  1262. this.scrollTo(St.Side.BOTTOM);
  1263. this._oldMaxScrollAdjustment = adjustment.upper;
  1264. }));
  1265.  
  1266. this.rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu',
  1267. vertical: true });
  1268.  
  1269. let item;
  1270.  
  1271. item = new PopupMenu.PopupMenuItem(_("Open"));
  1272. item.connect('activate', Lang.bind(this, function() {
  1273. source.open();
  1274. this.emit('done-displaying-content');
  1275. }));
  1276. this.rightClickMenu.add(item.actor);
  1277.  
  1278. item = new PopupMenu.PopupMenuItem(_("Remove"));
  1279. item.connect('activate', Lang.bind(this, function() {
  1280. source.destroy();
  1281. this.emit('done-displaying-content');
  1282. }));
  1283. this.rightClickMenu.add(item.actor);
  1284.  
  1285. let focusManager = St.FocusManager.get_for_stage(global.stage);
  1286. focusManager.add_group(this.rightClickMenu);
  1287. },
  1288.  
  1289. // getTitleNaturalWidth, getTitleWidth, and setTitleWidth include
  1290. // the spacing between the icon and title (which is actually
  1291. // _sourceTitle's padding-left) as part of the width.
  1292.  
  1293. getTitleNaturalWidth: function() {
  1294. let [minWidth, naturalWidth] = this._sourceTitle.get_preferred_width(-1);
  1295.  
  1296. return Math.min(naturalWidth, MAX_SOURCE_TITLE_WIDTH);
  1297. },
  1298.  
  1299. getTitleWidth: function() {
  1300. return this._sourceTitleBin.width;
  1301. },
  1302.  
  1303. setTitleWidth: function(width) {
  1304. width = Math.round(width);
  1305. if (width != this._sourceTitleBin.width)
  1306. this._sourceTitleBin.width = width;
  1307. },
  1308.  
  1309. setEllipsization: function(mode) {
  1310. this._sourceTitle.clutter_text.ellipsize = mode;
  1311. },
  1312.  
  1313. prepareNotificationStackForShowing: function() {
  1314. if (this.notificationStack.get_children().length > 0)
  1315. return;
  1316.  
  1317. for (let i = 0; i < this.source.notifications.length; i++) {
  1318. this._appendNotificationToStack(this.source.notifications[i]);
  1319. }
  1320. },
  1321.  
  1322. doneShowingNotificationStack: function() {
  1323. let notificationActors = this.notificationStack.get_children();
  1324. for (let i = 0; i < this._stackedNotifications.length; i++) {
  1325. let stackedNotification = this._stackedNotifications[i];
  1326. let notification = stackedNotification.notification;
  1327. notification.collapseCompleted();
  1328. notification.disconnect(stackedNotification.notificationExpandedId);
  1329. notification.disconnect(stackedNotification.notificationDoneDisplayingId);
  1330. notification.disconnect(stackedNotification.notificationDestroyedId);
  1331. if (notification.actor.get_parent() == this.notificationStack)
  1332. this.notificationStack.remove_actor(notification.actor);
  1333. notification.setIconVisible(true);
  1334. }
  1335. this._stackedNotifications = [];
  1336. },
  1337.  
  1338. _notificationAddedToSource: function(source, notification) {
  1339. if (this.notificationStack.mapped)
  1340. this._appendNotificationToStack(notification);
  1341. },
  1342.  
  1343. _appendNotificationToStack: function(notification) {
  1344. let stackedNotification = {};
  1345. stackedNotification.notification = notification;
  1346. stackedNotification.notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated));
  1347. stackedNotification.notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying));
  1348. stackedNotification.notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed));
  1349. this._stackedNotifications.push(stackedNotification);
  1350. if (this.notificationStack.get_children().length > 0)
  1351. notification.setIconVisible(false);
  1352. this.notificationStack.add(notification.actor);
  1353. notification.expand(false);
  1354. },
  1355.  
  1356. // scrollTo:
  1357. // @side: St.Side.TOP or St.Side.BOTTOM
  1358. //
  1359. // Scrolls the notifiction stack to the indicated edge
  1360. scrollTo: function(side) {
  1361. let adjustment = this.notificationStackView.vscroll.adjustment;
  1362. if (side == St.Side.TOP)
  1363. adjustment.value = adjustment.lower;
  1364. else if (side == St.Side.BOTTOM)
  1365. adjustment.value = adjustment.upper;
  1366. },
  1367.  
  1368. _contentUpdated: function() {
  1369. this.emit('content-updated');
  1370. },
  1371.  
  1372. _notificationDoneDisplaying: function() {
  1373. this.emit('done-displaying-content');
  1374. },
  1375.  
  1376. _notificationDestroyed: function(notification) {
  1377. for (let i = 0; i < this._stackedNotifications.length; i++) {
  1378. if (this._stackedNotifications[i].notification == notification) {
  1379. let stackedNotification = this._stackedNotifications[i];
  1380. notification.disconnect(stackedNotification.notificationExpandedId);
  1381. notification.disconnect(stackedNotification.notificationDoneDisplayingId);
  1382. notification.disconnect(stackedNotification.notificationDestroyedId);
  1383. this._stackedNotifications.splice(i, 1);
  1384. this._contentUpdated();
  1385. break;
  1386. }
  1387. }
  1388.  
  1389. if (this.notificationStack.get_children().length > 0)
  1390. this.notificationStack.get_children()[0]._delegate.setIconVisible(true);
  1391. }
  1392. };
  1393. Signals.addSignalMethods(SummaryItem.prototype);
  1394.  
  1395. function MessageTray() {
  1396. this._init();
  1397. }
  1398.  
  1399. MessageTray.prototype = {
  1400. _init: function() {
  1401. this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) {
  1402. this._onStatusChanged(proxy.status);
  1403. }));
  1404.  
  1405. this._userStatus = GnomeSession.PresenceStatus.AVAILABLE;
  1406. this._busy = false;
  1407. this._backFromAway = false;
  1408.  
  1409. this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) {
  1410. this._onStatusChanged(status);
  1411. }));
  1412.  
  1413. this._notificationBin = new St.Bin();
  1414. this._notificationBin.hide();
  1415. this._notificationQueue = [];
  1416. this._notification = null;
  1417. this._notificationClickedId = 0;
  1418.  
  1419. this._pointerBarrier = 0;
  1420.  
  1421. this._focusGrabber = new FocusGrabber();
  1422. this._focusGrabber.connect('focus-ungrabbed', Lang.bind(this, this._unlock));
  1423. this._focusGrabber.connect('button-pressed', Lang.bind(this,
  1424. function(focusGrabber, source) {
  1425. this._focusGrabber.ungrabFocus();
  1426. }));
  1427. this._focusGrabber.connect('escape-pressed', Lang.bind(this, this._escapeTray));
  1428.  
  1429. this._trayState = State.HIDDEN;
  1430. this._locked = false;
  1431. this._traySummoned = false;
  1432. this._useLongerTrayLeftTimeout = false;
  1433. this._trayLeftTimeoutId = 0;
  1434. this._pointerInTray = false;
  1435. this._pointerInKeyboard = false;
  1436. this._notificationState = State.HIDDEN;
  1437. this._notificationTimeoutId = 0;
  1438. this._notificationExpandedId = 0;
  1439. this._notificationRemoved = false;
  1440. this._reNotifyAfterHideNotification = null;
  1441.  
  1442. this._sources = [];
  1443. Main.layoutManager.addChrome(this._notificationBin);
  1444.  
  1445. Main.layoutManager.connect('monitors-changed', Lang.bind(this, this._setSizePosition));
  1446.  
  1447. // Settings
  1448. this.settings = new Gio.Settings({ schema: "org.cinnamon.desktop.notifications" })
  1449. function setting(self, source, camelCase, dashed) {
  1450. function updater() { self[camelCase] = source.get_boolean(dashed); }
  1451. source.connect('changed::'+dashed, updater);
  1452. updater();
  1453. }
  1454. setting(this, this.settings, "_notificationsEnabled", "display-notifications");
  1455. setting(this, this.settings, "fadeOnMouseover", "fade-on-mouseover");
  1456. this.fadeOpacity = this.settings.get_int("fade-opacity");
  1457. this.settings.connect("changed::fade-opacity", Lang.bind(this, function() {
  1458. this.fadeOpacity = this.settings.get_int("fade-opacity");
  1459. }))
  1460. this._setSizePosition();
  1461.  
  1462. let updateLockState = Lang.bind(this, function() {
  1463. if (this._locked) {
  1464. this._unlock();
  1465. } else {
  1466. this._updateState();
  1467. }
  1468. });
  1469.  
  1470. Main.overview.connect('showing', updateLockState);
  1471. Main.overview.connect('hiding', updateLockState);
  1472. Main.expo.connect('showing', updateLockState);
  1473. Main.expo.connect('hiding', updateLockState);
  1474. },
  1475.  
  1476. _setSizePosition: function() {
  1477. //let monitor = Main.layoutManager.primaryMonitor;
  1478. //this._notificationBin.x = monitor.width - 500;
  1479. //this._notificationBin.width = monitor.width;
  1480. },
  1481.  
  1482. contains: function(source) {
  1483. return this._getSourceIndex(source) >= 0;
  1484. },
  1485.  
  1486. _getSourceIndex: function(source) {
  1487. return this._sources.indexOf(source);
  1488. },
  1489.  
  1490. add: function(source) {
  1491. if (this.contains(source)) {
  1492. log('Trying to re-add source ' + source.title);
  1493. return;
  1494. }
  1495.  
  1496. source.connect('notify', Lang.bind(this, this._onNotify));
  1497.  
  1498. source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
  1499. },
  1500.  
  1501. _onSourceDestroy: function(source) {
  1502. let index = this._getSourceIndex(source);
  1503. if (index == -1)
  1504. return;
  1505.  
  1506. this._sources.splice(index, 1);
  1507.  
  1508. let needUpdate = false;
  1509.  
  1510. if (this._notification && this._notification.source == source) {
  1511. this._updateNotificationTimeout(0);
  1512. this._notificationRemoved = true;
  1513. needUpdate = true;
  1514. }
  1515.  
  1516. if (needUpdate)
  1517. this._updateState();
  1518. },
  1519.  
  1520. _onNotificationDestroy: function(notification) {
  1521. if (this._notification == notification && (this._notificationState == State.SHOWN || this._notificationState == State.SHOWING)) {
  1522. this._updateNotificationTimeout(0);
  1523. this._notificationRemoved = true;
  1524. this._updateState();
  1525. return;
  1526. }
  1527.  
  1528. let index = this._notificationQueue.indexOf(notification);
  1529. notification.destroy();
  1530. if (index != -1)
  1531. this._notificationQueue.splice(index, 1);
  1532. },
  1533.  
  1534. _lock: function() {
  1535. this._locked = true;
  1536. },
  1537.  
  1538. _unlock: function() {
  1539. if (!this._locked)
  1540. return;
  1541. this._locked = false;
  1542. this._updateState();
  1543. },
  1544.  
  1545. toggle: function() {
  1546. this._traySummoned = !this._traySummoned;
  1547. this._updateState();
  1548. },
  1549.  
  1550. hide: function() {
  1551. this._traySummoned = false;
  1552. this._updateState();
  1553. },
  1554.  
  1555. _onNotify: function(source, notification) {
  1556. if (this._notification == notification) {
  1557. // If a notification that is being shown is updated, we update
  1558. // how it is shown and extend the time until it auto-hides.
  1559. // If a new notification is updated while it is being hidden,
  1560. // we stop hiding it and show it again.
  1561. this._updateShowingNotification();
  1562. } else if (this._notificationQueue.indexOf(notification) < 0) {
  1563. notification.connect('destroy',
  1564. Lang.bind(this, this._onNotificationDestroy));
  1565. this._notificationQueue.push(notification);
  1566. this._notificationQueue.sort(function(notification1, notification2) {
  1567. return (notification2.urgency - notification1.urgency);
  1568. });
  1569. }
  1570. this._updateState();
  1571. },
  1572.  
  1573. _onStatusChanged: function(status) {
  1574. this._backFromAway = (this._userStatus == GnomeSession.PresenceStatus.IDLE && this._userStatus != status);
  1575. this._userStatus = status;
  1576.  
  1577. if (status == GnomeSession.PresenceStatus.BUSY) {
  1578. // remove notification and allow the summary to be closed now
  1579. this._updateNotificationTimeout(0);
  1580. this._busy = true;
  1581. } else if (status != GnomeSession.PresenceStatus.IDLE) {
  1582. // We preserve the previous value of this._busy if the status turns to IDLE
  1583. // so that we don't start showing notifications queued during the BUSY state
  1584. // as the screensaver gets activated.
  1585. this._busy = false;
  1586. }
  1587.  
  1588. this._updateState();
  1589. },
  1590.  
  1591. _escapeTray: function() {
  1592. this._unlock();
  1593. this._pointerInTray = false;
  1594. this._updateNotificationTimeout(0);
  1595. this._updateState();
  1596. },
  1597.  
  1598. // All of the logic for what happens when occurs here; the various
  1599. // event handlers merely update variables such as
  1600. // 'this._pointerInTray', 'this._summaryState', etc, and
  1601. // _updateState() figures out what (if anything) needs to be done
  1602. // at the present time.
  1603. _updateState: function() {
  1604. // Notifications
  1605. let notificationUrgent = this._notificationQueue.length > 0 && this._notificationQueue[0].urgency == Urgency.CRITICAL;
  1606. let notificationsPending = this._notificationQueue.length > 0 && (!this._busy || notificationUrgent);
  1607. let notificationExpanded = this._notificationBin.y < 0;
  1608.  
  1609. let notificationExpired = (this._notificationTimeoutId == 0 &&
  1610. !(this._notification && this._notification.urgency == Urgency.CRITICAL) &&
  1611. !this._pointerInTray &&
  1612. !this._locked &&
  1613. !(this._pointerInKeyboard && notificationExpanded)
  1614. ) || this._notificationRemoved;
  1615. let canShowNotification = notificationsPending && this._notificationsEnabled;
  1616.  
  1617. if (this._notificationState == State.HIDDEN) {
  1618. if (canShowNotification) {
  1619. this._showNotification();
  1620. }
  1621. else if (!this._notificationsEnabled) {
  1622. if (notificationsPending) {
  1623. this._notification = this._notificationQueue.shift();
  1624. if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS)) {
  1625. this.emit('notify-applet-update', this._notification);
  1626. }
  1627. }
  1628. }
  1629. } else if (this._notificationState == State.SHOWN) {
  1630. if (notificationExpired)
  1631. this._hideNotification();
  1632. }
  1633. },
  1634.  
  1635. _tween: function(actor, statevar, value, params) {
  1636. let onComplete = params.onComplete;
  1637. let onCompleteScope = params.onCompleteScope;
  1638. let onCompleteParams = params.onCompleteParams;
  1639.  
  1640. params.onComplete = this._tweenComplete;
  1641. params.onCompleteScope = this;
  1642. params.onCompleteParams = [statevar, value, onComplete, onCompleteScope, onCompleteParams];
  1643.  
  1644. Tweener.addTween(actor, params);
  1645.  
  1646. let valuing = (value == State.SHOWN) ? State.SHOWING : State.HIDING;
  1647. this[statevar] = valuing;
  1648. },
  1649.  
  1650. _tweenComplete: function(statevar, value, onComplete, onCompleteScope, onCompleteParams) {
  1651. this[statevar] = value;
  1652. if (onComplete)
  1653. onComplete.apply(onCompleteScope, onCompleteParams);
  1654. this._updateState();
  1655. },
  1656.  
  1657. _showNotification: function() {
  1658. this._notificationTimeoutId = 1; // this prevents a race condition with the messagetray wanting
  1659. // to hide a notification before it's done showing it, when updating from applet
  1660. this._notification = this._notificationQueue.shift();
  1661. if (this._notification.actor._parent_container) {
  1662. this._notification.collapseCompleted();
  1663. this._notification.actor._parent_container.remove_actor(this._notification.actor);
  1664. }
  1665. this._notificationClickedId = this._notification.connect('done-displaying',
  1666. Lang.bind(this, this._escapeTray));
  1667. this._notificationBin.child = this._notification.actor;
  1668. this._notificationBin.opacity = 0;
  1669.  
  1670. let monitor = Main.layoutManager.primaryMonitor;
  1671. let panel = Main.panelManager.getPanel(0, false); // We only want the top panel in monitor 0
  1672. let height = 5;
  1673. if (panel)
  1674. height += panel.actor.get_height();
  1675. this._notificationBin.y = monitor.y + height * 2; // Notifications appear from here (for the animation)
  1676.  
  1677. let margin = this._notification._table.get_theme_node().get_length('margin-from-right-edge-of-screen');
  1678. this._notificationBin.x = monitor.x + monitor.width - this._notification._table.width - margin;
  1679. this._notificationBin.show();
  1680.  
  1681. this._updateShowingNotification();
  1682.  
  1683. let [x, y, mods] = global.get_pointer();
  1684. // We save the position of the mouse at the time when we started showing the notification
  1685. // in order to determine if the notification popped up under it. We make that check if
  1686. // the user starts moving the mouse and _onTrayHoverChanged() gets called. We don't
  1687. // expand the notification if it just happened to pop up under the mouse unless the user
  1688. // explicitly mouses away from it and then mouses back in.
  1689. this._showNotificationMouseX = x;
  1690. this._showNotificationMouseY = y;
  1691. // We save the y coordinate of the mouse at the time when we started showing the notification
  1692. // and then we update it in _notifiationTimeout() if the mouse is moving towards the
  1693. // notification. We don't pop down the notification if the mouse is moving towards it.
  1694. this._lastSeenMouseY = y;
  1695. },
  1696.  
  1697. _updateShowingNotification: function() {
  1698. Tweener.removeTweens(this._notificationBin);
  1699.  
  1700. this._expandNotification(true);
  1701.  
  1702. // We tween all notifications to full opacity. This ensures that both new notifications and
  1703. // notifications that might have been in the process of hiding get full opacity.
  1704. //
  1705. // We tween any notification showing in the banner mode to banner height (this._notificationBin.y = 0).
  1706. // This ensures that both new notifications and notifications in the banner mode that might
  1707. // have been in the process of hiding are shown with the banner height.
  1708. //
  1709. // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
  1710. // notification is being shown.
  1711. //
  1712. // We don't set the y parameter for the tween for expanded notifications because
  1713. // this._expandNotification() will result in getting this._notificationBin.y set to the appropriate
  1714. // fully expanded value.
  1715. let tweenParams = { opacity: 255,
  1716. time: ANIMATION_TIME,
  1717. transition: 'easeOutQuad',
  1718. onComplete: this._showNotificationCompleted,
  1719. onCompleteScope: this
  1720. };
  1721. let monitor = Main.layoutManager.primaryMonitor;
  1722. let panel = Main.panelManager.getPanel(0, false); // We only want the top panel in monitor 0
  1723. let height = 5;
  1724. if (panel)
  1725. height += panel.actor.get_height();
  1726.  
  1727. if (!this._notification.expanded)
  1728. tweenParams.y = monitor.y + height;
  1729.  
  1730. this._tween(this._notificationBin, '_notificationState', State.SHOWN, tweenParams);
  1731. },
  1732.  
  1733. _showNotificationCompleted: function() {
  1734. this._notificationTimeoutId = 0;
  1735. if (this._notification.urgency != Urgency.CRITICAL) {
  1736. this._updateNotificationTimeout(NOTIFICATION_TIMEOUT * 1000);
  1737. } else if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS)) {
  1738. this._updateNotificationTimeout(NOTIFICATION_CRITICAL_TIMEOUT_WITH_APPLET * 1000);
  1739. }
  1740. },
  1741.  
  1742. _updateNotificationTimeout: function(timeout) {
  1743. if (this._notificationTimeoutId) {
  1744. Mainloop.source_remove(this._notificationTimeoutId);
  1745. this._notificationTimeoutId = 0;
  1746. }
  1747. if (timeout > 0)
  1748. this._notificationTimeoutId =
  1749. Mainloop.timeout_add(timeout,
  1750. Lang.bind(this, this._notificationTimeout));
  1751. },
  1752.  
  1753. _notificationTimeout: function() {
  1754. let [x, y, mods] = global.get_pointer();
  1755. if (y > this._lastSeenMouseY + 10) {
  1756. // The mouse is moving towards the notification, so don't
  1757. // hide it yet. (We just create a new timeout (and destroy
  1758. // the old one) each time because the bookkeeping is
  1759. // simpler.)
  1760. this._lastSeenMouseY = y;
  1761. this._updateNotificationTimeout(1000);
  1762. } else {
  1763. this._notificationTimeoutId = 0;
  1764. this._updateState();
  1765. }
  1766.  
  1767. return false;
  1768. },
  1769.  
  1770. _hideNotification: function() {
  1771. this._focusGrabber.ungrabFocus();
  1772. if (this._notificationExpandedId) {
  1773. this._notification.disconnect(this._notificationExpandedId);
  1774. this._notificationExpandedId = 0;
  1775. }
  1776.  
  1777. this._tween(this._notificationBin, '_notificationState', State.HIDDEN,
  1778. { y: Main.layoutManager.primaryMonitor.y,
  1779. opacity: 0,
  1780. time: ANIMATION_TIME,
  1781. transition: 'easeOutQuad',
  1782. onComplete: this._hideNotificationCompleted,
  1783. onCompleteScope: this
  1784. });
  1785. },
  1786.  
  1787. _hideNotificationCompleted: function() {
  1788. this._notificationBin.hide();
  1789. this._notificationBin.child = null;
  1790. this._notification.collapseCompleted();
  1791. this._notification.disconnect(this._notificationClickedId);
  1792. this._notificationClickedId = 0;
  1793. let notification = this._notification;
  1794. if (AppletManager.get_role_provider_exists(AppletManager.Roles.NOTIFICATIONS) && !this._notificationRemoved) {
  1795. this.emit('notify-applet-update', notification);
  1796. } else {
  1797. if (notification.isTransient)
  1798. notification.destroy(NotificationDestroyedReason.EXPIRED);
  1799. }
  1800. this._notification = null;
  1801. this._notificationRemoved = false;
  1802. },
  1803.  
  1804. _expandNotification: function(autoExpanding) {
  1805. // Don't grab focus in notifications that are auto-expanded.
  1806. if (!autoExpanding)
  1807. this._focusGrabber.grabFocus(this._notification.actor);
  1808.  
  1809. if (!this._notificationExpandedId)
  1810. this._notificationExpandedId =
  1811. this._notification.connect('expanded',
  1812. Lang.bind(this, this._onNotificationExpanded));
  1813. // Don't animate changes in notifications that are auto-expanding.
  1814. this._notification.expand(!autoExpanding);
  1815. },
  1816.  
  1817. _onNotificationExpanded: function() {
  1818. let expandedY = this._notification.actor.height - this._notificationBin.height;
  1819. // Don't animate the notification to its new position if it has shrunk:
  1820. // there will be a very visible "gap" that breaks the illusion.
  1821.  
  1822. // This isn't really working at the moment, but it was just crashing before
  1823. // if it encountered a critical notification. expandedY is always 0. For now
  1824. // just make sure it's not covering the top panel if there is one.
  1825.  
  1826. let monitor = Main.layoutManager.primaryMonitor;
  1827. let panel = Main.panelManager.getPanel(0, false); // We only want the top panel in monitor 0
  1828. let height = 5;
  1829. if (panel)
  1830. height += panel.actor.get_height();
  1831. let newY = monitor.y + height;
  1832.  
  1833. if (this._notificationBin.y < expandedY)
  1834. this._notificationBin.y = expandedY;
  1835. else if (this._notification.y != expandedY)
  1836. this._tween(this._notificationBin, '_notificationState', State.SHOWN,
  1837. { y: newY,
  1838. time: ANIMATION_TIME,
  1839. transition: 'easeOutQuad'
  1840. });
  1841.  
  1842. },
  1843.  
  1844. // We use this function to grab focus when the user moves the pointer
  1845. // to a notification with CRITICAL urgency that was already auto-expanded.
  1846. _ensureNotificationFocused: function() {
  1847. this._focusGrabber.grabFocus(this._notification.actor);
  1848. }
  1849. };
  1850. Signals.addSignalMethods(MessageTray.prototype);
  1851.  
  1852.  
  1853.  
  1854. function SystemNotificationSource() {
  1855. this._init();
  1856. }
  1857.  
  1858. SystemNotificationSource.prototype = {
  1859. __proto__: Source.prototype,
  1860.  
  1861. _init: function() {
  1862. Source.prototype._init.call(this, _("System Information"));
  1863.  
  1864. this._setSummaryIcon(this.createNotificationIcon());
  1865. },
  1866.  
  1867. createNotificationIcon: function() {
  1868. return new St.Icon({ icon_name: 'dialog-information',
  1869. icon_type: St.IconType.SYMBOLIC,
  1870. icon_size: this.ICON_SIZE });
  1871. },
  1872.  
  1873. open: function() {
  1874. this.destroy();
  1875. }
  1876. };
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement