[gnome-contacts/feature/role-details] Support showing a role of a contact




commit 6ac88b1dcfc2a3d00e8aee39f9489e0b2dc6bbeb
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Thu Jan 20 11:14:52 2022 +0100

    Support showing a role of a contact
    
    The role property contains 2 (optional) elements: an organisation name
    (note that "organisation" is broader than "company"), and the role of
    the person in that organisation (for example: "Board Member").
    
    This is mostly useful for people who work in a corporate setting, but
    can also be nice to keep track of what your relatives are doing.
    
    In the ContactSheet (ie. viewing mode), the property shows itself as a
    single row: "$ROLE at $ORGANISATION"; the text may slightly differ if
    one of the two is not available. In the ContactEditor, it gets split up
    into two rows in the same listbox: one for the organisation and one for
    the actual role.
    
    Finally, note that a contact can have multiple roles/organisations, so
    this property can occur more than once.

 data/contacts.gresource.xml                       |  1 +
 data/icons/scalable/actions/building-symbolic.svg |  4 ++
 src/contacts-contact-sheet.vala                   | 35 +++++++++++
 src/contacts-editor-persona.vala                  |  1 +
 src/contacts-editor-property.vala                 | 76 +++++++++++++++++++++++
 src/contacts-fake-persona-store.vala              | 31 ++++++++-
 src/contacts-utils.vala                           |  2 +-
 7 files changed, 147 insertions(+), 3 deletions(-)
---
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 246036e1..653c1d4c 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -4,6 +4,7 @@
     <file compressed="true">ui/style.css</file>
 
     <file preprocess="xml-stripblanks">icons/scalable/actions/birthday-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/actions/building-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/external-link-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/map-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/note-symbolic.svg</file>
diff --git a/data/icons/scalable/actions/building-symbolic.svg 
b/data/icons/scalable/actions/building-symbolic.svg
new file mode 100644
index 00000000..fcf36a6c
--- /dev/null
+++ b/data/icons/scalable/actions/building-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg";>
+    <path d="m 2 0 c -0.554688 0 -1.027344 0.445312 -1 1 v 14 h -1 v 1 h 16 v -1 h -1 v -14 c 0 -1 -1 -1 -1 
-1 z m 1.25 2 h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 
c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m 4 0 h 1.5 c 
0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 
-0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m 4 0 h 1.5 c 0.136719 0 0.25 0.113281 
0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v 
-1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m -8 3 h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 
0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 
0.113281 -0.25 0.25 -0.25 z m 4 0 h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 
0.25 -0.25 0.25 h -1.5 c -0.136719
  0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m 4 0 h 1.5 c 0.136719 0 
0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 
-0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m -8 3 h 1.5 c 0.136719 0 0.25 0.113281 0.25 
0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 
-0.136719 0.113281 -0.25 0.25 -0.25 z m 4 0 h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 
-0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 
-0.25 0.25 -0.25 z m 4 0 h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 
0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 -0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m -8 3 
h 1.5 c 0.136719 0 0.25 0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 
-0.25 -0.113281 -0.25 -0.25 v -1
 .5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m 3.75 1 h 2 v 3 h -2 z m 4.25 -1 h 1.5 c 0.136719 0 0.25 
0.113281 0.25 0.25 v 1.5 c 0 0.136719 -0.113281 0.25 -0.25 0.25 h -1.5 c -0.136719 0 -0.25 -0.113281 -0.25 
-0.25 v -1.5 c 0 -0.136719 0.113281 -0.25 0.25 -0.25 z m 0 0" fill="#2e3436"/>
+</svg>
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 0d99aede..6727251a 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -58,6 +58,7 @@ public class Contacts.ContactSheet : Gtk.Grid {
     "email-addresses",
     "phone-numbers",
     "im-addresses",
+    "roles",
     "urls",
     "nickname",
     "birthday",
@@ -203,12 +204,46 @@ public class Contacts.ContactSheet : Gtk.Grid {
       case "postal-addresses":
         add_postal_addresses (persona, property);
         break;
+      case "roles":
+        add_roles (persona, property);
+        break;
       default:
         debug ("Unsupported property: %s", property);
         break;
     }
   }
 
+  private void add_roles (Persona persona, string property) {
+    unowned var details = persona as RoleDetails;
+    if (details == null)
+      return;
+
+    var roles = Utils.sort_fields<RoleFieldDetails>(details.roles);
+    var rows = new GLib.List<Gtk.ListBoxRow> ();
+    foreach (var role in roles) {
+      if (role.value.is_empty ())
+        continue;
+
+      var role_str = "";
+      // TRANSLATORS: "$ROLE at $ORGANISATION", e.g. "CEO at Linux Inc."
+      if (role.value.title != "") {
+        if (role.value.organisation_name != "")
+          role_str = _("%s at %s").printf (role.value.title, role.value.organisation_name);
+        else
+          role_str = role.value.title;
+      } else {
+          role_str = role.value.organisation_name;
+      }
+
+      var row = new ContactSheetRow (property, role_str);
+
+      //XXX if no role: set "Organisation" tool tip
+      rows.append (row);
+    }
+
+    this.attach_rows (rows);
+  }
+
   private void add_emails (Persona persona, string property) {
     unowned var details = persona as EmailDetails;
     if (details == null)
diff --git a/src/contacts-editor-persona.vala b/src/contacts-editor-persona.vala
index bdb506f8..365e25fa 100644
--- a/src/contacts-editor-persona.vala
+++ b/src/contacts-editor-persona.vala
@@ -31,6 +31,7 @@ public class Contacts.EditorPersona : Gtk.Box {
   };
   private const string[] OTHER_PROPERTIES = {
     "im-addresses",
+    "roles",
     "urls",
     "nickname",
     "birthday",
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
index 47aa9bf0..b73819fa 100644
--- a/src/contacts-editor-property.vala
+++ b/src/contacts-editor-property.vala
@@ -187,6 +187,46 @@ public class Contacts.AddressEditor : Gtk.Box {
   }
 }
 
+public class Contacts.RoleEditor : Gtk.Box {
+
+  private Gtk.Entry role_entry;
+  private Gtk.Entry organisation_entry;
+
+  public signal void changed ();
+
+  construct {
+    this.add_css_class ("contacts-editor-role");
+    this.hexpand = true;
+    this.orientation = Gtk.Orientation.VERTICAL;
+
+    this.role_entry = new Gtk.Entry ();
+    this.role_entry.hexpand = true;
+    this.role_entry.placeholder_text = _("Role");
+    this.role_entry.add_css_class ("flat");
+    this.role_entry.changed.connect ((_) => { changed(); });
+    append (this.role_entry);
+
+    this.organisation_entry = new Gtk.Entry ();
+    this.organisation_entry.hexpand = true;
+    this.organisation_entry.placeholder_text = _("Organisation");
+    this.organisation_entry.add_css_class ("flat");
+    this.organisation_entry.changed.connect ((_) => { changed(); });
+    append (this.organisation_entry);
+  }
+
+  public RoleEditor (RoleFieldDetails details) {
+    details.value.bind_property ("title", this.role_entry, "text",
+                                 BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
+    details.value.bind_property ("organisation-name", this.organisation_entry, "text",
+                                 BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
+  }
+
+  public bool is_empty () {
+    return this.role_entry.get_text () != "" &&
+           this.organisation_entry.get_text () != "";
+  }
+}
+
 /**
  * Basic widget to show a single property of a contact (for example an email
  * address, a birthday, ...). It can show itself using a GtkRevealer animation.
@@ -456,6 +496,18 @@ public class Contacts.EditorProperty : Object, ListModel {
             this.rows.add (create_for_address (address_details.postal_addresses));
         }
         break;
+      case "roles":
+        unowned var role_details = p as RoleDetails;
+        if (role_details != null) {
+          if (!only_new) {
+            foreach (var role in role_details.roles) {
+              this.rows.add (create_for_role (role_details.roles, role));
+            }
+          }
+          if (this.writeable)
+            this.rows.add (create_for_role (role_details.roles));
+        }
+        break;
     }
   }
 
@@ -667,4 +719,28 @@ public class Contacts.EditorProperty : Object, ListModel {
     box.sensitive = this.writeable;
     return box;
   }
+
+  private EditorPropertyRow create_for_role (Gee.Set<RoleFieldDetails> details_set,
+                                             RoleFieldDetails? details = null) {
+    if (details == null) {
+      var new_details = new RoleFieldDetails (new Role ());
+      details_set.add (new_details);
+      details = new_details;
+    }
+    var box = new EditorPropertyRow ("roles");
+
+    var role_editor = new RoleEditor (details);
+    box.set_main_widget (role_editor);
+    box.is_empty = role_editor.is_empty ();
+
+    role_editor.changed.connect (() => {
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Role changed");
+      box.is_empty = role_editor.is_empty ();
+    });
+
+    box.sensitive = this.writeable;
+    return box;
+  }
 }
diff --git a/src/contacts-fake-persona-store.vala b/src/contacts-fake-persona-store.vala
index 06437b4b..283ad598 100644
--- a/src/contacts-fake-persona-store.vala
+++ b/src/contacts-fake-persona-store.vala
@@ -77,6 +77,7 @@ public class Contacts.FakePersona : Persona,
                                     NameDetails,
                                     NoteDetails,
                                     PhoneDetails,
+                                    RoleDetails,
                                     UrlDetails,
                                     PostalAddressDetails {
 
@@ -220,6 +221,24 @@ public class Contacts.FakePersona : Persona,
     }
   }
 
+  public Gee.Set<RoleFieldDetails> roles {
+    get {
+      unowned Value? value = this.properties.get ("roles");
+      if (value == null) {
+        var new_value = Value (typeof (Gee.Set));
+        var set = new FakeHashSet<RoleFieldDetails> ();
+        new_value.set_object (set);
+        set.changed.connect (() => { notify_property ("roles"); });
+        this.properties.set ("roles", new_value);
+        value = new_value;
+      }
+      return (Gee.Set<RoleFieldDetails>) value;
+    }
+    set {
+      this.properties.set ("roles", value);
+    }
+  }
+
   public DateTime? birthday {
     get { unowned Value? value = this.properties.get ("birthday");
       if (value == null)
@@ -363,7 +382,9 @@ public class Contacts.FakePersona : Persona,
         }
         break;
       case "roles":
-        //roles ((RoleDetails) persona).roles;
+        foreach (var role in ((RoleDetails) persona).roles) {
+          this.roles.add (new RoleFieldDetails (role.value, role.parameters));
+        }
         break;
       case "urls":
         foreach (var e in ((UrlDetails) persona).urls) {
@@ -481,7 +502,13 @@ public class Contacts.FakePersona : Persona,
         yield ((PostalAddressDetails) persona).change_postal_addresses (copy);
         break;
       case "roles":
-        yield ((RoleDetails) persona).change_roles ((Gee.Set<RoleFieldDetails>) new_value);
+        var original = (Gee.Set<RoleFieldDetails>) new_value;
+        var copy = new Gee.HashSet<RoleFieldDetails> ();
+        foreach (var e in original) {
+          if (e.value != null && !e.value.is_empty ())
+            copy.add (new RoleFieldDetails (e.value, e.parameters));
+        }
+        yield ((RoleDetails) persona).change_roles (copy);
         break;
       case "urls":
         var original = (Gee.Set<UrlFieldDetails>) new_value;
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index fd8196d2..658efa6f 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -563,7 +563,7 @@ namespace Contacts.Utils {
     { "notes", N_("Note"), "note-symbolic" },
     { "phone-numbers", N_("Phone number"), "phone-symbolic" },
     { "postal-addresses", N_("Address"), "mark-location-symbolic" },
-    { "roles", N_("Role"), null },
+    { "roles", N_("Role"), "building-symbolic" },
     { "structured-name", N_("Structured name"), "avatar-default-symbolic" },
     { "urls", N_("Website"), "web-browser-symbolic" },
     { "web-service-addresses", N_("Web service"), null },


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