Add a message tray with icons for ongoing conversations
Add a message tray that slides out when you move your mouse to the bottom of the screen. The icons for ongoing conversations are added to the message tray when the first message in the conversation is received. The icon is removed when the corresponding conversation window is closed. Store the avatar icons in the texture cache and use the checksum for the data bytes for the icon as the key. This allows to reuse the icon data for the message tray icon. Add st_box_layout_insert_actor() that allows inserting an actor at the arbitrary position in the container. It is needed to be able to add the icon representing the most recent conversation to the front of the list of icons in the message tray.
This commit is contained in:
parent
b0a0ee297c
commit
3b5c468cbf
@ -317,7 +317,19 @@ StTooltip {
|
|||||||
background: rgba(0,0,0,0.9);
|
background: rgba(0,0,0,0.9);
|
||||||
border: 1px solid rgba(128,128,128,0.45);
|
border: 1px solid rgba(128,128,128,0.45);
|
||||||
color: white;
|
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;
|
spacing: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ let wm = null;
|
|||||||
let messaging = null;
|
let messaging = null;
|
||||||
let notificationDaemon = null;
|
let notificationDaemon = null;
|
||||||
let notificationPopup = null;
|
let notificationPopup = null;
|
||||||
|
let messageTray = null;
|
||||||
let recorder = null;
|
let recorder = null;
|
||||||
let shellDBusService = null;
|
let shellDBusService = null;
|
||||||
let modalCount = 0;
|
let modalCount = 0;
|
||||||
@ -113,6 +114,7 @@ function start() {
|
|||||||
messaging = new Messaging.Messaging();
|
messaging = new Messaging.Messaging();
|
||||||
notificationDaemon = new NotificationDaemon.NotificationDaemon();
|
notificationDaemon = new NotificationDaemon.NotificationDaemon();
|
||||||
notificationPopup = new MessageTray.Notification();
|
notificationPopup = new MessageTray.Notification();
|
||||||
|
messageTray = new MessageTray.MessageTray();
|
||||||
|
|
||||||
global.screen.connect('toggle-recording', function() {
|
global.screen.connect('toggle-recording', function() {
|
||||||
if (recorder == null) {
|
if (recorder == null) {
|
||||||
|
@ -10,6 +10,8 @@ const Main = imports.ui.main;
|
|||||||
const ANIMATION_TIME = 0.2;
|
const ANIMATION_TIME = 0.2;
|
||||||
const NOTIFICATION_TIMEOUT = 4;
|
const NOTIFICATION_TIMEOUT = 4;
|
||||||
|
|
||||||
|
const MESSAGE_TRAY_TIMEOUT = 0.2;
|
||||||
|
|
||||||
function Notification() {
|
function Notification() {
|
||||||
this._init();
|
this._init();
|
||||||
}
|
}
|
||||||
@ -80,3 +82,100 @@ Notification.prototype = {
|
|||||||
this.actor.hide();
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@ -289,10 +289,13 @@ Source.prototype = {
|
|||||||
this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed));
|
this._closedId = this._channel.connect('Closed', Lang.bind(this, this._channelClosed));
|
||||||
|
|
||||||
this._targetId = targetId;
|
this._targetId = targetId;
|
||||||
log('channel for ' + this._targetId);
|
log('channel for ' + this._targetId + ' channelPath ' + channelPath);
|
||||||
|
|
||||||
this._pendingMessages = null;
|
this._pendingMessages = null;
|
||||||
|
|
||||||
|
this._avatar = null;
|
||||||
|
this._avatarBytes = null;
|
||||||
|
|
||||||
// FIXME: RequestAvatar is deprecated in favor of
|
// FIXME: RequestAvatar is deprecated in favor of
|
||||||
// RequestAvatars; but RequestAvatars provides no explicit
|
// RequestAvatars; but RequestAvatars provides no explicit
|
||||||
// indication of "no avatar available", so there's no way we
|
// indication of "no avatar available", so there's no way we
|
||||||
@ -316,8 +319,8 @@ Source.prototype = {
|
|||||||
|
|
||||||
_gotAvatar: function(result, excp) {
|
_gotAvatar: function(result, excp) {
|
||||||
if (result) {
|
if (result) {
|
||||||
let bytes = result[0];
|
this._avatarBytes = result[0];
|
||||||
this._avatar = Shell.TextureCache.get_default().load_from_data(bytes, bytes.length, AVATAR_SIZE, AVATAR_SIZE);
|
this._avatar = Shell.TextureCache.get_default().load_from_data(this._avatarBytes, this._avatarBytes.length, AVATAR_SIZE);
|
||||||
log('got avatar for ' + this._targetId);
|
log('got avatar for ' + this._targetId);
|
||||||
} else {
|
} else {
|
||||||
// fallback avatar (FIXME)
|
// fallback avatar (FIXME)
|
||||||
@ -338,14 +341,23 @@ Source.prototype = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
_channelClosed: function() {
|
_channelClosed: function() {
|
||||||
log('closed');
|
log('Channel closed ' + this._targetId);
|
||||||
this._channel.disconnect(this._closedId);
|
this._channel.disconnect(this._closedId);
|
||||||
this._channelText.disconnect(this._receivedId);
|
this._channelText.disconnect(this._receivedId);
|
||||||
|
Main.messageTray.remove(this._targetId);
|
||||||
},
|
},
|
||||||
|
|
||||||
_receivedMessage: function(channel, id, timestamp, sender,
|
_receivedMessage: function(channel, id, timestamp, sender,
|
||||||
type, flags, text) {
|
type, flags, text) {
|
||||||
log('Received: id ' + id + ', time ' + timestamp + ', sender ' + sender + ', type ' + type + ', flags ' + flags + ': ' + text);
|
log('Received: id ' + id + ', time ' + timestamp + ', sender ' + sender + ', type ' + type + ', flags ' + flags + ': ' + text);
|
||||||
Main.notificationPopup.show(this._avatar, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
#define GNOME_DESKTOP_USE_UNSTABLE_API
|
#define GNOME_DESKTOP_USE_UNSTABLE_API
|
||||||
#include <libgnomeui/gnome-desktop-thumbnail.h>
|
#include <libgnomeui/gnome-desktop-thumbnail.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
@ -15,6 +16,7 @@ typedef struct
|
|||||||
GIcon *icon;
|
GIcon *icon;
|
||||||
gchar *uri;
|
gchar *uri;
|
||||||
gchar *thumbnail_uri;
|
gchar *thumbnail_uri;
|
||||||
|
gchar *checksum;
|
||||||
|
|
||||||
/* This one is common to all */
|
/* This one is common to all */
|
||||||
guint size;
|
guint size;
|
||||||
@ -49,6 +51,8 @@ cache_key_hash (gconstpointer a)
|
|||||||
base_hash = g_str_hash (akey->uri);
|
base_hash = g_str_hash (akey->uri);
|
||||||
else if (akey->thumbnail_uri)
|
else if (akey->thumbnail_uri)
|
||||||
base_hash = g_str_hash (akey->thumbnail_uri);
|
base_hash = g_str_hash (akey->thumbnail_uri);
|
||||||
|
else if (akey->checksum)
|
||||||
|
base_hash = g_str_hash (akey->checksum);
|
||||||
else
|
else
|
||||||
g_assert_not_reached ();
|
g_assert_not_reached ();
|
||||||
return base_hash + 31*akey->size;
|
return base_hash + 31*akey->size;
|
||||||
@ -74,6 +78,8 @@ cache_key_equal (gconstpointer a,
|
|||||||
return strcmp (akey->uri, bkey->uri) == 0;
|
return strcmp (akey->uri, bkey->uri) == 0;
|
||||||
else if (akey->thumbnail_uri && bkey->thumbnail_uri)
|
else if (akey->thumbnail_uri && bkey->thumbnail_uri)
|
||||||
return strcmp (akey->thumbnail_uri, bkey->thumbnail_uri) == 0;
|
return strcmp (akey->thumbnail_uri, bkey->thumbnail_uri) == 0;
|
||||||
|
else if (akey->checksum && bkey->checksum)
|
||||||
|
return strcmp (akey->checksum, bkey->checksum) == 0;
|
||||||
|
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
@ -87,6 +93,7 @@ cache_key_dup (CacheKey *key)
|
|||||||
ret->icon = g_object_ref (key->icon);
|
ret->icon = g_object_ref (key->icon);
|
||||||
ret->uri = g_strdup (key->uri);
|
ret->uri = g_strdup (key->uri);
|
||||||
ret->thumbnail_uri = g_strdup (key->thumbnail_uri);
|
ret->thumbnail_uri = g_strdup (key->thumbnail_uri);
|
||||||
|
ret->checksum = g_strdup (key->checksum);
|
||||||
ret->size = key->size;
|
ret->size = key->size;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
@ -99,6 +106,7 @@ cache_key_destroy (gpointer a)
|
|||||||
g_object_unref (akey->icon);
|
g_object_unref (akey->icon);
|
||||||
g_free (akey->uri);
|
g_free (akey->uri);
|
||||||
g_free (akey->thumbnail_uri);
|
g_free (akey->thumbnail_uri);
|
||||||
|
g_free (akey->checksum);
|
||||||
g_free (akey);
|
g_free (akey);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,6 +666,7 @@ typedef struct {
|
|||||||
gboolean thumbnail;
|
gboolean thumbnail;
|
||||||
char *mimetype;
|
char *mimetype;
|
||||||
GtkRecentInfo *recent_info;
|
GtkRecentInfo *recent_info;
|
||||||
|
char *checksum;
|
||||||
GIcon *icon;
|
GIcon *icon;
|
||||||
GtkIconInfo *icon_info;
|
GtkIconInfo *icon_info;
|
||||||
guint width;
|
guint width;
|
||||||
@ -1145,8 +1154,7 @@ shell_texture_cache_load_uri_sync (ShellTextureCache *cache,
|
|||||||
* @cache: The texture cache instance
|
* @cache: The texture cache instance
|
||||||
* @data: Raw image data
|
* @data: Raw image data
|
||||||
* @len: length of @data
|
* @len: length of @data
|
||||||
* @available_width: available width for the image, can be -1 if not limited
|
* @size: Size in pixels to use for the resulting texture
|
||||||
* @available_height: available height for the image, can be -1 if not limited
|
|
||||||
* @error: Return location for error
|
* @error: Return location for error
|
||||||
*
|
*
|
||||||
* Synchronously creates an image from @data. The image is scaled down
|
* Synchronously creates an image from @data. The image is scaled down
|
||||||
@ -1161,24 +1169,47 @@ ClutterActor *
|
|||||||
shell_texture_cache_load_from_data (ShellTextureCache *cache,
|
shell_texture_cache_load_from_data (ShellTextureCache *cache,
|
||||||
const guchar *data,
|
const guchar *data,
|
||||||
gsize len,
|
gsize len,
|
||||||
int available_width,
|
int size,
|
||||||
int available_height,
|
|
||||||
GError **error)
|
GError **error)
|
||||||
{
|
{
|
||||||
ClutterTexture *texture;
|
ClutterTexture *texture;
|
||||||
CoglHandle texdata;
|
CoglHandle texdata;
|
||||||
GdkPixbuf *pixbuf;
|
GdkPixbuf *pixbuf;
|
||||||
|
CacheKey key;
|
||||||
pixbuf = impl_load_pixbuf_data (data, len, available_width, available_height, error);
|
gchar *checksum;
|
||||||
if (pixbuf == NULL)
|
|
||||||
return NULL;
|
|
||||||
|
|
||||||
texture = create_default_texture (cache);
|
texture = create_default_texture (cache);
|
||||||
|
clutter_actor_set_size (CLUTTER_ACTOR (texture), size, size);
|
||||||
|
|
||||||
|
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);
|
texdata = pixbuf_to_cogl_handle (pixbuf);
|
||||||
|
g_object_unref (pixbuf);
|
||||||
|
|
||||||
set_texture_cogl_texture (texture, texdata);
|
set_texture_cogl_texture (texture, texdata);
|
||||||
|
|
||||||
g_object_unref (pixbuf);
|
g_hash_table_insert (cache->priv->keyed_cache, cache_key_dup (&key), texdata);
|
||||||
cogl_handle_unref (texdata);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
g_debug ("using texture from cache");
|
||||||
|
set_texture_cogl_texture (texture, texdata);
|
||||||
|
}
|
||||||
|
|
||||||
return CLUTTER_ACTOR (texture);
|
return CLUTTER_ACTOR (texture);
|
||||||
}
|
}
|
||||||
|
@ -83,8 +83,7 @@ ClutterActor *shell_texture_cache_load_uri_sync (ShellTextureCache *cache,
|
|||||||
ClutterActor *shell_texture_cache_load_from_data (ShellTextureCache *cache,
|
ClutterActor *shell_texture_cache_load_from_data (ShellTextureCache *cache,
|
||||||
const guchar *data,
|
const guchar *data,
|
||||||
gsize len,
|
gsize len,
|
||||||
int available_width,
|
int size,
|
||||||
int available_height,
|
|
||||||
GError **error);
|
GError **error);
|
||||||
|
|
||||||
gboolean shell_texture_cache_pixbuf_equal (ShellTextureCache *cache, GdkPixbuf *a, GdkPixbuf *b);
|
gboolean shell_texture_cache_pixbuf_equal (ShellTextureCache *cache, GdkPixbuf *a, GdkPixbuf *b);
|
||||||
|
@ -1418,3 +1418,36 @@ st_box_layout_get_n_children (StBoxLayout *self)
|
|||||||
{
|
{
|
||||||
return g_list_length (self->priv->children);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -95,6 +95,14 @@ void st_box_layout_destroy_children (StBoxLayout *box);
|
|||||||
|
|
||||||
guint st_box_layout_get_n_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
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* _ST_BOX_LAYOUT_H */
|
#endif /* _ST_BOX_LAYOUT_H */
|
||||||
|
Loading…
Reference in New Issue
Block a user