ScreenShield: show notifications in the locked screen

Track screen lock status in the message tray, and filter banner
notifications. The message tray is completely hidden when the screen is
locked, but exceptions can be made for individual transient notifications,
such as shell messages and the on screen keyboard.
Non transient sources are shown in the middle of the lock screen. Resident
notifications (such as those from Rhythmbox) are shown in full, while
persistent ones are displayed as icon and message count.

https://bugzilla.gnome.org/show_bug.cgi?id=619955
This commit is contained in:
Giovanni Campagna 2012-05-23 00:02:00 +02:00
parent c3afe1a83a
commit 22eea750f3
5 changed files with 285 additions and 9 deletions

View File

@ -2221,3 +2221,30 @@ StScrollBar StButton#vhandle:hover
.screen-shield-clock-date {
font-size: 48px;
}
#screenShieldNotifications {
border-radius: 24px;
background-color: rgba(0.0, 0.0, 0.0, 0.9);
border: 2px solid #868686;
}
#screenShieldNotifications, .screen-shield-notifications-box {
spacing: 8px;
}
.screen-shield-notification-source {
padding: 24px 8px;
spacing: 5px;
}
.screen-shield-notification-label {
font-size: 1.2em;
font-weight: bold;
}
/* Remove background from notifications, otherwise
opacity is doubled and they look darker
*/
.screen-shield-notifications-box .notification {
background-color: transparent;
}

View File

@ -535,6 +535,7 @@ function notify(msg, details) {
messageTray.add(source);
let notification = new MessageTray.Notification(source, msg, details);
notification.setTransient(true);
notification.setShowWhenLocked(true);
source.notify(notification);
}

View File

@ -420,6 +420,7 @@ const Notification = new Lang.Class({
// 'transient' is a reserved keyword in JS, so we have to use an alternate variable name
this.isTransient = false;
this.expanded = false;
this.showWhenLocked = false;
this._destroyed = false;
this._useActionIcons = false;
this._customContent = false;
@ -816,6 +817,14 @@ const Notification = new Lang.Class({
this.isTransient = isTransient;
},
setShowWhenLocked: function(show) {
if (show && !this.isTransient) {
throw new Error('ShowWhenLocked can only be set on a transient notification');
}
this.showWhenLocked = show;
},
setUseActionIcons: function(useIcons) {
this._useActionIcons = useIcons;
},
@ -1237,6 +1246,10 @@ const Source = new Lang.Class({
// Default implementation is to destroy this source, but subclasses can override
_lastNotificationRemoved: function() {
this.destroy();
},
hasResidentNotification: function() {
return this.notifications.some(function(n) { return n.resident; });
}
});
Signals.addSignalMethods(Source.prototype);
@ -1578,6 +1591,9 @@ const MessageTray = new Lang.Class({
}
}));
this._isScreenLocked = false;
Main.screenShield.connect('lock-status-changed', Lang.bind(this, this._onScreenLockStatusChanged));
this._summaryItems = [];
// We keep a list of new summary items that were added to the summary since the last
// time it was shown to the user. We automatically show the summary to the user if there
@ -1684,6 +1700,12 @@ const MessageTray = new Lang.Class({
// *first* and not show the summary item until after it hides.
// So postpone calling _updateState() a tiny bit.
Meta.later_add(Meta.LaterType.BEFORE_REDRAW, Lang.bind(this, function() { this._updateState(); return false; }));
this.emit('summary-item-added', summaryItem);
},
getSummaryItems: function() {
return this._summaryItems;
},
_onSourceDestroy: function(source) {
@ -1758,6 +1780,11 @@ const MessageTray = new Lang.Class({
this._notificationQueue.splice(index, 1);
},
_onScreenLockStatusChanged: function(screenShield, locked) {
this._isScreenLocked = locked;
this._updateState();
},
_lock: function() {
this._locked = true;
},
@ -2088,18 +2115,36 @@ const MessageTray = new Lang.Class({
// at the present time.
_updateState: function() {
// Notifications
let notificationUrgent = this._notificationQueue.length > 0 && this._notificationQueue[0].urgency == Urgency.CRITICAL;
let notificationsPending = this._notificationQueue.length > 0 && ((!this._busy && !this._inFullscreen) || notificationUrgent);
let notificationQueue = this._notificationQueue.filter(Lang.bind(this, function(notification) {
if (this._isScreenLocked)
return notification.showWhenLocked;
else
return true;
}));
let notificationUrgent = notificationQueue.length > 0 && notificationQueue[0].urgency == Urgency.CRITICAL;
// notificationsLimited is false when the screen is locked, because they go through
// different filtering, and we want to show non urgent messages at times
let notificationsLimited = (this._busy || this._inFullscreen) && !this._isScreenLocked;
let notificationsPending = notificationQueue.length > 0 && (!notificationsLimited || notificationUrgent);
let nextNotification = notificationQueue.length > 0 ? notificationQueue[0] : null;
let notificationPinned = this._pointerInTray && !this._pointerInSummary && !this._notificationRemoved;
let notificationExpanded = this._notificationBin.y < - this.actor.height;
let notificationExpired = (this._notificationTimeoutId == 0 && !(this._notification && this._notification.urgency == Urgency.CRITICAL) && !this._pointerInTray && !this._locked && !(this._pointerInKeyboard && notificationExpanded)) || this._notificationRemoved;
let notificationExpired = this._notificationTimeoutId == 0 &&
!(this._notification && this._notification.urgency == Urgency.CRITICAL) &&
!this._pointerInTray &&
!this._locked &&
!(this._pointerInKeyboard && notificationExpanded);
let notificationLockedOut = this._isScreenLocked && (this._notification && !this._notification.showWhenLocked);
let notificationMustClose = this._notificationRemoved || notificationLockedOut || notificationExpired;
let canShowNotification = notificationsPending && this._summaryState == State.HIDDEN;
if (this._notificationState == State.HIDDEN) {
if (canShowNotification)
this._showNotification();
if (canShowNotification) {
this._showNotification(nextNotification);
this._notificationQueue.splice(this._notificationQueue.indexOf(nextNotification), 1);
}
} else if (this._notificationState == State.SHOWN) {
if (notificationExpired)
if (notificationMustClose)
this._hideNotification();
else if (notificationPinned && !notificationExpanded)
this._expandNotification(false);
@ -2120,7 +2165,7 @@ const MessageTray = new Lang.Class({
let summaryOptionalInOverview = this._overviewVisible && !this._locked && !summaryHovered;
let mustHideSummary = (notificationsPending && (notificationUrgent || summaryOptionalInOverview))
|| notificationsVisible;
|| notificationsVisible || this._isScreenLocked;
if (this._summaryState == State.HIDDEN && !mustHideSummary) {
if (summarySummoned) {
@ -2237,8 +2282,8 @@ const MessageTray = new Lang.Class({
}
},
_showNotification: function() {
this._notification = this._notificationQueue.shift();
_showNotification: function(notification) {
this._notification = notification;
this._unseenNotifications.push(this._notification);
if (this._idleMonitorWatchId == 0)
this._idleMonitorWatchId = this.idleMonitor.add_watch(1000,
@ -2587,6 +2632,7 @@ const MessageTray = new Lang.Class({
this._updateState();
}
});
Signals.addSignalMethods(MessageTray.prototype);
const SystemNotificationSource = new Lang.Class({
Name: 'SystemNotificationSource',
@ -2594,6 +2640,7 @@ const SystemNotificationSource = new Lang.Class({
_init: function() {
this.parent(_("System Information"), 'dialog-information', St.IconType.SYMBOLIC);
this.setTransient(true);
},
open: function() {

View File

@ -75,6 +75,7 @@ const ShellInfo = new Lang.Class({
let notification = null;
if (this._source.notifications.length == 0) {
notification = new MessageTray.Notification(this._source, text, null);
notification.setShowWhenLocked(true);
} else {
notification = this._source.notifications[0];
notification.update(text, null, { clear: true });

View File

@ -11,6 +11,7 @@ const St = imports.gi.St;
const GnomeSession = imports.misc.gnomeSession;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const Tweener = imports.ui.tweener;
const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
@ -21,6 +22,8 @@ const CURTAIN_SLIDE_TIME = 1.2;
// the slide up automatically
const ARROW_DRAG_TRESHOLD = 0.4;
const SUMMARY_ICON_SIZE = 48;
const Clock = new Lang.Class({
Name: 'ScreenShieldClock',
@ -58,6 +61,191 @@ const Clock = new Lang.Class({
}
});
const NotificationsBox = new Lang.Class({
Name: 'NotificationsBox',
_init: function() {
this.actor = new St.BoxLayout({ vertical: true,
name: 'screenShieldNotifications',
margin_top: 20
});
this._residentNotificationBox = new St.BoxLayout({ vertical: true,
style_class: 'screen-shield-notifications-box' });
this._persistentNotificationBox = new St.BoxLayout({ vertical: true,
style_class: 'screen-shield-notifications-box' });
this.actor.add(this._residentNotificationBox, { x_fill: true });
this.actor.add(this._persistentNotificationBox, { x_fill: false, x_align: St.Align.MIDDLE });
this._items = [];
Main.messageTray.getSummaryItems().forEach(Lang.bind(this, function(item) {
this._summaryItemAdded(Main.messageTray, item);
}));
this._summaryAddedId = Main.messageTray.connect('summary-item-added', Lang.bind(this, this._summaryItemAdded));
},
destroy: function() {
if (this._summaryAddedId) {
Main.messageTray.disconnect(this._summaryAddedId);
this._summaryAddedId = 0;
}
for (let i = 0; i < this._items.length; i++)
this._removeItem(this._items[i]);
this._items = [];
this.actor.destroy();
},
_updateVisibility: function() {
if (this._residentNotificationBox.get_n_children() > 0) {
this.actor.show();
return;
}
let children = this._persistentNotificationBox.get_children()
this.actor.visible = children.some(function(a) { return a.visible; });
},
_sourceIsResident: function(source) {
return source.hasResidentNotification() && !source.isChat;
},
_makeNotificationCountText: function(source) {
if (source.isChat)
return ngettext("%d new message", "%d new messages", source.count).format(source.count);
else
return ngettext("%d new notification", "%d new notifications", source.count).format(source.count);
},
_makeNotificationSource: function(source) {
let box = new St.BoxLayout({ style_class: 'screen-shield-notification-source' });
let iconClone = source.createIcon(SUMMARY_ICON_SIZE);
let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE);
sourceActor.setIcon(iconClone);
box.add(sourceActor.actor, { y_fill: true });
let textBox = new St.BoxLayout({ vertical: true });
box.add(textBox, { y_fill: true, expand: true });
let label = new St.Label({ text: source.title,
style_class: 'screen-shield-notification-label' });
textBox.add(label);
let countLabel = new St.Label({ text: this._makeNotificationCountText(source),
style_class: 'screen-shield-notification-count-text' });
textBox.add(countLabel);
box.visible = source.count != 0;
return [box, countLabel];
},
_summaryItemAdded: function(tray, item) {
// Ignore transient sources
if (item.source.isTransient)
return;
let obj = {
item: item,
source: item.source,
resident: this._sourceIsResident(item.source),
contentUpdatedId: 0,
sourceDestroyId: 0,
sourceBox: null,
countLabel: null,
};
if (obj.resident) {
item.prepareNotificationStackForShowing();
this._residentNotificationBox.add(item.notificationStackView);
} else {
[obj.sourceBox, obj.countLabel] = this._makeNotificationSource(item.source);
this._persistentNotificationBox.add(obj.sourceBox);
}
obj.contentUpdatedId = item.connect('content-updated', Lang.bind(this, this._onItemContentUpdated));
obj.sourceCountChangedId = item.source.connect('count-changed', Lang.bind(this, this._onSourceCountChanged));
obj.sourceDestroyId = item.source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
this._items.push(obj);
this._updateVisibility();
},
_findSource: function(source) {
for (let i = 0; i < this._items.length; i++) {
if (this._items[i].source == source)
return i;
}
return -1;
},
_onItemContentUpdated: function(item) {
let obj = this._items[this._findSource(item.source)];
this._updateItem(obj);
},
_onSourceCountChanged: function(source) {
let obj = this._items[this._findSource(source)];
this._updateItem(obj);
},
_updateItem: function(obj) {
let itemShouldBeResident = this._sourceIsResident(obj.source);
if (itemShouldBeResident && obj.resident) {
// Nothing to do here, the actor is already updated
return;
}
if (obj.resident && !itemShouldBeResident) {
// make into a regular item
this._residentNotificationBox.remove_actor(obj.item.notificationStackView);
[obj.sourceBox, obj.countLabel] = this._makeNotificationSource(obj.source);
this._persistentNotificationBox.add(obj.sourceBox);
} else if (itemShouldBeResident && !obj.resident) {
// make into a resident item
obj.sourceBox.destroy();
obj.sourceBox = obj.countLabel = null;
obj.item.prepareNotificationStackForShowing();
this._residentNotificationBox.add(obj.item.notificationStackView);
} else {
// just update the counter
obj.countLabel.text = this._makeNotificationCountText(obj.item.source);
obj.sourceBox.visible = obj.source.count != 0;
}
this._updateVisibility();
},
_onSourceDestroy: function(source) {
let idx = this._findSource(source);
this._removeItem(this._items[idx]);
this._items.splice(idx, 1);
this._updateVisibility();
},
_removeItem: function(obj) {
if (obj.resident) {
this._residentNotificationBox.remove_actor(obj.item.notificationStackView);
obj.item.doneShowingNotificationStack();
} else {
obj.sourceBox.destroy();
}
obj.item.disconnect(obj.contentUpdatedId);
obj.source.disconnect(obj.sourceDestroyId);
obj.source.disconnect(obj.sourceCountChangedId);
},
});
/**
* To test screen shield, make sure to kill gnome-screensaver.
*
@ -298,6 +486,13 @@ const ScreenShield = new Lang.Class({
this._lockScreenGroup.add_actor(this._lockScreenContentsBox);
if (this._settings.get_boolean('show-notifications')) {
this._notificationsBox = new NotificationsBox();
this._lockScreenContentsBox.add(this._notificationsBox.actor, { x_fill: true,
y_fill: true,
expand: true });
}
this._hasLockScreen = true;
},
@ -305,6 +500,11 @@ const ScreenShield = new Lang.Class({
this._clock.destroy();
this._clock = null;
if (this._notificationsBox) {
this._notificationsBox.destroy();
this._notificationsBox = null;
}
this._lockScreenContentsBox.destroy();
this._hasLockScreen = false;