2011-09-28 09:16:26 -04:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2010-01-13 15:05:20 -05:00
|
|
|
|
|
|
|
const Clutter = imports.gi.Clutter;
|
2010-11-24 02:27:47 +03:00
|
|
|
const GLib = imports.gi.GLib;
|
|
|
|
const Gio = imports.gi.Gio;
|
2010-02-22 14:23:36 -05:00
|
|
|
const Gtk = imports.gi.Gtk;
|
2012-08-20 00:06:50 -04:00
|
|
|
const GnomeDesktop = imports.gi.GnomeDesktop;
|
2012-02-20 20:27:27 +01:00
|
|
|
const Atk = imports.gi.Atk;
|
2010-01-13 15:05:20 -05:00
|
|
|
const Lang = imports.lang;
|
|
|
|
const Mainloop = imports.mainloop;
|
2010-09-09 16:18:07 -04:00
|
|
|
const Meta = imports.gi.Meta;
|
2010-02-01 12:10:38 -05:00
|
|
|
const Pango = imports.gi.Pango;
|
2010-01-28 12:04:26 -05:00
|
|
|
const Shell = imports.gi.Shell;
|
2010-01-13 15:05:20 -05:00
|
|
|
const Signals = imports.signals;
|
2010-01-28 12:04:26 -05:00
|
|
|
const St = imports.gi.St;
|
2013-04-19 11:12:39 -04:00
|
|
|
const Tp = imports.gi.TelepathyGLib;
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2010-10-19 23:17:36 +04:00
|
|
|
const BoxPointer = imports.ui.boxpointer;
|
2012-08-09 02:30:13 +02:00
|
|
|
const CtrlAltTab = imports.ui.ctrlAltTab;
|
2011-02-19 16:49:56 +01:00
|
|
|
const GnomeSession = imports.misc.gnomeSession;
|
2012-02-28 15:04:00 -05:00
|
|
|
const GrabHelper = imports.ui.grabHelper;
|
2012-08-08 19:51:31 +02:00
|
|
|
const Lightbox = imports.ui.lightbox;
|
2011-02-19 16:49:56 +01:00
|
|
|
const Main = imports.ui.main;
|
2012-08-17 16:07:42 -04:00
|
|
|
const PointerWatcher = imports.ui.pointerWatcher;
|
2011-02-12 03:43:01 +08:00
|
|
|
const PopupMenu = imports.ui.popupMenu;
|
2010-08-18 16:01:33 -04:00
|
|
|
const Params = imports.misc.params;
|
2011-02-19 16:49:56 +01:00
|
|
|
const Tweener = imports.ui.tweener;
|
2010-11-30 11:16:10 -05:00
|
|
|
const Util = imports.misc.util;
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2012-07-14 13:07:24 +02:00
|
|
|
const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
|
|
|
|
|
2010-01-13 15:05:20 -05:00
|
|
|
const ANIMATION_TIME = 0.2;
|
|
|
|
const NOTIFICATION_TIMEOUT = 4;
|
|
|
|
|
2010-02-25 14:42:18 -05:00
|
|
|
const HIDE_TIMEOUT = 0.2;
|
2010-08-31 15:50:18 -04:00
|
|
|
const LONGER_HIDE_TIMEOUT = 0.6;
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2010-10-31 03:54:43 +08:00
|
|
|
// We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
|
|
|
|
// range from the point where it left the tray.
|
|
|
|
const MOUSE_LEFT_ACTOR_THRESHOLD = 20;
|
|
|
|
|
2012-08-17 16:07:42 -04:00
|
|
|
// Time the user needs to leave the mouse on the bottom pixel row to open the tray
|
|
|
|
const TRAY_DWELL_TIME = 1000; // ms
|
|
|
|
// Time resolution when tracking the mouse to catch the open tray dwell
|
|
|
|
const TRAY_DWELL_CHECK_INTERVAL = 100; // ms
|
|
|
|
|
2012-08-10 18:54:33 -03:00
|
|
|
const IDLE_TIME = 1000;
|
|
|
|
|
[MessageTray] reimplement the state machine
Previously, every time _updateState was called, it would make some
change, and so it was necessary to very carefully set up all the calls
to it, to ensure it was always called at exactly the right time. Now,
instead, we keep a bunch of state variables like "_notificationState"
and "_pointerInSummary", and potentially multiple timeouts, and
_updateState looks at all of them and figure out what, if anything,
needs to be changed.
By making the rules about what causes changes more explicit, it will
be easier to change those rules in the future as we add new
functionality.
Also, update the rules a bit, so that notifications can appear while
the summary is visible, and the summary only shows after a
notification if the summary has changed.
https://bugzilla.gnome.org/show_bug.cgi?id=609765
2010-02-11 15:31:12 -05:00
|
|
|
const State = {
|
|
|
|
HIDDEN: 0,
|
|
|
|
SHOWING: 1,
|
|
|
|
SHOWN: 2,
|
|
|
|
HIDING: 3
|
2010-01-28 13:39:00 -05:00
|
|
|
};
|
|
|
|
|
2011-01-27 18:26:53 -05:00
|
|
|
// These reasons are useful when we destroy the notifications received through
|
|
|
|
// the notification daemon. We use EXPIRED for transient notifications that the
|
|
|
|
// user did not interact with, DISMISSED for all other notifications that were
|
|
|
|
// destroyed as a result of a user action, and SOURCE_CLOSED for the notifications
|
|
|
|
// that were requested to be destroyed by the associated source.
|
|
|
|
const NotificationDestroyedReason = {
|
|
|
|
EXPIRED: 1,
|
|
|
|
DISMISSED: 2,
|
2011-01-28 13:31:16 -05:00
|
|
|
SOURCE_CLOSED: 3
|
2011-01-27 18:26:53 -05:00
|
|
|
};
|
|
|
|
|
2011-01-04 17:34:57 +08:00
|
|
|
// Message tray has its custom Urgency enumeration. LOW, NORMAL and CRITICAL
|
|
|
|
// urgency values map to the corresponding values for the notifications received
|
|
|
|
// through the notification daemon. HIGH urgency value is used for chats received
|
|
|
|
// through the Telepathy client.
|
|
|
|
const Urgency = {
|
|
|
|
LOW: 0,
|
|
|
|
NORMAL: 1,
|
|
|
|
HIGH: 2,
|
|
|
|
CRITICAL: 3
|
2013-12-04 11:22:25 -05:00
|
|
|
};
|
2011-01-04 17:34:57 +08:00
|
|
|
|
2010-11-24 02:31:55 +03:00
|
|
|
function _fixMarkup(text, allowMarkup) {
|
|
|
|
if (allowMarkup) {
|
|
|
|
// Support &, ", ', < and >, escape all other
|
|
|
|
// occurrences of '&'.
|
|
|
|
let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&');
|
2011-05-04 11:15:45 -04:00
|
|
|
|
2010-11-24 02:31:55 +03:00
|
|
|
// Support <b>, <i>, and <u>, escape anything else
|
|
|
|
// so it displays as raw markup.
|
2011-05-04 11:15:45 -04:00
|
|
|
_text = _text.replace(/<(?!\/?[biu]>)/g, '<');
|
|
|
|
|
|
|
|
try {
|
|
|
|
Pango.parse_markup(_text, -1, '');
|
|
|
|
return _text;
|
|
|
|
} catch (e) {}
|
2010-11-24 02:31:55 +03:00
|
|
|
}
|
2011-05-04 11:15:45 -04:00
|
|
|
|
|
|
|
// !allowMarkup, or invalid markup
|
|
|
|
return GLib.markup_escape_text(text, -1);
|
2010-02-01 15:23:49 -05:00
|
|
|
}
|
|
|
|
|
2011-11-20 18:56:27 +01:00
|
|
|
const URLHighlighter = new Lang.Class({
|
|
|
|
Name: 'URLHighlighter',
|
2010-11-24 02:27:47 +03:00
|
|
|
|
2010-11-24 02:31:55 +03:00
|
|
|
_init: function(text, lineWrap, allowMarkup) {
|
2010-11-24 02:27:47 +03:00
|
|
|
if (!text)
|
|
|
|
text = '';
|
|
|
|
this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' });
|
|
|
|
this._linkColor = '#ccccff';
|
|
|
|
this.actor.connect('style-changed', Lang.bind(this, function() {
|
2011-02-14 09:20:22 -05:00
|
|
|
let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false);
|
2010-11-24 02:27:47 +03:00
|
|
|
if (hasColor) {
|
|
|
|
let linkColor = color.to_string().substr(0, 7);
|
|
|
|
if (linkColor != this._linkColor) {
|
|
|
|
this._linkColor = linkColor;
|
|
|
|
this._highlightUrls();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
if (lineWrap) {
|
|
|
|
this.actor.clutter_text.line_wrap = true;
|
|
|
|
this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
|
|
|
|
this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
|
|
|
}
|
|
|
|
|
2010-11-24 02:31:55 +03:00
|
|
|
this.setMarkup(text, allowMarkup);
|
2011-03-27 17:58:23 +02:00
|
|
|
this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) {
|
2011-08-29 19:10:14 -04:00
|
|
|
// Don't try to URL highlight when invisible.
|
|
|
|
// The MessageTray doesn't actually hide us, so
|
|
|
|
// we need to check for paint opacities as well.
|
|
|
|
if (!actor.visible || actor.get_paint_opacity() == 0)
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2011-08-29 19:10:14 -04:00
|
|
|
|
2011-03-23 16:05:38 -04:00
|
|
|
// Keep Notification.actor from seeing this and taking
|
|
|
|
// a pointer grab, which would block our button-release-event
|
2011-03-27 17:58:23 +02:00
|
|
|
// handler, if an URL is clicked
|
|
|
|
return this._findUrlAtPos(event) != -1;
|
|
|
|
}));
|
2010-11-24 02:27:47 +03:00
|
|
|
this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) {
|
2011-08-29 19:10:14 -04:00
|
|
|
if (!actor.visible || actor.get_paint_opacity() == 0)
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2011-08-29 19:10:14 -04:00
|
|
|
|
2010-11-24 02:27:47 +03:00
|
|
|
let urlId = this._findUrlAtPos(event);
|
|
|
|
if (urlId != -1) {
|
|
|
|
let url = this._urls[urlId].url;
|
|
|
|
if (url.indexOf(':') == -1)
|
|
|
|
url = 'http://' + url;
|
2012-10-16 12:20:39 -04:00
|
|
|
|
2014-01-19 18:34:32 +01:00
|
|
|
Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context(0, -1));
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_STOP;
|
2010-11-24 02:27:47 +03:00
|
|
|
}
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2010-11-24 02:27:47 +03:00
|
|
|
}));
|
|
|
|
this.actor.connect('motion-event', Lang.bind(this, function(actor, event) {
|
2011-08-29 19:10:14 -04:00
|
|
|
if (!actor.visible || actor.get_paint_opacity() == 0)
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2011-08-29 19:10:14 -04:00
|
|
|
|
2010-11-24 02:27:47 +03:00
|
|
|
let urlId = this._findUrlAtPos(event);
|
|
|
|
if (urlId != -1 && !this._cursorChanged) {
|
2013-09-11 17:35:58 +02:00
|
|
|
global.screen.set_cursor(Meta.Cursor.POINTING_HAND);
|
2010-11-24 02:27:47 +03:00
|
|
|
this._cursorChanged = true;
|
|
|
|
} else if (urlId == -1) {
|
2013-09-11 17:35:58 +02:00
|
|
|
global.screen.set_cursor(Meta.Cursor.DEFAULT);
|
2010-11-24 02:27:47 +03:00
|
|
|
this._cursorChanged = false;
|
|
|
|
}
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2010-11-24 02:27:47 +03:00
|
|
|
}));
|
|
|
|
this.actor.connect('leave-event', Lang.bind(this, function() {
|
2011-08-29 19:10:14 -04:00
|
|
|
if (!this.actor.visible || this.actor.get_paint_opacity() == 0)
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2011-08-29 19:10:14 -04:00
|
|
|
|
2010-11-24 02:27:47 +03:00
|
|
|
if (this._cursorChanged) {
|
|
|
|
this._cursorChanged = false;
|
2013-09-11 17:35:58 +02:00
|
|
|
global.screen.set_cursor(Meta.Cursor.DEFAULT);
|
2010-11-24 02:27:47 +03:00
|
|
|
}
|
2013-11-29 18:17:34 +00:00
|
|
|
return Clutter.EVENT_PROPAGATE;
|
2010-11-24 02:27:47 +03:00
|
|
|
}));
|
|
|
|
},
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
hasText: function() {
|
|
|
|
return !!this._text;
|
|
|
|
},
|
|
|
|
|
2010-11-24 02:31:55 +03:00
|
|
|
setMarkup: function(text, allowMarkup) {
|
|
|
|
text = text ? _fixMarkup(text, allowMarkup) : '';
|
2010-11-24 02:27:47 +03:00
|
|
|
this._text = text;
|
|
|
|
|
|
|
|
this.actor.clutter_text.set_markup(text);
|
|
|
|
/* clutter_text.text contain text without markup */
|
2010-11-30 11:16:10 -05:00
|
|
|
this._urls = Util.findUrls(this.actor.clutter_text.text);
|
2010-11-24 02:27:47 +03:00
|
|
|
this._highlightUrls();
|
|
|
|
},
|
|
|
|
|
|
|
|
_highlightUrls: function() {
|
|
|
|
// text here contain markup
|
2010-11-30 11:16:10 -05:00
|
|
|
let urls = Util.findUrls(this._text);
|
2010-11-24 02:27:47 +03:00
|
|
|
let markup = '';
|
|
|
|
let pos = 0;
|
|
|
|
for (let i = 0; i < urls.length; i++) {
|
|
|
|
let url = urls[i];
|
|
|
|
let str = this._text.substr(pos, url.pos - pos);
|
|
|
|
markup += str + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>';
|
|
|
|
pos = url.pos + url.url.length;
|
|
|
|
}
|
|
|
|
markup += this._text.substr(pos);
|
|
|
|
this.actor.clutter_text.set_markup(markup);
|
|
|
|
},
|
|
|
|
|
|
|
|
_findUrlAtPos: function(event) {
|
|
|
|
let success;
|
|
|
|
let [x, y] = event.get_coords();
|
|
|
|
[success, x, y] = this.actor.transform_stage_point(x, y);
|
|
|
|
let find_pos = -1;
|
|
|
|
for (let i = 0; i < this.actor.clutter_text.text.length; i++) {
|
|
|
|
let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i);
|
|
|
|
if (py > y || py + line_height < y || x < px)
|
|
|
|
continue;
|
|
|
|
find_pos = i;
|
|
|
|
}
|
|
|
|
if (find_pos != -1) {
|
|
|
|
for (let i = 0; i < this._urls.length; i++)
|
|
|
|
if (find_pos >= this._urls[i].pos &&
|
|
|
|
this._urls[i].pos + this._urls[i].url.length > find_pos)
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return -1;
|
|
|
|
}
|
2011-11-20 18:56:27 +01:00
|
|
|
});
|
2010-11-24 02:27:47 +03:00
|
|
|
|
2012-10-16 17:08:54 +02:00
|
|
|
// NotificationPolicy:
|
|
|
|
// An object that holds all bits of configurable policy related to a notification
|
|
|
|
// source, such as whether to play sound or honour the critical bit.
|
|
|
|
//
|
|
|
|
// A notification without a policy object will inherit the default one.
|
|
|
|
const NotificationPolicy = new Lang.Class({
|
|
|
|
Name: 'NotificationPolicy',
|
|
|
|
|
|
|
|
_init: function(params) {
|
|
|
|
params = Params.parse(params, { enable: true,
|
|
|
|
enableSound: true,
|
|
|
|
showBanners: true,
|
|
|
|
forceExpanded: false,
|
|
|
|
showInLockScreen: true,
|
|
|
|
detailsInLockScreen: false
|
|
|
|
});
|
|
|
|
Lang.copyProperties(params, this);
|
|
|
|
},
|
|
|
|
|
|
|
|
// Do nothing for the default policy. These methods are only useful for the
|
|
|
|
// GSettings policy.
|
|
|
|
store: function() { },
|
|
|
|
destroy: function() { }
|
|
|
|
});
|
|
|
|
Signals.addSignalMethods(NotificationPolicy.prototype);
|
|
|
|
|
2013-10-13 16:01:56 -04:00
|
|
|
const NotificationGenericPolicy = new Lang.Class({
|
|
|
|
Name: 'NotificationGenericPolicy',
|
|
|
|
Extends: NotificationPolicy,
|
|
|
|
|
|
|
|
_init: function() {
|
|
|
|
// Don't chain to parent, it would try setting
|
|
|
|
// our properties to the defaults
|
|
|
|
|
|
|
|
this.id = 'generic';
|
|
|
|
|
|
|
|
this._masterSettings = new Gio.Settings({ schema: 'org.gnome.desktop.notifications' });
|
|
|
|
this._masterSettings.connect('changed', Lang.bind(this, this._changed));
|
|
|
|
},
|
|
|
|
|
|
|
|
store: function() { },
|
|
|
|
|
|
|
|
destroy: function() {
|
|
|
|
this._masterSettings.run_dispose();
|
|
|
|
},
|
|
|
|
|
|
|
|
_changed: function(settings, key) {
|
|
|
|
this.emit('policy-changed', key);
|
|
|
|
},
|
|
|
|
|
|
|
|
get enable() {
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
get enableSound() {
|
|
|
|
return true;
|
|
|
|
},
|
|
|
|
|
|
|
|
get showBanners() {
|
|
|
|
return this._masterSettings.get_boolean('show-banners');
|
|
|
|
},
|
|
|
|
|
|
|
|
get forceExpanded() {
|
|
|
|
return false;
|
|
|
|
},
|
|
|
|
|
|
|
|
get showInLockScreen() {
|
|
|
|
return this._masterSettings.get_boolean('show-in-lock-screen');
|
|
|
|
},
|
|
|
|
|
|
|
|
get detailsInLockScreen() {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const NotificationApplicationPolicy = new Lang.Class({
|
|
|
|
Name: 'NotificationApplicationPolicy',
|
|
|
|
Extends: NotificationPolicy,
|
|
|
|
|
|
|
|
_init: function(id) {
|
|
|
|
// Don't chain to parent, it would try setting
|
|
|
|
// our properties to the defaults
|
|
|
|
|
|
|
|
this.id = id;
|
|
|
|
this._canonicalId = this._canonicalizeId(id);
|
|
|
|
|
|
|
|
this._masterSettings = new Gio.Settings({ schema: 'org.gnome.desktop.notifications' });
|
|
|
|
this._settings = new Gio.Settings({ schema: 'org.gnome.desktop.notifications.application',
|
|
|
|
path: '/org/gnome/desktop/notifications/application/' + this._canonicalId + '/' });
|
|
|
|
|
|
|
|
this._masterSettings.connect('changed', Lang.bind(this, this._changed));
|
|
|
|
this._settings.connect('changed', Lang.bind(this, this._changed));
|
|
|
|
},
|
|
|
|
|
|
|
|
store: function() {
|
|
|
|
this._settings.set_string('application-id', this.id + '.desktop');
|
|
|
|
|
|
|
|
let apps = this._masterSettings.get_strv('application-children');
|
|
|
|
if (apps.indexOf(this._canonicalId) < 0) {
|
|
|
|
apps.push(this._canonicalId);
|
|
|
|
this._masterSettings.set_strv('application-children', apps);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function() {
|
|
|
|
this._masterSettings.run_dispose();
|
|
|
|
this._settings.run_dispose();
|
|
|
|
},
|
|
|
|
|
|
|
|
_changed: function(settings, key) {
|
|
|
|
this.emit('policy-changed', key);
|
|
|
|
},
|
|
|
|
|
|
|
|
_canonicalizeId: function(id) {
|
|
|
|
// Keys are restricted to lowercase alphanumeric characters and dash,
|
|
|
|
// and two dashes cannot be in succession
|
|
|
|
return id.toLowerCase().replace(/[^a-z0-9\-]/g, '-').replace(/--+/g, '-');
|
|
|
|
},
|
|
|
|
|
|
|
|
get enable() {
|
|
|
|
return this._settings.get_boolean('enable');
|
|
|
|
},
|
|
|
|
|
|
|
|
get enableSound() {
|
|
|
|
return this._settings.get_boolean('enable-sound-alerts');
|
|
|
|
},
|
|
|
|
|
|
|
|
get showBanners() {
|
|
|
|
return this._masterSettings.get_boolean('show-banners') &&
|
|
|
|
this._settings.get_boolean('show-banners');
|
|
|
|
},
|
|
|
|
|
|
|
|
get forceExpanded() {
|
|
|
|
return this._settings.get_boolean('force-expanded');
|
|
|
|
},
|
|
|
|
|
|
|
|
get showInLockScreen() {
|
|
|
|
return this._masterSettings.get_boolean('show-in-lock-screen') &&
|
|
|
|
this._settings.get_boolean('show-in-lock-screen');
|
|
|
|
},
|
|
|
|
|
|
|
|
get detailsInLockScreen() {
|
|
|
|
return this._settings.get_boolean('details-in-lock-screen');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2010-02-01 12:10:38 -05:00
|
|
|
// Notification:
|
|
|
|
// @source: the notification's Source
|
|
|
|
// @title: the title
|
|
|
|
// @banner: the banner text
|
2010-08-18 16:01:33 -04:00
|
|
|
// @params: optional additional params
|
2010-02-01 12:10:38 -05:00
|
|
|
//
|
2010-08-30 16:03:08 -04:00
|
|
|
// Creates a notification. In the banner mode, the notification
|
|
|
|
// will show an icon, @title (in bold) and @banner, all on a single
|
|
|
|
// line (with @banner ellipsized if necessary).
|
2010-02-09 11:25:10 -05:00
|
|
|
//
|
2010-08-30 16:03:08 -04:00
|
|
|
// The notification will be expandable if either it has additional
|
|
|
|
// elements that were added to it or if the @banner text did not
|
|
|
|
// fit fully in the banner mode. When the notification is expanded,
|
|
|
|
// the @banner text from the top line is always removed. The complete
|
2013-12-05 00:19:53 -05:00
|
|
|
// @banner text is added to the notification by default. You can change
|
|
|
|
// what is displayed by setting the child of this._bodyBin.
|
2010-08-05 13:09:27 -04:00
|
|
|
//
|
2013-12-05 00:19:53 -05:00
|
|
|
// You can also add buttons to the notification with addButton(),
|
|
|
|
// and you can construct simple default buttons with addAction().
|
2010-08-30 16:03:08 -04:00
|
|
|
//
|
2012-09-14 11:33:52 -03:00
|
|
|
// By default, the icon shown is the same as the source's.
|
|
|
|
// However, if @params contains a 'gicon' parameter, the passed in gicon
|
|
|
|
// will be used.
|
|
|
|
//
|
|
|
|
// You can add a secondary icon to the banner with 'secondaryGIcon'. There
|
|
|
|
// is no fallback for this icon.
|
2010-08-30 16:03:08 -04:00
|
|
|
//
|
2013-07-03 17:43:33 -04:00
|
|
|
// If @params contains 'bannerMarkup', with the value %true, then
|
|
|
|
// the corresponding element is assumed to use pango markup. If the
|
|
|
|
// parameter is not present for an element, then anything that looks
|
|
|
|
// like markup in that element will appear literally in the output.
|
2010-11-24 02:31:55 +03:00
|
|
|
//
|
2010-08-30 16:03:08 -04:00
|
|
|
// If @params contains a 'clear' parameter with the value %true, then
|
|
|
|
// the content and the action area of the notification will be cleared.
|
2012-11-05 18:10:24 +01:00
|
|
|
//
|
|
|
|
// If @params contains 'soundName' or 'soundFile', the corresponding
|
|
|
|
// event sound is played when the notification is shown (if the policy for
|
|
|
|
// @source allows playing sounds).
|
2011-11-20 16:12:02 +01:00
|
|
|
const Notification = new Lang.Class({
|
|
|
|
Name: 'Notification',
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
ICON_SIZE: 32,
|
2012-09-14 11:33:52 -03:00
|
|
|
|
2010-08-18 16:01:33 -04:00
|
|
|
_init: function(source, title, banner, params) {
|
2010-01-28 13:39:00 -05:00
|
|
|
this.source = source;
|
2011-09-21 16:46:08 -04:00
|
|
|
this.title = title;
|
2011-01-04 17:34:57 +08:00
|
|
|
this.urgency = Urgency.NORMAL;
|
2013-01-30 19:47:55 +01:00
|
|
|
this.isMusic = false;
|
2012-11-02 18:06:40 +01:00
|
|
|
this.forFeedback = false;
|
2012-09-05 18:59:50 +02:00
|
|
|
this.focused = false;
|
2012-08-06 17:28:55 +02:00
|
|
|
this.acknowledged = false;
|
2011-01-30 18:33:49 -05:00
|
|
|
this._destroyed = false;
|
2012-11-05 18:10:24 +01:00
|
|
|
this._soundName = null;
|
|
|
|
this._soundFile = null;
|
|
|
|
this._soundPlayed = false;
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
// Let me draw you a picture. I am a bad artist:
|
|
|
|
//
|
|
|
|
// ,. this._iconBin ,. this._titleLabel
|
|
|
|
// | ,-- this._second|ryIconBin
|
|
|
|
// .----|--------|---------------|-----------.
|
2014-06-03 14:55:57 -04:00
|
|
|
// | .----. | .----.-------------------. | X |
|
|
|
|
// | | | | | | |-|----- this._titleBox
|
|
|
|
// | '....' | '....'...................' | |
|
|
|
|
// | | | |- this._hbox
|
|
|
|
// | | this._bodyBin | |-.
|
|
|
|
// |________|____________________________|___| |- this.actor
|
2013-12-05 00:19:53 -05:00
|
|
|
// | this._actionArea |-'
|
|
|
|
// |_________________________________________|
|
|
|
|
// | this._buttonBox |
|
|
|
|
// |_________________________________________|
|
|
|
|
|
|
|
|
this.actor = new St.BoxLayout({ vertical: true,
|
|
|
|
style_class: 'notification',
|
|
|
|
accessible_role: Atk.Role.NOTIFICATION });
|
2011-03-21 17:43:34 -04:00
|
|
|
this.actor._delegate = this;
|
2011-03-03 03:47:58 +03:00
|
|
|
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
2010-06-13 16:17:35 +02:00
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._mainButton = new St.Button({ style_class: 'notification-main-button',
|
|
|
|
can_focus: true,
|
|
|
|
x_fill: true, y_fill: true });
|
|
|
|
this._mainButton.connect('clicked', Lang.bind(this, this._onClicked));
|
|
|
|
this.actor.add_child(this._mainButton);
|
|
|
|
|
2014-06-03 14:55:57 -04:00
|
|
|
// Separates the icon, title/body and close button
|
2013-12-05 00:19:53 -05:00
|
|
|
this._hbox = new St.BoxLayout({ style_class: 'notification-main-content' });
|
|
|
|
this._mainButton.child = this._hbox;
|
|
|
|
|
|
|
|
this._iconBin = new St.Bin({ y_align: St.Align.START });
|
|
|
|
this._hbox.add_child(this._iconBin);
|
|
|
|
|
|
|
|
this._titleBodyBox = new St.BoxLayout({ style_class: 'notification-title-body-box',
|
|
|
|
vertical: true });
|
2014-06-03 14:55:57 -04:00
|
|
|
this._titleBodyBox.set_x_expand(true);
|
2013-12-05 00:19:53 -05:00
|
|
|
this._hbox.add_child(this._titleBodyBox);
|
|
|
|
|
2014-06-03 14:55:57 -04:00
|
|
|
this._closeButton = new St.Button({ style_class: 'notification-close-button',
|
|
|
|
can_focus: true });
|
|
|
|
this._closeButton.set_y_align(Clutter.ActorAlign.START);
|
|
|
|
this._closeButton.set_y_expand(true);
|
|
|
|
this._closeButton.child = new St.Icon({ icon_name: 'window-close-symbolic', icon_size: 16 });
|
|
|
|
this._closeButton.connect('clicked', Lang.bind(this, this._onCloseClicked));
|
|
|
|
this._hbox.add_child(this._closeButton);
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._titleBox = new St.BoxLayout({ style_class: 'notification-title-box',
|
|
|
|
x_expand: true, x_align: Clutter.ActorAlign.START });
|
|
|
|
this._secondaryIconBin = new St.Bin();
|
|
|
|
this._titleBox.add_child(this._secondaryIconBin);
|
|
|
|
this._titleLabel = new St.Label({ x_expand: true });
|
2014-06-12 14:01:47 -04:00
|
|
|
this._titleLabel.clutter_text.line_wrap = true;
|
|
|
|
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
2013-12-05 00:19:53 -05:00
|
|
|
this._titleBox.add_child(this._titleLabel);
|
|
|
|
this._titleBodyBox.add(this._titleBox);
|
|
|
|
|
|
|
|
this._bodyScrollArea = new St.ScrollView({ style_class: 'notification-scrollview',
|
|
|
|
hscrollbar_policy: Gtk.PolicyType.NEVER });
|
|
|
|
this._titleBodyBox.add(this._bodyScrollArea);
|
|
|
|
|
|
|
|
this._bodyScrollable = new St.BoxLayout();
|
|
|
|
this._bodyScrollArea.add_actor(this._bodyScrollable);
|
|
|
|
|
|
|
|
this._bodyBin = new St.Bin();
|
|
|
|
this._bodyScrollable.add_actor(this._bodyBin);
|
|
|
|
|
|
|
|
// By default, this._bodyBin contains a URL highlighter. Subclasses
|
|
|
|
// can override this to provide custom content if they want to.
|
|
|
|
this._bodyUrlHighlighter = new URLHighlighter();
|
2014-06-12 14:01:47 -04:00
|
|
|
this._bodyUrlHighlighter.actor.clutter_text.line_wrap = true;
|
|
|
|
this._bodyUrlHighlighter.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
|
2013-12-05 00:19:53 -05:00
|
|
|
this._bodyBin.child = this._bodyUrlHighlighter.actor;
|
|
|
|
|
|
|
|
this._actionAreaBin = new St.Bin({ style_class: 'notification-action-area',
|
|
|
|
x_expand: true, y_expand: true });
|
|
|
|
this.actor.add_child(this._actionAreaBin);
|
|
|
|
|
|
|
|
this._buttonBox = new St.BoxLayout({ style_class: 'notification-button-box',
|
|
|
|
x_expand: true, y_expand: true });
|
|
|
|
global.focus_manager.add_group(this._buttonBox);
|
|
|
|
this.actor.add_child(this._buttonBox);
|
2010-07-28 17:02:32 +02:00
|
|
|
|
2013-10-12 18:50:49 +02:00
|
|
|
// If called with only one argument we assume the caller
|
|
|
|
// will call .update() later on. This is the case of
|
|
|
|
// NotificationDaemon, which wants to use the same code
|
|
|
|
// for new and updated notifications
|
|
|
|
if (arguments.length != 1)
|
|
|
|
this.update(title, banner, params);
|
2013-12-05 00:19:53 -05:00
|
|
|
|
|
|
|
this._sync();
|
|
|
|
},
|
|
|
|
|
|
|
|
_sync: function() {
|
|
|
|
this._iconBin.visible = (this._icon != null && this._icon.visible);
|
|
|
|
this._secondaryIconBin.visible = (this._secondaryIcon != null);
|
|
|
|
|
2014-06-12 14:01:47 -04:00
|
|
|
this._actionAreaBin.visible = (this._actionAreaBin.child != null);
|
|
|
|
this._buttonBox.visible = (this._buttonBox.get_n_children() > 0);
|
2013-12-05 00:19:53 -05:00
|
|
|
|
|
|
|
this._bodyUrlHighlighter.actor.visible = this._bodyUrlHighlighter.hasText();
|
2010-02-09 11:25:10 -05:00
|
|
|
},
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2010-02-09 11:25:10 -05:00
|
|
|
// update:
|
|
|
|
// @title: the new title
|
|
|
|
// @banner: the new banner
|
2010-08-18 16:01:33 -04:00
|
|
|
// @params: as in the Notification constructor
|
2010-02-09 11:25:10 -05:00
|
|
|
//
|
|
|
|
// Updates the notification by regenerating its icon and updating
|
2010-08-18 16:01:33 -04:00
|
|
|
// the title/banner. If @params.clear is %true, it will also
|
|
|
|
// remove any additional actors/action buttons previously added.
|
|
|
|
update: function(title, banner, params) {
|
2013-12-05 00:19:53 -05:00
|
|
|
params = Params.parse(params, { gicon: null,
|
2012-09-14 11:33:52 -03:00
|
|
|
secondaryGIcon: null,
|
2010-11-24 02:31:55 +03:00
|
|
|
bannerMarkup: false,
|
2012-11-05 18:10:24 +01:00
|
|
|
clear: false,
|
|
|
|
soundName: null,
|
|
|
|
soundFile: null });
|
2010-08-18 16:01:33 -04:00
|
|
|
|
2011-03-02 09:48:29 -05:00
|
|
|
let oldFocus = global.stage.key_focus;
|
|
|
|
|
2010-08-18 16:01:33 -04:00
|
|
|
if (this._actionArea && params.clear) {
|
2011-03-02 09:48:29 -05:00
|
|
|
if (oldFocus && this._actionArea.contains(oldFocus))
|
|
|
|
this.actor.grab_key_focus();
|
|
|
|
|
2010-02-22 14:23:36 -05:00
|
|
|
this._actionArea.destroy();
|
|
|
|
this._actionArea = null;
|
2010-02-09 11:25:10 -05:00
|
|
|
}
|
2011-08-29 22:41:24 +05:30
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
if (params.clear) {
|
|
|
|
this._buttonBox.destroy_all_children();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._icon && (params.gicon || params.clear)) {
|
|
|
|
this._icon.destroy();
|
|
|
|
this._icon = null;
|
|
|
|
}
|
2010-02-09 11:25:10 -05:00
|
|
|
|
2012-09-14 11:33:52 -03:00
|
|
|
if (params.gicon) {
|
|
|
|
this._icon = new St.Icon({ gicon: params.gicon,
|
|
|
|
icon_size: this.ICON_SIZE });
|
|
|
|
} else {
|
|
|
|
this._icon = this.source.createIcon(this.ICON_SIZE);
|
|
|
|
}
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
if (this._icon)
|
|
|
|
this._iconBin.child = this._icon;
|
|
|
|
|
|
|
|
if (this._secondaryIcon && (params.secondaryGIcon || params.clear)) {
|
|
|
|
this._secondaryIcon.destroy();
|
|
|
|
this._secondaryIcon = null;
|
2011-09-21 16:47:49 -04:00
|
|
|
}
|
2010-02-01 12:10:38 -05:00
|
|
|
|
2012-09-14 11:33:52 -03:00
|
|
|
if (params.secondaryGIcon) {
|
|
|
|
this._secondaryIcon = new St.Icon({ gicon: params.secondaryGIcon,
|
|
|
|
style_class: 'secondary-icon' });
|
2013-12-05 00:19:53 -05:00
|
|
|
this._secondaryIconBin.child = this._secondaryIcon;
|
2012-07-05 19:47:13 +02:00
|
|
|
}
|
|
|
|
|
2011-09-21 16:46:08 -04:00
|
|
|
this.title = title;
|
2013-07-03 17:43:33 -04:00
|
|
|
title = title ? _fixMarkup(title.replace(/\n/g, ' '), false) : '';
|
2010-02-09 11:20:51 -05:00
|
|
|
this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>');
|
2010-02-01 12:10:38 -05:00
|
|
|
|
2014-06-03 14:26:59 -04:00
|
|
|
let titleDirection;
|
2011-04-06 18:23:49 +02:00
|
|
|
if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL)
|
2014-06-03 14:26:59 -04:00
|
|
|
titleDirection = Clutter.TextDirection.RTL;
|
2011-04-06 18:23:49 +02:00
|
|
|
else
|
2014-06-03 14:26:59 -04:00
|
|
|
titleDirection = Clutter.TextDirection.LTR;
|
2011-04-06 18:23:49 +02:00
|
|
|
|
|
|
|
// Let the title's text direction control the overall direction
|
|
|
|
// of the notification - in case where different scripts are used
|
|
|
|
// in the notification, this is the right thing for the icon, and
|
|
|
|
// arguably for action buttons as well. Labels other than the title
|
|
|
|
// will be allocated at the available width, so that their alignment
|
|
|
|
// is done correctly automatically.
|
2013-12-05 00:19:53 -05:00
|
|
|
this.actor.set_text_direction(titleDirection);
|
2010-11-24 02:27:47 +03:00
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._bodyUrlHighlighter.setMarkup(banner, params.bannerMarkup);
|
2010-08-30 16:03:08 -04:00
|
|
|
|
2012-11-05 18:10:24 +01:00
|
|
|
if (this._soundName != params.soundName ||
|
|
|
|
this._soundFile != params.soundFile) {
|
|
|
|
this._soundName = params.soundName;
|
|
|
|
this._soundFile = params.soundFile;
|
|
|
|
this._soundPlayed = false;
|
|
|
|
}
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._sync();
|
2010-02-09 11:25:10 -05:00
|
|
|
},
|
2010-02-01 12:10:38 -05:00
|
|
|
|
2011-03-21 17:43:34 -04:00
|
|
|
setIconVisible: function(visible) {
|
|
|
|
this._icon.visible = visible;
|
2013-12-05 00:19:53 -05:00
|
|
|
this._sync();
|
2011-03-21 17:43:34 -04:00
|
|
|
},
|
|
|
|
|
2011-03-22 03:40:53 -04:00
|
|
|
enableScrolling: function(enableScrolling) {
|
2013-12-05 00:19:53 -05:00
|
|
|
let scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
|
|
|
|
this._bodyScrollArea.vscrollbar_policy = scrollPolicy;
|
|
|
|
this._bodyScrollArea.enable_mouse_scrolling = enableScrolling;
|
2010-02-12 15:15:03 -05:00
|
|
|
},
|
|
|
|
|
2010-02-22 14:23:36 -05:00
|
|
|
// scrollTo:
|
|
|
|
// @side: St.Side.TOP or St.Side.BOTTOM
|
|
|
|
//
|
|
|
|
// Scrolls the content area (if scrollable) to the indicated edge
|
|
|
|
scrollTo: function(side) {
|
2013-12-05 00:19:53 -05:00
|
|
|
let adjustment = this._bodyScrollArea.vscroll.adjustment;
|
2010-02-22 14:23:36 -05:00
|
|
|
if (side == St.Side.TOP)
|
|
|
|
adjustment.value = adjustment.lower;
|
|
|
|
else if (side == St.Side.BOTTOM)
|
|
|
|
adjustment.value = adjustment.upper;
|
|
|
|
},
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
setActionArea: function(actor) {
|
|
|
|
if (this._actionArea)
|
2010-02-22 14:23:36 -05:00
|
|
|
this._actionArea.destroy();
|
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._actionArea = actor;
|
|
|
|
this._actionAreaBin.child = actor;
|
|
|
|
this._sync();
|
2010-02-22 14:23:36 -05:00
|
|
|
},
|
|
|
|
|
2013-10-13 23:06:09 -04:00
|
|
|
addButton: function(button, callback) {
|
2013-10-13 23:08:16 -04:00
|
|
|
this._buttonBox.add(button);
|
2013-10-13 23:06:09 -04:00
|
|
|
button.connect('clicked', Lang.bind(this, function() {
|
|
|
|
callback();
|
|
|
|
|
2013-07-03 16:42:53 -04:00
|
|
|
this.emit('done-displaying');
|
|
|
|
this.destroy();
|
2013-10-13 23:06:09 -04:00
|
|
|
}));
|
2013-10-13 23:08:16 -04:00
|
|
|
|
2013-12-05 00:19:53 -05:00
|
|
|
this._sync();
|
2013-10-13 23:08:16 -04:00
|
|
|
return button;
|
|
|
|
},
|
|
|
|
|
|
|
|
// addAction:
|
|
|
|
// @label: the label for the action's button
|
2013-10-13 23:06:09 -04:00
|
|
|
// @callback: the callback for the action
|
2013-10-13 23:08:16 -04:00
|
|
|
//
|
|
|
|
// Adds a button with the given @label to the notification. All
|
|
|
|
// action buttons will appear in a single row at the bottom of
|
|
|
|
// the notification.
|
2013-10-13 23:06:09 -04:00
|
|
|
addAction: function(label, callback) {
|
2013-10-13 23:13:31 -04:00
|
|
|
let button = new St.Button({ style_class: 'notification-button',
|
2013-12-05 00:19:53 -05:00
|
|
|
x_expand: true, label: label, can_focus: true });
|
2013-10-13 23:06:09 -04:00
|
|
|
|
|
|
|
return this.addButton(button, callback);
|
2012-05-17 21:05:36 +02:00
|
|
|
},
|
|
|
|
|
2011-01-04 17:34:57 +08:00
|
|
|
setUrgency: function(urgency) {
|
|
|
|
this.urgency = urgency;
|
2010-04-28 15:34:27 -04:00
|
|
|
},
|
|
|
|
|
2012-11-02 18:06:40 +01:00
|
|
|
setForFeedback: function(forFeedback) {
|
|
|
|
this.forFeedback = forFeedback;
|
|
|
|
},
|
|
|
|
|
2010-04-29 10:54:05 -04:00
|
|
|
_styleChanged: function() {
|
2011-03-03 15:14:52 -05:00
|
|
|
this._spacing = this._table.get_theme_node().get_length('spacing-columns');
|
2010-04-29 10:54:05 -04:00
|
|
|
},
|
|
|
|
|
2010-02-01 12:10:38 -05:00
|
|
|
_bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
|
2010-02-09 11:20:51 -05:00
|
|
|
let [titleMin, titleNat] = this._titleLabel.get_preferred_width(forHeight);
|
|
|
|
let [bannerMin, bannerNat] = this._bannerLabel.get_preferred_width(forHeight);
|
2010-01-28 13:39:00 -05:00
|
|
|
|
2012-07-05 19:47:13 +02:00
|
|
|
if (this._secondaryIcon) {
|
|
|
|
let [secondaryIconMin, secondaryIconNat] = this._secondaryIcon.get_preferred_width(forHeight);
|
|
|
|
|
|
|
|
alloc.min_size = secondaryIconMin + this._spacing + titleMin;
|
|
|
|
alloc.natural_size = secondaryIconNat + this._spacing + titleNat + this._spacing + bannerNat;
|
|
|
|
} else {
|
|
|
|
alloc.min_size = titleMin;
|
|
|
|
alloc.natural_size = titleNat + this._spacing + bannerNat;
|
|
|
|
}
|
2010-02-01 12:10:38 -05:00
|
|
|
},
|
|
|
|
|
2012-11-05 18:10:24 +01:00
|
|
|
playSound: function() {
|
|
|
|
if (this._soundPlayed)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (!this.source.policy.enableSound) {
|
|
|
|
this._soundPlayed = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._soundName) {
|
|
|
|
if (this.source.app) {
|
|
|
|
let app = this.source.app;
|
|
|
|
|
|
|
|
global.play_theme_sound_full(0, this._soundName,
|
|
|
|
this.title, null,
|
|
|
|
app.get_id(), app.get_name());
|
|
|
|
} else {
|
|
|
|
global.play_theme_sound(0, this._soundName, this.title, null);
|
|
|
|
}
|
|
|
|
} else if (this._soundFile) {
|
|
|
|
if (this.source.app) {
|
|
|
|
let app = this.source.app;
|
|
|
|
|
|
|
|
global.play_sound_file_full(0, this._soundFile,
|
|
|
|
this.title, null,
|
|
|
|
app.get_id(), app.get_name());
|
|
|
|
} else {
|
|
|
|
global.play_sound_file(0, this._soundFile, this.title, null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2010-12-16 15:49:47 -05:00
|
|
|
_onClicked: function() {
|
|
|
|
this.emit('clicked');
|
|
|
|
this.emit('done-displaying');
|
2013-07-03 16:42:53 -04:00
|
|
|
this.destroy();
|
2010-12-16 15:49:47 -05:00
|
|
|
},
|
|
|
|
|
2014-06-03 14:55:57 -04:00
|
|
|
_onCloseClicked: function() {
|
|
|
|
this.destroy();
|
|
|
|
},
|
|
|
|
|
2011-03-03 03:47:58 +03:00
|
|
|
_onDestroy: function() {
|
2011-01-30 18:33:49 -05:00
|
|
|
if (this._destroyed)
|
|
|
|
return;
|
|
|
|
this._destroyed = true;
|
2011-03-03 03:47:58 +03:00
|
|
|
if (!this._destroyedReason)
|
|
|
|
this._destroyedReason = NotificationDestroyedReason.DISMISSED;
|
|
|
|
this.emit('destroy', this._destroyedReason);
|
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function(reason) {
|
|
|
|
this._destroyedReason = reason;
|
|
|
|
this.actor.destroy();
|
2011-03-21 17:43:34 -04:00
|
|
|
this.actor._delegate = null;
|
2010-01-13 15:05:20 -05:00
|
|
|
}
|
2011-11-20 16:12:02 +01:00
|
|
|
});
|
2010-02-01 15:41:22 -05:00
|
|
|
Signals.addSignalMethods(Notification.prototype);
|
2010-01-13 15:05:20 -05:00
|
|
|
|
2012-07-19 15:05:17 +02:00
|
|
|
const Source = new Lang.Class({
|
|
|
|
Name: 'MessageTraySource',
|
|
|
|
|
2012-08-12 12:35:41 +02:00
|
|
|
SOURCE_ICON_SIZE: 48,
|
2012-07-19 15:05:17 +02:00
|
|
|
|
2012-05-30 09:58:37 -04:00
|
|
|
_init: function(title, iconName) {
|
2012-07-19 15:05:17 +02:00
|
|
|
this.title = title;
|
|
|
|
this.iconName = iconName;
|
|
|
|
|
|
|
|
this.isChat = false;
|
|
|
|
this.isMuted = false;
|
2012-09-21 17:21:25 +02:00
|
|
|
this.keepTrayOnSummaryClick = false;
|
2012-07-19 15:05:17 +02:00
|
|
|
|
|
|
|
this.notifications = [];
|
2012-10-16 17:08:54 +02:00
|
|
|
|
|
|
|
this.policy = this._createPolicy();
|
2012-07-19 15:05:17 +02:00
|
|
|
},
|
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
get count() {
|
|
|
|
return this.notifications.length;
|
|
|
|
},
|
2011-06-24 15:46:55 -04:00
|
|
|
|
2013-02-28 21:25:05 -05:00
|
|
|
get indicatorCount() {
|
messageTray: Remove support for transient notifications
Transient notifications have used for lots of different "system status"
notifications, like network, low power, low disk space, etc. However, a
majority of these notifications should really also be persistent
instead of going away after they appear.
Users have reported getting confused after seeing a notification appear
up in the corner of their eye, and then have no record of what it was
since the tray was empty.
To simplify the code, set the users more at ease, and also make things
like low power and low disk space more noticeable and urgent after they
go away.
Applications can and should explicitly close any notification it wants
to when state changes, so these notifications shouldn't linger if the
user e.g. plugs in his power cable, or clears up some disk space.
2014-06-12 14:04:54 -04:00
|
|
|
return this.notifications.length;
|
2013-02-28 21:25:05 -05:00
|
|
|
},
|
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
get unseenCount() {
|
|
|
|
return this.notifications.filter(function(n) { return !n.acknowledged; }).length;
|
2011-06-24 15:46:55 -04:00
|
|
|
},
|
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
get countVisible() {
|
|
|
|
return this.count > 1;
|
|
|
|
},
|
|
|
|
|
2013-10-11 13:59:19 -04:00
|
|
|
get isClearable() {
|
2013-07-03 16:42:53 -04:00
|
|
|
return !this.trayIcon && !this.isChat;
|
2013-10-11 13:59:19 -04:00
|
|
|
},
|
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
countUpdated: function() {
|
|
|
|
this.emit('count-updated');
|
2011-06-24 15:47:24 -04:00
|
|
|
},
|
|
|
|
|
2012-10-16 17:08:54 +02:00
|
|
|
_createPolicy: function() {
|
|
|
|
return new NotificationPolicy();
|
|
|
|
},
|
|
|
|
|
2011-06-08 02:27:57 -04:00
|
|
|
setTitle: function(newTitle) {
|
|
|
|
this.title = newTitle;
|
|
|
|
this.emit('title-changed');
|
|
|
|
},
|
|
|
|
|
2011-11-03 15:48:09 +01:00
|
|
|
setMuted: function(muted) {
|
|
|
|
if (!this.isChat || this.isMuted == muted)
|
|
|
|
return;
|
|
|
|
this.isMuted = muted;
|
|
|
|
this.emit('muted-changed');
|
|
|
|
},
|
|
|
|
|
2012-07-19 15:05:17 +02:00
|
|
|
// Called to create a new icon actor.
|
2011-10-07 18:31:26 -04:00
|
|
|
// Provides a sane default implementation, override if you need
|
|
|
|
// something more fancy.
|
2012-07-19 15:05:17 +02:00
|
|
|
createIcon: function(size) {
|
2012-09-15 03:10:15 -03:00
|
|
|
return new St.Icon({ gicon: this.getIcon(),
|
2012-07-19 15:05:17 +02:00
|
|
|
icon_size: size });
|
2010-01-13 15:05:20 -05:00
|
|
|
},
|
|
|
|
|
2012-09-15 03:10:15 -03:00
|
|
|
getIcon: function() {
|
|
|
|
return new Gio.ThemedIcon({ name: this.iconName });
|
|
|
|
},
|
|
|
|
|
2013-10-13 21:52:44 -04:00
|
|
|
_onNotificationDestroy: function(notification) {
|
|
|
|
let index = this.notifications.indexOf(notification);
|
|
|
|
if (index < 0)
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.notifications.splice(index, 1);
|
|
|
|
if (this.notifications.length == 0)
|
|
|
|
this._lastNotificationRemoved();
|
|
|
|
|
|
|
|
this.countUpdated();
|
|
|
|
},
|
|
|
|
|
2011-02-28 13:13:32 -05:00
|
|
|
pushNotification: function(notification) {
|
2013-10-13 21:51:30 -04:00
|
|
|
if (this.notifications.indexOf(notification) >= 0)
|
|
|
|
return;
|
2010-02-25 14:42:18 -05:00
|
|
|
|
2013-10-13 21:52:44 -04:00
|
|
|
notification.connect('destroy', Lang.bind(this, this._onNotificationDestroy));
|
2013-10-13 21:51:30 -04:00
|
|
|
this.notifications.push(notification);
|
|
|
|
this.emit('notification-added', notification);
|
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
this.countUpdated();
|
2011-02-28 13:13:32 -05:00
|
|
|
},
|
2010-02-25 14:42:18 -05:00
|
|
|
|
2011-02-28 13:13:32 -05:00
|
|
|
notify: function(notification) {
|
2012-08-06 17:28:55 +02:00
|
|
|
notification.acknowledged = false;
|
2011-02-28 13:13:32 -05:00
|
|
|
this.pushNotification(notification);
|
2012-10-16 17:08:54 +02:00
|
|
|
|
2012-11-05 18:10:24 +01:00
|
|
|
if (!this.isMuted) {
|
|
|
|
// Play the sound now, if banners are disabled.
|
|
|
|
// Otherwise, it will be played when the notification
|
|
|
|
// is next shown.
|
|
|
|
if (this.policy.showBanners) {
|
|
|
|
this.emit('notify', notification);
|
|
|
|
} else {
|
|
|
|
notification.playSound();
|
|
|
|
}
|
|
|
|
}
|
2010-01-13 15:05:20 -05:00
|
|
|
},
|
|
|
|
|
2011-03-21 17:43:34 -04:00
|
|
|
destroy: function(reason) {
|
2012-10-16 17:08:54 +02:00
|
|
|
this.policy.destroy();
|
2012-10-25 18:30:27 +02:00
|
|
|
|
|
|
|
let notifications = this.notifications;
|
|
|
|
this.notifications = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < notifications.length; i++)
|
|
|
|
notifications[i].destroy(reason);
|
|
|
|
|
2011-03-21 17:43:34 -04:00
|
|
|
this.emit('destroy', reason);
|
2010-08-05 13:09:27 -04:00
|
|
|
},
|
|
|
|
|
2012-07-23 01:07:51 -03:00
|
|
|
iconUpdated: function() {
|
|
|
|
this.emit('icon-updated');
|
|
|
|
},
|
|
|
|
|
2010-08-05 13:09:27 -04:00
|
|
|
//// Protected methods ////
|
|
|
|
_setSummaryIcon: function(icon) {
|
2012-07-23 01:07:51 -03:00
|
|
|
this._mainIcon.setIcon(icon);
|
|
|
|
this.iconUpdated();
|
MessageTray: untangle click notifications
Previously, when you clicked on a notification, it would call
this.source.clicked(), which would emit a 'clicked' signal on the
source, and then various other stuff would happen from there. This
used to make a little bit of sense, when clicking on a notification
was supposed to do the same thing as clicking on its source, but makes
less sense now, when clicking on the source itself *doesn't* call
source.clicked()...
Change it so that when you click on a notification, the notification
emits 'clicked' itself, and the source notices that and calls its
notificationClicked() method, and the various source subclasses do
what they need to do with that, and Source no longer has a clicked
method/signal.
https://bugzilla.gnome.org/show_bug.cgi?id=631042
2010-09-30 16:41:38 -04:00
|
|
|
},
|
|
|
|
|
2013-08-02 00:08:33 -04:00
|
|
|
// To be overridden by subclasses
|
|
|
|
open: function() {
|
2010-12-16 15:49:47 -05:00
|
|
|
},
|
|
|
|
|
2013-07-03 16:42:53 -04:00
|
|
|
destroyNotifications: function() {
|
2011-03-21 17:43:34 -04:00
|
|
|
for (let i = this.notifications.length - 1; i >= 0; i--)
|
2013-07-03 16:42:53 -04:00
|
|
|
this.notifications[i].destroy();
|
2011-06-24 15:47:24 -04:00
|
|
|
|
2012-08-06 17:28:55 +02:00
|
|
|
this.countUpdated();
|
2011-03-21 17:43:34 -04:00
|
|
|
},
|
|
|
|
|
2010-12-16 15:49:47 -05:00
|
|
|
// Default implementation is to destroy this source, but subclasses can override
|
2011-03-21 17:43:34 -04:00
|
|
|
_lastNotificationRemoved: function() {
|
2010-12-16 15:49:47 -05:00
|
|
|
this.destroy();
|
2012-05-23 00:02:00 +02:00
|
|
|
},
|
|
|
|
|
2013-01-30 19:47:55 +01:00
|
|
|
getMusicNotification: function() {
|
|
|
|
for (let i = 0; i < this.notifications.length; i++) {
|
|
|
|
if (this.notifications[i].isMusic)
|
|
|
|
return this.notifications[i];
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
},
|
2011-11-20 16:12:02 +01:00
|
|
|
});
|
2010-01-13 15:05:20 -05:00
|
|
|
Signals.addSignalMethods(Source.prototype);
|
|
|
|
|
2011-11-20 18:56:27 +01:00
|
|
|
const MessageTray = new Lang.Class({
|
|
|
|
Name: 'MessageTray',
|
2010-01-13 15:05:20 -05:00
|
|
|
|
|
|
|
_init: function() {
|
2014-01-14 23:49:47 +01:00
|
|
|
this._sources = new Map();
|
2013-02-15 16:09:44 +01:00
|
|
|
},
|
|
|
|
|
2014-02-26 14:44:53 +01:00
|
|
|
_expireNotification: function() {
|
|
|
|
this._notificationExpired = true;
|
|
|
|
this._updateState();
|
|
|
|
},
|
|
|
|
|
2010-01-13 15:05:20 -05:00
|
|
|
contains: function(source) {
|
2012-11-04 18:49:14 +01:00
|
|
|
return this._sources.has(source);
|
2010-01-13 15:05:20 -05:00
|
|
|
},
|
|
|
|
|
|
|
|
add: function(source) {
|
|
|
|
if (this.contains(source)) {
|
2010-08-09 13:18:15 -04:00
|
|
|
log('Trying to re-add source ' + source.title);
|
2010-01-13 15:05:20 -05:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-10-16 17:08:54 +02:00
|
|
|
// Register that we got a notification for this source
|
|
|
|
source.policy.store();
|
|
|
|
|
|
|
|
source.policy.connect('enable-changed', Lang.bind(this, this._onSourceEnableChanged, source));
|
2014-06-13 11:58:40 -04:00
|
|
|
// source.policy.connect('policy-changed', Lang.bind(this, this._updateState));
|
2012-10-16 17:08:54 +02:00
|
|
|
this._onSourceEnableChanged(source.policy, source);
|
2012-11-04 18:49:14 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
_addSource: function(source) {
|
|
|
|
let obj = {
|
|
|
|
source: source,
|
|
|
|
notifyId: 0,
|
|
|
|
destroyId: 0,
|
|
|
|
mutedChangedId: 0
|
|
|
|
};
|
2013-05-12 20:13:43 +02:00
|
|
|
|
2012-11-04 18:49:14 +01:00
|
|
|
this._sources.set(source, obj);
|
2010-01-28 13:12:03 -05:00
|
|
|
|
2012-11-04 18:49:14 +01:00
|
|
|
obj.notifyId = source.connect('notify', Lang.bind(this, this._onNotify));
|
|
|
|
obj.destroyId = source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
|
|
|
|
obj.mutedChangedId = source.connect('muted-changed', Lang.bind(this,
|
2011-11-03 15:48:09 +01:00
|
|
|
function () {
|
|
|
|
if (source.isMuted)
|
|
|
|
this._notificationQueue = this._notificationQueue.filter(function(notification) {
|
|
|
|
return source != notification.source;
|
|
|
|
});
|
|
|
|
}));
|
|
|
|
|
2013-01-30 19:47:55 +01:00
|
|
|
this.emit('source-added', source);
|
2012-05-23 00:02:00 +02:00
|
|
|
},
|
|
|
|
|
2012-11-04 18:49:14 +01:00
|
|
|
_removeSource: function(source) {
|
2014-01-14 23:49:47 +01:00
|
|
|
let obj = this._sources.get(source);
|
|
|
|
this._sources.delete(source);
|
2013-05-12 20:13:43 +02:00
|
|
|
|
2012-11-04 18:49:14 +01:00
|
|
|
source.disconnect(obj.notifyId);
|
|
|
|
source.disconnect(obj.destroyId);
|
|
|
|
source.disconnect(obj.mutedChangedId);
|
|
|
|
|
2013-02-05 19:06:19 +01:00
|
|
|
this.emit('source-removed', source);
|
2010-01-13 15:05:20 -05:00
|
|
|
},
|
|
|
|
|
2013-01-30 19:47:55 +01:00
|
|
|
getSources: function() {
|
2014-01-14 23:49:47 +01:00
|
|
|
return [k for (k of this._sources.keys())];
|
2012-11-04 18:49:14 +01:00
|
|
|
},
|
|
|
|
|
2012-10-16 17:08:54 +02:00
|
|
|
_onSourceEnableChanged: function(policy, source) {
|
|
|
|
let wasEnabled = this.contains(source);
|
|
|
|
let shouldBeEnabled = policy.enable;
|
|
|
|
|
|
|
|
if (wasEnabled != shouldBeEnabled) {
|
|
|
|
if (shouldBeEnabled)
|
|
|
|
this._addSource(source);
|
|
|
|
else
|
|
|
|
this._removeSource(source);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2012-11-04 18:49:14 +01:00
|
|
|
_onSourceDestroy: function(source) {
|
|
|
|
this._removeSource(source);
|
|
|
|
},
|
|
|
|
|
2010-02-01 15:23:49 -05:00
|
|
|
_onNotify: function(source, notification) {
|
2014-06-13 11:58:40 -04:00
|
|
|
// Fill in here.
|
2010-07-21 01:19:25 -04:00
|
|
|
},
|
2011-11-20 18:56:27 +01:00
|
|
|
});
|
2012-05-23 00:02:00 +02:00
|
|
|
Signals.addSignalMethods(MessageTray.prototype);
|
2010-11-30 10:47:28 -05:00
|
|
|
|
2011-11-20 16:12:02 +01:00
|
|
|
const SystemNotificationSource = new Lang.Class({
|
|
|
|
Name: 'SystemNotificationSource',
|
|
|
|
Extends: Source,
|
2010-11-30 10:47:28 -05:00
|
|
|
|
|
|
|
_init: function() {
|
2012-05-30 09:58:37 -04:00
|
|
|
this.parent(_("System Information"), 'dialog-information-symbolic');
|
2010-11-30 10:47:28 -05:00
|
|
|
},
|
|
|
|
|
2011-02-12 03:43:01 +08:00
|
|
|
open: function() {
|
2010-11-30 10:47:28 -05:00
|
|
|
this.destroy();
|
|
|
|
}
|
2011-11-20 16:12:02 +01:00
|
|
|
});
|