MessageTray: show multiple notifications per source
Add an ability to show multple notifications per source, so that the user doesn't miss seeing any notifications. https://bugzilla.gnome.org/show_bug.cgi?id=611611
This commit is contained in:
parent
f07fe0a8e7
commit
812812d817
@ -408,12 +408,12 @@ Notification.prototype = {
|
||||
this._spacing = 0;
|
||||
|
||||
source.connect('destroy', Lang.bind(this,
|
||||
// Avoid passing 'source' as an argument to this.destroy()
|
||||
function () {
|
||||
this.destroy();
|
||||
function (source, reason) {
|
||||
this.destroy(reason);
|
||||
}));
|
||||
|
||||
this.actor = new St.Button();
|
||||
this.actor._delegate = this;
|
||||
this.actor.connect('clicked', Lang.bind(this, this._onClicked));
|
||||
this.actor.connect('destroy', Lang.bind(this, this._onDestroy));
|
||||
|
||||
@ -516,6 +516,10 @@ Notification.prototype = {
|
||||
this._updated();
|
||||
},
|
||||
|
||||
setIconVisible: function(visible) {
|
||||
this._icon.visible = visible;
|
||||
},
|
||||
|
||||
_createScrollArea: function() {
|
||||
this._table.add_style_class_name('multi-line-notification');
|
||||
this._scrollArea = new St.ScrollView({ name: 'notification-scrollview',
|
||||
@ -806,6 +810,7 @@ Notification.prototype = {
|
||||
destroy: function(reason) {
|
||||
this._destroyedReason = reason;
|
||||
this.actor.destroy();
|
||||
this.actor._delegate = null;
|
||||
}
|
||||
};
|
||||
Signals.addSignalMethods(Notification.prototype);
|
||||
@ -825,6 +830,8 @@ Source.prototype = {
|
||||
y_fill: true });
|
||||
this.isTransient = false;
|
||||
this.isChat = false;
|
||||
|
||||
this.notifications = [];
|
||||
},
|
||||
|
||||
setTransient: function(isTransient) {
|
||||
@ -845,23 +852,21 @@ Source.prototype = {
|
||||
},
|
||||
|
||||
pushNotification: function(notification) {
|
||||
if (this.notification) {
|
||||
this.notification.disconnect(this._notificationClickedId);
|
||||
this.notification.disconnect(this._notificationDestroyedId);
|
||||
if (this.notifications.indexOf(notification) < 0) {
|
||||
this.notifications.push(notification);
|
||||
this.emit('notification-added', notification);
|
||||
}
|
||||
|
||||
// FIXME: Right now, we don't save multiple notifications.
|
||||
this.notification = notification;
|
||||
|
||||
this._notificationClickedId = notification.connect('clicked', Lang.bind(this, this.open));
|
||||
this._notificationDestroyedId = notification.connect('destroy', Lang.bind(this,
|
||||
notification.connect('clicked', Lang.bind(this, this.open));
|
||||
notification.connect('destroy', Lang.bind(this,
|
||||
function () {
|
||||
if (this.notification == notification) {
|
||||
this.notification = null;
|
||||
this._notificationDestroyedId = 0;
|
||||
this._notificationClickedId = 0;
|
||||
this._notificationRemoved();
|
||||
}
|
||||
let index = this.notifications.indexOf(notification);
|
||||
if (index < 0)
|
||||
return;
|
||||
|
||||
this.notifications.splice(index, 1);
|
||||
if (this.notifications.length == 0)
|
||||
this._lastNotificationRemoved();
|
||||
}));
|
||||
},
|
||||
|
||||
@ -870,8 +875,8 @@ Source.prototype = {
|
||||
this.emit('notify', notification);
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this.emit('destroy');
|
||||
destroy: function(reason) {
|
||||
this.emit('destroy', reason);
|
||||
},
|
||||
|
||||
// A subclass can redefine this to "steal" clicks from the
|
||||
@ -895,8 +900,14 @@ Source.prototype = {
|
||||
open: function(notification) {
|
||||
},
|
||||
|
||||
destroyNonResidentNotifications: function() {
|
||||
for (let i = this.notifications.length - 1; i >= 0; i--)
|
||||
if (!this.notifications[i].resident)
|
||||
this.notifications[i].destroy();
|
||||
},
|
||||
|
||||
// Default implementation is to destroy this source, but subclasses can override
|
||||
_notificationRemoved: function() {
|
||||
_lastNotificationRemoved: function() {
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
@ -909,6 +920,8 @@ function SummaryItem(source) {
|
||||
SummaryItem.prototype = {
|
||||
_init: function(source) {
|
||||
this.source = source;
|
||||
this.source.connect('notification-added', Lang.bind(this, this._notificationAddedToSource));
|
||||
|
||||
this.actor = new St.Button({ style_class: 'summary-source-button',
|
||||
y_fill: true,
|
||||
reactive: true,
|
||||
@ -930,6 +943,13 @@ SummaryItem.prototype = {
|
||||
this._sourceBox.add(this._sourceIcon, { y_fill: false });
|
||||
this._sourceBox.add(this._sourceTitleBin, { expand: true, y_fill: false });
|
||||
this.actor.child = this._sourceBox;
|
||||
|
||||
this.notificationStack = new St.BoxLayout({ name: 'summary-notification-stack',
|
||||
vertical: true });
|
||||
this._notificationExpandedIds = [];
|
||||
this._notificationDoneDisplayingIds = [];
|
||||
this._notificationDestroyedIds = [];
|
||||
|
||||
this.rightClickMenu = new St.BoxLayout({ name: 'summary-right-click-menu',
|
||||
vertical: true });
|
||||
|
||||
@ -938,14 +958,14 @@ SummaryItem.prototype = {
|
||||
item = new PopupMenu.PopupMenuItem(_("Open"));
|
||||
item.connect('activate', Lang.bind(this, function() {
|
||||
source.open();
|
||||
this.emit('right-click-menu-done-displaying');
|
||||
this.emit('done-displaying-content');
|
||||
}));
|
||||
this.rightClickMenu.add(item.actor);
|
||||
|
||||
item = new PopupMenu.PopupMenuItem(_("Remove"));
|
||||
item.connect('activate', Lang.bind(this, function() {
|
||||
source.destroy();
|
||||
this.emit('right-click-menu-done-displaying');
|
||||
this.emit('done-displaying-content');
|
||||
}));
|
||||
this.rightClickMenu.add(item.actor);
|
||||
|
||||
@ -975,6 +995,72 @@ SummaryItem.prototype = {
|
||||
|
||||
setEllipsization: function(mode) {
|
||||
this._sourceTitle.clutter_text.ellipsize = mode;
|
||||
},
|
||||
|
||||
prepareNotificationStackForShowing: function() {
|
||||
if (this.notificationStack.get_children().length > 0)
|
||||
return;
|
||||
|
||||
for (let i = 0; i < this.source.notifications.length; i++) {
|
||||
this._appendNotificationToStack(this.source.notifications[i]);
|
||||
}
|
||||
},
|
||||
|
||||
doneShowingNotificationStack: function() {
|
||||
let notificationActors = this.notificationStack.get_children();
|
||||
for (let i = 0; i < notificationActors.length; i++) {
|
||||
notificationActors[i]._delegate.collapseCompleted();
|
||||
notificationActors[i]._delegate.disconnect(this._notificationExpandedIds[i]);
|
||||
notificationActors[i]._delegate.disconnect(this._notificationDoneDisplayingIds[i]);
|
||||
notificationActors[i]._delegate.disconnect(this._notificationDestroyedIds[i]);
|
||||
this.notificationStack.remove_actor(notificationActors[i]);
|
||||
notificationActors[i]._delegate.setIconVisible(true);
|
||||
}
|
||||
this._notificationExpandedIds = [];
|
||||
this._notificationDoneDisplayingIds = [];
|
||||
this._notificationDestroyedIds = [];
|
||||
},
|
||||
|
||||
_notificationAddedToSource: function(source, notification) {
|
||||
if (this.notificationStack.mapped)
|
||||
this._appendNotificationToStack(notification);
|
||||
},
|
||||
|
||||
_appendNotificationToStack: function(notification) {
|
||||
let notificationExpandedId = notification.connect('expanded', Lang.bind(this, this._contentUpdated));
|
||||
this._notificationExpandedIds.push(notificationExpandedId);
|
||||
let notificationDoneDisplayingId = notification.connect('done-displaying', Lang.bind(this, this._notificationDoneDisplaying));
|
||||
this._notificationDoneDisplayingIds.push(notificationDoneDisplayingId);
|
||||
let notificationDestroyedId = notification.connect('destroy', Lang.bind(this, this._notificationDestroyed));
|
||||
this._notificationDestroyedIds.push(notificationDestroyedId);
|
||||
if (this.notificationStack.get_children().length > 0)
|
||||
notification.setIconVisible(false);
|
||||
this.notificationStack.add(notification.actor);
|
||||
notification.expand(false);
|
||||
},
|
||||
|
||||
_contentUpdated: function() {
|
||||
this.emit('content-updated');
|
||||
},
|
||||
|
||||
_notificationDoneDisplaying: function() {
|
||||
this.emit('done-displaying-content');
|
||||
},
|
||||
|
||||
_notificationDestroyed: function(notification) {
|
||||
let index = this.notificationStack.get_children().indexOf(notification.actor);
|
||||
if (index >= 0) {
|
||||
notification.disconnect(this._notificationExpandedIds[index]);
|
||||
this._notificationExpandedIds.splice(index, 1);
|
||||
notification.disconnect(this._notificationDoneDisplayingIds[index]);
|
||||
this._notificationDoneDisplayingIds.splice(index, 1);
|
||||
notification.disconnect(this._notificationDestroyedIds[index]);
|
||||
this._notificationDestroyedIds.splice(index, 1);
|
||||
this.notificationStack.remove_actor(notification.actor);
|
||||
this._contentUpdated();
|
||||
}
|
||||
if (this.notificationStack.get_children().length > 0)
|
||||
this.notificationStack.get_children()[0]._delegate.setIconVisible(true);
|
||||
}
|
||||
};
|
||||
Signals.addSignalMethods(SummaryItem.prototype);
|
||||
@ -1023,9 +1109,9 @@ MessageTray.prototype = {
|
||||
this._summaryBoxPointer.actor.lower_bottom();
|
||||
this._summaryBoxPointer.actor.hide();
|
||||
|
||||
this._summaryNotification = null;
|
||||
this._summaryNotificationClickedId = 0;
|
||||
this._summaryRightClickMenuClickedId = 0;
|
||||
this._summaryBoxPointerItem = null;
|
||||
this._summaryBoxPointerContentUpdatedId = 0;
|
||||
this._summaryBoxPointerDoneDisplayingId = 0;
|
||||
this._clickedSummaryItem = null;
|
||||
this._clickedSummaryItemMouseButton = -1;
|
||||
this._clickedSummaryItemAllocationChangedId = 0;
|
||||
@ -1066,11 +1152,10 @@ MessageTray.prototype = {
|
||||
this._notificationTimeoutId = 0;
|
||||
this._notificationExpandedId = 0;
|
||||
this._summaryBoxPointerState = State.HIDDEN;
|
||||
this._summaryNotificationTimeoutId = 0;
|
||||
this._summaryNotificationExpandedId = 0;
|
||||
this._summaryBoxPointerTimeoutId = 0;
|
||||
this._overviewVisible = Main.overview.visible;
|
||||
this._notificationRemoved = false;
|
||||
this._reNotifyWithSummaryNotificationAfterHide = false;
|
||||
this._reNotifyAfterHideNotification = null;
|
||||
|
||||
Main.chrome.addActor(this.actor, { affectsStruts: false,
|
||||
visibleInOverview: true });
|
||||
@ -1251,13 +1336,6 @@ MessageTray.prototype = {
|
||||
|
||||
if (needUpdate);
|
||||
this._updateState();
|
||||
|
||||
// remove all notifications with this source from the queue
|
||||
let newNotificationQueue = [];
|
||||
for (let i = this._notificationQueue.length - 1; i >= 0; i--) {
|
||||
if (this._notificationQueue[i].source == source)
|
||||
this._notificationQueue[i].destroy();
|
||||
}
|
||||
},
|
||||
|
||||
_onNotificationDestroy: function(notification) {
|
||||
@ -1286,14 +1364,18 @@ MessageTray.prototype = {
|
||||
},
|
||||
|
||||
_onNotify: function(source, notification) {
|
||||
if (notification == this._summaryNotification) {
|
||||
if (!this._summaryNotificationExpandedId)
|
||||
// We must be in the process of hiding the summary notification.
|
||||
// If the summary notification is updated while it is being
|
||||
// hidden, we show the update as a new notification. However,
|
||||
// we must first wait till the hide is complete and the
|
||||
// notification actor is not part of the stage.
|
||||
this._reNotifyWithSummaryNotificationAfterHide = true;
|
||||
if (this._summaryBoxPointerItem && this._summaryBoxPointerItem.source == source) {
|
||||
if (this._summaryBoxPointerState == State.HIDING)
|
||||
// We are in the process of hiding the summary box pointer.
|
||||
// If there is an update for one of the notifications or
|
||||
// a new notification to be added to the notification stack
|
||||
// while it is in the process of being hidden, we show it as
|
||||
// a new notification. However, we first wait till the hide
|
||||
// is complete. This is especially important if one of the
|
||||
// notifications in the stack was updated because we will
|
||||
// need to be able to re-parent its actor to a different
|
||||
// part of the stage.
|
||||
this._reNotifyAfterHideNotification = notification;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1629,15 +1711,18 @@ MessageTray.prototype = {
|
||||
let summarySourceIsMainNotificationSource = (haveClickedSummaryItem && this._notification &&
|
||||
this._clickedSummaryItem.source == this._notification.source);
|
||||
let canShowSummaryBoxPointer = this._summaryState == State.SHOWN;
|
||||
let wrongSummaryNotification = (this._clickedSummaryItemMouseButton == 1 &&
|
||||
this._summaryNotification != this._clickedSummaryItem.source.notification);
|
||||
// We only have sources with empty notification stacks for legacy tray icons. Currently, we never attempt
|
||||
// to show notifications for legacy tray icons, but this would be necessary if we did.
|
||||
let requestedNotificationStackIsEmpty = (this._clickedSummaryItemMouseButton == 1 && this._clickedSummaryItem.source.notifications.length == 0);
|
||||
let wrongSummaryNotificationStack = (this._clickedSummaryItemMouseButton == 1 &&
|
||||
this._summaryBoxPointer.bin.child != this._clickedSummaryItem.notificationStack);
|
||||
let wrongSummaryRightClickMenu = (this._clickedSummaryItemMouseButton == 3 &&
|
||||
this._summaryBoxPointer.bin.child != this._clickedSummaryItem.rightClickMenu);
|
||||
let wrongSummaryBoxPointer = (haveClickedSummaryItem &&
|
||||
(wrongSummaryNotification || wrongSummaryRightClickMenu));
|
||||
(wrongSummaryNotificationStack || wrongSummaryRightClickMenu));
|
||||
|
||||
if (this._summaryBoxPointerState == State.HIDDEN) {
|
||||
if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer)
|
||||
if (haveClickedSummaryItem && !summarySourceIsMainNotificationSource && canShowSummaryBoxPointer && !requestedNotificationStackIsEmpty)
|
||||
this._showSummaryBoxPointer();
|
||||
} else if (this._summaryBoxPointerState == State.SHOWN) {
|
||||
if (!haveClickedSummaryItem || !canShowSummaryBoxPointer || wrongSummaryBoxPointer)
|
||||
@ -1900,33 +1985,28 @@ MessageTray.prototype = {
|
||||
},
|
||||
|
||||
_showSummaryBoxPointer: function() {
|
||||
this._summaryBoxPointerItem = this._clickedSummaryItem;
|
||||
this._summaryBoxPointerContentUpdatedId = this._summaryBoxPointerItem.connect('content-updated',
|
||||
Lang.bind(this, this._adjustSummaryBoxPointerPosition));
|
||||
this._summaryBoxPointerDoneDisplayingId = this._summaryBoxPointerItem.connect('done-displaying-content',
|
||||
Lang.bind(this, this._escapeTray));
|
||||
if (this._clickedSummaryItemMouseButton == 1) {
|
||||
let clickedSummaryItemNotification = this._clickedSummaryItem.source.notification;
|
||||
let index = this._notificationQueue.indexOf(clickedSummaryItemNotification);
|
||||
if (index != -1)
|
||||
this._notificationQueue.splice(index, 1);
|
||||
|
||||
this._summaryNotification = clickedSummaryItemNotification;
|
||||
this._summaryNotificationClickedId = this._summaryNotification.connect('done-displaying',
|
||||
Lang.bind(this, this._escapeTray));
|
||||
this._summaryBoxPointer.bin.child = this._summaryNotification.actor;
|
||||
if (!this._summaryNotificationExpandedId)
|
||||
this._summaryNotificationExpandedId = this._summaryNotification.connect('expanded',
|
||||
Lang.bind(this, this._onSummaryBoxPointerExpanded));
|
||||
this._summaryNotification.expand(false);
|
||||
this._notificationQueue = this._notificationQueue.filter( Lang.bind(this,
|
||||
function(notification) {
|
||||
return this._summaryBoxPointerItem.source != notification.source;
|
||||
}));
|
||||
this._summaryBoxPointerItem.prepareNotificationStackForShowing();
|
||||
this._summaryBoxPointer.bin.child = this._summaryBoxPointerItem.notificationStack;
|
||||
} else if (this._clickedSummaryItemMouseButton == 3) {
|
||||
this._summaryRightClickMenuClickedId = this._clickedSummaryItem.connect('right-click-menu-done-displaying',
|
||||
Lang.bind(this, this._escapeTray));
|
||||
this._summaryBoxPointer.bin.child = this._clickedSummaryItem.rightClickMenu;
|
||||
}
|
||||
|
||||
this._focusGrabber.grabFocus(this._summaryBoxPointer.bin.child);
|
||||
|
||||
|
||||
this._clickedSummaryItemAllocationChangedId =
|
||||
this._clickedSummaryItem.actor.connect('allocation-changed',
|
||||
Lang.bind(this, this._adjustSummaryBoxPointerPosition));
|
||||
// _clickedSummaryItem.actor can change absolute postiion without changing allocation
|
||||
// _clickedSummaryItem.actor can change absolute position without changing allocation
|
||||
this._summaryMotionId = this._summary.connect('allocation-changed',
|
||||
Lang.bind(this, this._adjustSummaryBoxPointerPosition));
|
||||
|
||||
@ -1957,26 +2037,13 @@ MessageTray.prototype = {
|
||||
this._summaryMotionId = 0;
|
||||
}
|
||||
|
||||
if (this._summaryRightClickMenuClickedId) {
|
||||
this._clickedSummaryItem.disconnect(this._summaryRightClickMenuClickedId);
|
||||
this._summaryRightClickMenuClickedId = 0;
|
||||
}
|
||||
|
||||
if (this._clickedSummaryItem)
|
||||
this._clickedSummaryItem.actor.remove_style_pseudo_class('selected');
|
||||
this._clickedSummaryItem = null;
|
||||
this._clickedSummaryItemMouseButton = -1;
|
||||
},
|
||||
|
||||
_onSummaryBoxPointerExpanded: function() {
|
||||
this._adjustSummaryBoxPointerPosition();
|
||||
},
|
||||
|
||||
_hideSummaryBoxPointer: function() {
|
||||
if (this._summaryNotificationExpandedId) {
|
||||
this._summaryNotification.disconnect(this._summaryNotificationExpandedId);
|
||||
this._summaryNotificationExpandedId = 0;
|
||||
}
|
||||
// Unset this._clickedSummaryItem if we are no longer showing the summary
|
||||
if (this._summaryState != State.SHOWN)
|
||||
this._unsetClickedSummaryItem();
|
||||
@ -1987,21 +2054,32 @@ MessageTray.prototype = {
|
||||
},
|
||||
|
||||
_hideSummaryBoxPointerCompleted: function() {
|
||||
let doneShowingNotificationStack = (this._summaryBoxPointer.bin.child == this._summaryBoxPointerItem.notificationStack);
|
||||
|
||||
this._summaryBoxPointerState = State.HIDDEN;
|
||||
this._summaryBoxPointer.bin.child = null;
|
||||
if (this._summaryNotification != null) {
|
||||
this._summaryNotification.collapseCompleted();
|
||||
this._summaryNotification.disconnect(this._summaryNotificationClickedId);
|
||||
this._summaryNotificationClickedId = 0;
|
||||
let summaryNotification = this._summaryNotification;
|
||||
this._summaryNotification = null;
|
||||
if (summaryNotification.isTransient && !this._reNotifyWithSummaryNotificationAfterHide)
|
||||
summaryNotification.destroy(NotificationDestroyedReason.EXPIRED);
|
||||
if (this._reNotifyWithSummaryNotificationAfterHide) {
|
||||
this._onNotify(summaryNotification.source, summaryNotification);
|
||||
this._reNotifyWithSummaryNotificationAfterHide = false;
|
||||
this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerContentUpdatedId);
|
||||
this._summaryBoxPointerContentUpdatedId = 0;
|
||||
this._summaryBoxPointerItem.disconnect(this._summaryBoxPointerDoneDisplayingId);
|
||||
this._summaryBoxPointerDoneDisplayingId = 0;
|
||||
|
||||
let sourceNotificationStackDoneShowing = null;
|
||||
if (doneShowingNotificationStack) {
|
||||
this._summaryBoxPointerItem.doneShowingNotificationStack();
|
||||
sourceNotificationStackDoneShowing = this._summaryBoxPointerItem.source;
|
||||
}
|
||||
|
||||
this._summaryBoxPointerItem = null;
|
||||
|
||||
if (sourceNotificationStackDoneShowing) {
|
||||
if (sourceNotificationStackDoneShowing.isTransient && !this._reNotifyAfterHideNotification)
|
||||
sourceNotificationStackDoneShowing.destroy(NotificationDestroyedReason.EXPIRED);
|
||||
if (this._reNotifyAfterHideNotification) {
|
||||
this._onNotify(this._reNotifyAfterHideNotification.source, this._reNotifyAfterHideNotification);
|
||||
this._reNotifyAfterHideNotification = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._clickedSummaryItem)
|
||||
this._updateState();
|
||||
}
|
||||
|
@ -390,8 +390,7 @@ NotificationDaemon.prototype = {
|
||||
for (let id in this._sources) {
|
||||
let source = this._sources[id];
|
||||
if (source.app == tracker.focus_app) {
|
||||
if (source.notification && !source.notification.resident)
|
||||
source.notification.destroy();
|
||||
source.destroyNonResidentNotifications();
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -508,10 +507,11 @@ Source.prototype = {
|
||||
},
|
||||
|
||||
open: function(notification) {
|
||||
this.destroyNonResidentNotifications();
|
||||
this.openApp();
|
||||
},
|
||||
|
||||
_notificationRemoved: function() {
|
||||
_lastNotificationRemoved: function() {
|
||||
if (!this._trayIcon)
|
||||
this.destroy();
|
||||
},
|
||||
|
@ -75,11 +75,13 @@ ShellInfo.prototype = {
|
||||
Main.messageTray.add(this._source);
|
||||
}
|
||||
|
||||
let notification = this._source.notification;
|
||||
if (notification == null)
|
||||
let notification = null;
|
||||
if (this._source.notifications.length == 0) {
|
||||
notification = new MessageTray.Notification(this._source, text, null);
|
||||
else
|
||||
} else {
|
||||
notification = this._source.notifications[0];
|
||||
notification.update(text, null, { clear: true });
|
||||
}
|
||||
|
||||
notification.setTransient(true);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user