[gnome-contacts] Avatar: use GNOME 3.32 avatar styles for fallback



commit aac409f7e53676eefb2a55044d958005c21d96dd
Author: Julian Sparber <julian sparber net>
Date:   Thu Jul 25 10:30:40 2019 +0200

    Avatar: use GNOME 3.32 avatar styles for fallback

 src/contacts-avatar-utils.vala | 167 +++++++++++++++++++++++++++++++++++++++++
 src/contacts-avatar.vala       | 122 ++++++++++++++++--------------
 src/meson.build                |   1 +
 3 files changed, 232 insertions(+), 58 deletions(-)
---
diff --git a/src/contacts-avatar-utils.vala b/src/contacts-avatar-utils.vala
new file mode 100644
index 0000000..ff51d2d
--- /dev/null
+++ b/src/contacts-avatar-utils.vala
@@ -0,0 +1,167 @@
+/*
+ * 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;
+  }
+}
diff --git a/src/contacts-avatar.vala b/src/contacts-avatar.vala
index b3bec2f..d5d0c25 100644
--- a/src/contacts-avatar.vala
+++ b/src/contacts-avatar.vala
@@ -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 "";
   }
 }
diff --git a/src/meson.build b/src/meson.build
index 773c3dc..0401f78 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -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',
 )
 


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