diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 47d0b1b96..39ccc7663 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -1,8 +1,10 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ +const Clutter = imports.gi.Clutter; const Lang = imports.lang; const Mainloop = imports.mainloop; const St = imports.gi.St; +const Signals = imports.signals; const Tweener = imports.ui.tweener; const Main = imports.ui.main; @@ -68,9 +70,8 @@ Notification.prototype = { }, hideComplete: function() { - // We don't explicitly destroy the icon, since the caller may - // still want it. - this._iconBox.child = null; + if (this._iconBox.child) + this._iconBox.child.destroy(); // Don't hide the notification if we are showing a new one. if (this._hideTimeoutId == 0) @@ -78,6 +79,37 @@ Notification.prototype = { } }; +function Source(id, createIcon) { + this._init(id, createIcon); +} + +Source.prototype = { + _init: function(id, createIcon) { + this.id = id; + if (createIcon) + this.createIcon = createIcon; + }, + + // This can be overridden by a subclass, or by the createIcon + // parameter to _init() + createIcon: function(size) { + throw new Error('no implementation of createIcon in ' + this); + }, + + notify: function(text) { + Main.notificationPopup.show(this.createIcon(), text); + }, + + clicked: function() { + this.emit('clicked'); + }, + + destroy: function() { + this.emit('destroy'); + } +}; +Signals.addSignalMethods(Source.prototype); + function MessageTray() { this._init(); } @@ -105,30 +137,48 @@ MessageTray.prototype = { this._tray = new St.BoxLayout({ name: 'message-tray-inner' }); this.actor.child = this._tray; this._tray.expand = true; + this._sources = {}; this._icons = {}; }, - contains: function(id) { - return this._icons.hasOwnProperty(id); + contains: function(source) { + return this._sources.hasOwnProperty(source.id); }, - add: function(id, icon) { - if (this.contains(id)) + add: function(source) { + if (this.contains(source)) { + log('Trying to re-add source ' + source.id); return; + } - let iconBox = new St.Bin(); - iconBox.child = icon; + let iconBox = new St.Bin({ reactive: true }); + iconBox.child = source.createIcon(); this._tray.insert_actor(iconBox, 0); - this._icons[id] = iconBox; + this._icons[source.id] = iconBox; + this._sources[source.id] = source; + + iconBox.connect('button-release-event', Lang.bind(this, + function () { + source.clicked(); + })); + + source.connect('destroy', Lang.bind(this, + function () { + this.remove(source); + })); }, - remove: function(id) { - if (!this.contains(id)) + remove: function(source) { + if (!this.contains(source)) return; - this._tray.remove_actor(this._icons[id]); - this._icons[id].destroy(); - delete this._icons[id]; + this._tray.remove_actor(this._icons[source.id]); + delete this._icons[source.id]; + delete this._sources[source.id]; + }, + + getSource: function(id) { + return this._sources[id]; }, _onMessageTrayEntered: function() { diff --git a/js/ui/messaging.js b/js/ui/messaging.js index db007d794..8cfc335a1 100644 --- a/js/ui/messaging.js +++ b/js/ui/messaging.js @@ -6,8 +6,7 @@ const Shell = imports.gi.Shell; const St = imports.gi.St; const Main = imports.ui.main; - -const AVATAR_SIZE = 24; +const MessageTray = imports.ui.messageTray; const TELEPATHY = "org.freedesktop.Telepathy."; const CONN = TELEPATHY + "Connection"; @@ -386,7 +385,11 @@ function Source(conn, channelPath, channel_props, targetId) { } Source.prototype = { + __proto__: MessageTray.Source.prototype, + _init: function(conn, channelPath, targetHandle, targetId) { + MessageTray.Source.prototype._init.call(this, targetId); + let connName = nameify(conn.getPath()); this._channel = new Channel(connName, channelPath); this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed)); @@ -403,8 +406,8 @@ Source.prototype = { this._channelText.ListPendingMessagesRemote(false, Lang.bind(this, this._gotPendingMessages)); }, - _createIcon: function() { - return this._avatars.createAvatar(this._targetHandle, AVATAR_SIZE); + createIcon: function(size) { + return this._avatars.createAvatar(this._targetHandle, size); }, _gotPendingMessages: function(msgs, excp) { @@ -419,17 +422,14 @@ Source.prototype = { log('Channel closed ' + this._targetId); this._channel.disconnect(this._closedId); this._channelText.disconnect(this._receivedId); - Main.messageTray.remove(this._targetId); + this.destroy(); }, _receivedMessage: function(channel, id, timestamp, sender, type, flags, text) { log('Received: id ' + id + ', time ' + timestamp + ', sender ' + sender + ', type ' + type + ', flags ' + flags + ': ' + text); - let popupAvatar = this._createIcon(); - Main.notificationPopup.show(popupAvatar, text); - if (!Main.messageTray.contains(this._targetId)) { - let trayAvatar = this._createIcon(); - Main.messageTray.add(this._targetId, trayAvatar); - } + if (!Main.messageTray.contains(this)) + Main.messageTray.add(this); + this.notify(text); } }; diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index 8ae0157ca..51044f1ac 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -6,6 +6,9 @@ const Shell = imports.gi.Shell; const Mainloop = imports.mainloop; const Main = imports.ui.main; +const MessageTray = imports.ui.messageTray; + +let nextNotificationId = 1; const NotificationDaemonIface = { name: 'org.freedesktop.Notifications', @@ -24,7 +27,18 @@ const NotificationDaemonIface = { { 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 }; function NotificationDaemon() { @@ -49,20 +63,52 @@ NotificationDaemon.prototype = { }); }, + _sourceId: function(id) { + return 'notification-' + id; + }, + Notify: function(appName, replacesId, icon, summary, body, actions, hints, timeout) { - let iconActor = null; + let id, source = null; - if (icon != '') - iconActor = Shell.TextureCache.get_default().load_icon_name(icon, 24); - else { - // FIXME: load icon data from hints - } + if (replacesId != 0) { + id = replacesId; + source = Main.messageTray.getSource(this._sourceId(id)); + // source may be null if the current source was destroyed + // right as the client sent the new notification + } - Main.notificationPopup.show(iconActor, summary); + if (source == null) { + id = nextNotificationId++; + + source = new MessageTray.Source(this._sourceId(id), Lang.bind(this, + function (size) { + if (icon != '') + return Shell.TextureCache.get_default().load_icon_name(icon, size); + else { + // FIXME: load icon data from hints + // FIXME: better fallback icon + return Shell.TextureCache.get_default().load_icon_name('gtk-dialog-info', size); + } + })); + Main.messageTray.add(source); + + source.connect('clicked', Lang.bind(this, + function() { + source.destroy(); + this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED); + })); + } + + source.notify(summary); + return id; }, CloseNotification: function(id) { + let source = Main.messageTray.getSource(this._sourceId(id)); + if (source) + source.destroy(); + this._emitNotificationClosed(id, NotificationClosedReason.APP_CLOSED); }, GetCapabilities: function() { @@ -85,6 +131,13 @@ NotificationDaemon.prototype = { '0.1', // FIXME, get this from somewhere '1.0' ]; + }, + + _emitNotificationClosed: function(id, reason) { + DBus.session.emit_signal('/org/freedesktop/Notifications', + 'org.freedesktop.Notifications', + 'NotificationClosed', 'uu', + [id, reason]); } };