[folks] Bug 651672 — Individual should have a displ ay-name property



commit fded410fd72c290596fb2b2fb2929fa92339b1c6
Author: Philip Withnall <philip withnall collabora co uk>
Date:   Wed Jul 25 13:10:02 2012 -0600

    Bug 651672 — Individual should have a display-name property
    
    Based on work by Jeremy Whiting and Laurent Contzen.
    
    New API:
     • Individual.display_name
     • StructuredName.to_string_with_format()
    
    https://bugzilla.gnome.org/show_bug.cgi?id=651672

 NEWS                          |    3 +
 folks/individual.vala         |  233 ++++++++++++++++++++++++++++++++++++++++-
 folks/name-details.vala       |  193 ++++++++++++++++++++++++++++++++--
 tests/folks/Makefile.am       |    5 +
 tests/folks/name-details.vala |  137 ++++++++++++++++++++++++
 5 files changed, 559 insertions(+), 12 deletions(-)
---
diff --git a/NEWS b/NEWS
index e3883a8..836ba89 100644
--- a/NEWS
+++ b/NEWS
@@ -6,8 +6,11 @@ Dependencies:
 Major changes:
 
 Bugs fixed:
+ • Bug 651672 — Individual should have a display-name property
 
 API changes:
+ • Add Individual.display_name
+ • Add StructuredName.to_string_with_format()
 
 Overview of changes from libfolks 0.9.5 to libfolks 0.9.6
 =========================================================
diff --git a/folks/individual.vala b/folks/individual.vala
index 06cae99..7b3eccd 100644
--- a/folks/individual.vala
+++ b/folks/individual.vala
@@ -1,6 +1,6 @@
 /*
  * Copyright (C) 2010 Collabora Ltd.
- * Copyright (C) 2011 Philip Withnall
+ * Copyright (C) 2011, 2013 Philip Withnall
  *
  * This library is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
@@ -303,6 +303,29 @@ public class Folks.Individual : Object,
    */
   public signal void removed (Individual? replacement_individual);
 
+  private string _display_name = "";
+
+  /**
+   * The name of this Individual to display in the UI.
+   *
+   * This value is set according to the following list of possibilities, each
+   * one being tried first on the primary persona, then on all other personas in
+   * the Individual, before falling back to the next item on the list:
+   * # Alias
+   * # Full name, structured name or nickname
+   * # E-mail address
+   * # Display ID (e.g. foo example org)
+   * # Postal address
+   * # _("Unnamed Person")
+   *
+   * @since UNRELEASED
+   */
+  [CCode (notify = false)]
+  public string display_name
+    {
+      get { return this._display_name; }
+    }
+
   private string _alias = "";
 
   /**
@@ -1340,6 +1363,9 @@ public class Folks.Individual : Object,
       this._update_postal_addresses (false);
       this._update_local_ids (false);
       this._update_location ();
+
+      /* Entirely derived fields. */
+      this._update_display_name ();
     }
 
   /* Delegate to update the value of a property on this individual from the
@@ -1707,6 +1733,202 @@ public class Folks.Individual : Object,
         });
     }
 
+  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 string _look_up_name_details_for_display_name (Persona? p)
+    {
+      var n = p as NameDetails;
+      if (n != null)
+        {
+          if (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 "";
+    }
+
+  private string _look_up_email_address_for_display_name (Persona? p)
+    {
+      var e = p as EmailDetails;
+      if (e != null)
+        {
+          foreach (var email_fd in ((!) e).email_addresses)
+            {
+              if (email_fd.value != null)
+                {
+                  return email_fd.value;
+                }
+            }
+        }
+
+      return "";
+    }
+
+  private string _look_up_display_id_for_display_name (Persona? p)
+    {
+      if (p != null && p.display_id != null)
+        {
+          return p.display_id;
+        }
+
+      return "";
+    }
+
+  private string _look_up_postal_address_for_display_name (Persona? p)
+    {
+      var address_details = p as PostalAddressDetails;
+      if (address_details != null)
+        {
+          foreach (var pa_fd in ((!) address_details).postal_addresses)
+            {
+              var pa = pa_fd.value;
+              if (pa != null)
+                {
+                  return pa.to_string ();
+                }
+            }
+        }
+
+      return "";
+    }
+
+  private void _update_display_name ()
+    {
+      Persona? primary_persona = null;
+      var new_display_name = "";
+
+      /* Find the primary persona first. The primary persona's values will be
+       * preferred in every case where they're set. */
+      foreach (var p in this._persona_set)
+        {
+          if (p.store.is_primary_store)
+            {
+              primary_persona = p;
+              break;
+            }
+        }
+
+      /* See if any persona has an alias set. */
+      new_display_name = this._look_up_alias_for_display_name (primary_persona);
+
+      foreach (var p in this._persona_set)
+        {
+          if (new_display_name != "")
+            {
+              break;
+            }
+
+          new_display_name = this._look_up_alias_for_display_name (p);
+        }
+
+      /* Try NameDetails next. */
+      if (new_display_name == "")
+        {
+          new_display_name =
+              this._look_up_name_details_for_display_name (primary_persona);
+
+          foreach (var p in this._persona_set)
+            {
+              if (new_display_name != "")
+                {
+                  break;
+                }
+
+              new_display_name =
+                  this._look_up_name_details_for_display_name (p);
+            }
+        }
+
+      /* Now the e-mail addresses. */
+      if (new_display_name == "")
+        {
+          new_display_name =
+              this._look_up_email_address_for_display_name (primary_persona);
+
+          foreach (var p in this._persona_set)
+            {
+              if (new_display_name != "")
+                {
+                  break;
+                }
+
+              new_display_name =
+                  this._look_up_email_address_for_display_name (p);
+            }
+        }
+
+      /* Now the display-id. */
+      if (new_display_name == "")
+        {
+          new_display_name =
+              this._look_up_display_id_for_display_name (primary_persona);
+
+          foreach (var p in this._persona_set)
+            {
+              if (new_display_name != "")
+                {
+                  break;
+                }
+
+              new_display_name =
+                  this._look_up_display_id_for_display_name (p);
+            }
+        }
+
+      /* Finally fall back to the postal address. */
+      if (new_display_name == "")
+        {
+          new_display_name =
+              this._look_up_postal_address_for_display_name (primary_persona);
+
+          foreach (var p in this._persona_set)
+            {
+              if (new_display_name != "")
+                {
+                  break;
+                }
+
+              new_display_name =
+                  this._look_up_postal_address_for_display_name (p);
+            }
+        }
+
+      /* Ultimate fall back: a static string. */
+      if (new_display_name == "")
+        {
+          /* Translators: This is the default name for an Individual
+           * when displayed in the UI if no personal details are available
+           * for them. */
+          new_display_name = _("Unnamed Person");
+        }
+
+      if (new_display_name != this._display_name)
+        {
+          this._display_name = new_display_name;
+          debug ("Setting display name ‘%s’", new_display_name);
+          this.notify_property ("display-name");
+        }
+    }
+
   private void _update_alias ()
     {
       this._update_single_valued_property (typeof (AliasDetails), (p) =>
@@ -1751,7 +1973,10 @@ public class Folks.Individual : Object,
           if (this._alias != alias)
             {
               this._alias = alias;
+              debug ("Setting alias ‘%s’", alias);
               this.notify_property ("alias");
+
+              this._update_display_name ();
             }
         });
     }
@@ -1932,6 +2157,8 @@ public class Folks.Individual : Object,
             {
               this._structured_name = name;
               this.notify_property ("structured-name");
+
+              this._update_display_name ();
             }
         });
     }
@@ -1961,6 +2188,8 @@ public class Folks.Individual : Object,
             {
               this._full_name = new_full_name;
               this.notify_property ("full-name");
+
+              this._update_display_name ();
             }
         });
     }
@@ -1990,6 +2219,8 @@ public class Folks.Individual : Object,
             {
               this._nickname = new_nickname;
               this.notify_property ("nickname");
+
+              this._update_display_name ();
             }
         });
     }
diff --git a/folks/name-details.vala b/folks/name-details.vala
index 956346c..5e87e83 100644
--- a/folks/name-details.vala
+++ b/folks/name-details.vala
@@ -1,6 +1,6 @@
 /*
- * Copyright (C) 2011 Collabora Ltd.
- * Copyright (C) 2011 Philip Withnall
+ * Copyright (C) 2011, 2013 Collabora Ltd.
+ * Copyright (C) 2011, 2013 Philip Withnall
  *
  * This library is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Lesser General Public License as published by
@@ -186,22 +186,193 @@ public class Folks.StructuredName : Object
              this._suffixes         == other.suffixes;
     }
 
+  private string _extract_initials (string names)
+    {
+      /* Extract the first letter of each word (where a word is a group of
+       * characters following whitespace or a hyphen.
+       * I've made this up since the documentation on
+       * http://lh.2xlibre.net/values/name_fmt/ doesn't specify how to extract
+       * the initials from a set of names. It should work for Western names,
+       * but I'm not so sure about other names. */
+      var output = new StringBuilder ();
+      var at_start_of_word = true;
+      int index = 0;
+      unichar c;
+
+      while (names.get_next_char (ref index, out c) == true)
+        {
+          /* Grab a new initial from any word preceded by a space or a hyphen,
+           * so (e.g.) ‘Mary-Jane’ becomes ‘MJ’. */
+          if (c.isspace () || c == '-')
+            {
+              at_start_of_word = true;
+            }
+          else if (at_start_of_word)
+            {
+              output.append_unichar (c);
+              at_start_of_word = false;
+            }
+        }
+
+      return output.str;
+    }
+
   /**
    * Formatted version of the structured name.
    *
+   * @return name formatted according to the current locale
    * @since 0.4.0
    */
   public string to_string ()
     {
-      /* Translators: format for the formatted structured name.
-       * Parameters (in order) are: prefixes (for the name), given name,
-       * family name, additional names and (name) suffixes */
-      var str = "%s, %s, %s, %s, %s";
-      return str.printf (this.prefixes,
-          this.given_name,
-          this.family_name,
-          this.additional_names,
-          this.suffixes);
+      /* FIXME: Ideally we’d use a format string translated to the locale of the
+       * persona whose name is being formatted, but no backend provides
+       * information about personas’ locales, so we have to settle for the
+       * current user’s locale.
+       *
+       * We thought about using nl_langinfo(_NL_NAME_NAME_FMT) here, but
+       * decided against it because:
+       *  1. It’s not the best documented API in the world, and its stability
+       *     is in question.
+       *  2. An attempt to improve the interface in glibc met with a wall of
+       *     complaints: https://sourceware.org/bugzilla/show_bug.cgi?id=14641.
+       *
+       * However, we do re-use the string format placeholders from
+       * _NL_NAME_NAME_FMT (as documented here:
+       * http://lh.2xlibre.net/values/name_fmt/) because there’s a chance glibc
+       * might eventually grow a useful interface for this.
+       *
+       * It does mean we have to implement our own parser for the name_fmt
+       * format though, since glibc doesn’t provide a formatting function. */
+
+      /* Translators: This is a format string used to convert structured names
+       * to a single string. It should be translated to the predominant
+       * semi-formal name format for your locale, using the placeholders
+       * documented here: http://lh.2xlibre.net/values/name_fmt/. You may be
+       * able to re-use the existing glibc format string for your locale on that
+       * page if it’s suitable.
+       *
+       * More explicitly: the supported placeholders are %f, %F, %g, %G, %m, %M,
+       * %t. The romanisation modifier (e.g. %Rf) is recognized but ignored.
+       * %s, %S and %d are all replaced by the same thing (the ‘Honorific
+       * Prefixes’ from vCard) so please avoid using more than one.
+       *
+       * For example, the format string ‘%g%t%m%t%f’ expands to ‘John Andrew
+       * Lees’ when used for a persona with first name ‘John’, additional names
+       * ‘Andrew’ and family names ‘Lees’.
+       *
+       * If you need additional placeholders with other information or
+       * punctuation, please file a bug against libfolks:
+       *   https://bugzilla.gnome.org/enter_bug.cgi?product=folks
+       */
+      var name_fmt = _("%g%t%m%t%f");
+
+      return this.to_string_with_format (name_fmt);
+    }
+
+  /**
+   * Formatted version of the structured name.
+   *
+   * This allows a custom format string to be specified, using the placeholders
+   * described on [[http://lh.2xlibre.net/values/name_fmt/]]. This ``name_fmt``
+   * must almost always be translated to the current locale. (Ideally it would
+   * be translated to the locale of the persona whose name is being formatted,
+   * but such locale information isn’t available.)
+   *
+   * @param name_fmt format string for the name
+   * @return name formatted according to the given format
+   * @since UNRELEASED
+   */
+  public string to_string_with_format (string name_fmt)
+    {
+      var output = new StringBuilder ();
+      var in_field_descriptor = false;
+      var field_descriptor_romanised = false;
+      var field_descriptor_empty = true;
+      int index = 0;
+      unichar c;
+
+      while (name_fmt.get_next_char (ref index, out c) == true)
+        {
+          /* Start of a field descriptor. */
+          if (c == '%')
+            {
+              in_field_descriptor = !in_field_descriptor;
+
+              /* If entering a field descriptor, reset the state
+               * and continue to the next character. */
+              if (in_field_descriptor)
+                {
+                  field_descriptor_romanised = false;
+                  continue;
+                }
+            }
+
+          if (in_field_descriptor)
+            {
+              /* Romanisation, e.g. using a field descriptor ‘%Rg’. */
+              if (c == 'R')
+                {
+                  /* FIXME: Romanisation isn't supported yet. */
+                  field_descriptor_romanised = true;
+                  continue;
+                }
+
+              var val = "";
+
+              /* Handle the different types of field descriptor. */
+              if (c == 'f')
+                {
+                  val = this._family_name;
+                }
+              else if (c == 'F')
+                {
+                  val = this._family_name.up ();
+                }
+              else if (c == 'g')
+                {
+                  val = this._given_name;
+                }
+              else if (c == 'G')
+                {
+                  val = this._extract_initials (this._given_name);
+                }
+              else if (c == 'm')
+                {
+                  val = this._additional_names;
+                }
+              else if (c == 'M')
+                {
+                  val = this._extract_initials (this._additional_names);
+                }
+              else if (c == 's' || c == 'S' || c == 'd')
+                {
+                  /* FIXME: Not ideal, but prefixes will have to do. */
+                  val = this._prefixes;
+                }
+              else if (c == 't')
+                {
+                  val = (field_descriptor_empty == false) ? " " : "";
+                }
+              else if (c == 'l' || c == 'o' || c == 'p')
+                {
+                  /* FIXME: Not supported. */
+                  val = "";
+                }
+
+              /* Append the value of the field descriptor. */
+              output.append (val);
+              in_field_descriptor = false;
+              field_descriptor_empty = (val == "");
+            }
+          else
+            {
+              /* Handle non-field descriptor characters. */
+              output.append_unichar (c);
+            }
+        }
+
+      return output.str;
     }
 }
 
diff --git a/tests/folks/Makefile.am b/tests/folks/Makefile.am
index 0856529..1adaec3 100644
--- a/tests/folks/Makefile.am
+++ b/tests/folks/Makefile.am
@@ -68,6 +68,7 @@ noinst_PROGRAMS = \
        avatar-cache \
        object-cache \
        phone-field-details \
+       name-details \
        init \
        $(NULL)
 
@@ -109,6 +110,10 @@ phone_field_details_SOURCES = \
        phone-field-details.vala \
        $(NULL)
 
+name_details_SOURCES = \
+       name-details.vala \
+       $(NULL)
+
 init_SOURCES = \
        init.vala \
        $(NULL)
diff --git a/tests/folks/name-details.vala b/tests/folks/name-details.vala
new file mode 100644
index 0000000..cc451b1
--- /dev/null
+++ b/tests/folks/name-details.vala
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2013 Philip Withnall
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Philip Withnall <philip tecnocode co uk>
+ */
+
+using Gee;
+using Folks;
+
+public class NameDetailsTests : Folks.TestCase
+{
+  public NameDetailsTests ()
+    {
+      base ("NameDetails");
+
+      this.add_test ("structured-name-to-string",
+          this.test_structured_name_to_string);
+      this.add_test ("structured-name-to-string-with-format",
+          this.test_structured_name_to_string_with_format);
+    }
+
+  public void test_structured_name_to_string ()
+    {
+      /* This test can only run in the C locale. Ignore thread safety issues
+       * with calling setlocale(). In the C locale, we expect the NAME_FMT to
+       * be ‘%g%t%m%t%f’, as per StructuredName.to_string(). */
+      var old_locale = Intl.setlocale (LocaleCategory.ALL, null);
+      Intl.setlocale (LocaleCategory.ALL, "C");
+
+      /* Complete name. */
+      var name = new StructuredName ("Family", "Given", "Additional Names",
+          "Ms.", "Esq.");
+      assert (name.to_string () == "Given Additional Names Family");
+
+      /* More normal name. */
+      name = new StructuredName ("Family", "Given", null, null, null);
+      assert (name.to_string () == "Given Family");
+
+      /* Restore the locale. */
+      Intl.setlocale (LocaleCategory.ALL, old_locale);
+    }
+
+  private struct FormatPair
+    {
+      unowned string format;
+      unowned string result;
+    }
+
+  public void test_structured_name_to_string_with_format ()
+    {
+      /* This test isn’t locale-dependent. Hooray! Set up a single
+       * StructuredName and try to format it in different ways. */
+      var name = new StructuredName ("Wesson-Smythe", "John Graham-Charlie",
+          "De Mimsy", "Sir", "Esq.");
+
+      const FormatPair[] tests =
+       {
+         /* Individual format placeholders. */
+         { "%f", "Wesson-Smythe" },
+         { "%F", "WESSON-SMYTHE" },
+         { "%g", "John Graham-Charlie" },
+         { "%G", "JGC" },
+         { "%l", "" }, /* unhandled */
+         { "%o", "" }, /* unhandled */
+         { "%m", "De Mimsy" },
+         { "%M", "DM" },
+         { "%p", "" }, /* unhandled */
+         { "%s", "Sir" },
+         { "%S", "Sir" },
+         { "%d", "Sir" },
+         { "%t", "" },
+         { "%p%t", "" },
+         { "%f%t", "Wesson-Smythe " }, /* note the trailing space */
+         { "%%", "%" },
+         /* Romanised versions of the above (Romanisation is ignored). */
+         { "%Rf", "Wesson-Smythe" },
+         { "%RF", "WESSON-SMYTHE" },
+         { "%Rg", "John Graham-Charlie" },
+         { "%RG", "JGC" },
+         { "%Rl", "" }, /* unhandled */
+         { "%Ro", "" }, /* unhandled */
+         { "%Rm", "De Mimsy" },
+         { "%RM", "DM" },
+         { "%Rp", "" }, /* unhandled */
+         { "%Rs", "Sir" },
+         { "%RS", "Sir" },
+         { "%Rd", "Sir" },
+         { "%Rt", "" },
+         { "%Rp%t", "" },
+         { "%Rf%t", "Wesson-Smythe " }, /* note the trailing space */
+         /* Selected internationalised format strings from
+          * http://lh.2xlibre.net/values/name_fmt/. */
+         { "%d%t%g%t%m%t%f", "Sir John Graham-Charlie De Mimsy Wesson-Smythe" },
+         { "%p%t%f%t%g", "Wesson-Smythe John Graham-Charlie" },
+         /* yes, the ff_SN locale actually uses this: */
+         { "%p%t%g%m%t%f", "John Graham-CharlieDe Mimsy Wesson-Smythe" },
+         { "%g%t%f", "John Graham-Charlie Wesson-Smythe" },
+         {
+           /* and the fa_IR locale uses this: */
+           "%d%t%s%t%f%t%g%t%m",
+           "Sir Sir Wesson-Smythe John Graham-Charlie De Mimsy"
+         },
+         { "%f%t%d", "Wesson-Smythe Sir" },
+       };
+
+      /* Run the tests. */
+      foreach (var pair in tests)
+        {
+          assert (name.to_string_with_format (pair.format) == pair.result);
+        }
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  var tests = new NameDetailsTests ();
+  tests.register ();
+  Test.run ();
+  tests.final_tear_down ();
+
+  return 0;
+}


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