Merge lp:~darkxst/gnome-shell/lp1128804 into lp:ubuntu/raring/gnome-shell

Proposed by Tim Lunn
Status: Merged
Merge reported by: Martin Pitt
Merged at revision: not available
Proposed branch: lp:~darkxst/gnome-shell/lp1128804
Merge into: lp:ubuntu/raring/gnome-shell
Diff against target: 29889 lines (+7272/-12973)
65 files modified
.pc/11-no-gettext.patch/configure.ac (+1/-1)
.pc/14_make-GLX-optional.patch/configure.ac (+1/-1)
.pc/applied-patches (+1/-3)
.pc/git_messagetray_fix_expansion.patch/js/ui/messageTray.js (+22/-7)
.pc/git_messagetray_hideNotification.patch/js/ui/messageTray.js (+0/-2545)
.pc/git_messagetray_opacity_fix.patch/js/ui/messageTray.js (+0/-2550)
.pc/git_messagetray_remove_tweens.patch/js/ui/messageTray.js (+0/-2555)
NEWS (+18/-0)
configure (+16/-32)
configure.ac (+1/-1)
debian/changelog (+17/-0)
debian/control (+1/-1)
debian/control.in (+1/-1)
debian/patches/calendar-remove-strict-check.patch (+19/-0)
debian/patches/series (+1/-3)
docs/reference/shell/Makefile.in (+45/-32)
docs/reference/shell/html/ShellNetworkAgent.html (+20/-0)
docs/reference/shell/html/annotation-glossary.html (+3/-0)
docs/reference/shell/html/api-index-full.html (+4/-0)
docs/reference/shell/html/ch01.html (+1/-1)
docs/reference/shell/html/ch02.html (+1/-1)
docs/reference/shell/html/ch03.html (+1/-1)
docs/reference/shell/html/ch04.html (+1/-1)
docs/reference/shell/html/ch05.html (+1/-1)
docs/reference/shell/html/ch06.html (+1/-1)
docs/reference/shell/html/index.html (+1/-1)
docs/reference/shell/html/index.sgml (+20/-0)
docs/reference/shell/html/shell-ShellApp.html (+20/-0)
docs/reference/shell/html/shell-ShellGlobal.html (+93/-8)
docs/reference/shell/html/shell-shell-mobile-providers.html (+20/-0)
docs/reference/shell/html/shell.devhelp2 (+19/-0)
docs/reference/shell/shell-docs.sgml (+1/-1)
docs/reference/shell/shell-sections.txt (+1/-8)
docs/reference/st/Makefile.in (+45/-32)
docs/reference/st/html/StTextureCache.html (+16/-0)
docs/reference/st/html/index.html (+1/-1)
docs/reference/st/html/index.sgml (+22/-0)
docs/reference/st/html/pt01.html (+1/-1)
docs/reference/st/html/st-st-theme-node.html (+121/-1)
docs/reference/st/html/st.devhelp2 (+22/-0)
docs/reference/st/st-docs.sgml (+1/-1)
gtk-doc.make (+42/-22)
js/misc/config.js (+1/-1)
js/ui/messageTray.js (+12/-0)
js/ui/screenShield.js (+48/-15)
js/ui/unlockDialog.js (+5/-2)
m4/gtk-doc.m4 (+5/-1)
po/as.po (+179/-148)
po/bn_IN.po (+1382/-570)
po/et.po (+25/-15)
po/fr.po (+135/-122)
po/hi.po (+179/-199)
po/hu.po (+145/-127)
po/id.po (+145/-139)
po/ja.po (+5/-486)
po/kn.po (+220/-186)
po/ml.po (+219/-179)
po/mr.po (+134/-124)
po/nl.po (+1030/-867)
po/pt_BR.po (+148/-147)
po/ta.po (+304/-252)
po/te.po (+225/-178)
po/ug.po (+2087/-1400)
src/Makefile-calendar-server.am (+0/-1)
src/shell-recorder-src.c (+16/-0)
To merge this branch: bzr merge lp:~darkxst/gnome-shell/lp1128804
Reviewer Review Type Date Requested Status
Ubuntu branches Pending
Review via email: mp+149189@code.launchpad.net

Description of the change

update to new upstream release

To post a comment you must log in.
lp:~darkxst/gnome-shell/lp1128804 updated
70. By Tim Lunn

add missing patch

71. By Tim Lunn

remove dep3 comment from patch

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.pc/11-no-gettext.patch/configure.ac'
2--- .pc/11-no-gettext.patch/configure.ac 2012-11-12 23:11:33 +0000
3+++ .pc/11-no-gettext.patch/configure.ac 2013-02-19 20:52:20 +0000
4@@ -1,5 +1,5 @@
5 AC_PREREQ(2.63)
6-AC_INIT([gnome-shell],[3.6.2],[https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-shell],[gnome-shell])
7+AC_INIT([gnome-shell],[3.6.3],[https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-shell],[gnome-shell])
8
9 AC_CONFIG_HEADERS([config.h])
10 AC_CONFIG_SRCDIR([src/shell-global.c])
11
12=== modified file '.pc/14_make-GLX-optional.patch/configure.ac'
13--- .pc/14_make-GLX-optional.patch/configure.ac 2012-11-12 23:11:33 +0000
14+++ .pc/14_make-GLX-optional.patch/configure.ac 2013-02-19 20:52:20 +0000
15@@ -1,5 +1,5 @@
16 AC_PREREQ(2.63)
17-AC_INIT([gnome-shell],[3.6.2],[https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-shell],[gnome-shell])
18+AC_INIT([gnome-shell],[3.6.3],[https://bugzilla.gnome.org/enter_bug.cgi?product=gnome-shell],[gnome-shell])
19
20 AC_CONFIG_HEADERS([config.h])
21 AC_CONFIG_SRCDIR([src/shell-global.c])
22
23=== modified file '.pc/applied-patches'
24--- .pc/applied-patches 2013-01-31 14:53:11 +0000
25+++ .pc/applied-patches 2013-02-19 20:52:20 +0000
26@@ -5,7 +5,5 @@
27 ubuntu-lightdm-user-switching.patch
28 ubuntu_lock_on_suspend.patch
29 git-set-ally-wm-theme.patch
30-git_messagetray_opacity_fix.patch
31-git_messagetray_remove_tweens.patch
32 git_messagetray_fix_expansion.patch
33-git_messagetray_hideNotification.patch
34+calendar-remove-strict-check.patch
35
36=== modified file '.pc/git_messagetray_fix_expansion.patch/js/ui/messageTray.js'
37--- .pc/git_messagetray_fix_expansion.patch/js/ui/messageTray.js 2012-12-17 07:55:47 +0000
38+++ .pc/git_messagetray_fix_expansion.patch/js/ui/messageTray.js 2013-02-19 20:52:20 +0000
39@@ -2171,7 +2171,7 @@
40 Lang.bind(this, this._onIdleMonitorWatch));
41 this._notificationClickedId = this._notification.connect('done-displaying',
42 Lang.bind(this, this._escapeTray));
43- this._notification.connect('unfocused', Lang.bind(this, function() {
44+ this._notificationUnfocusedId = this._notification.connect('unfocused', Lang.bind(this, function() {
45 this._updateState();
46 }));
47 this._notificationBin.child = this._notification.actor;
48@@ -2279,6 +2279,18 @@
49 },
50
51 _hideNotification: function() {
52+ // HACK!
53+ // There seems to be a reentrancy issue in calling .ungrab() here,
54+ // which causes _updateState to be called before _notificationState
55+ // becomes HIDING. That hides the notification again, nullifying the
56+ // object but not setting _notificationState (and that's the weird part)
57+ // As then _notificationState is stuck into SHOWN but _notification
58+ // is null, every new _updateState fails and the message tray is
59+ // lost forever.
60+ //
61+ // See more at https://bugzilla.gnome.org/show_bug.cgi?id=683986
62+ this._notificationState = State.HIDING;
63+
64 this._grabHelper.ungrab({ actor: this._notification.actor });
65
66 if (this._notificationExpandedId) {
67@@ -2305,19 +2317,22 @@
68 },
69
70 _hideNotificationCompleted: function() {
71- this._notificationRemoved = false;
72- this._notificationWidget.hide();
73- this._closeButton.hide();
74- this._pointerInTray = false;
75- this.actor.hover = false; // Clutter doesn't emit notify::hover when actors move
76- this._notificationBin.child = null;
77 this._notification.collapseCompleted();
78 this._notification.disconnect(this._notificationClickedId);
79 this._notificationClickedId = 0;
80+ this._notification.disconnect(this._notificationUnfocusedId);
81+ this._notificationUnfocusedId = 0;
82 let notification = this._notification;
83 this._notification = null;
84 if (notification.isTransient)
85 notification.destroy(NotificationDestroyedReason.EXPIRED);
86+
87+ this._notificationRemoved = false;
88+ this._closeButton.hide();
89+ this._pointerInTray = false;
90+ this.actor.hover = false; // Clutter doesn't emit notify::hover when actors move
91+ this._notificationBin.child = null;
92+ this._notificationWidget.hide();
93 },
94
95 _expandNotification: function(autoExpanding) {
96
97=== removed directory '.pc/git_messagetray_hideNotification.patch'
98=== removed directory '.pc/git_messagetray_hideNotification.patch/js'
99=== removed directory '.pc/git_messagetray_hideNotification.patch/js/ui'
100=== removed file '.pc/git_messagetray_hideNotification.patch/js/ui/messageTray.js'
101--- .pc/git_messagetray_hideNotification.patch/js/ui/messageTray.js 2013-01-31 14:53:11 +0000
102+++ .pc/git_messagetray_hideNotification.patch/js/ui/messageTray.js 1970-01-01 00:00:00 +0000
103@@ -1,2545 +0,0 @@
104-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
105-
106-const Clutter = imports.gi.Clutter;
107-const GLib = imports.gi.GLib;
108-const Gio = imports.gi.Gio;
109-const Gtk = imports.gi.Gtk;
110-const Atk = imports.gi.Atk;
111-const Lang = imports.lang;
112-const Mainloop = imports.mainloop;
113-const Meta = imports.gi.Meta;
114-const Pango = imports.gi.Pango;
115-const Shell = imports.gi.Shell;
116-const Signals = imports.signals;
117-const St = imports.gi.St;
118-
119-const BoxPointer = imports.ui.boxpointer;
120-const CtrlAltTab = imports.ui.ctrlAltTab;
121-const GnomeSession = imports.misc.gnomeSession;
122-const GrabHelper = imports.ui.grabHelper;
123-const Lightbox = imports.ui.lightbox;
124-const Main = imports.ui.main;
125-const PointerWatcher = imports.ui.pointerWatcher;
126-const PopupMenu = imports.ui.popupMenu;
127-const Params = imports.misc.params;
128-const Tweener = imports.ui.tweener;
129-const Util = imports.misc.util;
130-
131-const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
132-
133-const ANIMATION_TIME = 0.2;
134-const NOTIFICATION_TIMEOUT = 4;
135-const SUMMARY_TIMEOUT = 1;
136-const LONGER_SUMMARY_TIMEOUT = 4;
137-
138-const HIDE_TIMEOUT = 0.2;
139-const LONGER_HIDE_TIMEOUT = 0.6;
140-
141-const NOTIFICATION_ICON_SIZE = 24;
142-
143-// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
144-// range from the point where it left the tray.
145-const MOUSE_LEFT_ACTOR_THRESHOLD = 20;
146-
147-// Time the user needs to leave the mouse on the bottom pixel row to open the tray
148-const TRAY_DWELL_TIME = 1000; // ms
149-// Time resolution when tracking the mouse to catch the open tray dwell
150-const TRAY_DWELL_CHECK_INTERVAL = 100; // ms
151-
152-const IDLE_TIME = 1000;
153-
154-const State = {
155- HIDDEN: 0,
156- SHOWING: 1,
157- SHOWN: 2,
158- HIDING: 3
159-};
160-
161-// These reasons are useful when we destroy the notifications received through
162-// the notification daemon. We use EXPIRED for transient notifications that the
163-// user did not interact with, DISMISSED for all other notifications that were
164-// destroyed as a result of a user action, and SOURCE_CLOSED for the notifications
165-// that were requested to be destroyed by the associated source.
166-const NotificationDestroyedReason = {
167- EXPIRED: 1,
168- DISMISSED: 2,
169- SOURCE_CLOSED: 3
170-};
171-
172-// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
173-// urgency values map to the corresponding values for the notifications received
174-// through the notification daemon. HIGH urgency value is used for chats received
175-// through the Telepathy client.
176-const Urgency = {
177- LOW: 0,
178- NORMAL: 1,
179- HIGH: 2,
180- CRITICAL: 3
181-}
182-
183-function _fixMarkup(text, allowMarkup) {
184- if (allowMarkup) {
185- // Support &, ", ', < and >, escape all other
186- // occurrences of '&'.
187- let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&');
188-
189- // Support <b>, <i>, and <u>, escape anything else
190- // so it displays as raw markup.
191- _text = _text.replace(/<(?!\/?[biu]>)/g, '&lt;');
192-
193- try {
194- Pango.parse_markup(_text, -1, '');
195- return _text;
196- } catch (e) {}
197- }
198-
199- // !allowMarkup, or invalid markup
200- return GLib.markup_escape_text(text, -1);
201-}
202-
203-const URLHighlighter = new Lang.Class({
204- Name: 'URLHighlighter',
205-
206- _init: function(text, lineWrap, allowMarkup) {
207- if (!text)
208- text = '';
209- this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' });
210- this._linkColor = '#ccccff';
211- this.actor.connect('style-changed', Lang.bind(this, function() {
212- let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false);
213- if (hasColor) {
214- let linkColor = color.to_string().substr(0, 7);
215- if (linkColor != this._linkColor) {
216- this._linkColor = linkColor;
217- this._highlightUrls();
218- }
219- }
220- }));
221- if (lineWrap) {
222- this.actor.clutter_text.line_wrap = true;
223- this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
224- this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
225- }
226-
227- this.setMarkup(text, allowMarkup);
228- this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) {
229- // Don't try to URL highlight when invisible.
230- // The MessageTray doesn't actually hide us, so
231- // we need to check for paint opacities as well.
232- if (!actor.visible || actor.get_paint_opacity() == 0)
233- return false;
234-
235- // Keep Notification.actor from seeing this and taking
236- // a pointer grab, which would block our button-release-event
237- // handler, if an URL is clicked
238- return this._findUrlAtPos(event) != -1;
239- }));
240- this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) {
241- if (!actor.visible || actor.get_paint_opacity() == 0)
242- return false;
243-
244- let urlId = this._findUrlAtPos(event);
245- if (urlId != -1) {
246- let url = this._urls[urlId].url;
247- if (url.indexOf(':') == -1)
248- url = 'http://' + url;
249- try {
250- Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context());
251- return true;
252- } catch (e) {
253- // TODO: remove this after gnome 3 release
254- Util.spawn(['gvfs-open', url]);
255- return true;
256- }
257- }
258- return false;
259- }));
260- this.actor.connect('motion-event', Lang.bind(this, function(actor, event) {
261- if (!actor.visible || actor.get_paint_opacity() == 0)
262- return false;
263-
264- let urlId = this._findUrlAtPos(event);
265- if (urlId != -1 && !this._cursorChanged) {
266- global.set_cursor(Shell.Cursor.POINTING_HAND);
267- this._cursorChanged = true;
268- } else if (urlId == -1) {
269- global.unset_cursor();
270- this._cursorChanged = false;
271- }
272- return false;
273- }));
274- this.actor.connect('leave-event', Lang.bind(this, function() {
275- if (!this.actor.visible || this.actor.get_paint_opacity() == 0)
276- return;
277-
278- if (this._cursorChanged) {
279- this._cursorChanged = false;
280- global.unset_cursor();
281- }
282- }));
283- },
284-
285- setMarkup: function(text, allowMarkup) {
286- text = text ? _fixMarkup(text, allowMarkup) : '';
287- this._text = text;
288-
289- this.actor.clutter_text.set_markup(text);
290- /* clutter_text.text contain text without markup */
291- this._urls = Util.findUrls(this.actor.clutter_text.text);
292- this._highlightUrls();
293- },
294-
295- _highlightUrls: function() {
296- // text here contain markup
297- let urls = Util.findUrls(this._text);
298- let markup = '';
299- let pos = 0;
300- for (let i = 0; i < urls.length; i++) {
301- let url = urls[i];
302- let str = this._text.substr(pos, url.pos - pos);
303- markup += str + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>';
304- pos = url.pos + url.url.length;
305- }
306- markup += this._text.substr(pos);
307- this.actor.clutter_text.set_markup(markup);
308- },
309-
310- _findUrlAtPos: function(event) {
311- let success;
312- let [x, y] = event.get_coords();
313- [success, x, y] = this.actor.transform_stage_point(x, y);
314- let find_pos = -1;
315- for (let i = 0; i < this.actor.clutter_text.text.length; i++) {
316- let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i);
317- if (py > y || py + line_height < y || x < px)
318- continue;
319- find_pos = i;
320- }
321- if (find_pos != -1) {
322- for (let i = 0; i < this._urls.length; i++)
323- if (find_pos >= this._urls[i].pos &&
324- this._urls[i].pos + this._urls[i].url.length > find_pos)
325- return i;
326- }
327- return -1;
328- }
329-});
330-
331-function makeCloseButton() {
332- let closeButton = new St.Button({ style_class: 'notification-close'});
333-
334- // This is a bit tricky. St.Bin has its own x-align/y-align properties
335- // that compete with Clutter's properties. This should be fixed for
336- // Clutter 2.0. Since St.Bin doesn't define its own setters, the
337- // setters are a workaround to get Clutter's version.
338- closeButton.set_x_align(Clutter.ActorAlign.END);
339- closeButton.set_y_align(Clutter.ActorAlign.START);
340-
341- // XXX Clutter 2.0 workaround: ClutterBinLayout needs expand
342- // to respect the alignments.
343- closeButton.set_x_expand(true);
344- closeButton.set_y_expand(true);
345-
346- closeButton.connect('style-changed', function() {
347- let themeNode = closeButton.get_theme_node();
348-
349- // libcroco doesn't support negative units
350- let direction = closeButton.get_text_direction() == Clutter.TextDirection.RTL ? -1 : 1;
351- closeButton.translation_x = direction * themeNode.get_length('-shell-close-overlap-x');
352-
353- // libcroco doesn't support negative units
354- closeButton.translation_y = -themeNode.get_length('-shell-close-overlap-y');
355- });
356-
357- return closeButton;
358-}
359-
360-// Notification:
361-// @source: the notification's Source
362-// @title: the title
363-// @banner: the banner text
364-// @params: optional additional params
365-//
366-// Creates a notification. In the banner mode, the notification
367-// will show an icon, @title (in bold) and @banner, all on a single
368-// line (with @banner ellipsized if necessary).
369-//
370-// The notification will be expandable if either it has additional
371-// elements that were added to it or if the @banner text did not
372-// fit fully in the banner mode. When the notification is expanded,
373-// the @banner text from the top line is always removed. The complete
374-// @banner text is added as the first element in the content section,
375-// unless 'customContent' parameter with the value 'true' is specified
376-// in @params.
377-//
378-// Additional notification content can be added with addActor() and
379-// addBody() methods. The notification content is put inside a
380-// scrollview, so if it gets too tall, the notification will scroll
381-// rather than continue to grow. In addition to this main content
382-// area, there is also a single-row action area, which is not
383-// scrolled and can contain a single actor. The action area can
384-// be set by calling setActionArea() method. There is also a
385-// convenience method addButton() for adding a button to the action
386-// area.
387-//
388-// @params can contain values for 'customContent', 'body', 'icon',
389-// 'titleMarkup', 'bannerMarkup', 'bodyMarkup', and 'clear'
390-// parameters.
391-//
392-// If @params contains a 'customContent' parameter with the value %true,
393-// then @banner will not be shown in the body of the notification when the
394-// notification is expanded and calls to update() will not clear the content
395-// unless 'clear' parameter with value %true is explicitly specified.
396-//
397-// If @params contains a 'body' parameter, then that text will be added to
398-// the content area (as with addBody()).
399-//
400-// By default, the icon shown is created by calling
401-// source.createIcon(). However, if @params contains an 'icon'
402-// parameter, the passed in icon will be used.
403-//
404-// If @params contains a 'titleMarkup', 'bannerMarkup', or
405-// 'bodyMarkup' parameter with the value %true, then the corresponding
406-// element is assumed to use pango markup. If the parameter is not
407-// present for an element, then anything that looks like markup in
408-// that element will appear literally in the output.
409-//
410-// If @params contains a 'clear' parameter with the value %true, then
411-// the content and the action area of the notification will be cleared.
412-// The content area is also always cleared if 'customContent' is false
413-// because it might contain the @banner that didn't fit in the banner mode.
414-const Notification = new Lang.Class({
415- Name: 'Notification',
416-
417- IMAGE_SIZE: 125,
418-
419- _init: function(source, title, banner, params) {
420- this.source = source;
421- this.title = title;
422- this.urgency = Urgency.NORMAL;
423- this.resident = false;
424- // 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
425- this.isTransient = false;
426- this.expanded = false;
427- this.focused = false;
428- this.acknowledged = false;
429- this._destroyed = false;
430- this._useActionIcons = false;
431- this._customContent = false;
432- this._bannerBodyText = null;
433- this._bannerBodyMarkup = false;
434- this._titleFitsInBannerMode = true;
435- this._titleDirection = Clutter.TextDirection.DEFAULT;
436- this._spacing = 0;
437- this._scrollPolicy = Gtk.PolicyType.AUTOMATIC;
438- this._imageBin = null;
439-
440- source.connect('destroy', Lang.bind(this,
441- function (source, reason) {
442- this.destroy(reason);
443- }));
444-
445- this.actor = new St.Button({ accessible_role: Atk.Role.NOTIFICATION });
446- this.actor.add_style_class_name('notification-unexpanded');
447- this.actor._delegate = this;
448- this.actor.connect('clicked', Lang.bind(this, this._onClicked));
449- this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
450-
451- this._table = new St.Table({ style_class: 'notification',
452- reactive: true });
453- this._table.connect('style-changed', Lang.bind(this, this._styleChanged));
454- this.actor.set_child(this._table);
455-
456- // The first line should have the title, followed by the
457- // banner text, but ellipsized if they won't both fit. We can't
458- // make St.Table or St.BoxLayout do this the way we want (don't
459- // show banner at all if title needs to be ellipsized), so we
460- // use Shell.GenericContainer.
461- this._bannerBox = new Shell.GenericContainer();
462- this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
463- this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
464- this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
465- this._table.add(this._bannerBox, { row: 0,
466- col: 1,
467- col_span: 2,
468- x_expand: false,
469- y_expand: false,
470- y_fill: false });
471-
472- // This is an empty cell that overlaps with this._bannerBox cell to ensure
473- // that this._bannerBox cell expands horizontally, while not forcing the
474- // this._imageBin that is also in col: 2 to expand horizontally.
475- this._table.add(new St.Bin(), { row: 0,
476- col: 2,
477- y_expand: false,
478- y_fill: false });
479-
480- this._titleLabel = new St.Label();
481- this._bannerBox.add_actor(this._titleLabel);
482- this._bannerUrlHighlighter = new URLHighlighter();
483- this._bannerLabel = this._bannerUrlHighlighter.actor;
484- this._bannerBox.add_actor(this._bannerLabel);
485-
486- this.update(title, banner, params);
487- },
488-
489- // update:
490- // @title: the new title
491- // @banner: the new banner
492- // @params: as in the Notification constructor
493- //
494- // Updates the notification by regenerating its icon and updating
495- // the title/banner. If @params.clear is %true, it will also
496- // remove any additional actors/action buttons previously added.
497- update: function(title, banner, params) {
498- params = Params.parse(params, { customContent: false,
499- body: null,
500- icon: null,
501- secondaryIcon: null,
502- titleMarkup: false,
503- bannerMarkup: false,
504- bodyMarkup: false,
505- clear: false });
506-
507- this._customContent = params.customContent;
508-
509- let oldFocus = global.stage.key_focus;
510-
511- if (this._icon && (params.icon || params.clear)) {
512- this._icon.destroy();
513- this._icon = null;
514- }
515-
516- if (this._secondaryIcon && (params.secondaryIcon || params.clear)) {
517- this._secondaryIcon.destroy();
518- this._secondaryIcon = null;
519- }
520-
521- // We always clear the content area if we don't have custom
522- // content because it might contain the @banner that didn't
523- // fit in the banner mode.
524- if (this._scrollArea && (!this._customContent || params.clear)) {
525- if (oldFocus && this._scrollArea.contains(oldFocus))
526- this.actor.grab_key_focus();
527-
528- this._scrollArea.destroy();
529- this._scrollArea = null;
530- this._contentArea = null;
531- }
532- if (this._actionArea && params.clear) {
533- if (oldFocus && this._actionArea.contains(oldFocus))
534- this.actor.grab_key_focus();
535-
536- this._actionArea.destroy();
537- this._actionArea = null;
538- this._buttonBox = null;
539- }
540- if (this._imageBin && params.clear)
541- this.unsetImage();
542-
543- if (!this._scrollArea && !this._actionArea && !this._imageBin)
544- this._table.remove_style_class_name('multi-line-notification');
545-
546- if (!this._icon) {
547- this._icon = params.icon || this.source.createIcon(NOTIFICATION_ICON_SIZE);
548- this._table.add(this._icon, { row: 0,
549- col: 0,
550- x_expand: false,
551- y_expand: false,
552- y_fill: false,
553- y_align: St.Align.START });
554- }
555-
556- if (!this._secondaryIcon) {
557- this._secondaryIcon = params.secondaryIcon;
558-
559- if (this._secondaryIcon)
560- this._bannerBox.add_actor(this._secondaryIcon);
561- }
562-
563- this.title = title;
564- title = title ? _fixMarkup(title.replace(/\n/g, ' '), params.titleMarkup) : '';
565- this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>');
566-
567- if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL)
568- this._titleDirection = Clutter.TextDirection.RTL;
569- else
570- this._titleDirection = Clutter.TextDirection.LTR;
571-
572- // Let the title's text direction control the overall direction
573- // of the notification - in case where different scripts are used
574- // in the notification, this is the right thing for the icon, and
575- // arguably for action buttons as well. Labels other than the title
576- // will be allocated at the available width, so that their alignment
577- // is done correctly automatically.
578- this._table.set_text_direction(this._titleDirection);
579-
580- // Unless the notification has custom content, we save this._bannerBodyText
581- // to add it to the content of the notification if the notification is
582- // expandable due to other elements in its content area or due to the banner
583- // not fitting fully in the single-line mode.
584- this._bannerBodyText = this._customContent ? null : banner;
585- this._bannerBodyMarkup = params.bannerMarkup;
586-
587- banner = banner ? banner.replace(/\n/g, ' ') : '';
588-
589- this._bannerUrlHighlighter.setMarkup(banner, params.bannerMarkup);
590- this._bannerLabel.queue_relayout();
591-
592- // Add the bannerBody now if we know for sure we'll need it
593- if (this._bannerBodyText && this._bannerBodyText.indexOf('\n') > -1)
594- this._addBannerBody();
595-
596- if (params.body)
597- this.addBody(params.body, params.bodyMarkup);
598- this.updated();
599- },
600-
601- setIconVisible: function(visible) {
602- this._icon.visible = visible;
603- },
604-
605- enableScrolling: function(enableScrolling) {
606- this._scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
607- if (this._scrollArea) {
608- this._scrollArea.vscrollbar_policy = this._scrollPolicy;
609- this._scrollArea.enable_mouse_scrolling = enableScrolling;
610- }
611- },
612-
613- _createScrollArea: function() {
614- this._table.add_style_class_name('multi-line-notification');
615- this._scrollArea = new St.ScrollView({ style_class: 'notification-scrollview',
616- vscrollbar_policy: this._scrollPolicy,
617- hscrollbar_policy: Gtk.PolicyType.NEVER });
618- this._table.add(this._scrollArea, { row: 1,
619- col: 2 });
620- this._updateLastColumnSettings();
621- this._contentArea = new St.BoxLayout({ style_class: 'notification-body',
622- vertical: true });
623- this._scrollArea.add_actor(this._contentArea);
624- // If we know the notification will be expandable, we need to add
625- // the banner text to the body as the first element.
626- this._addBannerBody();
627- },
628-
629- // addActor:
630- // @actor: actor to add to the body of the notification
631- //
632- // Appends @actor to the notification's body
633- addActor: function(actor, style) {
634- if (!this._scrollArea) {
635- this._createScrollArea();
636- }
637-
638- this._contentArea.add(actor, style ? style : {});
639- this.updated();
640- },
641-
642- // addBody:
643- // @text: the text
644- // @markup: %true if @text contains pango markup
645- // @style: style to use when adding the actor containing the text
646- //
647- // Adds a multi-line label containing @text to the notification.
648- //
649- // Return value: the newly-added label
650- addBody: function(text, markup, style) {
651- let label = new URLHighlighter(text, true, markup);
652-
653- this.addActor(label.actor, style);
654- return label.actor;
655- },
656-
657- _addBannerBody: function() {
658- if (this._bannerBodyText) {
659- let text = this._bannerBodyText;
660- this._bannerBodyText = null;
661- this.addBody(text, this._bannerBodyMarkup);
662- }
663- },
664-
665- // scrollTo:
666- // @side: St.Side.TOP or St.Side.BOTTOM
667- //
668- // Scrolls the content area (if scrollable) to the indicated edge
669- scrollTo: function(side) {
670- let adjustment = this._scrollArea.vscroll.adjustment;
671- if (side == St.Side.TOP)
672- adjustment.value = adjustment.lower;
673- else if (side == St.Side.BOTTOM)
674- adjustment.value = adjustment.upper;
675- },
676-
677- // setActionArea:
678- // @actor: the actor
679- // @props: (option) St.Table child properties
680- //
681- // Puts @actor into the action area of the notification, replacing
682- // the previous contents
683- setActionArea: function(actor, props) {
684- if (this._actionArea) {
685- this._actionArea.destroy();
686- this._actionArea = null;
687- if (this._buttonBox)
688- this._buttonBox = null;
689- } else {
690- this._addBannerBody();
691- }
692- this._actionArea = actor;
693-
694- if (!props)
695- props = {};
696- props.row = 2;
697- props.col = 2;
698-
699- this._table.add_style_class_name('multi-line-notification');
700- this._table.add(this._actionArea, props);
701- this._updateLastColumnSettings();
702- this.updated();
703- },
704-
705- _updateLastColumnSettings: function() {
706- if (this._scrollArea)
707- this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1,
708- col_span: this._imageBin ? 1 : 2 });
709- if (this._actionArea)
710- this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1,
711- col_span: this._imageBin ? 1 : 2 });
712- },
713-
714- setImage: function(image) {
715- if (this._imageBin)
716- this.unsetImage();
717- this._imageBin = new St.Bin();
718- this._imageBin.child = image;
719- this._imageBin.opacity = 230;
720- this._table.add_style_class_name('multi-line-notification');
721- this._table.add_style_class_name('notification-with-image');
722- this._addBannerBody();
723- this._updateLastColumnSettings();
724- this._table.add(this._imageBin, { row: 1,
725- col: 1,
726- row_span: 2,
727- x_expand: false,
728- y_expand: false,
729- x_fill: false,
730- y_fill: false });
731- },
732-
733- unsetImage: function() {
734- if (this._imageBin) {
735- this._table.remove_style_class_name('notification-with-image');
736- this._table.remove_actor(this._imageBin);
737- this._imageBin = null;
738- this._updateLastColumnSettings();
739- if (!this._scrollArea && !this._actionArea)
740- this._table.remove_style_class_name('multi-line-notification');
741- }
742- },
743-
744- // addButton:
745- // @id: the action ID
746- // @label: the label for the action's button
747- //
748- // Adds a button with the given @label to the notification. All
749- // action buttons will appear in a single row at the bottom of
750- // the notification.
751- //
752- // If the button is clicked, the notification will emit the
753- // %action-invoked signal with @id as a parameter
754- addButton: function(id, label) {
755- if (!this._buttonBox) {
756-
757- let box = new St.BoxLayout({ style_class: 'notification-actions' });
758- this.setActionArea(box, { x_expand: false,
759- y_expand: false,
760- x_fill: false,
761- y_fill: false,
762- x_align: St.Align.END });
763- this._buttonBox = box;
764- }
765-
766- let button = new St.Button({ can_focus: true });
767- button._actionId = id;
768-
769- if (this._useActionIcons && Gtk.IconTheme.get_default().has_icon(id)) {
770- button.add_style_class_name('notification-icon-button');
771- button.child = new St.Icon({ icon_name: id });
772- } else {
773- button.add_style_class_name('notification-button');
774- button.label = label;
775- }
776-
777- if (this._buttonBox.get_n_children() > 0)
778- global.focus_manager.remove_group(this._buttonBox);
779-
780- this._buttonBox.add(button);
781- global.focus_manager.add_group(this._buttonBox);
782- button.connect('clicked', Lang.bind(this, this._onActionInvoked, id));
783-
784- this.updated();
785- },
786-
787- // setButtonSensitive:
788- // @id: the action ID
789- // @sensitive: whether the button should be sensitive
790- //
791- // If the notification contains a button with action ID @id,
792- // its sensitivity will be set to @sensitive. Insensitive
793- // buttons cannot be clicked.
794- setButtonSensitive: function(id, sensitive) {
795- if (!this._buttonBox)
796- return;
797-
798- let button = this._buttonBox.get_children().filter(function(b) {
799- return b._actionId == id;
800- })[0];
801-
802- if (!button || button.reactive == sensitive)
803- return;
804-
805- button.reactive = sensitive;
806- },
807-
808- setUrgency: function(urgency) {
809- this.urgency = urgency;
810- },
811-
812- setResident: function(resident) {
813- this.resident = resident;
814- },
815-
816- setTransient: function(isTransient) {
817- this.isTransient = isTransient;
818- },
819-
820- setUseActionIcons: function(useIcons) {
821- this._useActionIcons = useIcons;
822- },
823-
824- _styleChanged: function() {
825- this._spacing = this._table.get_theme_node().get_length('spacing-columns');
826- },
827-
828- _bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
829- let [titleMin, titleNat] = this._titleLabel.get_preferred_width(forHeight);
830- let [bannerMin, bannerNat] = this._bannerLabel.get_preferred_width(forHeight);
831-
832- if (this._secondaryIcon) {
833- let [secondaryIconMin, secondaryIconNat] = this._secondaryIcon.get_preferred_width(forHeight);
834-
835- alloc.min_size = secondaryIconMin + this._spacing + titleMin;
836- alloc.natural_size = secondaryIconNat + this._spacing + titleNat + this._spacing + bannerNat;
837- } else {
838- alloc.min_size = titleMin;
839- alloc.natural_size = titleNat + this._spacing + bannerNat;
840- }
841- },
842-
843- _bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
844- [alloc.min_size, alloc.natural_size] =
845- this._titleLabel.get_preferred_height(forWidth);
846- },
847-
848- _bannerBoxAllocate: function(actor, box, flags) {
849- let availWidth = box.x2 - box.x1;
850-
851- let [titleMinW, titleNatW] = this._titleLabel.get_preferred_width(-1);
852- let [titleMinH, titleNatH] = this._titleLabel.get_preferred_height(availWidth);
853- let [bannerMinW, bannerNatW] = this._bannerLabel.get_preferred_width(availWidth);
854-
855- let rtl = (this._titleDirection == Clutter.TextDirection.RTL);
856- let x = rtl ? availWidth : 0;
857-
858- if (this._secondaryIcon) {
859- let [iconMinW, iconNatW] = this._secondaryIcon.get_preferred_width(-1);
860- let [iconMinH, iconNatH] = this._secondaryIcon.get_preferred_height(availWidth);
861-
862- let secondaryIconBox = new Clutter.ActorBox();
863- let secondaryIconBoxW = Math.min(iconNatW, availWidth);
864-
865- // allocate secondary icon box
866- if (rtl) {
867- secondaryIconBox.x1 = x - secondaryIconBoxW;
868- secondaryIconBox.x2 = x;
869- x = x - (secondaryIconBoxW + this._spacing);
870- } else {
871- secondaryIconBox.x1 = x;
872- secondaryIconBox.x2 = x + secondaryIconBoxW;
873- x = x + secondaryIconBoxW + this._spacing;
874- }
875- secondaryIconBox.y1 = 0;
876- // Using titleNatH ensures that the secondary icon is centered vertically
877- secondaryIconBox.y2 = titleNatH;
878-
879- availWidth = availWidth - (secondaryIconBoxW + this._spacing);
880- this._secondaryIcon.allocate(secondaryIconBox, flags);
881- }
882-
883- let titleBox = new Clutter.ActorBox();
884- let titleBoxW = Math.min(titleNatW, availWidth);
885- if (rtl) {
886- titleBox.x1 = availWidth - titleBoxW;
887- titleBox.x2 = availWidth;
888- } else {
889- titleBox.x1 = x;
890- titleBox.x2 = titleBox.x1 + titleBoxW;
891- }
892- titleBox.y1 = 0;
893- titleBox.y2 = titleNatH;
894- this._titleLabel.allocate(titleBox, flags);
895- this._titleFitsInBannerMode = (titleNatW <= availWidth);
896-
897- let bannerFits = true;
898- if (titleBoxW + this._spacing > availWidth) {
899- this._bannerLabel.opacity = 0;
900- bannerFits = false;
901- } else {
902- let bannerBox = new Clutter.ActorBox();
903-
904- if (rtl) {
905- bannerBox.x1 = 0;
906- bannerBox.x2 = titleBox.x1 - this._spacing;
907-
908- bannerFits = (bannerBox.x2 - bannerNatW >= 0);
909- } else {
910- bannerBox.x1 = titleBox.x2 + this._spacing;
911- bannerBox.x2 = availWidth;
912-
913- bannerFits = (bannerBox.x1 + bannerNatW <= availWidth);
914- }
915- bannerBox.y1 = 0;
916- bannerBox.y2 = titleNatH;
917- this._bannerLabel.allocate(bannerBox, flags);
918-
919- // Make _bannerLabel visible if the entire notification
920- // fits on one line, or if the notification is currently
921- // unexpanded and only showing one line anyway.
922- if (!this.expanded || (bannerFits && this._table.row_count == 1))
923- this._bannerLabel.opacity = 255;
924- }
925-
926- // If the banner doesn't fully fit in the banner box, we possibly need to add the
927- // banner to the body. We can't do that from here though since that will force a
928- // relayout, so we add it to the main loop.
929- if (!bannerFits && this._canExpandContent())
930- Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
931- Lang.bind(this,
932- function() {
933- if (this._canExpandContent()) {
934- this._addBannerBody();
935- this._table.add_style_class_name('multi-line-notification');
936- this.updated();
937- }
938- return false;
939- }));
940- },
941-
942- _canExpandContent: function() {
943- return this._bannerBodyText ||
944- (!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification'));
945- },
946-
947- updated: function() {
948- if (this.expanded)
949- this.expand(false);
950- },
951-
952- expand: function(animate) {
953- this.expanded = true;
954- this.actor.remove_style_class_name('notification-unexpanded');
955-
956- // The banner is never shown when the title did not fit, so this
957- // can be an if-else statement.
958- if (!this._titleFitsInBannerMode) {
959- // Remove ellipsization from the title label and make it wrap so that
960- // we show the full title when the notification is expanded.
961- this._titleLabel.clutter_text.line_wrap = true;
962- this._titleLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
963- this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
964- } else if (this._table.row_count > 1 && this._bannerLabel.opacity != 0) {
965- // We always hide the banner if the notification has additional content.
966- //
967- // We don't need to wrap the banner that doesn't fit the way we wrap the
968- // title that doesn't fit because we won't have a notification with
969- // row_count=1 that has a banner that doesn't fully fit. We'll either add
970- // that banner to the content of the notification in _bannerBoxAllocate()
971- // or the notification will have custom content.
972- if (animate)
973- Tweener.addTween(this._bannerLabel,
974- { opacity: 0,
975- time: ANIMATION_TIME,
976- transition: 'easeOutQuad' });
977- else
978- this._bannerLabel.opacity = 0;
979- }
980- this.emit('expanded');
981- },
982-
983- collapseCompleted: function() {
984- if (this._destroyed)
985- return;
986- this.expanded = false;
987- // Make sure we don't line wrap the title, and ellipsize it instead.
988- this._titleLabel.clutter_text.line_wrap = false;
989- this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
990- // Restore banner opacity in case the notification is shown in the
991- // banner mode again on update.
992- this._bannerLabel.opacity = 255;
993- // Restore height requisition
994- this.actor.add_style_class_name('notification-unexpanded');
995- this.emit('collapsed');
996- },
997-
998- _onActionInvoked: function(actor, mouseButtonClicked, id) {
999- this.emit('action-invoked', id);
1000- if (!this.resident) {
1001- // We don't hide a resident notification when the user invokes one of its actions,
1002- // because it is common for such notifications to update themselves with new
1003- // information based on the action. We'd like to display the updated information
1004- // in place, rather than pop-up a new notification.
1005- this.emit('done-displaying');
1006- this.destroy();
1007- }
1008- },
1009-
1010- _onClicked: function() {
1011- this.emit('clicked');
1012- // We hide all types of notifications once the user clicks on them because the common
1013- // outcome of clicking should be the relevant window being brought forward and the user's
1014- // attention switching to the window.
1015- this.emit('done-displaying');
1016- if (!this.resident)
1017- this.destroy();
1018- },
1019-
1020- _onDestroy: function() {
1021- if (this._destroyed)
1022- return;
1023- this._destroyed = true;
1024- if (!this._destroyedReason)
1025- this._destroyedReason = NotificationDestroyedReason.DISMISSED;
1026- this.emit('destroy', this._destroyedReason);
1027- },
1028-
1029- destroy: function(reason) {
1030- this._destroyedReason = reason;
1031- this.actor.destroy();
1032- this.actor._delegate = null;
1033- }
1034-});
1035-Signals.addSignalMethods(Notification.prototype);
1036-
1037-const SourceActor = new Lang.Class({
1038- Name: 'SourceActor',
1039-
1040- _init: function(source, size) {
1041- this._source = source;
1042- this._size = size;
1043-
1044- this.actor = new Shell.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, function() {
1049- this._actorDestroyed = true;
1050- }));
1051- this._actorDestroyed = false;
1052-
1053- this._counterLabel = new St.Label( {x_align: Clutter.ActorAlign.CENTER,
1054- x_expand: true,
1055- y_align: Clutter.ActorAlign.CENTER,
1056- y_expand: true });
1057-
1058- this._counterBin = new St.Bin({ style_class: 'summary-source-counter',
1059- child: this._counterLabel,
1060- layout_manager: new Clutter.BinLayout() });
1061- this._counterBin.hide();
1062-
1063- this._counterBin.connect('style-changed', Lang.bind(this, function() {
1064- let themeNode = this._counterBin.get_theme_node();
1065- this._counterBin.translation_x = themeNode.get_length('-shell-counter-overlap-x');
1066- this._counterBin.translation_y = themeNode.get_length('-shell-counter-overlap-y');
1067- }));
1068-
1069- this._iconBin = new St.Bin({ width: size,
1070- height: size,
1071- x_fill: true,
1072- y_fill: true });
1073-
1074- this.actor.add_actor(this._iconBin);
1075- this.actor.add_actor(this._counterBin);
1076-
1077- this._source.connect('count-updated', Lang.bind(this, this._updateCount));
1078- this._updateCount();
1079-
1080- this._source.connect('icon-updated', Lang.bind(this, this._updateIcon));
1081- this._updateIcon();
1082- },
1083-
1084- setIcon: function(icon) {
1085- this._iconBin.child = icon;
1086- this._iconSet = true;
1087- },
1088-
1089- _getPreferredWidth: function (actor, forHeight, alloc) {
1090- let [min, nat] = this._iconBin.get_preferred_width(forHeight);
1091- alloc.min_size = min; alloc.nat_size = nat;
1092- },
1093-
1094- _getPreferredHeight: function (actor, forWidth, alloc) {
1095- let [min, nat] = this._iconBin.get_preferred_height(forWidth);
1096- alloc.min_size = min; alloc.nat_size = nat;
1097- },
1098-
1099- _allocate: function(actor, box, flags) {
1100- // the iconBin should fill our entire box
1101- this._iconBin.allocate(box, flags);
1102-
1103- let childBox = new Clutter.ActorBox();
1104-
1105- let [minWidth, minHeight, naturalWidth, naturalHeight] = this._counterBin.get_preferred_size();
1106- let direction = this.actor.get_text_direction();
1107-
1108- if (direction == Clutter.TextDirection.LTR) {
1109- // allocate on the right in LTR
1110- childBox.x1 = box.x2 - naturalWidth;
1111- childBox.x2 = box.x2;
1112- } else {
1113- // allocate on the left in RTL
1114- childBox.x1 = 0;
1115- childBox.x2 = naturalWidth;
1116- }
1117-
1118- childBox.y1 = box.y2 - naturalHeight;
1119- childBox.y2 = box.y2;
1120-
1121- this._counterBin.allocate(childBox, flags);
1122- },
1123-
1124- _updateIcon: function() {
1125- if (this._actorDestroyed)
1126- return;
1127-
1128- if (!this._iconSet)
1129- this._iconBin.child = this._source.createIcon(this._size);
1130- },
1131-
1132- _updateCount: function() {
1133- if (this._actorDestroyed)
1134- return;
1135-
1136- this._counterBin.visible = this._source.countVisible;
1137-
1138- let text;
1139- if (this._source.count < 100)
1140- text = this._source.count.toString();
1141- else
1142- text = String.fromCharCode(0x22EF); // midline horizontal ellipsis
1143-
1144- this._counterLabel.set_text(text);
1145- }
1146-});
1147-
1148-const Source = new Lang.Class({
1149- Name: 'MessageTraySource',
1150-
1151- SOURCE_ICON_SIZE: 48,
1152-
1153- _init: function(title, iconName) {
1154- this.title = title;
1155- this.iconName = iconName;
1156-
1157- this.isTransient = false;
1158- this.isChat = false;
1159- this.isMuted = false;
1160- this.showInLockScreen = true;
1161- this.keepTrayOnSummaryClick = false;
1162-
1163- this.notifications = [];
1164- },
1165-
1166- get count() {
1167- return this.notifications.length;
1168- },
1169-
1170- get unseenCount() {
1171- return this.notifications.filter(function(n) { return !n.acknowledged; }).length;
1172- },
1173-
1174- get countVisible() {
1175- return this.count > 1;
1176- },
1177-
1178- countUpdated: function() {
1179- this.emit('count-updated');
1180- },
1181-
1182- buildRightClickMenu: function() {
1183- let item;
1184- let rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu',
1185- vertical: true });
1186-
1187- item = new PopupMenu.PopupMenuItem(_("Open"));
1188- item.connect('activate', Lang.bind(this, function() {
1189- this.open();
1190- this.emit('done-displaying-content');
1191- }));
1192- rightClickMenu.add(item.actor);
1193-
1194- item = new PopupMenu.PopupMenuItem(_("Remove"));
1195- item.connect('activate', Lang.bind(this, function() {
1196- this.destroy();
1197- this.emit('done-displaying-content');
1198- }));
1199- rightClickMenu.add(item.actor);
1200- return rightClickMenu;
1201- },
1202-
1203- setTransient: function(isTransient) {
1204- this.isTransient = isTransient;
1205- },
1206-
1207- setTitle: function(newTitle) {
1208- this.title = newTitle;
1209- this.emit('title-changed');
1210- },
1211-
1212- setMuted: function(muted) {
1213- if (!this.isChat || this.isMuted == muted)
1214- return;
1215- this.isMuted = muted;
1216- this.emit('muted-changed');
1217- },
1218-
1219- // Called to create a new icon actor.
1220- // Provides a sane default implementation, override if you need
1221- // something more fancy.
1222- createIcon: function(size) {
1223- return new St.Icon({ icon_name: this.iconName,
1224- icon_size: size });
1225- },
1226-
1227- _ensureMainIcon: function() {
1228- if (this._mainIcon)
1229- return;
1230-
1231- this._mainIcon = new SourceActor(this, this.SOURCE_ICON_SIZE);
1232- },
1233-
1234- // Unlike createIcon, this always returns the same actor;
1235- // there is only one summary icon actor for a Source.
1236- getSummaryIcon: function() {
1237- this._ensureMainIcon();
1238- return this._mainIcon.actor;
1239- },
1240-
1241- pushNotification: function(notification) {
1242- if (this.notifications.indexOf(notification) < 0) {
1243- this.notifications.push(notification);
1244- this.emit('notification-added', notification);
1245- }
1246-
1247- notification.connect('clicked', Lang.bind(this, this.open));
1248- notification.connect('destroy', Lang.bind(this,
1249- function () {
1250- let index = this.notifications.indexOf(notification);
1251- if (index < 0)
1252- return;
1253-
1254- this.notifications.splice(index, 1);
1255- if (this.notifications.length == 0)
1256- this._lastNotificationRemoved();
1257-
1258- this.countUpdated();
1259- }));
1260-
1261- this.countUpdated();
1262- },
1263-
1264- notify: function(notification) {
1265- notification.acknowledged = false;
1266- this.pushNotification(notification);
1267- if (!this.isMuted)
1268- this.emit('notify', notification);
1269- },
1270-
1271- destroy: function(reason) {
1272- this.emit('destroy', reason);
1273- },
1274-
1275- // A subclass can redefine this to "steal" clicks from the
1276- // summaryitem; Use Clutter.get_current_event() to get the
1277- // details, return true to prevent the default handling from
1278- // ocurring.
1279- handleSummaryClick: function() {
1280- return false;
1281- },
1282-
1283- iconUpdated: function() {
1284- this.emit('icon-updated');
1285- },
1286-
1287- //// Protected methods ////
1288- _setSummaryIcon: function(icon) {
1289- this._ensureMainIcon();
1290- this._mainIcon.setIcon(icon);
1291- this.iconUpdated();
1292- },
1293-
1294- open: function(notification) {
1295- this.emit('opened', notification);
1296- },
1297-
1298- destroyNonResidentNotifications: function() {
1299- for (let i = this.notifications.length - 1; i >= 0; i--)
1300- if (!this.notifications[i].resident)
1301- this.notifications[i].destroy();
1302-
1303- this.countUpdated();
1304- },
1305-
1306- // Default implementation is to destroy this source, but subclasses can override
1307- _lastNotificationRemoved: function() {
1308- this.destroy();
1309- },
1310-
1311- hasResidentNotification: function() {
1312- return this.notifications.some(function(n) { return n.resident; });
1313- }
1314-});
1315-Signals.addSignalMethods(Source.prototype);
1316-
1317-const SummaryItem = new Lang.Class({
1318- Name: 'SummaryItem',
1319-
1320- _init: function(source) {
1321- this.source = source;
1322- this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource));
1323-
1324- this.actor = new St.Button({ style_class: 'summary-source-button',
1325- y_fill: true,
1326- reactive: true,
1327- button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE,
1328- can_focus: true,
1329- track_hover: true });
1330- this.actor.label_actor = new St.Label({ text: source.title });
1331- this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPress));
1332- this._sourceBox = new St.BoxLayout({ style_class: 'summary-source' });
1333-
1334- this._sourceIcon = source.getSummaryIcon();
1335- this._sourceBox.add(this._sourceIcon, { y_fill: false });
1336- this.actor.child = this._sourceBox;
1337-
1338- this.notificationStackWidget = new St.Widget({ layout_manager: new Clutter.BinLayout() });
1339-
1340- this.notificationStackView = new St.ScrollView({ style_class: source.isChat ? '' : 'summary-notification-stack-scrollview',
1341- vscrollbar_policy: source.isChat ? Gtk.PolicyType.NEVER : Gtk.PolicyType.AUTOMATIC,
1342- hscrollbar_policy: Gtk.PolicyType.NEVER });
1343- this.notificationStackView.add_style_class_name('vfade');
1344- this.notificationStack = new St.BoxLayout({ style_class: 'summary-notification-stack',
1345- vertical: true });
1346- this.notificationStackView.add_actor(this.notificationStack);
1347- this.notificationStackWidget.add_actor(this.notificationStackView);
1348-
1349- this.closeButton = makeCloseButton();
1350- this.notificationStackWidget.add_actor(this.closeButton);
1351- this._stackedNotifications = [];
1352-
1353- this._oldMaxScrollAdjustment = 0;
1354-
1355- this.notificationStackView.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
1356- let currentValue = adjustment.value + adjustment.page_size;
1357- if (currentValue == this._oldMaxScrollAdjustment)
1358- this.scrollTo(St.Side.BOTTOM);
1359- this._oldMaxScrollAdjustment = adjustment.upper;
1360- }));
1361-
1362- this.rightClickMenu = source.buildRightClickMenu();
1363- if (this.rightClickMenu)
1364- global.focus_manager.add_group(this.rightClickMenu);
1365- },
1366-
1367- _onKeyPress: function(actor, event) {
1368- if (event.get_key_symbol() == Clutter.KEY_Up) {
1369- actor.emit('clicked', 1);
1370- return true;
1371- }
1372- return false;
1373- },
1374-
1375- prepareNotificationStackForShowing: function() {
1376- if (this.notificationStack.get_n_children() > 0)
1377- return;
1378-
1379- for (let i = 0; i < this.source.notifications.length; i++) {
1380- this._appendNotificationToStack(this.source.notifications[i]);
1381- }
1382-
1383- this.scrollTo(St.Side.BOTTOM);
1384- },
1385-
1386- doneShowingNotificationStack: function() {
1387- for (let i = 0; i < this._stackedNotifications.length; i++) {
1388- let stackedNotification = this._stackedNotifications[i];
1389- let notification = stackedNotification.notification;
1390- notification.collapseCompleted();
1391- notification.disconnect(stackedNotification.notificationExpandedId);
1392- notification.disconnect(stackedNotification.notificationDoneDisplayingId);
1393- notification.disconnect(stackedNotification.notificationDestroyedId);
1394- if (notification.actor.get_parent() == this.notificationStack)
1395- this.notificationStack.remove_actor(notification.actor);
1396- notification.setIconVisible(true);
1397- notification.enableScrolling(true);
1398- }
1399- this._stackedNotifications = [];
1400- },
1401-
1402- _notificationAddedToSource: function(source, notification) {
1403- if (this.notificationStack.mapped)
1404- this._appendNotificationToStack(notification);
1405- },
1406-
1407- _appendNotificationToStack: function(notification) {
1408- let stackedNotification = {};
1409- stackedNotification.notification = notification;
1410- stackedNotification.notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated));
1411- stackedNotification.notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying));
1412- stackedNotification.notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed));
1413- this._stackedNotifications.push(stackedNotification);
1414- if (!this.source.isChat)
1415- notification.enableScrolling(false);
1416- if (this.notificationStack.get_n_children() > 0)
1417- notification.setIconVisible(false);
1418- this.notificationStack.add(notification.actor);
1419- notification.expand(false);
1420- },
1421-
1422- // scrollTo:
1423- // @side: St.Side.TOP or St.Side.BOTTOM
1424- //
1425- // Scrolls the notifiction stack to the indicated edge
1426- scrollTo: function(side) {
1427- let adjustment = this.notificationStackView.vscroll.adjustment;
1428- if (side == St.Side.TOP)
1429- adjustment.value = adjustment.lower;
1430- else if (side == St.Side.BOTTOM)
1431- adjustment.value = adjustment.upper;
1432- },
1433-
1434- _contentUpdated: function() {
1435- this.emit('content-updated');
1436- },
1437-
1438- _notificationDoneDisplaying: function() {
1439- this.source.emit('done-displaying-content');
1440- },
1441-
1442- _notificationDestroyed: function(notification) {
1443- for (let i = 0; i < this._stackedNotifications.length; i++) {
1444- if (this._stackedNotifications[i].notification == notification) {
1445- let stackedNotification = this._stackedNotifications[i];
1446- notification.disconnect(stackedNotification.notificationExpandedId);
1447- notification.disconnect(stackedNotification.notificationDoneDisplayingId);
1448- notification.disconnect(stackedNotification.notificationDestroyedId);
1449- this._stackedNotifications.splice(i, 1);
1450- if (notification.actor.get_parent() == this.notificationStack)
1451- this.notificationStack.remove_actor(notification.actor);
1452- this._contentUpdated();
1453- break;
1454- }
1455- }
1456-
1457- let firstNotification = this._stackedNotifications[0];
1458- if (firstNotification)
1459- firstNotification.notification.setIconVisible(true);
1460- }
1461-});
1462-Signals.addSignalMethods(SummaryItem.prototype);
1463-
1464-const MessageTray = new Lang.Class({
1465- Name: 'MessageTray',
1466-
1467- _init: function() {
1468- this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) {
1469- this._onStatusChanged(proxy.status);
1470- }));
1471- this._busy = false;
1472- this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) {
1473- this._onStatusChanged(status);
1474- }));
1475-
1476- this.actor = new St.Widget({ name: 'message-tray',
1477- reactive: true,
1478- track_hover: true,
1479- layout_manager: new Clutter.BinLayout(),
1480- x_expand: true,
1481- y_expand: true,
1482- y_align: Clutter.ActorAlign.START,
1483- });
1484- this.actor.connect('notify::hover', Lang.bind(this, this._onTrayHoverChanged));
1485-
1486- this._notificationWidget = new St.Widget({ name: 'notification-container',
1487- y_align: Clutter.ActorAlign.START,
1488- x_align: Clutter.ActorAlign.CENTER,
1489- y_expand: true,
1490- x_expand: true,
1491- layout_manager: new Clutter.BinLayout() });
1492- this.actor.add_actor(this._notificationWidget);
1493-
1494- this._notificationBin = new St.Bin({ y_expand: true });
1495- this._notificationBin.set_y_align(Clutter.ActorAlign.START);
1496- this._notificationWidget.add_actor(this._notificationBin);
1497- this._notificationWidget.hide();
1498- this._notificationQueue = [];
1499- this._notification = null;
1500- this._notificationClickedId = 0;
1501-
1502- this.actor.connect('button-release-event', Lang.bind(this, function(actor, event) {
1503- this._setClickedSummaryItem(null);
1504- this._updateState();
1505- actor.grab_key_focus();
1506- }));
1507- global.focus_manager.add_group(this.actor);
1508- this._summary = new St.BoxLayout({ name: 'summary-mode',
1509- reactive: true,
1510- track_hover: true,
1511- x_align: Clutter.ActorAlign.END,
1512- x_expand: true,
1513- y_align: Clutter.ActorAlign.CENTER,
1514- y_expand: true });
1515- this._summary.connect('notify::hover', Lang.bind(this, this._onSummaryHoverChanged));
1516- this.actor.add_actor(this._summary);
1517- this._summary.opacity = 0;
1518-
1519- this._summaryMotionId = 0;
1520- this._trayMotionId = 0;
1521-
1522- this._summaryBoxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM,
1523- { reactive: true,
1524- track_hover: true });
1525- this._summaryBoxPointer.actor.connect('key-press-event',
1526- Lang.bind(this, this._onSummaryBoxPointerKeyPress));
1527- this._summaryBoxPointer.actor.style_class = 'summary-boxpointer';
1528- this._summaryBoxPointer.actor.hide();
1529- Main.layoutManager.addChrome(this._summaryBoxPointer.actor);
1530-
1531- this._summaryBoxPointerItem = null;
1532- this._summaryBoxPointerContentUpdatedId = 0;
1533- this._summaryBoxPointerDoneDisplayingId = 0;
1534- this._clickedSummaryItem = null;
1535- this._clickedSummaryItemMouseButton = -1;
1536- this._clickedSummaryItemAllocationChangedId = 0;
1537- this._pointerBarrier = 0;
1538-
1539- this._closeButton = makeCloseButton();
1540- this._closeButton.hide();
1541- this._closeButton.connect('clicked', Lang.bind(this, this._onCloseClicked));
1542- this._notificationWidget.add_actor(this._closeButton);
1543-
1544- this._idleMonitorWatchId = 0;
1545- this._userActiveWhileNotificationShown = false;
1546-
1547- this.idleMonitor = Shell.IdleMonitor.get();
1548-
1549- this._grabHelper = new GrabHelper.GrabHelper(this.actor);
1550- this._grabHelper.addActor(this._summaryBoxPointer.actor);
1551- this._grabHelper.addActor(this.actor);
1552- if (Main.panel.statusArea.activities)
1553- this._grabHelper.addActor(Main.panel.statusArea.activities.hotCorner.actor);
1554-
1555- Main.layoutManager.keyboardBox.connect('notify::hover', Lang.bind(this, this._onKeyboardHoverChanged));
1556- Main.layoutManager.connect('keyboard-visible-changed', Lang.bind(this, this._onKeyboardVisibleChanged));
1557-
1558- this._trayState = State.HIDDEN;
1559- this._locked = false;
1560- this._traySummoned = false;
1561- this._useLongerTrayLeftTimeout = false;
1562- this._trayLeftTimeoutId = 0;
1563- this._pointerInTray = false;
1564- this._pointerInKeyboard = false;
1565- this._keyboardVisible = false;
1566- this._summaryState = State.HIDDEN;
1567- this._pointerInSummary = false;
1568- this._notificationClosed = false;
1569- this._notificationState = State.HIDDEN;
1570- this._notificationTimeoutId = 0;
1571- this._notificationExpandedId = 0;
1572- this._summaryBoxPointerState = State.HIDDEN;
1573- this._summaryBoxPointerTimeoutId = 0;
1574- this._desktopCloneState = State.HIDDEN;
1575- this._overviewVisible = Main.overview.visible;
1576- this._notificationRemoved = false;
1577- this._reNotifyAfterHideNotification = null;
1578- this._inFullscreen = false;
1579- this._desktopClone = null;
1580-
1581- this._lightbox = new Lightbox.Lightbox(global.window_group,
1582- { inhibitEvents: true,
1583- fadeInTime: ANIMATION_TIME,
1584- fadeOutTime: ANIMATION_TIME,
1585- fadeFactor: 0.2
1586- });
1587-
1588- Main.layoutManager.trayBox.add_actor(this.actor);
1589- this.actor.y = 0;
1590- Main.layoutManager.trackChrome(this.actor);
1591- Main.layoutManager.trackChrome(this._notificationWidget);
1592- Main.layoutManager.trackChrome(this._closeButton);
1593-
1594- Main.layoutManager.connect('primary-fullscreen-changed', Lang.bind(this, this._onFullscreenChanged));
1595-
1596- Main.overview.connect('showing', Lang.bind(this,
1597- function() {
1598- this._overviewVisible = true;
1599- this._grabHelper.ungrab(); // drop modal grab if necessary
1600- this.actor.add_style_pseudo_class('overview');
1601- this._updateState();
1602- }));
1603- Main.overview.connect('hiding', Lang.bind(this,
1604- function() {
1605- this._overviewVisible = false;
1606- this._escapeTray();
1607- this.actor.remove_style_pseudo_class('overview');
1608- this._updateState();
1609- }));
1610-
1611- Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
1612-
1613- global.display.add_keybinding('toggle-message-tray',
1614- new Gio.Settings({ schema: SHELL_KEYBINDINGS_SCHEMA }),
1615- Meta.KeyBindingFlags.NONE,
1616- Lang.bind(this, this.toggleAndNavigate));
1617-
1618- this._summaryItems = [];
1619- this._chatSummaryItemsCount = 0;
1620-
1621- let pointerWatcher = PointerWatcher.getPointerWatcher();
1622- pointerWatcher.addWatch(TRAY_DWELL_CHECK_INTERVAL, Lang.bind(this, this._checkTrayDwell));
1623- this._trayDwellTimeoutId = 0;
1624- this._trayDwelling = false;
1625- this._trayDwellUserTime = 0;
1626-
1627- this._sessionUpdated();
1628- },
1629-
1630- _sessionUpdated: function() {
1631- if (Main.sessionMode.isLocked || Main.sessionMode.isGreeter)
1632- Main.ctrlAltTabManager.removeGroup(this._summary);
1633- else
1634- Main.ctrlAltTabManager.addGroup(this._summary, _("Message Tray"), 'start-here-symbolic',
1635- { focusCallback: Lang.bind(this, this.toggleAndNavigate),
1636- sortGroup: CtrlAltTab.SortGroup.BOTTOM });
1637- this._updateState();
1638- },
1639-
1640- _checkTrayDwell: function(x, y) {
1641- let monitor = Main.layoutManager.bottomMonitor;
1642- let shouldDwell = (x >= monitor.x && x <= monitor.x + monitor.width &&
1643- y == monitor.y + monitor.height - 1);
1644- if (shouldDwell) {
1645- // We only set up dwell timeout when the user is not hovering over the tray
1646- // (!this.actor.hover). This avoids bringing up the message tray after the
1647- // user clicks on a notification with the pointer on the bottom pixel
1648- // of the monitor. The _trayDwelling variable is used so that we only try to
1649- // fire off one tray dwell - if it fails (because, say, the user has the mouse down),
1650- // we don't try again until the user moves the mouse up and down again.
1651- if (!this._trayDwelling && !this.actor.hover && this._trayDwellTimeoutId == 0) {
1652- // Save the interaction timestamp so we can detect user input
1653- let focusWindow = global.display.focus_window;
1654- this._trayDwellUserTime = focusWindow ? focusWindow.user_time : 0;
1655-
1656- this._trayDwellTimeoutId = Mainloop.timeout_add(TRAY_DWELL_TIME,
1657- Lang.bind(this, this._trayDwellTimeout));
1658- }
1659- this._trayDwelling = true;
1660- } else {
1661- this._cancelTrayDwell();
1662- this._trayDwelling = false;
1663- }
1664- },
1665-
1666- _cancelTrayDwell: function() {
1667- if (this._trayDwellTimeoutId != 0) {
1668- Mainloop.source_remove(this._trayDwellTimeoutId);
1669- this._trayDwellTimeoutId = 0;
1670- }
1671- },
1672-
1673- _trayDwellTimeout: function() {
1674- if (Main.modalCount > 0)
1675- return false;
1676-
1677- // If the user interacted with the focus window since we started the tray
1678- // dwell (by clicking or typing), don't activate the message tray
1679- let focusWindow = global.display.focus_window;
1680- let currentUserTime = focusWindow ? focusWindow.user_time : 0;
1681- if (currentUserTime != this._trayDwellUserTime)
1682- return false;
1683-
1684- this._trayDwellTimeoutId = 0;
1685-
1686- this._traySummoned = true;
1687- this._updateState();
1688-
1689- return false;
1690- },
1691-
1692- _onCloseClicked: function() {
1693- if (this._notificationState == State.SHOWN) {
1694- this._closeButton.hide();
1695- this._notificationClosed = true;
1696- this._updateState();
1697- this._notificationClosed = false;
1698- }
1699- },
1700-
1701- contains: function(source) {
1702- return this._getIndexOfSummaryItemForSource(source) >= 0;
1703- },
1704-
1705- _getIndexOfSummaryItemForSource: function(source) {
1706- for (let i = 0; i < this._summaryItems.length; i++) {
1707- if (this._summaryItems[i].source == source)
1708- return i;
1709- }
1710- return -1;
1711- },
1712-
1713- add: function(source) {
1714- if (this.contains(source)) {
1715- log('Trying to re-add source ' + source.title);
1716- return;
1717- }
1718-
1719- let summaryItem = new SummaryItem(source);
1720-
1721- if (source.isChat) {
1722- this._summary.insert_child_at_index(summaryItem.actor, 0);
1723- this._chatSummaryItemsCount++;
1724- } else {
1725- this._summary.insert_child_at_index(summaryItem.actor, this._chatSummaryItemsCount);
1726- }
1727-
1728- this._summaryItems.push(summaryItem);
1729-
1730- source.connect('notify', Lang.bind(this, this._onNotify));
1731-
1732- source.connect('muted-changed', Lang.bind(this,
1733- function () {
1734- if (source.isMuted)
1735- this._notificationQueue = this._notificationQueue.filter(function(notification) {
1736- return source != notification.source;
1737- });
1738- }));
1739-
1740- summaryItem.actor.connect('clicked', Lang.bind(this,
1741- function(actor, button) {
1742- actor.grab_key_focus();
1743- this._onSummaryItemClicked(summaryItem, button);
1744- }));
1745- summaryItem.actor.connect('popup-menu', Lang.bind(this,
1746- function(actor, button) {
1747- actor.grab_key_focus();
1748- this._onSummaryItemClicked(summaryItem, 3);
1749- }));
1750-
1751- source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
1752-
1753- // We need to display the newly-added summary item, but if the
1754- // caller is about to post a notification, we want to show that
1755- // *first* and not show the summary item until after it hides.
1756- // So postpone calling _updateState() a tiny bit.
1757- Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { this._updateState(); return false; }));
1758-
1759- this.emit('summary-item-added', summaryItem);
1760- },
1761-
1762- getSummaryItems: function() {
1763- return this._summaryItems;
1764- },
1765-
1766- _onSourceDestroy: function(source) {
1767- let index = this._getIndexOfSummaryItemForSource(source);
1768- if (index == -1)
1769- return;
1770-
1771- let summaryItemToRemove = this._summaryItems[index];
1772-
1773- this._summaryItems.splice(index, 1);
1774-
1775- if (source.isChat)
1776- this._chatSummaryItemsCount--;
1777-
1778- let needUpdate = false;
1779-
1780- if (this._notification && this._notification.source == source) {
1781- this._updateNotificationTimeout(0);
1782- this._notificationRemoved = true;
1783- needUpdate = true;
1784- }
1785- if (this._clickedSummaryItem == summaryItemToRemove) {
1786- this._setClickedSummaryItem(null);
1787- needUpdate = true;
1788- }
1789-
1790- summaryItemToRemove.actor.destroy();
1791-
1792- if (needUpdate)
1793- this._updateState();
1794- },
1795-
1796- _onNotificationDestroy: function(notification) {
1797- if (this._notification == notification && (this._notificationState == State.SHOWN || this._notificationState == State.SHOWING)) {
1798- this._updateNotificationTimeout(0);
1799- this._notificationRemoved = true;
1800- this._updateState();
1801- return;
1802- }
1803-
1804- let index = this._notificationQueue.indexOf(notification);
1805- notification.destroy();
1806- if (index != -1)
1807- this._notificationQueue.splice(index, 1);
1808- },
1809-
1810- _lock: function() {
1811- this._locked = true;
1812- },
1813-
1814- _unlock: function() {
1815- if (!this._locked)
1816- return;
1817- this._locked = false;
1818- this._pointerInTray = this.actor.hover;
1819- this._updateState();
1820- },
1821-
1822- toggle: function() {
1823- this._traySummoned = !this._traySummoned;
1824- this._updateState();
1825- },
1826-
1827- toggleAndNavigate: function() {
1828- this.toggle();
1829- this._summary.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
1830- },
1831-
1832- hide: function() {
1833- this._traySummoned = false;
1834- this.actor.set_hover(false);
1835- this._summary.set_hover(false);
1836- this._updateState();
1837- },
1838-
1839- _onNotify: function(source, notification) {
1840- if (this._summaryBoxPointerItem && this._summaryBoxPointerItem.source == source) {
1841- if (this._summaryBoxPointerState == State.HIDING) {
1842- // We are in the process of hiding the summary box pointer.
1843- // If there is an update for one of the notifications or
1844- // a new notification to be added to the notification stack
1845- // while it is in the process of being hidden, we show it as
1846- // a new notification. However, we first wait till the hide
1847- // is complete. This is especially important if one of the
1848- // notifications in the stack was updated because we will
1849- // need to be able to re-parent its actor to a different
1850- // part of the stage.
1851- this._reNotifyAfterHideNotification = notification;
1852- } else {
1853- // The summary box pointer is showing or shown (otherwise,
1854- // this._summaryBoxPointerItem would be null)
1855- // Immediately mark the notification as acknowledged, as it's
1856- // not going into the queue
1857- notification.acknowledged = true;
1858- }
1859-
1860- return;
1861- }
1862-
1863- if (this._notification == notification) {
1864- // If a notification that is being shown is updated, we update
1865- // how it is shown and extend the time until it auto-hides.
1866- // If a new notification is updated while it is being hidden,
1867- // we stop hiding it and show it again.
1868- this._updateShowingNotification();
1869- } else if (this._notificationQueue.indexOf(notification) < 0) {
1870- notification.connect('destroy',
1871- Lang.bind(this, this._onNotificationDestroy));
1872- this._notificationQueue.push(notification);
1873- this._notificationQueue.sort(function(notification1, notification2) {
1874- return (notification2.urgency - notification1.urgency);
1875- });
1876- }
1877- this._updateState();
1878- },
1879-
1880- _onSummaryItemClicked: function(summaryItem, button) {
1881- if (summaryItem.source.handleSummaryClick()) {
1882- if (summaryItem.source.keepTrayOnSummaryClick)
1883- this._setClickedSummaryItem(null);
1884- else
1885- this._escapeTray();
1886- } else {
1887- if (!this._setClickedSummaryItem(summaryItem, button))
1888- this._setClickedSummaryItem(null);
1889- }
1890-
1891- this._updateState();
1892- },
1893-
1894- _onSummaryHoverChanged: function() {
1895- this._pointerInSummary = this._summary.hover;
1896- this._updateState();
1897- },
1898-
1899- _onTrayHoverChanged: function() {
1900- if (this.actor.hover) {
1901- // No dwell inside notifications at the bottom of the screen
1902- this._cancelTrayDwell();
1903-
1904- // Don't do anything if the one pixel area at the bottom is hovered over while the tray is hidden.
1905- if (this._trayState == State.HIDDEN && this._notificationState == State.HIDDEN)
1906- return;
1907-
1908- // Don't do anything if this._useLongerTrayLeftTimeout is true, meaning the notification originally
1909- // popped up under the pointer, but this._trayLeftTimeoutId is 0, meaning the pointer didn't leave
1910- // the tray yet. We need to check for this case because sometimes _onTrayHoverChanged() gets called
1911- // multiple times while this.actor.hover is true.
1912- if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId)
1913- return;
1914-
1915- this._useLongerTrayLeftTimeout = false;
1916- if (this._trayLeftTimeoutId) {
1917- Mainloop.source_remove(this._trayLeftTimeoutId);
1918- this._trayLeftTimeoutId = 0;
1919- this._trayLeftMouseX = -1;
1920- this._trayLeftMouseY = -1;
1921- return;
1922- }
1923-
1924- if (this._showNotificationMouseX >= 0) {
1925- let actorAtShowNotificationPosition =
1926- global.stage.get_actor_at_pos(Clutter.PickMode.ALL, this._showNotificationMouseX, this._showNotificationMouseY);
1927- this._showNotificationMouseX = -1;
1928- this._showNotificationMouseY = -1;
1929- // Don't set this._pointerInTray to true if the pointer was initially in the area where the notification
1930- // popped up. That way we will not be expanding notifications that happen to pop up over the pointer
1931- // automatically. Instead, the user is able to expand the notification by mousing away from it and then
1932- // mousing back in. Because this is an expected action, we set the boolean flag that indicates that a longer
1933- // timeout should be used before popping down the notification.
1934- if (this.actor.contains(actorAtShowNotificationPosition)) {
1935- this._useLongerTrayLeftTimeout = true;
1936- return;
1937- }
1938- }
1939- this._pointerInTray = true;
1940- this._updateState();
1941- } else {
1942- // We record the position of the mouse the moment it leaves the tray. These coordinates are used in
1943- // this._onTrayLeftTimeout() to determine if the mouse has moved far enough during the initial timeout for us
1944- // to consider that the user intended to leave the tray and therefore hide the tray. If the mouse is still
1945- // close to its previous position, we extend the timeout once.
1946- let [x, y, mods] = global.get_pointer();
1947- this._trayLeftMouseX = x;
1948- this._trayLeftMouseY = y;
1949-
1950- // We wait just a little before hiding the message tray in case the user quickly moves the mouse back into it.
1951- // We wait for a longer period if the notification popped up where the mouse pointer was already positioned.
1952- // That gives the user more time to mouse away from the notification and mouse back in in order to expand it.
1953- let timeout = this._useLongerTrayLeftTimeout ? LONGER_HIDE_TIMEOUT * 1000 : HIDE_TIMEOUT * 1000;
1954- this._trayLeftTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._onTrayLeftTimeout));
1955- }
1956- },
1957-
1958- _onKeyboardHoverChanged: function(keyboard) {
1959- this._pointerInKeyboard = keyboard.hover;
1960-
1961- if (!keyboard.hover) {
1962- let event = Clutter.get_current_event();
1963- if (event && event.type() == Clutter.EventType.LEAVE) {
1964- let into = event.get_related();
1965- if (into && this.actor.contains(into)) {
1966- // Don't call _updateState, because pointerInTray is
1967- // still false
1968- return;
1969- }
1970- }
1971- }
1972-
1973- this._updateState();
1974- },
1975-
1976- _onKeyboardVisibleChanged: function(layoutManager, keyboardVisible) {
1977- if (this._keyboardVisible == keyboardVisible)
1978- return;
1979-
1980- this._keyboardVisible = keyboardVisible;
1981-
1982- if (keyboardVisible)
1983- this.actor.add_style_pseudo_class('keyboard');
1984- else
1985- this.actor.remove_style_pseudo_class('keyboard');
1986-
1987- this._updateState();
1988- },
1989-
1990- _onFullscreenChanged: function(obj, state) {
1991- this._inFullscreen = state;
1992- this._updateState();
1993- },
1994-
1995- _onStatusChanged: function(status) {
1996- if (status == GnomeSession.PresenceStatus.BUSY) {
1997- // remove notification and allow the summary to be closed now
1998- this._updateNotificationTimeout(0);
1999- this._busy = true;
2000- } else if (status != GnomeSession.PresenceStatus.IDLE) {
2001- // We preserve the previous value of this._busy if the status turns to IDLE
2002- // so that we don't start showing notifications queued during the BUSY state
2003- // as the screensaver gets activated.
2004- this._busy = false;
2005- }
2006-
2007- this._updateState();
2008- },
2009-
2010- _onTrayLeftTimeout: function() {
2011- let [x, y, mods] = global.get_pointer();
2012- // We extend the timeout once if the mouse moved no further than MOUSE_LEFT_ACTOR_THRESHOLD to either side or up.
2013- // We don't check how far down the mouse moved because any point above the tray, but below the exit coordinate,
2014- // is close to the tray.
2015- if (this._trayLeftMouseX > -1 &&
2016- y > this._trayLeftMouseY - MOUSE_LEFT_ACTOR_THRESHOLD &&
2017- x < this._trayLeftMouseX + MOUSE_LEFT_ACTOR_THRESHOLD &&
2018- x > this._trayLeftMouseX - MOUSE_LEFT_ACTOR_THRESHOLD) {
2019- this._trayLeftMouseX = -1;
2020- this._trayLeftTimeoutId = Mainloop.timeout_add(LONGER_HIDE_TIMEOUT * 1000,
2021- Lang.bind(this, this._onTrayLeftTimeout));
2022- } else {
2023- this._trayLeftTimeoutId = 0;
2024- this._useLongerTrayLeftTimeout = false;
2025- this._pointerInTray = false;
2026- this._pointerInSummary = false;
2027- this._updateNotificationTimeout(0);
2028- this._updateState();
2029- }
2030- return false;
2031- },
2032-
2033- _escapeTray: function() {
2034- this._unlock();
2035- this._pointerInTray = false;
2036- this._pointerInSummary = false;
2037- this._traySummoned = false;
2038- this._setClickedSummaryItem(null);
2039- this._updateNotificationTimeout(0);
2040- this._updateState();
2041- },
2042-
2043- // All of the logic for what happens when occurs here; the various
2044- // event handlers merely update variables such as
2045- // 'this._pointerInTray', 'this._summaryState', etc, and
2046- // _updateState() figures out what (if anything) needs to be done
2047- // at the present time.
2048- _updateState: function() {
2049- // Notifications
2050- let notificationQueue = this._notificationQueue;
2051- let notificationUrgent = notificationQueue.length > 0 && notificationQueue[0].urgency == Urgency.CRITICAL;
2052- let notificationsLimited = this._busy || this._inFullscreen;
2053- let notificationsPending = notificationQueue.length > 0 && (!notificationsLimited || notificationUrgent) && Main.sessionMode.hasNotifications;
2054- let nextNotification = notificationQueue.length > 0 ? notificationQueue[0] : null;
2055- let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved;
2056- let notificationExpanded = this._notification && this._notification.expanded;
2057- let notificationExpired = this._notificationTimeoutId == 0 &&
2058- !(this._notification && this._notification.urgency == Urgency.CRITICAL) &&
2059- !(this._notification && this._notification.focused) &&
2060- !this._pointerInTray &&
2061- !this._locked &&
2062- !(this._pointerInKeyboard && notificationExpanded);
2063- let notificationLockedOut = !Main.sessionMode.hasNotifications && this._notification;
2064- let notificationMustClose = this._notificationRemoved || notificationLockedOut || (notificationExpired && this._userActiveWhileNotificationShown) || this._notificationClosed;
2065- let canShowNotification = notificationsPending && this._summaryState == State.HIDDEN;
2066-
2067- if (this._notificationState == State.HIDDEN) {
2068- if (canShowNotification) {
2069- this._showNotification();
2070- }
2071- } else if (this._notificationState == State.SHOWN) {
2072- if (notificationMustClose)
2073- this._hideNotification();
2074- else if (notificationPinned && !notificationExpanded)
2075- this._expandNotification(false);
2076- else if (notificationPinned)
2077- this._ensureNotificationFocused();
2078- }
2079-
2080- // Summary
2081- let summarySummoned = this._pointerInSummary || this._overviewVisible || this._traySummoned;
2082- let summaryPinned = this._pointerInTray || summarySummoned || this._locked;
2083- let summaryHovered = this._pointerInTray || this._pointerInSummary;
2084-
2085- let notificationsVisible = (this._notificationState == State.SHOWING ||
2086- this._notificationState == State.SHOWN);
2087- let notificationsDone = !notificationsVisible && !notificationsPending;
2088-
2089- let summaryOptionalInOverview = this._overviewVisible && !this._locked && !summaryHovered;
2090- let mustHideSummary = (notificationsPending && (notificationUrgent || summaryOptionalInOverview))
2091- || notificationsVisible || !Main.sessionMode.hasNotifications;
2092-
2093- if (this._summaryState == State.HIDDEN && !mustHideSummary && summarySummoned)
2094- this._showSummary();
2095- else if (this._summaryState == State.SHOWN && (!summaryPinned || mustHideSummary))
2096- this._hideSummary();
2097-
2098- // Summary notification
2099- let haveClickedSummaryItem = this._clickedSummaryItem != null;
2100- let summarySourceIsMainNotificationSource = (haveClickedSummaryItem && this._notification &&
2101- this._clickedSummaryItem.source == this._notification.source);
2102- let canShowSummaryBoxPointer = this._summaryState == State.SHOWN;
2103- // We only have sources with empty notification stacks for legacy tray icons. Currently, we never attempt
2104- // to show notifications for legacy tray icons, but this would be necessary if we did.
2105- let requestedNotificationStackIsEmpty = (this._clickedSummaryItemMouseButton == 1 && this._clickedSummaryItem.source.notifications.length == 0);
2106- let wrongSummaryNotificationStack = (this._clickedSummaryItemMouseButton == 1 &&
2107- this._summaryBoxPointer.bin.child != this._clickedSummaryItem.notificationStackWidget);
2108- let wrongSummaryRightClickMenu = (this._clickedSummaryItemMouseButton == 3 &&
2109- this._summaryBoxPointer.bin.child != this._clickedSummaryItem.rightClickMenu);
2110- let wrongSummaryBoxPointer = (haveClickedSummaryItem &&
2111- (wrongSummaryNotificationStack || wrongSummaryRightClickMenu));
2112-
2113- if (this._summaryBoxPointerState == State.HIDDEN) {
2114- if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer && !requestedNotificationStackIsEmpty)
2115- this._showSummaryBoxPointer();
2116- } else if (this._summaryBoxPointerState == State.SHOWN) {
2117- if (!haveClickedSummaryItem || !canShowSummaryBoxPointer || wrongSummaryBoxPointer || mustHideSummary) {
2118- this._hideSummaryBoxPointer();
2119- if (wrongSummaryBoxPointer)
2120- this._showSummaryBoxPointer();
2121- }
2122- }
2123-
2124- // Tray itself
2125- let trayIsVisible = (this._trayState == State.SHOWING ||
2126- this._trayState == State.SHOWN);
2127- let trayShouldBeVisible = (this._summaryState == State.SHOWING ||
2128- this._summaryState == State.SHOWN);
2129- if (!trayIsVisible && trayShouldBeVisible)
2130- trayShouldBeVisible = this._showTray();
2131- else if (trayIsVisible && !trayShouldBeVisible)
2132- this._hideTray();
2133-
2134- // Desktop clone
2135- let desktopCloneIsVisible = (this._desktopCloneState == State.SHOWING ||
2136- this._desktopCloneState == State.SHOWN);
2137- let desktopCloneShouldBeVisible = (trayShouldBeVisible &&
2138- !this._overviewVisible &&
2139- !this._keyboardVisible);
2140-
2141- if (!desktopCloneIsVisible && desktopCloneShouldBeVisible) {
2142- this._showDesktopClone();
2143- } else if (desktopCloneIsVisible && !desktopCloneShouldBeVisible) {
2144- this._hideDesktopClone (this._keyboardVisible);
2145- }
2146- },
2147-
2148- _tween: function(actor, statevar, value, params) {
2149- let onComplete = params.onComplete;
2150- let onCompleteScope = params.onCompleteScope;
2151- let onCompleteParams = params.onCompleteParams;
2152-
2153- params.onComplete = this._tweenComplete;
2154- params.onCompleteScope = this;
2155- params.onCompleteParams = [statevar, value, onComplete, onCompleteScope, onCompleteParams];
2156-
2157- // Remove other tweens that could mess with the state machine
2158- Tweener.removeTweens(actor);
2159- Tweener.addTween(actor, params);
2160-
2161- let valuing = (value == State.SHOWN) ? State.SHOWING : State.HIDING;
2162- this[statevar] = valuing;
2163- },
2164-
2165- _tweenComplete: function(statevar, value, onComplete, onCompleteScope, onCompleteParams) {
2166- this[statevar] = value;
2167- if (onComplete)
2168- onComplete.apply(onCompleteScope, onCompleteParams);
2169- this._updateState();
2170- },
2171-
2172- _showTray: function() {
2173- // Don't actually take a modal grab in the overview.
2174- // Just add something to the grab stack that we can
2175- // pop later.
2176- let modal = !this._overviewVisible;
2177-
2178- if (!this._grabHelper.grab({ actor: this.actor,
2179- modal: modal,
2180- onUngrab: Lang.bind(this, this._escapeTray) })) {
2181- this._traySummoned = false;
2182- return false;
2183- }
2184-
2185- this._tween(this.actor, '_trayState', State.SHOWN,
2186- { y: -this.actor.height,
2187- time: ANIMATION_TIME,
2188- transition: 'easeOutQuad'
2189- });
2190-
2191- if (!this._overviewVisible)
2192- this._lightbox.show();
2193-
2194- return true;
2195- },
2196-
2197- _updateDesktopCloneClip: function() {
2198- let geometry = this._bottomMonitorGeometry;
2199- let progress = -Math.round(this._desktopClone.y);
2200- this._desktopClone.set_clip(geometry.x,
2201- geometry.y + progress,
2202- geometry.width,
2203- geometry.height - progress);
2204- },
2205-
2206- _showDesktopClone: function() {
2207- let bottomMonitor = Main.layoutManager.bottomMonitor;
2208- this._bottomMonitorGeometry = { x: bottomMonitor.x,
2209- y: bottomMonitor.y,
2210- width: bottomMonitor.width,
2211- height: bottomMonitor.height };
2212-
2213- if (this._desktopClone)
2214- this._desktopClone.destroy();
2215- this._desktopClone = new Clutter.Clone({ source: global.window_group, clip: new Clutter.Geometry(this._bottomMonitorGeometry) });
2216- Main.uiGroup.insert_child_above(this._desktopClone, global.window_group);
2217- this._desktopClone.x = 0;
2218- this._desktopClone.y = 0;
2219- this._desktopClone.show();
2220-
2221- this._tween(this._desktopClone, '_desktopCloneState', State.SHOWN,
2222- { y: -this.actor.height,
2223- time: ANIMATION_TIME,
2224- transition: 'easeOutQuad',
2225- onUpdate: Lang.bind(this, this._updateDesktopCloneClip)
2226- });
2227- },
2228-
2229- _hideTray: function() {
2230- // Having the summary item animate out while sliding down the tray
2231- // is distracting, so hide it immediately in case it was visible.
2232- this._summaryBoxPointer.actor.hide();
2233-
2234- this._tween(this.actor, '_trayState', State.HIDDEN,
2235- { y: 0,
2236- time: ANIMATION_TIME,
2237- transition: 'easeOutQuad'
2238- });
2239-
2240- // Note that we might have entered here without a grab,
2241- // which would happen if GrabHelper ungrabbed for us.
2242- // This is a no-op in that case.
2243- this._grabHelper.ungrab({ actor: this.actor });
2244- this._lightbox.hide();
2245- },
2246-
2247- _hideDesktopClone: function(now) {
2248- this._tween(this._desktopClone, '_desktopCloneState', State.HIDDEN,
2249- { y: 0,
2250- time: now ? 0 : ANIMATION_TIME,
2251- transition: 'easeOutQuad',
2252- onComplete: Lang.bind(this, function() {
2253- this._desktopClone.destroy();
2254- this._desktopClone = null;
2255- this._bottomMonitorGeometry = null;
2256- }),
2257- onUpdate: Lang.bind(this, this._updateDesktopCloneClip)
2258- });
2259- },
2260-
2261- _onIdleMonitorWatch: function(monitor, id, userBecameIdle) {
2262- this.idleMonitor.remove_watch(this._idleMonitorWatchId);
2263- this._idleMonitorWatchId = 0;
2264- if (!userBecameIdle)
2265- this._updateNotificationTimeout(2000);
2266- this._userActiveWhileNotificationShown = true;
2267- this._updateState();
2268- },
2269-
2270- _showNotification: function() {
2271- this._notification = this._notificationQueue.shift();
2272- this._userActiveWhileNotificationShown = this.idleMonitor.get_idletime() <= IDLE_TIME;
2273- this._idleMonitorWatchId = this.idleMonitor.add_watch(IDLE_TIME,
2274- Lang.bind(this, this._onIdleMonitorWatch));
2275- this._notificationClickedId = this._notification.connect('done-displaying',
2276- Lang.bind(this, this._escapeTray));
2277- this._notification.connect('unfocused', Lang.bind(this, function() {
2278- this._updateState();
2279- }));
2280- this._notificationBin.child = this._notification.actor;
2281-
2282- this._notificationWidget.opacity = 0;
2283- this._notificationWidget.y = 0;
2284- this._notificationWidget.show();
2285-
2286- this._updateShowingNotification();
2287-
2288- let [x, y, mods] = global.get_pointer();
2289- // We save the position of the mouse at the time when we started showing the notification
2290- // in order to determine if the notification popped up under it. We make that check if
2291- // the user starts moving the mouse and _onTrayHoverChanged() gets called. We don't
2292- // expand the notification if it just happened to pop up under the mouse unless the user
2293- // explicitly mouses away from it and then mouses back in.
2294- this._showNotificationMouseX = x;
2295- this._showNotificationMouseY = y;
2296- // We save the coordinates of the mouse at the time when we started showing the notification
2297- // and then we update it in _notificationTimeout(). We don't pop down the notification if
2298- // the mouse is moving towards it or within it.
2299- this._lastSeenMouseX = x;
2300- this._lastSeenMouseY = y;
2301- },
2302-
2303- _updateShowingNotification: function() {
2304- this._notification.acknowledged = true;
2305-
2306- // We auto-expand notifications with CRITICAL urgency.
2307- if (this._notification.urgency == Urgency.CRITICAL)
2308- this._expandNotification(true);
2309-
2310- // We tween all notifications to full opacity. This ensures that both new notifications and
2311- // notifications that might have been in the process of hiding get full opacity.
2312- //
2313- // We tween any notification showing in the banner mode to the appropriate height
2314- // (which is banner height or expanded height, depending on the notification state)
2315- // This ensures that both new notifications and notifications in the banner mode that might
2316- // have been in the process of hiding are shown with the correct height.
2317- //
2318- // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
2319- // notification is being shown.
2320-
2321- let tweenParams = { opacity: 255,
2322- y: -this._notificationWidget.height,
2323- time: ANIMATION_TIME,
2324- transition: 'easeOutQuad',
2325- onComplete: this._showNotificationCompleted,
2326- onCompleteScope: this
2327- };
2328-
2329- this._tween(this._notificationWidget, '_notificationState', State.SHOWN, tweenParams);
2330- },
2331-
2332- _showNotificationCompleted: function() {
2333- if (this._notification.urgency != Urgency.CRITICAL)
2334- this._updateNotificationTimeout(NOTIFICATION_TIMEOUT * 1000);
2335- },
2336-
2337- _updateNotificationTimeout: function(timeout) {
2338- if (this._notificationTimeoutId) {
2339- Mainloop.source_remove(this._notificationTimeoutId);
2340- this._notificationTimeoutId = 0;
2341- }
2342- if (timeout > 0)
2343- this._notificationTimeoutId =
2344- Mainloop.timeout_add(timeout,
2345- Lang.bind(this, this._notificationTimeout));
2346- },
2347-
2348- _notificationTimeout: function() {
2349- let [x, y, mods] = global.get_pointer();
2350- if (y > this._lastSeenMouseY + 10 && !this.actor.hover) {
2351- // The mouse is moving towards the notification, so don't
2352- // hide it yet. (We just create a new timeout (and destroy
2353- // the old one) each time because the bookkeeping is
2354- // simpler.)
2355- this._updateNotificationTimeout(1000);
2356- } else if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId &&
2357- (x != this._lastSeenMouseX || y != this._lastSeenMouseY)) {
2358- // Refresh the timeout if the notification originally
2359- // popped up under the pointer, and the pointer is hovering
2360- // inside it.
2361- this._updateNotificationTimeout(1000);
2362- } else {
2363- this._notificationTimeoutId = 0;
2364- this._updateState();
2365- }
2366-
2367- this._lastSeenMouseX = x;
2368- this._lastSeenMouseY = y;
2369- return false;
2370- },
2371-
2372- _hideNotification: function() {
2373- this._grabHelper.ungrab({ actor: this._notification.actor });
2374-
2375- if (this._notificationExpandedId) {
2376- this._notification.disconnect(this._notificationExpandedId);
2377- this._notificationExpandedId = 0;
2378- }
2379-
2380- if (this._notificationRemoved) {
2381- this._notificationWidget.y = this.actor.height;
2382- this._notificationWidget.opacity = 0;
2383- this._notificationState = State.HIDDEN;
2384- this._hideNotificationCompleted();
2385- } else {
2386- this._tween(this._notificationWidget, '_notificationState', State.HIDDEN,
2387- { y: this.actor.height,
2388- opacity: 0,
2389- time: ANIMATION_TIME,
2390- transition: 'easeOutQuad',
2391- onComplete: this._hideNotificationCompleted,
2392- onCompleteScope: this
2393- });
2394-
2395- }
2396- },
2397-
2398- _hideNotificationCompleted: function() {
2399- this._notificationRemoved = false;
2400- this._notificationWidget.hide();
2401- this._closeButton.hide();
2402- this._pointerInTray = false;
2403- this.actor.hover = false; // Clutter doesn't emit notify::hover when actors move
2404- this._notificationBin.child = null;
2405- this._notification.collapseCompleted();
2406- this._notification.disconnect(this._notificationClickedId);
2407- this._notificationClickedId = 0;
2408- let notification = this._notification;
2409- this._notification = null;
2410- if (notification.isTransient)
2411- notification.destroy(NotificationDestroyedReason.EXPIRED);
2412- },
2413-
2414- _expandNotification: function(autoExpanding) {
2415- // Don't grab focus in notifications that are auto-expanded.
2416- if (!autoExpanding)
2417- this._grabHelper.grab({ actor: this._notification.actor,
2418- grabFocus: true });
2419-
2420- if (!this._notificationExpandedId)
2421- this._notificationExpandedId =
2422- this._notification.connect('expanded',
2423- Lang.bind(this, this._onNotificationExpanded));
2424- // Don't animate changes in notifications that are auto-expanding.
2425- this._notification.expand(!autoExpanding);
2426- },
2427-
2428- _onNotificationExpanded: function() {
2429- let expandedY = - this._notificationWidget.height;
2430- this._closeButton.show();
2431-
2432- // Don't animate the notification to its new position if it has shrunk:
2433- // there will be a very visible "gap" that breaks the illusion.
2434- if (this._notificationWidget.y < expandedY) {
2435- this._notificationWidget.y = expandedY;
2436- } else if (this._notification.y != expandedY) {
2437- // Tween also opacity here, to override a possible tween that's
2438- // currently hiding the notification. This will ensure that the
2439- // notification is not removed when the onComplete handler for this
2440- // one triggers.
2441- this._tween(this._notificationWidget, '_notificationState', State.SHOWN,
2442- { y: expandedY,
2443- opacity: 255,
2444- time: ANIMATION_TIME,
2445- transition: 'easeOutQuad'
2446- });
2447- }
2448- },
2449-
2450- // We use this function to grab focus when the user moves the pointer
2451- // to a notification with CRITICAL urgency that was already auto-expanded.
2452- _ensureNotificationFocused: function() {
2453- this._grabHelper.grab({ actor: this._notification.actor,
2454- grabFocus: true });
2455- },
2456-
2457- _showSummary: function() {
2458- this._summary.opacity = 0;
2459- this._tween(this._summary, '_summaryState', State.SHOWN,
2460- { opacity: 255,
2461- time: ANIMATION_TIME,
2462- transition: 'easeOutQuad',
2463- });
2464- },
2465-
2466- _hideSummary: function() {
2467- this._tween(this._summary, '_summaryState', State.HIDDEN,
2468- { opacity: 0,
2469- time: ANIMATION_TIME,
2470- transition: 'easeOutQuad',
2471- });
2472- },
2473-
2474- _showSummaryBoxPointer: function() {
2475- this._summaryBoxPointerItem = this._clickedSummaryItem;
2476- this._summaryBoxPointerContentUpdatedId = this._summaryBoxPointerItem.connect('content-updated',
2477- Lang.bind(this, this._onSummaryBoxPointerContentUpdated));
2478- this._sourceDoneDisplayingId = this._summaryBoxPointerItem.source.connect('done-displaying-content',
2479- Lang.bind(this, this._escapeTray));
2480-
2481- let hasRightClickMenu = this._summaryBoxPointerItem.rightClickMenu != null;
2482- if (this._clickedSummaryItemMouseButton == 1 || !hasRightClickMenu) {
2483- let newQueue = [];
2484- for (let i = 0; i < this._notificationQueue.length; i++) {
2485- let notification = this._notificationQueue[i];
2486- let sameSource = this._summaryBoxPointerItem.source == notification.source;
2487- if (sameSource)
2488- notification.acknowledged = true;
2489- else
2490- newQueue.push(notification);
2491- }
2492- this._notificationQueue = newQueue;
2493-
2494- this._summaryBoxPointer.bin.child = this._summaryBoxPointerItem.notificationStackWidget;
2495-
2496- let closeButton = this._summaryBoxPointerItem.closeButton;
2497- closeButton.show();
2498- this._summaryBoxPointerCloseClickedId = closeButton.connect('clicked', Lang.bind(this, this._hideSummaryBoxPointer));
2499- this._summaryBoxPointerItem.prepareNotificationStackForShowing();
2500- } else if (this._clickedSummaryItemMouseButton == 3) {
2501- this._summaryBoxPointer.bin.child = this._clickedSummaryItem.rightClickMenu;
2502- }
2503-
2504- this._grabHelper.grab({ actor: this._summaryBoxPointer.bin.child,
2505- grabFocus: true,
2506- onUngrab: Lang.bind(this, this._onSummaryBoxPointerUngrabbed) });
2507- this._lock();
2508-
2509- this._summaryBoxPointer.actor.opacity = 0;
2510- this._summaryBoxPointer.actor.show();
2511- this._adjustSummaryBoxPointerPosition();
2512-
2513- this._summaryBoxPointerState = State.SHOWING;
2514- this._clickedSummaryItem.actor.add_style_pseudo_class('selected');
2515- this._summaryBoxPointer.show(BoxPointer.PopupAnimation.FULL, Lang.bind(this, function() {
2516- this._summaryBoxPointerState = State.SHOWN;
2517- }));
2518- },
2519-
2520- _onSummaryBoxPointerContentUpdated: function() {
2521- if (this._summaryBoxPointerItem.notificationStack.get_n_children() == 0)
2522- this._hideSummaryBoxPointer();
2523- this._adjustSummaryBoxPointerPosition();
2524- },
2525-
2526- _adjustSummaryBoxPointerPosition: function() {
2527- if (!this._clickedSummaryItem)
2528- return;
2529-
2530- this._summaryBoxPointer.setPosition(this._clickedSummaryItem.actor, 0);
2531- },
2532-
2533- _setClickedSummaryItem: function(item, button) {
2534- if (item == this._clickedSummaryItem &&
2535- button == this._clickedSummaryItemMouseButton)
2536- return false;
2537-
2538- if (this._clickedSummaryItem) {
2539- this._clickedSummaryItem.actor.remove_style_pseudo_class('selected');
2540- this._clickedSummaryItem.actor.disconnect(this._clickedSummaryItemAllocationChangedId);
2541- this._summary.disconnect(this._summaryMotionId);
2542- Main.layoutManager.trayBox.disconnect(this._trayMotionId);
2543- this._clickedSummaryItemAllocationChangedId = 0;
2544- this._summaryMotionId = 0;
2545- this._trayMotionId = 0;
2546- }
2547-
2548- this._clickedSummaryItem = item;
2549- this._clickedSummaryItemMouseButton = button;
2550-
2551- if (this._clickedSummaryItem) {
2552- this._clickedSummaryItem.source.emit('summary-item-clicked', button);
2553- this._clickedSummaryItem.actor.add_style_pseudo_class('selected');
2554- this._clickedSummaryItemAllocationChangedId =
2555- this._clickedSummaryItem.actor.connect('allocation-changed',
2556- Lang.bind(this, this._adjustSummaryBoxPointerPosition));
2557- // _clickedSummaryItem.actor can change absolute position without changing allocation
2558- this._summaryMotionId = this._summary.connect('allocation-changed',
2559- Lang.bind(this, this._adjustSummaryBoxPointerPosition));
2560- this._trayMotionId = Main.layoutManager.trayBox.connect('notify::anchor-y',
2561- Lang.bind(this, this._adjustSummaryBoxPointerPosition));
2562- }
2563-
2564- return true;
2565- },
2566-
2567- _onSummaryBoxPointerKeyPress: function(actor, event) {
2568- switch (event.get_key_symbol()) {
2569- case Clutter.KEY_Down:
2570- case Clutter.KEY_Escape:
2571- this._setClickedSummaryItem(null);
2572- this._updateState();
2573- return true;
2574- case Clutter.KEY_Delete:
2575- this._clickedSummaryItem.source.destroy();
2576- this._escapeTray();
2577- return true;
2578- }
2579- return false;
2580- },
2581-
2582- _onSummaryBoxPointerUngrabbed: function() {
2583- this._summaryBoxPointerState = State.HIDING;
2584- this._unlock();
2585-
2586- if (this._summaryBoxPointerItem.source.notifications.length == 0) {
2587- this._summaryBoxPointer.actor.hide();
2588- this._hideSummaryBoxPointerCompleted();
2589- } else {
2590- if (global.stage.key_focus &&
2591- !this.actor.contains(global.stage.key_focus))
2592- this._setClickedSummaryItem(null);
2593- this._summaryBoxPointer.hide(BoxPointer.PopupAnimation.FULL, Lang.bind(this, this._hideSummaryBoxPointerCompleted));
2594- }
2595- },
2596-
2597- _hideSummaryBoxPointer: function() {
2598- this._grabHelper.ungrab({ actor: this._summaryBoxPointer.bin.child });
2599- },
2600-
2601- _hideSummaryBoxPointerCompleted: function() {
2602- let doneShowingNotificationStack = (this._summaryBoxPointer.bin.child == this._summaryBoxPointerItem.notificationStackWidget);
2603-
2604- this._summaryBoxPointerState = State.HIDDEN;
2605- this._summaryBoxPointer.bin.child = null;
2606- this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerContentUpdatedId);
2607- this._summaryBoxPointerContentUpdatedId = 0;
2608- this._summaryBoxPointerItem.closeButton.disconnect(this._summaryBoxPointerCloseClickedId);
2609- this._summaryBoxPointerCloseClickedId = 0;
2610- this._summaryBoxPointerItem.source.disconnect(this._sourceDoneDisplayingId);
2611- this._summaryBoxPointerDoneDisplayingId = 0;
2612-
2613- let sourceNotificationStackDoneShowing = null;
2614- if (doneShowingNotificationStack) {
2615- this._summaryBoxPointerItem.doneShowingNotificationStack();
2616- sourceNotificationStackDoneShowing = this._summaryBoxPointerItem.source;
2617- }
2618-
2619- this._summaryBoxPointerItem = null;
2620-
2621- if (sourceNotificationStackDoneShowing) {
2622- if (sourceNotificationStackDoneShowing.isTransient && !this._reNotifyAfterHideNotification)
2623- sourceNotificationStackDoneShowing.destroy(NotificationDestroyedReason.EXPIRED);
2624- if (this._reNotifyAfterHideNotification) {
2625- this._onNotify(this._reNotifyAfterHideNotification.source, this._reNotifyAfterHideNotification);
2626- this._reNotifyAfterHideNotification = null;
2627- }
2628- }
2629-
2630- if (this._clickedSummaryItem)
2631- this._updateState();
2632- }
2633-});
2634-Signals.addSignalMethods(MessageTray.prototype);
2635-
2636-const SystemNotificationSource = new Lang.Class({
2637- Name: 'SystemNotificationSource',
2638- Extends: Source,
2639-
2640- _init: function() {
2641- this.parent(_("System Information"), 'dialog-information-symbolic');
2642- this.setTransient(true);
2643- },
2644-
2645- open: function() {
2646- this.destroy();
2647- }
2648-});
2649
2650=== removed directory '.pc/git_messagetray_opacity_fix.patch'
2651=== removed directory '.pc/git_messagetray_opacity_fix.patch/js'
2652=== removed directory '.pc/git_messagetray_opacity_fix.patch/js/ui'
2653=== removed file '.pc/git_messagetray_opacity_fix.patch/js/ui/messageTray.js'
2654--- .pc/git_messagetray_opacity_fix.patch/js/ui/messageTray.js 2012-11-27 08:27:57 +0000
2655+++ .pc/git_messagetray_opacity_fix.patch/js/ui/messageTray.js 1970-01-01 00:00:00 +0000
2656@@ -1,2550 +0,0 @@
2657-// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
2658-
2659-const Clutter = imports.gi.Clutter;
2660-const GLib = imports.gi.GLib;
2661-const Gio = imports.gi.Gio;
2662-const Gtk = imports.gi.Gtk;
2663-const Atk = imports.gi.Atk;
2664-const Lang = imports.lang;
2665-const Mainloop = imports.mainloop;
2666-const Meta = imports.gi.Meta;
2667-const Pango = imports.gi.Pango;
2668-const Shell = imports.gi.Shell;
2669-const Signals = imports.signals;
2670-const St = imports.gi.St;
2671-
2672-const BoxPointer = imports.ui.boxpointer;
2673-const CtrlAltTab = imports.ui.ctrlAltTab;
2674-const GnomeSession = imports.misc.gnomeSession;
2675-const GrabHelper = imports.ui.grabHelper;
2676-const Lightbox = imports.ui.lightbox;
2677-const Main = imports.ui.main;
2678-const PointerWatcher = imports.ui.pointerWatcher;
2679-const PopupMenu = imports.ui.popupMenu;
2680-const Params = imports.misc.params;
2681-const Tweener = imports.ui.tweener;
2682-const Util = imports.misc.util;
2683-
2684-const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
2685-
2686-const ANIMATION_TIME = 0.2;
2687-const NOTIFICATION_TIMEOUT = 4;
2688-const SUMMARY_TIMEOUT = 1;
2689-const LONGER_SUMMARY_TIMEOUT = 4;
2690-
2691-const HIDE_TIMEOUT = 0.2;
2692-const LONGER_HIDE_TIMEOUT = 0.6;
2693-
2694-const NOTIFICATION_ICON_SIZE = 24;
2695-
2696-// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
2697-// range from the point where it left the tray.
2698-const MOUSE_LEFT_ACTOR_THRESHOLD = 20;
2699-
2700-// Time the user needs to leave the mouse on the bottom pixel row to open the tray
2701-const TRAY_DWELL_TIME = 1000; // ms
2702-// Time resolution when tracking the mouse to catch the open tray dwell
2703-const TRAY_DWELL_CHECK_INTERVAL = 100; // ms
2704-
2705-const IDLE_TIME = 1000;
2706-
2707-const State = {
2708- HIDDEN: 0,
2709- SHOWING: 1,
2710- SHOWN: 2,
2711- HIDING: 3
2712-};
2713-
2714-// These reasons are useful when we destroy the notifications received through
2715-// the notification daemon. We use EXPIRED for transient notifications that the
2716-// user did not interact with, DISMISSED for all other notifications that were
2717-// destroyed as a result of a user action, and SOURCE_CLOSED for the notifications
2718-// that were requested to be destroyed by the associated source.
2719-const NotificationDestroyedReason = {
2720- EXPIRED: 1,
2721- DISMISSED: 2,
2722- SOURCE_CLOSED: 3
2723-};
2724-
2725-// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
2726-// urgency values map to the corresponding values for the notifications received
2727-// through the notification daemon. HIGH urgency value is used for chats received
2728-// through the Telepathy client.
2729-const Urgency = {
2730- LOW: 0,
2731- NORMAL: 1,
2732- HIGH: 2,
2733- CRITICAL: 3
2734-}
2735-
2736-function _fixMarkup(text, allowMarkup) {
2737- if (allowMarkup) {
2738- // Support &amp;, &quot;, &apos;, &lt; and &gt;, escape all other
2739- // occurrences of '&'.
2740- let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&amp;');
2741-
2742- // Support <b>, <i>, and <u>, escape anything else
2743- // so it displays as raw markup.
2744- _text = _text.replace(/<(?!\/?[biu]>)/g, '&lt;');
2745-
2746- try {
2747- Pango.parse_markup(_text, -1, '');
2748- return _text;
2749- } catch (e) {}
2750- }
2751-
2752- // !allowMarkup, or invalid markup
2753- return GLib.markup_escape_text(text, -1);
2754-}
2755-
2756-const URLHighlighter = new Lang.Class({
2757- Name: 'URLHighlighter',
2758-
2759- _init: function(text, lineWrap, allowMarkup) {
2760- if (!text)
2761- text = '';
2762- this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' });
2763- this._linkColor = '#ccccff';
2764- this.actor.connect('style-changed', Lang.bind(this, function() {
2765- let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false);
2766- if (hasColor) {
2767- let linkColor = color.to_string().substr(0, 7);
2768- if (linkColor != this._linkColor) {
2769- this._linkColor = linkColor;
2770- this._highlightUrls();
2771- }
2772- }
2773- }));
2774- if (lineWrap) {
2775- this.actor.clutter_text.line_wrap = true;
2776- this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
2777- this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
2778- }
2779-
2780- this.setMarkup(text, allowMarkup);
2781- this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) {
2782- // Don't try to URL highlight when invisible.
2783- // The MessageTray doesn't actually hide us, so
2784- // we need to check for paint opacities as well.
2785- if (!actor.visible || actor.get_paint_opacity() == 0)
2786- return false;
2787-
2788- // Keep Notification.actor from seeing this and taking
2789- // a pointer grab, which would block our button-release-event
2790- // handler, if an URL is clicked
2791- return this._findUrlAtPos(event) != -1;
2792- }));
2793- this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) {
2794- if (!actor.visible || actor.get_paint_opacity() == 0)
2795- return false;
2796-
2797- let urlId = this._findUrlAtPos(event);
2798- if (urlId != -1) {
2799- let url = this._urls[urlId].url;
2800- if (url.indexOf(':') == -1)
2801- url = 'http://' + url;
2802- try {
2803- Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context());
2804- return true;
2805- } catch (e) {
2806- // TODO: remove this after gnome 3 release
2807- Util.spawn(['gvfs-open', url]);
2808- return true;
2809- }
2810- }
2811- return false;
2812- }));
2813- this.actor.connect('motion-event', Lang.bind(this, function(actor, event) {
2814- if (!actor.visible || actor.get_paint_opacity() == 0)
2815- return false;
2816-
2817- let urlId = this._findUrlAtPos(event);
2818- if (urlId != -1 && !this._cursorChanged) {
2819- global.set_cursor(Shell.Cursor.POINTING_HAND);
2820- this._cursorChanged = true;
2821- } else if (urlId == -1) {
2822- global.unset_cursor();
2823- this._cursorChanged = false;
2824- }
2825- return false;
2826- }));
2827- this.actor.connect('leave-event', Lang.bind(this, function() {
2828- if (!this.actor.visible || this.actor.get_paint_opacity() == 0)
2829- return;
2830-
2831- if (this._cursorChanged) {
2832- this._cursorChanged = false;
2833- global.unset_cursor();
2834- }
2835- }));
2836- },
2837-
2838- setMarkup: function(text, allowMarkup) {
2839- text = text ? _fixMarkup(text, allowMarkup) : '';
2840- this._text = text;
2841-
2842- this.actor.clutter_text.set_markup(text);
2843- /* clutter_text.text contain text without markup */
2844- this._urls = Util.findUrls(this.actor.clutter_text.text);
2845- this._highlightUrls();
2846- },
2847-
2848- _highlightUrls: function() {
2849- // text here contain markup
2850- let urls = Util.findUrls(this._text);
2851- let markup = '';
2852- let pos = 0;
2853- for (let i = 0; i < urls.length; i++) {
2854- let url = urls[i];
2855- let str = this._text.substr(pos, url.pos - pos);
2856- markup += str + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>';
2857- pos = url.pos + url.url.length;
2858- }
2859- markup += this._text.substr(pos);
2860- this.actor.clutter_text.set_markup(markup);
2861- },
2862-
2863- _findUrlAtPos: function(event) {
2864- let success;
2865- let [x, y] = event.get_coords();
2866- [success, x, y] = this.actor.transform_stage_point(x, y);
2867- let find_pos = -1;
2868- for (let i = 0; i < this.actor.clutter_text.text.length; i++) {
2869- let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i);
2870- if (py > y || py + line_height < y || x < px)
2871- continue;
2872- find_pos = i;
2873- }
2874- if (find_pos != -1) {
2875- for (let i = 0; i < this._urls.length; i++)
2876- if (find_pos >= this._urls[i].pos &&
2877- this._urls[i].pos + this._urls[i].url.length > find_pos)
2878- return i;
2879- }
2880- return -1;
2881- }
2882-});
2883-
2884-function makeCloseButton() {
2885- let closeButton = new St.Button({ style_class: 'notification-close'});
2886-
2887- // This is a bit tricky. St.Bin has its own x-align/y-align properties
2888- // that compete with Clutter's properties. This should be fixed for
2889- // Clutter 2.0. Since St.Bin doesn't define its own setters, the
2890- // setters are a workaround to get Clutter's version.
2891- closeButton.set_x_align(Clutter.ActorAlign.END);
2892- closeButton.set_y_align(Clutter.ActorAlign.START);
2893-
2894- // XXX Clutter 2.0 workaround: ClutterBinLayout needs expand
2895- // to respect the alignments.
2896- closeButton.set_x_expand(true);
2897- closeButton.set_y_expand(true);
2898-
2899- closeButton.connect('style-changed', function() {
2900- let themeNode = closeButton.get_theme_node();
2901-
2902- // libcroco doesn't support negative units
2903- let direction = closeButton.get_text_direction() == Clutter.TextDirection.RTL ? -1 : 1;
2904- closeButton.translation_x = direction * themeNode.get_length('-shell-close-overlap-x');
2905-
2906- // libcroco doesn't support negative units
2907- closeButton.translation_y = -themeNode.get_length('-shell-close-overlap-y');
2908- });
2909-
2910- return closeButton;
2911-}
2912-
2913-// Notification:
2914-// @source: the notification's Source
2915-// @title: the title
2916-// @banner: the banner text
2917-// @params: optional additional params
2918-//
2919-// Creates a notification. In the banner mode, the notification
2920-// will show an icon, @title (in bold) and @banner, all on a single
2921-// line (with @banner ellipsized if necessary).
2922-//
2923-// The notification will be expandable if either it has additional
2924-// elements that were added to it or if the @banner text did not
2925-// fit fully in the banner mode. When the notification is expanded,
2926-// the @banner text from the top line is always removed. The complete
2927-// @banner text is added as the first element in the content section,
2928-// unless 'customContent' parameter with the value 'true' is specified
2929-// in @params.
2930-//
2931-// Additional notification content can be added with addActor() and
2932-// addBody() methods. The notification content is put inside a
2933-// scrollview, so if it gets too tall, the notification will scroll
2934-// rather than continue to grow. In addition to this main content
2935-// area, there is also a single-row action area, which is not
2936-// scrolled and can contain a single actor. The action area can
2937-// be set by calling setActionArea() method. There is also a
2938-// convenience method addButton() for adding a button to the action
2939-// area.
2940-//
2941-// @params can contain values for 'customContent', 'body', 'icon',
2942-// 'titleMarkup', 'bannerMarkup', 'bodyMarkup', and 'clear'
2943-// parameters.
2944-//
2945-// If @params contains a 'customContent' parameter with the value %true,
2946-// then @banner will not be shown in the body of the notification when the
2947-// notification is expanded and calls to update() will not clear the content
2948-// unless 'clear' parameter with value %true is explicitly specified.
2949-//
2950-// If @params contains a 'body' parameter, then that text will be added to
2951-// the content area (as with addBody()).
2952-//
2953-// By default, the icon shown is created by calling
2954-// source.createIcon(). However, if @params contains an 'icon'
2955-// parameter, the passed in icon will be used.
2956-//
2957-// If @params contains a 'titleMarkup', 'bannerMarkup', or
2958-// 'bodyMarkup' parameter with the value %true, then the corresponding
2959-// element is assumed to use pango markup. If the parameter is not
2960-// present for an element, then anything that looks like markup in
2961-// that element will appear literally in the output.
2962-//
2963-// If @params contains a 'clear' parameter with the value %true, then
2964-// the content and the action area of the notification will be cleared.
2965-// The content area is also always cleared if 'customContent' is false
2966-// because it might contain the @banner that didn't fit in the banner mode.
2967-const Notification = new Lang.Class({
2968- Name: 'Notification',
2969-
2970- IMAGE_SIZE: 125,
2971-
2972- _init: function(source, title, banner, params) {
2973- this.source = source;
2974- this.title = title;
2975- this.urgency = Urgency.NORMAL;
2976- this.resident = false;
2977- // 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
2978- this.isTransient = false;
2979- this.expanded = false;
2980- this.focused = false;
2981- this.acknowledged = false;
2982- this._destroyed = false;
2983- this._useActionIcons = false;
2984- this._customContent = false;
2985- this._bannerBodyText = null;
2986- this._bannerBodyMarkup = false;
2987- this._titleFitsInBannerMode = true;
2988- this._titleDirection = Clutter.TextDirection.DEFAULT;
2989- this._spacing = 0;
2990- this._scrollPolicy = Gtk.PolicyType.AUTOMATIC;
2991- this._imageBin = null;
2992-
2993- source.connect('destroy', Lang.bind(this,
2994- function (source, reason) {
2995- this.destroy(reason);
2996- }));
2997-
2998- this.actor = new St.Button({ accessible_role: Atk.Role.NOTIFICATION });
2999- this.actor.add_style_class_name('notification-unexpanded');
3000- this.actor._delegate = this;
3001- this.actor.connect('clicked', Lang.bind(this, this._onClicked));
3002- this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
3003-
3004- this._table = new St.Table({ style_class: 'notification',
3005- reactive: true });
3006- this._table.connect('style-changed', Lang.bind(this, this._styleChanged));
3007- this.actor.set_child(this._table);
3008-
3009- // The first line should have the title, followed by the
3010- // banner text, but ellipsized if they won't both fit. We can't
3011- // make St.Table or St.BoxLayout do this the way we want (don't
3012- // show banner at all if title needs to be ellipsized), so we
3013- // use Shell.GenericContainer.
3014- this._bannerBox = new Shell.GenericContainer();
3015- this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
3016- this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
3017- this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
3018- this._table.add(this._bannerBox, { row: 0,
3019- col: 1,
3020- col_span: 2,
3021- x_expand: false,
3022- y_expand: false,
3023- y_fill: false });
3024-
3025- // This is an empty cell that overlaps with this._bannerBox cell to ensure
3026- // that this._bannerBox cell expands horizontally, while not forcing the
3027- // this._imageBin that is also in col: 2 to expand horizontally.
3028- this._table.add(new St.Bin(), { row: 0,
3029- col: 2,
3030- y_expand: false,
3031- y_fill: false });
3032-
3033- this._titleLabel = new St.Label();
3034- this._bannerBox.add_actor(this._titleLabel);
3035- this._bannerUrlHighlighter = new URLHighlighter();
3036- this._bannerLabel = this._bannerUrlHighlighter.actor;
3037- this._bannerBox.add_actor(this._bannerLabel);
3038-
3039- this.update(title, banner, params);
3040- },
3041-
3042- // update:
3043- // @title: the new title
3044- // @banner: the new banner
3045- // @params: as in the Notification constructor
3046- //
3047- // Updates the notification by regenerating its icon and updating
3048- // the title/banner. If @params.clear is %true, it will also
3049- // remove any additional actors/action buttons previously added.
3050- update: function(title, banner, params) {
3051- params = Params.parse(params, { customContent: false,
3052- body: null,
3053- icon: null,
3054- secondaryIcon: null,
3055- titleMarkup: false,
3056- bannerMarkup: false,
3057- bodyMarkup: false,
3058- clear: false });
3059-
3060- this._customContent = params.customContent;
3061-
3062- let oldFocus = global.stage.key_focus;
3063-
3064- if (this._icon && (params.icon || params.clear)) {
3065- this._icon.destroy();
3066- this._icon = null;
3067- }
3068-
3069- if (this._secondaryIcon && (params.secondaryIcon || params.clear)) {
3070- this._secondaryIcon.destroy();
3071- this._secondaryIcon = null;
3072- }
3073-
3074- // We always clear the content area if we don't have custom
3075- // content because it might contain the @banner that didn't
3076- // fit in the banner mode.
3077- if (this._scrollArea && (!this._customContent || params.clear)) {
3078- if (oldFocus && this._scrollArea.contains(oldFocus))
3079- this.actor.grab_key_focus();
3080-
3081- this._scrollArea.destroy();
3082- this._scrollArea = null;
3083- this._contentArea = null;
3084- }
3085- if (this._actionArea && params.clear) {
3086- if (oldFocus && this._actionArea.contains(oldFocus))
3087- this.actor.grab_key_focus();
3088-
3089- this._actionArea.destroy();
3090- this._actionArea = null;
3091- this._buttonBox = null;
3092- }
3093- if (this._imageBin && params.clear)
3094- this.unsetImage();
3095-
3096- if (!this._scrollArea && !this._actionArea && !this._imageBin)
3097- this._table.remove_style_class_name('multi-line-notification');
3098-
3099- if (!this._icon) {
3100- this._icon = params.icon || this.source.createIcon(NOTIFICATION_ICON_SIZE);
3101- this._table.add(this._icon, { row: 0,
3102- col: 0,
3103- x_expand: false,
3104- y_expand: false,
3105- y_fill: false,
3106- y_align: St.Align.START });
3107- }
3108-
3109- if (!this._secondaryIcon) {
3110- this._secondaryIcon = params.secondaryIcon;
3111-
3112- if (this._secondaryIcon)
3113- this._bannerBox.add_actor(this._secondaryIcon);
3114- }
3115-
3116- this.title = title;
3117- title = title ? _fixMarkup(title.replace(/\n/g, ' '), params.titleMarkup) : '';
3118- this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>');
3119-
3120- if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL)
3121- this._titleDirection = Clutter.TextDirection.RTL;
3122- else
3123- this._titleDirection = Clutter.TextDirection.LTR;
3124-
3125- // Let the title's text direction control the overall direction
3126- // of the notification - in case where different scripts are used
3127- // in the notification, this is the right thing for the icon, and
3128- // arguably for action buttons as well. Labels other than the title
3129- // will be allocated at the available width, so that their alignment
3130- // is done correctly automatically.
3131- this._table.set_text_direction(this._titleDirection);
3132-
3133- // Unless the notification has custom content, we save this._bannerBodyText
3134- // to add it to the content of the notification if the notification is
3135- // expandable due to other elements in its content area or due to the banner
3136- // not fitting fully in the single-line mode.
3137- this._bannerBodyText = this._customContent ? null : banner;
3138- this._bannerBodyMarkup = params.bannerMarkup;
3139-
3140- banner = banner ? banner.replace(/\n/g, ' ') : '';
3141-
3142- this._bannerUrlHighlighter.setMarkup(banner, params.bannerMarkup);
3143- this._bannerLabel.queue_relayout();
3144-
3145- // Add the bannerBody now if we know for sure we'll need it
3146- if (this._bannerBodyText && this._bannerBodyText.indexOf('\n') > -1)
3147- this._addBannerBody();
3148-
3149- if (params.body)
3150- this.addBody(params.body, params.bodyMarkup);
3151- this.updated();
3152- },
3153-
3154- setIconVisible: function(visible) {
3155- this._icon.visible = visible;
3156- },
3157-
3158- enableScrolling: function(enableScrolling) {
3159- this._scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
3160- if (this._scrollArea) {
3161- this._scrollArea.vscrollbar_policy = this._scrollPolicy;
3162- this._scrollArea.enable_mouse_scrolling = enableScrolling;
3163- }
3164- },
3165-
3166- _createScrollArea: function() {
3167- this._table.add_style_class_name('multi-line-notification');
3168- this._scrollArea = new St.ScrollView({ style_class: 'notification-scrollview',
3169- vscrollbar_policy: this._scrollPolicy,
3170- hscrollbar_policy: Gtk.PolicyType.NEVER });
3171- this._table.add(this._scrollArea, { row: 1,
3172- col: 2 });
3173- this._updateLastColumnSettings();
3174- this._contentArea = new St.BoxLayout({ style_class: 'notification-body',
3175- vertical: true });
3176- this._scrollArea.add_actor(this._contentArea);
3177- // If we know the notification will be expandable, we need to add
3178- // the banner text to the body as the first element.
3179- this._addBannerBody();
3180- },
3181-
3182- // addActor:
3183- // @actor: actor to add to the body of the notification
3184- //
3185- // Appends @actor to the notification's body
3186- addActor: function(actor, style) {
3187- if (!this._scrollArea) {
3188- this._createScrollArea();
3189- }
3190-
3191- this._contentArea.add(actor, style ? style : {});
3192- this.updated();
3193- },
3194-
3195- // addBody:
3196- // @text: the text
3197- // @markup: %true if @text contains pango markup
3198- // @style: style to use when adding the actor containing the text
3199- //
3200- // Adds a multi-line label containing @text to the notification.
3201- //
3202- // Return value: the newly-added label
3203- addBody: function(text, markup, style) {
3204- let label = new URLHighlighter(text, true, markup);
3205-
3206- this.addActor(label.actor, style);
3207- return label.actor;
3208- },
3209-
3210- _addBannerBody: function() {
3211- if (this._bannerBodyText) {
3212- let text = this._bannerBodyText;
3213- this._bannerBodyText = null;
3214- this.addBody(text, this._bannerBodyMarkup);
3215- }
3216- },
3217-
3218- // scrollTo:
3219- // @side: St.Side.TOP or St.Side.BOTTOM
3220- //
3221- // Scrolls the content area (if scrollable) to the indicated edge
3222- scrollTo: function(side) {
3223- let adjustment = this._scrollArea.vscroll.adjustment;
3224- if (side == St.Side.TOP)
3225- adjustment.value = adjustment.lower;
3226- else if (side == St.Side.BOTTOM)
3227- adjustment.value = adjustment.upper;
3228- },
3229-
3230- // setActionArea:
3231- // @actor: the actor
3232- // @props: (option) St.Table child properties
3233- //
3234- // Puts @actor into the action area of the notification, replacing
3235- // the previous contents
3236- setActionArea: function(actor, props) {
3237- if (this._actionArea) {
3238- this._actionArea.destroy();
3239- this._actionArea = null;
3240- if (this._buttonBox)
3241- this._buttonBox = null;
3242- } else {
3243- this._addBannerBody();
3244- }
3245- this._actionArea = actor;
3246-
3247- if (!props)
3248- props = {};
3249- props.row = 2;
3250- props.col = 2;
3251-
3252- this._table.add_style_class_name('multi-line-notification');
3253- this._table.add(this._actionArea, props);
3254- this._updateLastColumnSettings();
3255- this.updated();
3256- },
3257-
3258- _updateLastColumnSettings: function() {
3259- if (this._scrollArea)
3260- this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1,
3261- col_span: this._imageBin ? 1 : 2 });
3262- if (this._actionArea)
3263- this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1,
3264- col_span: this._imageBin ? 1 : 2 });
3265- },
3266-
3267- setImage: function(image) {
3268- if (this._imageBin)
3269- this.unsetImage();
3270- this._imageBin = new St.Bin();
3271- this._imageBin.child = image;
3272- this._imageBin.opacity = 230;
3273- this._table.add_style_class_name('multi-line-notification');
3274- this._table.add_style_class_name('notification-with-image');
3275- this._addBannerBody();
3276- this._updateLastColumnSettings();
3277- this._table.add(this._imageBin, { row: 1,
3278- col: 1,
3279- row_span: 2,
3280- x_expand: false,
3281- y_expand: false,
3282- x_fill: false,
3283- y_fill: false });
3284- },
3285-
3286- unsetImage: function() {
3287- if (this._imageBin) {
3288- this._table.remove_style_class_name('notification-with-image');
3289- this._table.remove_actor(this._imageBin);
3290- this._imageBin = null;
3291- this._updateLastColumnSettings();
3292- if (!this._scrollArea && !this._actionArea)
3293- this._table.remove_style_class_name('multi-line-notification');
3294- }
3295- },
3296-
3297- // addButton:
3298- // @id: the action ID
3299- // @label: the label for the action's button
3300- //
3301- // Adds a button with the given @label to the notification. All
3302- // action buttons will appear in a single row at the bottom of
3303- // the notification.
3304- //
3305- // If the button is clicked, the notification will emit the
3306- // %action-invoked signal with @id as a parameter
3307- addButton: function(id, label) {
3308- if (!this._buttonBox) {
3309-
3310- let box = new St.BoxLayout({ style_class: 'notification-actions' });
3311- this.setActionArea(box, { x_expand: false,
3312- y_expand: false,
3313- x_fill: false,
3314- y_fill: false,
3315- x_align: St.Align.END });
3316- this._buttonBox = box;
3317- }
3318-
3319- let button = new St.Button({ can_focus: true });
3320- button._actionId = id;
3321-
3322- if (this._useActionIcons && Gtk.IconTheme.get_default().has_icon(id)) {
3323- button.add_style_class_name('notification-icon-button');
3324- button.child = new St.Icon({ icon_name: id });
3325- } else {
3326- button.add_style_class_name('notification-button');
3327- button.label = label;
3328- }
3329-
3330- if (this._buttonBox.get_n_children() > 0)
3331- global.focus_manager.remove_group(this._buttonBox);
3332-
3333- this._buttonBox.add(button);
3334- global.focus_manager.add_group(this._buttonBox);
3335- button.connect('clicked', Lang.bind(this, this._onActionInvoked, id));
3336-
3337- this.updated();
3338- },
3339-
3340- // setButtonSensitive:
3341- // @id: the action ID
3342- // @sensitive: whether the button should be sensitive
3343- //
3344- // If the notification contains a button with action ID @id,
3345- // its sensitivity will be set to @sensitive. Insensitive
3346- // buttons cannot be clicked.
3347- setButtonSensitive: function(id, sensitive) {
3348- if (!this._buttonBox)
3349- return;
3350-
3351- let button = this._buttonBox.get_children().filter(function(b) {
3352- return b._actionId == id;
3353- })[0];
3354-
3355- if (!button || button.reactive == sensitive)
3356- return;
3357-
3358- button.reactive = sensitive;
3359- },
3360-
3361- setUrgency: function(urgency) {
3362- this.urgency = urgency;
3363- },
3364-
3365- setResident: function(resident) {
3366- this.resident = resident;
3367- },
3368-
3369- setTransient: function(isTransient) {
3370- this.isTransient = isTransient;
3371- },
3372-
3373- setUseActionIcons: function(useIcons) {
3374- this._useActionIcons = useIcons;
3375- },
3376-
3377- _styleChanged: function() {
3378- this._spacing = this._table.get_theme_node().get_length('spacing-columns');
3379- },
3380-
3381- _bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
3382- let [titleMin, titleNat] = this._titleLabel.get_preferred_width(forHeight);
3383- let [bannerMin, bannerNat] = this._bannerLabel.get_preferred_width(forHeight);
3384-
3385- if (this._secondaryIcon) {
3386- let [secondaryIconMin, secondaryIconNat] = this._secondaryIcon.get_preferred_width(forHeight);
3387-
3388- alloc.min_size = secondaryIconMin + this._spacing + titleMin;
3389- alloc.natural_size = secondaryIconNat + this._spacing + titleNat + this._spacing + bannerNat;
3390- } else {
3391- alloc.min_size = titleMin;
3392- alloc.natural_size = titleNat + this._spacing + bannerNat;
3393- }
3394- },
3395-
3396- _bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
3397- [alloc.min_size, alloc.natural_size] =
3398- this._titleLabel.get_preferred_height(forWidth);
3399- },
3400-
3401- _bannerBoxAllocate: function(actor, box, flags) {
3402- let availWidth = box.x2 - box.x1;
3403-
3404- let [titleMinW, titleNatW] = this._titleLabel.get_preferred_width(-1);
3405- let [titleMinH, titleNatH] = this._titleLabel.get_preferred_height(availWidth);
3406- let [bannerMinW, bannerNatW] = this._bannerLabel.get_preferred_width(availWidth);
3407-
3408- let rtl = (this._titleDirection == Clutter.TextDirection.RTL);
3409- let x = rtl ? availWidth : 0;
3410-
3411- if (this._secondaryIcon) {
3412- let [iconMinW, iconNatW] = this._secondaryIcon.get_preferred_width(-1);
3413- let [iconMinH, iconNatH] = this._secondaryIcon.get_preferred_height(availWidth);
3414-
3415- let secondaryIconBox = new Clutter.ActorBox();
3416- let secondaryIconBoxW = Math.min(iconNatW, availWidth);
3417-
3418- // allocate secondary icon box
3419- if (rtl) {
3420- secondaryIconBox.x1 = x - secondaryIconBoxW;
3421- secondaryIconBox.x2 = x;
3422- x = x - (secondaryIconBoxW + this._spacing);
3423- } else {
3424- secondaryIconBox.x1 = x;
3425- secondaryIconBox.x2 = x + secondaryIconBoxW;
3426- x = x + secondaryIconBoxW + this._spacing;
3427- }
3428- secondaryIconBox.y1 = 0;
3429- // Using titleNatH ensures that the secondary icon is centered vertically
3430- secondaryIconBox.y2 = titleNatH;
3431-
3432- availWidth = availWidth - (secondaryIconBoxW + this._spacing);
3433- this._secondaryIcon.allocate(secondaryIconBox, flags);
3434- }
3435-
3436- let titleBox = new Clutter.ActorBox();
3437- let titleBoxW = Math.min(titleNatW, availWidth);
3438- if (rtl) {
3439- titleBox.x1 = availWidth - titleBoxW;
3440- titleBox.x2 = availWidth;
3441- } else {
3442- titleBox.x1 = x;
3443- titleBox.x2 = titleBox.x1 + titleBoxW;
3444- }
3445- titleBox.y1 = 0;
3446- titleBox.y2 = titleNatH;
3447- this._titleLabel.allocate(titleBox, flags);
3448- this._titleFitsInBannerMode = (titleNatW <= availWidth);
3449-
3450- let bannerFits = true;
3451- if (titleBoxW + this._spacing > availWidth) {
3452- this._bannerLabel.opacity = 0;
3453- bannerFits = false;
3454- } else {
3455- let bannerBox = new Clutter.ActorBox();
3456-
3457- if (rtl) {
3458- bannerBox.x1 = 0;
3459- bannerBox.x2 = titleBox.x1 - this._spacing;
3460-
3461- bannerFits = (bannerBox.x2 - bannerNatW >= 0);
3462- } else {
3463- bannerBox.x1 = titleBox.x2 + this._spacing;
3464- bannerBox.x2 = availWidth;
3465-
3466- bannerFits = (bannerBox.x1 + bannerNatW <= availWidth);
3467- }
3468- bannerBox.y1 = 0;
3469- bannerBox.y2 = titleNatH;
3470- this._bannerLabel.allocate(bannerBox, flags);
3471-
3472- // Make _bannerLabel visible if the entire notification
3473- // fits on one line, or if the notification is currently
3474- // unexpanded and only showing one line anyway.
3475- if (!this.expanded || (bannerFits && this._table.row_count == 1))
3476- this._bannerLabel.opacity = 255;
3477- }
3478-
3479- // If the banner doesn't fully fit in the banner box, we possibly need to add the
3480- // banner to the body. We can't do that from here though since that will force a
3481- // relayout, so we add it to the main loop.
3482- if (!bannerFits && this._canExpandContent())
3483- Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
3484- Lang.bind(this,
3485- function() {
3486- if (this._canExpandContent()) {
3487- this._addBannerBody();
3488- this._table.add_style_class_name('multi-line-notification');
3489- this.updated();
3490- }
3491- return false;
3492- }));
3493- },
3494-
3495- _canExpandContent: function() {
3496- return this._bannerBodyText ||
3497- (!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification'));
3498- },
3499-
3500- updated: function() {
3501- if (this.expanded)
3502- this.expand(false);
3503- },
3504-
3505- expand: function(animate) {
3506- this.expanded = true;
3507- this.actor.remove_style_class_name('notification-unexpanded');
3508-
3509- // The banner is never shown when the title did not fit, so this
3510- // can be an if-else statement.
3511- if (!this._titleFitsInBannerMode) {
3512- // Remove ellipsization from the title label and make it wrap so that
3513- // we show the full title when the notification is expanded.
3514- this._titleLabel.clutter_text.line_wrap = true;
3515- this._titleLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
3516- this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
3517- } else if (this._table.row_count > 1 && this._bannerLabel.opacity != 0) {
3518- // We always hide the banner if the notification has additional content.
3519- //
3520- // We don't need to wrap the banner that doesn't fit the way we wrap the
3521- // title that doesn't fit because we won't have a notification with
3522- // row_count=1 that has a banner that doesn't fully fit. We'll either add
3523- // that banner to the content of the notification in _bannerBoxAllocate()
3524- // or the notification will have custom content.
3525- if (animate)
3526- Tweener.addTween(this._bannerLabel,
3527- { opacity: 0,
3528- time: ANIMATION_TIME,
3529- transition: 'easeOutQuad' });
3530- else
3531- this._bannerLabel.opacity = 0;
3532- }
3533- this.emit('expanded');
3534- },
3535-
3536- collapseCompleted: function() {
3537- if (this._destroyed)
3538- return;
3539- this.expanded = false;
3540- // Make sure we don't line wrap the title, and ellipsize it instead.
3541- this._titleLabel.clutter_text.line_wrap = false;
3542- this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
3543- // Restore banner opacity in case the notification is shown in the
3544- // banner mode again on update.
3545- this._bannerLabel.opacity = 255;
3546- // Restore height requisition
3547- this.actor.add_style_class_name('notification-unexpanded');
3548- this.emit('collapsed');
3549- },
3550-
3551- _onActionInvoked: function(actor, mouseButtonClicked, id) {
3552- this.emit('action-invoked', id);
3553- if (!this.resident) {
3554- // We don't hide a resident notification when the user invokes one of its actions,
3555- // because it is common for such notifications to update themselves with new
3556- // information based on the action. We'd like to display the updated information
3557- // in place, rather than pop-up a new notification.
3558- this.emit('done-displaying');
3559- this.destroy();
3560- }
3561- },
3562-
3563- _onClicked: function() {
3564- this.emit('clicked');
3565- // We hide all types of notifications once the user clicks on them because the common
3566- // outcome of clicking should be the relevant window being brought forward and the user's
3567- // attention switching to the window.
3568- this.emit('done-displaying');
3569- if (!this.resident)
3570- this.destroy();
3571- },
3572-
3573- _onDestroy: function() {
3574- if (this._destroyed)
3575- return;
3576- this._destroyed = true;
3577- if (!this._destroyedReason)
3578- this._destroyedReason = NotificationDestroyedReason.DISMISSED;
3579- this.emit('destroy', this._destroyedReason);
3580- },
3581-
3582- destroy: function(reason) {
3583- this._destroyedReason = reason;
3584- this.actor.destroy();
3585- this.actor._delegate = null;
3586- }
3587-});
3588-Signals.addSignalMethods(Notification.prototype);
3589-
3590-const SourceActor = new Lang.Class({
3591- Name: 'SourceActor',
3592-
3593- _init: function(source, size) {
3594- this._source = source;
3595- this._size = size;
3596-
3597- this.actor = new Shell.GenericContainer();
3598- this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth));
3599- this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight));
3600- this.actor.connect('allocate', Lang.bind(this, this._allocate));
3601- this.actor.connect('destroy', Lang.bind(this, function() {
3602- this._actorDestroyed = true;
3603- }));
3604- this._actorDestroyed = false;
3605-
3606- this._counterLabel = new St.Label( {x_align: Clutter.ActorAlign.CENTER,
3607- x_expand: true,
3608- y_align: Clutter.ActorAlign.CENTER,
3609- y_expand: true });
3610-
3611- this._counterBin = new St.Bin({ style_class: 'summary-source-counter',
3612- child: this._counterLabel,
3613- layout_manager: new Clutter.BinLayout() });
3614- this._counterBin.hide();
3615-
3616- this._counterBin.connect('style-changed', Lang.bind(this, function() {
3617- let themeNode = this._counterBin.get_theme_node();
3618- this._counterBin.translation_x = themeNode.get_length('-shell-counter-overlap-x');
3619- this._counterBin.translation_y = themeNode.get_length('-shell-counter-overlap-y');
3620- }));
3621-
3622- this._iconBin = new St.Bin({ width: size,
3623- height: size,
3624- x_fill: true,
3625- y_fill: true });
3626-
3627- this.actor.add_actor(this._iconBin);
3628- this.actor.add_actor(this._counterBin);
3629-
3630- this._source.connect('count-updated', Lang.bind(this, this._updateCount));
3631- this._updateCount();
3632-
3633- this._source.connect('icon-updated', Lang.bind(this, this._updateIcon));
3634- this._updateIcon();
3635- },
3636-
3637- setIcon: function(icon) {
3638- this._iconBin.child = icon;
3639- this._iconSet = true;
3640- },
3641-
3642- _getPreferredWidth: function (actor, forHeight, alloc) {
3643- let [min, nat] = this._iconBin.get_preferred_width(forHeight);
3644- alloc.min_size = min; alloc.nat_size = nat;
3645- },
3646-
3647- _getPreferredHeight: function (actor, forWidth, alloc) {
3648- let [min, nat] = this._iconBin.get_preferred_height(forWidth);
3649- alloc.min_size = min; alloc.nat_size = nat;
3650- },
3651-
3652- _allocate: function(actor, box, flags) {
3653- // the iconBin should fill our entire box
3654- this._iconBin.allocate(box, flags);
3655-
3656- let childBox = new Clutter.ActorBox();
3657-
3658- let [minWidth, minHeight, naturalWidth, naturalHeight] = this._counterBin.get_preferred_size();
3659- let direction = this.actor.get_text_direction();
3660-
3661- if (direction == Clutter.TextDirection.LTR) {
3662- // allocate on the right in LTR
3663- childBox.x1 = box.x2 - naturalWidth;
3664- childBox.x2 = box.x2;
3665- } else {
3666- // allocate on the left in RTL
3667- childBox.x1 = 0;
3668- childBox.x2 = naturalWidth;
3669- }
3670-
3671- childBox.y1 = box.y2 - naturalHeight;
3672- childBox.y2 = box.y2;
3673-
3674- this._counterBin.allocate(childBox, flags);
3675- },
3676-
3677- _updateIcon: function() {
3678- if (this._actorDestroyed)
3679- return;
3680-
3681- if (!this._iconSet)
3682- this._iconBin.child = this._source.createIcon(this._size);
3683- },
3684-
3685- _updateCount: function() {
3686- if (this._actorDestroyed)
3687- return;
3688-
3689- this._counterBin.visible = this._source.countVisible;
3690-
3691- let text;
3692- if (this._source.count < 100)
3693- text = this._source.count.toString();
3694- else
3695- text = String.fromCharCode(0x22EF); // midline horizontal ellipsis
3696-
3697- this._counterLabel.set_text(text);
3698- }
3699-});
3700-
3701-const Source = new Lang.Class({
3702- Name: 'MessageTraySource',
3703-
3704- SOURCE_ICON_SIZE: 48,
3705-
3706- _init: function(title, iconName) {
3707- this.title = title;
3708- this.iconName = iconName;
3709-
3710- this.isTransient = false;
3711- this.isChat = false;
3712- this.isMuted = false;
3713- this.showInLockScreen = true;
3714- this.keepTrayOnSummaryClick = false;
3715-
3716- this.notifications = [];
3717- },
3718-
3719- get count() {
3720- return this.notifications.length;
3721- },
3722-
3723- get unseenCount() {
3724- return this.notifications.filter(function(n) { return !n.acknowledged; }).length;
3725- },
3726-
3727- get countVisible() {
3728- return this.count > 1;
3729- },
3730-
3731- countUpdated: function() {
3732- this.emit('count-updated');
3733- },
3734-
3735- buildRightClickMenu: function() {
3736- let item;
3737- let rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu',
3738- vertical: true });
3739-
3740- item = new PopupMenu.PopupMenuItem(_("Open"));
3741- item.connect('activate', Lang.bind(this, function() {
3742- this.open();
3743- this.emit('done-displaying-content');
3744- }));
3745- rightClickMenu.add(item.actor);
3746-
3747- item = new PopupMenu.PopupMenuItem(_("Remove"));
3748- item.connect('activate', Lang.bind(this, function() {
3749- this.destroy();
3750- this.emit('done-displaying-content');
3751- }));
3752- rightClickMenu.add(item.actor);
3753- return rightClickMenu;
3754- },
3755-
3756- setTransient: function(isTransient) {
3757- this.isTransient = isTransient;
3758- },
3759-
3760- setTitle: function(newTitle) {
3761- this.title = newTitle;
3762- this.emit('title-changed');
3763- },
3764-
3765- setMuted: function(muted) {
3766- if (!this.isChat || this.isMuted == muted)
3767- return;
3768- this.isMuted = muted;
3769- this.emit('muted-changed');
3770- },
3771-
3772- // Called to create a new icon actor.
3773- // Provides a sane default implementation, override if you need
3774- // something more fancy.
3775- createIcon: function(size) {
3776- return new St.Icon({ icon_name: this.iconName,
3777- icon_size: size });
3778- },
3779-
3780- _ensureMainIcon: function() {
3781- if (this._mainIcon)
3782- return;
3783-
3784- this._mainIcon = new SourceActor(this, this.SOURCE_ICON_SIZE);
3785- },
3786-
3787- // Unlike createIcon, this always returns the same actor;
3788- // there is only one summary icon actor for a Source.
3789- getSummaryIcon: function() {
3790- this._ensureMainIcon();
3791- return this._mainIcon.actor;
3792- },
3793-
3794- pushNotification: function(notification) {
3795- if (this.notifications.indexOf(notification) < 0) {
3796- this.notifications.push(notification);
3797- this.emit('notification-added', notification);
3798- }
3799-
3800- notification.connect('clicked', Lang.bind(this, this.open));
3801- notification.connect('destroy', Lang.bind(this,
3802- function () {
3803- let index = this.notifications.indexOf(notification);
3804- if (index < 0)
3805- return;
3806-
3807- this.notifications.splice(index, 1);
3808- if (this.notifications.length == 0)
3809- this._lastNotificationRemoved();
3810-
3811- this.countUpdated();
3812- }));
3813-
3814- this.countUpdated();
3815- },
3816-
3817- notify: function(notification) {
3818- notification.acknowledged = false;
3819- this.pushNotification(notification);
3820- if (!this.isMuted)
3821- this.emit('notify', notification);
3822- },
3823-
3824- destroy: function(reason) {
3825- this.emit('destroy', reason);
3826- },
3827-
3828- // A subclass can redefine this to "steal" clicks from the
3829- // summaryitem; Use Clutter.get_current_event() to get the
3830- // details, return true to prevent the default handling from
3831- // ocurring.
3832- handleSummaryClick: function() {
3833- return false;
3834- },
3835-
3836- iconUpdated: function() {
3837- this.emit('icon-updated');
3838- },
3839-
3840- //// Protected methods ////
3841- _setSummaryIcon: function(icon) {
3842- this._ensureMainIcon();
3843- this._mainIcon.setIcon(icon);
3844- this.iconUpdated();
3845- },
3846-
3847- open: function(notification) {
3848- this.emit('opened', notification);
3849- },
3850-
3851- destroyNonResidentNotifications: function() {
3852- for (let i = this.notifications.length - 1; i >= 0; i--)
3853- if (!this.notifications[i].resident)
3854- this.notifications[i].destroy();
3855-
3856- this.countUpdated();
3857- },
3858-
3859- // Default implementation is to destroy this source, but subclasses can override
3860- _lastNotificationRemoved: function() {
3861- this.destroy();
3862- },
3863-
3864- hasResidentNotification: function() {
3865- return this.notifications.some(function(n) { return n.resident; });
3866- }
3867-});
3868-Signals.addSignalMethods(Source.prototype);
3869-
3870-const SummaryItem = new Lang.Class({
3871- Name: 'SummaryItem',
3872-
3873- _init: function(source) {
3874- this.source = source;
3875- this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource));
3876-
3877- this.actor = new St.Button({ style_class: 'summary-source-button',
3878- y_fill: true,
3879- reactive: true,
3880- button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE,
3881- can_focus: true,
3882- track_hover: true });
3883- this.actor.label_actor = new St.Label({ text: source.title });
3884- this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPress));
3885- this._sourceBox = new St.BoxLayout({ style_class: 'summary-source' });
3886-
3887- this._sourceIcon = source.getSummaryIcon();
3888- this._sourceBox.add(this._sourceIcon, { y_fill: false });
3889- this.actor.child = this._sourceBox;
3890-
3891- this.notificationStackWidget = new St.Widget({ layout_manager: new Clutter.BinLayout() });
3892-
3893- this.notificationStackView = new St.ScrollView({ style_class: source.isChat ? '' : 'summary-notification-stack-scrollview',
3894- vscrollbar_policy: source.isChat ? Gtk.PolicyType.NEVER : Gtk.PolicyType.AUTOMATIC,
3895- hscrollbar_policy: Gtk.PolicyType.NEVER });
3896- this.notificationStackView.add_style_class_name('vfade');
3897- this.notificationStack = new St.BoxLayout({ style_class: 'summary-notification-stack',
3898- vertical: true });
3899- this.notificationStackView.add_actor(this.notificationStack);
3900- this.notificationStackWidget.add_actor(this.notificationStackView);
3901-
3902- this.closeButton = makeCloseButton();
3903- this.notificationStackWidget.add_actor(this.closeButton);
3904- this._stackedNotifications = [];
3905-
3906- this._oldMaxScrollAdjustment = 0;
3907-
3908- this.notificationStackView.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
3909- let currentValue = adjustment.value + adjustment.page_size;
3910- if (currentValue == this._oldMaxScrollAdjustment)
3911- this.scrollTo(St.Side.BOTTOM);
3912- this._oldMaxScrollAdjustment = adjustment.upper;
3913- }));
3914-
3915- this.rightClickMenu = source.buildRightClickMenu();
3916- if (this.rightClickMenu)
3917- global.focus_manager.add_group(this.rightClickMenu);
3918- },
3919-
3920- _onKeyPress: function(actor, event) {
3921- if (event.get_key_symbol() == Clutter.KEY_Up) {
3922- actor.emit('clicked', 1);
3923- return true;
3924- }
3925- return false;
3926- },
3927-
3928- prepareNotificationStackForShowing: function() {
3929- if (this.notificationStack.get_n_children() > 0)
3930- return;
3931-
3932- for (let i = 0; i < this.source.notifications.length; i++) {
3933- this._appendNotificationToStack(this.source.notifications[i]);
3934- }
3935-
3936- this.scrollTo(St.Side.BOTTOM);
3937- },
3938-
3939- doneShowingNotificationStack: function() {
3940- for (let i = 0; i < this._stackedNotifications.length; i++) {
3941- let stackedNotification = this._stackedNotifications[i];
3942- let notification = stackedNotification.notification;
3943- notification.collapseCompleted();
3944- notification.disconnect(stackedNotification.notificationExpandedId);
3945- notification.disconnect(stackedNotification.notificationDoneDisplayingId);
3946- notification.disconnect(stackedNotification.notificationDestroyedId);
3947- if (notification.actor.get_parent() == this.notificationStack)
3948- this.notificationStack.remove_actor(notification.actor);
3949- notification.setIconVisible(true);
3950- notification.enableScrolling(true);
3951- }
3952- this._stackedNotifications = [];
3953- },
3954-
3955- _notificationAddedToSource: function(source, notification) {
3956- if (this.notificationStack.mapped)
3957- this._appendNotificationToStack(notification);
3958- },
3959-
3960- _appendNotificationToStack: function(notification) {
3961- let stackedNotification = {};
3962- stackedNotification.notification = notification;
3963- stackedNotification.notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated));
3964- stackedNotification.notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying));
3965- stackedNotification.notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed));
3966- this._stackedNotifications.push(stackedNotification);
3967- if (!this.source.isChat)
3968- notification.enableScrolling(false);
3969- if (this.notificationStack.get_n_children() > 0)
3970- notification.setIconVisible(false);
3971- this.notificationStack.add(notification.actor);
3972- notification.expand(false);
3973- },
3974-
3975- // scrollTo:
3976- // @side: St.Side.TOP or St.Side.BOTTOM
3977- //
3978- // Scrolls the notifiction stack to the indicated edge
3979- scrollTo: function(side) {
3980- let adjustment = this.notificationStackView.vscroll.adjustment;
3981- if (side == St.Side.TOP)
3982- adjustment.value = adjustment.lower;
3983- else if (side == St.Side.BOTTOM)
3984- adjustment.value = adjustment.upper;
3985- },
3986-
3987- _contentUpdated: function() {
3988- this.emit('content-updated');
3989- },
3990-
3991- _notificationDoneDisplaying: function() {
3992- this.source.emit('done-displaying-content');
3993- },
3994-
3995- _notificationDestroyed: function(notification) {
3996- for (let i = 0; i < this._stackedNotifications.length; i++) {
3997- if (this._stackedNotifications[i].notification == notification) {
3998- let stackedNotification = this._stackedNotifications[i];
3999- notification.disconnect(stackedNotification.notificationExpandedId);
4000- notification.disconnect(stackedNotification.notificationDoneDisplayingId);
4001- notification.disconnect(stackedNotification.notificationDestroyedId);
4002- this._stackedNotifications.splice(i, 1);
4003- if (notification.actor.get_parent() == this.notificationStack)
4004- this.notificationStack.remove_actor(notification.actor);
4005- this._contentUpdated();
4006- break;
4007- }
4008- }
4009-
4010- let firstNotification = this._stackedNotifications[0];
4011- if (firstNotification)
4012- firstNotification.notification.setIconVisible(true);
4013- }
4014-});
4015-Signals.addSignalMethods(SummaryItem.prototype);
4016-
4017-const MessageTray = new Lang.Class({
4018- Name: 'MessageTray',
4019-
4020- _init: function() {
4021- this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) {
4022- this._onStatusChanged(proxy.status);
4023- }));
4024- this._busy = false;
4025- this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) {
4026- this._onStatusChanged(status);
4027- }));
4028-
4029- this.actor = new St.Widget({ name: 'message-tray',
4030- reactive: true,
4031- track_hover: true,
4032- layout_manager: new Clutter.BinLayout(),
4033- x_expand: true,
4034- y_expand: true,
4035- y_align: Clutter.ActorAlign.START,
4036- });
4037- this.actor.connect('notify::hover', Lang.bind(this, this._onTrayHoverChanged));
4038-
4039- this._notificationWidget = new St.Widget({ name: 'notification-container',
4040- y_align: Clutter.ActorAlign.START,
4041- x_align: Clutter.ActorAlign.CENTER,
4042- y_expand: true,
4043- x_expand: true,
4044- layout_manager: new Clutter.BinLayout() });
4045- this.actor.add_actor(this._notificationWidget);
4046-
4047- this._notificationBin = new St.Bin({ y_expand: true });
4048- this._notificationBin.set_y_align(Clutter.ActorAlign.START);
4049- this._notificationWidget.add_actor(this._notificationBin);
4050- this._notificationWidget.hide();
4051- this._notificationQueue = [];
4052- this._notification = null;
4053- this._notificationClickedId = 0;
4054-
4055- this.actor.connect('button-release-event', Lang.bind(this, function(actor, event) {
4056- this._setClickedSummaryItem(null);
4057- this._updateState();
4058- actor.grab_key_focus();
4059- }));
4060- global.focus_manager.add_group(this.actor);
4061- this._summary = new St.BoxLayout({ name: 'summary-mode',
4062- reactive: true,
4063- track_hover: true,
4064- x_align: Clutter.ActorAlign.END,
4065- x_expand: true,
4066- y_align: Clutter.ActorAlign.CENTER,
4067- y_expand: true });
4068- this._summary.connect('notify::hover', Lang.bind(this, this._onSummaryHoverChanged));
4069- this.actor.add_actor(this._summary);
4070- this._summary.opacity = 0;
4071-
4072- this._summaryMotionId = 0;
4073- this._trayMotionId = 0;
4074-
4075- this._summaryBoxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM,
4076- { reactive: true,
4077- track_hover: true });
4078- this._summaryBoxPointer.actor.connect('key-press-event',
4079- Lang.bind(this, this._onSummaryBoxPointerKeyPress));
4080- this._summaryBoxPointer.actor.style_class = 'summary-boxpointer';
4081- this._summaryBoxPointer.actor.hide();
4082- Main.layoutManager.addChrome(this._summaryBoxPointer.actor);
4083-
4084- this._summaryBoxPointerItem = null;
4085- this._summaryBoxPointerContentUpdatedId = 0;
4086- this._summaryBoxPointerDoneDisplayingId = 0;
4087- this._clickedSummaryItem = null;
4088- this._clickedSummaryItemMouseButton = -1;
4089- this._clickedSummaryItemAllocationChangedId = 0;
4090- this._pointerBarrier = 0;
4091-
4092- this._closeButton = makeCloseButton();
4093- this._closeButton.hide();
4094- this._closeButton.connect('clicked', Lang.bind(this, this._onCloseClicked));
4095- this._notificationWidget.add_actor(this._closeButton);
4096-
4097- this._idleMonitorWatchId = 0;
4098- this._userActiveWhileNotificationShown = false;
4099-
4100- this.idleMonitor = Shell.IdleMonitor.get();
4101-
4102- this._grabHelper = new GrabHelper.GrabHelper(this.actor);
4103- this._grabHelper.addActor(this._summaryBoxPointer.actor);
4104- this._grabHelper.addActor(this.actor);
4105- if (Main.panel.statusArea.activities)
4106- this._grabHelper.addActor(Main.panel.statusArea.activities.hotCorner.actor);
4107-
4108- Main.layoutManager.keyboardBox.connect('notify::hover', Lang.bind(this, this._onKeyboardHoverChanged));
4109- Main.layoutManager.connect('keyboard-visible-changed', Lang.bind(this, this._onKeyboardVisibleChanged));
4110-
4111- this._trayState = State.HIDDEN;
4112- this._locked = false;
4113- this._traySummoned = false;
4114- this._useLongerTrayLeftTimeout = false;
4115- this._trayLeftTimeoutId = 0;
4116- this._pointerInTray = false;
4117- this._pointerInKeyboard = false;
4118- this._keyboardVisible = false;
4119- this._summaryState = State.HIDDEN;
4120- this._pointerInSummary = false;
4121- this._notificationClosed = false;
4122- this._notificationState = State.HIDDEN;
4123- this._notificationTimeoutId = 0;
4124- this._notificationExpandedId = 0;
4125- this._summaryBoxPointerState = State.HIDDEN;
4126- this._summaryBoxPointerTimeoutId = 0;
4127- this._desktopCloneState = State.HIDDEN;
4128- this._overviewVisible = Main.overview.visible;
4129- this._notificationRemoved = false;
4130- this._reNotifyAfterHideNotification = null;
4131- this._inFullscreen = false;
4132- this._desktopClone = null;
4133-
4134- this._lightbox = new Lightbox.Lightbox(global.window_group,
4135- { inhibitEvents: true,
4136- fadeInTime: ANIMATION_TIME,
4137- fadeOutTime: ANIMATION_TIME,
4138- fadeFactor: 0.2
4139- });
4140-
4141- Main.layoutManager.trayBox.add_actor(this.actor);
4142- this.actor.y = 0;
4143- Main.layoutManager.trackChrome(this.actor);
4144- Main.layoutManager.trackChrome(this._notificationWidget);
4145- Main.layoutManager.trackChrome(this._closeButton);
4146-
4147- Main.layoutManager.connect('primary-fullscreen-changed', Lang.bind(this, this._onFullscreenChanged));
4148-
4149- Main.overview.connect('showing', Lang.bind(this,
4150- function() {
4151- this._overviewVisible = true;
4152- this._grabHelper.ungrab(); // drop modal grab if necessary
4153- this.actor.add_style_pseudo_class('overview');
4154- this._updateState();
4155- }));
4156- Main.overview.connect('hiding', Lang.bind(this,
4157- function() {
4158- this._overviewVisible = false;
4159- this._escapeTray();
4160- this.actor.remove_style_pseudo_class('overview');
4161- this._updateState();
4162- }));
4163-
4164- Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated));
4165-
4166- global.display.add_keybinding('toggle-message-tray',
4167- new Gio.Settings({ schema: SHELL_KEYBINDINGS_SCHEMA }),
4168- Meta.KeyBindingFlags.NONE,
4169- Lang.bind(this, this.toggleAndNavigate));
4170-
4171- this._summaryItems = [];
4172- this._chatSummaryItemsCount = 0;
4173-
4174- let pointerWatcher = PointerWatcher.getPointerWatcher();
4175- pointerWatcher.addWatch(TRAY_DWELL_CHECK_INTERVAL, Lang.bind(this, this._checkTrayDwell));
4176- this._trayDwellTimeoutId = 0;
4177- this._trayDwelling = false;
4178- this._trayDwellUserTime = 0;
4179-
4180- this._sessionUpdated();
4181- },
4182-
4183- _sessionUpdated: function() {
4184- if (Main.sessionMode.isLocked || Main.sessionMode.isGreeter)
4185- Main.ctrlAltTabManager.removeGroup(this._summary);
4186- else
4187- Main.ctrlAltTabManager.addGroup(this._summary, _("Message Tray"), 'start-here-symbolic',
4188- { focusCallback: Lang.bind(this, this.toggleAndNavigate),
4189- sortGroup: CtrlAltTab.SortGroup.BOTTOM });
4190- this._updateState();
4191- },
4192-
4193- _checkTrayDwell: function(x, y) {
4194- let monitor = Main.layoutManager.bottomMonitor;
4195- let shouldDwell = (x >= monitor.x && x <= monitor.x + monitor.width &&
4196- y == monitor.y + monitor.height - 1);
4197- if (shouldDwell) {
4198- // We only set up dwell timeout when the user is not hovering over the tray
4199- // (!this.actor.hover). This avoids bringing up the message tray after the
4200- // user clicks on a notification with the pointer on the bottom pixel
4201- // of the monitor. The _trayDwelling variable is used so that we only try to
4202- // fire off one tray dwell - if it fails (because, say, the user has the mouse down),
4203- // we don't try again until the user moves the mouse up and down again.
4204- if (!this._trayDwelling && !this.actor.hover && this._trayDwellTimeoutId == 0) {
4205- // Save the interaction timestamp so we can detect user input
4206- let focusWindow = global.display.focus_window;
4207- this._trayDwellUserTime = focusWindow ? focusWindow.user_time : 0;
4208-
4209- this._trayDwellTimeoutId = Mainloop.timeout_add(TRAY_DWELL_TIME,
4210- Lang.bind(this, this._trayDwellTimeout));
4211- }
4212- this._trayDwelling = true;
4213- } else {
4214- this._cancelTrayDwell();
4215- this._trayDwelling = false;
4216- }
4217- },
4218-
4219- _cancelTrayDwell: function() {
4220- if (this._trayDwellTimeoutId != 0) {
4221- Mainloop.source_remove(this._trayDwellTimeoutId);
4222- this._trayDwellTimeoutId = 0;
4223- }
4224- },
4225-
4226- _trayDwellTimeout: function() {
4227- if (Main.modalCount > 0)
4228- return false;
4229-
4230- // If the user interacted with the focus window since we started the tray
4231- // dwell (by clicking or typing), don't activate the message tray
4232- let focusWindow = global.display.focus_window;
4233- let currentUserTime = focusWindow ? focusWindow.user_time : 0;
4234- if (currentUserTime != this._trayDwellUserTime)
4235- return false;
4236-
4237- this._trayDwellTimeoutId = 0;
4238-
4239- this._traySummoned = true;
4240- this._updateState();
4241-
4242- return false;
4243- },
4244-
4245- _onCloseClicked: function() {
4246- if (this._notificationState == State.SHOWN) {
4247- this._closeButton.hide();
4248- this._notificationClosed = true;
4249- this._updateState();
4250- this._notificationClosed = false;
4251- }
4252- },
4253-
4254- contains: function(source) {
4255- return this._getIndexOfSummaryItemForSource(source) >= 0;
4256- },
4257-
4258- _getIndexOfSummaryItemForSource: function(source) {
4259- for (let i = 0; i < this._summaryItems.length; i++) {
4260- if (this._summaryItems[i].source == source)
4261- return i;
4262- }
4263- return -1;
4264- },
4265-
4266- add: function(source) {
4267- if (this.contains(source)) {
4268- log('Trying to re-add source ' + source.title);
4269- return;
4270- }
4271-
4272- let summaryItem = new SummaryItem(source);
4273-
4274- if (source.isChat) {
4275- this._summary.insert_child_at_index(summaryItem.actor, 0);
4276- this._chatSummaryItemsCount++;
4277- } else {
4278- this._summary.insert_child_at_index(summaryItem.actor, this._chatSummaryItemsCount);
4279- }
4280-
4281- this._summaryItems.push(summaryItem);
4282-
4283- source.connect('notify', Lang.bind(this, this._onNotify));
4284-
4285- source.connect('muted-changed', Lang.bind(this,
4286- function () {
4287- if (source.isMuted)
4288- this._notificationQueue = this._notificationQueue.filter(function(notification) {
4289- return source != notification.source;
4290- });
4291- }));
4292-
4293- summaryItem.actor.connect('clicked', Lang.bind(this,
4294- function(actor, button) {
4295- actor.grab_key_focus();
4296- this._onSummaryItemClicked(summaryItem, button);
4297- }));
4298- summaryItem.actor.connect('popup-menu', Lang.bind(this,
4299- function(actor, button) {
4300- actor.grab_key_focus();
4301- this._onSummaryItemClicked(summaryItem, 3);
4302- }));
4303-
4304- source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
4305-
4306- // We need to display the newly-added summary item, but if the
4307- // caller is about to post a notification, we want to show that
4308- // *first* and not show the summary item until after it hides.
4309- // So postpone calling _updateState() a tiny bit.
4310- Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { this._updateState(); return false; }));
4311-
4312- this.emit('summary-item-added', summaryItem);
4313- },
4314-
4315- getSummaryItems: function() {
4316- return this._summaryItems;
4317- },
4318-
4319- _onSourceDestroy: function(source) {
4320- let index = this._getIndexOfSummaryItemForSource(source);
4321- if (index == -1)
4322- return;
4323-
4324- let summaryItemToRemove = this._summaryItems[index];
4325-
4326- this._summaryItems.splice(index, 1);
4327-
4328- if (source.isChat)
4329- this._chatSummaryItemsCount--;
4330-
4331- let needUpdate = false;
4332-
4333- if (this._notification && this._notification.source == source) {
4334- this._updateNotificationTimeout(0);
4335- this._notificationRemoved = true;
4336- needUpdate = true;
4337- }
4338- if (this._clickedSummaryItem == summaryItemToRemove) {
4339- this._setClickedSummaryItem(null);
4340- needUpdate = true;
4341- }
4342-
4343- summaryItemToRemove.actor.destroy();
4344-
4345- if (needUpdate)
4346- this._updateState();
4347- },
4348-
4349- _onNotificationDestroy: function(notification) {
4350- if (this._notification == notification && (this._notificationState == State.SHOWN || this._notificationState == State.SHOWING)) {
4351- this._updateNotificationTimeout(0);
4352- this._notificationRemoved = true;
4353- this._updateState();
4354- return;
4355- }
4356-
4357- let index = this._notificationQueue.indexOf(notification);
4358- notification.destroy();
4359- if (index != -1)
4360- this._notificationQueue.splice(index, 1);
4361- },
4362-
4363- _lock: function() {
4364- this._locked = true;
4365- },
4366-
4367- _unlock: function() {
4368- if (!this._locked)
4369- return;
4370- this._locked = false;
4371- this._pointerInTray = this.actor.hover;
4372- this._updateState();
4373- },
4374-
4375- toggle: function() {
4376- this._traySummoned = !this._traySummoned;
4377- this._updateState();
4378- },
4379-
4380- toggleAndNavigate: function() {
4381- this.toggle();
4382- this._summary.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false);
4383- },
4384-
4385- hide: function() {
4386- this._traySummoned = false;
4387- this.actor.set_hover(false);
4388- this._summary.set_hover(false);
4389- this._updateState();
4390- },
4391-
4392- _onNotify: function(source, notification) {
4393- if (this._summaryBoxPointerItem && this._summaryBoxPointerItem.source == source) {
4394- if (this._summaryBoxPointerState == State.HIDING) {
4395- // We are in the process of hiding the summary box pointer.
4396- // If there is an update for one of the notifications or
4397- // a new notification to be added to the notification stack
4398- // while it is in the process of being hidden, we show it as
4399- // a new notification. However, we first wait till the hide
4400- // is complete. This is especially important if one of the
4401- // notifications in the stack was updated because we will
4402- // need to be able to re-parent its actor to a different
4403- // part of the stage.
4404- this._reNotifyAfterHideNotification = notification;
4405- } else {
4406- // The summary box pointer is showing or shown (otherwise,
4407- // this._summaryBoxPointerItem would be null)
4408- // Immediately mark the notification as acknowledged, as it's
4409- // not going into the queue
4410- notification.acknowledged = true;
4411- }
4412-
4413- return;
4414- }
4415-
4416- if (this._notification == notification) {
4417- // If a notification that is being shown is updated, we update
4418- // how it is shown and extend the time until it auto-hides.
4419- // If a new notification is updated while it is being hidden,
4420- // we stop hiding it and show it again.
4421- this._updateShowingNotification();
4422- } else if (this._notificationQueue.indexOf(notification) < 0) {
4423- notification.connect('destroy',
4424- Lang.bind(this, this._onNotificationDestroy));
4425- this._notificationQueue.push(notification);
4426- this._notificationQueue.sort(function(notification1, notification2) {
4427- return (notification2.urgency - notification1.urgency);
4428- });
4429- }
4430- this._updateState();
4431- },
4432-
4433- _onSummaryItemClicked: function(summaryItem, button) {
4434- if (summaryItem.source.handleSummaryClick()) {
4435- if (summaryItem.source.keepTrayOnSummaryClick)
4436- this._setClickedSummaryItem(null);
4437- else
4438- this._escapeTray();
4439- } else {
4440- if (!this._setClickedSummaryItem(summaryItem, button))
4441- this._setClickedSummaryItem(null);
4442- }
4443-
4444- this._updateState();
4445- },
4446-
4447- _onSummaryHoverChanged: function() {
4448- this._pointerInSummary = this._summary.hover;
4449- this._updateState();
4450- },
4451-
4452- _onTrayHoverChanged: function() {
4453- if (this.actor.hover) {
4454- // No dwell inside notifications at the bottom of the screen
4455- this._cancelTrayDwell();
4456-
4457- // Don't do anything if the one pixel area at the bottom is hovered over while the tray is hidden.
4458- if (this._trayState == State.HIDDEN && this._notificationState == State.HIDDEN)
4459- return;
4460-
4461- // Don't do anything if this._useLongerTrayLeftTimeout is true, meaning the notification originally
4462- // popped up under the pointer, but this._trayLeftTimeoutId is 0, meaning the pointer didn't leave
4463- // the tray yet. We need to check for this case because sometimes _onTrayHoverChanged() gets called
4464- // multiple times while this.actor.hover is true.
4465- if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId)
4466- return;
4467-
4468- this._useLongerTrayLeftTimeout = false;
4469- if (this._trayLeftTimeoutId) {
4470- Mainloop.source_remove(this._trayLeftTimeoutId);
4471- this._trayLeftTimeoutId = 0;
4472- this._trayLeftMouseX = -1;
4473- this._trayLeftMouseY = -1;
4474- return;
4475- }
4476-
4477- if (this._showNotificationMouseX >= 0) {
4478- let actorAtShowNotificationPosition =
4479- global.stage.get_actor_at_pos(Clutter.PickMode.ALL, this._showNotificationMouseX, this._showNotificationMouseY);
4480- this._showNotificationMouseX = -1;
4481- this._showNotificationMouseY = -1;
4482- // Don't set this._pointerInTray to true if the pointer was initially in the area where the notification
4483- // popped up. That way we will not be expanding notifications that happen to pop up over the pointer
4484- // automatically. Instead, the user is able to expand the notification by mousing away from it and then
4485- // mousing back in. Because this is an expected action, we set the boolean flag that indicates that a longer
4486- // timeout should be used before popping down the notification.
4487- if (this.actor.contains(actorAtShowNotificationPosition)) {
4488- this._useLongerTrayLeftTimeout = true;
4489- return;
4490- }
4491- }
4492- this._pointerInTray = true;
4493- this._updateState();
4494- } else {
4495- // We record the position of the mouse the moment it leaves the tray. These coordinates are used in
4496- // this._onTrayLeftTimeout() to determine if the mouse has moved far enough during the initial timeout for us
4497- // to consider that the user intended to leave the tray and therefore hide the tray. If the mouse is still
4498- // close to its previous position, we extend the timeout once.
4499- let [x, y, mods] = global.get_pointer();
4500- this._trayLeftMouseX = x;
4501- this._trayLeftMouseY = y;
4502-
4503- // We wait just a little before hiding the message tray in case the user quickly moves the mouse back into it.
4504- // We wait for a longer period if the notification popped up where the mouse pointer was already positioned.
4505- // That gives the user more time to mouse away from the notification and mouse back in in order to expand it.
4506- let timeout = this._useLongerTrayLeftTimeout ? LONGER_HIDE_TIMEOUT * 1000 : HIDE_TIMEOUT * 1000;
4507- this._trayLeftTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._onTrayLeftTimeout));
4508- }
4509- },
4510-
4511- _onKeyboardHoverChanged: function(keyboard) {
4512- this._pointerInKeyboard = keyboard.hover;
4513-
4514- if (!keyboard.hover) {
4515- let event = Clutter.get_current_event();
4516- if (event && event.type() == Clutter.EventType.LEAVE) {
4517- let into = event.get_related();
4518- if (into && this.actor.contains(into)) {
4519- // Don't call _updateState, because pointerInTray is
4520- // still false
4521- return;
4522- }
4523- }
4524- }
4525-
4526- this._updateState();
4527- },
4528-
4529- _onKeyboardVisibleChanged: function(layoutManager, keyboardVisible) {
4530- if (this._keyboardVisible == keyboardVisible)
4531- return;
4532-
4533- this._keyboardVisible = keyboardVisible;
4534-
4535- if (keyboardVisible)
4536- this.actor.add_style_pseudo_class('keyboard');
4537- else
4538- this.actor.remove_style_pseudo_class('keyboard');
4539-
4540- this._updateState();
4541- },
4542-
4543- _onFullscreenChanged: function(obj, state) {
4544- this._inFullscreen = state;
4545- this._updateState();
4546- },
4547-
4548- _onStatusChanged: function(status) {
4549- if (status == GnomeSession.PresenceStatus.BUSY) {
4550- // remove notification and allow the summary to be closed now
4551- this._updateNotificationTimeout(0);
4552- this._busy = true;
4553- } else if (status != GnomeSession.PresenceStatus.IDLE) {
4554- // We preserve the previous value of this._busy if the status turns to IDLE
4555- // so that we don't start showing notifications queued during the BUSY state
4556- // as the screensaver gets activated.
4557- this._busy = false;
4558- }
4559-
4560- this._updateState();
4561- },
4562-
4563- _onTrayLeftTimeout: function() {
4564- let [x, y, mods] = global.get_pointer();
4565- // We extend the timeout once if the mouse moved no further than MOUSE_LEFT_ACTOR_THRESHOLD to either side or up.
4566- // We don't check how far down the mouse moved because any point above the tray, but below the exit coordinate,
4567- // is close to the tray.
4568- if (this._trayLeftMouseX > -1 &&
4569- y > this._trayLeftMouseY - MOUSE_LEFT_ACTOR_THRESHOLD &&
4570- x < this._trayLeftMouseX + MOUSE_LEFT_ACTOR_THRESHOLD &&
4571- x > this._trayLeftMouseX - MOUSE_LEFT_ACTOR_THRESHOLD) {
4572- this._trayLeftMouseX = -1;
4573- this._trayLeftTimeoutId = Mainloop.timeout_add(LONGER_HIDE_TIMEOUT * 1000,
4574- Lang.bind(this, this._onTrayLeftTimeout));
4575- } else {
4576- this._trayLeftTimeoutId = 0;
4577- this._useLongerTrayLeftTimeout = false;
4578- this._pointerInTray = false;
4579- this._pointerInSummary = false;
4580- this._updateNotificationTimeout(0);
4581- this._updateState();
4582- }
4583- return false;
4584- },
4585-
4586- _escapeTray: function() {
4587- this._unlock();
4588- this._pointerInTray = false;
4589- this._pointerInSummary = false;
4590- this._traySummoned = false;
4591- this._setClickedSummaryItem(null);
4592- this._updateNotificationTimeout(0);
4593- this._updateState();
4594- },
4595-
4596- // All of the logic for what happens when occurs here; the various
4597- // event handlers merely update variables such as
4598- // 'this._pointerInTray', 'this._summaryState', etc, and
4599- // _updateState() figures out what (if anything) needs to be done
4600- // at the present time.
4601- _updateState: function() {
4602- // Notifications
4603- let notificationQueue = this._notificationQueue;
4604- let notificationUrgent = notificationQueue.length > 0 && notificationQueue[0].urgency == Urgency.CRITICAL;
4605- let notificationsLimited = this._busy || this._inFullscreen;
4606- let notificationsPending = notificationQueue.length > 0 && (!notificationsLimited || notificationUrgent) && Main.sessionMode.hasNotifications;
4607- let nextNotification = notificationQueue.length > 0 ? notificationQueue[0] : null;
4608- let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved;
4609- let notificationExpanded = this._notification && this._notification.expanded;
4610- let notificationExpired = this._notificationTimeoutId == 0 &&
4611- !(this._notification && this._notification.urgency == Urgency.CRITICAL) &&
4612- !(this._notification && this._notification.focused) &&
4613- !this._pointerInTray &&
4614- !this._locked &&
4615- !(this._pointerInKeyboard && notificationExpanded);
4616- let notificationLockedOut = !Main.sessionMode.hasNotifications && this._notification;
4617- let notificationMustClose = this._notificationRemoved || notificationLockedOut || (notificationExpired && this._userActiveWhileNotificationShown) || this._notificationClosed;
4618- let canShowNotification = notificationsPending && this._summaryState == State.HIDDEN;
4619-
4620- if (this._notificationState == State.HIDDEN) {
4621- if (canShowNotification) {
4622- this._showNotification();
4623- }
4624- } else if (this._notificationState == State.SHOWN) {
4625- if (notificationMustClose)
4626- this._hideNotification();
4627- else if (notificationPinned && !notificationExpanded)
4628- this._expandNotification(false);
4629- else if (notificationPinned)
4630- this._ensureNotificationFocused();
4631- }
4632-
4633- // Summary
4634- let summarySummoned = this._pointerInSummary || this._overviewVisible || this._traySummoned;
4635- let summaryPinned = this._pointerInTray || summarySummoned || this._locked;
4636- let summaryHovered = this._pointerInTray || this._pointerInSummary;
4637-
4638- let notificationsVisible = (this._notificationState == State.SHOWING ||
4639- this._notificationState == State.SHOWN);
4640- let notificationsDone = !notificationsVisible && !notificationsPending;
4641-
4642- let summaryOptionalInOverview = this._overviewVisible && !this._locked && !summaryHovered;
4643- let mustHideSummary = (notificationsPending && (notificationUrgent || summaryOptionalInOverview))
4644- || notificationsVisible || !Main.sessionMode.hasNotifications;
4645-
4646- if (this._summaryState == State.HIDDEN && !mustHideSummary && summarySummoned)
4647- this._showSummary();
4648- else if (this._summaryState == State.SHOWN && (!summaryPinned || mustHideSummary))
4649- this._hideSummary();
4650-
4651- // Summary notification
4652- let haveClickedSummaryItem = this._clickedSummaryItem != null;
4653- let summarySourceIsMainNotificationSource = (haveClickedSummaryItem && this._notification &&
4654- this._clickedSummaryItem.source == this._notification.source);
4655- let canShowSummaryBoxPointer = this._summaryState == State.SHOWN;
4656- // We only have sources with empty notification stacks for legacy tray icons. Currently, we never attempt
4657- // to show notifications for legacy tray icons, but this would be necessary if we did.
4658- let requestedNotificationStackIsEmpty = (this._clickedSummaryItemMouseButton == 1 && this._clickedSummaryItem.source.notifications.length == 0);
4659- let wrongSummaryNotificationStack = (this._clickedSummaryItemMouseButton == 1 &&
4660- this._summaryBoxPointer.bin.child != this._clickedSummaryItem.notificationStackWidget);
4661- let wrongSummaryRightClickMenu = (this._clickedSummaryItemMouseButton == 3 &&
4662- this._summaryBoxPointer.bin.child != this._clickedSummaryItem.rightClickMenu);
4663- let wrongSummaryBoxPointer = (haveClickedSummaryItem &&
4664- (wrongSummaryNotificationStack || wrongSummaryRightClickMenu));
4665-
4666- if (this._summaryBoxPointerState == State.HIDDEN) {
4667- if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer && !requestedNotificationStackIsEmpty)
4668- this._showSummaryBoxPointer();
4669- } else if (this._summaryBoxPointerState == State.SHOWN) {
4670- if (!haveClickedSummaryItem || !canShowSummaryBoxPointer || wrongSummaryBoxPointer || mustHideSummary) {
4671- this._hideSummaryBoxPointer();
4672- if (wrongSummaryBoxPointer)
4673- this._showSummaryBoxPointer();
4674- }
4675- }
4676-
4677- // Tray itself
4678- let trayIsVisible = (this._trayState == State.SHOWING ||
4679- this._trayState == State.SHOWN);
4680- let trayShouldBeVisible = (this._summaryState == State.SHOWING ||
4681- this._summaryState == State.SHOWN);
4682- if (!trayIsVisible && trayShouldBeVisible)
4683- trayShouldBeVisible = this._showTray();
4684- else if (trayIsVisible && !trayShouldBeVisible)
4685- this._hideTray();
4686-
4687- // Desktop clone
4688- let desktopCloneIsVisible = (this._desktopCloneState == State.SHOWING ||
4689- this._desktopCloneState == State.SHOWN);
4690- let desktopCloneShouldBeVisible = (trayShouldBeVisible &&
4691- !this._overviewVisible &&
4692- !this._keyboardVisible);
4693-
4694- if (!desktopCloneIsVisible && desktopCloneShouldBeVisible) {
4695- this._showDesktopClone();
4696- } else if (desktopCloneIsVisible && !desktopCloneShouldBeVisible) {
4697- this._hideDesktopClone (this._keyboardVisible);
4698- }
4699- },
4700-
4701- _tween: function(actor, statevar, value, params) {
4702- let onComplete = params.onComplete;
4703- let onCompleteScope = params.onCompleteScope;
4704- let onCompleteParams = params.onCompleteParams;
4705-
4706- params.onComplete = this._tweenComplete;
4707- params.onCompleteScope = this;
4708- params.onCompleteParams = [statevar, value, onComplete, onCompleteScope, onCompleteParams];
4709-
4710- Tweener.addTween(actor, params);
4711-
4712- let valuing = (value == State.SHOWN) ? State.SHOWING : State.HIDING;
4713- this[statevar] = valuing;
4714- },
4715-
4716- _tweenComplete: function(statevar, value, onComplete, onCompleteScope, onCompleteParams) {
4717- this[statevar] = value;
4718- if (onComplete)
4719- onComplete.apply(onCompleteScope, onCompleteParams);
4720- this._updateState();
4721- },
4722-
4723- _showTray: function() {
4724- // Don't actually take a modal grab in the overview.
4725- // Just add something to the grab stack that we can
4726- // pop later.
4727- let modal = !this._overviewVisible;
4728-
4729- if (!this._grabHelper.grab({ actor: this.actor,
4730- modal: modal,
4731- onUngrab: Lang.bind(this, this._escapeTray) })) {
4732- this._traySummoned = false;
4733- return false;
4734- }
4735-
4736- this._tween(this.actor, '_trayState', State.SHOWN,
4737- { y: -this.actor.height,
4738- time: ANIMATION_TIME,
4739- transition: 'easeOutQuad'
4740- });
4741-
4742- if (!this._overviewVisible)
4743- this._lightbox.show();
4744-
4745- return true;
4746- },
4747-
4748- _updateDesktopCloneClip: function() {
4749- let geometry = this._bottomMonitorGeometry;
4750- let progress = -Math.round(this._desktopClone.y);
4751- this._desktopClone.set_clip(geometry.x,
4752- geometry.y + progress,
4753- geometry.width,
4754- geometry.height - progress);
4755- },
4756-
4757- _showDesktopClone: function() {
4758- let bottomMonitor = Main.layoutManager.bottomMonitor;
4759- this._bottomMonitorGeometry = { x: bottomMonitor.x,
4760- y: bottomMonitor.y,
4761- width: bottomMonitor.width,
4762- height: bottomMonitor.height };
4763-
4764- if (this._desktopClone)
4765- this._desktopClone.destroy();
4766- this._desktopClone = new Clutter.Clone({ source: global.window_group, clip: new Clutter.Geometry(this._bottomMonitorGeometry) });
4767- Main.uiGroup.insert_child_above(this._desktopClone, global.window_group);
4768- this._desktopClone.x = 0;
4769- this._desktopClone.y = 0;
4770- this._desktopClone.show();
4771-
4772- this._tween(this._desktopClone, '_desktopCloneState', State.SHOWN,
4773- { y: -this.actor.height,
4774- time: ANIMATION_TIME,
4775- transition: 'easeOutQuad',
4776- onUpdate: Lang.bind(this, this._updateDesktopCloneClip)
4777- });
4778- },
4779-
4780- _hideTray: function() {
4781- // Having the summary item animate out while sliding down the tray
4782- // is distracting, so hide it immediately in case it was visible.
4783- this._summaryBoxPointer.actor.hide();
4784-
4785- this._tween(this.actor, '_trayState', State.HIDDEN,
4786- { y: 0,
4787- time: ANIMATION_TIME,
4788- transition: 'easeOutQuad'
4789- });
4790-
4791- // Note that we might have entered here without a grab,
4792- // which would happen if GrabHelper ungrabbed for us.
4793- // This is a no-op in that case.
4794- this._grabHelper.ungrab({ actor: this.actor });
4795- this._lightbox.hide();
4796- },
4797-
4798- _hideDesktopClone: function(now) {
4799- this._tween(this._desktopClone, '_desktopCloneState', State.HIDDEN,
4800- { y: 0,
4801- time: now ? 0 : ANIMATION_TIME,
4802- transition: 'easeOutQuad',
4803- onComplete: Lang.bind(this, function() {
4804- this._desktopClone.destroy();
4805- this._desktopClone = null;
4806- this._bottomMonitorGeometry = null;
4807- }),
4808- onUpdate: Lang.bind(this, this._updateDesktopCloneClip)
4809- });
4810- },
4811-
4812- _onIdleMonitorWatch: function(monitor, id, userBecameIdle) {
4813- this.idleMonitor.remove_watch(this._idleMonitorWatchId);
4814- this._idleMonitorWatchId = 0;
4815- if (!userBecameIdle)
4816- this._updateNotificationTimeout(2000);
4817- this._userActiveWhileNotificationShown = true;
4818- this._updateState();
4819- },
4820-
4821- _showNotification: function() {
4822- this._notification = this._notificationQueue.shift();
4823- this._userActiveWhileNotificationShown = this.idleMonitor.get_idletime() <= IDLE_TIME;
4824- this._idleMonitorWatchId = this.idleMonitor.add_watch(IDLE_TIME,
4825- Lang.bind(this, this._onIdleMonitorWatch));
4826- this._notificationClickedId = this._notification.connect('done-displaying',
4827- Lang.bind(this, this._escapeTray));
4828- this._notification.connect('unfocused', Lang.bind(this, function() {
4829- this._updateState();
4830- }));
4831- this._notificationBin.child = this._notification.actor;
4832-
4833- this._notificationWidget.opacity = 0;
4834- this._notificationWidget.y = 0;
4835- this._notificationWidget.show();
4836-
4837- this._updateShowingNotification();
4838-
4839- let [x, y, mods] = global.get_pointer();
4840- // We save the position of the mouse at the time when we started showing the notification
4841- // in order to determine if the notification popped up under it. We make that check if
4842- // the user starts moving the mouse and _onTrayHoverChanged() gets called. We don't
4843- // expand the notification if it just happened to pop up under the mouse unless the user
4844- // explicitly mouses away from it and then mouses back in.
4845- this._showNotificationMouseX = x;
4846- this._showNotificationMouseY = y;
4847- // We save the coordinates of the mouse at the time when we started showing the notification
4848- // and then we update it in _notificationTimeout(). We don't pop down the notification if
4849- // the mouse is moving towards it or within it.
4850- this._lastSeenMouseX = x;
4851- this._lastSeenMouseY = y;
4852- },
4853-
4854- _updateShowingNotification: function() {
4855- this._notification.acknowledged = true;
4856-
4857- Tweener.removeTweens(this._notificationWidget);
4858-
4859- // We auto-expand notifications with CRITICAL urgency.
4860- // We use Tweener.removeTweens() to remove a tween that was hiding the notification we are
4861- // updating, in case that notification was in the process of being hidden. However,
4862- // Tweener.removeTweens() would also remove a tween that was updating the position of the
4863- // notification we are updating, in case that notification was already expanded and its height
4864- // changed. Therefore we need to call this._expandNotification() for expanded notifications
4865- // to make sure their position is updated.
4866- if (this._notification.urgency == Urgency.CRITICAL || this._notification.expanded)
4867- this._expandNotification(true);
4868-
4869- // We tween all notifications to full opacity. This ensures that both new notifications and
4870- // notifications that might have been in the process of hiding get full opacity.
4871- //
4872- // We tween any notification showing in the banner mode to banner height
4873- // (this._notificationWidget.y = -this._notificationWidget.height).
4874- // This ensures that both new notifications and notifications in the banner mode that might
4875- // have been in the process of hiding are shown with the banner height.
4876- //
4877- // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
4878- // notification is being shown.
4879- //
4880- // We don't set the y parameter for the tween for expanded notifications because
4881- // this._expandNotification() will result in getting this._notificationWidget.y set to the appropriate
4882- // fully expanded value.
4883- let tweenParams = { opacity: 255,
4884- time: ANIMATION_TIME,
4885- transition: 'easeOutQuad',
4886- onComplete: this._showNotificationCompleted,
4887- onCompleteScope: this
4888- };
4889- if (!this._notification.expanded)
4890- tweenParams.y = -this._notificationWidget.height;
4891-
4892- this._tween(this._notificationWidget, '_notificationState', State.SHOWN, tweenParams);
4893- },
4894-
4895- _showNotificationCompleted: function() {
4896- if (this._notification.urgency != Urgency.CRITICAL)
4897- this._updateNotificationTimeout(NOTIFICATION_TIMEOUT * 1000);
4898- },
4899-
4900- _updateNotificationTimeout: function(timeout) {
4901- if (this._notificationTimeoutId) {
4902- Mainloop.source_remove(this._notificationTimeoutId);
4903- this._notificationTimeoutId = 0;
4904- }
4905- if (timeout > 0)
4906- this._notificationTimeoutId =
4907- Mainloop.timeout_add(timeout,
4908- Lang.bind(this, this._notificationTimeout));
4909- },
4910-
4911- _notificationTimeout: function() {
4912- let [x, y, mods] = global.get_pointer();
4913- if (y > this._lastSeenMouseY + 10 && !this.actor.hover) {
4914- // The mouse is moving towards the notification, so don't
4915- // hide it yet. (We just create a new timeout (and destroy
4916- // the old one) each time because the bookkeeping is
4917- // simpler.)
4918- this._updateNotificationTimeout(1000);
4919- } else if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId &&
4920- (x != this._lastSeenMouseX || y != this._lastSeenMouseY)) {
4921- // Refresh the timeout if the notification originally
4922- // popped up under the pointer, and the pointer is hovering
4923- // inside it.
4924- this._updateNotificationTimeout(1000);
4925- } else {
4926- this._notificationTimeoutId = 0;
4927- this._updateState();
4928- }
4929-
4930- this._lastSeenMouseX = x;
4931- this._lastSeenMouseY = y;
4932- return false;
4933- },
4934-
4935- _hideNotification: function() {
4936- this._grabHelper.ungrab({ actor: this._notification.actor });
4937-
4938- if (this._notificationExpandedId) {
4939- this._notification.disconnect(this._notificationExpandedId);
4940- this._notificationExpandedId = 0;
4941- }
4942-
4943- if (this._notificationRemoved) {
4944- this._notificationWidget.y = this.actor.height;
4945- this._notificationWidget.opacity = 0;
4946- this._notificationState = State.HIDDEN;
4947- this._hideNotificationCompleted();
4948- } else {
4949- this._tween(this._notificationWidget, '_notificationState', State.HIDDEN,
4950- { y: this.actor.height,
4951- opacity: 0,
4952- time: ANIMATION_TIME,
4953- transition: 'easeOutQuad',
4954- onComplete: this._hideNotificationCompleted,
4955- onCompleteScope: this
4956- });
4957-
4958- }
4959- },
4960-
4961- _hideNotificationCompleted: function() {
4962- this._notificationRemoved = false;
4963- this._notificationWidget.hide();
4964- this._closeButton.hide();
4965- this._pointerInTray = false;
4966- this.actor.hover = false; // Clutter doesn't emit notify::hover when actors move
4967- this._notificationBin.child = null;
4968- this._notification.collapseCompleted();
4969- this._notification.disconnect(this._notificationClickedId);
4970- this._notificationClickedId = 0;
4971- let notification = this._notification;
4972- this._notification = null;
4973- if (notification.isTransient)
4974- notification.destroy(NotificationDestroyedReason.EXPIRED);
4975- },
4976-
4977- _expandNotification: function(autoExpanding) {
4978- // Don't grab focus in notifications that are auto-expanded.
4979- if (!autoExpanding)
4980- this._grabHelper.grab({ actor: this._notification.actor,
4981- grabFocus: true });
4982-
4983- if (!this._notificationExpandedId)
4984- this._notificationExpandedId =
4985- this._notification.connect('expanded',
4986- Lang.bind(this, this._onNotificationExpanded));
4987- // Don't animate changes in notifications that are auto-expanding.
4988- this._notification.expand(!autoExpanding);
4989- },
4990-
4991- _onNotificationExpanded: function() {
4992- let expandedY = - this._notificationWidget.height;
4993- this._closeButton.show();
4994-
4995- // Don't animate the notification to its new position if it has shrunk:
4996- // there will be a very visible "gap" that breaks the illusion.
4997- if (this._notificationWidget.y < expandedY) {
4998- this._notificationWidget.y = expandedY;
4999- } else if (this._notification.y != expandedY) {
5000- this._tween(this._notificationWidget, '_notificationState', State.SHOWN,
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: