diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index b04cb79a7..922a7b418 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -317,7 +317,19 @@ StTooltip { background: rgba(0,0,0,0.9); border: 1px solid rgba(128,128,128,0.45); color: white; - padding: 10px; + padding: 4px; + spacing: 10px; +} + +#message-tray { + border-radius: 5px; + background: rgba(0,0,0,0.9); + border: 1px solid rgba(128,128,128,0.45); + padding: 4px; + height: 40px; +} + +#message-tray-inner { spacing: 10px; } diff --git a/js/ui/main.js b/js/ui/main.js index 0bea5d59e..10ea3a546 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -40,6 +40,7 @@ let wm = null; let messaging = null; let notificationDaemon = null; let notificationPopup = null; +let messageTray = null; let recorder = null; let shellDBusService = null; let modalCount = 0; @@ -113,6 +114,7 @@ function start() { messaging = new Messaging.Messaging(); notificationDaemon = new NotificationDaemon.NotificationDaemon(); notificationPopup = new MessageTray.Notification(); + messageTray = new MessageTray.MessageTray(); global.screen.connect('toggle-recording', function() { if (recorder == null) { diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index 512aec00a..d105c98fa 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -10,6 +10,8 @@ const Main = imports.ui.main; const ANIMATION_TIME = 0.2; const NOTIFICATION_TIMEOUT = 4; +const MESSAGE_TRAY_TIMEOUT = 0.2; + function Notification() { this._init(); } @@ -80,3 +82,100 @@ Notification.prototype = { this.actor.hide(); } }; + +function MessageTray() { + this._init(); +} + +MessageTray.prototype = { + _init: function() { + this.actor = new St.Bin({ name: 'message-tray', + reactive: true, + x_align: St.Align.END }); + // Directly adding the actor to Main.chrome.actor is a hack to + // work around the fact that there is no way to add an actor that + // affects the input region but not the shape. + // See: https://bugzilla.gnome.org/show_bug.cgi?id=597044 + Main.chrome.actor.add_actor(this.actor); + Main.chrome.addInputRegionActor(this.actor); + + let primary = global.get_primary_monitor(); + this.actor.x = 0; + this.actor.y = primary.height - 1; + + this.actor.width = primary.width; + + this.actor.connect('enter-event', + Lang.bind(this, this._onMessageTrayEntered)); + this.actor.connect('leave-event', + Lang.bind(this, this._onMessageTrayLeft)); + this._isShowing = false; + this.actor.show(); + + this._tray = new St.BoxLayout({ name: 'message-tray-inner' }); + this.actor.child = this._tray; + this._tray.expand = true; + this._icons = {}; + }, + + contains: function(id) { + return this._icons.hasOwnProperty(id); + }, + + add: function(id, icon) { + if (this.contains(id)) + return; + + let iconBox = new St.Bin(); + iconBox.child = icon; + this._tray.insert_actor(iconBox, 0); + this._icons[id] = iconBox; + }, + + remove: function(id) { + if (!this.contains(id)) + return; + + this._tray.remove_actor(this._icons[id]); + this._icons[id].destroy(); + delete this._icons[id]; + }, + + _onMessageTrayEntered: function() { + if (this._hideTimeoutId > 0) + Mainloop.source_remove(this._hideTimeoutId); + + if (this._isShowing) + return; + + this._isShowing = true; + let primary = global.get_primary_monitor(); + Tweener.addTween(this.actor, + { y: primary.height - this.actor.height, + time: ANIMATION_TIME, + transition: "easeOutQuad" + }); + }, + + _onMessageTrayLeft: function() { + if (!this._isShowing) + return; + + this._hideTimeoutId = Mainloop.timeout_add(MESSAGE_TRAY_TIMEOUT * 1000, Lang.bind(this, this._hide)); + }, + + _hide: function() { + this._isShowing = false; + this._hideTimeoutId = 0; + + let primary = global.get_primary_monitor(); + + Tweener.addTween(this.actor, + { y: primary.height - 1, + time: ANIMATION_TIME, + transition: "easeOutQuad" + }); + return false; + } +}; + diff --git a/js/ui/messaging.js b/js/ui/messaging.js index 8113f87f4..2e54cbbd9 100644 --- a/js/ui/messaging.js +++ b/js/ui/messaging.js @@ -289,10 +289,13 @@ Source.prototype = { this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed)); this._targetId = targetId; - log('channel for ' + this._targetId); + log('channel for ' + this._targetId + ' channelPath ' + channelPath); this._pendingMessages = null; + this._avatar = null; + this._avatarBytes = null; + // FIXME: RequestAvatar is deprecated in favor of // RequestAvatars; but RequestAvatars provides no explicit // indication of "no avatar available", so there's no way we @@ -316,8 +319,8 @@ Source.prototype = { _gotAvatar: function(result, excp) { if (result) { - let bytes = result[0]; - this._avatar = Shell.TextureCache.get_default().load_from_data(bytes, bytes.length, AVATAR_SIZE, AVATAR_SIZE); + this._avatarBytes = result[0]; + this._avatar = Shell.TextureCache.get_default().load_from_data(this._avatarBytes, this._avatarBytes.length, AVATAR_SIZE); log('got avatar for ' + this._targetId); } else { // fallback avatar (FIXME) @@ -338,14 +341,23 @@ Source.prototype = { }, _channelClosed: function() { - log('closed'); + log('Channel closed ' + this._targetId); this._channel.disconnect(this._closedId); this._channelText.disconnect(this._receivedId); + Main.messageTray.remove(this._targetId); }, _receivedMessage: function(channel, id, timestamp, sender, type, flags, text) { log('Received: id ' + id + ', time ' + timestamp + ', sender ' + sender + ', type ' + type + ', flags ' + flags + ': ' + text); Main.notificationPopup.show(this._avatar, text); + if (!Main.messageTray.contains(this._targetId)) { + let avatarForMessageTray = null; + if (this._avatarBytes) + avatarForMessageTray = Shell.TextureCache.get_default().load_from_data(this._avatarBytes, this._avatarBytes.length, AVATAR_SIZE); + else + avatarForMessageTray = Shell.TextureCache.get_default().load_icon_name("stock_person", AVATAR_SIZE); + Main.messageTray.add(this._targetId, avatarForMessageTray); + } } }; diff --git a/src/shell-texture-cache.c b/src/shell-texture-cache.c index 2bea03841..bbbec883e 100644 --- a/src/shell-texture-cache.c +++ b/src/shell-texture-cache.c @@ -6,6 +6,7 @@ #define GNOME_DESKTOP_USE_UNSTABLE_API #include #include +#include typedef struct { @@ -15,6 +16,7 @@ typedef struct GIcon *icon; gchar *uri; gchar *thumbnail_uri; + gchar *checksum; /* This one is common to all */ guint size; @@ -49,6 +51,8 @@ cache_key_hash (gconstpointer a) base_hash = g_str_hash (akey->uri); else if (akey->thumbnail_uri) base_hash = g_str_hash (akey->thumbnail_uri); + else if (akey->checksum) + base_hash = g_str_hash (akey->checksum); else g_assert_not_reached (); return base_hash + 31*akey->size; @@ -74,6 +78,8 @@ cache_key_equal (gconstpointer a, return strcmp (akey->uri, bkey->uri) == 0; else if (akey->thumbnail_uri && bkey->thumbnail_uri) return strcmp (akey->thumbnail_uri, bkey->thumbnail_uri) == 0; + else if (akey->checksum && bkey->checksum) + return strcmp (akey->checksum, bkey->checksum) == 0; return FALSE; } @@ -87,6 +93,7 @@ cache_key_dup (CacheKey *key) ret->icon = g_object_ref (key->icon); ret->uri = g_strdup (key->uri); ret->thumbnail_uri = g_strdup (key->thumbnail_uri); + ret->checksum = g_strdup (key->checksum); ret->size = key->size; return ret; } @@ -99,6 +106,7 @@ cache_key_destroy (gpointer a) g_object_unref (akey->icon); g_free (akey->uri); g_free (akey->thumbnail_uri); + g_free (akey->checksum); g_free (akey); } @@ -658,6 +666,7 @@ typedef struct { gboolean thumbnail; char *mimetype; GtkRecentInfo *recent_info; + char *checksum; GIcon *icon; GtkIconInfo *icon_info; guint width; @@ -1145,8 +1154,7 @@ shell_texture_cache_load_uri_sync (ShellTextureCache *cache, * @cache: The texture cache instance * @data: Raw image data * @len: length of @data - * @available_width: available width for the image, can be -1 if not limited - * @available_height: available height for the image, can be -1 if not limited + * @size: Size in pixels to use for the resulting texture * @error: Return location for error * * Synchronously creates an image from @data. The image is scaled down @@ -1161,24 +1169,47 @@ ClutterActor * shell_texture_cache_load_from_data (ShellTextureCache *cache, const guchar *data, gsize len, - int available_width, - int available_height, + int size, GError **error) { ClutterTexture *texture; CoglHandle texdata; GdkPixbuf *pixbuf; - - pixbuf = impl_load_pixbuf_data (data, len, available_width, available_height, error); - if (pixbuf == NULL) - return NULL; + CacheKey key; + gchar *checksum; texture = create_default_texture (cache); - texdata = pixbuf_to_cogl_handle (pixbuf); - set_texture_cogl_texture (texture, texdata); + clutter_actor_set_size (CLUTTER_ACTOR (texture), size, size); - g_object_unref (pixbuf); - cogl_handle_unref (texdata); + checksum = g_compute_checksum_for_data (G_CHECKSUM_SHA1, data, len); + + memset (&key, 0, sizeof(key)); + key.size = size; + key.checksum = checksum; + + texdata = g_hash_table_lookup (cache->priv->keyed_cache, &key); + if (texdata == NULL) + { + g_debug ("creating new pixbuf"); + pixbuf = impl_load_pixbuf_data (data, len, size, size, error); + if (!pixbuf) + { + g_object_unref (texture); + return NULL; + } + + texdata = pixbuf_to_cogl_handle (pixbuf); + g_object_unref (pixbuf); + + set_texture_cogl_texture (texture, texdata); + + g_hash_table_insert (cache->priv->keyed_cache, cache_key_dup (&key), texdata); + } + else + { + g_debug ("using texture from cache"); + set_texture_cogl_texture (texture, texdata); + } return CLUTTER_ACTOR (texture); } diff --git a/src/shell-texture-cache.h b/src/shell-texture-cache.h index d88967a53..14d132e72 100644 --- a/src/shell-texture-cache.h +++ b/src/shell-texture-cache.h @@ -83,8 +83,7 @@ ClutterActor *shell_texture_cache_load_uri_sync (ShellTextureCache *cache, ClutterActor *shell_texture_cache_load_from_data (ShellTextureCache *cache, const guchar *data, gsize len, - int available_width, - int available_height, + int size, GError **error); gboolean shell_texture_cache_pixbuf_equal (ShellTextureCache *cache, GdkPixbuf *a, GdkPixbuf *b); diff --git a/src/st/st-box-layout.c b/src/st/st-box-layout.c index 88c2d9309..d23226a91 100644 --- a/src/st/st-box-layout.c +++ b/src/st/st-box-layout.c @@ -1418,3 +1418,36 @@ st_box_layout_get_n_children (StBoxLayout *self) { return g_list_length (self->priv->children); } + +void +st_box_layout_move_child (StBoxLayout *self, + ClutterActor *actor, + int pos) +{ + StBoxLayoutPrivate *priv = ST_BOX_LAYOUT (self)->priv; + + GList *item = NULL; + + item = g_list_find (priv->children, actor); + + if (item == NULL) + { + g_warning ("Actor of type '%s' is not a child of the StBoxLayout container", + g_type_name (G_OBJECT_TYPE (actor))); + return; + } + + priv->children = g_list_delete_link (priv->children, item); + priv->children = g_list_insert (priv->children, actor, pos); + clutter_actor_queue_relayout ((ClutterActor*) self); +} + +void +st_box_layout_insert_actor (StBoxLayout *self, + ClutterActor *actor, + int pos) +{ + clutter_container_add_actor((ClutterContainer*) self, actor); + st_box_layout_move_child(self, actor, pos); +} + diff --git a/src/st/st-box-layout.h b/src/st/st-box-layout.h index 21cd0b757..33b59240a 100644 --- a/src/st/st-box-layout.h +++ b/src/st/st-box-layout.h @@ -95,6 +95,14 @@ void st_box_layout_destroy_children (StBoxLayout *box); guint st_box_layout_get_n_children (StBoxLayout *box); +void st_box_layout_move_child (StBoxLayout *self, + ClutterActor *actor, + int pos); + +void st_box_layout_insert_actor (StBoxLayout *self, + ClutterActor *actor, + int pos); + G_END_DECLS #endif /* _ST_BOX_LAYOUT_H */