bacfdbbb03
ES6 finally adds standard class syntax to the language, so we can replace our custom Lang.Class framework with the new syntax. Any classes that inherit from GObject will need special treatment, so limit the port to regular javascript classes for now. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/361
831 lines
28 KiB
JavaScript
831 lines
28 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
||
|
||
const Clutter = imports.gi.Clutter;
|
||
const GdkPixbuf = imports.gi.GdkPixbuf;
|
||
const Gio = imports.gi.Gio;
|
||
const GLib = imports.gi.GLib;
|
||
const Gtk = imports.gi.Gtk;
|
||
const Shell = imports.gi.Shell;
|
||
const Mainloop = imports.mainloop;
|
||
const St = imports.gi.St;
|
||
|
||
const Config = imports.misc.config;
|
||
const Main = imports.ui.main;
|
||
const MessageTray = imports.ui.messageTray;
|
||
const Params = imports.misc.params;
|
||
const Util = imports.misc.util;
|
||
|
||
const { loadInterfaceXML } = imports.misc.fileUtils;
|
||
|
||
// Should really be defined in Gio.js
|
||
const BusIface = loadInterfaceXML('org.freedesktop.DBus');
|
||
var BusProxy = Gio.DBusProxy.makeProxyWrapper(BusIface);
|
||
function Bus() {
|
||
return new BusProxy(Gio.DBus.session, 'org.freedesktop.DBus', '/org/freedesktop/DBus');
|
||
}
|
||
|
||
const FdoNotificationsIface = loadInterfaceXML('org.freedesktop.Notifications');
|
||
|
||
var NotificationClosedReason = {
|
||
EXPIRED: 1,
|
||
DISMISSED: 2,
|
||
APP_CLOSED: 3,
|
||
UNDEFINED: 4
|
||
};
|
||
|
||
var Urgency = {
|
||
LOW: 0,
|
||
NORMAL: 1,
|
||
CRITICAL: 2
|
||
};
|
||
|
||
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>' }
|
||
]
|
||
};
|
||
|
||
var FdoNotificationDaemon = class FdoNotificationDaemon {
|
||
constructor() {
|
||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FdoNotificationsIface, this);
|
||
this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications');
|
||
|
||
this._sources = [];
|
||
this._senderToPid = {};
|
||
this._notifications = {};
|
||
this._busProxy = new Bus();
|
||
|
||
this._nextNotificationId = 1;
|
||
|
||
Shell.WindowTracker.get_default().connect('notify::focus-app',
|
||
this._onFocusAppChanged.bind(this));
|
||
Main.overview.connect('hidden',
|
||
this._onFocusAppChanged.bind(this));
|
||
}
|
||
|
||
_imageForNotificationData(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 this._iconForNotificationData(hints['image-path']);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
_fallbackIconForNotificationData(hints) {
|
||
let stockIcon;
|
||
switch (hints.urgency) {
|
||
case Urgency.LOW:
|
||
case Urgency.NORMAL:
|
||
stockIcon = 'dialog-information';
|
||
break;
|
||
case Urgency.CRITICAL:
|
||
stockIcon = 'dialog-error';
|
||
break;
|
||
}
|
||
return new Gio.ThemedIcon({ name: stockIcon });
|
||
}
|
||
|
||
_iconForNotificationData(icon) {
|
||
if (icon) {
|
||
if (icon.substr(0, 7) == 'file://')
|
||
return new Gio.FileIcon({ file: Gio.File.new_for_uri(icon) });
|
||
else if (icon[0] == '/')
|
||
return new Gio.FileIcon({ file: Gio.File.new_for_path(icon) });
|
||
else
|
||
return new Gio.ThemedIcon({ name: icon });
|
||
}
|
||
return null;
|
||
}
|
||
|
||
_lookupSource(title, pid) {
|
||
for (let i = 0; i < this._sources.length; i++) {
|
||
let source = this._sources[i];
|
||
if (source.pid == pid && source.initialTitle == title)
|
||
return source;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// Returns the source associated with ndata.notification if it is set.
|
||
// 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.
|
||
//
|
||
// Either a pid or ndata.notification is needed to retrieve or
|
||
// create a source.
|
||
_getSource(title, pid, ndata, sender) {
|
||
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 source = this._lookupSource(title, pid);
|
||
if (source) {
|
||
source.setTitle(title);
|
||
return source;
|
||
}
|
||
|
||
let appId = ndata ? ndata.hints['desktop-entry'] || null : null;
|
||
source = new FdoNotificationDaemonSource(title, pid, sender, appId);
|
||
|
||
this._sources.push(source);
|
||
source.connect('destroy', () => {
|
||
let index = this._sources.indexOf(source);
|
||
if (index >= 0)
|
||
this._sources.splice(index, 1);
|
||
});
|
||
|
||
Main.messageTray.add(source);
|
||
return source;
|
||
}
|
||
|
||
NotifyAsync(params, invocation) {
|
||
let [appName, replacesId, icon, summary, body, actions, hints, timeout] = params;
|
||
let id;
|
||
|
||
for (let hint in hints) {
|
||
// unpack the variants
|
||
hints[hint] = hints[hint].deep_unpack();
|
||
}
|
||
|
||
hints = Params.parse(hints, { urgency: Urgency.NORMAL }, true);
|
||
|
||
// Filter out chat, presence, calls and invitation notifications from
|
||
// Empathy, since we handle that information from telepathyClient.js
|
||
//
|
||
// Note that empathy uses im.received for one to one chats and
|
||
// x-empathy.im.mentioned for multi-user, so we're good here
|
||
if (appName == 'Empathy' && hints['category'] == 'im.received') {
|
||
// Ignore replacesId since we already sent back a
|
||
// NotificationClosed for that id.
|
||
id = this._nextNotificationId++;
|
||
let idle_id = Mainloop.idle_add(() => {
|
||
this._emitNotificationClosed(id, NotificationClosedReason.DISMISSED);
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
GLib.Source.set_name_by_id(idle_id, '[gnome-shell] this._emitNotificationClosed');
|
||
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 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']) {
|
||
if (hints['image_data'])
|
||
hints['image-data'] = hints['image_data']; // version 1.1 of the spec
|
||
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'];
|
||
}
|
||
|
||
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 = this._nextNotificationId++;
|
||
}
|
||
this._notifications[id] = ndata;
|
||
|
||
let sender = invocation.get_sender();
|
||
let pid = this._senderToPid[sender];
|
||
|
||
let source = this._getSource(appName, pid, ndata, sender, null);
|
||
|
||
if (source) {
|
||
this._notifyForSource(source, ndata);
|
||
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
||
}
|
||
|
||
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.
|
||
return invocation.return_value(GLib.Variant.new('(u)', [id]));;
|
||
}
|
||
|
||
this._busProxy.GetConnectionUnixProcessIDRemote(sender, (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;
|
||
source = this._getSource(appName, pid, ndata, sender, null);
|
||
|
||
this._senderToPid[sender] = pid;
|
||
source.connect('destroy', () => {
|
||
delete this._senderToPid[sender];
|
||
});
|
||
this._notifyForSource(source, ndata);
|
||
});
|
||
|
||
return invocation.return_value(GLib.Variant.new('(u)', [id]));
|
||
}
|
||
|
||
_notifyForSource(source, ndata) {
|
||
let [id, icon, summary, body, actions, hints, notification] =
|
||
[ndata.id, ndata.icon, ndata.summary, ndata.body,
|
||
ndata.actions, ndata.hints, ndata.notification];
|
||
|
||
if (notification == null) {
|
||
notification = new MessageTray.Notification(source);
|
||
ndata.notification = notification;
|
||
notification.connect('destroy', (n, reason) => {
|
||
delete this._notifications[ndata.id];
|
||
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;
|
||
}
|
||
this._emitNotificationClosed(ndata.id, notificationClosedReason);
|
||
});
|
||
}
|
||
|
||
let gicon = this._iconForNotificationData(icon);
|
||
let gimage = this._imageForNotificationData(hints);
|
||
|
||
// 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, the icon and takes precedence.
|
||
if (!gicon && gimage)
|
||
gicon = gimage;
|
||
else if (!gicon)
|
||
gicon = this._fallbackIconForNotificationData(hints);
|
||
|
||
notification.update(summary, body, { gicon: gicon,
|
||
bannerMarkup: true,
|
||
clear: true,
|
||
soundFile: hints['sound-file'],
|
||
soundName: hints['sound-name'] });
|
||
|
||
let hasDefaultAction = false;
|
||
|
||
if (actions.length) {
|
||
for (let i = 0; i < actions.length - 1; i += 2) {
|
||
let [actionId, label] = [actions[i], actions[i+1]];
|
||
if (actionId == 'default')
|
||
hasDefaultAction = true;
|
||
else
|
||
notification.addAction(label, () => {
|
||
this._emitActionInvoked(ndata.id, actionId);
|
||
});
|
||
}
|
||
}
|
||
|
||
if (hasDefaultAction) {
|
||
notification.connect('activated', () => {
|
||
this._emitActionInvoked(ndata.id, 'default');
|
||
});
|
||
} else {
|
||
notification.connect('activated', () => {
|
||
source.open();
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
notification.setResident(!!hints.resident);
|
||
// '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']);
|
||
|
||
let sourceGIcon = source.useNotificationIcon ? gicon : null;
|
||
source.processNotification(notification, sourceGIcon);
|
||
}
|
||
|
||
CloseNotification(id) {
|
||
let ndata = this._notifications[id];
|
||
if (ndata) {
|
||
if (ndata.notification)
|
||
ndata.notification.destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
|
||
delete this._notifications[id];
|
||
}
|
||
}
|
||
|
||
GetCapabilities() {
|
||
return [
|
||
'actions',
|
||
// 'action-icons',
|
||
'body',
|
||
// 'body-hyperlinks',
|
||
// 'body-images',
|
||
'body-markup',
|
||
// 'icon-multi',
|
||
'icon-static',
|
||
'persistence',
|
||
'sound',
|
||
];
|
||
}
|
||
|
||
GetServerInformation() {
|
||
return [
|
||
Config.PACKAGE_NAME,
|
||
'GNOME',
|
||
Config.PACKAGE_VERSION,
|
||
'1.2'
|
||
];
|
||
}
|
||
|
||
_onFocusAppChanged() {
|
||
let tracker = Shell.WindowTracker.get_default();
|
||
if (!tracker.focus_app)
|
||
return;
|
||
|
||
for (let i = 0; i < this._sources.length; i++) {
|
||
let source = this._sources[i];
|
||
if (source.app == tracker.focus_app) {
|
||
source.destroyNonResidentNotifications();
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
_emitNotificationClosed(id, reason) {
|
||
this._dbusImpl.emit_signal('NotificationClosed',
|
||
GLib.Variant.new('(uu)', [id, reason]));
|
||
}
|
||
|
||
_emitActionInvoked(id, action) {
|
||
this._dbusImpl.emit_signal('ActionInvoked',
|
||
GLib.Variant.new('(us)', [id, action]));
|
||
}
|
||
};
|
||
|
||
var FdoNotificationDaemonSource =
|
||
class FdoNotificationDaemonSource extends MessageTray.Source {
|
||
constructor(title, pid, sender, appId) {
|
||
super(title);
|
||
|
||
this.pid = pid;
|
||
this.app = this._getApp(appId);
|
||
|
||
this.initialTitle = title;
|
||
|
||
if (this.app)
|
||
this.title = this.app.get_name();
|
||
else
|
||
this.useNotificationIcon = true;
|
||
|
||
if (sender)
|
||
this._nameWatcherId = Gio.DBus.session.watch_name(sender,
|
||
Gio.BusNameWatcherFlags.NONE,
|
||
null,
|
||
this._onNameVanished.bind(this));
|
||
else
|
||
this._nameWatcherId = 0;
|
||
}
|
||
|
||
_createPolicy() {
|
||
if (this.app && this.app.get_app_info()) {
|
||
let id = this.app.get_id().replace(/\.desktop$/,'');
|
||
return new MessageTray.NotificationApplicationPolicy(id);
|
||
} else {
|
||
return new MessageTray.NotificationGenericPolicy();
|
||
}
|
||
}
|
||
|
||
_onNameVanished() {
|
||
// Destroy the notification source when its sender is removed from DBus.
|
||
// Only do so if this.app is set to avoid removing "notify-send" sources, senders
|
||
// of which аre removed from DBus immediately.
|
||
// 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.
|
||
if (this.app)
|
||
this.destroy();
|
||
}
|
||
|
||
processNotification(notification, gicon) {
|
||
if (gicon)
|
||
this._gicon = gicon;
|
||
this.iconUpdated();
|
||
|
||
let tracker = Shell.WindowTracker.get_default();
|
||
if (notification.resident && this.app && tracker.focus_app == this.app)
|
||
this.pushNotification(notification);
|
||
else
|
||
this.notify(notification);
|
||
}
|
||
|
||
_getApp(appId) {
|
||
let app;
|
||
|
||
app = Shell.WindowTracker.get_default().get_app_from_pid(this.pid);
|
||
if (app != null)
|
||
return app;
|
||
|
||
if (appId) {
|
||
app = Shell.AppSystem.get_default().lookup_app(appId + '.desktop');
|
||
if (app != null)
|
||
return app;
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
setTitle(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;
|
||
|
||
super.setTitle(title);
|
||
}
|
||
|
||
open() {
|
||
this.openApp();
|
||
this.destroyNonResidentNotifications();
|
||
}
|
||
|
||
openApp() {
|
||
if (this.app == null)
|
||
return;
|
||
|
||
this.app.activate();
|
||
Main.overview.hide();
|
||
Main.panel.closeCalendar();
|
||
}
|
||
|
||
destroy() {
|
||
if (this._nameWatcherId) {
|
||
Gio.DBus.session.unwatch_name(this._nameWatcherId);
|
||
this._nameWatcherId = 0;
|
||
}
|
||
|
||
super.destroy();
|
||
}
|
||
|
||
createIcon(size) {
|
||
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;
|
||
}
|
||
}
|
||
};
|
||
|
||
const PRIORITY_URGENCY_MAP = {
|
||
low: MessageTray.Urgency.LOW,
|
||
normal: MessageTray.Urgency.NORMAL,
|
||
high: MessageTray.Urgency.HIGH,
|
||
urgent: MessageTray.Urgency.CRITICAL
|
||
};
|
||
|
||
var GtkNotificationDaemonNotification =
|
||
class GtkNotificationDaemonNotification extends MessageTray.Notification {
|
||
constructor(source, notification) {
|
||
super(source);
|
||
this._serialized = GLib.Variant.new('a{sv}', notification);
|
||
|
||
let { "title": title,
|
||
"body": body,
|
||
"icon": gicon,
|
||
"urgent": urgent,
|
||
"priority": priority,
|
||
"buttons": buttons,
|
||
"default-action": defaultAction,
|
||
"default-action-target": defaultActionTarget,
|
||
"timestamp": time } = notification;
|
||
|
||
if (priority) {
|
||
let urgency = PRIORITY_URGENCY_MAP[priority.unpack()];
|
||
this.setUrgency(urgency != undefined ? urgency : MessageTray.Urgency.NORMAL);
|
||
} else if (urgent) {
|
||
this.setUrgency(urgent.unpack() ? MessageTray.Urgency.CRITICAL
|
||
: MessageTray.Urgency.NORMAL);
|
||
} else {
|
||
this.setUrgency(MessageTray.Urgency.NORMAL);
|
||
}
|
||
|
||
if (buttons) {
|
||
buttons.deep_unpack().forEach(button => {
|
||
this.addAction(button.label.unpack(), () => {
|
||
this._onButtonClicked(button);
|
||
});
|
||
});
|
||
}
|
||
|
||
this._defaultAction = defaultAction ? defaultAction.unpack() : null;
|
||
this._defaultActionTarget = defaultActionTarget;
|
||
|
||
this.update(title.unpack(), body ? body.unpack() : null,
|
||
{ gicon: gicon ? Gio.icon_deserialize(gicon) : null,
|
||
datetime : time ? GLib.DateTime.new_from_unix_local(time.unpack()) : null });
|
||
}
|
||
|
||
_activateAction(namespacedActionId, target) {
|
||
if (namespacedActionId) {
|
||
if (namespacedActionId.startsWith('app.')) {
|
||
let actionId = namespacedActionId.slice('app.'.length);
|
||
this.source.activateAction(actionId, target);
|
||
}
|
||
} else {
|
||
this.source.open();
|
||
}
|
||
}
|
||
|
||
_onButtonClicked(button) {
|
||
let { 'action': action, 'target': actionTarget } = button;
|
||
this._activateAction(action.unpack(), actionTarget);
|
||
}
|
||
|
||
activate() {
|
||
this._activateAction(this._defaultAction, this._defaultActionTarget);
|
||
super.activate();
|
||
}
|
||
|
||
serialize() {
|
||
return this._serialized;
|
||
}
|
||
};
|
||
|
||
const FdoApplicationIface = loadInterfaceXML('org.freedesktop.Application');
|
||
const FdoApplicationProxy = Gio.DBusProxy.makeProxyWrapper(FdoApplicationIface);
|
||
|
||
function objectPathFromAppId(appId) {
|
||
return '/' + appId.replace(/\./g, '/').replace(/-/g, '_');
|
||
}
|
||
|
||
function getPlatformData() {
|
||
let startupId = GLib.Variant.new('s', '_TIME' + global.get_current_time());
|
||
return { "desktop-startup-id": startupId };
|
||
}
|
||
|
||
function InvalidAppError() {}
|
||
|
||
var GtkNotificationDaemonAppSource =
|
||
class GtkNotificationDaemonAppSource extends MessageTray.Source {
|
||
constructor(appId) {
|
||
let objectPath = objectPathFromAppId(appId);
|
||
if (!GLib.Variant.is_object_path(objectPath))
|
||
throw new InvalidAppError();
|
||
|
||
let app = Shell.AppSystem.get_default().lookup_app(appId + '.desktop');
|
||
if (!app)
|
||
throw new InvalidAppError();
|
||
|
||
super(app.get_name());
|
||
|
||
this._appId = appId;
|
||
this._app = app;
|
||
this._objectPath = objectPath;
|
||
|
||
this._notifications = {};
|
||
this._notificationPending = false;
|
||
}
|
||
|
||
createIcon(size) {
|
||
return this._app.create_icon_texture(size);
|
||
}
|
||
|
||
_createPolicy() {
|
||
return new MessageTray.NotificationApplicationPolicy(this._appId);
|
||
}
|
||
|
||
_createApp(callback) {
|
||
return new FdoApplicationProxy(Gio.DBus.session, this._appId, this._objectPath, callback);
|
||
}
|
||
|
||
_createNotification(params) {
|
||
return new GtkNotificationDaemonNotification(this, params);
|
||
}
|
||
|
||
activateAction(actionId, target) {
|
||
this._createApp((app, error) => {
|
||
if (error == null)
|
||
app.ActivateActionRemote(actionId, target ? [target] : [], getPlatformData());
|
||
else
|
||
logError(error, 'Failed to activate application proxy');
|
||
});
|
||
Main.overview.hide();
|
||
Main.panel.closeCalendar();
|
||
}
|
||
|
||
open() {
|
||
this._createApp((app, error) => {
|
||
if (error == null)
|
||
app.ActivateRemote(getPlatformData());
|
||
else
|
||
logError(error, 'Failed to open application proxy');
|
||
});
|
||
Main.overview.hide();
|
||
Main.panel.closeCalendar();
|
||
}
|
||
|
||
addNotification(notificationId, notificationParams, showBanner) {
|
||
this._notificationPending = true;
|
||
|
||
if (this._notifications[notificationId])
|
||
this._notifications[notificationId].destroy(MessageTray.NotificationDestroyedReason.REPLACED);
|
||
|
||
let notification = this._createNotification(notificationParams);
|
||
notification.connect('destroy', () => {
|
||
delete this._notifications[notificationId];
|
||
});
|
||
this._notifications[notificationId] = notification;
|
||
|
||
if (showBanner)
|
||
this.notify(notification);
|
||
else
|
||
this.pushNotification(notification);
|
||
|
||
this._notificationPending = false;
|
||
}
|
||
|
||
destroy(reason) {
|
||
if (this._notificationPending)
|
||
return;
|
||
super.destroy(reason);
|
||
}
|
||
|
||
removeNotification(notificationId) {
|
||
if (this._notifications[notificationId])
|
||
this._notifications[notificationId].destroy(MessageTray.NotificationDestroyedReason.SOURCE_CLOSED);
|
||
}
|
||
|
||
serialize() {
|
||
let notifications = [];
|
||
for (let notificationId in this._notifications) {
|
||
let notification = this._notifications[notificationId];
|
||
notifications.push([notificationId, notification.serialize()]);
|
||
}
|
||
return [this._appId, notifications];
|
||
}
|
||
};
|
||
|
||
const GtkNotificationsIface = loadInterfaceXML('org.gtk.Notifications');
|
||
|
||
var GtkNotificationDaemon = class GtkNotificationDaemon {
|
||
constructor() {
|
||
this._sources = {};
|
||
|
||
this._loadNotifications();
|
||
|
||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GtkNotificationsIface, this);
|
||
this._dbusImpl.export(Gio.DBus.session, '/org/gtk/Notifications');
|
||
|
||
Gio.DBus.session.own_name('org.gtk.Notifications', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
||
}
|
||
|
||
_ensureAppSource(appId) {
|
||
if (this._sources[appId])
|
||
return this._sources[appId];
|
||
|
||
let source = new GtkNotificationDaemonAppSource(appId);
|
||
|
||
source.connect('destroy', () => {
|
||
delete this._sources[appId];
|
||
this._saveNotifications();
|
||
});
|
||
source.connect('count-updated', this._saveNotifications.bind(this));
|
||
Main.messageTray.add(source);
|
||
this._sources[appId] = source;
|
||
return source;
|
||
}
|
||
|
||
_loadNotifications() {
|
||
this._isLoading = true;
|
||
|
||
let value = global.get_persistent_state('a(sa(sv))', 'notifications');
|
||
if (value) {
|
||
let sources = value.deep_unpack();
|
||
sources.forEach(([appId, notifications]) => {
|
||
if (notifications.length == 0)
|
||
return;
|
||
|
||
let source;
|
||
try {
|
||
source = this._ensureAppSource(appId);
|
||
} catch(e) {
|
||
if (e instanceof InvalidAppError)
|
||
return;
|
||
throw e;
|
||
}
|
||
|
||
notifications.forEach(([notificationId, notification]) => {
|
||
source.addNotification(notificationId, notification.deep_unpack(), false);
|
||
});
|
||
});
|
||
}
|
||
|
||
this._isLoading = false;
|
||
}
|
||
|
||
_saveNotifications() {
|
||
if (this._isLoading)
|
||
return;
|
||
|
||
let sources = [];
|
||
for (let appId in this._sources) {
|
||
let source = this._sources[appId];
|
||
sources.push(source.serialize());
|
||
}
|
||
|
||
global.set_persistent_state('notifications', new GLib.Variant('a(sa(sv))', sources));
|
||
}
|
||
|
||
AddNotificationAsync(params, invocation) {
|
||
let [appId, notificationId, notification] = params;
|
||
|
||
let source;
|
||
try {
|
||
source = this._ensureAppSource(appId);
|
||
} catch(e) {
|
||
if (e instanceof InvalidAppError) {
|
||
invocation.return_dbus_error('org.gtk.Notifications.InvalidApp', 'The app by ID "%s" could not be found'.format(appId));
|
||
return;
|
||
}
|
||
throw e;
|
||
}
|
||
|
||
let timestamp = GLib.DateTime.new_now_local().to_unix();
|
||
notification['timestamp'] = new GLib.Variant('x', timestamp);
|
||
|
||
source.addNotification(notificationId, notification, true);
|
||
|
||
invocation.return_value(null);
|
||
}
|
||
|
||
RemoveNotificationAsync(params, invocation) {
|
||
let [appId, notificationId] = params;
|
||
let source = this._sources[appId];
|
||
if (source)
|
||
source.removeNotification(notificationId);
|
||
|
||
invocation.return_value(null);
|
||
}
|
||
};
|
||
|
||
var NotificationDaemon = class NotificationDaemon {
|
||
constructor() {
|
||
this._fdoNotificationDaemon = new FdoNotificationDaemon();
|
||
this._gtkNotificationDaemon = new GtkNotificationDaemon();
|
||
}
|
||
};
|