[gnome-shell] [MessageTray] do pop-out on hover for long notifications



commit fa6016576486e47a89c0de95873d6de9cb5acc3c
Author: Dan Winship <danw gnome org>
Date:   Mon Feb 1 12:10:38 2010 -0500

    [MessageTray] do pop-out on hover for long notifications
    
    https://bugzilla.gnome.org/show_bug.cgi?id=606755

 data/theme/gnome-shell.css  |    5 +-
 js/ui/messageTray.js        |  175 +++++++++++++++++++++++++++++++++++++------
 js/ui/notificationDaemon.js |    5 +-
 3 files changed, 155 insertions(+), 30 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index ee16b9f..ee7a756 100644
--- a/data/theme/gnome-shell.css
+++ b/data/theme/gnome-shell.css
@@ -556,8 +556,9 @@ StTooltip {
     border-radius: 5px;
     background: rgba(0,0,0,0.9);
     color: white;
-    padding: 2px 10px;
-    spacing: 10px;
+    padding: 2px 10px 10px 10px;
+    spacing-rows: 5px;
+    spacing-columns: 10px;
 }
 
 #summary-mode {
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index 5657083..ddfb4c4 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -3,6 +3,7 @@
 const Clutter = imports.gi.Clutter;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
+const Pango = imports.gi.Pango;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 const St = imports.gi.St;
@@ -25,17 +26,38 @@ const MessageTrayState = {
     TRAY_ONLY: 3 // neither notifiations nor summary are visible, only tray
 };
 
-function Notification(icon, text, source) {
-    this._init(icon, text, source);
+// Notification:
+// @source: the notification's Source
+// @icon: a ClutterActor for the notification's icon
+// @title: the title
+// @banner: the banner text
+// @body: the body text, or %null
+//
+// Creates a notification. In banner mode, it will show
+// @icon, @title (in bold) and @banner, all on a single line
+// (with @banner ellipsized if necessary). If @body is not %null, then
+// the notification will be expandable. In expanded mode, it will show
+// just @icon and @title (in bold) on the first line, and @body on
+// multiple lines underneath.
+function Notification(source, icon, title, banner, body) {
+    this._init(source, icon, title, banner, body);
 }
 
 Notification.prototype = {
-    _init: function(icon, text, source) {
-        this.icon = icon;
-        this.text = text;
+    _init: function(source, icon, title, banner, body) {
         this.source = source;
+        this.icon = icon;
+        this.title = title ? this._cleanMarkup(title.replace('\n', ' ')) : '';
+        this.banner = banner ? this._cleanMarkup(banner.replace('\n', '  ')) : '';
+        this.body = body ? this._cleanMarkup(body) : null;
+    },
+
+    _cleanMarkup: function(text) {
+        // Support <b>, <i>, and <u>, escape anything else
+        // so it displays as raw markup.
+        return text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, "&lt;$1");
     }
-}
+};
 
 function NotificationBox() {
     this._init();
@@ -43,29 +65,122 @@ function NotificationBox() {
 
 NotificationBox.prototype = {
     _init: function() {
-        this.actor = new St.BoxLayout({ name: 'notification' });
+        this.actor = new St.Table({ name: 'notification' });
 
         this._iconBox = new St.Bin();
-        this.actor.add(this._iconBox);
-
-        this._text = new St.Label();
-        this.actor.add(this._text, { expand: true, x_fill: false, y_fill: false, y_align: St.Align.MIDDLE });
-
-        this.notification = null;
+        this.actor.add(this._iconBox, { row: 0,
+                                        col: 0,
+                                        x_expand: false,
+                                        y_expand: false,
+                                        y_fill: false });
+
+        // The first line should have the title, followed by the
+        // banner text, but ellipsized if they won't both fit. We can't
+        // make St.Table or St.BoxLayout do this the way we want (don't
+        // show banner at all if title needs to be ellipsized), so we
+        // use Shell.GenericContainer.
+        this._bannerBox = new Shell.GenericContainer();
+        this._bannerBox.connect('get-preferred-width', Lang.bind(this, this._bannerBoxGetPreferredWidth));
+        this._bannerBox.connect('get-preferred-height', Lang.bind(this, this._bannerBoxGetPreferredHeight));
+        this._bannerBox.connect('allocate', Lang.bind(this, this._bannerBoxAllocate));
+        this.actor.add(this._bannerBox, { row: 0,
+                                          col: 1,
+                                          y_expand: false,
+                                          y_fill: false });
+
+        this._titleText = new St.Label();
+        this._bannerBox.add_actor(this._titleText);
+
+        this._bannerText = new St.Label();
+        this._bannerBox.add_actor(this._bannerText);
+
+        this._bodyText = new St.Label();
+        this._bodyText.clutter_text.line_wrap = true;
+        this._bodyText.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
+        this.actor.add(this._bodyText, { row: 1,
+                                         col: 1 });
     },
 
     setContent: function(notification) {
         this.notification = notification;
 
         this._iconBox.child = notification.icon;
+        this._titleText.clutter_text.set_markup('<b>' + notification.title + '</b>');
+        this._bannerText.clutter_text.set_markup(notification.banner);
+
+        if (notification.body) {
+            this._bodyText.clutter_text.set_markup(notification.body);
+            this._canPopOut = true;
+        } else {
+            // If there's no body, then normally we wouldn't do pop-out.
+            // But if title+banner is too wide for the notification, then
+            // we'd need to pop out to show the full banner. So we set up
+            // bodyText with that now.
+            this._bodyText.clutter_text.set_markup(notification.banner);
+            this._canPopOut = false;
+        }
+    },
 
-        // Support <b>, <i>, and <u>, escape anything else
-        // so it displays as raw markup.
-        let markup = notification.text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, "&lt;$1");
-        this._text.clutter_text.set_markup(markup);
+    _bannerBoxGetPreferredWidth: function(actor, forHeight, alloc) {
+        let [titleMin, titleNat] = this._titleText.get_preferred_width(forHeight);
+        let [bannerMin, bannerNat] = this._bannerText.get_preferred_width(forHeight);
+        let [has_spacing, spacing] = this.actor.get_theme_node().get_length('spacing-columns', false);
 
-        let primary = global.get_primary_monitor();
-        this.actor.x = Math.round((primary.width - this.actor.width) / 2);
+        alloc.min_size = titleMin;
+        alloc.natural_size = titleNat + (has_spacing ? spacing : 0) + bannerNat;
+    },
+
+    _bannerBoxGetPreferredHeight: function(actor, forWidth, alloc) {
+        [alloc.min_size, alloc.natural_size] =
+            this._titleText.get_preferred_height(forWidth);
+    },
+
+    _bannerBoxAllocate: function(actor, box, flags) {
+        let [titleMinW, titleNatW] = this._titleText.get_preferred_width(-1);
+        let [titleMinH, titleNatH] = this._titleText.get_preferred_height(-1);
+        let [bannerMinW, bannerNatW] = this._bannerText.get_preferred_width(-1);
+        let [has_spacing, spacing] = this.actor.get_theme_node().get_length('spacing-columns', false);
+        if (!has_spacing)
+            spacing = 0;
+        let availWidth = box.x2 - box.x1;
+
+        let titleBox = new Clutter.ActorBox();
+        titleBox.x1 = titleBox.y1 = 0;
+        titleBox.x2 = Math.min(titleNatW, availWidth);
+        titleBox.y2 = titleNatH;
+        this._titleText.allocate(titleBox, flags);
+
+        let bannerBox = new Clutter.ActorBox();
+        bannerBox.x1 = Math.min(titleBox.x2 + spacing, availWidth);
+        bannerBox.y1 = 0;
+        bannerBox.x2 = Math.min(bannerBox.x1 + bannerNatW, availWidth);
+        bannerBox.y2 = titleNatH;
+        this._bannerText.allocate(bannerBox, flags);
+
+        if (bannerBox.x2 < bannerBox.x1 + bannerNatW)
+            this._canPopOut = true;
+    },
+
+    popOut: function() {
+        if (!this._canPopOut)
+            return false;
+
+        Tweener.addTween(this._bannerText,
+                         { opacity: 0,
+                           time: ANIMATION_TIME,
+                           transition: "easeOutQuad" });
+        return true;
+    },
+
+    popIn: function() {
+        if (!this._canPopOut)
+            return false;
+
+        Tweener.addTween(this._bannerText,
+                         { opacity: 255,
+                           time: ANIMATION_TIME,
+                           transition: "easeOutQuad" });
+        return true;
     }
 };
 
@@ -87,8 +202,9 @@ Source.prototype = {
         throw new Error('no implementation of createIcon in ' + this);
     },
 
-    notify: function(text) {
-        this.text = text;
+    notify: function(title, banner, body) {
+        this.notification = new Notification(this, this.createIcon(ICON_SIZE),
+                                             title, banner, body);
         this.emit('notify');
     },
 
@@ -111,7 +227,7 @@ MessageTray.prototype = {
         this.actor = new St.BoxLayout({ name: 'message-tray',
                                         reactive: true });
 
-        this._notificationBin = new St.Bin();
+        this._notificationBin = new St.Bin({ reactive: true });
         this.actor.add(this._notificationBin);
         this._notificationBin.hide();
         this._notificationBox = new NotificationBox();
@@ -133,6 +249,7 @@ MessageTray.prototype = {
         this._state = MessageTrayState.HIDDEN;
         this.actor.show();
         Main.chrome.addActor(this.actor, { affectsStruts: false });
+        Main.chrome.trackActor(this._notificationBin, { affectsStruts: false });
 
         global.connect('screen-size-changed',
                        Lang.bind(this, this._setSizePosition));
@@ -211,8 +328,7 @@ MessageTray.prototype = {
     },
 
     _onNotify: function(source) {
-        let notification = new Notification(source.createIcon(ICON_SIZE), source.text, source)
-        this._notificationQueue.push(notification);
+        this._notificationQueue.push(source.notification);
 
         if (this._state == MessageTrayState.HIDDEN)
             this._updateState();
@@ -229,6 +345,15 @@ MessageTray.prototype = {
 
         if (this._state == MessageTrayState.HIDDEN)
             this._updateState();
+        else if (this._state == MessageTrayState.NOTIFICATION) {
+            if (this._notificationBox.popOut()) {
+                Tweener.addTween(this._notificationBin,
+                                 { y: this.actor.height - this._notificationBin.height,
+                                   time: ANIMATION_TIME,
+                                   transition: "easeOutQuad"
+                                 });
+            }
+        }
     },
 
     _onMessageTrayLeft: function() {
@@ -345,6 +470,8 @@ MessageTray.prototype = {
     },
 
     _hideNotification: function() {
+        this._notificationBox.popIn();
+
         Tweener.addTween(this._notificationBin,
                          { y: this.actor.height,
                            opacity: 0,
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
index 0190d66..26779c5 100644
--- a/js/ui/notificationDaemon.js
+++ b/js/ui/notificationDaemon.js
@@ -122,10 +122,7 @@ NotificationDaemon.prototype = {
         }
 
         summary = GLib.markup_escape_text(summary, -1);
-        if (body)
-            source.notify('<b>' + summary + '</b>: ' + body);
-        else
-            source.notify('<b>' + summary + '</b>');
+        source.notify(summary, body);
         return id;
     },
 



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