Commit aac409f7 authored by Julian Sparber's avatar Julian Sparber

Avatar: use GNOME 3.32 avatar styles for fallback

parent 4ad06cc4
/*
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
namespace Contacts.AvatarUtils {
// The following was based on code written by Felipe Borges for
// gnome-control-enter in panels/user-accounts/user-utils.c commit
// 02c288ab6f069a0c106323a93400f192a63cb67e. The copyright in that
// file is: "Copyright 2009-2010 Red Hat, Inc,"
public Gdk.Pixbuf generate_user_picture(string name, int size, bool label = true) {
Cairo.Surface surface = new Cairo.ImageSurface(
Cairo.Format.ARGB32, size, size
);
Cairo.Context cr = new Cairo.Context(surface);
cr.rectangle(0, 0, size, size);
/* Fill the background with a colour for the name */
Gdk.RGBA color = get_color_for_name(name);
cr.set_source_rgb(
color.red / 255.0, color.green / 255.0, color.blue / 255.0
);
cr.fill();
/* Draw the initials on top */
if (label) {
string? initials = extract_initials_from_name(name);
if (initials != null) {
string font = "Sans %d".printf((int) GLib.Math.ceil(size / 2.5));
cr.set_source_rgb(1.0, 1.0, 1.0);
Pango.Layout layout = Pango.cairo_create_layout(cr);
layout.set_text(initials, -1);
layout.set_font_description(Pango.FontDescription.from_string(font));
int width, height;
layout.get_size(out width, out height);
cr.translate(size / 2, size / 2);
cr.move_to(
-((double) width / Pango.SCALE) / 2,
-((double) height / Pango.SCALE) / 2
);
Pango.cairo_show_layout(cr, layout);
}
}
return Gdk.pixbuf_get_from_surface(
surface, 0, 0, size, size
);
}
public Gdk.Pixbuf round_image(Gdk.Pixbuf source) {
int size = source.width;
Cairo.Surface surface = new Cairo.ImageSurface(
Cairo.Format.ARGB32, size, size
);
Cairo.Context cr = new Cairo.Context(surface);
/* Clip a circle */
cr.arc(size / 2, size / 2, size / 2, 0, 2 * GLib.Math.PI);
cr.clip();
cr.new_path();
Gdk.cairo_set_source_pixbuf(cr, source, 0, 0);
cr.paint();
return Gdk.pixbuf_get_from_surface(
surface, 0, 0, size, size
);
}
public string? extract_initials_from_name(string name) {
string normalized = name.strip().up().normalize();
string? initials = null;
if (normalized != "") {
GLib.StringBuilder buf = new GLib.StringBuilder();
unichar c = 0;
int index = 0;
// Get the first alphanumeric char of the string
for (int i = 0; normalized.get_next_char(ref index, out c); i++) {
if (c.isalnum()) {
buf.append_unichar(c);
break;
}
}
// Get the first alphanumeric char of the last word of the string
index = normalized.last_index_of_char(' ');
if (index >= 0) {
for (int i = 0; normalized.get_next_char(ref index, out c); i++) {
if (c.isalnum()) {
buf.append_unichar(c);
break;
}
}
}
if (buf.data.length > 0) {
initials = (string) buf.data;
}
}
return initials;
}
public Gdk.RGBA get_color_for_name(string name) {
// https://gitlab.gnome.org/Community/Design/HIG-app-icons/blob/master/GNOME%20HIG.gpl
const double[,3] GNOME_COLOR_PALETTE = {
{ 98, 160, 234 },
{ 53, 132, 228 },
{ 28, 113, 216 },
{ 26, 95, 180 },
{ 87, 227, 137 },
{ 51, 209, 122 },
{ 46, 194, 126 },
{ 38, 162, 105 },
{ 248, 228, 92 },
{ 246, 211, 45 },
{ 245, 194, 17 },
{ 229, 165, 10 },
{ 255, 163, 72 },
{ 255, 120, 0 },
{ 230, 97, 0 },
{ 198, 70, 0 },
{ 237, 51, 59 },
{ 224, 27, 36 },
{ 192, 28, 40 },
{ 165, 29, 45 },
{ 192, 97, 203 },
{ 163, 71, 186 },
{ 129, 61, 156 },
{ 97, 53, 131 },
{ 181, 131, 90 },
{ 152, 106, 68 },
{ 134, 94, 60 },
{ 99, 69, 44 }
};
Gdk.RGBA color = { 255, 255, 255, 1.0 };
uint hash;
uint number_of_colors = GNOME_COLOR_PALETTE.length[0];
uint idx;
if (name == "") {
// Return a random color if we don't have a name
idx = Random.int_range (0, (int32) number_of_colors);
color.red = GNOME_COLOR_PALETTE[idx,0];
color.green = GNOME_COLOR_PALETTE[idx,1];
color.blue = GNOME_COLOR_PALETTE[idx,2];
return color;
}
hash = name.hash();
idx = hash % number_of_colors;
color.red = GNOME_COLOR_PALETTE[idx,0];
color.green = GNOME_COLOR_PALETTE[idx,1];
color.blue = GNOME_COLOR_PALETTE[idx,2];
return color;
}
}
......@@ -26,18 +26,13 @@ using Gee;
public class Contacts.Avatar : DrawingArea {
private int size;
private Gdk.Pixbuf? pixbuf = null;
private Cairo.Surface? cache = null;
private Gdk.Pixbuf? cache = null;
private Contact? contact = null;
// We want to lazily load the Pixbuf to make sure we don't draw all contact avatars at once.
// As long as there is no need for it to be drawn, keep this to false.
private bool avatar_loaded = false;
// The background color used in case of a fallback avatar
private Gdk.RGBA? bg_color = null;
// The color used for an initial or the fallback icon
private const Gdk.RGBA fg_color = { 0, 0, 0, 0.25 };
public Avatar (int size, Contact? contact = null) {
this.contact = contact;
if (contact != null) {
......@@ -96,69 +91,80 @@ public class Contacts.Avatar : DrawingArea {
}
private void draw_cached_avatar (Cairo.Context cr) {
cr.set_source_surface (this.cache, 0, 0);
Gdk.cairo_set_source_pixbuf (cr, this.cache, 0, 0);
cr.paint ();
}
private Cairo.Surface create_contact_avatar () {
Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, size, size);
Cairo.Context cr = new Cairo.Context (surface);
Gdk.cairo_set_source_pixbuf (cr, this.pixbuf, 0, 0);
// Clip with a circle
create_circle (cr);
cr.clip_preserve ();
cr.paint ();
return surface;
private Gdk.Pixbuf create_contact_avatar () {
return AvatarUtils.round_image(this.pixbuf);
}
private Cairo.Surface create_fallback () {
Cairo.ImageSurface surface = new Cairo.ImageSurface (Cairo.Format.ARGB32, size, size);
Cairo.Context cr = new Cairo.Context (surface);
// The background color
if (this.bg_color == null)
calculate_color ();
private Gdk.Pixbuf create_fallback () {
string name = "";
bool show_label = false;
if (this.contact != null && this.contact.individual != null) {
name = find_display_name ();
/* If we don't have a usable name use the display_name
* to generate the color but don't show any label
*/
if (name == "") {
name = this.contact.individual.display_name;
} else {
show_label = true;
}
}
var pixbuf = AvatarUtils.generate_user_picture(name, this.size, show_label);
pixbuf = AvatarUtils.round_image(pixbuf);
// Fill the background circle
cr.set_source_rgb (this.bg_color.red, this.bg_color.green, this.bg_color.blue);
cr.arc (this.size / 2, this.size / 2, this.size / 2, 0, 2*Math.PI);
create_circle (cr);
cr.fill_preserve ();
return pixbuf;
}
// Draw the icon
try {
var theme = IconTheme.get_default ();
var fallback_avatar = theme.lookup_icon ("avatar-default",
this.size * 4 / 5,
IconLookupFlags.FORCE_SYMBOLIC);
var icon_pixbuf = fallback_avatar.load_symbolic (fg_color);
create_circle (cr);
cr.clip_preserve ();
Gdk.cairo_set_source_pixbuf (cr, icon_pixbuf, 1 + this.size / 10, 1 + this.size / 5);
cr.paint ();
} catch (Error e) {
warning ("Couldn't get default avatar icon: %s", e.message);
/* Find a nice name to generate the label and color for the fallback avatar
* This code is mostly copied from folks, but folks also tries email and phone number
* as a display name which we don't want to have as a label
*/
private string find_display_name () {
string name = "";
Persona primary_persona = null;
foreach (var p in this.contact.individual.personas) {
if (p.store.is_primary_store) {
primary_persona = p;
break;
}
}
name = look_up_alias_for_display_name (primary_persona);
if (name == "") {
foreach (var p in this.contact.individual.personas) {
name = look_up_alias_for_display_name (p);
}
}
if (name == "") {
foreach (var p in this.contact.individual.personas) {
name = look_up_name_details_for_display_name (p);
}
}
return surface;
return name;
}
private void calculate_color () {
// We use the hash of the id so we get the same color each time for the same contact
var hash = (this.contact != null)? str_hash (this.contact.individual.id) : Gdk.CURRENT_TIME;
var r = ((hash & 0xFF0000) >> 16) / 255.0;
var g = ((hash & 0x00FF00) >> 8) / 255.0;
var b = (hash & 0x0000FF) / 255.0;
// Make it a bit lighter by default (and since the foreground will be darker)
this.bg_color = Gdk.RGBA () {
red = (r + 2) / 3.0,
green = (g + 2) / 3.0,
blue = (b + 2) / 3.0,
alpha = 0
};
private string look_up_alias_for_display_name (Persona? p) {
var a = p as AliasDetails;
if (a != null && a.alias != null)
return a.alias;
return "";
}
private void create_circle (Cairo.Context cr) {
cr.arc (this.size / 2, this.size / 2, this.size / 2, 0, 2*Math.PI);
private string look_up_name_details_for_display_name (Persona? p) {
var n = p as NameDetails;
if (n != null) {
if (n.full_name != null && n.full_name != "")
return n.full_name;
else if (n.structured_name != null)
return n.structured_name.to_string ();
else if (n.nickname != "")
return n.nickname;
}
return "";
}
}
......@@ -14,6 +14,7 @@ libcontacts_sources = files(
'contacts-typeset.vala',
'contacts-type-descriptor.vala',
'contacts-utils.vala',
'contacts-avatar-utils.vala',
'contacts-vcard-type-mapping.vala',
)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment