Merge lp:~darkxst/gnome-shell/lp1128804 into lp:ubuntu/raring/gnome-shell
- lp1128804
- Merge into raring
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Ubuntu branches | Pending | ||
Review via email: mp+149189@code.launchpad.net |
Commit message
Description of the change
update to new upstream release
To post a comment you must log in.
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, '<'); |
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 &, ", ', < and >, escape all other |
2739 | - // occurrences of '&'. |
2740 | - let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&'); |
2741 | - |
2742 | - // Support <b>, <i>, and <u>, escape anything else |
2743 | - // so it displays as raw markup. |
2744 | - _text = _text.replace(/<(?!\/?[biu]>)/g, '<'); |
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.