2010-01-13 20:05:20 +00:00
|
|
|
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
|
|
|
|
const DBus = imports.dbus;
|
|
|
|
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
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
|
|
|
const MessageTray = imports.ui.messageTray;
|
|
|
|
const Params = imports.misc.params;
|
|
|
|
|
|
|
|
let nextNotificationId = 1;
|
|
|
|
|
2010-02-15 17:27:28 +00:00
|
|
|
// Should really be defined in dbus.js
|
|
|
|
const BusIface = {
|
|
|
|
name: 'org.freedesktop.DBus',
|
|
|
|
methods: [{ name: 'GetConnectionUnixProcessID',
|
|
|
|
inSignature: 's',
|
|
|
|
outSignature: 'i' }]
|
2010-03-15 13:50:05 +00:00
|
|
|
};
|
2010-02-15 17:27:28 +00:00
|
|
|
|
|
|
|
const Bus = function () {
|
|
|
|
this._init();
|
2010-03-15 13:50:05 +00:00
|
|
|
};
|
2010-02-15 17:27:28 +00:00
|
|
|
|
|
|
|
Bus.prototype = {
|
|
|
|
_init: function() {
|
|
|
|
DBus.session.proxifyObject(this, 'org.freedesktop.DBus', '/org/freedesktop/DBus');
|
|
|
|
}
|
2010-03-15 13:50:05 +00:00
|
|
|
};
|
2010-02-15 17:27:28 +00:00
|
|
|
|
|
|
|
DBus.proxifyPrototype(Bus.prototype, BusIface);
|
|
|
|
|
2010-01-13 20:05:20 +00:00
|
|
|
const NotificationDaemonIface = {
|
|
|
|
name: 'org.freedesktop.Notifications',
|
|
|
|
methods: [{ name: 'Notify',
|
|
|
|
inSignature: 'susssasa{sv}i',
|
|
|
|
outSignature: 'u'
|
|
|
|
},
|
|
|
|
{ name: 'CloseNotification',
|
|
|
|
inSignature: 'u',
|
|
|
|
outSignature: ''
|
|
|
|
},
|
|
|
|
{ name: 'GetCapabilities',
|
|
|
|
inSignature: '',
|
|
|
|
outSignature: 'as'
|
|
|
|
},
|
|
|
|
{ name: 'GetServerInformation',
|
|
|
|
inSignature: '',
|
|
|
|
outSignature: 'ssss'
|
|
|
|
}],
|
|
|
|
signals: [{ name: 'NotificationClosed',
|
|
|
|
inSignature: 'uu' },
|
|
|
|
{ name: 'ActionInvoked',
|
|
|
|
inSignature: 'us' }]
|
|
|
|
};
|
|
|
|
|
|
|
|
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*) \(.*\)$/,
|
|
|
|
replacement: '<$1>' },
|
|
|
|
{ pattern: /^XChat: New public message from: (\S*) \((.*)\)$/,
|
|
|
|
replacement: '$2 <$1>' },
|
|
|
|
{ pattern: /^XChat: Highlighted message from: (\S*) \((.*)\)$/,
|
|
|
|
replacement: '$2 <$1>' }
|
|
|
|
]
|
|
|
|
};
|
2010-06-23 19:20:39 +00:00
|
|
|
|
|
|
|
// The notification spec stipulates using formal names for the appName the applications
|
|
|
|
// pass in. However, not all applications do that. Here is a list of the offenders we
|
|
|
|
// encountered so far.
|
|
|
|
const appNameMap = {
|
|
|
|
'evolution-mail-notification': 'Evolution Mail',
|
|
|
|
'rhythmbox': 'Rhythmbox'
|
|
|
|
};
|
|
|
|
|
2010-01-13 20:05:20 +00:00
|
|
|
function NotificationDaemon() {
|
|
|
|
this._init();
|
|
|
|
}
|
|
|
|
|
|
|
|
NotificationDaemon.prototype = {
|
|
|
|
_init: function() {
|
|
|
|
DBus.session.exportObject('/org/freedesktop/Notifications', this);
|
|
|
|
|
|
|
|
this._everAcquiredName = false;
|
|
|
|
DBus.session.acquire_name('org.freedesktop.Notifications',
|
|
|
|
// We pass MANY_INSTANCES so that if
|
|
|
|
// notification-daemon is running, we'll
|
|
|
|
// get queued behind it and then get the
|
|
|
|
// name after killing it below
|
|
|
|
DBus.MANY_INSTANCES,
|
|
|
|
Lang.bind(this, this._acquiredName),
|
|
|
|
Lang.bind(this, this._lostName));
|
2010-02-22 22:19:32 +00:00
|
|
|
|
|
|
|
this._currentNotifications = {};
|
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));
|
2010-01-13 20:05:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_acquiredName: function() {
|
|
|
|
this._everAcquiredName = true;
|
|
|
|
},
|
|
|
|
|
|
|
|
_lostName: function() {
|
|
|
|
if (this._everAcquiredName)
|
|
|
|
log('Lost name org.freedesktop.Notifications!');
|
|
|
|
else if (GLib.getenv('GNOME_SHELL_NO_REPLACE'))
|
|
|
|
log('Failed to acquire org.freedesktop.Notifications');
|
|
|
|
else {
|
|
|
|
log('Failed to acquire org.freedesktop.Notifications; trying again');
|
|
|
|
|
|
|
|
// kill the notification-daemon. pkill is more portable
|
|
|
|
// than killall, but on Linux at least it won't match if
|
|
|
|
// you pass more than 15 characters of the process name...
|
2010-05-13 19:46:04 +00:00
|
|
|
// However, if you use the '-f' flag to match the entire
|
2010-01-13 20:05:20 +00:00
|
|
|
// command line, it will work, but we have to be careful
|
2010-05-13 19:46:04 +00:00
|
|
|
// in that case that we don't match 'gedit
|
|
|
|
// notification-daemon.c' or whatever...
|
2010-01-13 20:05:20 +00:00
|
|
|
let p = new Shell.Process({ args: ['pkill', '-f',
|
|
|
|
'^([^ ]*/)?(notification-daemon|notify-osd)$']});
|
|
|
|
p.run();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_sourceId: function(id) {
|
2010-02-22 22:19:32 +00:00
|
|
|
return 'source-' + id;
|
2010-01-13 20:05:20 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
Notify: function(appName, replacesId, icon, summary, body,
|
|
|
|
actions, hints, timeout) {
|
2010-02-23 15:50:35 +00:00
|
|
|
let source = Main.messageTray.getSource(this._sourceId(appName));
|
|
|
|
let id = null;
|
2010-01-13 20:05:20 +00:00
|
|
|
|
2010-02-02 15:21:47 +00:00
|
|
|
// Filter out notifications from Empathy, since we
|
|
|
|
// handle that information from telepathyClient.js
|
|
|
|
if (appName == 'Empathy') {
|
|
|
|
id = nextNotificationId++;
|
|
|
|
Mainloop.idle_add(Lang.bind(this,
|
|
|
|
function () {
|
|
|
|
this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED);
|
|
|
|
}));
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2010-04-28 19:34:27 +00:00
|
|
|
hints = Params.parse(hints, { urgency: Urgency.NORMAL }, true);
|
|
|
|
|
|
|
|
// Source may be null if we have never received a notification
|
|
|
|
// from this app or if all notifications from this app have
|
|
|
|
// been acknowledged.
|
2010-01-13 20:05:20 +00:00
|
|
|
if (source == null) {
|
2010-06-23 19:20:39 +00:00
|
|
|
let title = appNameMap[appName] || appName;
|
|
|
|
source = new Source(this._sourceId(appName), title, icon, hints);
|
2010-01-13 20:05:20 +00:00
|
|
|
Main.messageTray.add(source);
|
|
|
|
|
|
|
|
source.connect('clicked', Lang.bind(this,
|
|
|
|
function() {
|
|
|
|
source.destroy();
|
|
|
|
}));
|
2010-02-15 17:27:28 +00:00
|
|
|
|
|
|
|
let sender = DBus.getCurrentMessageContext().sender;
|
|
|
|
let busProxy = new Bus();
|
|
|
|
busProxy.GetConnectionUnixProcessIDRemote(sender, function (result, excp) {
|
|
|
|
let app = Shell.WindowTracker.get_default().get_app_from_pid(result);
|
2010-03-31 13:21:54 +00:00
|
|
|
if (app)
|
|
|
|
source.setApp(app);
|
2010-02-15 17:27:28 +00:00
|
|
|
});
|
2010-02-22 22:19:32 +00:00
|
|
|
} else {
|
|
|
|
source.update(icon, hints);
|
2010-01-13 20:05:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
summary = GLib.markup_escape_text(summary, -1);
|
2010-02-01 20:23:49 +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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-02-23 15:50:35 +00:00
|
|
|
let notification;
|
|
|
|
if (replacesId != 0) {
|
|
|
|
id = replacesId;
|
|
|
|
notification = this._currentNotifications[id];
|
|
|
|
}
|
|
|
|
|
2010-02-22 22:19:32 +00:00
|
|
|
if (notification == null) {
|
2010-02-23 15:50:35 +00:00
|
|
|
id = nextNotificationId++;
|
2010-02-22 22:19:32 +00:00
|
|
|
notification = new MessageTray.Notification(id, source, summary, body, true);
|
|
|
|
this._currentNotifications[id] = notification;
|
|
|
|
notification.connect('dismissed', Lang.bind(this,
|
|
|
|
function(n) {
|
|
|
|
this._emitNotificationClosed(n.id, NotificationClosedReason.DISMISSED);
|
|
|
|
}));
|
2010-07-14 21:07:06 +00:00
|
|
|
notification.connect('destroy', Lang.bind(this,
|
|
|
|
function(n) {
|
|
|
|
delete this._currentNotifications[id];
|
|
|
|
}));
|
|
|
|
notification.connect('action-invoked', Lang.bind(this, this._actionInvoked, source, id));
|
2010-02-22 22:19:32 +00:00
|
|
|
} else {
|
|
|
|
// passing in true as the last parameter will clear out extra actors,
|
|
|
|
// such as actions
|
|
|
|
notification.update(summary, body, true);
|
|
|
|
}
|
|
|
|
|
2010-02-01 20:41:22 +00:00
|
|
|
if (actions.length) {
|
|
|
|
for (let i = 0; i < actions.length - 1; i += 2)
|
2010-02-22 19:23:36 +00:00
|
|
|
notification.addButton(actions[i], actions[i + 1]);
|
2010-02-01 20:41:22 +00:00
|
|
|
}
|
|
|
|
|
2010-04-28 19:34:27 +00:00
|
|
|
notification.setUrgent(hints.urgency == Urgency.CRITICAL);
|
|
|
|
|
2010-02-01 20:23:49 +00:00
|
|
|
source.notify(notification);
|
2010-01-13 20:05:20 +00:00
|
|
|
return id;
|
|
|
|
},
|
|
|
|
|
|
|
|
CloseNotification: function(id) {
|
2010-02-22 22:19:32 +00:00
|
|
|
let notification = this._currentNotifications[id];
|
|
|
|
if (notification)
|
|
|
|
notification.destroy();
|
2010-01-13 20:05:20 +00:00
|
|
|
this._emitNotificationClosed(id, NotificationClosedReason.APP_CLOSED);
|
|
|
|
},
|
|
|
|
|
|
|
|
GetCapabilities: function() {
|
|
|
|
return [
|
2010-02-01 20:41:22 +00:00
|
|
|
'actions',
|
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-01-13 20:05:20 +00:00
|
|
|
// 'sound',
|
2010-06-11 21:19:18 +00:00
|
|
|
'x-gnome-icon-buttons'
|
2010-01-13 20:05:20 +00:00
|
|
|
];
|
|
|
|
},
|
|
|
|
|
|
|
|
GetServerInformation: function() {
|
|
|
|
return [
|
|
|
|
'GNOME Shell',
|
|
|
|
'GNOME',
|
|
|
|
'0.1', // FIXME, get this from somewhere
|
|
|
|
'1.0'
|
|
|
|
];
|
|
|
|
},
|
|
|
|
|
2010-02-24 20:46:00 +00:00
|
|
|
_onFocusAppChanged: function() {
|
|
|
|
let tracker = Shell.WindowTracker.get_default();
|
2010-04-07 19:22:08 +00:00
|
|
|
if (tracker.focus_app)
|
|
|
|
Main.messageTray.removeSourceByApp(tracker.focus_app);
|
2010-02-21 05:25:23 +00:00
|
|
|
},
|
|
|
|
|
2010-02-01 20:41:22 +00:00
|
|
|
_actionInvoked: function(notification, action, source, id) {
|
|
|
|
source.destroy();
|
2010-07-14 21:07:06 +00:00
|
|
|
this._emitActionInvoked(id, action);
|
2010-02-01 20:41:22 +00:00
|
|
|
},
|
|
|
|
|
2010-01-13 20:05:20 +00:00
|
|
|
_emitNotificationClosed: function(id, reason) {
|
|
|
|
DBus.session.emit_signal('/org/freedesktop/Notifications',
|
|
|
|
'org.freedesktop.Notifications',
|
|
|
|
'NotificationClosed', 'uu',
|
|
|
|
[id, reason]);
|
2010-02-01 20:41:22 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
_emitActionInvoked: function(id, action) {
|
|
|
|
DBus.session.emit_signal('/org/freedesktop/Notifications',
|
|
|
|
'org.freedesktop.Notifications',
|
|
|
|
'ActionInvoked', 'us',
|
|
|
|
[id, action]);
|
2010-01-13 20:05:20 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
DBus.conformExport(NotificationDaemon.prototype, NotificationDaemonIface);
|
|
|
|
|
2010-06-23 19:20:39 +00:00
|
|
|
function Source(sourceId, title, icon, hints) {
|
|
|
|
this._init(sourceId, title, icon, hints);
|
2010-01-13 20:05:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Source.prototype = {
|
|
|
|
__proto__: MessageTray.Source.prototype,
|
|
|
|
|
2010-06-23 19:20:39 +00:00
|
|
|
_init: function(sourceId, title, icon, hints) {
|
|
|
|
MessageTray.Source.prototype._init.call(this, sourceId, title);
|
2010-01-13 20:05:20 +00:00
|
|
|
|
2010-02-23 14:58:33 +00:00
|
|
|
this.app = null;
|
|
|
|
this._openAppRequested = false;
|
|
|
|
|
2010-02-22 22:19:32 +00:00
|
|
|
this.update(icon, hints);
|
|
|
|
},
|
|
|
|
|
|
|
|
update: function(icon, hints) {
|
2010-01-13 20:05:20 +00:00
|
|
|
this._icon = icon;
|
|
|
|
this._iconData = hints.icon_data;
|
|
|
|
this._urgency = hints.urgency;
|
|
|
|
},
|
|
|
|
|
|
|
|
createIcon: function(size) {
|
2010-02-09 17:42:07 +00:00
|
|
|
let textureCache = St.TextureCache.get_default();
|
2010-01-13 20:05:20 +00:00
|
|
|
|
|
|
|
if (this._icon) {
|
|
|
|
if (this._icon.substr(0, 7) == 'file://')
|
|
|
|
return textureCache.load_uri_async(this._icon, size, size);
|
|
|
|
else if (this._icon[0] == '/') {
|
|
|
|
let uri = GLib.filename_to_uri(this._icon, null);
|
|
|
|
return textureCache.load_uri_async(uri, size, size);
|
|
|
|
} else
|
|
|
|
return textureCache.load_icon_name(this._icon, size);
|
|
|
|
} else if (this._iconData) {
|
|
|
|
let [width, height, rowStride, hasAlpha,
|
|
|
|
bitsPerSample, nChannels, data] = this._iconData;
|
|
|
|
return textureCache.load_from_raw(data, data.length, hasAlpha,
|
|
|
|
width, height, rowStride, size);
|
|
|
|
} else {
|
|
|
|
let stockIcon;
|
|
|
|
switch (this._urgency) {
|
|
|
|
case Urgency.LOW:
|
|
|
|
case Urgency.NORMAL:
|
|
|
|
stockIcon = 'gtk-dialog-info';
|
|
|
|
break;
|
|
|
|
case Urgency.CRITICAL:
|
|
|
|
stockIcon = 'gtk-dialog-error';
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return textureCache.load_icon_name(stockIcon, size);
|
|
|
|
}
|
2010-02-15 17:27:28 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
clicked: function() {
|
|
|
|
this.openApp();
|
|
|
|
MessageTray.Source.prototype.clicked.call(this);
|
|
|
|
},
|
|
|
|
|
|
|
|
setApp: function(app) {
|
|
|
|
this.app = app;
|
|
|
|
if (this._openAppRequested)
|
|
|
|
this.openApp();
|
|
|
|
},
|
|
|
|
|
|
|
|
openApp: function() {
|
|
|
|
if (this.app == null) {
|
|
|
|
this._openAppRequested = true;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let windows = this.app.get_windows();
|
|
|
|
if (windows.length > 0) {
|
|
|
|
let mostRecentWindow = windows[0];
|
|
|
|
Main.activateWindow(mostRecentWindow);
|
|
|
|
}
|
|
|
|
this._openAppRequested = false;
|
2010-01-13 20:05:20 +00:00
|
|
|
}
|
|
|
|
};
|