68
by Tim Lunn
Upstream bugfix for disappearing notifications (LP: #1088759) |
1 |
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2 |
||
3 |
const Clutter = imports.gi.Clutter; |
|
4 |
const GLib = imports.gi.GLib; |
|
5 |
const Gio = imports.gi.Gio; |
|
6 |
const Gtk = imports.gi.Gtk; |
|
7 |
const Atk = imports.gi.Atk; |
|
8 |
const Lang = imports.lang; |
|
9 |
const Mainloop = imports.mainloop; |
|
10 |
const Meta = imports.gi.Meta; |
|
11 |
const Pango = imports.gi.Pango; |
|
12 |
const Shell = imports.gi.Shell; |
|
13 |
const Signals = imports.signals; |
|
14 |
const St = imports.gi.St; |
|
15 |
||
16 |
const BoxPointer = imports.ui.boxpointer; |
|
17 |
const CtrlAltTab = imports.ui.ctrlAltTab; |
|
18 |
const GnomeSession = imports.misc.gnomeSession; |
|
19 |
const GrabHelper = imports.ui.grabHelper; |
|
20 |
const Lightbox = imports.ui.lightbox; |
|
21 |
const Main = imports.ui.main; |
|
22 |
const PointerWatcher = imports.ui.pointerWatcher; |
|
23 |
const PopupMenu = imports.ui.popupMenu; |
|
24 |
const Params = imports.misc.params; |
|
25 |
const Tweener = imports.ui.tweener; |
|
26 |
const Util = imports.misc.util; |
|
27 |
||
28 |
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings'; |
|
29 |
||
30 |
const ANIMATION_TIME = 0.2; |
|
31 |
const NOTIFICATION_TIMEOUT = 4; |
|
32 |
const SUMMARY_TIMEOUT = 1; |
|
33 |
const LONGER_SUMMARY_TIMEOUT = 4; |
|
34 |
||
35 |
const HIDE_TIMEOUT = 0.2; |
|
36 |
const LONGER_HIDE_TIMEOUT = 0.6; |
|
37 |
||
38 |
const NOTIFICATION_ICON_SIZE = 24; |
|
39 |
||
40 |
// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
|
|
41 |
// range from the point where it left the tray.
|
|
42 |
const MOUSE_LEFT_ACTOR_THRESHOLD = 20; |
|
43 |
||
44 |
// Time the user needs to leave the mouse on the bottom pixel row to open the tray
|
|
45 |
const TRAY_DWELL_TIME = 1000; // ms |
|
46 |
// Time resolution when tracking the mouse to catch the open tray dwell
|
|
47 |
const TRAY_DWELL_CHECK_INTERVAL = 100; // ms |
|
48 |
||
49 |
const IDLE_TIME = 1000; |
|
50 |
||
51 |
const State = { |
|
52 |
HIDDEN: 0, |
|
53 |
SHOWING: 1, |
|
54 |
SHOWN: 2, |
|
55 |
HIDING: 3 |
|
56 |
};
|
|
57 |
||
58 |
// These reasons are useful when we destroy the notifications received through
|
|
59 |
// the notification daemon. We use EXPIRED for transient notifications that the
|
|
60 |
// user did not interact with, DISMISSED for all other notifications that were
|
|
61 |
// destroyed as a result of a user action, and SOURCE_CLOSED for the notifications
|
|
62 |
// that were requested to be destroyed by the associated source.
|
|
63 |
const NotificationDestroyedReason = { |
|
64 |
EXPIRED: 1, |
|
65 |
DISMISSED: 2, |
|
66 |
SOURCE_CLOSED: 3 |
|
67 |
};
|
|
68 |
||
69 |
// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
|
|
70 |
// urgency values map to the corresponding values for the notifications received
|
|
71 |
// through the notification daemon. HIGH urgency value is used for chats received
|
|
72 |
// through the Telepathy client.
|
|
73 |
const Urgency = { |
|
74 |
LOW: 0, |
|
75 |
NORMAL: 1, |
|
76 |
HIGH: 2, |
|
77 |
CRITICAL: 3 |
|
78 |
}
|
|
79 |
||
80 |
function _fixMarkup(text, allowMarkup) { |
|
81 |
if (allowMarkup) { |
|
82 |
// Support &, ", ', < and >, escape all other
|
|
83 |
// occurrences of '&'.
|
|
84 |
let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&'); |
|
85 |
||
86 |
// Support <b>, <i>, and <u>, escape anything else
|
|
87 |
// so it displays as raw markup.
|
|
88 |
_text = _text.replace(/<(?!\/?[biu]>)/g, '<'); |
|
89 |
||
90 |
try { |
|
91 |
Pango.parse_markup(_text, -1, ''); |
|
92 |
return _text; |
|
93 |
} catch (e) {} |
|
94 |
}
|
|
95 |
||
96 |
// !allowMarkup, or invalid markup
|
|
97 |
return GLib.markup_escape_text(text, -1); |
|
98 |
}
|
|
99 |
||
100 |
const URLHighlighter = new Lang.Class({ |
|
101 |
Name: 'URLHighlighter', |
|
102 |
||
103 |
_init: function(text, lineWrap, allowMarkup) { |
|
104 |
if (!text) |
|
105 |
text = ''; |
|
106 |
this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' }); |
|
107 |
this._linkColor = '#ccccff'; |
|
108 |
this.actor.connect('style-changed', Lang.bind(this, function() { |
|
109 |
let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false); |
|
110 |
if (hasColor) { |
|
111 |
let linkColor = color.to_string().substr(0, 7); |
|
112 |
if (linkColor != this._linkColor) { |
|
113 |
this._linkColor = linkColor; |
|
114 |
this._highlightUrls(); |
|
115 |
}
|
|
116 |
}
|
|
117 |
}));
|
|
118 |
if (lineWrap) { |
|
119 |
this.actor.clutter_text.line_wrap = true; |
|
120 |
this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; |
|
121 |
this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; |
|
122 |
}
|
|
123 |
||
124 |
this.setMarkup(text, allowMarkup); |
|
125 |
this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { |
|
126 |
// Don't try to URL highlight when invisible.
|
|
127 |
// The MessageTray doesn't actually hide us, so
|
|
128 |
// we need to check for paint opacities as well.
|
|
129 |
if (!actor.visible || actor.get_paint_opacity() == 0) |
|
130 |
return false; |
|
131 |
||
132 |
// Keep Notification.actor from seeing this and taking
|
|
133 |
// a pointer grab, which would block our button-release-event
|
|
134 |
// handler, if an URL is clicked
|
|
135 |
return this._findUrlAtPos(event) != -1; |
|
136 |
}));
|
|
137 |
this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { |
|
138 |
if (!actor.visible || actor.get_paint_opacity() == 0) |
|
139 |
return false; |
|
140 |
||
141 |
let urlId = this._findUrlAtPos(event); |
|
142 |
if (urlId != -1) { |
|
143 |
let url = this._urls[urlId].url; |
|
144 |
if (url.indexOf(':') == -1) |
|
145 |
url = 'http://' + url; |
|
146 |
try { |
|
147 |
Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context()); |
|
148 |
return true; |
|
149 |
} catch (e) { |
|
150 |
// TODO: remove this after gnome 3 release
|
|
151 |
Util.spawn(['gvfs-open', url]); |
|
152 |
return true; |
|
153 |
}
|
|
154 |
}
|
|
155 |
return false; |
|
156 |
}));
|
|
157 |
this.actor.connect('motion-event', Lang.bind(this, function(actor, event) { |
|
158 |
if (!actor.visible || actor.get_paint_opacity() == 0) |
|
159 |
return false; |
|
160 |
||
161 |
let urlId = this._findUrlAtPos(event); |
|
162 |
if (urlId != -1 && !this._cursorChanged) { |
|
163 |
global.set_cursor(Shell.Cursor.POINTING_HAND); |
|
164 |
this._cursorChanged = true; |
|
165 |
} else if (urlId == -1) { |
|
166 |
global.unset_cursor(); |
|
167 |
this._cursorChanged = false; |
|
168 |
}
|
|
169 |
return false; |
|
170 |
}));
|
|
171 |
this.actor.connect('leave-event', Lang.bind(this, function() { |
|
172 |
if (!this.actor.visible || this.actor.get_paint_opacity() == 0) |
|
173 |
return; |
|
174 |
||
175 |
if (this._cursorChanged) { |
|
176 |
this._cursorChanged = false; |
|
177 |
global.unset_cursor(); |
|
178 |
}
|
|
179 |
}));
|
|
180 |
},
|
|
181 |
||
182 |
setMarkup: function(text, allowMarkup) { |
|
183 |
text = text ? _fixMarkup(text, allowMarkup) : ''; |
|
184 |
this._text = text; |
|
185 |
||
186 |
this.actor.clutter_text.set_markup(text); |
|
187 |
/* clutter_text.text contain text without markup */
|
|
188 |
this._urls = Util.findUrls(this.actor.clutter_text.text); |
|
189 |
this._highlightUrls(); |
|
190 |
},
|
|
191 |
||
192 |
_highlightUrls: function() { |
|
193 |
// text here contain markup
|
|
194 |
let urls = Util.findUrls(this._text); |
|
195 |
let markup = ''; |
|
196 |
let pos = 0; |
|
197 |
for (let i = 0; i < urls.length; i++) { |
|
198 |
let url = urls[i]; |
|
199 |
let str = this._text.substr(pos, url.pos - pos); |
|
200 |
markup += str + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>'; |
|
201 |
pos = url.pos + url.url.length; |
|
202 |
}
|
|
203 |
markup += this._text.substr(pos); |
|
204 |
this.actor.clutter_text.set_markup(markup); |
|
205 |
},
|
|
206 |
||
207 |
_findUrlAtPos: function(event) { |
|
208 |
let success; |
|
209 |
let [x, y] = event.get_coords(); |
|
210 |
[success, x, y] = this.actor.transform_stage_point(x, y); |
|
211 |
let find_pos = -1; |
|
212 |
for (let i = 0; i < this.actor.clutter_text.text.length; i++) { |
|
213 |
let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i); |
|
214 |
if (py > y || py + line_height < y || x < px) |
|
215 |
continue; |
|
216 |
find_pos = i; |
|
217 |
}
|
|
218 |
if (find_pos != -1) { |
|
219 |
for (let i = 0; i < this._urls.length; i++) |
|
220 |
if (find_pos >= this._urls[i].pos && |
|
221 |
this._urls[i].pos + this._urls[i].url.length > find_pos) |
|
222 |
return i; |
|
223 |
}
|
|
224 |
return -1; |
|
225 |
}
|
|
226 |
});
|
|
227 |
||
228 |
function makeCloseButton() { |
|
229 |
let closeButton = new St.Button({ style_class: 'notification-close'}); |
|
230 |
||
231 |
// This is a bit tricky. St.Bin has its own x-align/y-align properties
|
|
232 |
// that compete with Clutter's properties. This should be fixed for
|
|
233 |
// Clutter 2.0. Since St.Bin doesn't define its own setters, the
|
|
234 |
// setters are a workaround to get Clutter's version.
|
|
235 |
closeButton.set_x_align(Clutter.ActorAlign.END); |
|
236 |
closeButton.set_y_align(Clutter.ActorAlign.START); |
|
237 |
||
238 |
// XXX Clutter 2.0 workaround: ClutterBinLayout needs expand
|
|
239 |
// to respect the alignments.
|
|
240 |
closeButton.set_x_expand(true); |
|
241 |
closeButton.set_y_expand(true); |
|
242 |
||
243 |
closeButton.connect('style-changed', function() { |
|
244 |
let themeNode = closeButton.get_theme_node(); |
|
245 |
||
246 |
// libcroco doesn't support negative units
|
|
247 |
let direction = closeButton.get_text_direction() == Clutter.TextDirection.RTL ? -1 : 1; |
|
248 |
closeButton.translation_x = direction * themeNode.get_length('-shell-close-overlap-x'); |
|
249 |
||
250 |
// libcroco doesn't support negative units
|
|
251 |
closeButton.translation_y = -themeNode.get_length('-shell-close-overlap-y'); |
|
252 |
});
|
|
253 |
||
254 |
return closeButton; |
|
255 |
}
|
|
256 |
||
257 |
// Notification:
|
|
258 |
// @source: the notification's Source
|
|
259 |
// @title: the title
|
|
260 |
// @banner: the banner text
|
|
261 |
// @params: optional additional params
|
|
262 |
//
|
|
263 |
// Creates a notification. In the banner mode, the notification
|
|
264 |
// will show an icon, @title (in bold) and @banner, all on a single
|
|
265 |
// line (with @banner ellipsized if necessary).
|
|
266 |
//
|
|
267 |
// The notification will be expandable if either it has additional
|
|
268 |
// elements that were added to it or if the @banner text did not
|
|
269 |
// fit fully in the banner mode. When the notification is expanded,
|
|
270 |
// the @banner text from the top line is always removed. The complete
|
|
271 |
// @banner text is added as the first element in the content section,
|
|
272 |
// unless 'customContent' parameter with the value 'true' is specified
|
|
273 |
// in @params.
|
|
274 |
//
|
|
275 |
// Additional notification content can be added with addActor() and
|
|
276 |
// addBody() methods. The notification content is put inside a
|
|
277 |
// scrollview, so if it gets too tall, the notification will scroll
|
|
278 |
// rather than continue to grow. In addition to this main content
|
|
279 |
// area, there is also a single-row action area, which is not
|
|
280 |
// scrolled and can contain a single actor. The action area can
|
|
281 |
// be set by calling setActionArea() method. There is also a
|
|
282 |
// convenience method addButton() for adding a button to the action
|
|
283 |
// area.
|
|
284 |
//
|
|
285 |
// @params can contain values for 'customContent', 'body', 'icon',
|
|
286 |
// 'titleMarkup', 'bannerMarkup', 'bodyMarkup', and 'clear'
|
|
287 |
// parameters.
|
|
288 |
//
|
|
289 |
// If @params contains a 'customContent' parameter with the value %true,
|
|
290 |
// then @banner will not be shown in the body of the notification when the
|
|
291 |
// notification is expanded and calls to update() will not clear the content
|
|
292 |
// unless 'clear' parameter with value %true is explicitly specified.
|
|
293 |
//
|
|
294 |
// If @params contains a 'body' parameter, then that text will be added to
|
|
295 |
// the content area (as with addBody()).
|
|
296 |
//
|
|
297 |
// By default, the icon shown is created by calling
|
|
298 |
// source.createIcon(). However, if @params contains an 'icon'
|
|
299 |
// parameter, the passed in icon will be used.
|
|
300 |
//
|
|
301 |
// If @params contains a 'titleMarkup', 'bannerMarkup', or
|
|
302 |
// 'bodyMarkup' parameter with the value %true, then the corresponding
|
|
303 |
// element is assumed to use pango markup. If the parameter is not
|
|
304 |
// present for an element, then anything that looks like markup in
|
|
305 |
// that element will appear literally in the output.
|
|
306 |
//
|
|
307 |
// If @params contains a 'clear' parameter with the value %true, then
|
|
308 |
// the content and the action area of the notification will be cleared.
|
|
309 |
// The content area is also always cleared if 'customContent' is false
|
|
310 |
// because it might contain the @banner that didn't fit in the banner mode.
|
|
311 |
const Notification = new Lang.Class({ |
|
312 |
Name: 'Notification', |
|
313 |
||
314 |
IMAGE_SIZE: 125, |
|
315 |
||
316 |
_init: function(source, title, banner, params) { |
|
317 |
this.source = source; |
|
318 |
this.title = title; |
|
319 |
this.urgency = Urgency.NORMAL; |
|
320 |
this.resident = false; |
|
321 |
// 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
|
|
322 |
this.isTransient = false; |
|
323 |
this.expanded = false; |
|
324 |
this.focused = false; |
|
325 |
this.acknowledged = false; |
|
326 |
this._destroyed = false; |
|
327 |
this._useActionIcons = false; |
|
328 |
this._customContent = false; |
|
329 |
this._bannerBodyText = null; |
|
330 |
this._bannerBodyMarkup = false; |
|
331 |
this._titleFitsInBannerMode = true; |
|
332 |
this._titleDirection = Clutter.TextDirection.DEFAULT; |
|
333 |
this._spacing = 0; |
|
334 |
this._scrollPolicy = Gtk.PolicyType.AUTOMATIC; |
|
335 |
this._imageBin = null; |
|
336 |
||
337 |
source.connect('destroy', Lang.bind(this, |
|
338 |
function (source, reason) { |
|
339 |
this.destroy(reason); |
|
340 |
}));
|
|
341 |
||
342 |
this.actor = new St.Button({ accessible_role: Atk.Role.NOTIFICATION }); |
|
343 |
this.actor.add_style_class_name('notification-unexpanded'); |
|
344 |
this.actor._delegate = this; |
|
345 |
this.actor.connect('clicked', Lang.bind(this, this._onClicked)); |
|
346 |
this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); |
|
347 |
||
348 |
this._table = new St.Table({ style_class: 'notification', |
|
349 |
reactive: true }); |
|
350 |
this._table.connect('style-changed', Lang.bind(this, this._styleChanged)); |
|
351 |
this.actor.set_child(this._table); |
|
352 |
||
353 |
// The first line should have the title, followed by the
|
|
354 |
// banner text, but ellipsized if they won't both fit. We can't
|
|
355 |
// make St.Table or St.BoxLayout do this the way we want (don't
|
|
356 |
// show banner at all if title needs to be ellipsized), so we
|
|
357 |
// use Shell.GenericContainer.
|
|
358 |
this._bannerBox = new Shell.GenericContainer(); |
|
359 |
this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth)); |
|
360 |
this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight)); |
|
361 |
this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate)); |
|
362 |
this._table.add(this._bannerBox, { row: 0, |
|
363 |
col: 1, |
|
364 |
col_span: 2, |
|
365 |
x_expand: false, |
|
366 |
y_expand: false, |
|
367 |
y_fill: false }); |
|
368 |
||
369 |
// This is an empty cell that overlaps with this._bannerBox cell to ensure
|
|
370 |
// that this._bannerBox cell expands horizontally, while not forcing the
|
|
371 |
// this._imageBin that is also in col: 2 to expand horizontally.
|
|
372 |
this._table.add(new St.Bin(), { row: 0, |
|
373 |
col: 2, |
|
374 |
y_expand: false, |
|
375 |
y_fill: false }); |
|
376 |
||
377 |
this._titleLabel = new St.Label(); |
|
378 |
this._bannerBox.add_actor(this._titleLabel); |
|
379 |
this._bannerUrlHighlighter = new URLHighlighter(); |
|
380 |
this._bannerLabel = this._bannerUrlHighlighter.actor; |
|
381 |
this._bannerBox.add_actor(this._bannerLabel); |
|
382 |
||
383 |
this.update(title, banner, params); |
|
384 |
},
|
|
385 |
||
386 |
// update:
|
|
387 |
// @title: the new title
|
|
388 |
// @banner: the new banner
|
|
389 |
// @params: as in the Notification constructor
|
|
390 |
//
|
|
391 |
// Updates the notification by regenerating its icon and updating
|
|
392 |
// the title/banner. If @params.clear is %true, it will also
|
|
393 |
// remove any additional actors/action buttons previously added.
|
|
394 |
update: function(title, banner, params) { |
|
395 |
params = Params.parse(params, { customContent: false, |
|
396 |
body: null, |
|
397 |
icon: null, |
|
398 |
secondaryIcon: null, |
|
399 |
titleMarkup: false, |
|
400 |
bannerMarkup: false, |
|
401 |
bodyMarkup: false, |
|
402 |
clear: false }); |
|
403 |
||
404 |
this._customContent = params.customContent; |
|
405 |
||
406 |
let oldFocus = global.stage.key_focus; |
|
407 |
||
408 |
if (this._icon && (params.icon || params.clear)) { |
|
409 |
this._icon.destroy(); |
|
410 |
this._icon = null; |
|
411 |
}
|
|
412 |
||
413 |
if (this._secondaryIcon && (params.secondaryIcon || params.clear)) { |
|
414 |
this._secondaryIcon.destroy(); |
|
415 |
this._secondaryIcon = null; |
|
416 |
}
|
|
417 |
||
418 |
// We always clear the content area if we don't have custom
|
|
419 |
// content because it might contain the @banner that didn't
|
|
420 |
// fit in the banner mode.
|
|
421 |
if (this._scrollArea && (!this._customContent || params.clear)) { |
|
422 |
if (oldFocus && this._scrollArea.contains(oldFocus)) |
|
423 |
this.actor.grab_key_focus(); |
|
424 |
||
425 |
this._scrollArea.destroy(); |
|
426 |
this._scrollArea = null; |
|
427 |
this._contentArea = null; |
|
428 |
}
|
|
429 |
if (this._actionArea && params.clear) { |
|
430 |
if (oldFocus && this._actionArea.contains(oldFocus)) |
|
431 |
this.actor.grab_key_focus(); |
|
432 |
||
433 |
this._actionArea.destroy(); |
|
434 |
this._actionArea = null; |
|
435 |
this._buttonBox = null; |
|
436 |
}
|
|
437 |
if (this._imageBin && params.clear) |
|
438 |
this.unsetImage(); |
|
439 |
||
440 |
if (!this._scrollArea && !this._actionArea && !this._imageBin) |
|
441 |
this._table.remove_style_class_name('multi-line-notification'); |
|
442 |
||
443 |
if (!this._icon) { |
|
444 |
this._icon = params.icon || this.source.createIcon(NOTIFICATION_ICON_SIZE); |
|
445 |
this._table.add(this._icon, { row: 0, |
|
446 |
col: 0, |
|
447 |
x_expand: false, |
|
448 |
y_expand: false, |
|
449 |
y_fill: false, |
|
450 |
y_align: St.Align.START }); |
|
451 |
}
|
|
452 |
||
453 |
if (!this._secondaryIcon) { |
|
454 |
this._secondaryIcon = params.secondaryIcon; |
|
455 |
||
456 |
if (this._secondaryIcon) |
|
457 |
this._bannerBox.add_actor(this._secondaryIcon); |
|
458 |
}
|
|
459 |
||
460 |
this.title = title; |
|
461 |
title = title ? _fixMarkup(title.replace(/\n/g, ' '), params.titleMarkup) : ''; |
|
462 |
this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>'); |
|
463 |
||
464 |
if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL) |
|
465 |
this._titleDirection = Clutter.TextDirection.RTL; |
|
466 |
else
|
|
467 |
this._titleDirection = Clutter.TextDirection.LTR; |
|
468 |
||
469 |
// Let the title's text direction control the overall direction
|
|
470 |
// of the notification - in case where different scripts are used
|
|
471 |
// in the notification, this is the right thing for the icon, and
|
|
472 |
// arguably for action buttons as well. Labels other than the title
|
|
473 |
// will be allocated at the available width, so that their alignment
|
|
474 |
// is done correctly automatically.
|
|
475 |
this._table.set_text_direction(this._titleDirection); |
|
476 |
||
477 |
// Unless the notification has custom content, we save this._bannerBodyText
|
|
478 |
// to add it to the content of the notification if the notification is
|
|
479 |
// expandable due to other elements in its content area or due to the banner
|
|
480 |
// not fitting fully in the single-line mode.
|
|
481 |
this._bannerBodyText = this._customContent ? null : banner; |
|
482 |
this._bannerBodyMarkup = params.bannerMarkup; |
|
483 |
||
484 |
banner = banner ? banner.replace(/\n/g, ' ') : ''; |
|
485 |
||
486 |
this._bannerUrlHighlighter.setMarkup(banner, params.bannerMarkup); |
|
487 |
this._bannerLabel.queue_relayout(); |
|
488 |
||
489 |
// Add the bannerBody now if we know for sure we'll need it
|
|
490 |
if (this._bannerBodyText && this._bannerBodyText.indexOf('\n') > -1) |
|
491 |
this._addBannerBody(); |
|
492 |
||
493 |
if (params.body) |
|
494 |
this.addBody(params.body, params.bodyMarkup); |
|
495 |
this.updated(); |
|
496 |
},
|
|
497 |
||
498 |
setIconVisible: function(visible) { |
|
499 |
this._icon.visible = visible; |
|
500 |
},
|
|
501 |
||
502 |
enableScrolling: function(enableScrolling) { |
|
503 |
this._scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER; |
|
504 |
if (this._scrollArea) { |
|
505 |
this._scrollArea.vscrollbar_policy = this._scrollPolicy; |
|
506 |
this._scrollArea.enable_mouse_scrolling = enableScrolling; |
|
507 |
}
|
|
508 |
},
|
|
509 |
||
510 |
_createScrollArea: function() { |
|
511 |
this._table.add_style_class_name('multi-line-notification'); |
|
512 |
this._scrollArea = new St.ScrollView({ style_class: 'notification-scrollview', |
|
513 |
vscrollbar_policy: this._scrollPolicy, |
|
514 |
hscrollbar_policy: Gtk.PolicyType.NEVER }); |
|
515 |
this._table.add(this._scrollArea, { row: 1, |
|
516 |
col: 2 }); |
|
517 |
this._updateLastColumnSettings(); |
|
518 |
this._contentArea = new St.BoxLayout({ style_class: 'notification-body', |
|
519 |
vertical: true }); |
|
520 |
this._scrollArea.add_actor(this._contentArea); |
|
521 |
// If we know the notification will be expandable, we need to add
|
|
522 |
// the banner text to the body as the first element.
|
|
523 |
this._addBannerBody(); |
|
524 |
},
|
|
525 |
||
526 |
// addActor:
|
|
527 |
// @actor: actor to add to the body of the notification
|
|
528 |
//
|
|
529 |
// Appends @actor to the notification's body
|
|
530 |
addActor: function(actor, style) { |
|
531 |
if (!this._scrollArea) { |
|
532 |
this._createScrollArea(); |
|
533 |
}
|
|
534 |
||
535 |
this._contentArea.add(actor, style ? style : {}); |
|
536 |
this.updated(); |
|
537 |
},
|
|
538 |
||
539 |
// addBody:
|
|
540 |
// @text: the text
|
|
541 |
// @markup: %true if @text contains pango markup
|
|
542 |
// @style: style to use when adding the actor containing the text
|
|
543 |
//
|
|
544 |
// Adds a multi-line label containing @text to the notification.
|
|
545 |
//
|
|
546 |
// Return value: the newly-added label
|
|
547 |
addBody: function(text, markup, style) { |
|
548 |
let label = new URLHighlighter(text, true, markup); |
|
549 |
||
550 |
this.addActor(label.actor, style); |
|
551 |
return label.actor; |
|
552 |
},
|
|
553 |
||
554 |
_addBannerBody: function() { |
|
555 |
if (this._bannerBodyText) { |
|
556 |
let text = this._bannerBodyText; |
|
557 |
this._bannerBodyText = null; |
|
558 |
this.addBody(text, this._bannerBodyMarkup); |
|
559 |
}
|
|
560 |
},
|
|
561 |
||
562 |
// scrollTo:
|
|
563 |
// @side: St.Side.TOP or St.Side.BOTTOM
|
|
564 |
//
|
|
565 |
// Scrolls the content area (if scrollable) to the indicated edge
|
|
566 |
scrollTo: function(side) { |
|
567 |
let adjustment = this._scrollArea.vscroll.adjustment; |
|
568 |
if (side == St.Side.TOP) |
|
569 |
adjustment.value = adjustment.lower; |
|
570 |
else if (side == St.Side.BOTTOM) |
|
571 |
adjustment.value = adjustment.upper; |
|
572 |
},
|
|
573 |
||
574 |
// setActionArea:
|
|
575 |
// @actor: the actor
|
|
576 |
// @props: (option) St.Table child properties
|
|
577 |
//
|
|
578 |
// Puts @actor into the action area of the notification, replacing
|
|
579 |
// the previous contents
|
|
580 |
setActionArea: function(actor, props) { |
|
581 |
if (this._actionArea) { |
|
582 |
this._actionArea.destroy(); |
|
583 |
this._actionArea = null; |
|
584 |
if (this._buttonBox) |
|
585 |
this._buttonBox = null; |
|
586 |
} else { |
|
587 |
this._addBannerBody(); |
|
588 |
}
|
|
589 |
this._actionArea = actor; |
|
590 |
||
591 |
if (!props) |
|
592 |
props = {}; |
|
593 |
props.row = 2; |
|
594 |
props.col = 2; |
|
595 |
||
596 |
this._table.add_style_class_name('multi-line-notification'); |
|
597 |
this._table.add(this._actionArea, props); |
|
598 |
this._updateLastColumnSettings(); |
|
599 |
this.updated(); |
|
600 |
},
|
|
601 |
||
602 |
_updateLastColumnSettings: function() { |
|
603 |
if (this._scrollArea) |
|
604 |
this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1, |
|
605 |
col_span: this._imageBin ? 1 : 2 }); |
|
606 |
if (this._actionArea) |
|
607 |
this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1, |
|
608 |
col_span: this._imageBin ? 1 : 2 }); |
|
609 |
},
|
|
610 |
||
611 |
setImage: function(image) { |
|
612 |
if (this._imageBin) |
|
613 |
this.unsetImage(); |
|
614 |
this._imageBin = new St.Bin(); |
|
615 |
this._imageBin.child = image; |
|
616 |
this._imageBin.opacity = 230; |
|
617 |
this._table.add_style_class_name('multi-line-notification'); |
|
618 |
this._table.add_style_class_name('notification-with-image'); |
|
619 |
this._addBannerBody(); |
|
620 |
this._updateLastColumnSettings(); |
|
621 |
this._table.add(this._imageBin, { row: 1, |
|
622 |
col: 1, |
|
623 |
row_span: 2, |
|
624 |
x_expand: false, |
|
625 |
y_expand: false, |
|
626 |
x_fill: false, |
|
627 |
y_fill: false }); |
|
628 |
},
|
|
629 |
||
630 |
unsetImage: function() { |
|
631 |
if (this._imageBin) { |
|
632 |
this._table.remove_style_class_name('notification-with-image'); |
|
633 |
this._table.remove_actor(this._imageBin); |
|
634 |
this._imageBin = null; |
|
635 |
this._updateLastColumnSettings(); |
|
636 |
if (!this._scrollArea && !this._actionArea) |
|
637 |
this._table.remove_style_class_name('multi-line-notification'); |
|
638 |
}
|
|
639 |
},
|
|
640 |
||
641 |
// addButton:
|
|
642 |
// @id: the action ID
|
|
643 |
// @label: the label for the action's button
|
|
644 |
//
|
|
645 |
// Adds a button with the given @label to the notification. All
|
|
646 |
// action buttons will appear in a single row at the bottom of
|
|
647 |
// the notification.
|
|
648 |
//
|
|
649 |
// If the button is clicked, the notification will emit the
|
|
650 |
// %action-invoked signal with @id as a parameter
|
|
651 |
addButton: function(id, label) { |
|
652 |
if (!this._buttonBox) { |
|
653 |
||
654 |
let box = new St.BoxLayout({ style_class: 'notification-actions' }); |
|
655 |
this.setActionArea(box, { x_expand: false, |
|
656 |
y_expand: false, |
|
657 |
x_fill: false, |
|
658 |
y_fill: false, |
|
659 |
x_align: St.Align.END }); |
|
660 |
this._buttonBox = box; |
|
661 |
}
|
|
662 |
||
663 |
let button = new St.Button({ can_focus: true }); |
|
664 |
button._actionId = id; |
|
665 |
||
666 |
if (this._useActionIcons && Gtk.IconTheme.get_default().has_icon(id)) { |
|
667 |
button.add_style_class_name('notification-icon-button'); |
|
668 |
button.child = new St.Icon({ icon_name: id }); |
|
669 |
} else { |
|
670 |
button.add_style_class_name('notification-button'); |
|
671 |
button.label = label; |
|
672 |
}
|
|
673 |
||
674 |
if (this._buttonBox.get_n_children() > 0) |
|
675 |
global.focus_manager.remove_group(this._buttonBox); |
|
676 |
||
677 |
this._buttonBox.add(button); |
|
678 |
global.focus_manager.add_group(this._buttonBox); |
|
679 |
button.connect('clicked', Lang.bind(this, this._onActionInvoked, id)); |
|
680 |
||
681 |
this.updated(); |
|
682 |
},
|
|
683 |
||
684 |
// setButtonSensitive:
|
|
685 |
// @id: the action ID
|
|
686 |
// @sensitive: whether the button should be sensitive
|
|
687 |
//
|
|
688 |
// If the notification contains a button with action ID @id,
|
|
689 |
// its sensitivity will be set to @sensitive. Insensitive
|
|
690 |
// buttons cannot be clicked.
|
|
691 |
setButtonSensitive: function(id, sensitive) { |
|
692 |
if (!this._buttonBox) |
|
693 |
return; |
|
694 |
||
695 |
let button = this._buttonBox.get_children().filter(function(b) { |
|
696 |
return b._actionId == id; |
|
697 |
})[0]; |
|
698 |
||
699 |
if (!button || button.reactive == sensitive) |
|
700 |
return; |
|
701 |
||
702 |
button.reactive = sensitive; |
|
703 |
},
|
|
704 |
||
705 |
setUrgency: function(urgency) { |
|
706 |
this.urgency = urgency; |
|
707 |
},
|
|
708 |
||
709 |
setResident: function(resident) { |
|
710 |
this.resident = resident; |
|
711 |
},
|
|
712 |
||
713 |
setTransient: function(isTransient) { |
|
714 |
this.isTransient = isTransient; |
|
715 |
},
|
|
716 |
||
717 |
setUseActionIcons: function(useIcons) { |
|
718 |
this._useActionIcons = useIcons; |
|
719 |
},
|
|
720 |
||
721 |
_styleChanged: function() { |
|
722 |
this._spacing = this._table.get_theme_node().get_length('spacing-columns'); |
|
723 |
},
|
|
724 |
||
725 |
_bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) { |
|
726 |
let [titleMin, titleNat] = this._titleLabel.get_preferred_width(forHeight); |
|
727 |
let [bannerMin, bannerNat] = this._bannerLabel.get_preferred_width(forHeight); |
|
728 |
||
729 |
if (this._secondaryIcon) { |
|
730 |
let [secondaryIconMin, secondaryIconNat] = this._secondaryIcon.get_preferred_width(forHeight); |
|
731 |
||
732 |
alloc.min_size = secondaryIconMin + this._spacing + titleMin; |
|
733 |
alloc.natural_size = secondaryIconNat + this._spacing + titleNat + this._spacing + bannerNat; |
|
734 |
} else { |
|
735 |
alloc.min_size = titleMin; |
|
736 |
alloc.natural_size = titleNat + this._spacing + bannerNat; |
|
737 |
}
|
|
738 |
},
|
|
739 |
||
740 |
_bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) { |
|
741 |
[alloc.min_size, alloc.natural_size] = |
|
742 |
this._titleLabel.get_preferred_height(forWidth); |
|
743 |
},
|
|
744 |
||
745 |
_bannerBoxAllocate: function(actor, box, flags) { |
|
746 |
let availWidth = box.x2 - box.x1; |
|
747 |
||
748 |
let [titleMinW, titleNatW] = this._titleLabel.get_preferred_width(-1); |
|
749 |
let [titleMinH, titleNatH] = this._titleLabel.get_preferred_height(availWidth); |
|
750 |
let [bannerMinW, bannerNatW] = this._bannerLabel.get_preferred_width(availWidth); |
|
751 |
||
752 |
let rtl = (this._titleDirection == Clutter.TextDirection.RTL); |
|
753 |
let x = rtl ? availWidth : 0; |
|
754 |
||
755 |
if (this._secondaryIcon) { |
|
756 |
let [iconMinW, iconNatW] = this._secondaryIcon.get_preferred_width(-1); |
|
757 |
let [iconMinH, iconNatH] = this._secondaryIcon.get_preferred_height(availWidth); |
|
758 |
||
759 |
let secondaryIconBox = new Clutter.ActorBox(); |
|
760 |
let secondaryIconBoxW = Math.min(iconNatW, availWidth); |
|
761 |
||
762 |
// allocate secondary icon box
|
|
763 |
if (rtl) { |
|
764 |
secondaryIconBox.x1 = x - secondaryIconBoxW; |
|
765 |
secondaryIconBox.x2 = x; |
|
766 |
x = x - (secondaryIconBoxW + this._spacing); |
|
767 |
} else { |
|
768 |
secondaryIconBox.x1 = x; |
|
769 |
secondaryIconBox.x2 = x + secondaryIconBoxW; |
|
770 |
x = x + secondaryIconBoxW + this._spacing; |
|
771 |
}
|
|
772 |
secondaryIconBox.y1 = 0; |
|
773 |
// Using titleNatH ensures that the secondary icon is centered vertically
|
|
774 |
secondaryIconBox.y2 = titleNatH; |
|
775 |
||
776 |
availWidth = availWidth - (secondaryIconBoxW + this._spacing); |
|
777 |
this._secondaryIcon.allocate(secondaryIconBox, flags); |
|
778 |
}
|
|
779 |
||
780 |
let titleBox = new Clutter.ActorBox(); |
|
781 |
let titleBoxW = Math.min(titleNatW, availWidth); |
|
782 |
if (rtl) { |
|
783 |
titleBox.x1 = availWidth - titleBoxW; |
|
784 |
titleBox.x2 = availWidth; |
|
785 |
} else { |
|
786 |
titleBox.x1 = x; |
|
787 |
titleBox.x2 = titleBox.x1 + titleBoxW; |
|
788 |
}
|
|
789 |
titleBox.y1 = 0; |
|
790 |
titleBox.y2 = titleNatH; |
|
791 |
this._titleLabel.allocate(titleBox, flags); |
|
792 |
this._titleFitsInBannerMode = (titleNatW <= availWidth); |
|
793 |
||
794 |
let bannerFits = true; |
|
795 |
if (titleBoxW + this._spacing > availWidth) { |
|
796 |
this._bannerLabel.opacity = 0; |
|
797 |
bannerFits = false; |
|
798 |
} else { |
|
799 |
let bannerBox = new Clutter.ActorBox(); |
|
800 |
||
801 |
if (rtl) { |
|
802 |
bannerBox.x1 = 0; |
|
803 |
bannerBox.x2 = titleBox.x1 - this._spacing; |
|
804 |
||
805 |
bannerFits = (bannerBox.x2 - bannerNatW >= 0); |
|
806 |
} else { |
|
807 |
bannerBox.x1 = titleBox.x2 + this._spacing; |
|
808 |
bannerBox.x2 = availWidth; |
|
809 |
||
810 |
bannerFits = (bannerBox.x1 + bannerNatW <= availWidth); |
|
811 |
}
|
|
812 |
bannerBox.y1 = 0; |
|
813 |
bannerBox.y2 = titleNatH; |
|
814 |
this._bannerLabel.allocate(bannerBox, flags); |
|
815 |
||
816 |
// Make _bannerLabel visible if the entire notification
|
|
817 |
// fits on one line, or if the notification is currently
|
|
818 |
// unexpanded and only showing one line anyway.
|
|
819 |
if (!this.expanded || (bannerFits && this._table.row_count == 1)) |
|
820 |
this._bannerLabel.opacity = 255; |
|
821 |
}
|
|
822 |
||
823 |
// If the banner doesn't fully fit in the banner box, we possibly need to add the
|
|
824 |
// banner to the body. We can't do that from here though since that will force a
|
|
825 |
// relayout, so we add it to the main loop.
|
|
826 |
if (!bannerFits && this._canExpandContent()) |
|
827 |
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, |
|
828 |
Lang.bind(this, |
|
829 |
function() { |
|
830 |
if (this._canExpandContent()) { |
|
831 |
this._addBannerBody(); |
|
832 |
this._table.add_style_class_name('multi-line-notification'); |
|
833 |
this.updated(); |
|
834 |
}
|
|
835 |
return false; |
|
836 |
}));
|
|
837 |
},
|
|
838 |
||
839 |
_canExpandContent: function() { |
|
840 |
return this._bannerBodyText || |
|
841 |
(!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification')); |
|
842 |
},
|
|
843 |
||
844 |
updated: function() { |
|
845 |
if (this.expanded) |
|
846 |
this.expand(false); |
|
847 |
},
|
|
848 |
||
849 |
expand: function(animate) { |
|
850 |
this.expanded = true; |
|
851 |
this.actor.remove_style_class_name('notification-unexpanded'); |
|
852 |
||
853 |
// The banner is never shown when the title did not fit, so this
|
|
854 |
// can be an if-else statement.
|
|
855 |
if (!this._titleFitsInBannerMode) { |
|
856 |
// Remove ellipsization from the title label and make it wrap so that
|
|
857 |
// we show the full title when the notification is expanded.
|
|
858 |
this._titleLabel.clutter_text.line_wrap = true; |
|
859 |
this._titleLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; |
|
860 |
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; |
|
861 |
} else if (this._table.row_count > 1 && this._bannerLabel.opacity != 0) { |
|
862 |
// We always hide the banner if the notification has additional content.
|
|
863 |
//
|
|
864 |
// We don't need to wrap the banner that doesn't fit the way we wrap the
|
|
865 |
// title that doesn't fit because we won't have a notification with
|
|
866 |
// row_count=1 that has a banner that doesn't fully fit. We'll either add
|
|
867 |
// that banner to the content of the notification in _bannerBoxAllocate()
|
|
868 |
// or the notification will have custom content.
|
|
869 |
if (animate) |
|
870 |
Tweener.addTween(this._bannerLabel, |
|
871 |
{ opacity: 0, |
|
872 |
time: ANIMATION_TIME, |
|
873 |
transition: 'easeOutQuad' }); |
|
874 |
else
|
|
875 |
this._bannerLabel.opacity = 0; |
|
876 |
}
|
|
877 |
this.emit('expanded'); |
|
878 |
},
|
|
879 |
||
880 |
collapseCompleted: function() { |
|
881 |
if (this._destroyed) |
|
882 |
return; |
|
883 |
this.expanded = false; |
|
884 |
// Make sure we don't line wrap the title, and ellipsize it instead.
|
|
885 |
this._titleLabel.clutter_text.line_wrap = false; |
|
886 |
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END; |
|
887 |
// Restore banner opacity in case the notification is shown in the
|
|
888 |
// banner mode again on update.
|
|
889 |
this._bannerLabel.opacity = 255; |
|
890 |
// Restore height requisition
|
|
891 |
this.actor.add_style_class_name('notification-unexpanded'); |
|
892 |
this.emit('collapsed'); |
|
893 |
},
|
|
894 |
||
895 |
_onActionInvoked: function(actor, mouseButtonClicked, id) { |
|
896 |
this.emit('action-invoked', id); |
|
897 |
if (!this.resident) { |
|
898 |
// We don't hide a resident notification when the user invokes one of its actions,
|
|
899 |
// because it is common for such notifications to update themselves with new
|
|
900 |
// information based on the action. We'd like to display the updated information
|
|
901 |
// in place, rather than pop-up a new notification.
|
|
902 |
this.emit('done-displaying'); |
|
903 |
this.destroy(); |
|
904 |
}
|
|
905 |
},
|
|
906 |
||
907 |
_onClicked: function() { |
|
908 |
this.emit('clicked'); |
|
909 |
// We hide all types of notifications once the user clicks on them because the common
|
|
910 |
// outcome of clicking should be the relevant window being brought forward and the user's
|
|
911 |
// attention switching to the window.
|
|
912 |
this.emit('done-displaying'); |
|
913 |
if (!this.resident) |
|
914 |
this.destroy(); |
|
915 |
},
|
|
916 |
||
917 |
_onDestroy: function() { |
|
918 |
if (this._destroyed) |
|
919 |
return; |
|
920 |
this._destroyed = true; |
|
921 |
if (!this._destroyedReason) |
|
922 |
this._destroyedReason = NotificationDestroyedReason.DISMISSED; |
|
923 |
this.emit('destroy', this._destroyedReason); |
|
924 |
},
|
|
925 |
||
926 |
destroy: function(reason) { |
|
927 |
this._destroyedReason = reason; |
|
928 |
this.actor.destroy(); |
|
929 |
this.actor._delegate = null; |
|
930 |
}
|
|
931 |
});
|
|
932 |
Signals.addSignalMethods(Notification.prototype); |
|
933 |
||
934 |
const SourceActor = new Lang.Class({ |
|
935 |
Name: 'SourceActor', |
|
936 |
||
937 |
_init: function(source, size) { |
|
938 |
this._source = source; |
|
939 |
this._size = size; |
|
940 |
||
941 |
this.actor = new Shell.GenericContainer(); |
|
942 |
this.actor.connect('get-preferred-width', Lang.bind(this, this._getPreferredWidth)); |
|
943 |
this.actor.connect('get-preferred-height', Lang.bind(this, this._getPreferredHeight)); |
|
944 |
this.actor.connect('allocate', Lang.bind(this, this._allocate)); |
|
945 |
this.actor.connect('destroy', Lang.bind(this, function() { |
|
946 |
this._actorDestroyed = true; |
|
947 |
}));
|
|
948 |
this._actorDestroyed = false; |
|
949 |
||
950 |
this._counterLabel = new St.Label( {x_align: Clutter.ActorAlign.CENTER, |
|
951 |
x_expand: true, |
|
952 |
y_align: Clutter.ActorAlign.CENTER, |
|
953 |
y_expand: true }); |
|
954 |
||
955 |
this._counterBin = new St.Bin({ style_class: 'summary-source-counter', |
|
956 |
child: this._counterLabel, |
|
957 |
layout_manager: new Clutter.BinLayout() }); |
|
958 |
this._counterBin.hide(); |
|
959 |
||
960 |
this._counterBin.connect('style-changed', Lang.bind(this, function() { |
|
961 |
let themeNode = this._counterBin.get_theme_node(); |
|
962 |
this._counterBin.translation_x = themeNode.get_length('-shell-counter-overlap-x'); |
|
963 |
this._counterBin.translation_y = themeNode.get_length('-shell-counter-overlap-y'); |
|
964 |
}));
|
|
965 |
||
966 |
this._iconBin = new St.Bin({ width: size, |
|
967 |
height: size, |
|
968 |
x_fill: true, |
|
969 |
y_fill: true }); |
|
970 |
||
971 |
this.actor.add_actor(this._iconBin); |
|
972 |
this.actor.add_actor(this._counterBin); |
|
973 |
||
974 |
this._source.connect('count-updated', Lang.bind(this, this._updateCount)); |
|
975 |
this._updateCount(); |
|
976 |
||
977 |
this._source.connect('icon-updated', Lang.bind(this, this._updateIcon)); |
|
978 |
this._updateIcon(); |
|
979 |
},
|
|
980 |
||
981 |
setIcon: function(icon) { |
|
982 |
this._iconBin.child = icon; |
|
983 |
this._iconSet = true; |
|
984 |
},
|
|
985 |
||
986 |
_getPreferredWidth: function (actor, forHeight, alloc) { |
|
987 |
let [min, nat] = this._iconBin.get_preferred_width(forHeight); |
|
988 |
alloc.min_size = min; alloc.nat_size = nat; |
|
989 |
},
|
|
990 |
||
991 |
_getPreferredHeight: function (actor, forWidth, alloc) { |
|
992 |
let [min, nat] = this._iconBin.get_preferred_height(forWidth); |
|
993 |
alloc.min_size = min; alloc.nat_size = nat; |
|
994 |
},
|
|
995 |
||
996 |
_allocate: function(actor, box, flags) { |
|
997 |
// the iconBin should fill our entire box
|
|
998 |
this._iconBin.allocate(box, flags); |
|
999 |
||
1000 |
let childBox = new Clutter.ActorBox(); |
|
1001 |
||
1002 |
let [minWidth, minHeight, naturalWidth, naturalHeight] = this._counterBin.get_preferred_size(); |
|
1003 |
let direction = this.actor.get_text_direction(); |
|
1004 |
||
1005 |
if (direction == Clutter.TextDirection.LTR) { |
|
1006 |
// allocate on the right in LTR
|
|
1007 |
childBox.x1 = box.x2 - naturalWidth; |
|
1008 |
childBox.x2 = box.x2; |
|
1009 |
} else { |
|
1010 |
// allocate on the left in RTL
|
|
1011 |
childBox.x1 = 0; |
|
1012 |
childBox.x2 = naturalWidth; |
|
1013 |
}
|
|
1014 |
||
1015 |
childBox.y1 = box.y2 - naturalHeight; |
|
1016 |
childBox.y2 = box.y2; |
|
1017 |
||
1018 |
this._counterBin.allocate(childBox, flags); |
|
1019 |
},
|
|
1020 |
||
1021 |
_updateIcon: function() { |
|
1022 |
if (this._actorDestroyed) |
|
1023 |
return; |
|
1024 |
||
1025 |
if (!this._iconSet) |
|
1026 |
this._iconBin.child = this._source.createIcon(this._size); |
|
1027 |
},
|
|
1028 |
||
1029 |
_updateCount: function() { |
|
1030 |
if (this._actorDestroyed) |
|
1031 |
return; |
|
1032 |
||
1033 |
this._counterBin.visible = this._source.countVisible; |
|
1034 |
||
1035 |
let text; |
|
1036 |
if (this._source.count < 100) |
|
1037 |
text = this._source.count.toString(); |
|
1038 |
else
|
|
1039 |
text = String.fromCharCode(0x22EF); // midline horizontal ellipsis |
|
1040 |
||
1041 |
this._counterLabel.set_text(text); |
|
1042 |
}
|
|
1043 |
});
|
|
1044 |
||
1045 |
const Source = new Lang.Class({ |
|
1046 |
Name: 'MessageTraySource', |
|
1047 |
||
1048 |
SOURCE_ICON_SIZE: 48, |
|
1049 |
||
1050 |
_init: function(title, iconName) { |
|
1051 |
this.title = title; |
|
1052 |
this.iconName = iconName; |
|
1053 |
||
1054 |
this.isTransient = false; |
|
1055 |
this.isChat = false; |
|
1056 |
this.isMuted = false; |
|
1057 |
this.showInLockScreen = true; |
|
1058 |
this.keepTrayOnSummaryClick = false; |
|
1059 |
||
1060 |
this.notifications = []; |
|
1061 |
},
|
|
1062 |
||
1063 |
get count() { |
|
1064 |
return this.notifications.length; |
|
1065 |
},
|
|
1066 |
||
1067 |
get unseenCount() { |
|
1068 |
return this.notifications.filter(function(n) { return !n.acknowledged; }).length; |
|
1069 |
},
|
|
1070 |
||
1071 |
get countVisible() { |
|
1072 |
return this.count > 1; |
|
1073 |
},
|
|
1074 |
||
1075 |
countUpdated: function() { |
|
1076 |
this.emit('count-updated'); |
|
1077 |
},
|
|
1078 |
||
1079 |
buildRightClickMenu: function() { |
|
1080 |
let item; |
|
1081 |
let rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu', |
|
1082 |
vertical: true }); |
|
1083 |
||
1084 |
item = new PopupMenu.PopupMenuItem(_("Open")); |
|
1085 |
item.connect('activate', Lang.bind(this, function() { |
|
1086 |
this.open(); |
|
1087 |
this.emit('done-displaying-content'); |
|
1088 |
}));
|
|
1089 |
rightClickMenu.add(item.actor); |
|
1090 |
||
1091 |
item = new PopupMenu.PopupMenuItem(_("Remove")); |
|
1092 |
item.connect('activate', Lang.bind(this, function() { |
|
1093 |
this.destroy(); |
|
1094 |
this.emit('done-displaying-content'); |
|
1095 |
}));
|
|
1096 |
rightClickMenu.add(item.actor); |
|
1097 |
return rightClickMenu; |
|
1098 |
},
|
|
1099 |
||
1100 |
setTransient: function(isTransient) { |
|
1101 |
this.isTransient = isTransient; |
|
1102 |
},
|
|
1103 |
||
1104 |
setTitle: function(newTitle) { |
|
1105 |
this.title = newTitle; |
|
1106 |
this.emit('title-changed'); |
|
1107 |
},
|
|
1108 |
||
1109 |
setMuted: function(muted) { |
|
1110 |
if (!this.isChat || this.isMuted == muted) |
|
1111 |
return; |
|
1112 |
this.isMuted = muted; |
|
1113 |
this.emit('muted-changed'); |
|
1114 |
},
|
|
1115 |
||
1116 |
// Called to create a new icon actor.
|
|
1117 |
// Provides a sane default implementation, override if you need
|
|
1118 |
// something more fancy.
|
|
1119 |
createIcon: function(size) { |
|
1120 |
return new St.Icon({ icon_name: this.iconName, |
|
1121 |
icon_size: size }); |
|
1122 |
},
|
|
1123 |
||
1124 |
_ensureMainIcon: function() { |
|
1125 |
if (this._mainIcon) |
|
1126 |
return; |
|
1127 |
||
1128 |
this._mainIcon = new SourceActor(this, this.SOURCE_ICON_SIZE); |
|
1129 |
},
|
|
1130 |
||
1131 |
// Unlike createIcon, this always returns the same actor;
|
|
1132 |
// there is only one summary icon actor for a Source.
|
|
1133 |
getSummaryIcon: function() { |
|
1134 |
this._ensureMainIcon(); |
|
1135 |
return this._mainIcon.actor; |
|
1136 |
},
|
|
1137 |
||
1138 |
pushNotification: function(notification) { |
|
1139 |
if (this.notifications.indexOf(notification) < 0) { |
|
1140 |
this.notifications.push(notification); |
|
1141 |
this.emit('notification-added', notification); |
|
1142 |
}
|
|
1143 |
||
1144 |
notification.connect('clicked', Lang.bind(this, this.open)); |
|
1145 |
notification.connect('destroy', Lang.bind(this, |
|
1146 |
function () { |
|
1147 |
let index = this.notifications.indexOf(notification); |
|
1148 |
if (index < 0) |
|
1149 |
return; |
|
1150 |
||
1151 |
this.notifications.splice(index, 1); |
|
1152 |
if (this.notifications.length == 0) |
|
1153 |
this._lastNotificationRemoved(); |
|
1154 |
||
1155 |
this.countUpdated(); |
|
1156 |
}));
|
|
1157 |
||
1158 |
this.countUpdated(); |
|
1159 |
},
|
|
1160 |
||
1161 |
notify: function(notification) { |
|
1162 |
notification.acknowledged = false; |
|
1163 |
this.pushNotification(notification); |
|
1164 |
if (!this.isMuted) |
|
1165 |
this.emit('notify', notification); |
|
1166 |
},
|
|
1167 |
||
1168 |
destroy: function(reason) { |
|
1169 |
this.emit('destroy', reason); |
|
1170 |
},
|
|
1171 |
||
1172 |
// A subclass can redefine this to "steal" clicks from the
|
|
1173 |
// summaryitem; Use Clutter.get_current_event() to get the
|
|
1174 |
// details, return true to prevent the default handling from
|
|
1175 |
// ocurring.
|
|
1176 |
handleSummaryClick: function() { |
|
1177 |
return false; |
|
1178 |
},
|
|
1179 |
||
1180 |
iconUpdated: function() { |
|
1181 |
this.emit('icon-updated'); |
|
1182 |
},
|
|
1183 |
||
1184 |
//// Protected methods ////
|
|
1185 |
_setSummaryIcon: function(icon) { |
|
1186 |
this._ensureMainIcon(); |
|
1187 |
this._mainIcon.setIcon(icon); |
|
1188 |
this.iconUpdated(); |
|
1189 |
},
|
|
1190 |
||
1191 |
open: function(notification) { |
|
1192 |
this.emit('opened', notification); |
|
1193 |
},
|
|
1194 |
||
1195 |
destroyNonResidentNotifications: function() { |
|
1196 |
for (let i = this.notifications.length - 1; i >= 0; i--) |
|
1197 |
if (!this.notifications[i].resident) |
|
1198 |
this.notifications[i].destroy(); |
|
1199 |
||
1200 |
this.countUpdated(); |
|
1201 |
},
|
|
1202 |
||
1203 |
// Default implementation is to destroy this source, but subclasses can override
|
|
1204 |
_lastNotificationRemoved: function() { |
|
1205 |
this.destroy(); |
|
1206 |
},
|
|
1207 |
||
1208 |
hasResidentNotification: function() { |
|
1209 |
return this.notifications.some(function(n) { return n.resident; }); |
|
1210 |
}
|
|
1211 |
});
|
|
1212 |
Signals.addSignalMethods(Source.prototype); |
|
1213 |
||
1214 |
const SummaryItem = new Lang.Class({ |
|
1215 |
Name: 'SummaryItem', |
|
1216 |
||
1217 |
_init: function(source) { |
|
1218 |
this.source = source; |
|
1219 |
this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource)); |
|
1220 |
||
1221 |
this.actor = new St.Button({ style_class: 'summary-source-button', |
|
1222 |
y_fill: true, |
|
1223 |
reactive: true, |
|
1224 |
button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO | St.ButtonMask.THREE, |
|
1225 |
can_focus: true, |
|
1226 |
track_hover: true }); |
|
1227 |
this.actor.label_actor = new St.Label({ text: source.title }); |
|
1228 |
this.actor.connect('key-press-event', Lang.bind(this, this._onKeyPress)); |
|
1229 |
this._sourceBox = new St.BoxLayout({ style_class: 'summary-source' }); |
|
1230 |
||
1231 |
this._sourceIcon = source.getSummaryIcon(); |
|
1232 |
this._sourceBox.add(this._sourceIcon, { y_fill: false }); |
|
1233 |
this.actor.child = this._sourceBox; |
|
1234 |
||
1235 |
this.notificationStackWidget = new St.Widget({ layout_manager: new Clutter.BinLayout() }); |
|
1236 |
||
1237 |
this.notificationStackView = new St.ScrollView({ style_class: source.isChat ? '' : 'summary-notification-stack-scrollview', |
|
1238 |
vscrollbar_policy: source.isChat ? Gtk.PolicyType.NEVER : Gtk.PolicyType.AUTOMATIC, |
|
1239 |
hscrollbar_policy: Gtk.PolicyType.NEVER }); |
|
1240 |
this.notificationStackView.add_style_class_name('vfade'); |
|
1241 |
this.notificationStack = new St.BoxLayout({ style_class: 'summary-notification-stack', |
|
1242 |
vertical: true }); |
|
1243 |
this.notificationStackView.add_actor(this.notificationStack); |
|
1244 |
this.notificationStackWidget.add_actor(this.notificationStackView); |
|
1245 |
||
1246 |
this.closeButton = makeCloseButton(); |
|
1247 |
this.notificationStackWidget.add_actor(this.closeButton); |
|
1248 |
this._stackedNotifications = []; |
|
1249 |
||
1250 |
this._oldMaxScrollAdjustment = 0; |
|
1251 |
||
1252 |
this.notificationStackView.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) { |
|
1253 |
let currentValue = adjustment.value + adjustment.page_size; |
|
1254 |
if (currentValue == this._oldMaxScrollAdjustment) |
|
1255 |
this.scrollTo(St.Side.BOTTOM); |
|
1256 |
this._oldMaxScrollAdjustment = adjustment.upper; |
|
1257 |
}));
|
|
1258 |
||
1259 |
this.rightClickMenu = source.buildRightClickMenu(); |
|
1260 |
if (this.rightClickMenu) |
|
1261 |
global.focus_manager.add_group(this.rightClickMenu); |
|
1262 |
},
|
|
1263 |
||
1264 |
_onKeyPress: function(actor, event) { |
|
1265 |
if (event.get_key_symbol() == Clutter.KEY_Up) { |
|
1266 |
actor.emit('clicked', 1); |
|
1267 |
return true; |
|
1268 |
}
|
|
1269 |
return false; |
|
1270 |
},
|
|
1271 |
||
1272 |
prepareNotificationStackForShowing: function() { |
|
1273 |
if (this.notificationStack.get_n_children() > 0) |
|
1274 |
return; |
|
1275 |
||
1276 |
for (let i = 0; i < this.source.notifications.length; i++) { |
|
1277 |
this._appendNotificationToStack(this.source.notifications[i]); |
|
1278 |
}
|
|
1279 |
||
1280 |
this.scrollTo(St.Side.BOTTOM); |
|
1281 |
},
|
|
1282 |
||
1283 |
doneShowingNotificationStack: function() { |
|
1284 |
for (let i = 0; i < this._stackedNotifications.length; i++) { |
|
1285 |
let stackedNotification = this._stackedNotifications[i]; |
|
1286 |
let notification = stackedNotification.notification; |
|
1287 |
notification.collapseCompleted(); |
|
1288 |
notification.disconnect(stackedNotification.notificationExpandedId); |
|
1289 |
notification.disconnect(stackedNotification.notificationDoneDisplayingId); |
|
1290 |
notification.disconnect(stackedNotification.notificationDestroyedId); |
|
1291 |
if (notification.actor.get_parent() == this.notificationStack) |
|
1292 |
this.notificationStack.remove_actor(notification.actor); |
|
1293 |
notification.setIconVisible(true); |
|
1294 |
notification.enableScrolling(true); |
|
1295 |
}
|
|
1296 |
this._stackedNotifications = []; |
|
1297 |
},
|
|
1298 |
||
1299 |
_notificationAddedToSource: function(source, notification) { |
|
1300 |
if (this.notificationStack.mapped) |
|
1301 |
this._appendNotificationToStack(notification); |
|
1302 |
},
|
|
1303 |
||
1304 |
_appendNotificationToStack: function(notification) { |
|
1305 |
let stackedNotification = {}; |
|
1306 |
stackedNotification.notification = notification; |
|
1307 |
stackedNotification.notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated)); |
|
1308 |
stackedNotification.notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying)); |
|
1309 |
stackedNotification.notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed)); |
|
1310 |
this._stackedNotifications.push(stackedNotification); |
|
1311 |
if (!this.source.isChat) |
|
1312 |
notification.enableScrolling(false); |
|
1313 |
if (this.notificationStack.get_n_children() > 0) |
|
1314 |
notification.setIconVisible(false); |
|
1315 |
this.notificationStack.add(notification.actor); |
|
1316 |
notification.expand(false); |
|
1317 |
},
|
|
1318 |
||
1319 |
// scrollTo:
|
|
1320 |
// @side: St.Side.TOP or St.Side.BOTTOM
|
|
1321 |
//
|
|
1322 |
// Scrolls the notifiction stack to the indicated edge
|
|
1323 |
scrollTo: function(side) { |
|
1324 |
let adjustment = this.notificationStackView.vscroll.adjustment; |
|
1325 |
if (side == St.Side.TOP) |
|
1326 |
adjustment.value = adjustment.lower; |
|
1327 |
else if (side == St.Side.BOTTOM) |
|
1328 |
adjustment.value = adjustment.upper; |
|
1329 |
},
|
|
1330 |
||
1331 |
_contentUpdated: function() { |
|
1332 |
this.emit('content-updated'); |
|
1333 |
},
|
|
1334 |
||
1335 |
_notificationDoneDisplaying: function() { |
|
1336 |
this.source.emit('done-displaying-content'); |
|
1337 |
},
|
|
1338 |
||
1339 |
_notificationDestroyed: function(notification) { |
|
1340 |
for (let i = 0; i < this._stackedNotifications.length; i++) { |
|
1341 |
if (this._stackedNotifications[i].notification == notification) { |
|
1342 |
let stackedNotification = this._stackedNotifications[i]; |
|
1343 |
notification.disconnect(stackedNotification.notificationExpandedId); |
|
1344 |
notification.disconnect(stackedNotification.notificationDoneDisplayingId); |
|
1345 |
notification.disconnect(stackedNotification.notificationDestroyedId); |
|
1346 |
this._stackedNotifications.splice(i, 1); |
|
1347 |
if (notification.actor.get_parent() == this.notificationStack) |
|
1348 |
this.notificationStack.remove_actor(notification.actor); |
|
1349 |
this._contentUpdated(); |
|
1350 |
break; |
|
1351 |
}
|
|
1352 |
}
|
|
1353 |
||
1354 |
let firstNotification = this._stackedNotifications[0]; |
|
1355 |
if (firstNotification) |
|
1356 |
firstNotification.notification.setIconVisible(true); |
|
1357 |
}
|
|
1358 |
});
|
|
1359 |
Signals.addSignalMethods(SummaryItem.prototype); |
|
1360 |
||
1361 |
const MessageTray = new Lang.Class({ |
|
1362 |
Name: 'MessageTray', |
|
1363 |
||
1364 |
_init: function() { |
|
1365 |
this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) { |
|
1366 |
this._onStatusChanged(proxy.status); |
|
1367 |
}));
|
|
1368 |
this._busy = false; |
|
1369 |
this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) { |
|
1370 |
this._onStatusChanged(status); |
|
1371 |
}));
|
|
1372 |
||
1373 |
this.actor = new St.Widget({ name: 'message-tray', |
|
1374 |
reactive: true, |
|
1375 |
track_hover: true, |
|
1376 |
layout_manager: new Clutter.BinLayout(), |
|
1377 |
x_expand: true, |
|
1378 |
y_expand: true, |
|
1379 |
y_align: Clutter.ActorAlign.START, |
|
1380 |
});
|
|
1381 |
this.actor.connect('notify::hover', Lang.bind(this, this._onTrayHoverChanged)); |
|
1382 |
||
1383 |
this._notificationWidget = new St.Widget({ name: 'notification-container', |
|
1384 |
y_align: Clutter.ActorAlign.START, |
|
1385 |
x_align: Clutter.ActorAlign.CENTER, |
|
1386 |
y_expand: true, |
|
1387 |
x_expand: true, |
|
1388 |
layout_manager: new Clutter.BinLayout() }); |
|
1389 |
this.actor.add_actor(this._notificationWidget); |
|
1390 |
||
1391 |
this._notificationBin = new St.Bin({ y_expand: true }); |
|
1392 |
this._notificationBin.set_y_align(Clutter.ActorAlign.START); |
|
1393 |
this._notificationWidget.add_actor(this._notificationBin); |
|
1394 |
this._notificationWidget.hide(); |
|
1395 |
this._notificationQueue = []; |
|
1396 |
this._notification = null; |
|
1397 |
this._notificationClickedId = 0; |
|
1398 |
||
1399 |
this.actor.connect('button-release-event', Lang.bind(this, function(actor, event) { |
|
1400 |
this._setClickedSummaryItem(null); |
|
1401 |
this._updateState(); |
|
1402 |
actor.grab_key_focus(); |
|
1403 |
}));
|
|
1404 |
global.focus_manager.add_group(this.actor); |
|
1405 |
this._summary = new St.BoxLayout({ name: 'summary-mode', |
|
1406 |
reactive: true, |
|
1407 |
track_hover: true, |
|
1408 |
x_align: Clutter.ActorAlign.END, |
|
1409 |
x_expand: true, |
|
1410 |
y_align: Clutter.ActorAlign.CENTER, |
|
1411 |
y_expand: true }); |
|
1412 |
this._summary.connect('notify::hover', Lang.bind(this, this._onSummaryHoverChanged)); |
|
1413 |
this.actor.add_actor(this._summary); |
|
1414 |
this._summary.opacity = 0; |
|
1415 |
||
1416 |
this._summaryMotionId = 0; |
|
1417 |
this._trayMotionId = 0; |
|
1418 |
||
1419 |
this._summaryBoxPointer = new BoxPointer.BoxPointer(St.Side.BOTTOM, |
|
1420 |
{ reactive: true, |
|
1421 |
track_hover: true }); |
|
1422 |
this._summaryBoxPointer.actor.connect('key-press-event', |
|
1423 |
Lang.bind(this, this._onSummaryBoxPointerKeyPress)); |
|
1424 |
this._summaryBoxPointer.actor.style_class = 'summary-boxpointer'; |
|
1425 |
this._summaryBoxPointer.actor.hide(); |
|
1426 |
Main.layoutManager.addChrome(this._summaryBoxPointer.actor); |
|
1427 |
||
1428 |
this._summaryBoxPointerItem = null; |
|
1429 |
this._summaryBoxPointerContentUpdatedId = 0; |
|
1430 |
this._summaryBoxPointerDoneDisplayingId = 0; |
|
1431 |
this._clickedSummaryItem = null; |
|
1432 |
this._clickedSummaryItemMouseButton = -1; |
|
1433 |
this._clickedSummaryItemAllocationChangedId = 0; |
|
1434 |
this._pointerBarrier = 0; |
|
1435 |
||
1436 |
this._closeButton = makeCloseButton(); |
|
1437 |
this._closeButton.hide(); |
|
1438 |
this._closeButton.connect('clicked', Lang.bind(this, this._onCloseClicked)); |
|
1439 |
this._notificationWidget.add_actor(this._closeButton); |
|
1440 |
||
1441 |
this._idleMonitorWatchId = 0; |
|
1442 |
this._userActiveWhileNotificationShown = false; |
|
1443 |
||
1444 |
this.idleMonitor = Shell.IdleMonitor.get(); |
|
1445 |
||
1446 |
this._grabHelper = new GrabHelper.GrabHelper(this.actor); |
|
1447 |
this._grabHelper.addActor(this._summaryBoxPointer.actor); |
|
1448 |
this._grabHelper.addActor(this.actor); |
|
1449 |
if (Main.panel.statusArea.activities) |
|
1450 |
this._grabHelper.addActor(Main.panel.statusArea.activities.hotCorner.actor); |
|
1451 |
||
1452 |
Main.layoutManager.keyboardBox.connect('notify::hover', Lang.bind(this, this._onKeyboardHoverChanged)); |
|
1453 |
Main.layoutManager.connect('keyboard-visible-changed', Lang.bind(this, this._onKeyboardVisibleChanged)); |
|
1454 |
||
1455 |
this._trayState = State.HIDDEN; |
|
1456 |
this._locked = false; |
|
1457 |
this._traySummoned = false; |
|
1458 |
this._useLongerTrayLeftTimeout = false; |
|
1459 |
this._trayLeftTimeoutId = 0; |
|
1460 |
this._pointerInTray = false; |
|
1461 |
this._pointerInKeyboard = false; |
|
1462 |
this._keyboardVisible = false; |
|
1463 |
this._summaryState = State.HIDDEN; |
|
1464 |
this._pointerInSummary = false; |
|
1465 |
this._notificationClosed = false; |
|
1466 |
this._notificationState = State.HIDDEN; |
|
1467 |
this._notificationTimeoutId = 0; |
|
1468 |
this._notificationExpandedId = 0; |
|
1469 |
this._summaryBoxPointerState = State.HIDDEN; |
|
1470 |
this._summaryBoxPointerTimeoutId = 0; |
|
1471 |
this._desktopCloneState = State.HIDDEN; |
|
1472 |
this._overviewVisible = Main.overview.visible; |
|
1473 |
this._notificationRemoved = false; |
|
1474 |
this._reNotifyAfterHideNotification = null; |
|
1475 |
this._inFullscreen = false; |
|
1476 |
this._desktopClone = null; |
|
1477 |
||
1478 |
this._lightbox = new Lightbox.Lightbox(global.window_group, |
|
1479 |
{ inhibitEvents: true, |
|
1480 |
fadeInTime: ANIMATION_TIME, |
|
1481 |
fadeOutTime: ANIMATION_TIME, |
|
1482 |
fadeFactor: 0.2 |
|
1483 |
});
|
|
1484 |
||
1485 |
Main.layoutManager.trayBox.add_actor(this.actor); |
|
1486 |
this.actor.y = 0; |
|
1487 |
Main.layoutManager.trackChrome(this.actor); |
|
1488 |
Main.layoutManager.trackChrome(this._notificationWidget); |
|
1489 |
Main.layoutManager.trackChrome(this._closeButton); |
|
1490 |
||
1491 |
Main.layoutManager.connect('primary-fullscreen-changed', Lang.bind(this, this._onFullscreenChanged)); |
|
1492 |
||
1493 |
Main.overview.connect('showing', Lang.bind(this, |
|
1494 |
function() { |
|
1495 |
this._overviewVisible = true; |
|
1496 |
this._grabHelper.ungrab(); // drop modal grab if necessary |
|
1497 |
this.actor.add_style_pseudo_class('overview'); |
|
1498 |
this._updateState(); |
|
1499 |
}));
|
|
1500 |
Main.overview.connect('hiding', Lang.bind(this, |
|
1501 |
function() { |
|
1502 |
this._overviewVisible = false; |
|
1503 |
this._escapeTray(); |
|
1504 |
this.actor.remove_style_pseudo_class('overview'); |
|
1505 |
this._updateState(); |
|
1506 |
}));
|
|
1507 |
||
1508 |
Main.sessionMode.connect('updated', Lang.bind(this, this._sessionUpdated)); |
|
1509 |
||
1510 |
global.display.add_keybinding('toggle-message-tray', |
|
1511 |
new Gio.Settings({ schema: SHELL_KEYBINDINGS_SCHEMA }), |
|
1512 |
Meta.KeyBindingFlags.NONE, |
|
1513 |
Lang.bind(this, this.toggleAndNavigate)); |
|
1514 |
||
1515 |
this._summaryItems = []; |
|
1516 |
this._chatSummaryItemsCount = 0; |
|
1517 |
||
1518 |
let pointerWatcher = PointerWatcher.getPointerWatcher(); |
|
1519 |
pointerWatcher.addWatch(TRAY_DWELL_CHECK_INTERVAL, Lang.bind(this, this._checkTrayDwell)); |
|
1520 |
this._trayDwellTimeoutId = 0; |
|
1521 |
this._trayDwelling = false; |
|
1522 |
this._trayDwellUserTime = 0; |
|
1523 |
||
1524 |
this._sessionUpdated(); |
|
1525 |
},
|
|
1526 |
||
1527 |
_sessionUpdated: function() { |
|
1528 |
if (Main.sessionMode.isLocked || Main.sessionMode.isGreeter) |
|
1529 |
Main.ctrlAltTabManager.removeGroup(this._summary); |
|
1530 |
else
|
|
1531 |
Main.ctrlAltTabManager.addGroup(this._summary, _("Message Tray"), 'start-here-symbolic', |
|
1532 |
{ focusCallback: Lang.bind(this, this.toggleAndNavigate), |
|
1533 |
sortGroup: CtrlAltTab.SortGroup.BOTTOM }); |
|
1534 |
this._updateState(); |
|
1535 |
},
|
|
1536 |
||
1537 |
_checkTrayDwell: function(x, y) { |
|
1538 |
let monitor = Main.layoutManager.bottomMonitor; |
|
1539 |
let shouldDwell = (x >= monitor.x && x <= monitor.x + monitor.width && |
|
1540 |
y == monitor.y + monitor.height - 1); |
|
1541 |
if (shouldDwell) { |
|
1542 |
// We only set up dwell timeout when the user is not hovering over the tray
|
|
1543 |
// (!this.actor.hover). This avoids bringing up the message tray after the
|
|
1544 |
// user clicks on a notification with the pointer on the bottom pixel
|
|
1545 |
// of the monitor. The _trayDwelling variable is used so that we only try to
|
|
1546 |
// fire off one tray dwell - if it fails (because, say, the user has the mouse down),
|
|
1547 |
// we don't try again until the user moves the mouse up and down again.
|
|
1548 |
if (!this._trayDwelling && !this.actor.hover && this._trayDwellTimeoutId == 0) { |
|
1549 |
// Save the interaction timestamp so we can detect user input
|
|
1550 |
let focusWindow = global.display.focus_window; |
|
1551 |
this._trayDwellUserTime = focusWindow ? focusWindow.user_time : 0; |
|
1552 |
||
1553 |
this._trayDwellTimeoutId = Mainloop.timeout_add(TRAY_DWELL_TIME, |
|
1554 |
Lang.bind(this, this._trayDwellTimeout)); |
|
1555 |
}
|
|
1556 |
this._trayDwelling = true; |
|
1557 |
} else { |
|
1558 |
this._cancelTrayDwell(); |
|
1559 |
this._trayDwelling = false; |
|
1560 |
}
|
|
1561 |
},
|
|
1562 |
||
1563 |
_cancelTrayDwell: function() { |
|
1564 |
if (this._trayDwellTimeoutId != 0) { |
|
1565 |
Mainloop.source_remove(this._trayDwellTimeoutId); |
|
1566 |
this._trayDwellTimeoutId = 0; |
|
1567 |
}
|
|
1568 |
},
|
|
1569 |
||
1570 |
_trayDwellTimeout: function() { |
|
1571 |
if (Main.modalCount > 0) |
|
1572 |
return false; |
|
1573 |
||
1574 |
// If the user interacted with the focus window since we started the tray
|
|
1575 |
// dwell (by clicking or typing), don't activate the message tray
|
|
1576 |
let focusWindow = global.display.focus_window; |
|
1577 |
let currentUserTime = focusWindow ? focusWindow.user_time : 0; |
|
1578 |
if (currentUserTime != this._trayDwellUserTime) |
|
1579 |
return false; |
|
1580 |
||
1581 |
this._trayDwellTimeoutId = 0; |
|
1582 |
||
1583 |
this._traySummoned = true; |
|
1584 |
this._updateState(); |
|
1585 |
||
1586 |
return false; |
|
1587 |
},
|
|
1588 |
||
1589 |
_onCloseClicked: function() { |
|
1590 |
if (this._notificationState == State.SHOWN) { |
|
1591 |
this._closeButton.hide(); |
|
1592 |
this._notificationClosed = true; |
|
1593 |
this._updateState(); |
|
1594 |
this._notificationClosed = false; |
|
1595 |
}
|
|
1596 |
},
|
|
1597 |
||
1598 |
contains: function(source) { |
|
1599 |
return this._getIndexOfSummaryItemForSource(source) >= 0; |
|
1600 |
},
|
|
1601 |
||
1602 |
_getIndexOfSummaryItemForSource: function(source) { |
|
1603 |
for (let i = 0; i < this._summaryItems.length; i++) { |
|
1604 |
if (this._summaryItems[i].source == source) |
|
1605 |
return i; |
|
1606 |
}
|
|
1607 |
return -1; |
|
1608 |
},
|
|
1609 |
||
1610 |
add: function(source) { |
|
1611 |
if (this.contains(source)) { |
|
1612 |
log('Trying to re-add source ' + source.title); |
|
1613 |
return; |
|
1614 |
}
|
|
1615 |
||
1616 |
let summaryItem = new SummaryItem(source); |
|
1617 |
||
1618 |
if (source.isChat) { |
|
1619 |
this._summary.insert_child_at_index(summaryItem.actor, 0); |
|
1620 |
this._chatSummaryItemsCount++; |
|
1621 |
} else { |
|
1622 |
this._summary.insert_child_at_index(summaryItem.actor, this._chatSummaryItemsCount); |
|
1623 |
}
|
|
1624 |
||
1625 |
this._summaryItems.push(summaryItem); |
|
1626 |
||
1627 |
source.connect('notify', Lang.bind(this, this._onNotify)); |
|
1628 |
||
1629 |
source.connect('muted-changed', Lang.bind(this, |
|
1630 |
function () { |
|
1631 |
if (source.isMuted) |
|
1632 |
this._notificationQueue = this._notificationQueue.filter(function(notification) { |
|
1633 |
return source != notification.source; |
|
1634 |
});
|
|
1635 |
}));
|
|
1636 |
||
1637 |
summaryItem.actor.connect('clicked', Lang.bind(this, |
|
1638 |
function(actor, button) { |
|
1639 |
actor.grab_key_focus(); |
|
1640 |
this._onSummaryItemClicked(summaryItem, button); |
|
1641 |
}));
|
|
1642 |
summaryItem.actor.connect('popup-menu', Lang.bind(this, |
|
1643 |
function(actor, button) { |
|
1644 |
actor.grab_key_focus(); |
|
1645 |
this._onSummaryItemClicked(summaryItem, 3); |
|
1646 |
}));
|
|
1647 |
||
1648 |
source.connect('destroy', Lang.bind(this, this._onSourceDestroy)); |
|
1649 |
||
1650 |
// We need to display the newly-added summary item, but if the
|
|
1651 |
// caller is about to post a notification, we want to show that
|
|
1652 |
// *first* and not show the summary item until after it hides.
|
|
1653 |
// So postpone calling _updateState() a tiny bit.
|
|
1654 |
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { this._updateState(); return false; })); |
|
1655 |
||
1656 |
this.emit('summary-item-added', summaryItem); |
|
1657 |
},
|
|
1658 |
||
1659 |
getSummaryItems: function() { |
|
1660 |
return this._summaryItems; |
|
1661 |
},
|
|
1662 |
||
1663 |
_onSourceDestroy: function(source) { |
|
1664 |
let index = this._getIndexOfSummaryItemForSource(source); |
|
1665 |
if (index == -1) |
|
1666 |
return; |
|
1667 |
||
1668 |
let summaryItemToRemove = this._summaryItems[index]; |
|
1669 |
||
1670 |
this._summaryItems.splice(index, 1); |
|
1671 |
||
1672 |
if (source.isChat) |
|
1673 |
this._chatSummaryItemsCount--; |
|
1674 |
||
1675 |
let needUpdate = false; |
|
1676 |
||
1677 |
if (this._notification && this._notification.source == source) { |
|
1678 |
this._updateNotificationTimeout(0); |
|
1679 |
this._notificationRemoved = true; |
|
1680 |
needUpdate = true; |
|
1681 |
}
|
|
1682 |
if (this._clickedSummaryItem == summaryItemToRemove) { |
|
1683 |
this._setClickedSummaryItem(null); |
|
1684 |
needUpdate = true; |
|
1685 |
}
|
|
1686 |
||
1687 |
summaryItemToRemove.actor.destroy(); |
|
1688 |
||
1689 |
if (needUpdate) |
|
1690 |
this._updateState(); |
|
1691 |
},
|
|
1692 |
||
1693 |
_onNotificationDestroy: function(notification) { |
|
1694 |
if (this._notification == notification && (this._notificationState == State.SHOWN || this._notificationState == State.SHOWING)) { |
|
1695 |
this._updateNotificationTimeout(0); |
|
1696 |
this._notificationRemoved = true; |
|
1697 |
this._updateState(); |
|
1698 |
return; |
|
1699 |
}
|
|
1700 |
||
1701 |
let index = this._notificationQueue.indexOf(notification); |
|
1702 |
notification.destroy(); |
|
1703 |
if (index != -1) |
|
1704 |
this._notificationQueue.splice(index, 1); |
|
1705 |
},
|
|
1706 |
||
1707 |
_lock: function() { |
|
1708 |
this._locked = true; |
|
1709 |
},
|
|
1710 |
||
1711 |
_unlock: function() { |
|
1712 |
if (!this._locked) |
|
1713 |
return; |
|
1714 |
this._locked = false; |
|
1715 |
this._pointerInTray = this.actor.hover; |
|
1716 |
this._updateState(); |
|
1717 |
},
|
|
1718 |
||
1719 |
toggle: function() { |
|
1720 |
this._traySummoned = !this._traySummoned; |
|
1721 |
this._updateState(); |
|
1722 |
},
|
|
1723 |
||
1724 |
toggleAndNavigate: function() { |
|
1725 |
this.toggle(); |
|
1726 |
this._summary.navigate_focus(null, Gtk.DirectionType.TAB_FORWARD, false); |
|
1727 |
},
|
|
1728 |
||
1729 |
hide: function() { |
|
1730 |
this._traySummoned = false; |
|
1731 |
this.actor.set_hover(false); |
|
1732 |
this._summary.set_hover(false); |
|
1733 |
this._updateState(); |
|
1734 |
},
|
|
1735 |
||
1736 |
_onNotify: function(source, notification) { |
|
1737 |
if (this._summaryBoxPointerItem && this._summaryBoxPointerItem.source == source) { |
|
1738 |
if (this._summaryBoxPointerState == State.HIDING) { |
|
1739 |
// We are in the process of hiding the summary box pointer.
|
|
1740 |
// If there is an update for one of the notifications or
|
|
1741 |
// a new notification to be added to the notification stack
|
|
1742 |
// while it is in the process of being hidden, we show it as
|
|
1743 |
// a new notification. However, we first wait till the hide
|
|
1744 |
// is complete. This is especially important if one of the
|
|
1745 |
// notifications in the stack was updated because we will
|
|
1746 |
// need to be able to re-parent its actor to a different
|
|
1747 |
// part of the stage.
|
|
1748 |
this._reNotifyAfterHideNotification = notification; |
|
1749 |
} else { |
|
1750 |
// The summary box pointer is showing or shown (otherwise,
|
|
1751 |
// this._summaryBoxPointerItem would be null)
|
|
1752 |
// Immediately mark the notification as acknowledged, as it's
|
|
1753 |
// not going into the queue
|
|
1754 |
notification.acknowledged = true; |
|
1755 |
}
|
|
1756 |
||
1757 |
return; |
|
1758 |
}
|
|
1759 |
||
1760 |
if (this._notification == notification) { |
|
1761 |
// If a notification that is being shown is updated, we update
|
|
1762 |
// how it is shown and extend the time until it auto-hides.
|
|
1763 |
// If a new notification is updated while it is being hidden,
|
|
1764 |
// we stop hiding it and show it again.
|
|
1765 |
this._updateShowingNotification(); |
|
1766 |
} else if (this._notificationQueue.indexOf(notification) < 0) { |
|
1767 |
notification.connect('destroy', |
|
1768 |
Lang.bind(this, this._onNotificationDestroy)); |
|
1769 |
this._notificationQueue.push(notification); |
|
1770 |
this._notificationQueue.sort(function(notification1, notification2) { |
|
1771 |
return (notification2.urgency - notification1.urgency); |
|
1772 |
});
|
|
1773 |
}
|
|
1774 |
this._updateState(); |
|
1775 |
},
|
|
1776 |
||
1777 |
_onSummaryItemClicked: function(summaryItem, button) { |
|
1778 |
if (summaryItem.source.handleSummaryClick()) { |
|
1779 |
if (summaryItem.source.keepTrayOnSummaryClick) |
|
1780 |
this._setClickedSummaryItem(null); |
|
1781 |
else
|
|
1782 |
this._escapeTray(); |
|
1783 |
} else { |
|
1784 |
if (!this._setClickedSummaryItem(summaryItem, button)) |
|
1785 |
this._setClickedSummaryItem(null); |
|
1786 |
}
|
|
1787 |
||
1788 |
this._updateState(); |
|
1789 |
},
|
|
1790 |
||
1791 |
_onSummaryHoverChanged: function() { |
|
1792 |
this._pointerInSummary = this._summary.hover; |
|
1793 |
this._updateState(); |
|
1794 |
},
|
|
1795 |
||
1796 |
_onTrayHoverChanged: function() { |
|
1797 |
if (this.actor.hover) { |
|
1798 |
// No dwell inside notifications at the bottom of the screen
|
|
1799 |
this._cancelTrayDwell(); |
|
1800 |
||
1801 |
// Don't do anything if the one pixel area at the bottom is hovered over while the tray is hidden.
|
|
1802 |
if (this._trayState == State.HIDDEN && this._notificationState == State.HIDDEN) |
|
1803 |
return; |
|
1804 |
||
1805 |
// Don't do anything if this._useLongerTrayLeftTimeout is true, meaning the notification originally
|
|
1806 |
// popped up under the pointer, but this._trayLeftTimeoutId is 0, meaning the pointer didn't leave
|
|
1807 |
// the tray yet. We need to check for this case because sometimes _onTrayHoverChanged() gets called
|
|
1808 |
// multiple times while this.actor.hover is true.
|
|
1809 |
if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId) |
|
1810 |
return; |
|
1811 |
||
1812 |
this._useLongerTrayLeftTimeout = false; |
|
1813 |
if (this._trayLeftTimeoutId) { |
|
1814 |
Mainloop.source_remove(this._trayLeftTimeoutId); |
|
1815 |
this._trayLeftTimeoutId = 0; |
|
1816 |
this._trayLeftMouseX = -1; |
|
1817 |
this._trayLeftMouseY = -1; |
|
1818 |
return; |
|
1819 |
}
|
|
1820 |
||
1821 |
if (this._showNotificationMouseX >= 0) { |
|
1822 |
let actorAtShowNotificationPosition = |
|
1823 |
global.stage.get_actor_at_pos(Clutter.PickMode.ALL, this._showNotificationMouseX, this._showNotificationMouseY); |
|
1824 |
this._showNotificationMouseX = -1; |
|
1825 |
this._showNotificationMouseY = -1; |
|
1826 |
// Don't set this._pointerInTray to true if the pointer was initially in the area where the notification
|
|
1827 |
// popped up. That way we will not be expanding notifications that happen to pop up over the pointer
|
|
1828 |
// automatically. Instead, the user is able to expand the notification by mousing away from it and then
|
|
1829 |
// mousing back in. Because this is an expected action, we set the boolean flag that indicates that a longer
|
|
1830 |
// timeout should be used before popping down the notification.
|
|
1831 |
if (this.actor.contains(actorAtShowNotificationPosition)) { |
|
1832 |
this._useLongerTrayLeftTimeout = true; |
|
1833 |
return; |
|
1834 |
}
|
|
1835 |
}
|
|
1836 |
this._pointerInTray = true; |
|
1837 |
this._updateState(); |
|
1838 |
} else { |
|
1839 |
// We record the position of the mouse the moment it leaves the tray. These coordinates are used in
|
|
1840 |
// this._onTrayLeftTimeout() to determine if the mouse has moved far enough during the initial timeout for us
|
|
1841 |
// to consider that the user intended to leave the tray and therefore hide the tray. If the mouse is still
|
|
1842 |
// close to its previous position, we extend the timeout once.
|
|
1843 |
let [x, y, mods] = global.get_pointer(); |
|
1844 |
this._trayLeftMouseX = x; |
|
1845 |
this._trayLeftMouseY = y; |
|
1846 |
||
1847 |
// We wait just a little before hiding the message tray in case the user quickly moves the mouse back into it.
|
|
1848 |
// We wait for a longer period if the notification popped up where the mouse pointer was already positioned.
|
|
1849 |
// That gives the user more time to mouse away from the notification and mouse back in in order to expand it.
|
|
1850 |
let timeout = this._useLongerTrayLeftTimeout ? LONGER_HIDE_TIMEOUT * 1000 : HIDE_TIMEOUT * 1000; |
|
1851 |
this._trayLeftTimeoutId = Mainloop.timeout_add(timeout, Lang.bind(this, this._onTrayLeftTimeout)); |
|
1852 |
}
|
|
1853 |
},
|
|
1854 |
||
1855 |
_onKeyboardHoverChanged: function(keyboard) { |
|
1856 |
this._pointerInKeyboard = keyboard.hover; |
|
1857 |
||
1858 |
if (!keyboard.hover) { |
|
1859 |
let event = Clutter.get_current_event(); |
|
1860 |
if (event && event.type() == Clutter.EventType.LEAVE) { |
|
1861 |
let into = event.get_related(); |
|
1862 |
if (into && this.actor.contains(into)) { |
|
1863 |
// Don't call _updateState, because pointerInTray is
|
|
1864 |
// still false
|
|
1865 |
return; |
|
1866 |
}
|
|
1867 |
}
|
|
1868 |
}
|
|
1869 |
||
1870 |
this._updateState(); |
|
1871 |
},
|
|
1872 |
||
1873 |
_onKeyboardVisibleChanged: function(layoutManager, keyboardVisible) { |
|
1874 |
if (this._keyboardVisible == keyboardVisible) |
|
1875 |
return; |
|
1876 |
||
1877 |
this._keyboardVisible = keyboardVisible; |
|
1878 |
||
1879 |
if (keyboardVisible) |
|
1880 |
this.actor.add_style_pseudo_class('keyboard'); |
|
1881 |
else
|
|
1882 |
this.actor.remove_style_pseudo_class('keyboard'); |
|
1883 |
||
1884 |
this._updateState(); |
|
1885 |
},
|
|
1886 |
||
1887 |
_onFullscreenChanged: function(obj, state) { |
|
1888 |
this._inFullscreen = state; |
|
1889 |
this._updateState(); |
|
1890 |
},
|
|
1891 |
||
1892 |
_onStatusChanged: function(status) { |
|
1893 |
if (status == GnomeSession.PresenceStatus.BUSY) { |
|
1894 |
// remove notification and allow the summary to be closed now
|
|
1895 |
this._updateNotificationTimeout(0); |
|
1896 |
this._busy = true; |
|
1897 |
} else if (status != GnomeSession.PresenceStatus.IDLE) { |
|
1898 |
// We preserve the previous value of this._busy if the status turns to IDLE
|
|
1899 |
// so that we don't start showing notifications queued during the BUSY state
|
|
1900 |
// as the screensaver gets activated.
|
|
1901 |
this._busy = false; |
|
1902 |
}
|
|
1903 |
||
1904 |
this._updateState(); |
|
1905 |
},
|
|
1906 |
||
1907 |
_onTrayLeftTimeout: function() { |
|
1908 |
let [x, y, mods] = global.get_pointer(); |
|
1909 |
// We extend the timeout once if the mouse moved no further than MOUSE_LEFT_ACTOR_THRESHOLD to either side or up.
|
|
1910 |
// We don't check how far down the mouse moved because any point above the tray, but below the exit coordinate,
|
|
1911 |
// is close to the tray.
|
|
1912 |
if (this._trayLeftMouseX > -1 && |
|
1913 |
y > this._trayLeftMouseY - MOUSE_LEFT_ACTOR_THRESHOLD && |
|
1914 |
x < this._trayLeftMouseX + MOUSE_LEFT_ACTOR_THRESHOLD && |
|
1915 |
x > this._trayLeftMouseX - MOUSE_LEFT_ACTOR_THRESHOLD) { |
|
1916 |
this._trayLeftMouseX = -1; |
|
1917 |
this._trayLeftTimeoutId = Mainloop.timeout_add(LONGER_HIDE_TIMEOUT * 1000, |
|
1918 |
Lang.bind(this, this._onTrayLeftTimeout)); |
|
1919 |
} else { |
|
1920 |
this._trayLeftTimeoutId = 0; |
|
1921 |
this._useLongerTrayLeftTimeout = false; |
|
1922 |
this._pointerInTray = false; |
|
1923 |
this._pointerInSummary = false; |
|
1924 |
this._updateNotificationTimeout(0); |
|
1925 |
this._updateState(); |
|
1926 |
}
|
|
1927 |
return false; |
|
1928 |
},
|
|
1929 |
||
1930 |
_escapeTray: function() { |
|
1931 |
this._unlock(); |
|
1932 |
this._pointerInTray = false; |
|
1933 |
this._pointerInSummary = false; |
|
1934 |
this._traySummoned = false; |
|
1935 |
this._setClickedSummaryItem(null); |
|
1936 |
this._updateNotificationTimeout(0); |
|
1937 |
this._updateState(); |
|
1938 |
},
|
|
1939 |
||
1940 |
// All of the logic for what happens when occurs here; the various
|
|
1941 |
// event handlers merely update variables such as
|
|
1942 |
// 'this._pointerInTray', 'this._summaryState', etc, and
|
|
1943 |
// _updateState() figures out what (if anything) needs to be done
|
|
1944 |
// at the present time.
|
|
1945 |
_updateState: function() { |
|
1946 |
// Notifications
|
|
1947 |
let notificationQueue = this._notificationQueue; |
|
1948 |
let notificationUrgent = notificationQueue.length > 0 && notificationQueue[0].urgency == Urgency.CRITICAL; |
|
1949 |
let notificationsLimited = this._busy || this._inFullscreen; |
|
1950 |
let notificationsPending = notificationQueue.length > 0 && (!notificationsLimited || notificationUrgent) && Main.sessionMode.hasNotifications; |
|
1951 |
let nextNotification = notificationQueue.length > 0 ? notificationQueue[0] : null; |
|
1952 |
let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved; |
|
1953 |
let notificationExpanded = this._notification && this._notification.expanded; |
|
1954 |
let notificationExpired = this._notificationTimeoutId == 0 && |
|
1955 |
!(this._notification && this._notification.urgency == Urgency.CRITICAL) && |
|
1956 |
!(this._notification && this._notification.focused) && |
|
1957 |
!this._pointerInTray && |
|
1958 |
!this._locked && |
|
1959 |
!(this._pointerInKeyboard && notificationExpanded); |
|
1960 |
let notificationLockedOut = !Main.sessionMode.hasNotifications && this._notification; |
|
1961 |
let notificationMustClose = this._notificationRemoved || notificationLockedOut || (notificationExpired && this._userActiveWhileNotificationShown) || this._notificationClosed; |
|
1962 |
let canShowNotification = notificationsPending && this._summaryState == State.HIDDEN; |
|
1963 |
||
1964 |
if (this._notificationState == State.HIDDEN) { |
|
1965 |
if (canShowNotification) { |
|
1966 |
this._showNotification(); |
|
1967 |
}
|
|
1968 |
} else if (this._notificationState == State.SHOWN) { |
|
1969 |
if (notificationMustClose) |
|
1970 |
this._hideNotification(); |
|
1971 |
else if (notificationPinned && !notificationExpanded) |
|
1972 |
this._expandNotification(false); |
|
1973 |
else if (notificationPinned) |
|
1974 |
this._ensureNotificationFocused(); |
|
1975 |
}
|
|
1976 |
||
1977 |
// Summary
|
|
1978 |
let summarySummoned = this._pointerInSummary || this._overviewVisible || this._traySummoned; |
|
1979 |
let summaryPinned = this._pointerInTray || summarySummoned || this._locked; |
|
1980 |
let summaryHovered = this._pointerInTray || this._pointerInSummary; |
|
1981 |
||
1982 |
let notificationsVisible = (this._notificationState == State.SHOWING || |
|
1983 |
this._notificationState == State.SHOWN); |
|
1984 |
let notificationsDone = !notificationsVisible && !notificationsPending; |
|
1985 |
||
1986 |
let summaryOptionalInOverview = this._overviewVisible && !this._locked && !summaryHovered; |
|
1987 |
let mustHideSummary = (notificationsPending && (notificationUrgent || summaryOptionalInOverview)) |
|
1988 |
|| notificationsVisible || !Main.sessionMode.hasNotifications; |
|
1989 |
||
1990 |
if (this._summaryState == State.HIDDEN && !mustHideSummary && summarySummoned) |
|
1991 |
this._showSummary(); |
|
1992 |
else if (this._summaryState == State.SHOWN && (!summaryPinned || mustHideSummary)) |
|
1993 |
this._hideSummary(); |
|
1994 |
||
1995 |
// Summary notification
|
|
1996 |
let haveClickedSummaryItem = this._clickedSummaryItem != null; |
|
1997 |
let summarySourceIsMainNotificationSource = (haveClickedSummaryItem && this._notification && |
|
1998 |
this._clickedSummaryItem.source == this._notification.source); |
|
1999 |
let canShowSummaryBoxPointer = this._summaryState == State.SHOWN; |
|
2000 |
// We only have sources with empty notification stacks for legacy tray icons. Currently, we never attempt
|
|
2001 |
// to show notifications for legacy tray icons, but this would be necessary if we did.
|
|
2002 |
let requestedNotificationStackIsEmpty = (this._clickedSummaryItemMouseButton == 1 && this._clickedSummaryItem.source.notifications.length == 0); |
|
2003 |
let wrongSummaryNotificationStack = (this._clickedSummaryItemMouseButton == 1 && |
|
2004 |
this._summaryBoxPointer.bin.child != this._clickedSummaryItem.notificationStackWidget); |
|
2005 |
let wrongSummaryRightClickMenu = (this._clickedSummaryItemMouseButton == 3 && |
|
2006 |
this._summaryBoxPointer.bin.child != this._clickedSummaryItem.rightClickMenu); |
|
2007 |
let wrongSummaryBoxPointer = (haveClickedSummaryItem && |
|
2008 |
(wrongSummaryNotificationStack || wrongSummaryRightClickMenu)); |
|
2009 |
||
2010 |
if (this._summaryBoxPointerState == State.HIDDEN) { |
|
2011 |
if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer && !requestedNotificationStackIsEmpty) |
|
2012 |
this._showSummaryBoxPointer(); |
|
2013 |
} else if (this._summaryBoxPointerState == State.SHOWN) { |
|
2014 |
if (!haveClickedSummaryItem || !canShowSummaryBoxPointer || wrongSummaryBoxPointer || mustHideSummary) { |
|
2015 |
this._hideSummaryBoxPointer(); |
|
2016 |
if (wrongSummaryBoxPointer) |
|
2017 |
this._showSummaryBoxPointer(); |
|
2018 |
}
|
|
2019 |
}
|
|
2020 |
||
2021 |
// Tray itself
|
|
2022 |
let trayIsVisible = (this._trayState == State.SHOWING || |
|
2023 |
this._trayState == State.SHOWN); |
|
2024 |
let trayShouldBeVisible = (this._summaryState == State.SHOWING || |
|
2025 |
this._summaryState == State.SHOWN); |
|
2026 |
if (!trayIsVisible && trayShouldBeVisible) |
|
2027 |
trayShouldBeVisible = this._showTray(); |
|
2028 |
else if (trayIsVisible && !trayShouldBeVisible) |
|
2029 |
this._hideTray(); |
|
2030 |
||
2031 |
// Desktop clone
|
|
2032 |
let desktopCloneIsVisible = (this._desktopCloneState == State.SHOWING || |
|
2033 |
this._desktopCloneState == State.SHOWN); |
|
2034 |
let desktopCloneShouldBeVisible = (trayShouldBeVisible && |
|
2035 |
!this._overviewVisible && |
|
2036 |
!this._keyboardVisible); |
|
2037 |
||
2038 |
if (!desktopCloneIsVisible && desktopCloneShouldBeVisible) { |
|
2039 |
this._showDesktopClone(); |
|
2040 |
} else if (desktopCloneIsVisible && !desktopCloneShouldBeVisible) { |
|
2041 |
this._hideDesktopClone (this._keyboardVisible); |
|
2042 |
}
|
|
2043 |
},
|
|
2044 |
||
2045 |
_tween: function(actor, statevar, value, params) { |
|
2046 |
let onComplete = params.onComplete; |
|
2047 |
let onCompleteScope = params.onCompleteScope; |
|
2048 |
let onCompleteParams = params.onCompleteParams; |
|
2049 |
||
2050 |
params.onComplete = this._tweenComplete; |
|
2051 |
params.onCompleteScope = this; |
|
2052 |
params.onCompleteParams = [statevar, value, onComplete, onCompleteScope, onCompleteParams]; |
|
2053 |
||
2054 |
// Remove other tweens that could mess with the state machine
|
|
2055 |
Tweener.removeTweens(actor); |
|
2056 |
Tweener.addTween(actor, params); |
|
2057 |
||
2058 |
let valuing = (value == State.SHOWN) ? State.SHOWING : State.HIDING; |
|
2059 |
this[statevar] = valuing; |
|
2060 |
},
|
|
2061 |
||
2062 |
_tweenComplete: function(statevar, value, onComplete, onCompleteScope, onCompleteParams) { |
|
2063 |
this[statevar] = value; |
|
2064 |
if (onComplete) |
|
2065 |
onComplete.apply(onCompleteScope, onCompleteParams); |
|
2066 |
this._updateState(); |
|
2067 |
},
|
|
2068 |
||
2069 |
_showTray: function() { |
|
2070 |
// Don't actually take a modal grab in the overview.
|
|
2071 |
// Just add something to the grab stack that we can
|
|
2072 |
// pop later.
|
|
2073 |
let modal = !this._overviewVisible; |
|
2074 |
||
2075 |
if (!this._grabHelper.grab({ actor: this.actor, |
|
2076 |
modal: modal, |
|
2077 |
onUngrab: Lang.bind(this, this._escapeTray) })) { |
|
2078 |
this._traySummoned = false; |
|
2079 |
return false; |
|
2080 |
}
|
|
2081 |
||
2082 |
this._tween(this.actor, '_trayState', State.SHOWN, |
|
2083 |
{ y: -this.actor.height, |
|
2084 |
time: ANIMATION_TIME, |
|
2085 |
transition: 'easeOutQuad' |
|
2086 |
});
|
|
2087 |
||
2088 |
if (!this._overviewVisible) |
|
2089 |
this._lightbox.show(); |
|
2090 |
||
2091 |
return true; |
|
2092 |
},
|
|
2093 |
||
2094 |
_updateDesktopCloneClip: function() { |
|
2095 |
let geometry = this._bottomMonitorGeometry; |
|
2096 |
let progress = -Math.round(this._desktopClone.y); |
|
2097 |
this._desktopClone.set_clip(geometry.x, |
|
2098 |
geometry.y + progress, |
|
2099 |
geometry.width, |
|
2100 |
geometry.height - progress); |
|
2101 |
},
|
|
2102 |
||
2103 |
_showDesktopClone: function() { |
|
2104 |
let bottomMonitor = Main.layoutManager.bottomMonitor; |
|
2105 |
this._bottomMonitorGeometry = { x: bottomMonitor.x, |
|
2106 |
y: bottomMonitor.y, |
|
2107 |
width: bottomMonitor.width, |
|
2108 |
height: bottomMonitor.height }; |
|
2109 |
||
2110 |
if (this._desktopClone) |
|
2111 |
this._desktopClone.destroy(); |
|
2112 |
this._desktopClone = new Clutter.Clone({ source: global.window_group, clip: new Clutter.Geometry(this._bottomMonitorGeometry) }); |
|
2113 |
Main.uiGroup.insert_child_above(this._desktopClone, global.window_group); |
|
2114 |
this._desktopClone.x = 0; |
|
2115 |
this._desktopClone.y = 0; |
|
2116 |
this._desktopClone.show(); |
|
2117 |
||
2118 |
this._tween(this._desktopClone, '_desktopCloneState', State.SHOWN, |
|
2119 |
{ y: -this.actor.height, |
|
2120 |
time: ANIMATION_TIME, |
|
2121 |
transition: 'easeOutQuad', |
|
2122 |
onUpdate: Lang.bind(this, this._updateDesktopCloneClip) |
|
2123 |
});
|
|
2124 |
},
|
|
2125 |
||
2126 |
_hideTray: function() { |
|
2127 |
// Having the summary item animate out while sliding down the tray
|
|
2128 |
// is distracting, so hide it immediately in case it was visible.
|
|
2129 |
this._summaryBoxPointer.actor.hide(); |
|
2130 |
||
2131 |
this._tween(this.actor, '_trayState', State.HIDDEN, |
|
2132 |
{ y: 0, |
|
2133 |
time: ANIMATION_TIME, |
|
2134 |
transition: 'easeOutQuad' |
|
2135 |
});
|
|
2136 |
||
2137 |
// Note that we might have entered here without a grab,
|
|
2138 |
// which would happen if GrabHelper ungrabbed for us.
|
|
2139 |
// This is a no-op in that case.
|
|
2140 |
this._grabHelper.ungrab({ actor: this.actor }); |
|
2141 |
this._lightbox.hide(); |
|
2142 |
},
|
|
2143 |
||
2144 |
_hideDesktopClone: function(now) { |
|
2145 |
this._tween(this._desktopClone, '_desktopCloneState', State.HIDDEN, |
|
2146 |
{ y: 0, |
|
2147 |
time: now ? 0 : ANIMATION_TIME, |
|
2148 |
transition: 'easeOutQuad', |
|
2149 |
onComplete: Lang.bind(this, function() { |
|
2150 |
this._desktopClone.destroy(); |
|
2151 |
this._desktopClone = null; |
|
2152 |
this._bottomMonitorGeometry = null; |
|
2153 |
}),
|
|
2154 |
onUpdate: Lang.bind(this, this._updateDesktopCloneClip) |
|
2155 |
});
|
|
2156 |
},
|
|
2157 |
||
2158 |
_onIdleMonitorWatch: function(monitor, id, userBecameIdle) { |
|
2159 |
this.idleMonitor.remove_watch(this._idleMonitorWatchId); |
|
2160 |
this._idleMonitorWatchId = 0; |
|
2161 |
if (!userBecameIdle) |
|
2162 |
this._updateNotificationTimeout(2000); |
|
2163 |
this._userActiveWhileNotificationShown = true; |
|
2164 |
this._updateState(); |
|
2165 |
},
|
|
2166 |
||
2167 |
_showNotification: function() { |
|
2168 |
this._notification = this._notificationQueue.shift(); |
|
2169 |
this._userActiveWhileNotificationShown = this.idleMonitor.get_idletime() <= IDLE_TIME; |
|
2170 |
this._idleMonitorWatchId = this.idleMonitor.add_watch(IDLE_TIME, |
|
2171 |
Lang.bind(this, this._onIdleMonitorWatch)); |
|
2172 |
this._notificationClickedId = this._notification.connect('done-displaying', |
|
2173 |
Lang.bind(this, this._escapeTray)); |
|
2174 |
this._notification.connect('unfocused', Lang.bind(this, function() { |
|
2175 |
this._updateState(); |
|
2176 |
}));
|
|
2177 |
this._notificationBin.child = this._notification.actor; |
|
2178 |
||
2179 |
this._notificationWidget.opacity = 0; |
|
2180 |
this._notificationWidget.y = 0; |
|
2181 |
this._notificationWidget.show(); |
|
2182 |
||
2183 |
this._updateShowingNotification(); |
|
2184 |
||
2185 |
let [x, y, mods] = global.get_pointer(); |
|
2186 |
// We save the position of the mouse at the time when we started showing the notification
|
|
2187 |
// in order to determine if the notification popped up under it. We make that check if
|
|
2188 |
// the user starts moving the mouse and _onTrayHoverChanged() gets called. We don't
|
|
2189 |
// expand the notification if it just happened to pop up under the mouse unless the user
|
|
2190 |
// explicitly mouses away from it and then mouses back in.
|
|
2191 |
this._showNotificationMouseX = x; |
|
2192 |
this._showNotificationMouseY = y; |
|
2193 |
// We save the coordinates of the mouse at the time when we started showing the notification
|
|
2194 |
// and then we update it in _notificationTimeout(). We don't pop down the notification if
|
|
2195 |
// the mouse is moving towards it or within it.
|
|
2196 |
this._lastSeenMouseX = x; |
|
2197 |
this._lastSeenMouseY = y; |
|
2198 |
},
|
|
2199 |
||
2200 |
_updateShowingNotification: function() { |
|
2201 |
this._notification.acknowledged = true; |
|
2202 |
||
2203 |
// We auto-expand notifications with CRITICAL urgency.
|
|
2204 |
if (this._notification.urgency == Urgency.CRITICAL) |
|
2205 |
this._expandNotification(true); |
|
2206 |
||
2207 |
// We tween all notifications to full opacity. This ensures that both new notifications and
|
|
2208 |
// notifications that might have been in the process of hiding get full opacity.
|
|
2209 |
//
|
|
2210 |
// We tween any notification showing in the banner mode to the appropriate height
|
|
2211 |
// (which is banner height or expanded height, depending on the notification state)
|
|
2212 |
// This ensures that both new notifications and notifications in the banner mode that might
|
|
2213 |
// have been in the process of hiding are shown with the correct height.
|
|
2214 |
//
|
|
2215 |
// We use this._showNotificationCompleted() onComplete callback to extend the time the updated
|
|
2216 |
// notification is being shown.
|
|
2217 |
||
2218 |
let tweenParams = { opacity: 255, |
|
2219 |
y: -this._notificationWidget.height, |
|
2220 |
time: ANIMATION_TIME, |
|
2221 |
transition: 'easeOutQuad', |
|
2222 |
onComplete: this._showNotificationCompleted, |
|
2223 |
onCompleteScope: this |
|
2224 |
};
|
|
2225 |
||
2226 |
this._tween(this._notificationWidget, '_notificationState', State.SHOWN, tweenParams); |
|
2227 |
},
|
|
2228 |
||
2229 |
_showNotificationCompleted: function() { |
|
2230 |
if (this._notification.urgency != Urgency.CRITICAL) |
|
2231 |
this._updateNotificationTimeout(NOTIFICATION_TIMEOUT * 1000); |
|
2232 |
},
|
|
2233 |
||
2234 |
_updateNotificationTimeout: function(timeout) { |
|
2235 |
if (this._notificationTimeoutId) { |
|
2236 |
Mainloop.source_remove(this._notificationTimeoutId); |
|
2237 |
this._notificationTimeoutId = 0; |
|
2238 |
}
|
|
2239 |
if (timeout > 0) |
|
2240 |
this._notificationTimeoutId = |
|
2241 |
Mainloop.timeout_add(timeout, |
|
2242 |
Lang.bind(this, this._notificationTimeout)); |
|
2243 |
},
|
|
2244 |
||
2245 |
_notificationTimeout: function() { |
|
2246 |
let [x, y, mods] = global.get_pointer(); |
|
2247 |
if (y > this._lastSeenMouseY + 10 && !this.actor.hover) { |
|
2248 |
// The mouse is moving towards the notification, so don't
|
|
2249 |
// hide it yet. (We just create a new timeout (and destroy
|
|
2250 |
// the old one) each time because the bookkeeping is
|
|
2251 |
// simpler.)
|
|
2252 |
this._updateNotificationTimeout(1000); |
|
2253 |
} else if (this._useLongerTrayLeftTimeout && !this._trayLeftTimeoutId && |
|
2254 |
(x != this._lastSeenMouseX || y != this._lastSeenMouseY)) { |
|
2255 |
// Refresh the timeout if the notification originally
|
|
2256 |
// popped up under the pointer, and the pointer is hovering
|
|
2257 |
// inside it.
|
|
2258 |
this._updateNotificationTimeout(1000); |
|
2259 |
} else { |
|
2260 |
this._notificationTimeoutId = 0; |
|
2261 |
this._updateState(); |
|
2262 |
}
|
|
2263 |
||
2264 |
this._lastSeenMouseX = x; |
|
2265 |
this._lastSeenMouseY = y; |
|
2266 |
return false; |
|
2267 |
},
|
|
2268 |
||
2269 |
_hideNotification: function() { |
|
2270 |
this._grabHelper.ungrab({ actor: this._notification.actor }); |
|
2271 |
||
2272 |
if (this._notificationExpandedId) { |
|
2273 |
this._notification.disconnect(this._notificationExpandedId); |
|
2274 |
this._notificationExpandedId = 0; |
|
2275 |
}
|
|
2276 |
||
2277 |
if (this._notificationRemoved) { |
|
2278 |
this._notificationWidget.y = this.actor.height; |
|
2279 |
this._notificationWidget.opacity = 0; |
|
2280 |
this._notificationState = State.HIDDEN; |
|
2281 |
this._hideNotificationCompleted(); |
|
2282 |
} else { |
|
2283 |
this._tween(this._notificationWidget, '_notificationState', State.HIDDEN, |
|
2284 |
{ y: this.actor.height, |
|
2285 |
opacity: 0, |
|
2286 |
time: ANIMATION_TIME, |
|
2287 |
transition: 'easeOutQuad', |
|
2288 |
onComplete: this._hideNotificationCompleted, |
|
2289 |
onCompleteScope: this |
|
2290 |
});
|
|
2291 |
||
2292 |
}
|
|
2293 |
},
|
|
2294 |
||
2295 |
_hideNotificationCompleted: function() { |
|
2296 |
this._notificationRemoved = false; |
|
2297 |
this._notificationWidget.hide(); |
|
2298 |
this._closeButton.hide(); |
|
2299 |
this._pointerInTray = false; |
|
2300 |
this.actor.hover = false; // Clutter doesn't emit notify::hover when actors move |
|
2301 |
this._notificationBin.child = null; |
|
2302 |
this._notification.collapseCompleted(); |
|
2303 |
this._notification.disconnect(this._notificationClickedId); |
|
2304 |
this._notificationClickedId = 0; |
|
2305 |
let notification = this._notification; |
|
2306 |
this._notification = null; |
|
2307 |
if (notification.isTransient) |
|
2308 |
notification.destroy(NotificationDestroyedReason.EXPIRED); |
|
2309 |
},
|
|
2310 |
||
2311 |
_expandNotification: function(autoExpanding) { |
|
2312 |
// Don't grab focus in notifications that are auto-expanded.
|
|
2313 |
if (!autoExpanding) |
|
2314 |
this._grabHelper.grab({ actor: this._notification.actor, |
|
2315 |
grabFocus: true }); |
|
2316 |
||
2317 |
if (!this._notificationExpandedId) |
|
2318 |
this._notificationExpandedId = |
|
2319 |
this._notification.connect('expanded', |
|
2320 |
Lang.bind(this, this._onNotificationExpanded)); |
|
2321 |
// Don't animate changes in notifications that are auto-expanding.
|
|
2322 |
this._notification.expand(!autoExpanding); |
|
2323 |
},
|
|
2324 |
||
2325 |
_onNotificationExpanded: function() { |
|
2326 |
let expandedY = - this._notificationWidget.height; |
|
2327 |
this._closeButton.show(); |
|
2328 |
||
2329 |
// Don't animate the notification to its new position if it has shrunk:
|
|
2330 |
// there will be a very visible "gap" that breaks the illusion.
|
|
2331 |
if (this._notificationWidget.y < expandedY) { |
|
2332 |
this._notificationWidget.y = expandedY; |
|
2333 |
} else if (this._notification.y != expandedY) { |
|
2334 |
// Tween also opacity here, to override a possible tween that's
|
|
2335 |
// currently hiding the notification. This will ensure that the
|
|
2336 |
// notification is not removed when the onComplete handler for this
|
|
2337 |
// one triggers.
|
|
2338 |
this._tween(this._notificationWidget, '_notificationState', State.SHOWN, |
|
2339 |
{ y: expandedY, |
|
2340 |
opacity: 255, |
|
2341 |
time: ANIMATION_TIME, |
|
2342 |
transition: 'easeOutQuad' |
|
2343 |
});
|
|
2344 |
}
|
|
2345 |
},
|
|
2346 |
||
2347 |
// We use this function to grab focus when the user moves the pointer
|
|
2348 |
// to a notification with CRITICAL urgency that was already auto-expanded.
|
|
2349 |
_ensureNotificationFocused: function() { |
|
2350 |
this._grabHelper.grab({ actor: this._notification.actor, |
|
2351 |
grabFocus: true }); |
|
2352 |
},
|
|
2353 |
||
2354 |
_showSummary: function() { |
|
2355 |
this._summary.opacity = 0; |
|
2356 |
this._tween(this._summary, '_summaryState', State.SHOWN, |
|
2357 |
{ opacity: 255, |
|
2358 |
time: ANIMATION_TIME, |
|
2359 |
transition: 'easeOutQuad', |
|
2360 |
});
|
|
2361 |
},
|
|
2362 |
||
2363 |
_hideSummary: function() { |
|
2364 |
this._tween(this._summary, '_summaryState', State.HIDDEN, |
|
2365 |
{ opacity: 0, |
|
2366 |
time: ANIMATION_TIME, |
|
2367 |
transition: 'easeOutQuad', |
|
2368 |
});
|
|
2369 |
},
|
|
2370 |
||
2371 |
_showSummaryBoxPointer: function() { |
|
2372 |
this._summaryBoxPointerItem = this._clickedSummaryItem; |
|
2373 |
this._summaryBoxPointerContentUpdatedId = this._summaryBoxPointerItem.connect('content-updated', |
|
2374 |
Lang.bind(this, this._onSummaryBoxPointerContentUpdated)); |
|
2375 |
this._sourceDoneDisplayingId = this._summaryBoxPointerItem.source.connect('done-displaying-content', |
|
2376 |
Lang.bind(this, this._escapeTray)); |
|
2377 |
||
2378 |
let hasRightClickMenu = this._summaryBoxPointerItem.rightClickMenu != null; |
|
2379 |
if (this._clickedSummaryItemMouseButton == 1 || !hasRightClickMenu) { |
|
2380 |
let newQueue = []; |
|
2381 |
for (let i = 0; i < this._notificationQueue.length; i++) { |
|
2382 |
let notification = this._notificationQueue[i]; |
|
2383 |
let sameSource = this._summaryBoxPointerItem.source == notification.source; |
|
2384 |
if (sameSource) |
|
2385 |
notification.acknowledged = true; |
|
2386 |
else
|
|
2387 |
newQueue.push(notification); |
|
2388 |
}
|
|
2389 |
this._notificationQueue = newQueue; |
|
2390 |
||
2391 |
this._summaryBoxPointer.bin.child = this._summaryBoxPointerItem.notificationStackWidget; |
|
2392 |
||
2393 |
let closeButton = this._summaryBoxPointerItem.closeButton; |
|
2394 |
closeButton.show(); |
|
2395 |
this._summaryBoxPointerCloseClickedId = closeButton.connect('clicked', Lang.bind(this, this._hideSummaryBoxPointer)); |
|
2396 |
this._summaryBoxPointerItem.prepareNotificationStackForShowing(); |
|
2397 |
} else if (this._clickedSummaryItemMouseButton == 3) { |
|
2398 |
this._summaryBoxPointer.bin.child = this._clickedSummaryItem.rightClickMenu; |
|
2399 |
}
|
|
2400 |
||
2401 |
this._grabHelper.grab({ actor: this._summaryBoxPointer.bin.child, |
|
2402 |
grabFocus: true, |
|
2403 |
onUngrab: Lang.bind(this, this._onSummaryBoxPointerUngrabbed) }); |
|
2404 |
this._lock(); |
|
2405 |
||
2406 |
this._summaryBoxPointer.actor.opacity = 0; |
|
2407 |
this._summaryBoxPointer.actor.show(); |
|
2408 |
this._adjustSummaryBoxPointerPosition(); |
|
2409 |
||
2410 |
this._summaryBoxPointerState = State.SHOWING; |
|
2411 |
this._clickedSummaryItem.actor.add_style_pseudo_class('selected'); |
|
2412 |
this._summaryBoxPointer.show(BoxPointer.PopupAnimation.FULL, Lang.bind(this, function() { |
|
2413 |
this._summaryBoxPointerState = State.SHOWN; |
|
2414 |
}));
|
|
2415 |
},
|
|
2416 |
||
2417 |
_onSummaryBoxPointerContentUpdated: function() { |
|
2418 |
if (this._summaryBoxPointerItem.notificationStack.get_n_children() == 0) |
|
2419 |
this._hideSummaryBoxPointer(); |
|
2420 |
this._adjustSummaryBoxPointerPosition(); |
|
2421 |
},
|
|
2422 |
||
2423 |
_adjustSummaryBoxPointerPosition: function() { |
|
2424 |
if (!this._clickedSummaryItem) |
|
2425 |
return; |
|
2426 |
||
2427 |
this._summaryBoxPointer.setPosition(this._clickedSummaryItem.actor, 0); |
|
2428 |
},
|
|
2429 |
||
2430 |
_setClickedSummaryItem: function(item, button) { |
|
2431 |
if (item == this._clickedSummaryItem && |
|
2432 |
button == this._clickedSummaryItemMouseButton) |
|
2433 |
return false; |
|
2434 |
||
2435 |
if (this._clickedSummaryItem) { |
|
2436 |
this._clickedSummaryItem.actor.remove_style_pseudo_class('selected'); |
|
2437 |
this._clickedSummaryItem.actor.disconnect(this._clickedSummaryItemAllocationChangedId); |
|
2438 |
this._summary.disconnect(this._summaryMotionId); |
|
2439 |
Main.layoutManager.trayBox.disconnect(this._trayMotionId); |
|
2440 |
this._clickedSummaryItemAllocationChangedId = 0; |
|
2441 |
this._summaryMotionId = 0; |
|
2442 |
this._trayMotionId = 0; |
|
2443 |
}
|
|
2444 |
||
2445 |
this._clickedSummaryItem = item; |
|
2446 |
this._clickedSummaryItemMouseButton = button; |
|
2447 |
||
2448 |
if (this._clickedSummaryItem) { |
|
2449 |
this._clickedSummaryItem.source.emit('summary-item-clicked', button); |
|
2450 |
this._clickedSummaryItem.actor.add_style_pseudo_class('selected'); |
|
2451 |
this._clickedSummaryItemAllocationChangedId = |
|
2452 |
this._clickedSummaryItem.actor.connect('allocation-changed', |
|
2453 |
Lang.bind(this, this._adjustSummaryBoxPointerPosition)); |
|
2454 |
// _clickedSummaryItem.actor can change absolute position without changing allocation
|
|
2455 |
this._summaryMotionId = this._summary.connect('allocation-changed', |
|
2456 |
Lang.bind(this, this._adjustSummaryBoxPointerPosition)); |
|
2457 |
this._trayMotionId = Main.layoutManager.trayBox.connect('notify::anchor-y', |
|
2458 |
Lang.bind(this, this._adjustSummaryBoxPointerPosition)); |
|
2459 |
}
|
|
2460 |
||
2461 |
return true; |
|
2462 |
},
|
|
2463 |
||
2464 |
_onSummaryBoxPointerKeyPress: function(actor, event) { |
|
2465 |
switch (event.get_key_symbol()) { |
|
2466 |
case Clutter.KEY_Down: |
|
2467 |
case Clutter.KEY_Escape: |
|
2468 |
this._setClickedSummaryItem(null); |
|
2469 |
this._updateState(); |
|
2470 |
return true; |
|
2471 |
case Clutter.KEY_Delete: |
|
2472 |
this._clickedSummaryItem.source.destroy(); |
|
2473 |
this._escapeTray(); |
|
2474 |
return true; |
|
2475 |
}
|
|
2476 |
return false; |
|
2477 |
},
|
|
2478 |
||
2479 |
_onSummaryBoxPointerUngrabbed: function() { |
|
2480 |
this._summaryBoxPointerState = State.HIDING; |
|
2481 |
this._unlock(); |
|
2482 |
||
2483 |
if (this._summaryBoxPointerItem.source.notifications.length == 0) { |
|
2484 |
this._summaryBoxPointer.actor.hide(); |
|
2485 |
this._hideSummaryBoxPointerCompleted(); |
|
2486 |
} else { |
|
2487 |
if (global.stage.key_focus && |
|
2488 |
!this.actor.contains(global.stage.key_focus)) |
|
2489 |
this._setClickedSummaryItem(null); |
|
2490 |
this._summaryBoxPointer.hide(BoxPointer.PopupAnimation.FULL, Lang.bind(this, this._hideSummaryBoxPointerCompleted)); |
|
2491 |
}
|
|
2492 |
},
|
|
2493 |
||
2494 |
_hideSummaryBoxPointer: function() { |
|
2495 |
this._grabHelper.ungrab({ actor: this._summaryBoxPointer.bin.child }); |
|
2496 |
},
|
|
2497 |
||
2498 |
_hideSummaryBoxPointerCompleted: function() { |
|
2499 |
let doneShowingNotificationStack = (this._summaryBoxPointer.bin.child == this._summaryBoxPointerItem.notificationStackWidget); |
|
2500 |
||
2501 |
this._summaryBoxPointerState = State.HIDDEN; |
|
2502 |
this._summaryBoxPointer.bin.child = null; |
|
2503 |
this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerContentUpdatedId); |
|
2504 |
this._summaryBoxPointerContentUpdatedId = 0; |
|
2505 |
this._summaryBoxPointerItem.closeButton.disconnect(this._summaryBoxPointerCloseClickedId); |
|
2506 |
this._summaryBoxPointerCloseClickedId = 0; |
|
2507 |
this._summaryBoxPointerItem.source.disconnect(this._sourceDoneDisplayingId); |
|
2508 |
this._summaryBoxPointerDoneDisplayingId = 0; |
|
2509 |
||
2510 |
let sourceNotificationStackDoneShowing = null; |
|
2511 |
if (doneShowingNotificationStack) { |
|
2512 |
this._summaryBoxPointerItem.doneShowingNotificationStack(); |
|
2513 |
sourceNotificationStackDoneShowing = this._summaryBoxPointerItem.source; |
|
2514 |
}
|
|
2515 |
||
2516 |
this._summaryBoxPointerItem = null; |
|
2517 |
||
2518 |
if (sourceNotificationStackDoneShowing) { |
|
2519 |
if (sourceNotificationStackDoneShowing.isTransient && !this._reNotifyAfterHideNotification) |
|
2520 |
sourceNotificationStackDoneShowing.destroy(NotificationDestroyedReason.EXPIRED); |
|
2521 |
if (this._reNotifyAfterHideNotification) { |
|
2522 |
this._onNotify(this._reNotifyAfterHideNotification.source, this._reNotifyAfterHideNotification); |
|
2523 |
this._reNotifyAfterHideNotification = null; |
|
2524 |
}
|
|
2525 |
}
|
|
2526 |
||
2527 |
if (this._clickedSummaryItem) |
|
2528 |
this._updateState(); |
|
2529 |
}
|
|
2530 |
});
|
|
2531 |
Signals.addSignalMethods(MessageTray.prototype); |
|
2532 |
||
2533 |
const SystemNotificationSource = new Lang.Class({ |
|
2534 |
Name: 'SystemNotificationSource', |
|
2535 |
Extends: Source, |
|
2536 |
||
2537 |
_init: function() { |
|
2538 |
this.parent(_("System Information"), 'dialog-information-symbolic'); |
|
2539 |
this.setTransient(true); |
|
2540 |
},
|
|
2541 |
||
2542 |
open: function() { |
|
2543 |
this.destroy(); |
|
2544 |
}
|
|
2545 |
});
|