Compare commits

...

12 Commits

Author SHA1 Message Date
Jasper St. Pierre
672a4a56bd messageTray: Remove an unused variable 2014-06-04 12:58:34 -04:00
Jasper St. Pierre
50421ede37 messageTray: Add notification close button inside notification
And remove the old close button outside it.
2014-06-04 12:39:34 -04:00
Jasper St. Pierre
826a7fa02a messageTray: Implement new notification designs
Rather than use an StTable, a custom ShellGenericContainer, and plenty
of hacky style classes, replace them all with standard BoxLayouts and
Bins.

Remove the customContent parameter in favor of subclasses setting the
child of this._bodyBin instead.

With this comes a whole new notification implementation to implement
the new notification designs.

We lose a few of the fancy features like showing the first part of
the body, ellipsized, next the banner when it will fit, and some other
layout logic. But since the design of notifications is changing
substantially anyways, I don't feel too bad...
2014-06-04 12:39:23 -04:00
Jasper St. Pierre
d7844bc7f4 messageTray: Glue the notification to the bottom of the screen, always
We'll animate notifications popping up with another system soon enough,
instead. The idea here is that instead of carefully animating the Y
position of the notificationWidget when a notification updates, we
simply animate the height of the new actor inside the notification.
This will fix some of the awkward updates where instead of the
notification content expanding, we see the buttons or action area
pushed off the edge of the screen...

Animations that happen as a result of adding something new to the
notification or expanding it should be done by tweeing the new actors
in inside the notification.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
485444a451 messageTray: Remove addBody as a public API
As it's unused.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
e5318d5235 messageTray: Remove support for notifications with images
This sufficiently complicates the code, and won't fit in the
new design.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
e6e15489aa messageTray: Remove support for resident notifications
Now the only resident notification is a chat notification. The convenient
thing about this special-case behavior is that there's already special-case
code for it and the shell, and we always know that a chat notification will
always be 1:1 with its chat source.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
0951d36e8f notificationDaemon: Remove support for resident notifications
They're not really an API that has caught on, and not really one
we want to support, either.
2014-06-03 14:48:48 -04:00
Jasper St. Pierre
c38939902f notificationDaemon: Remove the special-case hack for system tray icons
Nothing in the system actually has a standard system tray icon anymore,
so this hack isn't necessary.
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
97974a3f2a telepathyClient: Remove all the fancy features
This can be done with another app, like Empathy or Chat.
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
8b6cab741d autorunManager: Remove the resident "Removable Devices" notification
Users aren't usually the best at obeying the rules, and systems can
deal with hotplug without ejecting first.

https://bugzilla.gnome.org/show_bug.cgi?id=719857
2014-06-03 14:48:47 -04:00
Jasper St. Pierre
2aad0eac31 messageTray: Don't use a member variable when we can use a local
Tiny cleanup.
2014-06-03 14:26:59 -04:00
7 changed files with 254 additions and 1465 deletions

View File

@ -414,10 +414,7 @@ StScrollBar StButton#vhandle:active {
/* Buttons */ /* Buttons */
.candidate-page-button, .candidate-page-button,
.notification-button,
.notification-icon-button,
.hotplug-notification-item, .hotplug-notification-item,
.hotplug-resident-eject-button,
.modal-dialog-button, .modal-dialog-button,
.app-view-control { .app-view-control {
border: 1px solid #8b8b8b; border: 1px solid #8b8b8b;
@ -431,17 +428,12 @@ StScrollBar StButton#vhandle:active {
} }
.candidate-page-button:hover, .candidate-page-button:hover,
.notification-button:hover,
.notification-icon-button:hover,
.hotplug-notification-item:hover, .hotplug-notification-item:hover,
.hotplug-resident-eject-button:hover,
.modal-dialog-button:hover { .modal-dialog-button:hover {
background-gradient-start: rgba(255, 255, 255, 0.3); background-gradient-start: rgba(255, 255, 255, 0.3);
background-gradient-end: rgba(255, 255, 255, 0.1); background-gradient-end: rgba(255, 255, 255, 0.1);
} }
.notification-button:focus,
.notification-icon-button:focus,
.hotplug-notification-item:focus, .hotplug-notification-item:focus,
.modal-dialog-button:focus, .modal-dialog-button:focus,
.app-view-control:focus { .app-view-control:focus {
@ -455,10 +447,7 @@ StScrollBar StButton#vhandle:active {
.candidate-page-button:active, .candidate-page-button:active,
.candidate-page-button:pressed, .candidate-page-button:pressed,
.notification-button:active,
.notification-icon-button:active,
.hotplug-notification-item:active, .hotplug-notification-item:active,
.hotplug-resident-eject-button:active,
.modal-dialog-button:active, .modal-dialog-button:active,
.modal-dialog-button:pressed, .modal-dialog-button:pressed,
.app-view-control:checked { .app-view-control:checked {
@ -467,8 +456,6 @@ StScrollBar StButton#vhandle:active {
} }
.candidate-page-button:insensitive, .candidate-page-button:insensitive,
.notification-button:insensitive,
.notification-icon-button:insensitive,
.modal-dialog-button:insensitive { .modal-dialog-button:insensitive {
border-color: #666666; border-color: #666666;
color: #9f9f9f; color: #9f9f9f;
@ -480,7 +467,6 @@ StScrollBar StButton#vhandle:active {
#searchEntry, #searchEntry,
.modal-dialog-button, .modal-dialog-button,
.notification-button,
.hotplug-notification-item, .hotplug-notification-item,
.app-view-controls, .app-view-controls,
#screenShieldNotifications { #screenShieldNotifications {
@ -1535,36 +1521,69 @@ StScrollBar StButton#vhandle:active {
color: #999999; color: #999999;
} }
.notification {
border-radius: 10px 10px 0px 0px;
background: rgba(0,0,0,0.9);
padding: 8px 8px 4px 8px;
spacing-rows: 4px;
spacing-columns: 10px;
}
.notification, #notification-container { .notification, #notification-container {
font-size: 11pt; font-size: 11pt;
width: 34em; width: 34em;
} }
.notification.multi-line-notification { .notification-main-button,
padding-bottom: 8px; .notification-button {
background: rgba(0,0,0,0.9);
} }
.notification-unexpanded { .notification-main-button {
/* We want to force the actor at a specific size, irrespective border-radius: 10px 10px 0px 0px;
of its minimum and preferred size, so we override both */
min-height: 36px;
height: 36px;
} }
/* We use row-span = 2 for the image cell, which prevents its height preferences to be .notification-main-content {
taken into account during allocation, so its height ends up being limited by the height padding: 8px;
of the content in the other rows. To avoid showing a stretched image, we set the minimum spacing: 8px;
height of the table to be ICON_SIZE + IMAGE_SIZE + spacing-rows = 24 + 125 + 10 = 159 */ }
.notification-with-image {
min-height: 159px; .notification-close-button {
padding: 8px;
border-radius: 4px;
}
.notification-action-area {
padding: 8px;
}
.notification-action-area,
.notification-button {
border-top: 1px solid #666;
}
.notification-button {
padding: 8px 0px;
border-right: 1px solid #666;
}
.notification-main-button:hover,
.notification-button:hover,
.notification-close-button:hover {
background: rgba(100,100,100,0.9);
}
.notification-main-button:active,
.notification-button:active {
background: rgba(255,255,255,0.1);
}
.notification-button:last-child {
border-right-width: 0px;
}
.notification-title-box {
spacing: 8px;
}
.notification-scrollview:ltr > StScrollBar {
padding-left: 6px;
}
.notification-scrollview:rtl > StScrollBar {
padding-right: 6px;
} }
.summary-boxpointer { .summary-boxpointer {
@ -1606,47 +1625,6 @@ StScrollBar StButton#vhandle:active {
-st-vfade-offset: 24px; -st-vfade-offset: 24px;
} }
.notification-scrollview:ltr > StScrollBar {
padding-left: 6px;
}
.notification-scrollview:rtl > StScrollBar {
padding-right: 6px;
}
.notification-body {
spacing: 5px;
}
.notification-actions {
padding-top: 18px;
spacing: 10px;
}
.notification-button {
-st-natural-width: 140px;
padding: 4px 4px 5px;
}
.notification-button:focus {
-st-natural-width: 138px;
padding: 3px 4px 4px;
}
.notification-icon-button {
border-radius: 5px;
padding: 5px;
}
.notification-icon-button:focus {
padding: 4px;
}
.notification-icon-button > StIcon {
icon-size: 16px;
padding: 8px;
}
.secondary-icon { .secondary-icon {
icon-size: 1.09em; icon-size: 1.09em;
} }
@ -1669,45 +1647,6 @@ StScrollBar StButton#vhandle:active {
padding: 2px 5px; padding: 2px 5px;
} }
.hotplug-resident-box {
spacing: 8px;
}
.hotplug-resident-mount {
spacing: 8px;
border-radius: 4px;
color: #ccc;
}
.hotplug-resident-mount:hover {
background-gradient-direction: horizontal;
background-gradient-start: rgba(255, 255, 255, 0.1);
background-gradient-end: rgba(255, 255, 255, 0);
color: #fff;
}
.hotplug-resident-mount-label {
color: inherit;
padding-left: 6px;
}
.hotplug-resident-mount-icon {
icon-size: 24px;
padding-left: 6px;
}
.hotplug-resident-eject-icon {
icon-size: 16px;
}
.hotplug-resident-eject-button {
padding: 7px;
border-radius: 5px;
color: #ccc;
}
.chat-log-message { .chat-log-message {
color: #888888; color: #888888;
} }
@ -1747,7 +1686,11 @@ StScrollBar StButton#vhandle:active {
padding-right: 4px; padding-right: 4px;
} }
.chat-notification-scrollview{ .chat-notification-body-box {
spacing: 5px;
}
.chat-notification-scrollview {
max-height: 22em; max-height: 22em;
} }
@ -2666,8 +2609,7 @@ StScrollBar StButton#vhandle:active {
padding-bottom: 0px; padding-bottom: 0px;
} }
#screenShieldNotifications .notification-button, #screenShieldNotifications .notification-button {
#screenShieldNotifications .notification-icon-button {
border: 1px rgba(255,255,255,0.5); border: 1px rgba(255,255,255,0.5);
} }

View File

@ -170,17 +170,6 @@ const AutorunManager = new Lang.Class({
this._transDispatcher = new AutorunTransientDispatcher(this); this._transDispatcher = new AutorunTransientDispatcher(this);
}, },
_ensureResidentSource: function() {
if (this._residentSource)
return;
this._residentSource = new AutorunResidentSource(this);
let destroyId = this._residentSource.connect('destroy', Lang.bind(this, function() {
this._residentSource.disconnect(destroyId);
this._residentSource = null;
}));
},
enable: function() { enable: function() {
this._scanMounts(); this._scanMounts();
@ -189,17 +178,12 @@ const AutorunManager = new Lang.Class({
}, },
disable: function() { disable: function() {
if (this._residentSource)
this._residentSource.destroy();
this._volumeMonitor.disconnect(this._mountAddedId); this._volumeMonitor.disconnect(this._mountAddedId);
this._volumeMonitor.disconnect(this._mountRemovedId); this._volumeMonitor.disconnect(this._mountRemovedId);
}, },
_processMount: function(mount, hotplug) { _processMount: function(mount, hotplug) {
let discoverer = new ContentTypeDiscoverer(Lang.bind(this, function(mount, apps, contentTypes) { let discoverer = new ContentTypeDiscoverer(Lang.bind(this, function(mount, apps, contentTypes) {
this._ensureResidentSource();
this._residentSource.addMount(mount, apps);
if (hotplug) if (hotplug)
this._transDispatcher.addMount(mount, apps, contentTypes); this._transDispatcher.addMount(mount, apps, contentTypes);
})); }));
@ -224,8 +208,6 @@ const AutorunManager = new Lang.Class({
_onMountRemoved: function(monitor, mount) { _onMountRemoved: function(monitor, mount) {
this._transDispatcher.removeMount(mount); this._transDispatcher.removeMount(mount);
if (this._residentSource)
this._residentSource.removeMount(mount);
}, },
ejectMount: function(mount) { ejectMount: function(mount) {
@ -288,153 +270,6 @@ const AutorunManager = new Lang.Class({
}, },
}); });
const AutorunResidentSource = new Lang.Class({
Name: 'AutorunResidentSource',
Extends: MessageTray.Source,
_init: function(manager) {
this.parent(_("Removable Devices"), 'media-removable');
this.resident = true;
this._mounts = [];
this._manager = manager;
this._notification = new AutorunResidentNotification(this._manager, this);
},
_createPolicy: function() {
return new MessageTray.NotificationPolicy({ showInLockScreen: false });
},
buildRightClickMenu: function() {
return null;
},
addMount: function(mount, apps) {
if (!shouldAutorunMount(mount, false))
return;
let filtered = this._mounts.filter(function (element) {
return (element.mount == mount);
});
if (filtered.length != 0)
return;
let element = { mount: mount, apps: apps };
this._mounts.push(element);
this._redisplay();
},
removeMount: function(mount) {
this._mounts =
this._mounts.filter(function (element) {
return (element.mount != mount);
});
this._redisplay();
},
_redisplay: function() {
if (this._mounts.length == 0) {
this._notification.destroy();
this.destroy();
return;
}
this._notification.updateForMounts(this._mounts);
// add ourselves as a source, and push the notification
if (!Main.messageTray.contains(this)) {
Main.messageTray.add(this);
this.pushNotification(this._notification);
}
}
});
const AutorunResidentNotification = new Lang.Class({
Name: 'AutorunResidentNotification',
Extends: MessageTray.Notification,
_init: function(manager, source) {
this.parent(source, source.title, null, { customContent: true });
// set the notification as resident
this.setResident(true);
this._layout = new St.BoxLayout ({ style_class: 'hotplug-resident-box',
vertical: true });
this._manager = manager;
this.addActor(this._layout,
{ x_expand: true,
x_fill: true });
},
updateForMounts: function(mounts) {
// remove all the layout content
this._layout.destroy_all_children();
for (let idx = 0; idx < mounts.length; idx++) {
let element = mounts[idx];
let actor = this._itemForMount(element.mount, element.apps);
this._layout.add(actor, { x_fill: true,
expand: true });
}
},
_itemForMount: function(mount, apps) {
let item = new St.BoxLayout();
// prepare the mount button content
let mountLayout = new St.BoxLayout();
let mountIcon = new St.Icon({ gicon: mount.get_icon(),
style_class: 'hotplug-resident-mount-icon' });
mountLayout.add_actor(mountIcon);
let labelBin = new St.Bin({ y_align: St.Align.MIDDLE });
let mountLabel =
new St.Label({ text: mount.get_name(),
style_class: 'hotplug-resident-mount-label',
track_hover: true,
reactive: true });
labelBin.add_actor(mountLabel);
mountLayout.add_actor(labelBin);
let mountButton = new St.Button({ child: mountLayout,
x_align: St.Align.START,
x_fill: true,
style_class: 'hotplug-resident-mount',
button_mask: St.ButtonMask.ONE });
item.add(mountButton, { x_align: St.Align.START,
expand: true });
let ejectIcon =
new St.Icon({ icon_name: 'media-eject-symbolic',
style_class: 'hotplug-resident-eject-icon' });
let ejectButton =
new St.Button({ style_class: 'hotplug-resident-eject-button',
button_mask: St.ButtonMask.ONE,
child: ejectIcon });
item.add(ejectButton, { x_align: St.Align.END });
// now connect signals
mountButton.connect('clicked', Lang.bind(this, function(actor, event) {
startAppForMount(apps[0], mount);
}));
ejectButton.connect('clicked', Lang.bind(this, function() {
this._manager.ejectMount(mount);
}));
return item;
},
});
const AutorunTransientDispatcher = new Lang.Class({ const AutorunTransientDispatcher = new Lang.Class({
Name: 'AutorunTransientDispatcher', Name: 'AutorunTransientDispatcher',
@ -559,12 +394,12 @@ const AutorunTransientNotification = new Lang.Class({
Extends: MessageTray.Notification, Extends: MessageTray.Notification,
_init: function(manager, source) { _init: function(manager, source) {
this.parent(source, source.title, null, { customContent: true }); this.parent(source, source.title);
this._manager = manager; this._manager = manager;
this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box', this._box = new St.BoxLayout({ style_class: 'hotplug-transient-box',
vertical: true }); vertical: true });
this.addActor(this._box); this._bodyBin.child = this._box;
this._mount = source.mount; this._mount = source.mount;

View File

@ -102,15 +102,6 @@ const TelepathyClient = new Lang.Class({
this._tpClient.set_handle_channels_func( this._tpClient.set_handle_channels_func(
Lang.bind(this, this._handleChannels)); Lang.bind(this, this._handleChannels));
// Watch subscription requests and connection errors
this._subscriptionSource = null;
this._accountSource = null;
// Workaround for gjs not supporting GPtrArray in signals.
// See BGO bug #653941 for context.
this._tpClient.set_contact_list_changed_func(
Lang.bind(this, this._contactListChanged));
// Allow other clients (such as Empathy) to pre-empt our channels if // Allow other clients (such as Empathy) to pre-empt our channels if
// needed // needed
this._tpClient.set_delegated_channels_callback( this._tpClient.set_delegated_channels_callback(
@ -124,17 +115,12 @@ const TelepathyClient = new Lang.Class({
throw new Error('Couldn\'t register Telepathy client. Error: \n' + e); throw new Error('Couldn\'t register Telepathy client. Error: \n' + e);
} }
this._accountManagerValidityChangedId = this._accountManager.connect('account-validity-changed',
Lang.bind(this, this._accountValidityChanged));
if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core())) if (!this._accountManager.is_prepared(Tp.AccountManager.get_feature_quark_core()))
this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared)); this._accountManager.prepare_async(null, Lang.bind(this, this._accountManagerPrepared));
}, },
disable: function() { disable: function() {
this._tpClient.unregister(); this._tpClient.unregister();
this._accountManager.disconnect(this._accountManagerValidityChangedId);
this._accountManagerValidityChangedId = 0;
}, },
_observeChannels: function(observer, account, conn, channels, _observeChannels: function(observer, account, conn, channels,
@ -219,33 +205,6 @@ const TelepathyClient = new Lang.Class({
} }
}, },
_displayRoomInvitation: function(conn, channel, dispatchOp, context) {
// We can only approve the rooms if we have been invited to it
let selfContact = channel.group_get_self_contact();
if (selfContact == null) {
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
message: 'Not invited to the room' }));
return;
}
let [invited, inviter, reason, msg] = channel.group_get_local_pending_contact_info(selfContact);
if (!invited) {
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
message: 'Not invited to the room' }));
return;
}
// FIXME: We don't have a 'chat room' icon (bgo #653737) use
// system-users for now as Empathy does.
let source = new ApproverSource(dispatchOp, _("Invitation"),
Gio.icon_new_for_string('system-users'));
Main.messageTray.add(source);
let notif = new RoomInviteNotification(source, dispatchOp, channel, inviter);
source.notify(notif);
context.accept();
},
_approveChannels: function(approver, account, conn, channels, _approveChannels: function(approver, account, conn, channels,
dispatchOp, context) { dispatchOp, context) {
let channel = channels[0]; let channel = channels[0];
@ -259,10 +218,6 @@ const TelepathyClient = new Lang.Class({
if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT) if (chanType == Tp.IFACE_CHANNEL_TYPE_TEXT)
this._approveTextChannel(account, conn, channel, dispatchOp, context); this._approveTextChannel(account, conn, channel, dispatchOp, context);
else if (chanType == Tp.IFACE_CHANNEL_TYPE_CALL)
this._approveCall(account, conn, channel, dispatchOp, context);
else if (chanType == Tp.IFACE_CHANNEL_TYPE_FILE_TRANSFER)
this._approveFileTransfer(account, conn, channel, dispatchOp, context);
else else
context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT, context.fail(new Tp.Error({ code: Tp.Error.INVALID_ARGUMENT,
message: 'Unsupported channel type' })); message: 'Unsupported channel type' }));
@ -283,45 +238,9 @@ const TelepathyClient = new Lang.Class({
}})); }}));
context.accept(); context.accept();
} else {
this._displayRoomInvitation(conn, channel, dispatchOp, context);
} }
}, },
_approveCall: function(account, conn, channel, dispatchOp, context) {
let isVideo = false;
let props = channel.borrow_immutable_properties();
if (props[Tp.PROP_CHANNEL_TYPE_CALL_INITIAL_VIDEO])
isVideo = true;
// We got the TpContact
let source = new ApproverSource(dispatchOp, _("Call"), isVideo ?
Gio.icon_new_for_string('camera-web') :
Gio.icon_new_for_string('audio-input-microphone'));
Main.messageTray.add(source);
let notif = new AudioVideoNotification(source, dispatchOp, channel,
channel.get_target_contact(), isVideo);
source.notify(notif);
context.accept();
},
_approveFileTransfer: function(account, conn, channel, dispatchOp, context) {
// Use the icon of the file being transferred
let gicon = Gio.content_type_get_icon(channel.get_mime_type());
// We got the TpContact
let source = new ApproverSource(dispatchOp, _("File Transfer"), gicon);
Main.messageTray.add(source);
let notif = new FileTransferNotification(source, dispatchOp, channel,
channel.get_target_contact());
source.notify(notif);
context.accept();
},
_delegatedChannelsCb: function(client, channels) { _delegatedChannelsCb: function(client, channels) {
// Nothing to do as we don't make a distinction between observed and // Nothing to do as we don't make a distinction between observed and
// handled channels. // handled channels.
@ -329,105 +248,7 @@ const TelepathyClient = new Lang.Class({
_accountManagerPrepared: function(am, result) { _accountManagerPrepared: function(am, result) {
am.prepare_finish(result); am.prepare_finish(result);
let accounts = am.get_valid_accounts();
for (let i = 0; i < accounts.length; i++) {
this._accountValidityChanged(am, accounts[i], true);
}
}, },
_accountValidityChanged: function(am, account, valid) {
if (!valid)
return;
// It would be better to connect to "status-changed" but we cannot.
// See discussion in https://bugzilla.gnome.org/show_bug.cgi?id=654159
account.connect("notify::connection-status",
Lang.bind(this, this._accountConnectionStatusNotifyCb));
account.connect('notify::connection',
Lang.bind(this, this._connectionChanged));
this._connectionChanged(account);
},
_connectionChanged: function(account) {
let conn = account.get_connection();
if (conn == null)
return;
this._tpClient.grab_contact_list_changed(conn);
if (conn.get_contact_list_state() == Tp.ContactListState.SUCCESS) {
this._contactListChanged(conn, conn.dup_contact_list(), []);
}
},
_contactListChanged: function(conn, added, removed) {
for (let i = 0; i < added.length; i++) {
let contact = added[i];
contact.connect('subscription-states-changed',
Lang.bind(this, this._subscriptionStateChanged));
this._subscriptionStateChanged(contact);
}
},
_subscriptionStateChanged: function(contact) {
if (contact.get_publish_state() != Tp.SubscriptionState.ASK)
return;
/* Implicitly accept publish requests if contact is already subscribed */
if (contact.get_subscribe_state() == Tp.SubscriptionState.YES ||
contact.get_subscribe_state() == Tp.SubscriptionState.ASK) {
contact.authorize_publication_async(function(src, result) {
src.authorize_publication_finish(result)});
return;
}
/* Display notification to ask user to accept/reject request */
let source = this._ensureAppSource();
let notif = new SubscriptionRequestNotification(source, contact);
source.notify(notif);
},
_accountConnectionStatusNotifyCb: function(account) {
let connectionError = account.connection_error;
if (account.connection_status != Tp.ConnectionStatus.DISCONNECTED ||
connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED)) {
return;
}
let notif = this._accountNotifications[account.get_object_path()];
if (notif)
return;
/* Display notification that account failed to connect */
let source = this._ensureAppSource();
notif = new AccountNotification(source, account, connectionError);
this._accountNotifications[account.get_object_path()] = notif;
notif.connect('destroy', Lang.bind(this, function() {
delete this._accountNotifications[account.get_object_path()];
}));
source.notify(notif);
},
_ensureAppSource: function() {
if (this._appSource == null) {
this._appSource = new MessageTray.Source(_("Chat"), 'empathy');
this._appSource.policy = new MessageTray.NotificationApplicationPolicy('empathy');
Main.messageTray.add(this._appSource);
this._appSource.connect('destroy', Lang.bind(this, function () {
this._appSource = null;
}));
}
return this._appSource;
}
}); });
const ChatSource = new Lang.Class({ const ChatSource = new Lang.Class({
@ -545,7 +366,7 @@ const ChatSource = new Lang.Class({
_updateAvatarIcon: function() { _updateAvatarIcon: function() {
this.iconUpdated(); this.iconUpdated();
this._notification.update(this._notification.title, null, { customContent: true }); this._notification.update(this._notification.title);
}, },
open: function() { open: function() {
@ -737,7 +558,7 @@ const ChatSource = new Lang.Class({
title = GLib.markup_escape_text(this.title, -1); title = GLib.markup_escape_text(this.title, -1);
this._notification.update(this._notification.title, null, { customContent: true, secondaryGIcon: this.getSecondaryIcon() }); this._notification.update(this._notification.title, null, { secondaryGIcon: this.getSecondaryIcon() });
if (message) if (message)
msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>'; msg += ' <i>(' + GLib.markup_escape_text(message, -1) + ')</i>';
@ -764,8 +585,7 @@ const ChatNotification = new Lang.Class({
Extends: MessageTray.Notification, Extends: MessageTray.Notification,
_init: function(source) { _init: function(source) {
this.parent(source, source.title, null, { customContent: true, secondaryGIcon: source.getSecondaryIcon() }); this.parent(source, source.title, null, { secondaryGIcon: source.getSecondaryIcon() });
this.setResident(true);
this._responseEntry = new St.Entry({ style_class: 'chat-response', this._responseEntry = new St.Entry({ style_class: 'chat-response',
can_focus: true }); can_focus: true });
@ -781,15 +601,17 @@ const ChatNotification = new Lang.Class({
this.emit('unfocused'); this.emit('unfocused');
})); }));
this._createScrollArea();
this._lastGroup = null; this._lastGroup = null;
this._bodyBox = new St.BoxLayout({ style_class: 'chat-notification-body-box' });
this._bodyBin.child = this._bodyBox;
// Keep track of the bottom position for the current adjustment and // Keep track of the bottom position for the current adjustment and
// force a scroll to the bottom if things change while we were at the // force a scroll to the bottom if things change while we were at the
// bottom // bottom
this._oldMaxScrollValue = this._scrollArea.vscroll.adjustment.value; this._oldMaxScrollValue = this._bodyScrollArea.vscroll.adjustment.value;
this._scrollArea.add_style_class_name('chat-notification-scrollview'); this._bodyScrollArea.add_style_class_name('chat-notification-scrollview');
this._scrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) { this._bodyScrollArea.vscroll.adjustment.connect('changed', Lang.bind(this, function(adjustment) {
if (adjustment.value == this._oldMaxScrollValue) if (adjustment.value == this._oldMaxScrollValue)
this.scrollTo(St.Side.BOTTOM); this.scrollTo(St.Side.BOTTOM);
this._oldMaxScrollValue = Math.max(adjustment.lower, adjustment.upper - adjustment.page_size); this._oldMaxScrollValue = Math.max(adjustment.lower, adjustment.upper - adjustment.page_size);
@ -826,8 +648,7 @@ const ChatNotification = new Lang.Class({
} }
if (message.direction == NotificationDirection.RECEIVED) { if (message.direction == NotificationDirection.RECEIVED) {
this.update(this.source.title, messageBody, { customContent: true, this.update(this.source.title, messageBody, { bannerMarkup: true });
bannerMarkup: true });
} }
let group = (message.direction == NotificationDirection.RECEIVED ? let group = (message.direction == NotificationDirection.RECEIVED ?
@ -864,7 +685,7 @@ const ChatNotification = new Lang.Class({
expired[i].actor.destroy(); expired[i].actor.destroy();
} }
let groups = this._contentArea.get_children(); let groups = this._bodyBox.get_children();
for (let i = 0; i < groups.length; i++) { for (let i = 0; i < groups.length; i++) {
let group = groups[i]; let group = groups[i];
if (group.get_n_children() == 0) if (group.get_n_children() == 0)
@ -896,9 +717,9 @@ const ChatNotification = new Lang.Class({
if (this._timestampTimeoutId) if (this._timestampTimeoutId)
Mainloop.source_remove(this._timestampTimeoutId); Mainloop.source_remove(this._timestampTimeoutId);
let highlighter = new MessageTray.URLHighlighter(props.body, let highlighter = new MessageTray.URLHighlighter();
true, // line wrap? highlighter.actor.clutter_text.line_wrap = true;
true); // allow markup? highlighter.setMarkup(props.body, true);
let body = highlighter.actor; let body = highlighter.actor;
@ -910,14 +731,12 @@ const ChatNotification = new Lang.Class({
if (group != this._lastGroup) { if (group != this._lastGroup) {
this._lastGroup = group; this._lastGroup = group;
let emptyLine = new St.Label({ style_class: 'chat-empty-line' }); let emptyLine = new St.Label({ style_class: 'chat-empty-line' });
this.addActor(emptyLine); this._bodyBox.add_child(emptyLine);
} }
this._lastMessageBox = new St.BoxLayout({ vertical: false }); this._lastMessageBox = new St.BoxLayout({ vertical: false });
this._lastMessageBox.add(body, props.childProps); this._lastMessageBox.add(body, props.childProps);
this.addActor(this._lastMessageBox); this._bodyBox.add_child(this._lastMessageBox);
this.updated();
let timestamp = props.timestamp; let timestamp = props.timestamp;
this._history.unshift({ actor: body, time: timestamp, this._history.unshift({ actor: body, time: timestamp,
@ -1052,7 +871,7 @@ const ChatNotification = new Lang.Class({
group: 'meta', group: 'meta',
styles: ['chat-meta-message'] }); styles: ['chat-meta-message'] });
this.update(newAlias, null, { customContent: true }); this.update(newAlias);
this._filterMessages(); this._filterMessages();
}, },
@ -1105,359 +924,4 @@ const ChatNotification = new Lang.Class({
} }
}); });
const ApproverSource = new Lang.Class({
Name: 'ApproverSource',
Extends: MessageTray.Source,
_init: function(dispatchOp, text, gicon) {
this._gicon = gicon;
this.parent(text);
this._dispatchOp = dispatchOp;
// Destroy the source if the channel dispatch operation is invalidated
// as we can't approve any more.
this._invalidId = dispatchOp.connect('invalidated',
Lang.bind(this, function(domain, code, msg) {
this.destroy();
}));
},
_createPolicy: function() {
return new MessageTray.NotificationApplicationPolicy('empathy');
},
destroy: function() {
if (this._invalidId != 0) {
this._dispatchOp.disconnect(this._invalidId);
this._invalidId = 0;
}
this.parent();
},
getIcon: function() {
return this._gicon;
}
});
const RoomInviteNotification = new Lang.Class({
Name: 'RoomInviteNotification',
Extends: MessageTray.Notification,
_init: function(source, dispatchOp, channel, inviter) {
this.parent(source,
/* translators: argument is a room name like
* room@jabber.org for example. */
_("Invitation to %s").format(channel.get_identifier()),
null,
{ customContent: true });
this.setResident(true);
/* translators: first argument is the name of a contact and the second
* one the name of a room. "Alice is inviting you to join room@jabber.org
* for example. */
this.addBody(_("%s is inviting you to join %s").format(inviter.get_alias(), channel.get_identifier()));
this.addAction(_("Decline"), Lang.bind(this, function() {
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE, '', function(src, result) {
src.leave_channels_finish(result);
});
this.destroy();
}));
this.addAction(_("Accept"), Lang.bind(this, function() {
dispatchOp.handle_with_time_async('', global.get_current_time(), function(src, result) {
src.handle_with_time_finish(result);
});
this.destroy();
}));
}
});
// Audio Video
const AudioVideoNotification = new Lang.Class({
Name: 'AudioVideoNotification',
Extends: MessageTray.Notification,
_init: function(source, dispatchOp, channel, contact, isVideo) {
let title = '';
if (isVideo)
/* translators: argument is a contact name like Alice for example. */
title = _("Video call from %s").format(contact.get_alias());
else
/* translators: argument is a contact name like Alice for example. */
title = _("Call from %s").format(contact.get_alias());
this.parent(source, title, null, { customContent: true });
this.setResident(true);
this.setUrgency(MessageTray.Urgency.CRITICAL);
this.addAction(_("Decline"), Lang.bind(this, function() {
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE, '', function(src, result) {
src.leave_channels_finish(result);
});
this.destroy();
}));
/* translators: this is a button label (verb), not a noun */
this.addAction(_("Answer"), Lang.bind(this, function() {
dispatchOp.handle_with_time_async('', global.get_current_time(), function(src, result) {
src.handle_with_time_finish(result);
});
this.destroy();
}));
}
});
// File Transfer
const FileTransferNotification = new Lang.Class({
Name: 'FileTransferNotification',
Extends: MessageTray.Notification,
_init: function(source, dispatchOp, channel, contact) {
this.parent(source,
/* To translators: The first parameter is
* the contact's alias and the second one is the
* file name. The string will be something
* like: "Alice is sending you test.ogg"
*/
_("%s is sending you %s").format(contact.get_alias(),
channel.get_filename()),
null,
{ customContent: true });
this.setResident(true);
this.addAction(_("Decline"), Lang.bind(this, function() {
dispatchOp.leave_channels_async(Tp.ChannelGroupChangeReason.NONE, '', function(src, result) {
src.leave_channels_finish(result);
});
this.destroy();
}));
this.addAction(_("Accept"), Lang.bind(this, function() {
dispatchOp.handle_with_time_async('', global.get_current_time(), function(src, result) {
src.handle_with_time_finish(result);
});
this.destroy();
}));
}
});
// Subscription request
const SubscriptionRequestNotification = new Lang.Class({
Name: 'SubscriptionRequestNotification',
Extends: MessageTray.Notification,
_init: function(source, contact) {
this.parent(source,
/* To translators: The parameter is the contact's alias */
_("%s would like permission to see when you are online").format(contact.get_alias()),
null, { customContent: true });
this._contact = contact;
this._connection = contact.get_connection();
let layout = new St.BoxLayout({ vertical: false });
// Display avatar
let iconBox = new St.Bin({ style_class: 'avatar-box' });
iconBox._size = 48;
let textureCache = St.TextureCache.get_default();
let file = contact.get_avatar_file();
if (file) {
let uri = file.get_uri();
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
iconBox.child = textureCache.load_uri_async(uri, iconBox._size, iconBox._size, scaleFactor);
}
else {
iconBox.child = new St.Icon({ icon_name: 'avatar-default',
icon_size: iconBox._size });
}
layout.add(iconBox);
// subscription request message
let label = new St.Label({ style_class: 'subscription-message',
text: contact.get_publish_request() });
layout.add(label);
this.addActor(layout);
this.addAction(_("Decline"), Lang.bind(this, function() {
contact.remove_async(function(src, result) {
src.remove_finish(result);
});
}));
this.addAction(_("Accept"), Lang.bind(this, function() {
// Authorize the contact and request to see his status as well
contact.authorize_publication_async(function(src, result) {
src.authorize_publication_finish(result);
});
contact.request_subscription_async('', function(src, result) {
src.request_subscription_finish(result);
});
}));
this._changedId = contact.connect('subscription-states-changed',
Lang.bind(this, this._subscriptionStatesChangedCb));
this._invalidatedId = this._connection.connect('invalidated',
Lang.bind(this, this.destroy));
},
destroy: function() {
if (this._changedId != 0) {
this._contact.disconnect(this._changedId);
this._changedId = 0;
}
if (this._invalidatedId != 0) {
this._connection.disconnect(this._invalidatedId);
this._invalidatedId = 0;
}
this.parent();
},
_subscriptionStatesChangedCb: function(contact, subscribe, publish, msg) {
// Destroy the notification if the subscription request has been
// answered
if (publish != Tp.SubscriptionState.ASK)
this.destroy();
}
});
// Messages from empathy/libempathy/empathy-utils.c
// create_errors_to_message_hash()
/* Translator note: these should be the same messages that are
* used in Empathy, so just copy and paste from there. */
let _connectionErrorMessages = {};
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.NETWORK_ERROR)]
= _("Network error");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.AUTHENTICATION_FAILED)]
= _("Authentication failed");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_ERROR)]
= _("Encryption error");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_PROVIDED)]
= _("Certificate not provided");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_UNTRUSTED)]
= _("Certificate untrusted");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_EXPIRED)]
= _("Certificate expired");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_NOT_ACTIVATED)]
= _("Certificate not activated");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_HOSTNAME_MISMATCH)]
= _("Certificate hostname mismatch");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_FINGERPRINT_MISMATCH)]
= _("Certificate fingerprint mismatch");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_SELF_SIGNED)]
= _("Certificate self-signed");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CANCELLED)]
= _("Status is set to offline");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ENCRYPTION_NOT_AVAILABLE)]
= _("Encryption is not available");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INVALID)]
= _("Certificate is invalid");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REFUSED)]
= _("Connection has been refused");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_FAILED)]
= _("Connection can't be established");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_LOST)]
= _("Connection has been lost");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.ALREADY_CONNECTED)]
= _("This account is already connected to the server");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CONNECTION_REPLACED)]
= _("Connection has been replaced by a new connection using the same resource");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.REGISTRATION_EXISTS)]
= _("The account already exists on the server");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.SERVICE_BUSY)]
= _("Server is currently too busy to handle the connection");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_REVOKED)]
= _("Certificate has been revoked");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_INSECURE)]
= _("Certificate uses an insecure cipher algorithm or is cryptographically weak");
_connectionErrorMessages[Tp.error_get_dbus_name(Tp.Error.CERT_LIMIT_EXCEEDED)]
= _("The length of the server certificate, or the depth of the server certificate chain, exceed the limits imposed by the cryptography library");
_connectionErrorMessages['org.freedesktop.DBus.Error.NoReply']
= _("Internal error");
const AccountNotification = new Lang.Class({
Name: 'AccountNotification',
Extends: MessageTray.Notification,
_init: function(source, account, connectionError) {
this.parent(source,
/* translators: argument is the account name, like
* name@jabber.org for example. */
_("Unable to connect to %s").format(account.get_display_name()),
this._getMessage(connectionError));
this._account = account;
this.addAction(_("View account"), Lang.bind(this, function() {
let cmd = 'empathy-accounts --select-account=' +
account.get_path_suffix();
let app_info = Gio.app_info_create_from_commandline(cmd, null, 0);
app_info.launch([], global.create_app_launch_context(0, -1));
}));
this._enabledId = account.connect('notify::enabled',
Lang.bind(this, function() {
if (!account.is_enabled())
this.destroy();
}));
this._invalidatedId = account.connect('invalidated',
Lang.bind(this, this.destroy));
this._connectionStatusId = account.connect('notify::connection-status',
Lang.bind(this, function() {
let status = account.connection_status;
if (status == Tp.ConnectionStatus.CONNECTED) {
this.destroy();
} else if (status == Tp.ConnectionStatus.DISCONNECTED) {
let connectionError = account.connection_error;
if (connectionError == Tp.error_get_dbus_name(Tp.Error.CANCELLED))
this.destroy();
else
this.update(this.title, this._getMessage(connectionError));
}
}));
},
_getMessage: function(connectionError) {
let message;
if (connectionError in _connectionErrorMessages) {
message = _connectionErrorMessages[connectionError];
} else {
message = _("Unknown reason");
}
return message;
},
destroy: function() {
if (this._enabledId != 0) {
this._account.disconnect(this._enabledId);
this._enabledId = 0;
}
if (this._invalidatedId != 0) {
this._account.disconnect(this._invalidatedId);
this._invalidatedId = 0;
}
if (this._connectionStatusId != 0) {
this._account.disconnect(this._connectionStatusId);
this._connectionStatusId = 0;
}
this.parent();
}
});
const Component = TelepathyClient; const Component = TelepathyClient;

View File

@ -111,7 +111,6 @@ const FocusGrabber = new Lang.Class({
if (this._focused) if (this._focused)
return; return;
this._prevFocusedWindow = global.display.focus_window;
this._prevKeyFocusActor = global.stage.get_key_focus(); this._prevKeyFocusActor = global.stage.get_key_focus();
this._focusActorChangedId = global.stage.connect('notify::key-focus', Lang.bind(this, this._focusActorChanged)); this._focusActorChangedId = global.stage.connect('notify::key-focus', Lang.bind(this, this._focusActorChanged));
@ -234,6 +233,10 @@ const URLHighlighter = new Lang.Class({
})); }));
}, },
hasText: function() {
return !!this._text;
},
setMarkup: function(text, allowMarkup) { setMarkup: function(text, allowMarkup) {
text = text ? _fixMarkup(text, allowMarkup) : ''; text = text ? _fixMarkup(text, allowMarkup) : '';
this._text = text; this._text = text;
@ -440,24 +443,11 @@ const NotificationApplicationPolicy = new Lang.Class({
// elements that were added to it or if the @banner text did not // elements that were added to it or if the @banner text did not
// fit fully in the banner mode. When the notification is expanded, // fit fully in the banner mode. When the notification is expanded,
// the @banner text from the top line is always removed. The complete // the @banner text from the top line is always removed. The complete
// @banner text is added as the first element in the content section, // @banner text is added to the notification by default. You can change
// unless 'customContent' parameter with the value 'true' is specified // what is displayed by setting the child of this._bodyBin.
// in @params.
// //
// Additional notification content can be added with addActor() and // You can also add buttons to the notification with addButton(),
// addBody() methods. The notification content is put inside a // and you can construct simple default buttons with addAction().
// scrollview, so if it gets too tall, the notification will scroll
// rather than continue to grow. In addition to this main content
// area, there is also a single-row action area, which is not
// scrolled and can contain a single actor. The action area can
// be set by calling setActionArea() method. There is also a
// convenience method addButton() for adding a button to the action
// area.
//
// If @params contains a 'customContent' parameter with the value %true,
// then @banner will not be shown in the body of the notification when the
// notification is expanded and calls to update() will not clear the content
// unless 'clear' parameter with value %true is explicitly specified.
// //
// By default, the icon shown is the same as the source's. // By default, the icon shown is the same as the source's.
// However, if @params contains a 'gicon' parameter, the passed in gicon // However, if @params contains a 'gicon' parameter, the passed in gicon
@ -473,8 +463,6 @@ const NotificationApplicationPolicy = new Lang.Class({
// //
// If @params contains a 'clear' parameter with the value %true, then // If @params contains a 'clear' parameter with the value %true, then
// the content and the action area of the notification will be cleared. // the content and the action area of the notification will be cleared.
// The content area is also always cleared if 'customContent' is false
// because it might contain the @banner that didn't fit in the banner mode.
// //
// If @params contains 'soundName' or 'soundFile', the corresponding // If @params contains 'soundName' or 'soundFile', the corresponding
// event sound is played when the notification is shown (if the policy for // event sound is played when the notification is shown (if the policy for
@ -482,15 +470,12 @@ const NotificationApplicationPolicy = new Lang.Class({
const Notification = new Lang.Class({ const Notification = new Lang.Class({
Name: 'Notification', Name: 'Notification',
ICON_SIZE: 24, ICON_SIZE: 32,
IMAGE_SIZE: 125,
_init: function(source, title, banner, params) { _init: function(source, title, banner, params) {
this.source = source; this.source = source;
this.title = title; this.title = title;
this.urgency = Urgency.NORMAL; this.urgency = Urgency.NORMAL;
this.resident = false;
// 'transient' is a reserved keyword in JS, so we have to use an alternate variable name // 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
this.isTransient = false; this.isTransient = false;
this.isMusic = false; this.isMusic = false;
@ -499,59 +484,89 @@ const Notification = new Lang.Class({
this.focused = false; this.focused = false;
this.acknowledged = false; this.acknowledged = false;
this._destroyed = false; this._destroyed = false;
this._customContent = false;
this.bannerBodyText = null;
this.bannerBodyMarkup = false;
this._bannerBodyAdded = false;
this._titleFitsInBannerMode = true;
this._titleDirection = Clutter.TextDirection.DEFAULT;
this._spacing = 0;
this._scrollPolicy = Gtk.PolicyType.AUTOMATIC;
this._imageBin = null;
this._soundName = null; this._soundName = null;
this._soundFile = null; this._soundFile = null;
this._soundPlayed = false; this._soundPlayed = false;
this.actor = new St.Button({ accessible_role: Atk.Role.NOTIFICATION }); // Let me draw you a picture. I am a bad artist:
this.actor.add_style_class_name('notification-unexpanded'); //
// ,. this._iconBin ,. this._titleLabel
// | ,-- this._second|ryIconBin
// .----|--------|---------------|-----------.
// | .----. | .----.-------------------. | X |
// | | | | | | |-|----- this._titleBox
// | '....' | '....'...................' | |
// | | | |- this._hbox
// | | this._bodyBin | |-.
// |________|____________________________|___| |- this.actor
// | this._actionArea |-'
// |_________________________________________|
// | this._buttonBox |
// |_________________________________________|
this.actor = new St.BoxLayout({ vertical: true,
style_class: 'notification',
accessible_role: Atk.Role.NOTIFICATION });
this.actor._delegate = this; this.actor._delegate = this;
this.actor.connect('clicked', Lang.bind(this, this._onClicked));
this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
this._table = new St.Table({ style_class: 'notification', this._mainButton = new St.Button({ style_class: 'notification-main-button',
reactive: true }); can_focus: true,
this._table.connect('style-changed', Lang.bind(this, this._styleChanged)); x_fill: true, y_fill: true });
this.actor.set_child(this._table); this._mainButton.connect('clicked', Lang.bind(this, this._onClicked));
this.actor.add_child(this._mainButton);
// The first line should have the title, followed by the // Separates the icon, title/body and close button
// banner text, but ellipsized if they won't both fit. We can't this._hbox = new St.BoxLayout({ style_class: 'notification-main-content' });
// make St.Table or St.BoxLayout do this the way we want (don't this._mainButton.child = this._hbox;
// show banner at all if title needs to be ellipsized), so we
// use Shell.GenericContainer.
this._bannerBox = new Shell.GenericContainer();
this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
this._table.add(this._bannerBox, { row: 0,
col: 1,
col_span: 2,
x_expand: false,
y_expand: false,
y_fill: false });
// This is an empty cell that overlaps with this._bannerBox cell to ensure this._iconBin = new St.Bin({ y_align: St.Align.START });
// that this._bannerBox cell expands horizontally, while not forcing the this._hbox.add_child(this._iconBin);
// this._imageBin that is also in col: 2 to expand horizontally.
this._table.add(new St.Bin(), { row: 0,
col: 2,
y_expand: false,
y_fill: false });
this._titleLabel = new St.Label(); this._titleBodyBox = new St.BoxLayout({ style_class: 'notification-title-body-box',
this._bannerBox.add_actor(this._titleLabel); vertical: true });
this._bannerUrlHighlighter = new URLHighlighter(); this._titleBodyBox.set_x_expand(true);
this._bannerLabel = this._bannerUrlHighlighter.actor; this._hbox.add_child(this._titleBodyBox);
this._bannerBox.add_actor(this._bannerLabel);
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);
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 });
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();
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);
// If called with only one argument we assume the caller // If called with only one argument we assume the caller
// will call .update() later on. This is the case of // will call .update() later on. This is the case of
@ -559,6 +574,31 @@ const Notification = new Lang.Class({
// for new and updated notifications // for new and updated notifications
if (arguments.length != 1) if (arguments.length != 1)
this.update(title, banner, params); this.update(title, banner, params);
this._sync();
},
_sync: function() {
this._actionAreaBin.visible = this.expanded && (this._actionArea != null);
this._buttonBox.visible = this.expanded && (this._buttonBox.get_n_children() > 0);
this._iconBin.visible = (this._icon != null && this._icon.visible);
this._secondaryIconBin.visible = (this._secondaryIcon != null);
if (this.expanded) {
this._titleLabel.clutter_text.line_wrap = true;
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
this._bodyUrlHighlighter.actor.clutter_text.line_wrap = true;
this._bodyUrlHighlighter.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
} else {
this._titleLabel.clutter_text.line_wrap = false;
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
this._bodyUrlHighlighter.actor.clutter_text.line_wrap = false;
this._bodyUrlHighlighter.actor.clutter_text.ellipsize = Pango.EllipsizeMode.END;
}
this.enableScrolling(this.expanded);
this._bodyUrlHighlighter.actor.visible = this._bodyUrlHighlighter.hasText();
}, },
// update: // update:
@ -570,52 +610,31 @@ const Notification = new Lang.Class({
// the title/banner. If @params.clear is %true, it will also // the title/banner. If @params.clear is %true, it will also
// remove any additional actors/action buttons previously added. // remove any additional actors/action buttons previously added.
update: function(title, banner, params) { update: function(title, banner, params) {
params = Params.parse(params, { customContent: false, params = Params.parse(params, { gicon: null,
gicon: null,
secondaryGIcon: null, secondaryGIcon: null,
bannerMarkup: false, bannerMarkup: false,
clear: false, clear: false,
soundName: null, soundName: null,
soundFile: null }); soundFile: null });
this._customContent = params.customContent;
let oldFocus = global.stage.key_focus; let oldFocus = global.stage.key_focus;
if (this._icon && (params.gicon || params.clear)) {
this._icon.destroy();
this._icon = null;
}
if (this._secondaryIcon && (params.secondaryGIcon || params.clear)) {
this._secondaryIcon.destroy();
this._secondaryIcon = null;
}
// We always clear the content area if we don't have custom
// content because it might contain the @banner that didn't
// fit in the banner mode.
if (this._scrollArea && (!this._customContent || params.clear)) {
if (oldFocus && this._scrollArea.contains(oldFocus))
this.actor.grab_key_focus();
this._scrollArea.destroy();
this._scrollArea = null;
this._contentArea = null;
}
if (this._actionArea && params.clear) { if (this._actionArea && params.clear) {
if (oldFocus && this._actionArea.contains(oldFocus)) if (oldFocus && this._actionArea.contains(oldFocus))
this.actor.grab_key_focus(); this.actor.grab_key_focus();
this._actionArea.destroy(); this._actionArea.destroy();
this._actionArea = null; this._actionArea = null;
this._buttonBox = null;
} }
if (params.clear)
this.unsetImage();
if (!this._scrollArea && !this._actionArea && !this._imageBin) if (params.clear) {
this._table.remove_style_class_name('multi-line-notification'); this._buttonBox.destroy_all_children();
}
if (this._icon && (params.gicon || params.clear)) {
this._icon.destroy();
this._icon = null;
}
if (params.gicon) { if (params.gicon) {
this._icon = new St.Icon({ gicon: params.gicon, this._icon = new St.Icon({ gicon: params.gicon,
@ -624,29 +643,29 @@ const Notification = new Lang.Class({
this._icon = this.source.createIcon(this.ICON_SIZE); this._icon = this.source.createIcon(this.ICON_SIZE);
} }
if (this._icon) { if (this._icon)
this._table.add(this._icon, { row: 0, this._iconBin.child = this._icon;
col: 0,
x_expand: false, if (this._secondaryIcon && (params.secondaryGIcon || params.clear)) {
y_expand: false, this._secondaryIcon.destroy();
y_fill: false, this._secondaryIcon = null;
y_align: St.Align.START });
} }
if (params.secondaryGIcon) { if (params.secondaryGIcon) {
this._secondaryIcon = new St.Icon({ gicon: params.secondaryGIcon, this._secondaryIcon = new St.Icon({ gicon: params.secondaryGIcon,
style_class: 'secondary-icon' }); style_class: 'secondary-icon' });
this._bannerBox.add_actor(this._secondaryIcon); this._secondaryIconBin.child = this._secondaryIcon;
} }
this.title = title; this.title = title;
title = title ? _fixMarkup(title.replace(/\n/g, ' '), false) : ''; title = title ? _fixMarkup(title.replace(/\n/g, ' '), false) : '';
this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>'); this._titleLabel.clutter_text.set_markup('<b>' + title + '</b>');
let titleDirection;
if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL) if (Pango.find_base_dir(title, -1) == Pango.Direction.RTL)
this._titleDirection = Clutter.TextDirection.RTL; titleDirection = Clutter.TextDirection.RTL;
else else
this._titleDirection = Clutter.TextDirection.LTR; titleDirection = Clutter.TextDirection.LTR;
// Let the title's text direction control the overall direction // Let the title's text direction control the overall direction
// of the notification - in case where different scripts are used // of the notification - in case where different scripts are used
@ -654,24 +673,9 @@ const Notification = new Lang.Class({
// arguably for action buttons as well. Labels other than the title // arguably for action buttons as well. Labels other than the title
// will be allocated at the available width, so that their alignment // will be allocated at the available width, so that their alignment
// is done correctly automatically. // is done correctly automatically.
this._table.set_text_direction(this._titleDirection); this.actor.set_text_direction(titleDirection);
// Unless the notification has custom content, we save this.bannerBodyText this._bodyUrlHighlighter.setMarkup(banner, params.bannerMarkup);
// to add it to the content of the notification if the notification is
// expandable due to other elements in its content area or due to the banner
// not fitting fully in the single-line mode.
this.bannerBodyText = this._customContent ? null : banner;
this.bannerBodyMarkup = params.bannerMarkup;
this._bannerBodyAdded = false;
banner = banner ? banner.replace(/\n/g, ' ') : '';
this._bannerUrlHighlighter.setMarkup(banner, params.bannerMarkup);
this._bannerLabel.queue_relayout();
// Add the bannerBody now if we know for sure we'll need it
if (this.bannerBodyText && this.bannerBodyText.indexOf('\n') > -1)
this._addBannerBody();
if (this._soundName != params.soundName || if (this._soundName != params.soundName ||
this._soundFile != params.soundFile) { this._soundFile != params.soundFile) {
@ -680,71 +684,18 @@ const Notification = new Lang.Class({
this._soundPlayed = false; this._soundPlayed = false;
} }
this.updated(); this._sync();
}, },
setIconVisible: function(visible) { setIconVisible: function(visible) {
this._icon.visible = visible; this._icon.visible = visible;
this._sync();
}, },
enableScrolling: function(enableScrolling) { enableScrolling: function(enableScrolling) {
this._scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER; let scrollPolicy = enableScrolling ? Gtk.PolicyType.AUTOMATIC : Gtk.PolicyType.NEVER;
if (this._scrollArea) { this._bodyScrollArea.vscrollbar_policy = scrollPolicy;
this._scrollArea.vscrollbar_policy = this._scrollPolicy; this._bodyScrollArea.enable_mouse_scrolling = enableScrolling;
this._scrollArea.enable_mouse_scrolling = enableScrolling;
}
},
_createScrollArea: function() {
this._table.add_style_class_name('multi-line-notification');
this._scrollArea = new St.ScrollView({ style_class: 'notification-scrollview',
vscrollbar_policy: this._scrollPolicy,
hscrollbar_policy: Gtk.PolicyType.NEVER,
visible: this.expanded });
this._table.add(this._scrollArea, { row: 1,
col: 2 });
this._updateLastColumnSettings();
this._contentArea = new St.BoxLayout({ style_class: 'notification-body',
vertical: true });
this._scrollArea.add_actor(this._contentArea);
// If we know the notification will be expandable, we need to add
// the banner text to the body as the first element.
this._addBannerBody();
},
// addActor:
// @actor: actor to add to the body of the notification
//
// Appends @actor to the notification's body
addActor: function(actor, style) {
if (!this._scrollArea) {
this._createScrollArea();
}
this._contentArea.add(actor, style ? style : {});
this.updated();
},
// addBody:
// @text: the text
// @markup: %true if @text contains pango markup
// @style: style to use when adding the actor containing the text
//
// Adds a multi-line label containing @text to the notification.
//
// Return value: the newly-added label
addBody: function(text, markup, style) {
let label = new URLHighlighter(text, true, markup);
this.addActor(label.actor, style);
return label.actor;
},
_addBannerBody: function() {
if (this.bannerBodyText && !this._bannerBodyAdded) {
this._bannerBodyAdded = true;
this.addBody(this.bannerBodyText, this.bannerBodyMarkup);
}
}, },
// scrollTo: // scrollTo:
@ -752,112 +703,32 @@ const Notification = new Lang.Class({
// //
// Scrolls the content area (if scrollable) to the indicated edge // Scrolls the content area (if scrollable) to the indicated edge
scrollTo: function(side) { scrollTo: function(side) {
let adjustment = this._scrollArea.vscroll.adjustment; let adjustment = this._bodyScrollArea.vscroll.adjustment;
if (side == St.Side.TOP) if (side == St.Side.TOP)
adjustment.value = adjustment.lower; adjustment.value = adjustment.lower;
else if (side == St.Side.BOTTOM) else if (side == St.Side.BOTTOM)
adjustment.value = adjustment.upper; adjustment.value = adjustment.upper;
}, },
// setActionArea: setActionArea: function(actor) {
// @actor: the actor
// @props: (option) St.Table child properties
//
// Puts @actor into the action area of the notification, replacing
// the previous contents
setActionArea: function(actor, props) {
if (this._actionArea) {
this._actionArea.destroy();
this._actionArea = null;
if (this._buttonBox)
this._buttonBox = null;
} else {
this._addBannerBody();
}
this._actionArea = actor;
this._actionArea.visible = this.expanded;
if (!props)
props = {};
props.row = 2;
props.col = 2;
this._table.add_style_class_name('multi-line-notification');
this._table.add(this._actionArea, props);
this._updateLastColumnSettings();
this.updated();
},
_updateLastColumnSettings: function() {
if (this._scrollArea)
this._table.child_set(this._scrollArea, { col: this._imageBin ? 2 : 1,
col_span: this._imageBin ? 1 : 2 });
if (this._actionArea) if (this._actionArea)
this._table.child_set(this._actionArea, { col: this._imageBin ? 2 : 1, this._actionArea.destroy();
col_span: this._imageBin ? 1 : 2 });
},
setImage: function(image) { this._actionArea = actor;
this.unsetImage(); this._actionAreaBin.child = actor;
this._sync();
if (!image)
return;
this._imageBin = new St.Bin({ opacity: 230,
child: image,
visible: this.expanded });
this._table.add_style_class_name('multi-line-notification');
this._table.add_style_class_name('notification-with-image');
this._addBannerBody();
this._updateLastColumnSettings();
this._table.add(this._imageBin, { row: 1,
col: 1,
row_span: 2,
x_expand: false,
y_expand: false,
x_fill: false,
y_fill: false });
},
unsetImage: function() {
if (this._imageBin) {
this._table.remove_style_class_name('notification-with-image');
this._table.remove_actor(this._imageBin);
this._imageBin = null;
this._updateLastColumnSettings();
if (!this._scrollArea && !this._actionArea)
this._table.remove_style_class_name('multi-line-notification');
}
}, },
addButton: function(button, callback) { addButton: function(button, callback) {
if (!this._buttonBox) {
let box = new St.BoxLayout({ style_class: 'notification-actions' });
this.setActionArea(box, { x_expand: false,
y_expand: false,
x_fill: false,
y_fill: false,
x_align: St.Align.END });
this._buttonBox = box;
global.focus_manager.add_group(this._buttonBox);
}
this._buttonBox.add(button); this._buttonBox.add(button);
button.connect('clicked', Lang.bind(this, function() { button.connect('clicked', Lang.bind(this, function() {
callback(); callback();
if (!this.resident) { this.emit('done-displaying');
// We don't hide a resident notification when the user invokes one of its actions, this.destroy();
// because it is common for such notifications to update themselves with new
// information based on the action. We'd like to display the updated information
// in place, rather than pop-up a new notification.
this.emit('done-displaying');
this.destroy();
}
})); }));
this.updated(); this._sync();
return button; return button;
}, },
@ -870,8 +741,7 @@ const Notification = new Lang.Class({
// the notification. // the notification.
addAction: function(label, callback) { addAction: function(label, callback) {
let button = new St.Button({ style_class: 'notification-button', let button = new St.Button({ style_class: 'notification-button',
label: label, x_expand: true, label: label, can_focus: true });
can_focus: true });
return this.addButton(button, callback); return this.addButton(button, callback);
}, },
@ -880,10 +750,6 @@ const Notification = new Lang.Class({
this.urgency = urgency; this.urgency = urgency;
}, },
setResident: function(resident) {
this.resident = resident;
},
setTransient: function(isTransient) { setTransient: function(isTransient) {
this.isTransient = isTransient; this.isTransient = isTransient;
}, },
@ -911,113 +777,6 @@ const Notification = new Lang.Class({
} }
}, },
_bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
[alloc.min_size, alloc.natural_size] =
this._titleLabel.get_preferred_height(forWidth);
},
_bannerBoxAllocate: function(actor, box, flags) {
let availWidth = box.x2 - box.x1;
let [titleMinW, titleNatW] = this._titleLabel.get_preferred_width(-1);
let [titleMinH, titleNatH] = this._titleLabel.get_preferred_height(availWidth);
let [bannerMinW, bannerNatW] = this._bannerLabel.get_preferred_width(availWidth);
let rtl = (this._titleDirection == Clutter.TextDirection.RTL);
let x = rtl ? availWidth : 0;
if (this._secondaryIcon) {
let [iconMinW, iconNatW] = this._secondaryIcon.get_preferred_width(-1);
let [iconMinH, iconNatH] = this._secondaryIcon.get_preferred_height(availWidth);
let secondaryIconBox = new Clutter.ActorBox();
let secondaryIconBoxW = Math.min(iconNatW, availWidth);
// allocate secondary icon box
if (rtl) {
secondaryIconBox.x1 = x - secondaryIconBoxW;
secondaryIconBox.x2 = x;
x = x - (secondaryIconBoxW + this._spacing);
} else {
secondaryIconBox.x1 = x;
secondaryIconBox.x2 = x + secondaryIconBoxW;
x = x + secondaryIconBoxW + this._spacing;
}
secondaryIconBox.y1 = 0;
// Using titleNatH ensures that the secondary icon is centered vertically
secondaryIconBox.y2 = titleNatH;
availWidth = availWidth - (secondaryIconBoxW + this._spacing);
this._secondaryIcon.allocate(secondaryIconBox, flags);
}
let titleBox = new Clutter.ActorBox();
let titleBoxW = Math.min(titleNatW, availWidth);
if (rtl) {
titleBox.x1 = availWidth - titleBoxW;
titleBox.x2 = availWidth;
} else {
titleBox.x1 = x;
titleBox.x2 = titleBox.x1 + titleBoxW;
}
titleBox.y1 = 0;
titleBox.y2 = titleNatH;
this._titleLabel.allocate(titleBox, flags);
this._titleFitsInBannerMode = (titleNatW <= availWidth);
let bannerFits = true;
if (titleBoxW + this._spacing > availWidth) {
this._bannerLabel.opacity = 0;
bannerFits = false;
} else {
let bannerBox = new Clutter.ActorBox();
if (rtl) {
bannerBox.x1 = 0;
bannerBox.x2 = titleBox.x1 - this._spacing;
bannerFits = (bannerBox.x2 - bannerNatW >= 0);
} else {
bannerBox.x1 = titleBox.x2 + this._spacing;
bannerBox.x2 = availWidth;
bannerFits = (bannerBox.x1 + bannerNatW <= availWidth);
}
bannerBox.y1 = 0;
bannerBox.y2 = titleNatH;
this._bannerLabel.allocate(bannerBox, flags);
// Make _bannerLabel visible if the entire notification
// fits on one line, or if the notification is currently
// unexpanded and only showing one line anyway.
if (!this.expanded || (bannerFits && this._table.row_count == 1))
this._bannerLabel.opacity = 255;
}
// If the banner doesn't fully fit in the banner box, we possibly need to add the
// banner to the body. We can't do that from here though since that will force a
// relayout, so we add it to the main loop.
if (!bannerFits && this._canExpandContent())
Meta.later_add(Meta.LaterType.BEFORE_REDRAW,
Lang.bind(this,
function() {
if (this._destroyed)
return false;
if (this._canExpandContent()) {
this._addBannerBody();
this._table.add_style_class_name('multi-line-notification');
this.updated();
}
return false;
}));
},
_canExpandContent: function() {
return (this.bannerBodyText && !this._bannerBodyAdded) ||
(!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification'));
},
playSound: function() { playSound: function() {
if (this._soundPlayed) if (this._soundPlayed)
return; return;
@ -1050,83 +809,27 @@ const Notification = new Lang.Class({
} }
}, },
updated: function() {
if (this.expanded)
this.expand(false);
},
expand: function(animate) { expand: function(animate) {
this.expanded = true; this.expanded = true;
this.actor.remove_style_class_name('notification-unexpanded'); this._sync();
// Show additional content that we keep hidden in banner mode
if (this._imageBin)
this._imageBin.show();
if (this._actionArea)
this._actionArea.show();
if (this._scrollArea)
this._scrollArea.show();
// The banner is never shown when the title did not fit, so this
// can be an if-else statement.
if (!this._titleFitsInBannerMode) {
// Remove ellipsization from the title label and make it wrap so that
// we show the full title when the notification is expanded.
this._titleLabel.clutter_text.line_wrap = true;
this._titleLabel.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR;
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
} else if (this._table.row_count > 1 && this._bannerLabel.opacity != 0) {
// We always hide the banner if the notification has additional content.
//
// We don't need to wrap the banner that doesn't fit the way we wrap the
// title that doesn't fit because we won't have a notification with
// row_count=1 that has a banner that doesn't fully fit. We'll either add
// that banner to the content of the notification in _bannerBoxAllocate()
// or the notification will have custom content.
if (animate)
Tweener.addTween(this._bannerLabel,
{ opacity: 0,
time: ANIMATION_TIME,
transition: 'easeOutQuad' });
else
this._bannerLabel.opacity = 0;
}
this.emit('expanded');
}, },
collapseCompleted: function() { collapseCompleted: function() {
if (this._destroyed) if (this._destroyed)
return; return;
this.expanded = false; this.expanded = false;
this._sync();
// Hide additional content that we keep hidden in banner mode
if (this._imageBin)
this._imageBin.hide();
if (this._actionArea)
this._actionArea.hide();
if (this._scrollArea)
this._scrollArea.hide();
// Make sure we don't line wrap the title, and ellipsize it instead.
this._titleLabel.clutter_text.line_wrap = false;
this._titleLabel.clutter_text.ellipsize = Pango.EllipsizeMode.END;
// Restore banner opacity in case the notification is shown in the
// banner mode again on update.
this._bannerLabel.opacity = 255;
// Restore height requisition
this.actor.add_style_class_name('notification-unexpanded');
}, },
_onClicked: function() { _onClicked: function() {
this.emit('clicked'); this.emit('clicked');
// We hide all types of notifications once the user clicks on them because the common
// outcome of clicking should be the relevant window being brought forward and the user's
// attention switching to the window.
this.emit('done-displaying'); this.emit('done-displaying');
if (!this.resident) this.destroy();
this.destroy(); },
_onCloseClicked: function() {
this.destroy();
}, },
_onDestroy: function() { _onDestroy: function() {
@ -1294,7 +997,7 @@ const Source = new Lang.Class({
}, },
get indicatorCount() { get indicatorCount() {
let notifications = this.notifications.filter(function(n) { return !n.isTransient && !n.resident; }); let notifications = this.notifications.filter(function(n) { return !n.isTransient; });
return notifications.length; return notifications.length;
}, },
@ -1307,7 +1010,7 @@ const Source = new Lang.Class({
}, },
get isClearable() { get isClearable() {
return !this.trayIcon && !this.isChat && !this.resident; return !this.trayIcon && !this.isChat;
}, },
countUpdated: function() { countUpdated: function() {
@ -1451,10 +1154,9 @@ const Source = new Lang.Class({
open: function() { open: function() {
}, },
destroyNonResidentNotifications: function() { destroyNotifications: function() {
for (let i = this.notifications.length - 1; i >= 0; i--) for (let i = this.notifications.length - 1; i >= 0; i--)
if (!this.notifications[i].resident) this.notifications[i].destroy();
this.notifications[i].destroy();
this.countUpdated(); this.countUpdated();
}, },
@ -1786,6 +1488,9 @@ const MessageTray = new Lang.Class({
layout_manager: new Clutter.BinLayout() }); layout_manager: new Clutter.BinLayout() });
this._notificationWidget.connect('key-release-event', Lang.bind(this, this._onNotificationKeyRelease)); this._notificationWidget.connect('key-release-event', Lang.bind(this, this._onNotificationKeyRelease));
this._notificationWidget.connect('notify::hover', Lang.bind(this, this._onNotificationHoverChanged)); this._notificationWidget.connect('notify::hover', Lang.bind(this, this._onNotificationHoverChanged));
this._notificationWidget.connect('notify::height', Lang.bind(this, function() {
this._notificationWidget.translation_y = -this._notificationWidget.height;
}));
this._notificationBin = new St.Bin({ y_expand: true }); this._notificationBin = new St.Bin({ y_expand: true });
this._notificationBin.set_y_align(Clutter.ActorAlign.START); this._notificationBin.set_y_align(Clutter.ActorAlign.START);
@ -1829,11 +1534,6 @@ const MessageTray = new Lang.Class({
this._clickedSummaryItemMouseButton = -1; this._clickedSummaryItemMouseButton = -1;
this._clickedSummaryItemAllocationChangedId = 0; this._clickedSummaryItemAllocationChangedId = 0;
this._closeButton = Util.makeCloseButton();
this._closeButton.hide();
this._closeButton.connect('clicked', Lang.bind(this, this._closeNotification));
this._notificationWidget.add_actor(this._closeButton);
this._userActiveWhileNotificationShown = false; this._userActiveWhileNotificationShown = false;
this.idleMonitor = Meta.IdleMonitor.get_core(); this.idleMonitor = Meta.IdleMonitor.get_core();
@ -1864,7 +1564,6 @@ const MessageTray = new Lang.Class({
this._keyboardVisible = false; this._keyboardVisible = false;
this._notificationState = State.HIDDEN; this._notificationState = State.HIDDEN;
this._notificationTimeoutId = 0; this._notificationTimeoutId = 0;
this._notificationExpandedId = 0;
this._summaryBoxPointerState = State.HIDDEN; this._summaryBoxPointerState = State.HIDDEN;
this._summaryBoxPointerTimeoutId = 0; this._summaryBoxPointerTimeoutId = 0;
this._desktopCloneState = State.HIDDEN; this._desktopCloneState = State.HIDDEN;
@ -1888,7 +1587,6 @@ const MessageTray = new Lang.Class({
Main.layoutManager.trayBox.add_actor(this._notificationWidget); Main.layoutManager.trayBox.add_actor(this._notificationWidget);
Main.layoutManager.trackChrome(this.actor); Main.layoutManager.trackChrome(this.actor);
Main.layoutManager.trackChrome(this._notificationWidget); Main.layoutManager.trackChrome(this._notificationWidget);
Main.layoutManager.trackChrome(this._closeButton);
global.screen.connect('in-fullscreen-changed', Lang.bind(this, this._updateState)); global.screen.connect('in-fullscreen-changed', Lang.bind(this, this._updateState));
Main.layoutManager.connect('hot-corners-changed', Lang.bind(this, this._hotCornersChanged)); Main.layoutManager.connect('hot-corners-changed', Lang.bind(this, this._hotCornersChanged));
@ -2040,14 +1738,6 @@ const MessageTray = new Lang.Class({
this._updateState(); this._updateState();
}, },
_closeNotification: function() {
if (this._notificationState == State.SHOWN) {
this._closeButton.hide();
this._notification.emit('done-displaying');
this._notification.destroy();
}
},
contains: function(source) { contains: function(source) {
return this._sources.has(source); return this._sources.has(source);
}, },
@ -2613,7 +2303,6 @@ const MessageTray = new Lang.Class({
this._notificationBin.child = this._notification.actor; this._notificationBin.child = this._notification.actor;
this._notificationWidget.opacity = 0; this._notificationWidget.opacity = 0;
this._notificationWidget.y = 0;
this._notificationWidget.show(); this._notificationWidget.show();
this._updateShowingNotification(); this._updateShowingNotification();
@ -2648,23 +2337,16 @@ const MessageTray = new Lang.Class({
// We tween all notifications to full opacity. This ensures that both new notifications and // We tween all notifications to full opacity. This ensures that both new notifications and
// notifications that might have been in the process of hiding get full opacity. // notifications that might have been in the process of hiding get full opacity.
// //
// We tween any notification showing in the banner mode to the appropriate height
// (which is banner height or expanded height, depending on the notification state)
// This ensures that both new notifications and notifications in the banner mode that might
// have been in the process of hiding are shown with the correct height.
//
// We use this._showNotificationCompleted() onComplete callback to extend the time the updated // We use this._showNotificationCompleted() onComplete callback to extend the time the updated
// notification is being shown. // notification is being shown.
let tweenParams = { opacity: 255, this._tween(this._notificationWidget, '_notificationState', State.SHOWN,
y: -this._notificationWidget.height, { opacity: 255,
time: ANIMATION_TIME, time: ANIMATION_TIME,
transition: 'easeOutQuad', transition: 'easeOutQuad',
onComplete: this._showNotificationCompleted, onComplete: this._showNotificationCompleted,
onCompleteScope: this onCompleteScope: this
}; });
this._tween(this._notificationWidget, '_notificationState', State.SHOWN, tweenParams);
}, },
_showNotificationCompleted: function() { _showNotificationCompleted: function() {
@ -2712,10 +2394,6 @@ const MessageTray = new Lang.Class({
_hideNotification: function(animate) { _hideNotification: function(animate) {
this._notificationFocusGrabber.ungrabFocus(); this._notificationFocusGrabber.ungrabFocus();
if (this._notificationExpandedId) {
this._notification.disconnect(this._notificationExpandedId);
this._notificationExpandedId = 0;
}
if (this._notificationClickedId) { if (this._notificationClickedId) {
this._notification.disconnect(this._notificationClickedId); this._notification.disconnect(this._notificationClickedId);
this._notificationClickedId = 0; this._notificationClickedId = 0;
@ -2729,8 +2407,7 @@ const MessageTray = new Lang.Class({
if (animate) { if (animate) {
this._tween(this._notificationWidget, '_notificationState', State.HIDDEN, this._tween(this._notificationWidget, '_notificationState', State.HIDDEN,
{ y: this.actor.height, { opacity: 0,
opacity: 0,
time: ANIMATION_TIME, time: ANIMATION_TIME,
transition: 'easeOutQuad', transition: 'easeOutQuad',
onComplete: this._hideNotificationCompleted, onComplete: this._hideNotificationCompleted,
@ -2738,7 +2415,6 @@ const MessageTray = new Lang.Class({
}); });
} else { } else {
Tweener.removeTweens(this._notificationWidget); Tweener.removeTweens(this._notificationWidget);
this._notificationWidget.y = this.actor.height;
this._notificationWidget.opacity = 0; this._notificationWidget.opacity = 0;
this._notificationState = State.HIDDEN; this._notificationState = State.HIDDEN;
this._hideNotificationCompleted(); this._hideNotificationCompleted();
@ -2753,7 +2429,6 @@ const MessageTray = new Lang.Class({
if (notification.isTransient) if (notification.isTransient)
notification.destroy(NotificationDestroyedReason.EXPIRED); notification.destroy(NotificationDestroyedReason.EXPIRED);
this._closeButton.hide();
this._pointerInNotification = false; this._pointerInNotification = false;
this._notificationRemoved = false; this._notificationRemoved = false;
this._notificationBin.child = null; this._notificationBin.child = null;
@ -2768,10 +2443,6 @@ const MessageTray = new Lang.Class({
}, },
_expandNotification: function(autoExpanding) { _expandNotification: function(autoExpanding) {
if (!this._notificationExpandedId)
this._notificationExpandedId =
this._notification.connect('expanded',
Lang.bind(this, this._onNotificationExpanded));
// Don't animate changes in notifications that are auto-expanding. // Don't animate changes in notifications that are auto-expanding.
this._notification.expand(!autoExpanding); this._notification.expand(!autoExpanding);
@ -2780,31 +2451,6 @@ const MessageTray = new Lang.Class({
this._ensureNotificationFocused(); this._ensureNotificationFocused();
}, },
_onNotificationExpanded: function() {
let expandedY = - this._notificationWidget.height;
this._closeButton.show();
// Don't animate the notification to its new position if it has shrunk:
// there will be a very visible "gap" that breaks the illusion.
if (this._notificationWidget.y < expandedY) {
this._notificationWidget.y = expandedY;
} else if (this._notification.y != expandedY) {
// Tween also opacity here, to override a possible tween that's
// currently hiding the notification.
Tweener.addTween(this._notificationWidget,
{ y: expandedY,
opacity: 255,
time: ANIMATION_TIME,
transition: 'easeOutQuad',
// HACK: Drive the state machine here better,
// instead of overwriting tweens
onComplete: Lang.bind(this, function() {
this._notificationState = State.SHOWN;
}),
});
}
},
_ensureNotificationFocused: function() { _ensureNotificationFocused: function() {
this._notificationFocusGrabber.grabFocus(); this._notificationFocusGrabber.grabFocus();
}, },

View File

@ -91,21 +91,6 @@ const rewriteRules = {
] ]
}; };
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'
};
const FdoNotificationDaemon = new Lang.Class({ const FdoNotificationDaemon = new Lang.Class({
Name: 'FdoNotificationDaemon', Name: 'FdoNotificationDaemon',
@ -334,13 +319,14 @@ const FdoNotificationDaemon = new Lang.Class({
}, },
_makeButton: function(id, label, useActionIcons) { _makeButton: function(id, label, useActionIcons) {
let button = new St.Button({ can_focus: true }); let button = new St.Button({ can_focus: true,
x_expand: true,
style_class: 'notification-button' });
let iconName = id.endsWith('-symbolic') ? id : id + '-symbolic'; let iconName = id.endsWith('-symbolic') ? id : id + '-symbolic';
if (useActionIcons && Gtk.IconTheme.get_default().has_icon(iconName)) { if (useActionIcons && Gtk.IconTheme.get_default().has_icon(iconName)) {
button.add_style_class_name('notification-icon-button'); button.child = new St.Icon({ icon_name: iconName, icon_size: 16 });
button.child = new St.Icon({ icon_name: iconName });
} else { } else {
button.add_style_class_name('notification-button');
button.label = label; button.label = label;
} }
return button; return button;
@ -379,8 +365,6 @@ const FdoNotificationDaemon = new Lang.Class({
let gicon = this._iconForNotificationData(icon, hints); let gicon = this._iconForNotificationData(icon, hints);
let gimage = this._imageForNotificationData(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 // 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 // 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 // notify_notification_set_icon_from_pixbuf() from libnotify, which in turn sets
@ -389,10 +373,7 @@ const FdoNotificationDaemon = new Lang.Class({
// So the logic here does the right thing for this case. If both an icon and either // 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 // one of 'image-data' or 'image-path' are specified, we show both an icon and
// a large image. // a large image.
if (gicon && gimage) if (!gicon && gimage)
image = new St.Icon({ gicon: gimage,
icon_size: notification.IMAGE_SIZE });
else if (!gicon && gimage)
gicon = gimage; gicon = gimage;
else if (!gicon) else if (!gicon)
gicon = this._fallbackIconForNotificationData(hints); gicon = this._fallbackIconForNotificationData(hints);
@ -402,7 +383,6 @@ const FdoNotificationDaemon = new Lang.Class({
clear: true, clear: true,
soundFile: hints['sound-file'], soundFile: hints['sound-file'],
soundName: hints['sound-name'] }); soundName: hints['sound-name'] });
notification.setImage(image);
let hasDefaultAction = false; let hasDefaultAction = false;
@ -442,7 +422,6 @@ const FdoNotificationDaemon = new Lang.Class({
notification.setUrgency(MessageTray.Urgency.CRITICAL); notification.setUrgency(MessageTray.Urgency.CRITICAL);
break; break;
} }
notification.setResident(hints.resident == true);
// 'transient' is a reserved keyword in JS, so we have to retrieve the value // '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 // of the 'transient' hint with hints['transient'] rather than hints.transient
notification.setTransient(hints['transient'] == true); notification.setTransient(hints['transient'] == true);
@ -470,7 +449,6 @@ const FdoNotificationDaemon = new Lang.Class({
'body-markup', 'body-markup',
// 'icon-multi', // 'icon-multi',
'icon-static', 'icon-static',
'persistence',
'sound', 'sound',
]; ];
}, },
@ -492,7 +470,7 @@ const FdoNotificationDaemon = new Lang.Class({
for (let i = 0; i < this._sources.length; i++) { for (let i = 0; i < this._sources.length; i++) {
let source = this._sources[i]; let source = this._sources[i];
if (source.app == tracker.focus_app) { if (source.app == tracker.focus_app) {
source.destroyNonResidentNotifications(); source.destroyNotifications();
return; return;
} }
} }
@ -509,10 +487,6 @@ const FdoNotificationDaemon = new Lang.Class({
}, },
_onTrayIconAdded: function(o, icon) { _onTrayIconAdded: function(o, icon) {
let wmClass = icon.wm_class ? icon.wm_class.toLowerCase() : '';
if (STANDARD_TRAY_ICON_IMPLEMENTATIONS[wmClass] !== undefined)
return;
let source = this._getSource(icon.title || icon.wm_class || C_("program", "Unknown"), icon.pid, null, null, icon); let source = this._getSource(icon.title || icon.wm_class || C_("program", "Unknown"), icon.pid, null, null, icon);
}, },
@ -584,7 +558,7 @@ const FdoNotificationDaemonSource = new Lang.Class({
this.iconUpdated(); this.iconUpdated();
let tracker = Shell.WindowTracker.get_default(); let tracker = Shell.WindowTracker.get_default();
if (notification.resident && this.app && tracker.focus_app == this.app) if (this.app && tracker.focus_app == this.app)
this.pushNotification(notification); this.pushNotification(notification);
else else
this.notify(notification); this.notify(notification);
@ -651,7 +625,7 @@ const FdoNotificationDaemonSource = new Lang.Class({
open: function() { open: function() {
this.openApp(); this.openApp();
this.destroyNonResidentNotifications(); this.destroyNotifications();
}, },
_lastNotificationRemoved: function() { _lastNotificationRemoved: function() {

View File

@ -21,10 +21,6 @@ struct _ShellTpClientPrivate
ShellTpClientHandleChannelsImpl handle_channels_impl; ShellTpClientHandleChannelsImpl handle_channels_impl;
gpointer user_data_handle_channels; gpointer user_data_handle_channels;
GDestroyNotify destroy_handle_channels; GDestroyNotify destroy_handle_channels;
ShellTpClientContactListChangedImpl contact_list_changed_impl;
gpointer user_data_contact_list_changed;
GDestroyNotify destroy_contact_list_changed;
}; };
/** /**
@ -83,16 +79,6 @@ struct _ShellTpClientPrivate
* Signature of the implementation of the HandleChannels method. * Signature of the implementation of the HandleChannels method.
*/ */
/**
* ShellTpClientContactListChangedImpl:
* @connection: a #TpConnection having %TP_CONNECTION_FEATURE_CORE prepared
* if possible
* @added: (element-type TelepathyGLib.Contact): a #GPtrArray of added #TpContact
* @removed: (element-type TelepathyGLib.Contact): a #GPtrArray of removed #TpContact
*
* Signature of the implementation of the ContactListChanged method.
*/
static void static void
shell_tp_client_init (ShellTpClient *self) shell_tp_client_init (ShellTpClient *self)
{ {
@ -226,13 +212,6 @@ shell_tp_client_dispose (GObject *object)
self->priv->user_data_handle_channels = NULL; self->priv->user_data_handle_channels = NULL;
} }
if (self->priv->destroy_contact_list_changed != NULL)
{
self->priv->destroy_contact_list_changed (self->priv->user_data_contact_list_changed);
self->priv->destroy_contact_list_changed = NULL;
self->priv->user_data_contact_list_changed = NULL;
}
if (dispose != NULL) if (dispose != NULL)
dispose (object); dispose (object);
} }
@ -290,40 +269,3 @@ shell_tp_client_set_handle_channels_func (ShellTpClient *self,
self->priv->user_data_handle_channels = user_data; self->priv->user_data_handle_channels = user_data;
self->priv->destroy_handle_channels = destroy; self->priv->destroy_handle_channels = destroy;
} }
void
shell_tp_client_set_contact_list_changed_func (ShellTpClient *self,
ShellTpClientContactListChangedImpl contact_list_changed_impl,
gpointer user_data,
GDestroyNotify destroy)
{
g_assert (self->priv->contact_list_changed_impl == NULL);
self->priv->contact_list_changed_impl = contact_list_changed_impl;
self->priv->user_data_handle_channels = user_data;
self->priv->destroy_handle_channels = destroy;
}
static void
on_contact_list_changed (TpConnection *conn,
GPtrArray *added,
GPtrArray *removed,
gpointer user_data)
{
ShellTpClient *self = (ShellTpClient *) user_data;
g_assert (self->priv->contact_list_changed_impl != NULL);
self->priv->contact_list_changed_impl (conn,
added, removed,
self->priv->user_data_contact_list_changed);
}
void
shell_tp_client_grab_contact_list_changed (ShellTpClient *self,
TpConnection *conn)
{
g_signal_connect (conn, "contact-list-changed",
G_CALLBACK (on_contact_list_changed),
self);
}

View File

@ -86,19 +86,5 @@ void shell_tp_client_set_handle_channels_func (ShellTpClient *self,
gpointer user_data, gpointer user_data,
GDestroyNotify destroy); GDestroyNotify destroy);
typedef void (*ShellTpClientContactListChangedImpl) (
TpConnection *connection,
GPtrArray *added,
GPtrArray *removed,
gpointer user_data);
void shell_tp_client_set_contact_list_changed_func (ShellTpClient *self,
ShellTpClientContactListChangedImpl contact_list_changed_impl,
gpointer user_data,
GDestroyNotify destroy);
void shell_tp_client_grab_contact_list_changed (ShellTpClient *self,
TpConnection *conn);
G_END_DECLS G_END_DECLS
#endif /* __SHELL_TP_CLIENT_H__ */ #endif /* __SHELL_TP_CLIENT_H__ */