[gnome-shell] ScreenShield: show notifications in the locked screen



commit 22eea750f3bedf5048184a529fc2f2438105e7bb
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Wed May 23 00:02:00 2012 +0200

    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 25cddd3..01c0e7b 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 319fb7b..2dc152f 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 393b305..d1c726d 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 b73f372..925b2e6 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 1e3d7b9..7a8bd34 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;



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]