From 22eea750f3bedf5048184a529fc2f2438105e7bb Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Wed, 23 May 2012 00:02:00 +0200 Subject: [PATCH] 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 --- data/theme/gnome-shell.css | 27 +++++ js/ui/main.js | 1 + js/ui/messageTray.js | 65 ++++++++++-- js/ui/overview.js | 1 + js/ui/screenShield.js | 200 +++++++++++++++++++++++++++++++++++++ 5 files changed, 285 insertions(+), 9 deletions(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 25cddd3f4..01c0e7b80 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -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; +} diff --git a/js/ui/main.js b/js/ui/main.js index 319fb7b81..2dc152f63 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -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); } diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 393b30586..d1c726de8 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -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() { diff --git a/js/ui/overview.js b/js/ui/overview.js index b73f37203..925b2e631 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -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 }); diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js index 1e3d7b95d..7a8bd3434 100644 --- a/js/ui/screenShield.js +++ b/js/ui/screenShield.js @@ -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;