2011-09-28 13:16:26 +00:00
|
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-01-13 20:04:37 +00:00
|
|
|
|
const Clutter = imports.gi.Clutter;
|
2012-07-19 13:05:17 +00:00
|
|
|
|
const GdkPixbuf = imports.gi.GdkPixbuf;
|
2011-08-16 12:26:49 +00:00
|
|
|
|
const Gio = imports.gi.Gio;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
const GLib = imports.gi.GLib;
|
|
|
|
|
const Lang = imports.lang;
|
|
|
|
|
const Shell = imports.gi.Shell;
|
|
|
|
|
const Mainloop = imports.mainloop;
|
2010-03-08 13:13:59 +00:00
|
|
|
|
const St = imports.gi.St;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-01-11 21:34:09 +00:00
|
|
|
|
const Config = imports.misc.config;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
|
const MessageTray = imports.ui.messageTray;
|
|
|
|
|
const Params = imports.misc.params;
|
2010-12-17 20:30:12 +00:00
|
|
|
|
const Util = imports.misc.util;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
|
|
|
|
let nextNotificationId = 1;
|
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
// Should really be defined in Gio.js
|
|
|
|
|
const BusIface = <interface name="org.freedesktop.DBus">
|
|
|
|
|
<method name="GetConnectionUnixProcessID">
|
|
|
|
|
<arg type="s" direction="in" />
|
|
|
|
|
<arg type="u" direction="out" />
|
|
|
|
|
</method>
|
|
|
|
|
</interface>;
|
|
|
|
|
|
|
|
|
|
var BusProxy = Gio.DBusProxy.makeProxyWrapper(BusIface);
|
|
|
|
|
function Bus() {
|
|
|
|
|
return new BusProxy(Gio.DBus.session, 'org.freedesktop.DBus', '/org/freedesktop/DBus');
|
|
|
|
|
}
|
2010-02-15 17:27:28 +00:00
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
const NotificationDaemonIface = <interface name="org.freedesktop.Notifications">
|
|
|
|
|
<method name="Notify">
|
|
|
|
|
<arg type="s" direction="in"/>
|
|
|
|
|
<arg type="u" direction="in"/>
|
|
|
|
|
<arg type="s" direction="in"/>
|
|
|
|
|
<arg type="s" direction="in"/>
|
|
|
|
|
<arg type="s" direction="in"/>
|
|
|
|
|
<arg type="as" direction="in"/>
|
|
|
|
|
<arg type="a{sv}" direction="in"/>
|
|
|
|
|
<arg type="i" direction="in"/>
|
|
|
|
|
<arg type="u" direction="out"/>
|
|
|
|
|
</method>
|
|
|
|
|
<method name="CloseNotification">
|
|
|
|
|
<arg type="u" direction="in"/>
|
|
|
|
|
</method>
|
|
|
|
|
<method name="GetCapabilities">
|
|
|
|
|
<arg type="as" direction="out"/>
|
|
|
|
|
</method>
|
|
|
|
|
<method name="GetServerInformation">
|
|
|
|
|
<arg type="s" direction="out"/>
|
|
|
|
|
<arg type="s" direction="out"/>
|
|
|
|
|
<arg type="s" direction="out"/>
|
|
|
|
|
<arg type="s" direction="out"/>
|
|
|
|
|
</method>
|
|
|
|
|
<signal name="NotificationClosed">
|
|
|
|
|
<arg type="u"/>
|
|
|
|
|
<arg type="u"/>
|
|
|
|
|
</signal>
|
|
|
|
|
<signal name="ActionInvoked">
|
|
|
|
|
<arg type="u"/>
|
|
|
|
|
<arg type="s"/>
|
|
|
|
|
</signal>
|
|
|
|
|
</interface>;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
|
|
|
|
const NotificationClosedReason = {
|
|
|
|
|
EXPIRED: 1,
|
|
|
|
|
DISMISSED: 2,
|
|
|
|
|
APP_CLOSED: 3,
|
|
|
|
|
UNDEFINED: 4
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const Urgency = {
|
|
|
|
|
LOW: 0,
|
|
|
|
|
NORMAL: 1,
|
|
|
|
|
CRITICAL: 2
|
|
|
|
|
};
|
|
|
|
|
|
2010-02-16 22:32:19 +00:00
|
|
|
|
const rewriteRules = {
|
|
|
|
|
'XChat': [
|
|
|
|
|
{ pattern: /^XChat: Private message from: (\S*) \(.*\)$/,
|
2010-11-29 16:12:21 +00:00
|
|
|
|
replacement: '<$1>' },
|
2010-02-16 22:32:19 +00:00
|
|
|
|
{ pattern: /^XChat: New public message from: (\S*) \((.*)\)$/,
|
2010-11-29 16:12:21 +00:00
|
|
|
|
replacement: '$2 <$1>' },
|
2010-02-16 22:32:19 +00:00
|
|
|
|
{ pattern: /^XChat: Highlighted message from: (\S*) \((.*)\)$/,
|
2010-11-29 16:12:21 +00:00
|
|
|
|
replacement: '$2 <$1>' }
|
2010-02-16 22:32:19 +00:00
|
|
|
|
]
|
|
|
|
|
};
|
2010-06-23 19:20:39 +00:00
|
|
|
|
|
2012-07-24 17:47:39 +00:00
|
|
|
|
const STANDARD_TRAY_ICON_IMPLEMENTATIONS = {
|
|
|
|
|
'bluetooth-applet': 'bluetooth',
|
|
|
|
|
'gnome-volume-control-applet': 'volume', // renamed to gnome-sound-applet
|
|
|
|
|
// when moved to control center
|
|
|
|
|
'gnome-sound-applet': 'volume',
|
|
|
|
|
'nm-applet': 'network',
|
|
|
|
|
'gnome-power-manager': 'battery',
|
|
|
|
|
'keyboard': 'keyboard',
|
|
|
|
|
'a11y-keyboard': 'a11y',
|
|
|
|
|
'kbd-scrolllock': 'keyboard',
|
|
|
|
|
'kbd-numlock': 'keyboard',
|
|
|
|
|
'kbd-capslock': 'keyboard',
|
|
|
|
|
'ibus-ui-gtk': 'keyboard'
|
|
|
|
|
};
|
|
|
|
|
|
2011-11-20 17:56:27 +00:00
|
|
|
|
const NotificationDaemon = new Lang.Class({
|
|
|
|
|
Name: 'NotificationDaemon',
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
|
|
|
|
_init: function() {
|
2011-08-16 12:26:49 +00:00
|
|
|
|
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(NotificationDaemonIface, this);
|
|
|
|
|
this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications');
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
this._sources = [];
|
2010-09-02 14:45:53 +00:00
|
|
|
|
this._senderToPid = {};
|
|
|
|
|
this._notifications = {};
|
|
|
|
|
this._busProxy = new Bus();
|
2010-02-21 05:25:23 +00:00
|
|
|
|
|
2012-07-24 17:47:39 +00:00
|
|
|
|
this._trayManager = new Shell.TrayManager();
|
2012-10-06 14:10:03 +00:00
|
|
|
|
this._trayIconAddedId = this._trayManager.connect('tray-icon-added', Lang.bind(this, this._onTrayIconAdded));
|
|
|
|
|
this._trayIconRemovedId = this._trayManager.connect('tray-icon-removed', Lang.bind(this, this._onTrayIconRemoved));
|
2010-08-09 16:33:34 +00:00
|
|
|
|
|
2010-02-21 05:25:23 +00:00
|
|
|
|
Shell.WindowTracker.get_default().connect('notify::focus-app',
|
|
|
|
|
Lang.bind(this, this._onFocusAppChanged));
|
2010-02-24 20:46:00 +00:00
|
|
|
|
Main.overview.connect('hidden',
|
|
|
|
|
Lang.bind(this, this._onFocusAppChanged));
|
2012-07-24 17:47:39 +00:00
|
|
|
|
|
|
|
|
|
this._trayManager.manage_stage(global.stage, Main.messageTray.actor);
|
2010-01-13 20:05:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
2012-12-30 16:45:43 +00:00
|
|
|
|
_imageForNotificationData: function(hints) {
|
|
|
|
|
if (hints['image-data']) {
|
|
|
|
|
let [width, height, rowStride, hasAlpha,
|
|
|
|
|
bitsPerSample, nChannels, data] = hints['image-data'];
|
|
|
|
|
return Shell.util_create_pixbuf_from_data(data, GdkPixbuf.Colorspace.RGB, hasAlpha,
|
|
|
|
|
bitsPerSample, width, height, rowStride);
|
|
|
|
|
} else if (hints['image-path']) {
|
|
|
|
|
return new Gio.FileIcon({ file: Gio.File.new_for_path(hints['image-path']) });
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
2012-12-30 16:31:11 +00:00
|
|
|
|
_fallbackIconForNotificationData: function(hints) {
|
|
|
|
|
let stockIcon;
|
|
|
|
|
switch (hints.urgency) {
|
|
|
|
|
case Urgency.LOW:
|
|
|
|
|
case Urgency.NORMAL:
|
|
|
|
|
stockIcon = 'gtk-dialog-info';
|
|
|
|
|
break;
|
|
|
|
|
case Urgency.CRITICAL:
|
|
|
|
|
stockIcon = 'gtk-dialog-error';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
return new Gio.ThemedIcon({ name: stockIcon });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_iconForNotificationData: function(icon) {
|
2010-08-05 17:09:27 +00:00
|
|
|
|
if (icon) {
|
|
|
|
|
if (icon.substr(0, 7) == 'file://')
|
2012-07-19 13:05:17 +00:00
|
|
|
|
return new Gio.FileIcon({ file: Gio.File.new_for_uri(icon) });
|
2012-12-30 16:25:28 +00:00
|
|
|
|
else if (icon[0] == '/')
|
2012-07-19 13:05:17 +00:00
|
|
|
|
return new Gio.FileIcon({ file: Gio.File.new_for_path(icon) });
|
2012-12-30 16:25:28 +00:00
|
|
|
|
else
|
2012-07-19 13:05:17 +00:00
|
|
|
|
return new Gio.ThemedIcon({ name: icon });
|
2010-08-05 17:09:27 +00:00
|
|
|
|
}
|
2012-12-30 16:31:11 +00:00
|
|
|
|
return null;
|
2010-08-05 17:09:27 +00:00
|
|
|
|
},
|
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
_lookupSource: function(title, pid, trayIcon) {
|
|
|
|
|
for (let i = 0; i < this._sources.length; i++) {
|
|
|
|
|
let source = this._sources[i];
|
|
|
|
|
if (source.pid == pid &&
|
|
|
|
|
(source.initialTitle == title || source.trayIcon || trayIcon))
|
|
|
|
|
return source;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
2010-12-15 21:30:50 +00:00
|
|
|
|
// Returns the source associated with ndata.notification if it is set.
|
2011-12-20 05:51:35 +00:00
|
|
|
|
// Otherwise, returns the source associated with the title and pid if
|
|
|
|
|
// such source is stored in this._sources and the notification is not
|
|
|
|
|
// transient. If the existing or requested source is associated with
|
|
|
|
|
// a tray icon and passed in pid matches a pid of an existing source,
|
|
|
|
|
// the title match is ignored to enable representing a tray icon and
|
|
|
|
|
// notifications from the same application with a single source.
|
|
|
|
|
//
|
|
|
|
|
// If no existing source is found, a new source is created as long as
|
|
|
|
|
// pid is provided.
|
2010-12-15 21:30:50 +00:00
|
|
|
|
//
|
|
|
|
|
// Either a pid or ndata.notification is needed to retrieve or
|
|
|
|
|
// create a source.
|
2011-12-20 05:51:35 +00:00
|
|
|
|
_getSource: function(title, pid, ndata, sender, trayIcon) {
|
2010-12-15 21:30:50 +00:00
|
|
|
|
if (!pid && !(ndata && ndata.notification))
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
// We use notification's source for the notifications we still have
|
|
|
|
|
// around that are getting replaced because we don't keep sources
|
|
|
|
|
// for transient notifications in this._sources, but we still want
|
|
|
|
|
// the notification associated with them to get replaced correctly.
|
|
|
|
|
if (ndata && ndata.notification)
|
|
|
|
|
return ndata.notification.source;
|
|
|
|
|
|
|
|
|
|
let isForTransientNotification = (ndata && ndata.hints['transient'] == true);
|
|
|
|
|
|
|
|
|
|
// We don't want to override a persistent notification
|
|
|
|
|
// with a transient one from the same sender, so we
|
|
|
|
|
// always create a new source object for new transient notifications
|
|
|
|
|
// and never add it to this._sources .
|
2011-12-20 05:51:35 +00:00
|
|
|
|
if (!isForTransientNotification) {
|
|
|
|
|
let source = this._lookupSource(title, pid, trayIcon);
|
|
|
|
|
if (source) {
|
|
|
|
|
source.setTitle(title);
|
|
|
|
|
return source;
|
|
|
|
|
}
|
2011-06-08 06:27:57 +00:00
|
|
|
|
}
|
2010-12-15 21:30:50 +00:00
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
let source = new Source(title, pid, sender, trayIcon);
|
2010-12-15 21:30:50 +00:00
|
|
|
|
source.setTransient(isForTransientNotification);
|
2010-08-09 16:33:34 +00:00
|
|
|
|
|
2010-12-15 21:30:50 +00:00
|
|
|
|
if (!isForTransientNotification) {
|
2011-12-20 05:51:35 +00:00
|
|
|
|
this._sources.push(source);
|
2010-12-15 21:30:50 +00:00
|
|
|
|
source.connect('destroy', Lang.bind(this,
|
|
|
|
|
function() {
|
2011-12-20 05:51:35 +00:00
|
|
|
|
let index = this._sources.indexOf(source);
|
|
|
|
|
if (index >= 0)
|
|
|
|
|
this._sources.splice(index, 1);
|
2010-12-15 21:30:50 +00:00
|
|
|
|
}));
|
|
|
|
|
}
|
2010-08-09 16:33:34 +00:00
|
|
|
|
|
|
|
|
|
Main.messageTray.add(source);
|
|
|
|
|
return source;
|
|
|
|
|
},
|
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
NotifyAsync: function(params, invocation) {
|
|
|
|
|
let [appName, replacesId, icon, summary, body, actions, hints, timeout] = params;
|
2010-09-02 14:45:53 +00:00
|
|
|
|
let id;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2012-05-31 09:28:41 +00:00
|
|
|
|
for (let hint in hints) {
|
|
|
|
|
// unpack the variants
|
|
|
|
|
hints[hint] = hints[hint].deep_unpack();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hints = Params.parse(hints, { urgency: Urgency.NORMAL }, true);
|
|
|
|
|
|
2011-07-11 11:09:45 +00:00
|
|
|
|
// Filter out chat, presence, calls and invitation notifications from
|
|
|
|
|
// Empathy, since we handle that information from telepathyClient.js
|
2011-03-29 08:04:47 +00:00
|
|
|
|
if (appName == 'Empathy' && (hints['category'] == 'im.received' ||
|
2011-06-30 12:05:41 +00:00
|
|
|
|
hints['category'] == 'x-empathy.im.room-invitation' ||
|
2011-07-11 11:09:45 +00:00
|
|
|
|
hints['category'] == 'x-empathy.call.incoming' ||
|
2012-05-16 15:28:05 +00:00
|
|
|
|
hints['category'] == 'x-empathy.transfer.incoming' ||
|
2011-08-26 10:27:40 +00:00
|
|
|
|
hints['category'] == 'x-empathy.im.subscription-request' ||
|
2011-03-29 08:04:47 +00:00
|
|
|
|
hints['category'] == 'presence.online' ||
|
|
|
|
|
hints['category'] == 'presence.offline')) {
|
2010-09-02 14:45:53 +00:00
|
|
|
|
// Ignore replacesId since we already sent back a
|
|
|
|
|
// NotificationClosed for that id.
|
2010-02-02 15:21:47 +00:00
|
|
|
|
id = nextNotificationId++;
|
|
|
|
|
Mainloop.idle_add(Lang.bind(this,
|
|
|
|
|
function () {
|
|
|
|
|
this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED);
|
2012-08-28 19:23:49 +00:00
|
|
|
|
return false;
|
2010-02-02 15:21:47 +00:00
|
|
|
|
}));
|
2011-08-16 12:26:49 +00:00
|
|
|
|
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
2010-02-02 15:21:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
2010-02-16 22:32:19 +00:00
|
|
|
|
let rewrites = rewriteRules[appName];
|
|
|
|
|
if (rewrites) {
|
|
|
|
|
for (let i = 0; i < rewrites.length; i++) {
|
|
|
|
|
let rule = rewrites[i];
|
|
|
|
|
if (summary.search(rule.pattern) != -1)
|
|
|
|
|
summary = summary.replace(rule.pattern, rule.replacement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-08-29 17:11:24 +00:00
|
|
|
|
// Be compatible with the various hints for image data and image path
|
|
|
|
|
// 'image-data' and 'image-path' are the latest name of these hints, introduced in 1.2
|
|
|
|
|
|
|
|
|
|
if (!hints['image-path'] && hints['image_path'])
|
|
|
|
|
hints['image-path'] = hints['image_path']; // version 1.1 of the spec
|
|
|
|
|
|
|
|
|
|
if (!hints['image-data'])
|
2011-01-19 15:30:01 +00:00
|
|
|
|
if (hints['image_data'])
|
|
|
|
|
hints['image-data'] = hints['image_data']; // version 1.1 of the spec
|
2011-08-29 17:11:24 +00:00
|
|
|
|
else if (hints['icon_data'] && !hints['image-path'])
|
|
|
|
|
// early versions of the spec; 'icon_data' should only be used if 'image-path' is not available
|
|
|
|
|
hints['image-data'] = hints['icon_data'];
|
2011-01-19 15:30:01 +00:00
|
|
|
|
|
2010-09-02 14:45:53 +00:00
|
|
|
|
let ndata = { appName: appName,
|
|
|
|
|
icon: icon,
|
|
|
|
|
summary: summary,
|
|
|
|
|
body: body,
|
|
|
|
|
actions: actions,
|
|
|
|
|
hints: hints,
|
|
|
|
|
timeout: timeout };
|
|
|
|
|
if (replacesId != 0 && this._notifications[replacesId]) {
|
|
|
|
|
ndata.id = id = replacesId;
|
|
|
|
|
ndata.notification = this._notifications[replacesId].notification;
|
|
|
|
|
} else {
|
|
|
|
|
replacesId = 0;
|
|
|
|
|
ndata.id = id = nextNotificationId++;
|
|
|
|
|
}
|
|
|
|
|
this._notifications[id] = ndata;
|
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
let sender = invocation.get_sender();
|
2010-09-02 14:45:53 +00:00
|
|
|
|
let pid = this._senderToPid[sender];
|
2010-12-15 21:30:50 +00:00
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
let source = this._getSource(appName, pid, ndata, sender, null);
|
2010-09-02 14:45:53 +00:00
|
|
|
|
|
|
|
|
|
if (source) {
|
|
|
|
|
this._notifyForSource(source, ndata);
|
2011-08-16 12:26:49 +00:00
|
|
|
|
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
2010-02-23 15:50:35 +00:00
|
|
|
|
}
|
|
|
|
|
|
2010-09-02 14:45:53 +00:00
|
|
|
|
if (replacesId) {
|
|
|
|
|
// There's already a pending call to GetConnectionUnixProcessID,
|
|
|
|
|
// which will see the new notification data when it finishes,
|
|
|
|
|
// so we don't have to do anything.
|
2011-08-16 12:26:49 +00:00
|
|
|
|
return invocation.return_value(GLib.Variant.new('(u)', [id]));;
|
2010-09-02 14:45:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
this._busProxy.GetConnectionUnixProcessIDRemote(sender, Lang.bind(this, function (result, excp) {
|
|
|
|
|
// The app may have updated or removed the notification
|
|
|
|
|
ndata = this._notifications[id];
|
|
|
|
|
if (!ndata)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (excp) {
|
|
|
|
|
logError(excp, 'Call to GetConnectionUnixProcessID failed');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let [pid] = result;
|
2011-12-20 05:51:35 +00:00
|
|
|
|
source = this._getSource(appName, pid, ndata, sender, null);
|
2011-08-16 12:26:49 +00:00
|
|
|
|
|
|
|
|
|
// We only store sender-pid entries for persistent sources.
|
|
|
|
|
// Removing the entries once the source is destroyed
|
|
|
|
|
// would result in the entries associated with transient
|
|
|
|
|
// sources removed once the notification is shown anyway.
|
|
|
|
|
// However, keeping these pairs would mean that we would
|
|
|
|
|
// possibly remove an entry associated with a persistent
|
|
|
|
|
// source when a transient source for the same sender is
|
|
|
|
|
// distroyed.
|
|
|
|
|
if (!source.isTransient) {
|
|
|
|
|
this._senderToPid[sender] = pid;
|
|
|
|
|
source.connect('destroy', Lang.bind(this, function() {
|
|
|
|
|
delete this._senderToPid[sender];
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
this._notifyForSource(source, ndata);
|
|
|
|
|
}));
|
2010-09-02 14:45:53 +00:00
|
|
|
|
|
2011-08-16 12:26:49 +00:00
|
|
|
|
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
2010-09-02 14:45:53 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_notifyForSource: function(source, ndata) {
|
|
|
|
|
let [id, icon, summary, body, actions, hints, notification] =
|
|
|
|
|
[ndata.id, ndata.icon, ndata.summary, ndata.body,
|
|
|
|
|
ndata.actions, ndata.hints, ndata.notification];
|
|
|
|
|
|
2010-02-22 22:19:32 +00:00
|
|
|
|
if (notification == null) {
|
2012-12-30 16:37:43 +00:00
|
|
|
|
notification = new MessageTray.Notification(source);
|
2010-09-02 14:45:53 +00:00
|
|
|
|
ndata.notification = notification;
|
2010-07-14 21:07:06 +00:00
|
|
|
|
notification.connect('destroy', Lang.bind(this,
|
2011-01-27 23:26:53 +00:00
|
|
|
|
function(n, reason) {
|
2011-08-25 13:20:00 +00:00
|
|
|
|
delete this._notifications[ndata.id];
|
2011-01-27 23:26:53 +00:00
|
|
|
|
let notificationClosedReason;
|
|
|
|
|
switch (reason) {
|
|
|
|
|
case MessageTray.NotificationDestroyedReason.EXPIRED:
|
|
|
|
|
notificationClosedReason = NotificationClosedReason.EXPIRED;
|
|
|
|
|
break;
|
|
|
|
|
case MessageTray.NotificationDestroyedReason.DISMISSED:
|
|
|
|
|
notificationClosedReason = NotificationClosedReason.DISMISSED;
|
|
|
|
|
break;
|
|
|
|
|
case MessageTray.NotificationDestroyedReason.SOURCE_CLOSED:
|
|
|
|
|
notificationClosedReason = NotificationClosedReason.APP_CLOSED;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2011-08-25 13:20:00 +00:00
|
|
|
|
this._emitNotificationClosed(ndata.id, notificationClosedReason);
|
2010-07-14 21:07:06 +00:00
|
|
|
|
}));
|
2010-12-16 20:49:47 +00:00
|
|
|
|
notification.connect('action-invoked', Lang.bind(this,
|
|
|
|
|
function(n, actionId) {
|
2011-08-25 13:20:00 +00:00
|
|
|
|
this._emitActionInvoked(ndata.id, actionId);
|
2010-12-16 20:49:47 +00:00
|
|
|
|
}));
|
2010-02-22 22:19:32 +00:00
|
|
|
|
}
|
2011-08-29 17:11:24 +00:00
|
|
|
|
|
2012-12-30 16:31:11 +00:00
|
|
|
|
let gicon = this._iconForNotificationData(icon, hints);
|
|
|
|
|
let gimage = this._imageForNotificationData(hints);
|
|
|
|
|
|
|
|
|
|
let image = null;
|
|
|
|
|
|
|
|
|
|
// If an icon is not specified, we use 'image-data' or 'image-path' hint for an icon
|
|
|
|
|
// and don't show a large image. There are currently many applications that use
|
|
|
|
|
// notify_notification_set_icon_from_pixbuf() from libnotify, which in turn sets
|
|
|
|
|
// the 'image-data' hint. These applications don't typically pass in 'app_icon'
|
|
|
|
|
// argument to Notify() and actually expect the pixbuf to be shown as an icon.
|
|
|
|
|
// So the logic here does the right thing for this case. If both an icon and either
|
|
|
|
|
// one of 'image-data' or 'image-path' are specified, we show both an icon and
|
|
|
|
|
// a large image.
|
|
|
|
|
if (gicon && gimage)
|
|
|
|
|
image = new St.Icon({ gicon: gimage });
|
|
|
|
|
else if (!gicon && gimage)
|
|
|
|
|
gicon = gimage;
|
|
|
|
|
else if (!gicon)
|
|
|
|
|
gicon = this._fallbackIconForNotificationData(hints);
|
|
|
|
|
|
|
|
|
|
notification.setImage(image);
|
|
|
|
|
|
2012-12-30 16:37:43 +00:00
|
|
|
|
notification.update(summary, body, { gicon: gicon,
|
|
|
|
|
bannerMarkup: true,
|
|
|
|
|
clear: true });
|
|
|
|
|
|
2010-02-01 20:41:22 +00:00
|
|
|
|
if (actions.length) {
|
2010-10-30 06:29:21 +00:00
|
|
|
|
notification.setUseActionIcons(hints['action-icons'] == true);
|
2011-08-02 13:42:45 +00:00
|
|
|
|
for (let i = 0; i < actions.length - 1; i += 2) {
|
|
|
|
|
if (actions[i] == 'default')
|
|
|
|
|
notification.connect('clicked', Lang.bind(this,
|
|
|
|
|
function() {
|
2011-08-25 13:20:00 +00:00
|
|
|
|
this._emitActionInvoked(ndata.id, "default");
|
2011-08-03 19:21:00 +00:00
|
|
|
|
}));
|
2011-08-02 13:42:45 +00:00
|
|
|
|
else
|
|
|
|
|
notification.addButton(actions[i], actions[i + 1]);
|
|
|
|
|
}
|
2010-02-01 20:41:22 +00:00
|
|
|
|
}
|
2011-01-04 09:34:57 +00:00
|
|
|
|
switch (hints.urgency) {
|
|
|
|
|
case Urgency.LOW:
|
|
|
|
|
notification.setUrgency(MessageTray.Urgency.LOW);
|
|
|
|
|
break;
|
|
|
|
|
case Urgency.NORMAL:
|
|
|
|
|
notification.setUrgency(MessageTray.Urgency.NORMAL);
|
|
|
|
|
break;
|
|
|
|
|
case Urgency.CRITICAL:
|
|
|
|
|
notification.setUrgency(MessageTray.Urgency.CRITICAL);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2010-12-16 20:49:47 +00:00
|
|
|
|
notification.setResident(hints.resident == true);
|
2010-12-15 21:30:50 +00:00
|
|
|
|
// 'transient' is a reserved keyword in JS, so we have to retrieve the value
|
|
|
|
|
// of the 'transient' hint with hints['transient'] rather than hints.transient
|
|
|
|
|
notification.setTransient(hints['transient'] == true);
|
2010-04-28 19:34:27 +00:00
|
|
|
|
|
2012-12-30 16:29:32 +00:00
|
|
|
|
let sourceGIcon = source.useNotificationIcon ? gicon : null;
|
2012-07-19 13:05:17 +00:00
|
|
|
|
source.processNotification(notification, sourceGIcon);
|
2010-01-13 20:05:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
CloseNotification: function(id) {
|
2010-09-02 14:45:53 +00:00
|
|
|
|
let ndata = this._notifications[id];
|
|
|
|
|
if (ndata) {
|
|
|
|
|
if (ndata.notification)
|
2011-01-27 23:26:53 +00:00
|
|
|
|
ndata.notification.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
|
2010-09-02 14:45:53 +00:00
|
|
|
|
delete this._notifications[id];
|
|
|
|
|
}
|
2010-01-13 20:05:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
GetCapabilities: function() {
|
|
|
|
|
return [
|
2010-02-01 20:41:22 +00:00
|
|
|
|
'actions',
|
2010-10-30 06:29:21 +00:00
|
|
|
|
'action-icons',
|
2010-01-13 20:05:20 +00:00
|
|
|
|
'body',
|
|
|
|
|
// 'body-hyperlinks',
|
|
|
|
|
// 'body-images',
|
|
|
|
|
'body-markup',
|
|
|
|
|
// 'icon-multi',
|
2010-06-11 21:19:18 +00:00
|
|
|
|
'icon-static',
|
2010-10-09 02:34:04 +00:00
|
|
|
|
'persistence',
|
2010-01-13 20:05:20 +00:00
|
|
|
|
// 'sound',
|
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
GetServerInformation: function() {
|
|
|
|
|
return [
|
2011-01-11 21:34:09 +00:00
|
|
|
|
Config.PACKAGE_NAME,
|
2010-01-13 20:05:20 +00:00
|
|
|
|
'GNOME',
|
2011-01-11 21:34:09 +00:00
|
|
|
|
Config.PACKAGE_VERSION,
|
|
|
|
|
'1.2'
|
2010-01-13 20:05:20 +00:00
|
|
|
|
];
|
|
|
|
|
},
|
|
|
|
|
|
2010-02-24 20:46:00 +00:00
|
|
|
|
_onFocusAppChanged: function() {
|
|
|
|
|
let tracker = Shell.WindowTracker.get_default();
|
2010-08-09 17:18:15 +00:00
|
|
|
|
if (!tracker.focus_app)
|
|
|
|
|
return;
|
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
for (let i = 0; i < this._sources.length; i++) {
|
|
|
|
|
let source = this._sources[i];
|
2010-08-09 17:18:15 +00:00
|
|
|
|
if (source.app == tracker.focus_app) {
|
2011-03-21 21:43:34 +00:00
|
|
|
|
source.destroyNonResidentNotifications();
|
2010-08-09 17:18:15 +00:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
2010-02-21 05:25:23 +00:00
|
|
|
|
},
|
|
|
|
|
|
2010-01-13 20:05:20 +00:00
|
|
|
|
_emitNotificationClosed: function(id, reason) {
|
2011-08-16 12:26:49 +00:00
|
|
|
|
this._dbusImpl.emit_signal('NotificationClosed',
|
|
|
|
|
GLib.Variant.new('(uu)', [id, reason]));
|
2010-02-01 20:41:22 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_emitActionInvoked: function(id, action) {
|
2011-08-16 12:26:49 +00:00
|
|
|
|
this._dbusImpl.emit_signal('ActionInvoked',
|
|
|
|
|
GLib.Variant.new('(us)', [id, action]));
|
2010-08-09 16:33:34 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_onTrayIconAdded: function(o, icon) {
|
2012-09-10 13:43:24 +00:00
|
|
|
|
let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : '';
|
2012-07-24 17:47:39 +00:00
|
|
|
|
if (STANDARD_TRAY_ICON_IMPLEMENTATIONS[wmClass] !== undefined)
|
|
|
|
|
return;
|
|
|
|
|
|
2012-06-01 12:44:40 +00:00
|
|
|
|
let source = this._getSource(icon.title || icon.wm_class || C_("program", "Unknown"), icon.pid, null, null, icon);
|
2010-08-09 16:33:34 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
_onTrayIconRemoved: function(o, icon) {
|
2012-01-19 17:33:15 +00:00
|
|
|
|
let source = this._lookupSource(null, icon.pid, true);
|
2010-08-09 16:33:34 +00:00
|
|
|
|
if (source)
|
|
|
|
|
source.destroy();
|
2010-01-13 20:05:20 +00:00
|
|
|
|
}
|
2011-11-20 17:56:27 +00:00
|
|
|
|
});
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-11-20 15:12:02 +00:00
|
|
|
|
const Source = new Lang.Class({
|
|
|
|
|
Name: 'NotificationDaemonSource',
|
|
|
|
|
Extends: MessageTray.Source,
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
_init: function(title, pid, sender, trayIcon) {
|
2011-11-20 15:12:02 +00:00
|
|
|
|
this.parent(title);
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
2011-12-20 05:51:35 +00:00
|
|
|
|
this.initialTitle = title;
|
|
|
|
|
|
|
|
|
|
this.pid = pid;
|
2011-07-06 21:01:07 +00:00
|
|
|
|
if (sender)
|
2011-08-16 12:26:49 +00:00
|
|
|
|
this._nameWatcherId = Gio.DBus.session.watch_name(sender,
|
|
|
|
|
Gio.BusNameWatcherFlags.NONE,
|
|
|
|
|
null,
|
|
|
|
|
Lang.bind(this, this._onNameVanished));
|
|
|
|
|
else
|
|
|
|
|
this._nameWatcherId = 0;
|
2011-07-06 21:01:07 +00:00
|
|
|
|
|
2010-08-09 16:33:34 +00:00
|
|
|
|
this._setApp();
|
|
|
|
|
if (this.app)
|
2010-09-02 14:45:53 +00:00
|
|
|
|
this.title = this.app.get_name();
|
2010-08-09 16:33:34 +00:00
|
|
|
|
else
|
|
|
|
|
this.useNotificationIcon = true;
|
2011-12-20 05:51:35 +00:00
|
|
|
|
|
|
|
|
|
this.trayIcon = trayIcon;
|
|
|
|
|
if (this.trayIcon) {
|
|
|
|
|
this._setSummaryIcon(this.trayIcon);
|
|
|
|
|
this.useNotificationIcon = false;
|
|
|
|
|
}
|
2010-01-13 20:05:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
2011-07-06 21:01:07 +00:00
|
|
|
|
_onNameVanished: function() {
|
|
|
|
|
// Destroy the notification source when its sender is removed from DBus.
|
2011-07-27 21:08:08 +00:00
|
|
|
|
// Only do so if this.app is set to avoid removing "notify-send" sources, senders
|
|
|
|
|
// of which аre removed from DBus immediately.
|
2011-07-06 21:01:07 +00:00
|
|
|
|
// Sender being removed from DBus would normally result in a tray icon being removed,
|
|
|
|
|
// so allow the code path that handles the tray icon being removed to handle that case.
|
2011-07-27 21:08:08 +00:00
|
|
|
|
if (!this.trayIcon && this.app)
|
2011-07-06 21:01:07 +00:00
|
|
|
|
this.destroy();
|
|
|
|
|
},
|
|
|
|
|
|
2012-07-19 13:05:17 +00:00
|
|
|
|
processNotification: function(notification, gicon) {
|
|
|
|
|
if (gicon)
|
|
|
|
|
this._gicon = gicon;
|
2012-07-23 04:07:51 +00:00
|
|
|
|
if (!this.trayIcon)
|
|
|
|
|
this.iconUpdated();
|
2011-03-21 21:26:04 +00:00
|
|
|
|
|
|
|
|
|
let tracker = Shell.WindowTracker.get_default();
|
|
|
|
|
if (notification.resident && this.app && tracker.focus_app == this.app)
|
|
|
|
|
this.pushNotification(notification);
|
|
|
|
|
else
|
|
|
|
|
this.notify(notification);
|
2010-02-15 17:27:28 +00:00
|
|
|
|
},
|
|
|
|
|
|
2011-01-13 20:04:37 +00:00
|
|
|
|
handleSummaryClick: function() {
|
2011-12-20 05:51:35 +00:00
|
|
|
|
if (!this.trayIcon)
|
2011-01-13 20:04:37 +00:00
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
let event = Clutter.get_current_event();
|
|
|
|
|
if (event.type() != Clutter.EventType.BUTTON_RELEASE)
|
|
|
|
|
return false;
|
|
|
|
|
|
2011-03-26 14:57:23 +00:00
|
|
|
|
// Left clicks are passed through only where there aren't unacknowledged
|
|
|
|
|
// notifications, so it possible to open them in summary mode; right
|
|
|
|
|
// clicks are always forwarded, as the right click menu is not useful for
|
|
|
|
|
// tray icons
|
|
|
|
|
if (event.get_button() == 1 &&
|
|
|
|
|
this.notifications.length > 0)
|
|
|
|
|
return false;
|
|
|
|
|
|
2012-08-31 16:43:00 +00:00
|
|
|
|
let id = global.connect('notify::stage-input-mode', Lang.bind(this, function () {
|
|
|
|
|
global.disconnect(id);
|
2011-12-20 05:51:35 +00:00
|
|
|
|
this.trayIcon.click(event);
|
2012-08-31 16:43:00 +00:00
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
Main.overview.hide();
|
2011-01-13 20:04:37 +00:00
|
|
|
|
return true;
|
|
|
|
|
},
|
|
|
|
|
|
2012-04-09 01:39:59 +00:00
|
|
|
|
_getApp: function() {
|
|
|
|
|
let app;
|
|
|
|
|
|
|
|
|
|
app = Shell.WindowTracker.get_default().get_app_from_pid(this.pid);
|
|
|
|
|
if (app != null)
|
|
|
|
|
return app;
|
|
|
|
|
|
|
|
|
|
if (this.trayIcon) {
|
|
|
|
|
app = Shell.AppSystem.get_default().lookup_wmclass(this.trayIcon.wmclass);
|
|
|
|
|
if (app != null)
|
|
|
|
|
return app;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
},
|
|
|
|
|
|
2010-08-09 16:33:34 +00:00
|
|
|
|
_setApp: function() {
|
2010-12-16 20:49:47 +00:00
|
|
|
|
if (this.app)
|
|
|
|
|
return;
|
|
|
|
|
|
2012-04-09 01:39:59 +00:00
|
|
|
|
this.app = this._getApp();
|
2010-08-09 16:33:34 +00:00
|
|
|
|
if (!this.app)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Only override the icon if we were previously using
|
2011-01-07 15:22:28 +00:00
|
|
|
|
// notification-based icons (ie, not a trayicon) or if it was unset before
|
2011-12-20 05:51:35 +00:00
|
|
|
|
if (!this.trayIcon) {
|
2010-08-09 16:33:34 +00:00
|
|
|
|
this.useNotificationIcon = false;
|
2012-07-23 04:07:51 +00:00
|
|
|
|
this.iconUpdated();
|
2010-08-09 16:33:34 +00:00
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2012-08-06 14:34:20 +00:00
|
|
|
|
setTitle: function(title) {
|
|
|
|
|
// Do nothing if .app is set, we don't want to override the
|
|
|
|
|
// app name with whatever is provided through libnotify (usually
|
|
|
|
|
// garbage)
|
|
|
|
|
if (this.app)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
this.parent(title);
|
|
|
|
|
},
|
|
|
|
|
|
2011-02-11 19:43:01 +00:00
|
|
|
|
open: function(notification) {
|
2011-03-21 21:43:34 +00:00
|
|
|
|
this.destroyNonResidentNotifications();
|
2010-02-15 17:27:28 +00:00
|
|
|
|
this.openApp();
|
2010-09-30 20:56:50 +00:00
|
|
|
|
},
|
|
|
|
|
|
2011-03-21 21:43:34 +00:00
|
|
|
|
_lastNotificationRemoved: function() {
|
2011-12-20 05:51:35 +00:00
|
|
|
|
if (!this.trayIcon)
|
2010-09-30 20:56:50 +00:00
|
|
|
|
this.destroy();
|
2010-02-15 17:27:28 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
openApp: function() {
|
2010-09-02 14:45:53 +00:00
|
|
|
|
if (this.app == null)
|
2010-02-15 17:27:28 +00:00
|
|
|
|
return;
|
2010-09-02 14:45:53 +00:00
|
|
|
|
|
2010-02-15 17:27:28 +00:00
|
|
|
|
let windows = this.app.get_windows();
|
|
|
|
|
if (windows.length > 0) {
|
|
|
|
|
let mostRecentWindow = windows[0];
|
|
|
|
|
Main.activateWindow(mostRecentWindow);
|
|
|
|
|
}
|
2011-03-07 23:10:10 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
destroy: function() {
|
2011-08-16 12:26:49 +00:00
|
|
|
|
if (this._nameWatcherId) {
|
|
|
|
|
Gio.DBus.session.unwatch_name(this._nameWatcherId);
|
|
|
|
|
this._nameWatcherId = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2011-11-20 15:12:02 +00:00
|
|
|
|
this.parent();
|
2011-10-08 22:00:32 +00:00
|
|
|
|
},
|
|
|
|
|
|
2012-07-19 13:05:17 +00:00
|
|
|
|
createIcon: function(size) {
|
|
|
|
|
if (this.trayIcon) {
|
|
|
|
|
return new Clutter.Clone({ width: size,
|
|
|
|
|
height: size,
|
|
|
|
|
source: this.trayIcon });
|
|
|
|
|
} else if (this.app) {
|
|
|
|
|
return this.app.create_icon_texture(size);
|
|
|
|
|
} else if (this._gicon) {
|
|
|
|
|
return new St.Icon({ gicon: this._gicon,
|
|
|
|
|
icon_size: size });
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2010-01-13 20:05:20 +00:00
|
|
|
|
}
|
2011-11-20 15:12:02 +00:00
|
|
|
|
});
|