From 8cb3884fae67a20a2b87f442479cfdb5feb8a9c8 Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Wed, 30 Jan 2013 19:47:55 +0100 Subject: [PATCH] ScreenShield: decouple detailed notifications from resident notifications The designs says that only music notifications should be shown in full in the screenshield, the others should be either shown as a summary or with very light details. https://bugzilla.gnome.org/show_bug.cgi?id=685926 --- data/theme/gnome-shell.css | 1 + js/ui/messageTray.js | 42 +++--- js/ui/notificationDaemon.js | 3 + js/ui/screenShield.js | 270 +++++++++++++++++++++--------------- 4 files changed, 190 insertions(+), 126 deletions(-) diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 41a955750..e1c5fb736 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -2417,6 +2417,7 @@ StScrollBar StButton#vhandle:active { .screen-shield-notifications-box { spacing: 18px; + max-width: 34em; } .screen-shield-notification-source { diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 103248c92..6cc572961 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -345,6 +345,7 @@ const Notification = new Lang.Class({ this.resident = false; // 'transient' is a reserved keyword in JS, so we have to use an alternate variable name this.isTransient = false; + this.isMusic = false; this.forFeedback = false; this.expanded = false; this.focused = false; @@ -352,8 +353,8 @@ const Notification = new Lang.Class({ this._destroyed = false; this._useActionIcons = false; this._customContent = false; - this._bannerBodyText = null; - this._bannerBodyMarkup = false; + this.bannerBodyText = null; + this.bannerBodyMarkup = false; this._titleFitsInBannerMode = true; this._titleDirection = Clutter.TextDirection.DEFAULT; this._spacing = 0; @@ -505,12 +506,12 @@ const Notification = new Lang.Class({ // is done correctly automatically. this._table.set_text_direction(this._titleDirection); - // Unless the notification has custom content, we save this._bannerBodyText + // Unless the notification has custom content, we save this.bannerBodyText // 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.bannerBodyText = this._customContent ? null : banner; + this.bannerBodyMarkup = params.bannerMarkup; banner = banner ? banner.replace(/\n/g, ' ') : ''; @@ -518,7 +519,7 @@ const Notification = new Lang.Class({ 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) + if (this.bannerBodyText && this.bannerBodyText.indexOf('\n') > -1) this._addBannerBody(); if (params.body) @@ -584,10 +585,10 @@ const Notification = new Lang.Class({ }, _addBannerBody: function() { - if (this._bannerBodyText) { - let text = this._bannerBodyText; - this._bannerBodyText = null; - this.addBody(text, this._bannerBodyMarkup); + if (this.bannerBodyText) { + let text = this.bannerBodyText; + this.bannerBodyText = null; + this.addBody(text, this.bannerBodyMarkup); } }, @@ -879,7 +880,7 @@ const Notification = new Lang.Class({ }, _canExpandContent: function() { - return this._bannerBodyText || + return this.bannerBodyText || (!this._titleFitsInBannerMode && !this._table.has_style_class_name('multi-line-notification')); }, @@ -1279,7 +1280,16 @@ const Source = new Lang.Class({ hasResidentNotification: function() { return this.notifications.some(function(n) { return n.resident; }); - } + }, + + getMusicNotification: function() { + for (let i = 0; i < this.notifications.length; i++) { + if (this.notifications[i].isMusic) + return this.notifications[i]; + } + + return null; + }, }); Signals.addSignalMethods(Source.prototype); @@ -1777,7 +1787,7 @@ const MessageTray = new Lang.Class({ // 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); + this.emit('source-added', source); this._updateNoMessagesLabel(); }, @@ -1813,10 +1823,8 @@ const MessageTray = new Lang.Class({ this._updateState(); }, - getSummaryItems: function() { - return this._sources.values().map(function(v) { - return v.summaryItem; - }); + getSources: function() { + return this._sources.keys(); }, _onSourceEnableChanged: function(policy, source) { diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js index cbe28315a..e02bf22c3 100644 --- a/js/ui/notificationDaemon.js +++ b/js/ui/notificationDaemon.js @@ -501,6 +501,9 @@ const NotificationDaemon = new Lang.Class({ })); } + // Mark music notifications so they can be shown in the screen shield + notification.isMusic = (ndata.hints['category'] == 'x-gnome.music'); + let gicon = this._iconForNotificationData(icon, hints); let gimage = this._imageForNotificationData(hints); diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js index 38da9df15..0367f6078 100644 --- a/js/ui/screenShield.js +++ b/js/ui/screenShield.js @@ -13,6 +13,7 @@ const St = imports.gi.St; const TweenerEquations = imports.tweener.equations; const GnomeSession = imports.misc.gnomeSession; +const Hash = imports.misc.hash; const Layout = imports.ui.layout; const LoginManager = imports.misc.loginManager; const Lightbox = imports.ui.lightbox; @@ -127,49 +128,48 @@ const NotificationsBox = new Lang.Class({ name: 'screenShieldNotifications', style_class: 'screen-shield-notifications-box' }); - this._residentNotificationBox = new St.BoxLayout({ vertical: true, - style_class: 'screen-shield-notifications-box' }); - let scrollView = new St.ScrollView({ x_fill: false, x_align: St.Align.START }); - this._persistentNotificationBox = new St.BoxLayout({ vertical: true, - style_class: 'screen-shield-notifications-box' }); - scrollView.add_actor(this._persistentNotificationBox); + this._musicBin = new St.Bin({ style_class: 'screen-shield-notifications-box', + visible: false }); - this.actor.add(this._residentNotificationBox, { x_fill: true }); + let scrollView = new St.ScrollView({ x_fill: false, x_align: St.Align.START }); + this._notificationBox = new St.BoxLayout({ vertical: true, + style_class: 'screen-shield-notifications-box' }); + scrollView.add_actor(this._notificationBox); + + this.actor.add(this._musicBin); this.actor.add(scrollView, { x_fill: true, x_align: St.Align.START }); - this._items = []; - Main.messageTray.getSummaryItems().forEach(Lang.bind(this, function(item) { - this._summaryItemAdded(Main.messageTray, item, true); + this._sources = new Hash.Map(); + Main.messageTray.getSources().forEach(Lang.bind(this, function(source) { + this._sourceAdded(Main.messageTray, source, true); })); this._updateVisibility(); - this._summaryAddedId = Main.messageTray.connect('summary-item-added', Lang.bind(this, this._summaryItemAdded)); + this._sourceAddedId = Main.messageTray.connect('source-added', Lang.bind(this, this._sourceAdded)); }, destroy: function() { - if (this._summaryAddedId) { - Main.messageTray.disconnect(this._summaryAddedId); - this._summaryAddedId = 0; + if (this._sourceAddedId) { + Main.messageTray.disconnect(this._sourceAddedId); + this._sourceAddedId = 0; } - for (let i = 0; i < this._items.length; i++) - this._removeItem(this._items[i]); - this._items = []; + let items = this._sources.items(); + for (let i = 0; i < items.length; i++) { + let [source, obj] = items[i]; + this._removeSource(source, obj); + } this.actor.destroy(); }, _updateVisibility: function() { - this._residentNotificationBox.visible = this._residentNotificationBox.get_n_children() > 0; - this._persistentNotificationBox.visible = this._persistentNotificationBox.get_children().some(function(a) { + this._musicBin.visible = this._musicBin.child != null && this._musicBin.child.visible; + this._notificationBox.visible = this._notificationBox.get_children().some(function(a) { return a.visible; }); - this.actor.visible = this._residentNotificationBox.visible || this._persistentNotificationBox.visible; - }, - - _sourceIsResident: function(source) { - return source.hasResidentNotification() && !source.isChat; + this.actor.visible = this._musicBin.visible || this._notificationBox.visible; }, _makeNotificationCountText: function(count, isChat) { @@ -179,18 +179,16 @@ const NotificationsBox = new Lang.Class({ return ngettext("%d new notification", "%d new notifications", count).format(count); }, - _makeNotificationSource: function(source) { - let box = new St.BoxLayout({ style_class: 'screen-shield-notification-source' }); - + _makeNotificationSource: function(source, box) { let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE); box.add(sourceActor.actor, { y_fill: true }); let textBox = new St.BoxLayout({ vertical: true }); box.add(textBox, { y_fill: false, y_align: St.Align.START }); - let label = new St.Label({ text: source.title, + let title = new St.Label({ text: source.title, style_class: 'screen-shield-notification-label' }); - textBox.add(label); + textBox.add(title); let count = source.unseenCount; let countLabel = new St.Label({ text: this._makeNotificationCountText(count, source.isChat), @@ -198,118 +196,172 @@ const NotificationsBox = new Lang.Class({ textBox.add(countLabel); box.visible = count != 0; - return [box, countLabel]; + return [title, countLabel]; }, - _summaryItemAdded: function(tray, item, dontUpdateVisibility) { - // Ignore transient sources, or sources explicitly marked not to show - // in the lock screen - if (item.source.isTransient || !item.source.showInLockScreen) + _makeNotificationDetailedSource: function(source, box) { + let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE); + box.add(sourceActor.actor, { y_fill: true }); + + let textBox = new St.BoxLayout({ vertical: true }); + box.add(textBox, { y_fill: false, y_align: St.Align.START }); + + let title = new St.Label({ text: source.title, + style_class: 'screen-shield-notification-label' }); + textBox.add(title); + + let visible = false; + for (let i = 0; i < source.notifications.length; i++) { + let n = source.notifications[i]; + + if (n.acknowledged || n.isMusic) + continue; + + let body = ''; + if (n.bannerBodyText) + body = n.bannerBodyMarkup ? n.bannerBodyText : + GLib.markup_escape_text(n.bannerBodyMarkup, -1); + let label = new St.Label({ style_class: 'screen-shield-notification-count-text' }); + label.clutter_text.set_markup('' + n.title + ' ' + body); + textBox.add(label); + + visible = true; + } + + box.visible = visible; + return [title, null]; + }, + + _showSource: function(source, obj, box) { + let musicNotification = source.getMusicNotification(); + + if (musicNotification != null && + this._musicBin.child == null) { + if (musicNotification.actor.get_parent() != null) + musicNotification.actor.get_parent().remove_actor(musicNotification.actor); + this._musicBin.child = musicNotification.actor; + this._musicBin.child.visible = obj.visible; + + musicNotification.expand(false /* animate */); + + obj.musicNotification = musicNotification; + } + + if (obj.detailed) { + [obj.titleLabel, obj.countLabel] = this._makeNotificationDetailedSource(source, box); + } else { + [obj.titleLabel, obj.countLabel] = this._makeNotificationSource(source, box); + } + + box.visible = obj.visible && + (source.unseenCount > (musicNotification ? 1 : 0)); + }, + + _sourceAdded: function(tray, source, dontUpdateVisibility) { + // Ignore transient sources + if (source.isTransient) return; let obj = { - item: item, - source: item.source, - resident: this._sourceIsResident(item.source), - contentUpdatedId: 0, + visible: source.policy.showInLockScreen, + detailed: source.policy.detailsInLockScreen, sourceDestroyId: 0, + sourceCountChangedId: 0, + sourceTitleChangedId: 0, + sourceUpdatedId: 0, + musicNotification: null, sourceBox: null, + titleLabel: null, countLabel: null, }; - if (obj.resident) { - this._residentNotificationBox.add(item.notificationStackWidget); - item.closeButton.hide(); - item.prepareNotificationStackForShowing(); - } else { - [obj.sourceBox, obj.countLabel] = this._makeNotificationSource(item.source); - this._persistentNotificationBox.add(obj.sourceBox, { x_fill: false, x_align: St.Align.START }); - } + obj.sourceBox = new St.BoxLayout({ style_class: 'screen-shield-notification-source' }); + this._showSource(source, obj, obj.sourceBox); + this._notificationBox.add(obj.sourceBox, { x_fill: false, x_align: St.Align.START }); - obj.contentUpdatedId = item.connect('content-updated', Lang.bind(this, this._onItemContentUpdated)); - obj.sourceCountChangedId = item.source.connect('count-updated', Lang.bind(this, this._onSourceChanged)); - obj.sourceTitleChangedId = item.source.connect('title-changed', Lang.bind(this, this._onSourceChanged)); - obj.sourceDestroyId = item.source.connect('destroy', Lang.bind(this, this._onSourceDestroy)); - this._items.push(obj); + obj.sourceCountChangedId = source.connect('count-updated', Lang.bind(this, function(source) { + this._countChanged(source, obj); + })); + obj.sourceTitleChangedId = source.connect('title-changed', Lang.bind(this, function(source) { + this._titleChanged(source, obj); + })); + obj.policyChangedId = source.policy.connect('policy-changed', Lang.bind(this, function(policy, key) { + if (key == 'show-in-lock-screen') + this._visibleChanged(source, obj); + else + this._detailedChanged(source, obj); + })); + obj.sourceDestroyId = source.connect('destroy', Lang.bind(this, function(source) { + this._onSourceDestroy(source, obj); + })); + + this._sources.set(source, obj); if (!dontUpdateVisibility) this._updateVisibility(); }, - _findSource: function(source) { - for (let i = 0; i < this._items.length; i++) { - if (this._items[i].source == source) - return i; + _titleChanged: function(source, obj) { + obj.titleLabel.text = source.title; + }, + + _countChanged: function(source, obj) { + if (obj.detailed) { + // A new notification was pushed, or a previous notification was destroyed. + // Give up, and build the list again. + + obj.sourceBox.destroy_all_children(); + obj.titleLabel = obj.countLabel = null; + this._showSource(source, obj, obj.sourceBox); + } else { + let count = source.unseenCount; + obj.countLabel.text = this._makeNotificationCountText(count, source.isChat); } - return -1; + obj.sourceBox.visible = obj.visible && + (source.unseenCount > (obj.musicNotification ? 1 : 0)); + this._updateVisibility(); }, - _onItemContentUpdated: function(item) { - let obj = this._items[this._findSource(item.source)]; - this._updateItem(obj); - }, - - _onSourceChanged: 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 + _visibleChanged: function(source, obj) { + if (obj.visible == source.policy.showInLockScreen) return; - } - if (obj.resident && !itemShouldBeResident) { - // make into a regular item - obj.item.doneShowingNotificationStack(); - this._residentNotificationBox.remove_actor(obj.item.notificationStackWidget); - - [obj.sourceBox, obj.countLabel] = this._makeNotificationSource(obj.source); - this._persistentNotificationBox.add(obj.sourceBox, { x_fill: false, x_align: St.Align.START }); - } else if (itemShouldBeResident && !obj.resident) { - // make into a resident item - obj.sourceBox.destroy(); - obj.sourceBox = obj.countLabel = null; - obj.resident = true; - - this._residentNotificationBox.add(obj.item.notificationStackWidget); - obj.item.closeButton.hide(); - obj.item.prepareNotificationStackForShowing(); - } else { - // just update the counter - let count = obj.source.unseenCount; - obj.countLabel.text = this._makeNotificationCountText(count, obj.source.isChat); - obj.sourceBox.visible = count != 0; - } + obj.visible = source.policy.showInLockScreen; + if (obj.musicNotification) + obj.musicNotification.actor.visible = obj.visible; + obj.sourceBox.visible = obj.visible && + source.unseenCount > (obj.musicNotification ? 1 : 0); this._updateVisibility(); }, - _onSourceDestroy: function(source) { - let idx = this._findSource(source); + _detailedChanged: function(source, obj) { + if (obj.detailed == source.policy.detailsInLockScreen) + return; - this._removeItem(this._items[idx]); - this._items.splice(idx, 1); + obj.detailed = source.policy.detailsInLockScreen; + obj.sourceBox.destroy_all_children(); + obj.titleLabel = obj.countLabel = null; + this._showSource(source, obj, obj.sourceBox); + }, + + _onSourceDestroy: function(source, obj) { + this._removeSource(source, obj); this._updateVisibility(); }, - _removeItem: function(obj) { - if (obj.resident) { - obj.item.doneShowingNotificationStack(); - this._residentNotificationBox.remove_actor(obj.item.notificationStackWidget); - } else { - obj.sourceBox.destroy(); - } + _removeSource: function(source, obj) { + obj.sourceBox.destroy(); + obj.sourceBox = obj.titleLabel = obj.countLabel = null; - obj.item.disconnect(obj.contentUpdatedId); - obj.source.disconnect(obj.sourceDestroyId); - obj.source.disconnect(obj.sourceCountChangedId); - obj.source.disconnect(obj.sourceTitleChangedId); + source.disconnect(obj.sourceDestroyId); + source.disconnect(obj.sourceCountChangedId); + source.disconnect(obj.sourceTitleChangedId); + source.policy.disconnect(obj.policyChangedId); + + this._sources.delete(source); }, });