[gnome-contacts/nielsdg/gtk4: 9/10] wip




commit 9c6efd3f03cff8f4fde2a41df7b669764f7d12a2
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Wed Dec 8 12:53:32 2021 +0100

    wip

 data/ui/contacts-contact-pane.ui         |  30 +--
 data/ui/contacts-main-window.ui          |   2 +-
 data/ui/style.css                        |  23 +-
 src/contacts-accounts-list.vala          |   4 +-
 src/contacts-addressbook-list.vala       |   4 +-
 src/contacts-avatar.vala                 |   2 -
 src/contacts-contact-editor.vala         |  10 +-
 src/contacts-contact-pane.vala           |  24 +-
 src/contacts-contact-sheet.vala          | 251 +++++++++++---------
 src/contacts-crop-dialog.vala            |   3 +-
 src/contacts-editor-persona.vala         |  18 +-
 src/contacts-editor-property.vala        | 393 ++++++++++++++++---------------
 src/contacts-linked-personas-dialog.vala |   9 +-
 src/contacts-main-window.vala            |   9 +-
 src/contacts-type-combo.vala             |  70 ++----
 src/contacts-typeset.vala                | 146 +++++-------
 src/contacts-utils.vala                  |  71 ++++--
 17 files changed, 546 insertions(+), 523 deletions(-)
---
diff --git a/data/ui/contacts-contact-pane.ui b/data/ui/contacts-contact-pane.ui
index 710f5143..cab42e67 100644
--- a/data/ui/contacts-contact-pane.ui
+++ b/data/ui/contacts-contact-pane.ui
@@ -10,7 +10,6 @@
             <property name="name">none-selected-page</property>
             <property name="child">
               <object class="AdwStatusPage">
-                <property name="visible">True</property>
                 <property name="hexpand">True</property>
                 <property name="vexpand">True</property>
                 <property name="icon_name">avatar-default-symbolic</property>
@@ -24,23 +23,16 @@
             <property name="name">contact-sheet-page</property>
             <property name="child">
               <object class="GtkScrolledWindow" id="contact_sheet_view">
-                <property name="visible">True</property>
                 <property name="hexpand">True</property>
                 <property name="vexpand">True</property>
                 <property name="hscrollbar_policy">never</property>
                 <property name="vscrollbar_policy">automatic</property>
                 <child>
-                  <object class="AdwClamp">
-                    <property name="visible">True</property>
-                    <property name="margin-top">32</property>
-                    <property name="margin-bottom">32</property>
-                    <property name="margin-start">24</property>
-                    <property name="margin-end">24</property>
-                    <child>
-                      <object class="GtkBox" id="contact_sheet_box">
-                        <property name="visible">True</property>
-                      </object>
-                    </child>
+                  <object class="AdwClamp" id="contact_sheet_clamp">
+                    <property name="maximum-size">500</property>
+                    <style>
+                      <class name="contacts-contact-sheet-container"/>
+                    </style>
                   </object>
                 </child>
               </object>
@@ -58,15 +50,13 @@
                 <property name="hscrollbar_policy">never</property>
                 <property name="vscrollbar_policy">automatic</property>
                 <child>
-                  <object class="AdwClamp">
-                    <property name="visible">True</property>
-                    <property name="margin-top">32</property>
-                    <property name="margin-bottom">32</property>
-                    <property name="margin-start">24</property>
-                    <property name="margin-end">24</property>
+                  <object class="AdwClamp" id="contact_editor_clamp">
+                    <style>
+                      <class name="contacts-contact-editor-container"/>
+                    </style>
+                    <property name="maximum-size" bind-source="contact_sheet_clamp" 
bind-property="maximum-size" bind-flags="sync-create"/>
                     <child>
                       <object class="GtkBox" id="contact_editor_box">
-                        <property name="visible">True</property>
                       </object>
                     </child>
                   </object>
diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui
index 4d2981cb..7ef0e5f0 100644
--- a/data/ui/contacts-main-window.ui
+++ b/data/ui/contacts-main-window.ui
@@ -231,7 +231,7 @@
           <object class="GtkOverlay" id="notification_overlay">
             <child>
               <object class="AdwLeaflet" id="content_box">
-                <property name="can-swipe-back">True</property>
+                <property name="can-navigate-back">True</property>
                 <signal name="notify::folded" handler="on_folded" object="ContactsMainWindow" after="yes" 
swapped="no"/>
                 <signal name="notify::child-transition-running" handler="on_child_transition_running" 
object="ContactsMainWindow" after="yes" swapped="no"/>
                 <child>
diff --git a/data/ui/style.css b/data/ui/style.css
index 33ebf132..1027be8e 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -22,10 +22,13 @@
     margin: 12px;
   }
 
-/* Give the avatar in the ContactSheet some margin,
- * so it doesn't jump when switching to the editor. */
-.contacts-contact-sheet .contacts-avatar {
-  margin: 4px 8px;
+.contacts-contact-sheet-container,
+.contacts-contact-editor-container {
+  margin: 32px 48px;
+}
+
+.contacts-contact-sheet avatar {
+  margin: 4px 8px 4px 8px;
 }
 
 /* The style for the background "watermark" image and text.
@@ -61,14 +64,22 @@ flowboxchild.circular {
 /* Contact Editor-related CSS classes */
 
 /* Common class all widgets editing a property  */
-.contacts-editor-property-row {
-  padding: 12px;
+.contacts-editor-property {
 }
 
+  .contacts-editor-property .contacts-property-icon {
+    margin: 9px;
+  }
+
+  .contacts-editor-property .contacts-editor-main-entry {
+    padding: 12px 6px;
+  }
+
 /* Class for editing postal address */
 .contacts-editor-address entry {
   border-radius: 0;
   border-width: 1px 1px 0 1px;
+  padding: 6px 6px;
 }
 
   .contacts-editor-address entry:first-child {
diff --git a/src/contacts-accounts-list.vala b/src/contacts-accounts-list.vala
index 265c04cd..c2c49816 100644
--- a/src/contacts-accounts-list.vala
+++ b/src/contacts-accounts-list.vala
@@ -64,9 +64,9 @@ public class Contacts.AccountsList : Adw.Bin {
 
   public void update_contents (bool select_active) {
     // Remove all entries
-    var child = this.listbox.get_first_child ();
+    unowned var child = this.listbox.get_first_child ();
     while (child != null) {
-        var next = child.get_next_sibling ();
+        unowned var next = child.get_next_sibling ();
         this.listbox.remove (child);
         child = next;
     }
diff --git a/src/contacts-addressbook-list.vala b/src/contacts-addressbook-list.vala
index 8b8a1c66..87a775ae 100644
--- a/src/contacts-addressbook-list.vala
+++ b/src/contacts-addressbook-list.vala
@@ -73,9 +73,9 @@ public class Contacts.AddressbookList : Adw.Bin {
 
   public void update () {
     // Remove all entries
-    var child = this.listbox.get_first_child ();
+    unowned var child = this.listbox.get_first_child ();
     while (child != null) {
-        var next = child.get_next_sibling ();
+        unowned var next = child.get_next_sibling ();
         this.listbox.remove (child);
         child = next;
     }
diff --git a/src/contacts-avatar.vala b/src/contacts-avatar.vala
index 7ba6a7ac..70cd43e0 100644
--- a/src/contacts-avatar.vala
+++ b/src/contacts-avatar.vala
@@ -42,8 +42,6 @@ public class Contacts.Avatar : Adw.Bin {
     }
 
     this.child = new Adw.Avatar (size, name, show_initials);
-
-    show ();
   }
 
   /**
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
index 7e90841d..ca8c8968 100644
--- a/src/contacts-contact-editor.vala
+++ b/src/contacts-contact-editor.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
  * Copyright (C) 2019 Purism SPC
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,13 +23,20 @@ using Folks;
  * A widget that allows the user to edit a given {@link Contact}.
  */
 public class Contacts.ContactEditor : Gtk.Box {
+
   private Individual individual;
   private Gtk.Entry name_entry;
   private AvatarSelector avatar_selector = null;
   private Avatar avatar;
 
+  construct {
+    this.orientation = Gtk.Orientation.VERTICAL;
+    this.spacing = 12;
+
+    this.add_css_class ("contacts-contact-editor");
+  }
+
   public ContactEditor (Individual individual, IndividualAggregator aggregator) {
-    Object (orientation: Gtk.Orientation.VERTICAL, spacing: 24);
     this.individual = individual;
 
     Gtk.Box header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index 21a0a27b..a031c2e7 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -42,15 +43,15 @@ public class Contacts.ContactPane : Adw.Bin {
   private unowned Gtk.ScrolledWindow contact_sheet_view;
 
   [GtkChild]
-  private unowned Gtk.Box contact_sheet_box;
-  private ContactSheet? sheet = null;
+  private unowned Adw.Clamp contact_sheet_clamp;
+  private unowned ContactSheet? sheet = null;
 
   [GtkChild]
   private unowned Gtk.ScrolledWindow contact_editor_view;
 
   [GtkChild]
   private unowned Gtk.Box contact_editor_box;
-  private ContactEditor? editor = null;
+  private unowned ContactEditor? editor = null;
 
   public bool on_edit_mode = false;
   private LinkSuggestionGrid? suggestion_grid = null;
@@ -69,7 +70,7 @@ public class Contacts.ContactPane : Adw.Bin {
   }
 
   public void add_suggestion (Individual i) {
-    var parent_overlay = this.get_parent () as Gtk.Overlay;
+    unowned var parent_overlay = this.get_parent () as Gtk.Overlay;
 
     remove_suggestion_grid ();
     this.suggestion_grid = new LinkSuggestionGrid (i);
@@ -112,8 +113,10 @@ public class Contacts.ContactPane : Adw.Bin {
     assert (this.individual != null);
 
     remove_contact_sheet();
-    this.sheet = new ContactSheet (this.individual, this.store);
-    this.contact_sheet_box.append (this.sheet);
+    var contacts_sheet = new ContactSheet (this.individual, this.store);
+    contacts_sheet.hexpand = true;
+    this.sheet = contacts_sheet;
+    this.contact_sheet_clamp.set_child (this.sheet);
     this.stack.set_visible_child_name ("contact-sheet-page");
 
     var matches = this.store.aggregator.get_potential_matches (this.individual, MatchResult.HIGH);
@@ -132,15 +135,15 @@ public class Contacts.ContactPane : Adw.Bin {
     // Remove the suggestion grid that goes along with it.
     remove_suggestion_grid ();
 
-    this.contact_sheet_box.remove (this.sheet);
-    this.sheet.destroy();
+    this.contact_sheet_clamp.set_child (null);
     this.sheet = null;
   }
 
   private void create_contact_editor () {
     remove_contact_editor ();
 
-    this.editor = new ContactEditor (this.individual, store.aggregator);
+    var contact_editor = new ContactEditor (this.individual, store.aggregator);
+    this.editor = contact_editor;
 
     this.contact_editor_box.append (this.editor);
   }
@@ -275,7 +278,8 @@ public class Contacts.ContactPane : Adw.Bin {
     if (this.suggestion_grid == null)
       return;
 
-    this.suggestion_grid.destroy ();
+    unowned var parent_overlay = this.get_parent () as Gtk.Overlay;
+    parent_overlay.remove_overlay (suggestion_grid);
     this.suggestion_grid = null;
   }
 }
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 5d71b331..0fd94c3e 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -17,16 +17,43 @@
 
 using Folks;
 
+// XXX accesibility? tooltips?
+public class Contacts.ContactSheetRow : Adw.ActionRow {
+
+  public ContactSheetRow (string property_name, string title, string? subtitle = null) {
+    unowned var icon_name = Utils.get_icon_name_for_property (property_name);
+    if (icon_name != null) {
+      var icon = new Gtk.Image.from_icon_name (icon_name);
+      icon.add_css_class ("contacts-property-icon");
+      icon.tooltip_text = Utils.get_display_name_for_property (property_name);
+      this.add_prefix (icon);
+    }
+
+    this.title = Markup.escape_text (title);
+
+    if (subtitle != null)
+      this.subtitle = subtitle;
+  }
+
+  public Gtk.Button add_button (string icon) {
+    var button = new Gtk.Button.from_icon_name (icon);
+    button.valign = Gtk.Align.CENTER;
+    button.add_css_class ("flat");
+    this.add_suffix (button);
+    return button;
+  }
+}
+
 /**
  * The contact sheet displays the actual information of a contact.
  *
  * (Note: to edit a contact, use the {@link ContactEditor} instead.
  */
 public class Contacts.ContactSheet : Gtk.Grid {
+
   private int last_row = 0;
-  private Individual individual;
+  private unowned Individual individual;
   private unowned Store store;
-  public bool narrow { get; set; default = true; }
 
   private const string[] SORTED_PROPERTIES = {
     "email-addresses",
@@ -39,8 +66,14 @@ public class Contacts.ContactSheet : Gtk.Grid {
     "notes"
   };
 
+  construct {
+    this.row_spacing = 18;
+    this.column_spacing = 12;
+
+    this.add_css_class ("contacts-contact-sheet");
+  }
+
   public ContactSheet (Individual individual, Store store) {
-    Object (row_spacing: 12, column_spacing: 12);
     this.individual = individual;
     this.store = store;
 
@@ -56,37 +89,12 @@ public class Contacts.ContactSheet : Gtk.Grid {
     var attrList = new Pango.AttrList ();
     attrList.insert (Pango.attr_weight_new (Pango.Weight.BOLD));
     store_name.set_attributes (attrList);
-    store_name.set_halign (Gtk.Align.START);
-    store_name.set_ellipsize (Pango.EllipsizeMode.MIDDLE);
+    store_name.halign = Gtk.Align.START;
+    store_name.ellipsize = Pango.EllipsizeMode.MIDDLE;
 
     return store_name;
   }
 
-  private Gtk.Button create_button (string icon) {
-    var button = new Gtk.Button.from_icon_name (icon);
-    button.valign = Gtk.Align.CENTER;
-    button.add_css_class ("flat");
-
-    return button;
-  }
-
-  private void add_row_with_label (string label_value,
-                                   string value,
-                                   Gtk.Widget? btn1 = null,
-                                   Gtk.Widget? btn2 =null) {
-    if (value == "" || value == null)
-      return;
-
-    var action_row = new Adw.ActionRow ();
-    action_row.title = value;
-    action_row.subtitle = label_value;
-    if (btn1 != null)
-      action_row.add_suffix (btn1);
-    if (btn2 != null)
-      action_row.add_suffix (btn2);
-    attach_row (action_row);
-  }
-
   // Helper function that attaches a row to our grid
   private void attach_row (Gtk.ListBoxRow row) {
     var list_box = new Gtk.ListBox ();
@@ -102,39 +110,35 @@ public class Contacts.ContactSheet : Gtk.Grid {
     this.last_row = 0;
 
     // Remove all fields
-    var child = get_first_child ();
+    unowned var child = get_first_child ();
     while (child != null) {
-        var next = child.get_next_sibling ();
+        unowned var next = child.get_next_sibling ();
         remove (child);
         child = next;
     }
 
-    var image_frame = new Avatar (PROFILE_SIZE, this.individual);
-    image_frame.set_vexpand (false);
-    image_frame.set_valign (Gtk.Align.START);
-
-    this.attach (image_frame,  0, 0, 1, 3);
-
-    create_name_label ();
+    var header = create_header ();
+    this.attach (header, 0, 0, 1, 1);
 
-    this.last_row += 3; // Name/Avatar takes up 3 rows
+    this.last_row++;
 
     var personas = Utils.get_personas_for_display (this.individual);
     /* Cause personas are sorted properly I can do this */
-    foreach (var p in personas) {
-      bool is_first_persona = (this.last_row == 3);
+    for (int i = 0; i < personas.get_n_items (); i++) {
+      var p = (Persona) personas.get_item (i);
       int persona_store_pos = this.last_row;
-      if (!is_first_persona) {
+
+      if (i > 0) {
         this.attach (create_persona_store_label (p), 0, this.last_row, 3);
         this.last_row++;
       }
 
-      foreach (var prop in SORTED_PROPERTIES)
+      foreach (unowned var prop in SORTED_PROPERTIES)
         add_row_for_property (p, prop);
 
       // Nothing to show in the persona: don't mention it
       bool is_empty_persona = (this.last_row == persona_store_pos + 1);
-      if (!is_first_persona && is_empty_persona) {
+      if (i > 0 && is_empty_persona) {
         this.remove_row (persona_store_pos);
         this.last_row--;
       }
@@ -147,45 +151,55 @@ public class Contacts.ContactSheet : Gtk.Grid {
     name_label.set_markup (name);
   }
 
-  private void create_name_label () {
+  private Gtk.Widget create_header () {
+    var header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
+
+    var image_frame = new Avatar (PROFILE_SIZE, this.individual);
+    image_frame.vexpand = false;
+    image_frame.valign = Gtk.Align.START;
+    header.append (image_frame);
+
     var name_label = new Gtk.Label ("");
+    name_label.hexpand = true;
     name_label.ellipsize = Pango.EllipsizeMode.END;
     name_label.xalign = 0f;
     name_label.lines = 4;
     name_label.selectable = true;
-    name_label.set_can_focus (false);
-    this.attach (name_label,  1, 0, 1, 3);
+    name_label.can_focus = false;
+    header.append (name_label);
     update_name_label (name_label);
     this.individual.notify["display-name"].connect ((obj, spec) => {
       update_name_label (name_label);
     });
+
+    return header;
   }
 
   private void add_row_for_property (Persona persona, string property) {
     switch (property) {
       case "email-addresses":
-        add_emails (persona);
+        add_emails (persona, property);
         break;
       case "phone-numbers":
-        add_phone_nrs (persona);
+        add_phone_nrs (persona, property);
         break;
       case "im-addresses":
-        add_im_addresses (persona);
+        add_im_addresses (persona, property);
         break;
       case "urls":
-        add_urls (persona);
+        add_urls (persona, property);
         break;
       case "nickname":
-        add_nickname (persona);
+        add_nickname (persona, property);
         break;
       case "birthday":
-        add_birthday (persona);
+        add_birthday (persona, property);
         break;
       case "notes":
-        add_notes (persona);
+        add_notes (persona, property);
         break;
       case "postal-addresses":
-        add_postal_addresses (persona);
+        add_postal_addresses (persona, property);
         break;
       default:
         debug ("Unsupported property: %s", property);
@@ -193,83 +207,90 @@ public class Contacts.ContactSheet : Gtk.Grid {
     }
   }
 
-  private void add_emails (Persona persona) {
+  private void add_emails (Persona persona, string property) {
     unowned var details = persona as EmailDetails;
     if (details == null)
       return;
 
     var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
     foreach (var email in emails) {
-      var action_row = new Adw.ActionRow ();
+      if (email.value == "")
+        continue;
 
-      action_row.add_prefix (new Gtk.Image.from_icon_name ("mail-unread-symbolic"));
-      action_row.title = Markup.escape_text (email.value);
-      action_row.subtitle = TypeSet.email.format_type (email);
+      var row = new ContactSheetRow (property,
+                                     email.value,
+                                     TypeSet.email.format_type (email));
 
-      var button = create_button ("mail-send-symbolic");
+      var button = row.add_button ("mail-send-symbolic");
+      button.tooltip_text = _("Send an email to %s".printf (email.value));
       button.clicked.connect (() => {
         Utils.compose_mail ("%s <%s>".printf(this.individual.display_name, email.value));
       });
-      action_row.add_suffix (button);
 
-      this.attach_row (action_row);
+      this.attach_row (row);
     }
   }
 
-  private void add_phone_nrs (Persona persona) {
+  private void add_phone_nrs (Persona persona, string property) {
     unowned var phone_details = persona as PhoneDetails;
     if (phone_details == null)
       return;
 
     var phones = Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
     foreach (var phone in phones) {
-      var action_row = new Adw.ActionRow ();
+      if (phone.value == "")
+        continue;
 
-      action_row.add_prefix (new Gtk.Image.from_icon_name ("call-start-symbolic"));
-      action_row.title = Markup.escape_text (phone.value);
-      action_row.subtitle = TypeSet.phone.format_type (phone);
+      var row = new ContactSheetRow (property,
+                                     phone.value,
+                                     TypeSet.phone.format_type (phone));
 
 #if HAVE_TELEPATHY
       if (this.store.caller_account != null) {
-        var button = create_button ("call-start-symbolic");
+        var button = row.add_button ("call-start-symbolic");
+        button.tooltip_text = _("Start a call");
         button.clicked.connect (() => {
           Utils.start_call (phone.value, this.store.caller_account);
         });
-        action_row.add_suffix (button);
       }
 #endif
 
-      this.attach_row (action_row);
+      this.attach_row (row);
     }
   }
 
-  private void add_im_addresses (Persona persona) {
+  private void add_im_addresses (Persona persona, string property) {
 #if HAVE_TELEPATHY
-    var im_details = persona as ImDetails;
-    if (im_details != null) {
-      foreach (var protocol in im_details.im_addresses.get_keys ()) {
-        foreach (var id in im_details.im_addresses[protocol]) {
-          if (persona is Tpf.Persona) {
-            var button = create_button ("user-available-symbolic");
-            button.clicked.connect (() => {
-              var im_persona = Utils.find_im_persona (individual, protocol, id.value);
-              if (im_persona != null) {
-                var type = im_persona.presence_type;
-                if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
-                    type != PresenceType.OFFLINE && type != PresenceType.UNKNOWN) {
-                  Utils.start_chat (this.individual, protocol, id.value);
-                }
-              }
-            });
-            add_row_with_label (ImService.get_display_name (protocol), id.value, button);
+    unowned var im_details = persona as ImDetails;
+    if (im_details == null)
+      return;
+
+    foreach (var protocol in im_details.im_addresses.get_keys ()) {
+      foreach (var id in im_details.im_addresses[protocol]) {
+        if (!(persona is Tpf.Persona))
+          continue;
+
+        var row = new ContactSheetRow (property,
+                                       id.value,
+                                       ImService.get_display_name (protocol));
+        var button = row.add_button ("user-available-symbolic");
+        button.clicked.connect (() => {
+          var im_persona = Utils.find_im_persona (individual, protocol, id.value);
+          if (im_persona != null) {
+            var type = im_persona.presence_type;
+            if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
+                type != PresenceType.OFFLINE && type != PresenceType.UNKNOWN) {
+              Utils.start_chat (this.individual, protocol, id.value);
+            }
           }
-        }
+        });
+        this.attach_row (row);
       }
     }
 #endif
   }
 
-  private void add_urls (Persona persona) {
+  private void add_urls (Persona persona, string property) {
     unowned var url_details = persona as UrlDetails;
     if (url_details == null)
       return;
@@ -278,12 +299,10 @@ public class Contacts.ContactSheet : Gtk.Grid {
       if (url.value == "")
         continue;
 
-      var action_row = new Adw.ActionRow ();
-
-      action_row.add_prefix (new Gtk.Image.from_icon_name ("web-browser-symbolic"));
-      action_row.title = Markup.escape_text (url.value);
+      var row = new ContactSheetRow (property, url.value);
 
-      var button = create_button ("web-browser-symbolic");
+      var button = row.add_button ("web-browser-symbolic");
+      button.tooltip_text = _("Visit website");
       button.clicked.connect (() => {
         unowned var window = button.get_root () as MainWindow;
         if (window == null)
@@ -306,7 +325,7 @@ public class Contacts.ContactSheet : Gtk.Grid {
         }
       });
 
-      this.attach_row (action_row);
+      this.attach_row (row);
     }
   }
 
@@ -319,25 +338,26 @@ public class Contacts.ContactSheet : Gtk.Grid {
     return url;
   }
 
-  private void add_nickname (Persona persona) {
+  private void add_nickname (Persona persona, string property) {
     unowned var name_details = persona as NameDetails;
-    if (name_details == null || name_details.nickname != "")
+    if (name_details == null || name_details.nickname == "")
       return;
 
-    add_row_with_label (_("Nickname"), name_details.nickname);
+    var row = new ContactSheetRow (property, name_details.nickname);
+    this.attach_row (row);
   }
 
-  private void add_birthday (Persona persona) {
+  private void add_birthday (Persona persona, string property) {
     unowned var birthday_details = persona as BirthdayDetails;
     if (birthday_details == null || birthday_details.birthday == null)
       return;
 
-    var action_row = new Adw.ActionRow ();
-    action_row.title = Markup.escape_text (birthday_details.birthday.to_local ().format ("%x"));
-    this.attach_row (action_row);
+    var birthday_str = birthday_details.birthday.to_local ().format ("%x");
+    var row = new ContactSheetRow (property, birthday_str);
+    this.attach_row (row);
   }
 
-  private void add_notes (Persona persona) {
+  private void add_notes (Persona persona, string property) {
     unowned var note_details = persona as NoteDetails;
     if (note_details == null)
       return;
@@ -346,25 +366,24 @@ public class Contacts.ContactSheet : Gtk.Grid {
       if (note.value == "")
         continue;
 
-      var action_row = new Adw.ActionRow ();
-      action_row.title = Markup.escape_text (note.value);
-      this.attach_row (action_row);
+      var row = new ContactSheetRow (property, note.value);
+      this.attach_row (row);
     }
   }
 
-  private void add_postal_addresses (Persona persona) {
+  private void add_postal_addresses (Persona persona, string property) {
     unowned var addr_details = persona as PostalAddressDetails;
     if (addr_details == null)
       return;
 
     foreach (var addr in addr_details.postal_addresses) {
-      var action_row = new Adw.ActionRow ();
-
-      action_row.add_prefix (new Gtk.Image.from_icon_name ("mark-location-symbolic"));
-      action_row.title = Markup.escape_text (string.joinv ("\n", Utils.format_address (addr.value)));
-      action_row.subtitle = TypeSet.general.format_type (addr);
+      if (addr.value.is_empty ())
+        continue;
 
-      this.attach_row (action_row);
+      var row = new ContactSheetRow (property,
+                                     string.joinv ("\n", Utils.format_address (addr.value)),
+                                     TypeSet.general.format_type (addr));
+      this.attach_row (row);
     }
   }
 }
diff --git a/src/contacts-crop-dialog.vala b/src/contacts-crop-dialog.vala
index c3b20e7d..fba59f1f 100644
--- a/src/contacts-crop-dialog.vala
+++ b/src/contacts-crop-dialog.vala
@@ -15,6 +15,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+// XXX document and make gtkdialog
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-crop-dialog.ui")]
 public class Contacts.CropDialog : Gtk.Window {
 
@@ -111,7 +112,7 @@ public class Contacts.CropDialog : Gtk.Window {
     this.cheese = null;
 #endif
 
-    return Gdk.EVENT_STOP; // XXX what the hell am i supposed to retunr here?
+    return Gdk.EVENT_PROPAGATE;
   }
 
 }
diff --git a/src/contacts-editor-persona.vala b/src/contacts-editor-persona.vala
index 88a9889a..03079af0 100644
--- a/src/contacts-editor-persona.vala
+++ b/src/contacts-editor-persona.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2019 Purism SPC
  * Author: Julian Sparber <julian sparber puri sm>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -37,20 +38,21 @@ public class Contacts.EditorPersona : Gtk.Box {
     "notes"
   };
 
-  private Folks.Persona persona;
-  private Gtk.Box header;
+  private unowned Folks.Persona persona;
+  private unowned Gtk.Box header;
   private unowned Gtk.Box content;
 
-  private Folks.IndividualAggregator aggregator;
+  private unowned Folks.IndividualAggregator aggregator;
 
   construct {
-    this.header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
-    append (this.header);
+    var _header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+    this.append (_header);
+    this.header = _header;
 
     var listbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 12);
     this.content = listbox;
     this.content.add_css_class ("content");
-    append (this.content);
+    this.append (this.content);
   }
 
   public EditorPersona (Persona persona, IndividualAggregator aggregator) {
@@ -77,8 +79,10 @@ public class Contacts.EditorPersona : Gtk.Box {
     show_more_content.icon_name = "view-more-symbolic";
     show_more_content.label = _("Show More");
     show_more_button.set_child (show_more_content);
+    show_more_button.halign = Gtk.Align.CENTER;
+    show_more_button.add_css_class ("flat");
     show_more_button.clicked.connect ((current_row) => {
-        foreach (var property in OTHER_PROPERTIES) {
+        foreach (unowned string property in OTHER_PROPERTIES) {
           debug ("Create property entry for %s", property);
           var rows = new EditorProperty (persona, property);
           foreach (var row in rows) {
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
index d992bbcb..f486358d 100644
--- a/src/contacts-editor-property.vala
+++ b/src/contacts-editor-property.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2019 Purism SPC
  * Author: Julian Sparber <julian sparber puri sm>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -31,8 +32,8 @@ public class Contacts.BirthdayEditor : Gtk.Dialog {
   construct {
     // The grid that will contain the Y/M/D fields
     var grid = new Gtk.Grid ();
-    grid.set_column_spacing (12);
-    grid.set_row_spacing (12);
+    grid.column_spacing = 12;
+    grid.row_spacing = 12;
     grid.add_css_class ("contacts-editor-birthday");
     ((Gtk.Box) this.get_content_area ()).append (grid);
 
@@ -94,9 +95,9 @@ public class Contacts.BirthdayEditor : Gtk.Dialog {
   public BirthdayEditor (Gtk.Window window, DateTime birthday) {
     Object (transient_for: window, use_header_bar: 1);
 
-    this.day_spin.set_value ((double) birthday.to_local ().get_day_of_month ());
-    this.month_combo.set_active (birthday.to_local ().get_month () - 1);
-    this.year_spin.set_value ((double)birthday.to_local ().get_year ());
+    this.day_spin.set_value ((double) birthday.get_day_of_month ());
+    this.month_combo.set_active (birthday.get_month () - 1);
+    this.year_spin.set_value ((double)birthday.get_year ());
 
     update_date ();
     month_combo.changed.connect (() => {
@@ -153,19 +154,19 @@ public class Contacts.AddressEditor : Gtk.Box {
       string postal_part;
       details.value.get (AddressEditor.postal_element_props[i], out postal_part);
 
-      entries[i] = new Gtk.Entry ();
-      entries[i].set_hexpand (true);
-      entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
+      this.entries[i] = new Gtk.Entry ();
+      this.entries[i].hexpand = true;
+      this.entries[i].placeholder_text = AddressEditor.postal_element_names[i];
+      this.entries[i].add_css_class ("flat");
 
       if (postal_part != null)
-        entries[i].set_text (postal_part);
+        this.entries[i].text = postal_part;
 
-      append (entries[i]);
+      append (this.entries[i]);
 
-      var entry = entries[i];
       var prop_name = AddressEditor.postal_element_props[i];
       entries[i].changed.connect (() => {
-        details.value.set (prop_name, entry.get_text ());
+        details.value.set (prop_name, this.entries[i].text);
         changed ();
       });
     }
@@ -181,49 +182,43 @@ public class Contacts.AddressEditor : Gtk.Box {
   }
 }
 
-public class Contacts.EditorPropertyRow : Gtk.ListBoxRow {
+/**
+ * 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.
+ *
+ * To edit the value of the property, you should supply a widget and set it as
+ * the main widget.
+ */
+public class Contacts.EditorPropertyRow : Adw.Bin {
+
+  private unowned Gtk.Revealer revealer;
+  private unowned Gtk.ListBox listbox;
+
   public bool is_empty { get; set; default = true; }
   public bool is_removed { get; set; default = false; }
-  public string ptype { get; private set; }
-  public Gtk.Box container;
-  public Gtk.Box header;
-  public Gtk.Revealer revealer;
+  public bool removable { get; set; default = false; }
+
+  /** Internal type name of the property */
+  public string ptype { get; construct; }
 
   construct {
-    this.add_css_class ("contacts-editor-property-row");
-
-    this.revealer = new Gtk.Revealer ();
-    //TODO: bind orientation property to available space
-    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
-    box.set_valign (Gtk.Align.START);
-    this.container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
-    this.header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
-    box.append (this.header);
-    box.append (this.container);
-    this.set_activatable (false);
-    this.set_selectable (false);
-
-    this.revealer.set_child (box);
-    this.set_child (this.revealer);
-    this.revealer.bind_property ("reveal-child", this, "is-removed", BindingFlags.INVERT_BOOLEAN);
+    var _revealer = new Gtk.Revealer ();
+    _revealer.bind_property ("reveal-child", this, "is-removed",
+                             BindingFlags.BIDIRECTIONAL | BindingFlags.INVERT_BOOLEAN);
+    this.child = _revealer;
+    this.revealer = _revealer;
+
+    var list_box = new Gtk.ListBox ();
+    this.listbox = list_box;
+    this.listbox.selection_mode = Gtk.SelectionMode.NONE;
+    this.listbox.activate_on_single_click = true;
+    this.listbox.add_css_class ("content");
+    this.listbox.add_css_class ("contacts-editor-property");
+    this.revealer.set_child (listbox);
   }
 
   public EditorPropertyRow (string type) {
-    this.ptype = type;
-  }
-
-  // This hides the widget with an animation and then destroys it
-  public new void remove () {
-    this.revealer.set_reveal_child (false);
-    // Remove the separator during the animation to make it look a little better
-    Timeout.add (this.revealer.get_transition_duration ()/2, () => {
-      this.set_header (null);
-      return false;
-    });
-
-    this.revealer.notify["child-revealed"].connect ( () => {
-      this.destroy ();
-    });
+    Object (ptype: type);
   }
 
   public void show_with_animation (bool animate = true) {
@@ -236,114 +231,104 @@ public class Contacts.EditorPropertyRow : Gtk.ListBoxRow {
     }
   }
 
-  public void add_base_label (string label) {
-    var title_label = new Gtk.Label (label);
-    title_label.set_hexpand (false);
-    title_label.set_halign (Gtk.Align.START);
-    title_label.margin_end = 6;
-    this.header.append (title_label);
-  }
+  // This hides the widget with an animation and then destroys it
+  public void remove () {
+    debug ("Property %s is removed", this.ptype);
 
-  public void add_base_combo (Gee.Set<AbstractFieldDetails> details_set,
-                              string label,
-                              TypeSet combo_type,
-                              AbstractFieldDetails details) {
-    var title_label = new Gtk.Label (label);
-    title_label.set_halign (Gtk.Align.START);
-    this.header.append (title_label);
-    TypeCombo combo = new TypeCombo (combo_type);
-    combo.set_hexpand (false);
-    combo.set_active_from_field_details (details);
-    this.header.append (combo);
-
-    combo.changed.connect (() => {
-      combo.active_descriptor.save_to_field_details(details);
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property phone changed");
+    this.revealer.set_reveal_child (false);
+
+    // Remove the separator during the animation to make it look a little better
+    Timeout.add (this.revealer.get_transition_duration ()/2, () => {
+      return false;
     });
-  }
 
-  //FIXME: create only one add_base_entry
-  public void add_base_entry_email (Gee.Set<AbstractFieldDetails> details_set,
-                                    EmailFieldDetails details,
-                                    string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_input_purpose (Gtk.InputPurpose.EMAIL);
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.append (value_entry);
-
-    this.is_empty = details.value == "";
-
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property email changed");
-      this.is_empty = value_entry.get_text () == "";
+    this.revealer.notify["child-revealed"].connect ( () => {
+      this.destroy ();
     });
   }
 
-  public void add_base_entry_phone (Gee.Set<AbstractFieldDetails> details_set,
-                                    PhoneFieldDetails details,
-                                    string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_input_purpose (Gtk.InputPurpose.PHONE);
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.append (value_entry);
+  /**
+   * Setter for the main widget, which can be used to actually edit the property
+   */
+  public void set_main_widget (Gtk.Widget widget) {
+    var row = new Gtk.ListBoxRow ();
+    row.focusable = false;
+
+    var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+    widget.hexpand = true;
+    row.set_child (box);
+
+    // Start with the icon (if known)
+    unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
+    if (icon_name != null) {
+      var icon = new Gtk.Image.from_icon_name (icon_name);
+      icon.add_css_class ("contacts-property-icon");
+      icon.tooltip_text = Utils.get_display_name_for_property (this.ptype);
+      box.prepend (icon);
+    }
 
-    this.is_empty = details.value == "";
+    // Set the actual widget
+    // (mimic Adw.ActionRow's "activatable-widget")
+    box.append (widget);
+    this.listbox.row_activated.connect ((activated_row) => {
+      if (row == activated_row)
+          widget.mnemonic_activate (false);
+    });
 
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property type changed");
+    // Add a delete buton if needed
+    if (this.removable) {
+      var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic");
+      delete_button.tooltip_text = _("Delete field");
+      this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
 
-      this.is_empty = value_entry.get_text () == "";
-    });
+      delete_button.clicked.connect ((b) => { this.remove (); });
+
+      box.append (delete_button);
+    }
+
+    this.listbox.append (row);
   }
 
-  public void add_base_entry_url (Gee.Set<AbstractFieldDetails> details_set,
-                                  UrlFieldDetails details,
-                                  string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_input_purpose (Gtk.InputPurpose.URL);
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.append (value_entry);
+  /**
+   * Wrapper around set_main_widget() with some extra styling for GtkEntries,
+   * as well as making sure the "is-empty" property is updated.
+   */
+  public Gtk.Entry set_main_entry (string text, string? placeholder = null) {
+    var entry = new Gtk.Entry ();
+    entry.text = text;
+    entry.placeholder_text = placeholder;
+    entry.add_css_class ("flat");
+    entry.add_css_class ("contacts-editor-main-entry");
+    this.set_main_widget (entry);
+
+    this.is_empty = (text == "");
+    entry.changed.connect (() => {
+      this.is_empty = (entry.text == "");
+    });
 
-    this.is_empty = details.value == "";
+    return entry;
+  }
 
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
+  // Adds an extra row for a type combo, to choose between e.g. "Home" or "Work"
+  public void add_type_combo (Gee.Set<AbstractFieldDetails> details_set,
+                              TypeSet combo_type,
+                              AbstractFieldDetails details) {
+    var row = new TypeComboRow (combo_type);
+    row.title = _("Label");
+    row.set_selected_from_field_details (details);
+    this.listbox.append (row);
+
+    row.notify["selected-item"].connect ((obj, pspec) => {
+      unowned var descr = row.selected_descriptor;
+      descr.save_to_field_details (details);
       // Workaround: we shouldn't do a manual signal
       ((FakeHashSet) details_set).changed ();
-      debug ("Property type changed");
-
-      this.is_empty = value_entry.get_text () == "";
+      debug ("Property phone changed");
     });
   }
 
   public void add_base_delete (Gee.Set<AbstractFieldDetails> details_set,
                                AbstractFieldDetails details) {
-    var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic");
-    delete_button.tooltip_text = _("Delete field");
-    delete_button.set_valign (Gtk.Align.START);
-    this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
-    this.container.append (delete_button);
-
-
-    delete_button.clicked.connect (() => {
-      debug ("Property removed");
-      this.remove ();
-      details_set.remove (details);
-    });
   }
 }
 
@@ -355,7 +340,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
   public bool writeable { get; private set; default = false; }
 
   public EditorProperty (Persona persona, string property_name, bool only_new = false) {
-    foreach (var s in persona.writeable_properties) {
+    foreach (unowned string s in persona.writeable_properties) {
       if (s == property_name) {
         this.writeable = true;
         break;
@@ -368,7 +353,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
   private void create_for_property (Persona p, string prop_name, bool only_new) {
     switch (prop_name) {
       case "email-addresses":
-        var details = p as EmailDetails;
+        unowned var details = p as EmailDetails;
         if (details != null) {
           var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
           if (!only_new)
@@ -380,7 +365,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
         }
         break;
       case "phone-numbers":
-        var details = p as PhoneDetails;
+        unowned var details = p as PhoneDetails;
         if (details != null) {
           var phones = Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
           if (!only_new)
@@ -392,7 +377,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
         }
         break;
       case "urls":
-        var details = p as UrlDetails;
+        unowned var details = p as UrlDetails;
         if (details != null) {
           var urls = Utils.sort_fields<UrlFieldDetails>(details.urls);
           if (!only_new)
@@ -403,19 +388,19 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
         }
         break;
       case "nickname":
-        var name_details = p as NameDetails;
+        unowned var name_details = p as NameDetails;
         if (name_details != null && name_details.nickname != null && !only_new) {
           add (create_for_nick (name_details));
         }
         break;
       case "birthday":
-        var birthday_details = p as BirthdayDetails;
+        unowned var birthday_details = p as BirthdayDetails;
         if (birthday_details != null && !only_new) {
           add (create_for_birthday (birthday_details));
         }
         break;
       case "notes":
-        var note_details = p as NoteDetails;
+        unowned var note_details = p as NoteDetails;
         if (note_details != null) {
           if (!only_new)
             foreach (var note in note_details.notes) {
@@ -426,7 +411,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
         }
         break;
       case "postal-addresses":
-        var address_details = p as PostalAddressDetails;
+        unowned var address_details = p as PostalAddressDetails;
         if (address_details != null) {
           if (!only_new)
             foreach (var addr in address_details.postal_addresses) {
@@ -439,83 +424,105 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
     }
   }
 
-  private EditorPropertyRow create_for_email (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_email (Gee.Set<AbstractFieldDetails> details_set,
                                               EmailFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "PERSONAL";
       var new_details = new EmailFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
-    var box = new EditorPropertyRow ("email-addresses");
-    box.add_base_combo (set, _("Email address"), TypeSet.email, details);
-    box.add_base_entry_email (set, details, _("Add email"));
-    box.add_base_delete (set, details);
 
+    var box = new EditorPropertyRow ("email-addresses");
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("Add email"));
+    entry.set_input_purpose (Gtk.InputPurpose.EMAIL);
+    entry.changed.connect (() => {
+      details.value = entry.get_text ();
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property email changed");
+    });
+
+    box.add_type_combo (details_set, TypeSet.email, details);
+    box.add_base_delete (details_set, details);
+
     return box;
   }
 
-  private EditorPropertyRow create_for_phone (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_phone (Gee.Set<AbstractFieldDetails> details_set,
                                               PhoneFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "CELL";
       var new_details = new PhoneFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
 
     var box = new EditorPropertyRow ("phone-numbers");
-    box.add_base_combo (set, _("Phone number"), TypeSet.phone, details);
-    box.add_base_entry_phone (set, details, _("Add number"));
-    box.add_base_delete (set, details);
-
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("Add phone number"));
+    entry.set_input_purpose (Gtk.InputPurpose.PHONE);
+    entry.changed.connect (() => {
+      details.value = entry.text;
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property type changed");
+    });
+
+    box.add_type_combo (details_set, TypeSet.phone, details);
+    box.add_base_delete (details_set, details);
+
     return box;
   }
 
   // TODO: add support for different types of urls
-  private EditorPropertyRow create_for_url (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_url (Gee.Set<AbstractFieldDetails> details_set,
                                             UrlFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "PERSONAL";
       var new_details = new UrlFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
 
     var box = new EditorPropertyRow ("urls");
-    box.add_base_label (_("Website"));
-    box.add_base_entry_url (set, details, _("https://example.com";));
-    box.add_base_delete (set, details);
-
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("https://example.com";));
+    entry.set_input_purpose (Gtk.InputPurpose.PHONE);
+    entry.changed.connect (() => {
+      details.value = entry.get_text ();
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property type changed");
+    });
+
+    box.add_base_delete (details_set, details);
+
     return box;
   }
 
   private EditorPropertyRow create_for_nick (NameDetails details) {
     var box = new EditorPropertyRow ("nickname");
-    box.add_base_label (_("Nickname"));
-
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_text (details.nickname);
-    value_entry.set_hexpand (true);
-    box.container.append (value_entry);
+    box.sensitive = this.writeable;
 
-    value_entry.changed.connect (() => {
-      details.nickname = value_entry.get_text ();
+    var entry = box.set_main_entry (details.nickname, _("Nickname"));
+    entry.set_input_purpose (Gtk.InputPurpose.NAME);
+    entry.changed.connect (() => {
+      details.nickname = entry.text;
       debug ("Nickname changed");
-      box.is_empty = value_entry.get_text () == "";
     });
 
-    box.sensitive = this.writeable;
     return box;
   }
 
-  // TODO: support different types of nodes
+  // TODO: support different types of notes
   private EditorPropertyRow create_for_note (Gee.Set<NoteFieldDetails> details_set,
                                              NoteFieldDetails? details = null) {
     if (details == null) {
@@ -526,24 +533,24 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
       details = new_details;
     }
     var box = new EditorPropertyRow ("notes");
-    box.add_base_label (_("Note"));
 
     var sw = new Gtk.ScrolledWindow ();
-    sw.has_frame = true;
+    sw.focusable = false;
+    sw.has_frame = false;
     sw.set_size_request (-1, 100);
-    var value_text = new Gtk.TextView ();
-    value_text.get_buffer ().set_text (details.value);
-    value_text.set_hexpand (true);
-    sw.set_child (value_text);
-    box.container.append (sw);
+    var textview = new Gtk.TextView ();
+    textview.get_buffer ().set_text (details.value);
+    textview.hexpand = true;
+    sw.set_child (textview);
+    box.set_main_widget (sw);
 
     box.add_base_delete (details_set, details);
 
-    value_text.get_buffer ().changed.connect (() => {
+    textview.get_buffer ().changed.connect (() => {
       Gtk.TextIter start, end;
-      value_text.get_buffer ().get_start_iter (out start);
-      value_text.get_buffer ().get_end_iter (out end);
-      details.value = value_text.get_buffer ().get_text (start, end, true);
+      textview.get_buffer ().get_start_iter (out start);
+      textview.get_buffer ().get_end_iter (out end);
+      details.value = textview.get_buffer ().get_text (start, end, true);
       // Workaround: we shouldn't do a manual signal
       ((FakeHashSet) details_set).changed ();
       debug ("Property changed");
@@ -555,20 +562,17 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
   }
 
   private EditorPropertyRow create_for_birthday (BirthdayDetails? details) {
-    DateTime date;
+    var date = details.birthday ?? new DateTime.now ();
+
     Gtk.Button button;
     if (details.birthday == null) {
-      date = new DateTime.now ();
       button = new Gtk.Button.with_label (_("Set Birthday"));
     } else {
-      date = details.birthday;
       button = new Gtk.Button.with_label (details.birthday.to_local ().format ("%x"));
     }
 
     var box = new EditorPropertyRow ("birthday");
-    box.add_base_label (_("Birthday"));
-
-    box.container.append (button);
+    box.set_main_widget (button);
 
     button.clicked.connect (() => {
       unowned var parent_window = button.get_root () as Gtk.Window;
@@ -578,7 +582,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
         dialog.changed.connect (() => {
           if (dialog.is_set) {
             details.birthday = dialog.get_birthday ();
-            button.set_label (details.birthday.to_local ().format ("%x"));
+            button.set_label (details.birthday.format ("%x"));
             box.is_empty = false;
           }
         });
@@ -592,7 +596,7 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
     delete_button.tooltip_text = _("Delete field");
     delete_button.set_valign (Gtk.Align.START);
     box.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
-    box.container.append (delete_button);
+    // box.container.append (delete_button); XXX
 
     delete_button.clicked.connect (() => {
       debug ("Birthday removed");
@@ -616,13 +620,12 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
       details = new_details;
     }
     var box = new EditorPropertyRow ("postal-addresses");
-    box.add_base_combo (details_set, _("Address"), TypeSet.general, details);
 
     var value_address = new AddressEditor (details);
-    box.container.append (value_address);
-
+    box.set_main_widget (value_address);
     box.is_empty = value_address.is_empty ();
 
+    box.add_type_combo (details_set, TypeSet.general, details);
     box.add_base_delete (details_set, details);
 
     value_address.changed.connect (() => {
diff --git a/src/contacts-linked-personas-dialog.vala b/src/contacts-linked-personas-dialog.vala
index 4f89d892..f3b605da 100644
--- a/src/contacts-linked-personas-dialog.vala
+++ b/src/contacts-linked-personas-dialog.vala
@@ -40,13 +40,8 @@ public class Contacts.LinkedPersonasDialog : Gtk.Dialog {
 
     // loading personas for display
     var personas = Contacts.Utils.get_personas_for_display (individual);
-    bool is_first = true;
-    foreach (var p in personas) {
-      if (is_first) {
-        is_first = false;
-        continue;
-      }
-
+    for (int i = 1; i < personas.get_n_items (); i++) {
+      var p = (Persona) personas.get_item (i);
       var row_grid = new Gtk.Grid ();
 
       var image_frame = new Avatar (AVATAR_SIZE, individual);
diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala
index e471c326..62152a90 100644
--- a/src/contacts-main-window.vala
+++ b/src/contacts-main-window.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -196,9 +197,9 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     }
 
     // Allow the back gesture when not browsing
-    this.content_box.can_swipe_back = this.state == UiState.NORMAL ||
-                                      this.state == UiState.SHOWING ||
-                                      this.state == UiState.SELECTING;
+    this.content_box.can_navigate_back = this.state == UiState.NORMAL ||
+                                         this.state == UiState.SHOWING ||
+                                         this.state == UiState.SELECTING;
   }
 
   [GtkCallback]
@@ -444,7 +445,7 @@ public class Contacts.MainWindow : Adw.ApplicationWindow {
     this.settings.window_maximized = this.maximized;
     this.settings.window_fullscreen = this.fullscreened;
 
-    return Gdk.EVENT_STOP; // XXX what the hell am i supposed to retunr here?
+    return Gdk.EVENT_PROPAGATE;
   }
 
   void list_pane_selection_changed_cb (Individual? new_selection) {
diff --git a/src/contacts-type-combo.vala b/src/contacts-type-combo.vala
index 7104fcf9..3f252f7d 100644
--- a/src/contacts-type-combo.vala
+++ b/src/contacts-type-combo.vala
@@ -18,77 +18,47 @@
 using Folks;
 
 /**
- * The TypeCombo is a widget that fills itself with the types of a certain
+ * The TypeComboRow is a widget that fills itself with the types of a certain
  * category (using {@link Contacts.TypeSet}). For example, it allows the user
  * to choose between "Personal", "Home" and "Work" for email addresses,
  * together with all the custom labels it has encountered since then.
  */
-public class Contacts.TypeCombo : Gtk.ComboBox  {
+public class Contacts.TypeComboRow : Adw.ComboRow  {
 
-  private unowned TypeSet type_set;
-
-  /**
-   * The {@link Contacts.TypeDescriptor} that is currently shown
-   */
-  public TypeDescriptor active_descriptor {
-    get {
-      Gtk.TreeIter iter;
-
-      get_active_iter (out iter);
-      assert (!is_separator (this.model, iter));
-
-      unowned TypeDescriptor descriptor;
-      this.model.get (iter, 1, out descriptor);
-      return descriptor;
-    }
-    set {
-      set_active_iter (value.iter);
-    }
+  public TypeDescriptor selected_descriptor {
+    get { return (TypeDescriptor) this.selected_item; }
   }
 
-  construct {
-    this.valign = Gtk.Align.START;
-    this.halign = Gtk.Align.FILL;
-    this.hexpand = true;
-    this.visible = true;
-
-    var renderer = new Gtk.CellRendererText ();
-    pack_start (renderer, true);
-    set_attributes (renderer, "text", 0);
-
-    set_row_separator_func (is_separator);
+  public TypeSet type_set {
+    get { return (TypeSet) this.model; }
   }
 
   /**
-   * Creates a TypeCombo for the given TypeSet. To set the active value,
-   * use the "current-decsriptor" property, set_active_from_field_details(),
-   * or set_active_from_vcard_type()
+   * Creates a TypeComboRow for the given TypeSet.
    */
-  public TypeCombo (TypeSet type_set) {
-    this.type_set = type_set;
-    this.model = type_set.store;
-  }
-
-  private bool is_separator (Gtk.TreeModel model, Gtk.TreeIter iter) {
-    unowned string? s;
-    model.get (iter, 0, out s);
-    return s == null;
+  public TypeComboRow (TypeSet type_set) {
+    Object (
+      model: type_set,
+      expression: new Gtk.PropertyExpression (typeof (TypeDescriptor), null, "display-name")
+    );
   }
 
   /**
    * Sets the value to the type of the given {@link Folks.AbstractFieldDetails}.
    */
-  public void set_active_from_field_details (AbstractFieldDetails details) {
-    this.active_descriptor = this.type_set.lookup_descriptor_for_field_details (details);
+  public void set_selected_from_field_details (AbstractFieldDetails details) {
+    uint position = 0;
+    this.type_set.lookup_by_field_details (details, out position);
+    this.selected = position;
   }
 
   /**
    * Sets the value to the type that best matches the given vcard type
    * (for example "HOME" or "WORK").
    */
-  public void set_active_from_vcard_type (string type) {
-    Gtk.TreeIter iter;
-    this.type_set.get_iter_for_vcard_type (type, out iter);
-    set_active_iter (iter);
+  public void set_selected_from_vcard_type (string type) {
+    uint position = 0;
+    this.type_set.lookup_by_vcard_type (type, out position);
+    this.selected = position;
   }
 }
diff --git a/src/contacts-typeset.vala b/src/contacts-typeset.vala
index 3208ac47..731f9d30 100644
--- a/src/contacts-typeset.vala
+++ b/src/contacts-typeset.vala
@@ -22,7 +22,7 @@ using Folks;
  * phone number can be both for a personal phone, a work phone or even a fax
  * machine.
  */
-public class Contacts.TypeSet : Object  {
+public class Contacts.TypeSet : Object, GLib.ListModel  {
 
   /** Returns the category of typeset (mostly used for debugging). */
   public string category { get; construct set; }
@@ -34,58 +34,20 @@ public class Contacts.TypeSet : Object  {
   private Gee.List<VcardTypeMapping?> vcard_type_mappings
       = new Gee.ArrayList<VcardTypeMapping?> ();
 
-  // Contains 2 columns:
-  // 1. The type's display name (or null for a separator)
-  // 2. The TypeDescriptor
-  public Gtk.ListStore store { get; private set; }
+  private GenericArray<TypeDescriptor> descriptors = new GenericArray<TypeDescriptor> ();
 
   /**
    * Creates a TypeSet for the given category, e.g. "phones" (used for debugging)
    */
   private TypeSet (string? category) {
     Object (category: category);
-
-    this.store = new Gtk.ListStore (2, typeof (unowned string?), typeof (TypeDescriptor));
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds to the type of the
-   * given {@link Folks.AbstractFieldDetails}.
-   */
-  public void get_iter_for_field_details (AbstractFieldDetails detail,
-                                          out Gtk.TreeIter iter) {
-    // Note that we shouldn't have null here, but it's there just to be sure.
-    var d = lookup_descriptor_for_field_details (detail);
-    iter = d.iter;
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds the best to the given
-   * vcard type.
-   *
-   * @param type A VCard-like type, such as "HOME" or "CELL".
-   */
-  public void get_iter_for_vcard_type (string type, out Gtk.TreeIter iter) {
-    unowned TypeDescriptor? d = lookup_descriptor_by_vcard_type (type);
-    iter = (d != null)? d.iter : this.other_dummy.iter;
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds the best to the given
-   * custom label.
-   */
-  public void get_iter_for_custom_label (string label, out Gtk.TreeIter iter) {
-    var descr = get_descriptor_for_custom_label (label);
-    if (descr == null)
-      descr = create_descriptor_for_custom_label (label);
-    iter = descr.iter;
   }
 
   /**
    * Returns the display name for the type of the given AbstractFieldDetails.
    */
   public unowned string format_type (AbstractFieldDetails detail) {
-    var d = lookup_descriptor_for_field_details (detail);
+    var d = lookup_by_field_details (detail);
     return d.display_name;
   }
 
@@ -93,15 +55,10 @@ public class Contacts.TypeSet : Object  {
    * Adds the TypeDescriptor to the {@link TypeSet}'s store.
    * @param descriptor The TypeDescription to be added
    */
-  private void add_descriptor_to_store (TypeDescriptor descriptor) {
+  private void add_descriptor (TypeDescriptor descriptor) {
     debug ("%s: Adding type %s to store", this.category, descriptor.to_string ());
-
-    if (descriptor.is_custom ())
-      this.store.insert_before (out descriptor.iter, null);
-    else
-      this.store.append (out descriptor.iter);
-
-    store.set (descriptor.iter, 0, descriptor.display_name, 1, descriptor);
+    this.descriptors.add (descriptor);
+    this.items_changed (this.descriptors.length - 1, 0, 1);
   }
 
   /**
@@ -111,60 +68,57 @@ public class Contacts.TypeSet : Object  {
    * @param display_name The translated display name
    * @return The appropriate TypeDescriptor or null if no match was found.
    */
-  public unowned TypeDescriptor? lookup_descriptor_in_store (string display_name) {
-    Gtk.TreeIter iter;
-
-    // Make sure we handle an empty store
-    if (!this.store.get_iter_first (out iter))
-      return null;
-
-    do {
-      unowned TypeDescriptor? type_descr;
-      this.store.get (iter, 1, out type_descr);
-
-      if (display_name.ascii_casecmp (type_descr.display_name) == 0)
-        return type_descr;
-      if (display_name.ascii_casecmp (type_descr.name) == 0)
-        return type_descr;
-    } while (this.store.iter_next (ref iter));
+  public unowned TypeDescriptor? lookup_by_display_name (string display_name,
+                                                         out uint position) {
+    for (int i = 0; i < this.descriptors.length; i++) {
+      unowned var type_descr = this.descriptors[i];
+
+      if (display_name.ascii_casecmp (type_descr.display_name) != 0)
+        continue;
+      if (display_name.ascii_casecmp (type_descr.name) != 0)
+        continue;
+
+      position = i;
+      return type_descr;
+    }
 
     // Nothing was found
+    position = 0;
     return null;
   }
 
   private void add_vcard_mapping (VcardTypeMapping vcard_mapping) {
-    TypeDescriptor? descriptor = lookup_descriptor_in_store (vcard_mapping.name);
+    uint position;
+    var descriptor = lookup_by_display_name (vcard_mapping.name, out position);
     if (descriptor == null) {
       descriptor = new TypeDescriptor.vcard (vcard_mapping.name, vcard_mapping.types);
-      add_descriptor_to_store (descriptor);
+      debug ("%s: Adding VCard type %s to store", this.category, descriptor.to_string ());
+      this.add_descriptor (descriptor);
     }
 
     this.vcard_type_mappings.add (vcard_mapping);
   }
 
-  // Refers to the type of the detail, i.e. "Other" instead of "Personal" or "Work"
-  private void add_type_other () {
-    store.append (out other_dummy.iter);
-    store.set (other_dummy.iter, 0, other_dummy.display_name, 1, other_dummy);
-  }
-
   /**
    * Tries to find the TypeDescriptor matching the given custom label, or null if none.
    */
-  public unowned TypeDescriptor? get_descriptor_for_custom_label (string label) {
+  public TypeDescriptor? lookup_by_custom_label (string label,
+                                                 out uint position) {
     // Check in the current display names
-    unowned TypeDescriptor? descriptor = lookup_descriptor_in_store (label);
+    unowned var descriptor = lookup_by_display_name (label, out position);
     if (descriptor != null)
       return descriptor;
 
     // Try again, but use the vcard types too
-    descriptor = lookup_descriptor_by_vcard_type (label);
+    descriptor = lookup_by_vcard_type (label, out position);
     return descriptor;
   }
 
   private TypeDescriptor create_descriptor_for_custom_label (string label) {
     var new_descriptor = new TypeDescriptor.custom (label);
-    add_descriptor_to_store (new_descriptor);
+    debug ("%s: Adding custom type %s to store",
+           this.category, new_descriptor.to_string ());
+    this.add_descriptor (new_descriptor);
     return new_descriptor;
   }
 
@@ -172,19 +126,26 @@ public class Contacts.TypeSet : Object  {
    * Returns the TypeDescriptor which corresponds the best to the given vcard type.
    * @param str A VCard-like type, such as "HOME" or "CELL".
    */
-  private unowned TypeDescriptor? lookup_descriptor_by_vcard_type (string str) {
+  public unowned TypeDescriptor? lookup_by_vcard_type (string str,
+                                                       out uint position) {
     foreach (VcardTypeMapping? mapping in this.vcard_type_mappings) {
       if (mapping.contains (str))
-        return lookup_descriptor_in_store (mapping.name);
+        return lookup_by_display_name (mapping.name, out position);
     }
 
+    position = 0;
     return null;
   }
 
-  public TypeDescriptor lookup_descriptor_for_field_details (AbstractFieldDetails detail) {
+  /**
+   * Looks up the TypeDescriptor for the given field details. If the descriptor
+   * is not found, it will be created and returned, so this never returns null.
+   */
+  public TypeDescriptor lookup_by_field_details (AbstractFieldDetails detail,
+                                                 out uint position = null) {
     if (detail.parameters.contains (TypeDescriptor.X_GOOGLE_LABEL)) {
       var label = Utils.get_first<string> (detail.parameters[TypeDescriptor.X_GOOGLE_LABEL]);
-      var descriptor = get_descriptor_for_custom_label (label);
+      var descriptor = lookup_by_custom_label (label, out position);
       // Still didn't find it => create it
       if (descriptor == null)
         descriptor = create_descriptor_for_custom_label (label);
@@ -199,12 +160,26 @@ public class Contacts.TypeSet : Object  {
 
     foreach (VcardTypeMapping? d in this.vcard_type_mappings) {
       if (d.matches (types))
-        return lookup_descriptor_in_store (d.name);
+        return lookup_by_display_name (d.name, out position);
     }
 
     return this.other_dummy;
   }
 
+  public GLib.Type get_item_type () {
+    return typeof (TypeDescriptor);
+  }
+
+  public uint get_n_items () {
+    return this.descriptors.length;
+  }
+
+  public GLib.Object? get_item (uint i) {
+    if (i > this.descriptors.length)
+      return null;
+
+    return this.descriptors[i];
+  }
 
   private static TypeSet _general;
   private const VcardTypeMapping[] general_data = {
@@ -218,7 +193,8 @@ public class Contacts.TypeSet : Object  {
         _general = new TypeSet ("General");
         for (int i = 0; i < general_data.length; i++)
           _general.add_vcard_mapping (general_data[i]);
-        _general.add_type_other ();
+
+        _general.add_descriptor (general.other_dummy);
       }
 
       return _general;
@@ -238,7 +214,7 @@ public class Contacts.TypeSet : Object  {
         _email = new TypeSet ("Emails");
         for (int i = 0; i < email_data.length; i++)
           _email.add_vcard_mapping (email_data[i]);
-        _email.add_type_other ();
+        _email.add_descriptor (_email.other_dummy);
       }
 
       return _email;
@@ -275,7 +251,7 @@ public class Contacts.TypeSet : Object  {
         _phone = new TypeSet ("Phones");
         for (int i = 0; i < phone_data.length; i++)
           _phone.add_vcard_mapping (phone_data[i]);
-        _phone.add_type_other ();
+        _phone.add_descriptor (_phone.other_dummy);
       }
 
       return _phone;
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index f7a95f31..4627acee 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -348,18 +348,23 @@ namespace Contacts.Utils {
     return false;
   }
 
-  public Gee.List<Persona> get_personas_for_display (Individual individual) {
-    CompareDataFunc<Persona> compare_persona_by_store = (a, b) => {
-      unowned var store_a = a.store;
-      unowned var store_b = b.store;
+  public ListModel get_personas_for_display (Individual individual) {
+    var persona_list = new ListStore(typeof(Persona));
+    foreach (var persona in individual.personas)
+      if (persona.store.type_id != "key-file")
+        persona_list.append (persona);
+
+    persona_list.sort ((a, b) => {
+      unowned var store_a = ((Persona) a).store;
+      unowned var store_b = ((Persona) b).store;
 
       // In the same store, sort Google 'other' contacts last
       if (store_a == store_b) {
-        if (!persona_is_google (a))
+        if (!persona_is_google ((Persona) a))
           return 0;
 
-        var a_is_other = persona_is_google_other (a);
-        if (a_is_other != persona_is_google_other (b))
+        var a_is_other = persona_is_google_other ((Persona) a);
+        if (a_is_other != persona_is_google_other ((Persona) b))
           return a_is_other? 1 : -1;
       }
 
@@ -373,14 +378,8 @@ namespace Contacts.Utils {
 
       // Normal case: use alphabetical sorting
       return strcmp (store_a.id, store_b.id);
-    };
-
-    var persona_list = new Gee.ArrayList<Persona>();
-    foreach (var persona in individual.personas)
-      if (persona.store.type_id != "key-file")
-        persona_list.add (persona);
+    });
 
-    persona_list.sort ((owned) compare_persona_by_store);
     return persona_list;
   }
 
@@ -567,6 +566,50 @@ namespace Contacts.Utils {
     }
   }
 
+  // A helper struct to keep track on general properties on how each Persona
+  // property should be displayed
+  private struct PropertyDisplayInfo {
+    string property_name;
+    string display_name;
+    string icon_name;
+  }
+
+  private const PropertyDisplayInfo[] display_infos = {
+    { "alias", N_("Alias"), null },
+    { "avatar", N_("Avatar"), "emblem-photos-symbolic" },
+    { "birthday", N_("Birthday"), "x-office-calendar-symbolic" },
+    { "calendar-event-id", N_("Calendar event"), "x-office-calendar-symbolic" },
+    { "email-addresses", N_("Email address"), "mail-unread-symbolic" },
+    { "full-name", N_("Full name"), null },
+    { "gender", N_("Gender"), null },
+    { "groups", N_("Group"), null },
+    { "im-addresses", N_("Instant messaging"), "user-available-symbolic" },
+    { "is-favourite", N_("Favourite"), "emblem-favorite-symbolic" },
+    { "local-ids", N_("Local ID"), null },
+    { "nickname", N_("Nickname"), "avatar-default-symbolic" },
+    { "notes", N_("Note"), "accessories-text-editor-symbolic" },
+    { "phone-numbers", N_("Phone number"), "phone-symbolic" },
+    { "postal-addresses", N_("Address"), "mark-location-symbolic" },
+    { "roles", N_("Role"), null },
+    { "structured-name", N_("Structured name"), "avatar-default-symbolic" },
+    { "urls", N_("Website"), "web-browser-symbolic" },
+    { "web-service-addresses", N_("Web service"), null },
+  };
+
+  public unowned string get_display_name_for_property (string property_name) {
+    foreach (unowned var info in display_infos)
+      if (info.property_name == property_name)
+        return gettext (info.display_name);
+    return_val_if_reached (null);
+  }
+
+  public unowned string? get_icon_name_for_property (string property_name) {
+    foreach (unowned var info in display_infos)
+      if (info.property_name == property_name)
+        return info.icon_name;
+    return null;
+  }
+
 #if HAVE_TELEPATHY
   public void fetch_contact_info (Individual individual) {
     /* TODO: Ideally Folks should have API for this (#675131) */


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