[folks] Bug 651672 — Individual should have a displ ay-name property
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [folks] Bug 651672 — Individual should have a displ ay-name property
- Date: Fri, 8 Nov 2013 00:41:49 +0000 (UTC)
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]