[gnome-contacts] Editor: use listbox layout to edit contact and secondary menu



commit 368d11f94f411a63ab5792ebed55047ef7bcf0fd
Author: Julian Sparber <julian sparber net>
Date:   Fri Jan 10 15:46:17 2020 +0100

    Editor: use listbox layout to edit contact and secondary menu
    
    GNOME uses now listboxes as the standart design pattern instead of a
    grid. This replaces the grid and makes use of listboxes to allow the
    user to edit a contact.
    Some key features are:
    - Hide less important properties when not used
    - Dynamically fill the editor with properties so that the user has always
      one empty row to fill for each visible property
    - use a dialog for the birthday picker
    - Group properties by persona
    
    ContactSheet:
    Replace the edit button with a secondary menu.
    The secondary menu contains share (hidden for now), edit, unlink and delete.
    The reason for this change is that it doesn't make a lot of sense to have
    delete and unlink inside the edit mode, since they don't require to commit changed.
    
    Folks doesn't provied a staging features. So changes are commited
    directly to the backend. The FakePersona and FakeIndividual are used
    exactly for this. They work as a intermidiate layer so the editor can
    change the persona directly and then when the user presses "done" the
    changes can be copied to the real contact.

 data/contacts.gresource.xml          |   3 +-
 data/ui/contacts-contact-form.ui     |  37 --
 data/ui/contacts-contact-pane.ui     | 118 +++--
 data/ui/contacts-editor-menu.ui      |  19 +
 data/ui/contacts-window.ui           |  59 ++-
 data/ui/style.css                    |  25 +-
 src/contacts-addressbook-list.vala   | 156 ++++++
 src/contacts-app.vala                |  16 +-
 src/contacts-avatar-selector.vala    |   8 +-
 src/contacts-contact-editor.vala     | 941 ++---------------------------------
 src/contacts-contact-pane.vala       | 204 +++-----
 src/contacts-contact-sheet.vala      |  75 ++-
 src/contacts-editor-persona.vala     | 190 +++++++
 src/contacts-editor-property.vala    | 629 +++++++++++++++++++++++
 src/contacts-fake-persona-store.vala | 516 +++++++++++++++----
 src/contacts-linking.vala            |  54 +-
 src/contacts-max-width-bin.vala      |  66 ---
 src/contacts-store.vala              |  14 +-
 src/contacts-utils.vala              |  21 +
 src/contacts-window.vala             |  93 +++-
 src/meson.build                      |   5 +-
 21 files changed, 1830 insertions(+), 1419 deletions(-)
---
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 6618f43..4105fc5 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -5,10 +5,9 @@
     <file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-accounts-list.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-avatar-selector.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-editor.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-form.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-pane.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-crop-cheese-dialog.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-editor-menu.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-in-app-notification.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-link-suggestion-grid.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-linked-personas-dialog.ui</file>
diff --git a/data/ui/contacts-contact-pane.ui b/data/ui/contacts-contact-pane.ui
index 1d2362b..1958d97 100644
--- a/data/ui/contacts-contact-pane.ui
+++ b/data/ui/contacts-contact-pane.ui
@@ -1,62 +1,84 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <requires lib="gtk+" version="3.20"/>
-  <template class="ContactsContactPane" parent="GtkStack">
+  <template class="ContactsContactPane" parent="GtkScrolledWindow">
     <property name="visible">True</property>
-    <property name="visible-child">none_selected_page</property>
+    <property name="hexpand">True</property>
+    <property name="vexpand">True</property>
+    <property name="shadow_type">none</property>
+    <property name="hscrollbar_policy">never</property>
+    <property name="vscrollbar_policy">automatic</property>
     <child>
-      <object class="GtkGrid" id="none_selected_page">
+      <object class="HdyColumn">
         <property name="visible">True</property>
-        <property name="width_request">300</property>
-        <property name="orientation">vertical</property>
-        <property name="hexpand">True</property>
-        <property name="vexpand">True</property>
-        <property name="row_spacing">6</property>
+        <property name="maximum-width">600</property>
+        <property name="linear-growth-width">400</property>
+        <property name="margin-top">32</property>
+        <property name="margin-bottom">32</property>
+        <property name="margin-left">24</property>
+        <property name="margin-right">24</property>
         <child>
-          <object class="GtkImage">
+          <object class="GtkStack" id="stack">
             <property name="visible">True</property>
-            <property name="icon_name">avatar-default-symbolic</property>
-            <property name="vexpand">True</property>
-            <property name="valign">end</property>
-            <property name="pixel_size">144</property>
-            <style>
-              <class name="contacts-watermark"/>
-            </style>
+            <property name="visible-child">none_selected_page</property>
+            <child>
+              <object class="GtkGrid" id="none_selected_page">
+                <property name="visible">True</property>
+                <property name="width_request">300</property>
+                <property name="orientation">vertical</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="row_spacing">6</property>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon_name">avatar-default-symbolic</property>
+                    <property name="vexpand">True</property>
+                    <property name="valign">end</property>
+                    <property name="pixel_size">144</property>
+                    <style>
+                      <class name="contacts-watermark"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Select a contact</property>
+                    <property name="hexpand">True</property>
+                    <property name="vexpand">True</property>
+                    <property name="valign">start</property>
+                    <property name="margin_bottom">70</property>
+                    <style>
+                      <class name="contacts-watermark"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">none-selected-page</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="contact_sheet_page">
+                <property name="visible">True</property>
+              </object>
+              <packing>
+                <property name="name">contact-sheet-page</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkBox" id="contact_editor_page">
+                <property name="visible">True</property>
+              </object>
+              <packing>
+                <property name="name">contact-editor-page</property>
+              </packing>
+            </child>
+
           </object>
         </child>
-        <child>
-          <object class="GtkLabel">
-            <property name="visible">True</property>
-            <property name="label" translatable="yes">Select a contact</property>
-            <property name="hexpand">True</property>
-            <property name="vexpand">True</property>
-            <property name="valign">start</property>
-            <property name="margin_bottom">70</property>
-            <style>
-              <class name="contacts-watermark"/>
-            </style>
-          </object>
-        </child>
-      </object>
-      <packing>
-        <property name="name">none-selected-page</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkBox" id="contact_sheet_page">
-        <property name="visible">True</property>
-      </object>
-      <packing>
-        <property name="name">contact-sheet-page</property>
-      </packing>
-    </child>
-    <child>
-      <object class="GtkBox" id="contact_editor_page">
-        <property name="visible">True</property>
       </object>
-      <packing>
-        <property name="name">contact-editor-page</property>
-      </packing>
     </child>
   </template>
 </interface>
diff --git a/data/ui/contacts-editor-menu.ui b/data/ui/contacts-editor-menu.ui
new file mode 100644
index 0000000..21ac6bb
--- /dev/null
+++ b/data/ui/contacts-editor-menu.ui
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<interface>
+  <object class="GtkPopoverMenu" id="editor_menu">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="margin">10</property>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">True</property>
+            <property name="action-name">persona.change-addressbook</property>
+            <property name="text" translatable="yes">Change Addressbook</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/data/ui/contacts-window.ui b/data/ui/contacts-window.ui
index e203f73..616ac42 100644
--- a/data/ui/contacts-window.ui
+++ b/data/ui/contacts-window.ui
@@ -94,6 +94,48 @@
       </object>
     </child>
   </object>
+  <object class="GtkPopoverMenu" id="contact_sheet_menu">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="margin">10</property>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">False</property>
+            <property name="action-name">window.share-contact</property>
+            <property name="text" translatable="yes">Share</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">True</property>
+            <property name="action-name">window.edit-contact</property>
+            <property name="text" translatable="yes">Edit</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton" id="unlink_button">
+            <property name="visible">True</property>
+            <property name="action-name">window.unlink-contact</property>
+            <property name="text" translatable="yes">Unlink</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSeparator">
+            <property name="visible">True</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkModelButton">
+            <property name="visible">True</property>
+            <property name="action-name">window.delete-contact</property>
+            <property name="text" translatable="yes">Delete</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
   <template class="ContactsWindow" parent="GtkApplicationWindow">
     <property name="can_focus">False</property>
     <property name="default_width">800</property>
@@ -245,44 +287,43 @@
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkButton" id="edit_button">
+                  <object class="GtkToggleButton" id="favorite_button">
                     <property name="visible">False</property>
                     <property name="can_focus">True</property>
                     <property name="focus_on_click">False</property>
                     <property name="valign">center</property>
-                    <property name="tooltip_text" translatable="yes">Edit details</property>
-                    <signal name="clicked" handler="on_edit_button_clicked"/>
+                    <signal name="toggled" handler="on_favorite_button_toggled"/>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
-                        <property name="icon_name">document-edit-symbolic</property>
+                        <property name="icon_name">starred-symbolic</property>
                         <property name="icon_size">1</property>
                       </object>
                     </child>
                   </object>
                   <packing>
                     <property name="pack_type">end</property>
+                    <property name="position">2</property>
                   </packing>
                 </child>
                 <child>
-                  <object class="GtkToggleButton" id="favorite_button">
+                  <object class="GtkMenuButton" id="contact_menu_button">
                     <property name="visible">False</property>
                     <property name="can_focus">True</property>
                     <property name="focus_on_click">False</property>
-                    <property name="valign">center</property>
-                    <signal name="toggled" handler="on_favorite_button_toggled"/>
+                    <property name="popover">contact_sheet_menu</property>
                     <child>
                       <object class="GtkImage">
                         <property name="visible">True</property>
                         <property name="can_focus">False</property>
-                        <property name="icon_name">starred-symbolic</property>
-                        <property name="icon_size">1</property>
+                        <property name="icon_name">view-more-symbolic</property>
                       </object>
                     </child>
                   </object>
                   <packing>
                     <property name="pack_type">end</property>
+                    <property name="position">1</property>
                   </packing>
                 </child>
                 <child>
diff --git a/data/ui/style.css b/data/ui/style.css
index 763703c..de2f0f6 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -2,24 +2,11 @@
  * GNOME Contacts
  */
 
-.contacts-map {
-    background-color: @theme_bg_color;
-}
-
 /* The contacts in the left pane */
 .contacts-contact-list {
   background-color: transparent;
 }
 
-/* A single row in the contact list pane */
-row.contact-data-row {
-}
-
-/* Styles for a ContactsContactForm */
-.contacts-contact-form {
-  background-color: mix(@theme_bg_color, @theme_base_color, 0.4);
-}
-
 .contacts-suggestion {
   border-top: 1px solid @borders;
   background-color: shade(@theme_bg_color, 0.9);
@@ -69,3 +56,15 @@ row.contact-data-row {
   text-shadow: none; -gtk-icon-shadow: none;
   border: 1px solid rgba(205, 199, 194, 0.5);
 }
+
+/* remove padding from ListBoxRow so that the revealer doesn't jump */
+row.editor-property-row {
+  padding: 0px;
+}
+
+popover list {
+  background-color: @theme_bg_color;
+}
+popover list row:hover {
+  background-color: @theme_selected_fg_color
+}
diff --git a/src/contacts-addressbook-list.vala b/src/contacts-addressbook-list.vala
new file mode 100644
index 0000000..c59a30a
--- /dev/null
+++ b/src/contacts-addressbook-list.vala
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * Author: Julian Sparber
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Hdy;
+using Gtk;
+using Folks;
+
+public class Contacts.AddressbookList : ListBox {
+  private BackendStore store;
+  private Widget? checkmark;
+  private AddressbookRow? marked_row;
+  private bool show_icon;
+
+  public signal void addressbook_selected ();
+
+  public AddressbookList (BackendStore store, bool icon = true) {
+    this.store = store;
+    this.show_icon = icon;
+
+    this.set_header_func (list_box_update_header_func);
+    this.update ();
+  }
+
+  void list_box_update_header_func (ListBoxRow row, ListBoxRow? before) {
+    if (before == null) {
+      row.set_header (null);
+    } else if (row.get_header () == null) {
+      var header = new Separator (Orientation.HORIZONTAL);
+      header.show ();
+      row.set_header (header);
+    }
+  }
+
+  public override void row_activated (ListBoxRow row) {
+    var addressbook = row as AddressbookRow;
+    if (addressbook == null)
+      return;
+
+    if (marked_row != null &&
+        marked_row == addressbook) {
+      return;
+    }
+
+
+    if (marked_row != null) {
+      marked_row.unselect ();
+    }
+
+    addressbook.select ();
+    marked_row = addressbook;
+
+    addressbook_selected ();
+  }
+
+  public void update () {
+    foreach (var child in get_children ()) {
+      child.destroy ();
+    }
+
+    // Fill the list with address book
+    PersonaStore[] eds_stores = Utils.get_eds_address_books_from_backend (this.store);
+    debug ("Found %d EDS stores", eds_stores.length);
+
+    PersonaStore? local_store = null;
+    foreach (var persona_store in eds_stores) {
+      if (persona_store.id == "system-address-book") {
+        local_store = persona_store;
+        continue;
+      }
+      var source = (persona_store as Edsf.PersonaStore).source;
+      var parent_source = eds_source_registry.ref_source (source.parent);
+      var provider_name = Utils.format_persona_store_name (persona_store);
+
+      debug ("Contact store \"%s\"", provider_name);
+
+      var source_account_id = "";
+      if (parent_source.has_extension (E.SOURCE_EXTENSION_GOA)) {
+        var goa_source_ext = parent_source.get_extension (E.SOURCE_EXTENSION_GOA) as E.SourceGoa;
+        source_account_id = goa_source_ext.account_id;
+      }
+
+      Gtk.Image provider_image = null;
+      if (this.show_icon) {
+        if (source_account_id != "")
+          provider_image = Contacts.get_icon_for_goa_account (source_account_id);
+        else
+          provider_image = new Image.from_icon_name (Config.APP_ID, IconSize.DIALOG);
+      }
+
+      var row = new AddressbookRow (provider_name, parent_source.display_name, provider_image);
+      add (row);
+    }
+
+    if (local_store != null) {
+      var provider_image = (this.show_icon) ? new Image.from_icon_name (Config.APP_ID, IconSize.DIALOG) : 
null;
+      var local_row = new AddressbookRow (_("Local Address Book"), null, provider_image);
+      add (local_row);
+    }
+
+    /*
+       if (select_active &&
+       local_store == this.contacts_store.aggregator.primary_store) {
+       row_activated (local_row);
+       }
+     */
+
+    show_all ();
+  }
+}
+
+public class Contacts.AddressbookRow : Hdy.ActionRow {
+  Widget checkmark;
+  public AddressbookRow (string title, string? subtitle, Widget? image = null) {
+    this.set_selectable (false);
+    if (image != null) {
+      this.add_prefix (image);
+    }
+    this.title = title;
+    if (subtitle != null) {
+      this.subtitle = subtitle;
+    }
+    this.show_all ();
+    this.no_show_all = true;
+    this.checkmark = new Image.from_icon_name ("object-select-symbolic", IconSize.MENU);
+    this.checkmark.set ("margin-end", 6,
+                        "valign", Align.CENTER,
+                        "halign", Align.END,
+                        "vexpand", true,
+                        "hexpand", true);
+    this.add_action (this.checkmark);
+  }
+
+  public void unselect () {
+    this.checkmark.hide ();
+  }
+
+  public void select () {
+    this.checkmark.show ();
+  }
+}
diff --git a/src/contacts-app.vala b/src/contacts-app.vala
index 668c90e..af4bfa5 100644
--- a/src/contacts-app.vala
+++ b/src/contacts-app.vala
@@ -30,13 +30,13 @@ public class Contacts.App : Gtk.Application {
   private bool is_quiescent_scheduled = false;
 
   private const GLib.ActionEntry[] action_entries = {
-    { "quit",            quit                },
-    { "help",            show_help           },
-    { "about",           show_about          },
-    { "change-book",     change_address_book },
-    { "online-accounts", online_accounts     },
-    { "new-contact",     new_contact         },
-    { "show-contact",    on_show_contact,   "s"     }
+    { "quit",             quit                },
+    { "help",             show_help           },
+    { "about",            show_about          },
+    { "change-book",      change_address_book },
+    { "online-accounts",  online_accounts     },
+    { "new-contact",      new_contact         },
+    { "show-contact",     on_show_contact, "s"}
   };
 
   private const OptionEntry[] options = {
@@ -55,7 +55,7 @@ public class Contacts.App : Gtk.Application {
 
     this.settings = new Settings (this);
     add_main_option_entries (options);
-       create_actions ();
+         create_actions ();
   }
 
   public override int command_line (ApplicationCommandLine command_line) {
diff --git a/src/contacts-avatar-selector.vala b/src/contacts-avatar-selector.vala
index 126d06d..51a5f3a 100644
--- a/src/contacts-avatar-selector.vala
+++ b/src/contacts-avatar-selector.vala
@@ -48,11 +48,6 @@ public class Contacts.AvatarSelector : Popover {
   private Cheese.CameraDeviceMonitor camera_monitor;
 #endif
 
-  /**
-   * Fired after the user has definitely chosen a new avatar.
-   */
-  public signal void set_avatar (GLib.Icon avatar_icon);
-
   public AvatarSelector (Gtk.Widget relative, Individual? individual) {
     this.set_relative_to(relative);
     this.thumbnail_factory = new Gnome.DesktopThumbnailFactory (Gnome.ThumbnailSize.NORMAL);
@@ -105,7 +100,8 @@ public class Contacts.AvatarSelector : Popover {
       uint8[] buffer;
       pixbuf.save_to_buffer (out buffer, "png", null);
       var icon = new BytesIcon (new Bytes (buffer));
-      set_avatar (icon);
+      // Set the new avatar
+      this.individual.change_avatar(icon as LoadableIcon);
     } catch (GLib.Error e) {
       warning ("Failed to set avatar: %s", e.message);
       Utils.show_error_dialog (_("Failed to set avatar."),
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
index 396a681..c73da5d 100644
--- a/src/contacts-contact-editor.vala
+++ b/src/contacts-contact-editor.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2019 Purism SPC
  *
  * 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
@@ -19,885 +20,32 @@ using Gtk;
 using Folks;
 using Gee;
 
-public class Contacts.AddressEditor : Box {
-  public Entry? entries[7];  /* must be the number of elements in postal_element_props */
-  public PostalAddressFieldDetails details;
-
-  public const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code", 
"po_box", "country"};
-  public static string[] postal_element_names = {_("Street"), _("Extension"), _("City"), 
_("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};
-
-  public signal void changed ();
-
-  public AddressEditor (PostalAddressFieldDetails _details) {
-    set_hexpand (true);
-    set_orientation (Orientation.VERTICAL);
-
-    details = _details;
-
-    for (int i = 0; i < entries.length; i++) {
-      string postal_part;
-      details.value.get (AddressEditor.postal_element_props[i], out postal_part);
-
-      entries[i] = new Entry ();
-      entries[i].set_hexpand (true);
-      entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
-
-      if (postal_part != null)
-       entries[i].set_text (postal_part);
-
-      entries[i].get_style_context ().add_class ("contacts-postal-entry");
-      add (entries[i]);
-
-      entries[i].changed.connect (() => {
-         changed ();
-       });
-    }
-  }
-
-  public override void grab_focus () {
-    entries[0].grab_focus ();
-  }
-}
-
 /**
  * A widget that allows the user to edit a given {@link Contact}.
  */
-[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-editor.ui")]
-public class Contacts.ContactEditor : ContactForm {
-
-  private const string[] DEFAULT_PROPS_NEW_CONTACT = {
-    "email-addresses.personal",
-    "phone-numbers.cell",
-    "postal-addresses.home"
-  };
-
-  private weak Widget focus_widget;
-
+public class Contacts.ContactEditor : Box {
+  private Individual individual;
   private Entry name_entry;
-
+  private AvatarSelector avatar_selector = null;
   private Avatar avatar;
 
-  [GtkChild]
-  private MenuButton add_detail_button;
-
-  [GtkChild]
-  public Button linked_button;
-
-  [GtkChild]
-  public Button remove_button;
-
-  public struct PropertyData {
-    Persona? persona;
-    Value value;
-  }
-
-  struct RowData {
-    AbstractFieldDetails details;
-  }
-
-  struct Field {
-    bool changed;
-    HashMap<int, RowData?> rows;
-  }
-
-  private HashSet<Persona> unlink_personas;
-  /* the key of the hash_map is the uid of the persona */
-  private HashMap<string, HashMap<string, Field?>> writable_personas;
-
-  public bool has_birthday_row {
-    get; private set; default = false;
-  }
-
-  public bool has_nickname_row {
-    get; private set; default = false;
-  }
-
-  public bool has_notes_row {
-    get; private set; default = false;
-  }
-
-  construct {
-    this.unlink_personas = new HashSet<Persona> ();
-    this.writable_personas = new HashMap<string, HashMap<string, Field?>> ();
-    this.container_grid.size_allocate.connect(on_container_grid_size_allocate);
-  }
-
-  public ContactEditor (Individual? individual, Store store, GLib.ActionGroup editor_actions) {
-    this.store = store;
+  public ContactEditor (Individual individual, IndividualAggregator aggregator) {
+    Object (orientation: Orientation.VERTICAL, spacing: 24);
     this.individual = individual;
 
-    this.add_detail_button.get_popover ().insert_action_group ("edit", editor_actions);
-
-    if (individual != null) {
-      this.remove_button.sensitive = Contacts.Utils.can_remove_personas (individual);
-      this.linked_button.sensitive = individual.personas.size > 1;
-    } else {
-      this.remove_button.hide ();
-      this.linked_button.hide ();
-    }
-
-    create_avatar_button ();
-    create_name_entry ();
-
-    if (individual != null)
-      fill_in_contact ();
-    else
-      fill_in_empty ();
-
-    this.container_grid.show_all ();
-  }
-
-  private void fill_in_contact () {
-    int i = 3;
-    int last_store_position = 0;
-    bool is_first_persona = true;
-
-    var personas = Contacts.Utils.get_personas_for_display (individual);
-    foreach (var p in personas) {
-      if (!is_first_persona) {
-        this.container_grid.attach (create_persona_store_label (p), 0, i, 2);
-        last_store_position = ++i;
-      }
+    Box header = new Box (Orientation.HORIZONTAL, 6);
+    header.add (create_avatar_button ());
+    header.add (create_name_entry ());
+    add (header);
 
-      var rw_props = sort_persona_properties (p.writeable_properties);
-      if (rw_props.length != 0) {
-        this.writable_personas[p.uid] = new HashMap<string, Field?> ();
-        foreach (var prop in rw_props)
-          add_edit_row (p, prop, ref i);
-      }
-
-      if (is_first_persona)
-        this.last_row = i - 1;
-
-      if (i != 3)
-        is_first_persona = false;
-
-      if (i == last_store_position) {
-        i--;
-        this.container_grid.get_child_at (0, i).destroy ();
-      }
+    foreach (var p in individual.personas) {
+      add (new EditorPersona (p, aggregator));
     }
-  }
-
-  private void fill_in_empty () {
-    this.last_row = 2;
-
-    this.writable_personas["null-persona.hack"] = new HashMap<string, Field?> ();
-    foreach (var prop in DEFAULT_PROPS_NEW_CONTACT) {
-      var tok = prop.split (".");
-      add_new_row_for_property (null, tok[0], tok[1].up ());
-    }
-
-    this.focus_widget = this.name_entry;
-  }
-
-  Value get_value_from_emails (HashMap<int, RowData?> rows) {
-    var new_details = new HashSet<EmailFieldDetails>();
-
-    foreach (var row_entry in rows.entries) {
-      var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
-      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
-      /* Ignore empty entries. */
-      if (entry.get_text () == "")
-        continue;
-
-      combo.active_descriptor.save_to_field_details (row_entry.value.details);
-      var details = new EmailFieldDetails (entry.get_text (), row_entry.value.details.parameters);
-      new_details.add (details);
-    }
-    var new_value = Value (new_details.get_type ());
-    new_value.set_object (new_details);
-
-    return new_value;
-  }
-
-  Value get_value_from_phones (HashMap<int, RowData?> rows) {
-    var new_details = new HashSet<PhoneFieldDetails>();
-
-    foreach (var row_entry in rows.entries) {
-      var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
-      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
-      /* Ignore empty entries. */
-      if (entry.get_text () == "")
-        continue;
-
-      combo.active_descriptor.save_to_field_details (row_entry.value.details);
-      var details = new PhoneFieldDetails (entry.get_text (), row_entry.value.details.parameters);
-      new_details.add (details);
-    }
-    var new_value = Value (new_details.get_type ());
-    new_value.set_object (new_details);
-    return new_value;
-  }
-
-  Value get_value_from_urls (HashMap<int, RowData?> rows) {
-    var new_details = new HashSet<UrlFieldDetails>();
-
-    foreach (var row_entry in rows.entries) {
-      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
-      /* Ignore empty entries. */
-      if (entry.get_text () == "")
-        continue;
-
-      var details = new UrlFieldDetails (entry.get_text (), row_entry.value.details.parameters);
-      new_details.add (details);
-    }
-    var new_value = Value (new_details.get_type ());
-    new_value.set_object (new_details);
-    return new_value;
-  }
-
-  Value get_value_from_nickname (HashMap<int, RowData?> rows) {
-    var new_value = Value (typeof (string));
-    foreach (var row_entry in rows.entries) {
-      var entry = container_grid.get_child_at (1, row_entry.key) as Entry;
-
-      /* Ignore empty entries. */
-      if (entry.get_text () == "")
-        continue;
-
-      new_value.set_string (entry.get_text ());
-    }
-    return new_value;
-  }
-
-  Value get_value_from_birthday (HashMap<int, RowData?> rows) {
-    var new_value = Value (typeof (DateTime));
-    foreach (var row_entry in rows.entries) {
-      var box = container_grid.get_child_at (1, row_entry.key) as Grid;
-      var day_spin  = box.get_child_at (0, 0) as SpinButton;
-      var combo  = box.get_child_at (1, 0) as ComboBoxText;
-      var year_spin  = box.get_child_at (2, 0) as SpinButton;
-
-      var bday = new DateTime.local (year_spin.get_value_as_int (),
-                                    combo.get_active () + 1,
-                                    day_spin.get_value_as_int (),
-                                    0, 0, 0);
-      bday = bday.to_utc ();
-
-      new_value.set_boxed (bday);
-    }
-    return new_value;
-  }
-
-  Value get_value_from_notes (HashMap<int, RowData?> rows) {
-    var new_details = new HashSet<NoteFieldDetails>();
-
-    foreach (var row_entry in rows.entries) {
-      var text = (container_grid.get_child_at (1, row_entry.key) as Bin).get_child () as TextView;
-      TextIter start, end;
-      text.get_buffer ().get_start_iter (out start);
-      text.get_buffer ().get_end_iter (out end);
-      var value = text.get_buffer ().get_text (start, end, true);
-      if (value != "") {
-        var details = new NoteFieldDetails (value, row_entry.value.details.parameters);
-        new_details.add (details);
-      }
-    }
-    var new_value = Value (new_details.get_type ());
-    new_value.set_object (new_details);
-    return new_value;
-  }
-
-  Value get_value_from_addresses (HashMap<int, RowData?> rows) {
-    var new_details = new HashSet<PostalAddressFieldDetails>();
-
-    foreach (var row_entry in rows.entries) {
-      var combo = container_grid.get_child_at (0, row_entry.key) as TypeCombo;
-      var addr_editor = container_grid.get_child_at (1, row_entry.key) as AddressEditor;
-      combo.active_descriptor.save_to_field_details (row_entry.value.details);
-
-      var new_value = new PostalAddress (addr_editor.details.value.po_box,
-                                        addr_editor.details.value.extension,
-                                        addr_editor.details.value.street,
-                                        addr_editor.details.value.locality,
-                                        addr_editor.details.value.region,
-                                        addr_editor.details.value.postal_code,
-                                        addr_editor.details.value.country,
-                                        addr_editor.details.value.address_format,
-                                        addr_editor.details.id);
-      for (int i = 0; i < addr_editor.entries.length; i++)
-       new_value.set (AddressEditor.postal_element_props[i], addr_editor.entries[i].get_text ());
-
-      var details = new PostalAddressFieldDetails(new_value, row_entry.value.details.parameters);
-      new_details.add (details);
-    }
-    var new_value = Value (new_details.get_type ());
-    new_value.set_object (new_details);
-    return new_value;
-  }
-
-  void set_field_changed (int row) {
-    foreach (var fields in writable_personas.values) {
-      foreach (var entry in fields.entries) {
-       if (row in entry.value.rows.keys) {
-         if (entry.value.changed)
-           return;
-
-         entry.value.changed = true;
-         return;
-       }
-      }
-    }
-  }
-
-  new void remove_row (int row) {
-    foreach (var fields in writable_personas.values) {
-      foreach (var field_entry in fields.entries) {
-       foreach (var idx in field_entry.value.rows.keys) {
-         if (idx == row) {
-           var child = container_grid.get_child_at (0, row);
-           child.destroy ();
-           child = container_grid.get_child_at (1, row);
-           child.destroy ();
-           child = container_grid.get_child_at (2, row);
-           child.destroy ();
-
-           field_entry.value.changed = true;
-           field_entry.value.rows.unset (row);
-           return;
-         }
-       }
-      }
-    }
-  }
-
-  void attach_row_with_entry (int row, TypeSet type_set, AbstractFieldDetails details, string value, string? 
type = null) {
-    var combo = new TypeCombo (type_set);
-    combo.set_hexpand (false);
-    combo.set_active_from_field_details (details);
-    if (type != null)
-      combo.set_active_from_vcard_type (type);
-    combo.set_valign (Align.CENTER);
-    container_grid.attach (combo, 0, row, 1, 1);
-
-    var value_entry = new Entry ();
-    value_entry.set_text (value);
-    value_entry.set_hexpand (true);
-    container_grid.attach (value_entry, 1, row, 1, 1);
-
-    if (type_set == TypeSet.email) {
-      value_entry.placeholder_text = _("Add email");
-    } else if (type_set == TypeSet.phone) {
-      value_entry.placeholder_text = _("Add number");
-    }
-
-    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    container_grid.attach (delete_button, 2, row, 1, 1);
-
-    /* Notify change to upper layer */
-    combo.changed.connect ((c) => {
-       set_field_changed (get_current_row (combo));
-      });
-    value_entry.changed.connect (() => {
-       set_field_changed (get_current_row (value_entry));
-      });
-    delete_button.clicked.connect (() => {
-       remove_row (get_current_row (delete_button));
-      });
-
-    if (value == "")
-      focus_widget = value_entry;
-  }
-
-  void attach_row_with_entry_labeled (string title, AbstractFieldDetails? details, string value, int row) {
-    var title_label = new Label (title);
-    title_label.set_hexpand (false);
-    title_label.set_halign (Align.START);
-    title_label.margin_end = 6;
-    container_grid.attach (title_label, 0, row, 1, 1);
-
-    var value_entry = new Entry ();
-    value_entry.set_text (value);
-    value_entry.set_hexpand (true);
-    container_grid.attach (value_entry, 1, row, 1, 1);
-
-    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    container_grid.attach (delete_button, 2, row, 1, 1);
-
-    /* Notify change to upper layer */
-    value_entry.changed.connect (() => {
-       set_field_changed (get_current_row (value_entry));
-      });
-    delete_button.clicked.connect_after (() => {
-       remove_row (get_current_row (delete_button));
-      });
-
-    if (value == "")
-      focus_widget = value_entry;
-  }
-
-  void attach_row_with_text_labeled (string title, AbstractFieldDetails? details, string value, int row) {
-    var title_label = new Label (title);
-    title_label.set_hexpand (false);
-    title_label.set_halign (Align.START);
-    title_label.set_valign (Align.START);
-    title_label.margin_top = 3;
-    title_label.margin_end = 6;
-    container_grid.attach (title_label, 0, row, 1, 1);
-
-    var sw = new ScrolledWindow (null, null);
-    sw.set_shadow_type (ShadowType.OUT);
-    sw.set_size_request (-1, 100);
-    var value_text = new TextView ();
-    value_text.get_buffer ().set_text (value);
-    value_text.set_hexpand (true);
-    sw.add (value_text);
-    container_grid.attach (sw, 1, row, 1, 1);
-
-    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    delete_button.set_valign (Align.START);
-    container_grid.attach (delete_button, 2, row, 1, 1);
-
-    /* Notify change to upper layer */
-    value_text.get_buffer ().changed.connect (() => {
-       set_field_changed (get_current_row (sw));
-      });
-    delete_button.clicked.connect (() => {
-       remove_row (get_current_row (delete_button));
-       /* eventually will need to check against the details type */
-       has_notes_row = false;
-      });
-
-    if (value == "")
-      focus_widget = value_text;
-  }
-
-  delegate void AdjustingDateFn();
-
-  void attach_row_for_birthday (string title, AbstractFieldDetails? details, DateTime birthday, int row) {
-    var title_label = new Label (title);
-    title_label.set_hexpand (false);
-    title_label.set_halign (Align.START);
-    title_label.margin_end = 6;
-    container_grid.attach (title_label, 0, row, 1, 1);
-
-    var box = new Grid ();
-    box.set_column_spacing (12);
-    var day_spin = new SpinButton.with_range (1.0, 31.0, 1.0);
-    day_spin.set_digits (0);
-    day_spin.numeric = true;
-    day_spin.set_value ((double)birthday.to_local ().get_day_of_month ());
-
-    var month_combo = new ComboBoxText ();
-    var january = new DateTime.local (1, 1, 1, 1, 1, 1);
-    for (int i = 0; i < 12; i++) {
-        var month = january.add_months (i);
-        month_combo.append_text (month.format ("%B"));
-    }
-    month_combo.set_active (birthday.to_local ().get_month () - 1);
-    month_combo.hexpand = true;
-
-    var year_spin = new SpinButton.with_range (1800, 3000, 1);
-    year_spin.set_digits (0);
-    year_spin.numeric = true;
-    year_spin.set_value ((double)birthday.to_local ().get_year ());
-
-    box.add (day_spin);
-    box.add (month_combo);
-    box.add (year_spin);
-
-    container_grid.attach (box, 1, row, 1, 1);
-
-    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    container_grid.attach (delete_button, 2, row, 1, 1);
-
-    AdjustingDateFn fn = () => {
-      int[] month_of_31 = {3, 5, 8, 10};
-      if (month_combo.get_active () in month_of_31) {
-        day_spin.set_range (1, 30);
-      } else if (month_combo.get_active () == 1) {
-        if (year_spin.get_value_as_int () % 4 == 0 &&
-            year_spin.get_value_as_int () % 100 != 0) {
-          day_spin.set_range (1, 29);
-        } else {
-          day_spin.set_range (1, 28);
-        }
-      }
-    };
-
-    /* Notify change to upper layer */
-    day_spin.changed.connect (() => {
-        set_field_changed (get_current_row (day_spin));
-      });
-    month_combo.changed.connect (() => {
-        set_field_changed (get_current_row (month_combo));
-
-        /* adjusting day_spin value using selected month constraints*/
-        fn ();
-      });
-    year_spin.changed.connect (() => {
-        set_field_changed (get_current_row (year_spin));
-
-        fn ();
-      });
-    delete_button.clicked.connect (() => {
-        remove_row (get_current_row (delete_button));
-        has_birthday_row = false;
-      });
-  }
-
-  void attach_row_for_address (int row, TypeSet type_set, PostalAddressFieldDetails details, string? type = 
null) {
-    var combo = new TypeCombo (type_set);
-    combo.set_hexpand (false);
-    combo.set_active_from_field_details (details);
-    if (type != null)
-      combo.set_active_from_vcard_type (type);
-    container_grid.attach (combo, 0, row, 1, 1);
-
-    var value_address = new AddressEditor (details);
-    container_grid.attach (value_address, 1, row, 1, 1);
-
-    var delete_button = new Button.from_icon_name ("user-trash-symbolic", IconSize.MENU);
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    delete_button.set_valign (Align.START);
-    container_grid.attach (delete_button, 2, row, 1, 1);
-
-    /* Notify change to upper layer */
-    combo.changed.connect (() => {
-       set_field_changed (get_current_row (combo));
-      });
-    value_address.changed.connect (() => {
-       set_field_changed (get_current_row (value_address));
-      });
-    delete_button.clicked.connect (() => {
-       remove_row (get_current_row (delete_button));
-      });
-
-    focus_widget = value_address;
-  }
-
-  void add_edit_row (Persona? p, string prop_name, ref int row, bool add_empty = false, string? type = null) 
{
-    /* Here, we will need to add manually every type of field,
-     * we're planning to allow editing on */
-    string persona_uid = p != null ? p.uid : "null-persona.hack";
-    switch (prop_name) {
-    case "email-addresses":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var detail_field = new EmailFieldDetails ("");
-       attach_row_with_entry (row, TypeSet.email, detail_field, "", type);
-       rows.set (row, { detail_field });
-       row++;
-      } else {
-       var details = p as EmailDetails;
-       if (details != null) {
-         var emails = Contacts.Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
-         foreach (var email in emails) {
-           attach_row_with_entry (row, TypeSet.email, email, email.value);
-           rows.set (row, { email });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    case "phone-numbers":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var detail_field = new PhoneFieldDetails ("");
-       attach_row_with_entry (row, TypeSet.phone, detail_field, "", type);
-       rows.set (row, { detail_field });
-       row++;
-      } else {
-       var details = p as PhoneDetails;
-       if (details != null) {
-         var phones = Contacts.Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
-         foreach (var phone in phones) {
-           attach_row_with_entry (row, TypeSet.phone, phone, phone.value, type);
-           rows.set (row, { phone });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    case "urls":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var detail_field = new UrlFieldDetails ("");
-       attach_row_with_entry_labeled (_("Website"), detail_field, "", row);
-       rows.set (row, { detail_field });
-       row++;
-      } else {
-       var url_details = p as UrlDetails;
-       if (url_details != null) {
-         foreach (var url in url_details.urls) {
-           attach_row_with_entry_labeled (_("Website"), url, url.value, row);
-           rows.set (row, { url });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    case "nickname":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       attach_row_with_entry_labeled (_("Nickname"), null, "", row);
-       rows.set (row, { null });
-       row++;
-      } else {
-       var name_details = p as NameDetails;
-       if (name_details != null) {
-         if (is_set (name_details.nickname)) {
-           attach_row_with_entry_labeled (_("Nickname"), null, name_details.nickname, row);
-           rows.set (row, { null });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       has_nickname_row = true;
-       var delete_button = container_grid.get_child_at (2, row - 1) as Button;
-       delete_button.clicked.connect (() => {
-           has_nickname_row = false;
-         });
-
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    case "birthday":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var today = new DateTime.now_local ();
-       attach_row_for_birthday (_("Birthday"), null, today, row);
-       rows.set (row, { null });
-       row++;
-      } else {
-       var birthday_details = p as BirthdayDetails;
-       if (birthday_details != null) {
-         if (birthday_details.birthday != null) {
-           attach_row_for_birthday (_("Birthday"), null, birthday_details.birthday, row);
-           rows.set (row, { null });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       has_birthday_row = true;
-       writable_personas[persona_uid].set (prop_name, { add_empty, rows });
-      }
-      break;
-    case "notes":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var detail_field = new NoteFieldDetails ("");
-       attach_row_with_text_labeled (_("Note"), detail_field, "", row);
-       rows.set (row, { detail_field });
-       row++;
-      } else {
-       var note_details = p as NoteDetails;
-       if (note_details != null || add_empty) {
-         foreach (var note in note_details.notes) {
-           attach_row_with_text_labeled (_("Note"), note, note.value, row);
-           rows.set (row, { note });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       has_notes_row = true;
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    case "postal-addresses":
-      var rows = new HashMap<int, RowData?> ();
-      if (add_empty) {
-       var detail_field = new PostalAddressFieldDetails (
-                             new PostalAddress (null,
-                                               null,
-                                               null,
-                                               null,
-                                               null,
-                                               null,
-                                               null,
-                                               null,
-                                               null));
-       attach_row_for_address (row, TypeSet.general, detail_field, type);
-       rows.set (row, { detail_field });
-       row++;
-      } else {
-       var address_details = p as PostalAddressDetails;
-       if (address_details != null) {
-         foreach (var addr in address_details.postal_addresses) {
-           attach_row_for_address (row, TypeSet.general, addr, type);
-           rows.set (row, { addr });
-           row++;
-         }
-       }
-      }
-      if (! rows.is_empty) {
-       if (writable_personas[persona_uid].has_key (prop_name)) {
-         foreach (var entry in rows.entries) {
-           writable_personas[persona_uid][prop_name].rows.set (entry.key, entry.value);
-         }
-       } else {
-         writable_personas[persona_uid].set (prop_name, { false, rows });
-       }
-      }
-      break;
-    }
-  }
-
-  int get_current_row (Widget child) {
-    int row;
-
-    container_grid.child_get (child, "top-attach", out row);
-    return row;
-  }
-
-  void insert_row_at (int idx) {
-    foreach (var field_maps in writable_personas.values) {
-      foreach (var field in field_maps.values) {
-       foreach (var row in field.rows.keys) {
-         if (row >= idx) {
-           var new_rows = new HashMap <int, RowData?> ();
-           foreach (var old_row in field.rows.keys) {
-             /* move all rows +1 */
-             new_rows.set (old_row + 1, field.rows[old_row]);
-           }
-           field.rows = new_rows;
-           break;
-         }
-       }
-      }
-    }
-    foreach (var entry in writable_personas.entries) {
-      foreach (var field_entry in entry.value.entries) {
-       foreach (var row in field_entry.value.rows.keys) {
-         if (row >= idx) {
-           var new_rows = new HashMap <int, RowData?> ();
-           foreach (var old_row in field_entry.value.rows.keys) {
-             new_rows.set (old_row + 1, field_entry.value.rows[old_row]);
-           }
-           field_entry.value.rows = new_rows;
-           break;
-         }
-       }
-      }
-    }
-    container_grid.insert_row (idx);
-  }
-
-  private void on_container_grid_size_allocate (Allocation alloc) {
-    if (this.focus_widget != null && this.focus_widget is Widget) {
-      this.focus_widget.grab_focus ();
-      this.focus_widget = null;
-    }
-  }
-
-  public HashMap<string, PropertyData?> properties_changed () {
-    var props_set = new HashMap<string, PropertyData?> ();
-
-    foreach (var entry in writable_personas.entries) {
-      foreach (var field_entry in entry.value.entries) {
-       if (field_entry.value.changed && !props_set.has_key (field_entry.key)) {
-         PropertyData p = PropertyData ();
-         p.persona = null;
-         if (individual != null) {
-           p.persona = Contacts.Utils.find_persona_from_uid (individual, entry.key);
-         }
-
-         switch (field_entry.key) {
-           case "email-addresses":
-             p.value = get_value_from_emails (field_entry.value.rows);
-             break;
-           case "phone-numbers":
-             p.value = get_value_from_phones (field_entry.value.rows);
-             break;
-           case "urls":
-             p.value = get_value_from_urls (field_entry.value.rows);
-             break;
-           case "nickname":
-             p.value = get_value_from_nickname (field_entry.value.rows);
-             break;
-           case "birthday":
-             p.value = get_value_from_birthday (field_entry.value.rows);
-             break;
-           case "notes":
-             p.value = get_value_from_notes (field_entry.value.rows);
-             break;
-            case "postal-addresses":
-             p.value = get_value_from_addresses (field_entry.value.rows);
-             break;
-         }
-
-         props_set.set (field_entry.key, p);
-       }
-      }
-    }
-
-    return props_set;
-  }
-
-  public HashSet<Persona> get_unlink_personas () {
-    return unlink_personas;
-  }
-
-  public void add_new_row_for_property (Persona? persona, string prop_name, string? type = null) {
-    int next_idx = 0;
-    foreach (var fields in writable_personas.values) {
-      if (fields.has_key (prop_name)) {
-         foreach (var idx in fields[prop_name].rows.keys) {
-           if (idx < last_row)
-             next_idx = idx > next_idx ? idx : next_idx;
-         }
-         break;
-      }
-    }
-    next_idx = (next_idx == 0 ? last_row : next_idx) + 1;
-    insert_row_at (next_idx);
-    add_edit_row (persona, prop_name, ref next_idx, true, type);
-    last_row++;
-    container_grid.show_all ();
+    show_all ();
   }
 
   // Creates the contact's current avatar in a big button on top of the Editor
-  private void create_avatar_button () {
+  private Widget create_avatar_button () {
     this.avatar = new Avatar (PROFILE_SIZE, this.individual);
 
     var button = new Button ();
@@ -905,65 +53,36 @@ public class Contacts.ContactEditor : ContactForm {
     button.image = this.avatar;
     button.clicked.connect (on_avatar_button_clicked);
 
-    this.container_grid.attach (button, 0, 0, 1, 3);
+    return button;
   }
 
   // Show the avatar popover when the avatar is clicked
   private void on_avatar_button_clicked (Button avatar_button) {
-    var popover = new AvatarSelector (avatar_button, this.individual);
-    popover.set_avatar.connect ( (icon) =>  {
-        this.avatar.set_data ("value", icon);
-        this.avatar.set_data ("changed", true);
-
-        Gdk.Pixbuf? a_pixbuf = null;
-        try {
-          var stream = (icon as LoadableIcon).load (PROFILE_SIZE, null);
-          a_pixbuf = new Gdk.Pixbuf.from_stream_at_scale (stream, PROFILE_SIZE, PROFILE_SIZE, true);
-        } catch {
-        }
-
-        this.avatar.set_pixbuf (a_pixbuf);
-      });
-    popover.show();
-  }
-
-  public bool avatar_changed () {
-    return this.avatar.get_data<bool> ("changed");
-  }
-
-  public Value get_avatar_value () {
-    GLib.Icon icon = this.avatar.get_data<GLib.Icon> ("value");
-    Value v = Value (icon.get_type ());
-    v.set_object (icon);
-    return v;
+    if (this.avatar_selector == null)
+      this.avatar_selector = new AvatarSelector (avatar_button, this.individual);
+    this.avatar_selector.show();
   }
 
   // Creates the big name entry on the top
-  private void create_name_entry () {
+  private Widget create_name_entry () {
+    NameDetails name = this.individual as NameDetails;
     this.name_entry = new Entry ();
     this.name_entry.hexpand = true;
     this.name_entry.valign = Align.CENTER;
     this.name_entry.placeholder_text = _("Add name");
-    this.name_entry.set_data ("changed", false);
 
-    if (this.individual != null)
-        this.name_entry.text = this.individual.display_name;
+    // Get primary persona from this.individual
+    this.name_entry.text = name.full_name;
 
-    /* structured name change */
     this.name_entry.changed.connect (() => {
-        this.name_entry.set_data ("changed", true);
-      });
-
-    this.container_grid.attach (this.name_entry, 1, 0, 2, 3);
-  }
-
-  public bool name_changed () {
-    return this.name_entry.get_data<bool> ("changed");
-  }
+      foreach (var p in this.individual.personas) {
+        var name_p = p as NameDetails;
+        if (name_p != null) {
+          name_p.full_name = this.name_entry.get_text ();
+        }
+      }
+    });
 
-  public Value get_full_name_value () {
-    Value v = Value (typeof (string));
-    v.set_string (this.name_entry.get_text ());
-    return v;
+    return this.name_entry;
   }
 }
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index adaafeb..44db168 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -27,13 +27,16 @@ const int PROFILE_SIZE = 128;
  * and a ContactEditor to edit contact information.
  */
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-pane.ui")]
-public class Contacts.ContactPane : Stack {
+public class Contacts.ContactPane : ScrolledWindow {
 
   private Window parent_window;
 
   private Store store;
 
-  public Individual? individual = null;
+  public Individual? individual { get; set; default = null; }
+
+  [GtkChild]
+  private Stack stack;
 
   [GtkChild]
   private Grid none_selected_page;
@@ -46,27 +49,11 @@ public class Contacts.ContactPane : Stack {
   private Box contact_editor_page;
   private ContactEditor? editor = null;
 
-  private SimpleActionGroup edit_contact_actions = new SimpleActionGroup ();
-  private const GLib.ActionEntry[] action_entries = {
-    { "add.email-addresses.home", on_add_detail },
-    { "add.email-addresses.work", on_add_detail },
-    { "add.phone-numbers.cell", on_add_detail },
-    { "add.phone-numbers.home", on_add_detail },
-    { "add.phone-numbers.work", on_add_detail },
-    { "add.urls", on_add_detail },
-    { "add.nickname", on_add_detail },
-    { "add.birthday", on_add_detail },
-    { "add.postal-addresses.home", on_add_detail },
-    { "add.postal-addresses.work", on_add_detail },
-    { "add.notes", on_add_detail },
-  };
-
   public bool on_edit_mode = false;
   private LinkSuggestionGrid? suggestion_grid = null;
 
   /* Signals */
   public signal void contacts_linked (string? main_contact, string linked_contact, LinkOperation operation);
-  public signal void will_delete (Individual individual);
   /**
    * Passes the changed display name to all listeners after edit mode has been completed.
    */
@@ -76,8 +63,6 @@ public class Contacts.ContactPane : Stack {
   public ContactPane (Window parent_window, Store contacts_store) {
     this.parent_window = parent_window;
     this.store = contacts_store;
-
-    this.edit_contact_actions.add_action_entries (action_entries, this);
   }
 
   public void add_suggestion (Individual i) {
@@ -115,7 +100,7 @@ public class Contacts.ContactPane : Stack {
       show_contact_sheet ();
     } else {
       remove_contact_sheet ();
-      set_visible_child (this.none_selected_page);
+      this.stack.set_visible_child (this.none_selected_page);
     }
   }
 
@@ -125,7 +110,7 @@ public class Contacts.ContactPane : Stack {
     remove_contact_sheet();
     this.sheet = new ContactSheet (this.individual, this.store);
     this.contact_sheet_page.add (this.sheet);
-    set_visible_child (this.contact_sheet_page);
+    this.stack.set_visible_child (this.contact_sheet_page);
 
     var matches = this.store.aggregator.get_potential_matches (this.individual, MatchResult.HIGH);
     foreach (var i in matches.keys) {
@@ -149,26 +134,9 @@ public class Contacts.ContactPane : Stack {
   }
 
   private void create_contact_editor () {
-    if (this.editor != null)
-      remove_contact_editor ();
-
-    this.editor = new ContactEditor (this.individual, this.store, this.edit_contact_actions);
-
-    this.editor.linked_button.clicked.connect (linked_accounts);
-    this.editor.remove_button.clicked.connect (delete_contact);
-
-    /* enable/disable actions*/
-    var birthday_action = this.edit_contact_actions.lookup_action ("add.birthday") as SimpleAction;
-    this.editor.bind_property ("has-birthday-row", birthday_action, "enabled",
-                               BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
-
-    var nickname_action = this.edit_contact_actions.lookup_action ("add.nickname") as SimpleAction;
-    this.editor.bind_property ("has-nickname-row", nickname_action, "enabled",
-                               BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
+    remove_contact_editor ();
 
-    var notes_action = this.edit_contact_actions.lookup_action ("add.notes") as SimpleAction;
-    this.editor.bind_property ("has-notes-row", notes_action, "enabled",
-                               BindingFlags.SYNC_CREATE | BindingFlags.INVERT_BOOLEAN);
+    this.editor = new ContactEditor (this.individual, store.aggregator);
 
     this.contact_editor_page.add (this.editor);
   }
@@ -181,137 +149,90 @@ public class Contacts.ContactPane : Stack {
     this.editor = null;
   }
 
-  void on_add_detail (GLib.SimpleAction action, GLib.Variant? parameter) {
-    var tok = action.name.split (".");
-
-    if (tok[0] == "add") {
-      editor.add_new_row_for_property (Contacts.Utils.find_primary_persona (individual),
-                                      tok[1],
-                                      tok.length > 2 ? tok[2].up () : null);
-    }
-  }
-
-  private void linked_accounts () {
-    var dialog = new LinkedPersonasDialog (this.parent_window, this.store, individual);
-    if (dialog.run () == ResponseType.CLOSE && dialog.any_unlinked) {
-      /* update edited contact if any_unlinked */
-      stop_editing ();
-      start_editing ();
-    }
-    dialog.destroy ();
-  }
-
-  void delete_contact () {
-    if (individual != null) {
-      will_delete (individual);
-    }
-  }
-
-  public void start_editing() {
+  private void start_editing() {
     if (this.on_edit_mode || this.individual == null)
       return;
 
     this.on_edit_mode = true;
 
-    remove_contact_sheet ();
     create_contact_editor ();
-    set_visible_child (this.contact_editor_page);
+    this.stack.set_visible_child (this.contact_editor_page);
   }
 
-  public void stop_editing (bool drop_changes = false) {
+  public void stop_editing (bool cancel = false) {
     if (!this.on_edit_mode)
       return;
 
     this.on_edit_mode = false;
-    /* saving changes */
-    if (!drop_changes)
-      save_editor_changes.begin ();
-
     remove_contact_editor ();
 
-    if (this.individual != null)
-      show_contact_sheet ();
-    else
-      set_visible_child (this.none_selected_page);
-  }
-
-  private async void save_editor_changes () {
-    foreach (var prop in this.editor.properties_changed ().entries) {
-      try {
-        yield Contacts.Utils.set_persona_property (prop.value.persona, prop.key, prop.value.value);
-      } catch (Error e) {
-        show_message (e.message);
+    if (cancel) {
+      var fake_individual = individual as FakeIndividual;
+      if (fake_individual != null && fake_individual.real_individual != null) {
+        // Reset individual on to the real one
+        this.individual = fake_individual.real_individual;
+        this.stack.set_visible_child (this.contact_sheet_page);
+      } else {
+        this.stack.set_visible_child (this.none_selected_page);
       }
+      return;
     }
 
-    if (this.editor.name_changed ()) {
-      var v = this.editor.get_full_name_value ();
-      try {
-        yield Contacts.Utils.set_individual_property (individual, "full-name", v);
-        display_name_changed (v.get_string ());
-      } catch (Error e) {
-        show_message (e.message);
-      }
-    }
+    /* Save changes if editing wasn't canceled */
+    apply_changes.begin ();
+  }
 
-    if (this.editor.avatar_changed ()) {
-      var v = this.editor.get_avatar_value ();
-      try {
-        yield Contacts.Utils.set_individual_property (individual, "avatar", v);
-      } catch (Error e) {
-        show_message (e.message);
-      }
+  private async void apply_changes () {
+    /* Show fake contact to the user */
+    /* TODO: block changes to fake contact */
+    show_contact_sheet ();
+    var fake_individual = individual as FakeIndividual;
+    if (fake_individual != null && fake_individual.real_individual == null) {
+      // Create a new persona in the primary store based on the fake persona
+      yield create_contact (fake_individual.primary_persona);
+    } else {
+      yield fake_individual.apply_changes_to_real ();
+      /* Todo: we need to check if the changes where applied to the contact */
+      this.individual = fake_individual.real_individual;
     }
 
-    /* unlink personas */
-    if (this.editor.get_unlink_personas ().size > 0) {
-      var operation = new UnLinkOperation (this.store);
-      operation.do.begin (this.individual, this.editor.get_unlink_personas ());
-    }
+    /* Replace fake contact with real contact */
+    show_contact_sheet ();
   }
 
-  public void new_contact () {
-    this.on_edit_mode = true;
-    this.individual = null;
-    remove_contact_sheet ();
-    create_contact_editor ();
-    set_visible_child (this.contact_editor_page);
+  public void edit_contact () {
+    this.individual = new FakeIndividual.from_real (this.individual);
+    start_editing ();
   }
 
-  // Creates a new contact from the details in the ContactEditor
-  public async void create_contact () {
+  public void new_contact () {
     var details = new HashTable<string, Value?> (str_hash, str_equal);
+    string[] writeable_properties;
+    // TODO: make sure we have a primary_store
+    if (this.store.aggregator.primary_store != null) {
+      // FIXME: We shouldn't use this list but there isn't an other way to find writeable_properties, and we 
should expect that all properties are writeable
+      writeable_properties = this.store.aggregator.primary_store.always_writeable_properties;
+    } else {
+      writeable_properties = {};
+    }
 
-    // Collect the details from the editor
-    if (editor.name_changed ())
-      details["full-name"] = this.editor.get_full_name_value ();
-
-    if (editor.avatar_changed ())
-      details["avatar"] = this.editor.get_avatar_value ();
-
-    foreach (var prop in this.editor.properties_changed ().entries)
-      details[prop.key] = prop.value.value;
+    var fake_persona = new FakePersona (FakePersonaStore.the_store(), writeable_properties, details);
+    var fake_personas = new HashSet<FakePersona> ();
+    fake_personas.add (fake_persona);
+    this.individual = new FakeIndividual(fake_personas);
 
-    // Leave edit mode
-    stop_editing (true);
+    start_editing ();
+  }
 
-    if (details.size () == 0) {
-      show_message_dialog (_("You need to enter some data"));
-      return;
-    }
+  // Create a new contact from the FakePersona
+  public async void create_contact (FakePersona fake_persona) {
+    var details = fake_persona.get_details ();
 
     if (this.store.aggregator.primary_store == null) {
       show_message_dialog (_("No primary addressbook configured"));
       return;
     }
 
-    // Create a FakeContact temporary persona so we can show it already to the user
-    var fake_persona = new FakePersona (FakePersonaStore.the_store(), details);
-    var fake_personas = new HashSet<Persona> ();
-    fake_personas.add (fake_persona);
-    var fake_individual = new Individual(fake_personas);
-    this.parent_window.set_shown_contact (fake_individual);
-
     // Create the contact
     var primary_store = this.store.aggregator.primary_store;
     Persona? persona = null;
@@ -325,6 +246,7 @@ public class Contacts.ContactPane : Stack {
 
     // Now show the real persona to the user
     var individual = persona.individual;
+
     if (individual != null) {
       //FIXME: This causes a flicker, especially visibile when a avatar is set
       this.parent_window.set_shown_contact (individual);
@@ -345,12 +267,6 @@ public class Contacts.ContactPane : Stack {
     dialog.destroy ();
   }
 
-  private void show_message (string message) {
-    var notification = new InAppNotification (message);
-    notification.show ();
-    this.parent_window.add_notification (notification);
-  }
-
   private void remove_suggestion_grid () {
     if (this.suggestion_grid == null)
       return;
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index 0f6ebe8..b6c2249 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -24,33 +24,61 @@ using Gee;
  *
  * (Note: to edit a contact, use the {@link ContactEditor} instead.
  */
-public class Contacts.ContactSheet : ContactForm {
+public class Contacts.ContactSheet : Grid {
+  private int last_row = 0;
+  private Individual individual;
+  public bool narrow { get; set; default = true; }
+
+  private const string[] SORTED_PROPERTIES = {
+    "email-addresses",
+    "phone-numbers",
+    "im-addresses",
+    "urls",
+    "nickname",
+    "birthday",
+    "postal-addresses",
+    "notes"
+  };
+
   public ContactSheet (Individual individual, Store store) {
+    Object (row_spacing: 12, column_spacing: 12);
     this.individual = individual;
-    this.store = store;
 
     this.individual.notify.connect (update);
     this.individual.personas_changed.connect (update);
-    this.store.quiescent.connect (update);
+    store.quiescent.connect (update);
 
     update ();
   }
 
+  private Label create_persona_store_label (Persona p) {
+    var store_name = new Label (Utils.format_persona_store_name_for_contact (p));
+    var attrList = new Pango.AttrList ();
+    attrList.insert (Pango.attr_weight_new (Pango.Weight.BOLD));
+    store_name.set_attributes (attrList);
+    store_name.set_halign (Align.START);
+    store_name.set_ellipsize (Pango.EllipsizeMode.MIDDLE);
+
+    return store_name;
+  }
+
   private Button create_button (string icon) {
     var button = new Button.from_icon_name (icon, IconSize.BUTTON);
     button.set_halign (Align.END);
-    button.get_style_context ().add_class ("contacts-flatten");
+    button.get_style_context ().add_class ("flatten");
 
     return button;
   }
 
-  void add_row_with_label (string label_value, string value, Widget? buttons = null) {
+  void add_row_with_label (string label_value, string value, Widget? btn1 = null, Widget? btn2 =null) {
+    if (value == "" || value == null)
+      return;
     var type_label = new Label (label_value);
     type_label.xalign = 1.0f;
     type_label.set_halign (Align.END);
     type_label.set_valign (Align.CENTER);
     type_label.get_style_context ().add_class ("dim-label");
-    this.container_grid.attach (type_label, 0, this.last_row, 1, 1);
+    this.attach (type_label, 0, this.last_row, 1, 1);
 
     var value_label = new Label (value);
     value_label.set_line_wrap (true);
@@ -60,47 +88,52 @@ public class Contacts.ContactSheet : ContactForm {
     value_label.wrap_mode = Pango.WrapMode.CHAR;
     value_label.set_selectable (true);
 
-    if (buttons != null) {
+    if (btn1 != null || btn2 !=null) {
       var value_box = new Box(Orientation.HORIZONTAL, 12);
       value_box.pack_start(value_label, false, false, 0);
-      value_box.pack_end(buttons, false, false, 0);
-      this.container_grid.attach (value_box, 1, this.last_row, 1, 1);
+
+      if (btn1 != null)
+        value_box.pack_end(btn1, false, false, 0);
+      if (btn2 != null)
+        value_box.pack_end(btn2, false, false, 0);
+      this.attach (value_box, 1, this.last_row, 1, 1);
     } else {
-      this.container_grid.attach (value_label, 1, this.last_row, 1, 1);
+      this.attach (value_label, 1, this.last_row, 1, 1);
     }
     this.last_row++;
   }
 
   private void update () {
     this.last_row = 0;
-    this.container_grid.foreach ((child) => this.container_grid.remove (child));
+    this.foreach ((child) => this.remove (child));
 
     var image_frame = new Avatar (PROFILE_SIZE, this.individual);
     image_frame.set_vexpand (false);
     image_frame.set_valign (Align.START);
-    this.container_grid.attach (image_frame,  0, 0, 1, 3);
+
+    this.attach (image_frame,  0, 0, 1, 3);
 
     create_name_label ();
 
     this.last_row += 3; // Name/Avatar takes up 3 rows
 
-    var personas = Contacts.Utils.get_personas_for_display (this.individual);
+    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);
       int persona_store_pos = this.last_row;
       if (!is_first_persona) {
-        this.container_grid.attach (create_persona_store_label (p), 0, this.last_row, 3);
+        this.attach (create_persona_store_label (p), 0, this.last_row, 3);
         this.last_row++;
       }
 
-      foreach (var prop in ContactForm.SORTED_PROPERTIES)
+      foreach (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) {
-        this.container_grid.remove_row (persona_store_pos);
+        this.remove_row (persona_store_pos);
         this.last_row--;
       }
     }
@@ -119,7 +152,7 @@ public class Contacts.ContactSheet : ContactForm {
     name_label.ellipsize = Pango.EllipsizeMode.END;
     name_label.xalign = 0f;
     name_label.selectable = true;
-    this.container_grid.attach (name_label,  1, 0, 1, 3);
+    this.attach (name_label,  1, 0, 1, 3);
     update_name_label (name_label);
     this.individual.notify["display-name"].connect ((obj, spec) => {
       update_name_label (name_label);
@@ -161,7 +194,7 @@ public class Contacts.ContactSheet : ContactForm {
   private void add_emails (Persona persona) {
     var details = persona as EmailDetails;
     if (details != null) {
-      var emails = Contacts.Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
+      var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
       foreach (var email in emails) {
         var button = create_button ("mail-unread-symbolic");
         button.clicked.connect (() => {
@@ -175,7 +208,7 @@ public class Contacts.ContactSheet : ContactForm {
   private void add_phone_nrs (Persona persona) {
     var phone_details = persona as PhoneDetails;
     if (phone_details != null) {
-      var phones = Contacts.Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
+      var phones = Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
       foreach (var phone in phones) {
 #if HAVE_TELEPATHY
         if (this.store.caller_account != null) {
@@ -204,7 +237,7 @@ public class Contacts.ContactSheet : ContactForm {
           if (persona is Tpf.Persona) {
             var button = create_button ("user-available-symbolic");
             button.clicked.connect (() => {
-              var im_persona = Contacts.Utils.find_im_persona (individual, protocol, id.value);
+              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 &&
@@ -280,7 +313,7 @@ public class Contacts.ContactSheet : ContactForm {
     var addr_details = persona as PostalAddressDetails;
     if (addr_details != null) {
       foreach (var addr in addr_details.postal_addresses) {
-        var all_strs = string.joinv ("\n", Contacts.Utils.format_address (addr.value));
+        var all_strs = string.joinv ("\n", Utils.format_address (addr.value));
         add_row_with_label (TypeSet.general.format_type (addr), all_strs);
       }
     }
diff --git a/src/contacts-editor-persona.vala b/src/contacts-editor-persona.vala
new file mode 100644
index 0000000..14deec0
--- /dev/null
+++ b/src/contacts-editor-persona.vala
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ * Author: Julian Sparber <julian sparber puri sm>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Gtk;
+using Folks;
+using Gee;
+
+/**
+ * A widget representing a persona in the {@link ContactEditor}.
+ */
+public class Contacts.EditorPersona : Box {
+  private const GLib.ActionEntry[] action_entries = {
+    { "change-addressbook", change_addressbook },
+  };
+
+  // List of important properties and a list of secoundary properties
+  private const string[] PROPERTIES = {
+    "email-addresses",
+    "phone-numbers"
+  };
+  private const string[] OTHER_PROPERTIES = {
+    "im-addresses",
+    "urls",
+    "nickname",
+    "birthday",
+    "postal-addresses",
+    "notes"
+  };
+
+  private Persona persona;
+  private Box header;
+  private ListBox content;
+
+  private IndividualAggregator aggregator;
+
+  construct {
+    this.header = new Box (Orientation.HORIZONTAL, 0);
+    add (this.header);
+
+    var frame = new Frame (null);
+    this.content = new ListBox ();
+    this.content.set_header_func (list_box_update_header_func);
+    frame.add (this.content);
+    add (frame);
+
+    SimpleActionGroup actions = new SimpleActionGroup ();
+    actions.add_action_entries (action_entries, this);
+    this.insert_action_group ("persona", actions);
+  }
+
+  private void list_box_update_header_func (ListBoxRow row, ListBoxRow? before) {
+    if (before == null) {
+      row.set_header (null);
+      return;
+    }
+
+    if (row.get_header () == null) {
+      var header = new Separator (Orientation.HORIZONTAL);
+      header.show ();
+      row.set_header (header);
+    }
+  }
+
+  public EditorPersona (Persona persona, IndividualAggregator aggregator) {
+    Object (orientation: Orientation.VERTICAL, spacing: 6);
+    this.persona = persona;
+    this.aggregator = aggregator;
+    create_label ();
+    /* TODO: implement the possibility of changing the addressbook of a persona
+    create_button (); */
+
+    // Add most important properites
+    foreach (var property in PROPERTIES) {
+      debug ("Create property entry for %s", property);
+      var rows = new EditorProperty (persona, property);
+      foreach (var row in rows) {
+        row.show_with_animation (false);
+        connect_row (row);
+        this.content.add (row);
+      }
+    }
+    // Add a row with a button to show all properties
+    ListBoxRow show_all_row = new ListBoxRow ();
+    show_all_row.set_selectable (false);
+    // Add less important property when the show_more button is clicked
+    this.content.row_activated.connect ((current_row) => {
+      if (current_row == show_all_row) {
+        foreach (var property in OTHER_PROPERTIES) {
+          debug ("Create property entry for %s", property);
+          var rows = new EditorProperty (persona, property);
+          foreach (var row in rows) {
+            connect_row (row);
+            this.content.add (row);
+            row.show_with_animation ();
+          }
+        }
+        show_all_row.destroy ();
+      }
+    });
+    Image show_all = new Image.from_icon_name ("view-more-symbolic", IconSize.BUTTON);
+    show_all.margin = 12;
+    show_all_row.add (show_all);
+    this.content.add (show_all_row);
+  }
+
+  private void connect_row (EditorPropertyRow row) {
+    row.notify["is-empty"].connect ( () => {
+      var empty_rows_count = this.count_empty_rows (row.ptype);
+      if (row.is_empty) {
+        // destroy all rows of our type which is not us
+        this.destroy_empty_rows (row, row.ptype);
+      }
+      if (!row.is_empty && empty_rows_count == 0) {
+        // We are sure that we only created one new row
+        var new_rows = new EditorProperty (persona, row.ptype, true);
+        if (new_rows.size > 0) {
+          this.content.insert (new_rows[0], row.get_index () + 1);
+          connect_row (new_rows[0]);
+          new_rows[0].show_with_animation ();
+        } else {
+          debug ("Couldn't add new row with type %s", row.ptype);
+        }
+      }
+    });
+  }
+
+  private uint count_empty_rows (string type) {
+    uint count = 0;
+    foreach (var row in this.content.get_children ()) {
+      var prop = (row as EditorPropertyRow);
+      if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
+        count++;
+      }
+    }
+    return count;
+  }
+
+  private void destroy_empty_rows (ListBoxRow current_row, string type) {
+    foreach (var row in this.content.get_children ()) {
+      if (current_row != row) {
+        var prop = (row as EditorPropertyRow);
+        if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
+          prop.remove ();
+        }
+      }
+    }
+  }
+
+  private void change_addressbook () {
+    /* Not yet implemented */
+  }
+
+  private void create_label () {
+    string title = "";
+    FakePersona fake_persona = this.persona as FakePersona;
+    if (fake_persona != null && fake_persona.real_persona != null) {
+      title = fake_persona.real_persona.store.display_name;
+    } else {
+      title = this.aggregator.primary_store.display_name;
+    }
+
+    Label addressbook = new Label (title);
+    this.header.pack_start (addressbook, false, false, 0);
+  }
+
+  private void create_button () {
+    var image = new Image.from_icon_name ("emblem-system-symbolic", IconSize.BUTTON);
+    var button = new MenuButton ();
+    button.set_image (image);
+    var builder = new Builder.from_resource ("/org/gnome/Contacts/ui/contacts-editor-menu.ui");
+    var menu = builder.get_object ("editor_menu") as Widget;
+    button.set_popover (menu);
+    this.header.pack_end (button, false, false, 0);
+  }
+}
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
new file mode 100644
index 0000000..00dfe4a
--- /dev/null
+++ b/src/contacts-editor-property.vala
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ * Author: Julian Sparber <julian sparber puri sm>
+ *
+ * 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
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Gtk;
+using Folks;
+using Gee;
+
+
+public class Contacts.BirthdayEditor : Hdy.Dialog {
+  private SpinButton day_spin;
+  private ComboBoxText month_combo;
+  private SpinButton year_spin;
+  public bool is_set { get; set; default = false; }
+
+  public signal void changed ();
+  delegate void AdjustingDateFn ();
+
+  public DateTime get_birthday () {
+    return new DateTime.local (year_spin.get_value_as_int (),
+    month_combo.get_active () + 1,
+    day_spin.get_value_as_int (),
+    0, 0, 0).to_utc ();
+  }
+
+  public BirthdayEditor (Window window, DateTime birthday) {
+    Object (transient_for: window, use_header_bar: 1);
+    day_spin = new SpinButton.with_range (1.0, 31.0, 1.0);
+    day_spin.set_digits (0);
+    day_spin.numeric = true;
+    day_spin.set_value ((double)birthday.to_local ().get_day_of_month ());
+
+    month_combo = new ComboBoxText ();
+    var january = new DateTime.local (1, 1, 1, 1, 1, 1);
+    for (int i = 0; i < 12; i++) {
+      var month = january.add_months (i);
+      month_combo.append_text (month.format ("%B"));
+    }
+    month_combo.set_active (birthday.to_local ().get_month () - 1);
+    month_combo.hexpand = true;
+
+    year_spin = new SpinButton.with_range (1800, 3000, 1);
+    year_spin.set_digits (0);
+    year_spin.numeric = true;
+    year_spin.set_value ((double)birthday.to_local ().get_year ());
+
+    // Create grid and labels
+    Box box = new Box (Orientation.VERTICAL, 12);
+    Grid grid = new Grid ();
+    grid.set_column_spacing (12);
+    grid.set_row_spacing (12);
+    Label day = new Label(_("Day"));
+    day.set_halign (Align.END);
+    grid.attach (day, 0, 0);
+    grid.attach (day_spin, 1, 0);
+    Label month = new Label(_("Month"));
+    month.set_halign (Align.END);
+    grid.attach (month, 0, 1);
+    grid.attach (month_combo, 1, 1);
+    Label year = new Label(_("Year"));
+    year.set_halign (Align.END);
+    grid.attach (year, 0, 2);
+    grid.attach (year_spin, 1, 2);
+    box.pack_start (grid);
+
+    var content = this.get_content_area ();
+    content.set_valign (Align.CENTER);
+    content.add (box);
+
+    this.title = _("Change Address Book");
+    add_buttons (_("Set"), ResponseType.OK,
+                      _("Cancel"), ResponseType.CANCEL,
+                      null);
+    var ok_button = this.get_widget_for_response (ResponseType.OK);
+    ok_button.get_style_context ().add_class ("suggested-action");
+    this.response.connect ((id) => {
+      switch (id) {
+        case ResponseType.OK:
+          this.is_set = true;
+          changed ();
+          break;
+        case ResponseType.CANCEL:
+          break;
+      }
+      this.destroy ();
+    });
+
+    box.margin = 12;
+    box.show_all ();
+
+    AdjustingDateFn fn = () => {
+      int[] month_of_31 = {3, 5, 8, 10};
+      if (month_combo.get_active () in month_of_31) {
+        day_spin.set_range (1, 30);
+      } else if (month_combo.get_active () == 1) {
+        if (year_spin.get_value_as_int () % 4 == 0 &&
+            year_spin.get_value_as_int () % 100 != 0) {
+          day_spin.set_range (1, 29);
+        } else {
+          day_spin.set_range (1, 28);
+        }
+      }
+    };
+
+    /* adjusting day_spin value using selected month/year constraints*/
+    fn ();
+
+    month_combo.changed.connect (() => {
+      /* adjusting day_spin value using selected month constraints*/
+      fn ();
+    });
+    year_spin.value_changed.connect (() => {
+      /* adjusting day_spin value using selected year constraints*/
+      fn ();
+    });
+  }
+}
+
+public class Contacts.AddressEditor : Box {
+  private Entry? entries[7];  /* must be the number of elements in postal_element_props */
+
+  private const string[] postal_element_props = {"street", "extension", "locality", "region", "postal_code", 
"po_box", "country"};
+  private static string[] postal_element_names = {_("Street"), _("Extension"), _("City"), 
_("State/Province"), _("Zip/Postal Code"), _("PO box"), _("Country")};
+
+  public signal void changed ();
+
+  public AddressEditor (PostalAddressFieldDetails details) {
+    set_hexpand (true);
+    set_orientation (Orientation.VERTICAL);
+
+    for (int i = 0; i < entries.length; i++) {
+      string postal_part;
+      details.value.get (AddressEditor.postal_element_props[i], out postal_part);
+
+      entries[i] = new Entry ();
+      entries[i].set_hexpand (true);
+      entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
+
+      if (postal_part != null)
+        entries[i].set_text (postal_part);
+
+      entries[i].get_style_context ().add_class ("contacts-postal-entry");
+      add (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 ());
+        changed ();
+      });
+    }
+  }
+
+  public bool is_empty () {
+    foreach (var entry in entries) {
+      if (entry.get_text () != "") {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  public override void grab_focus () {
+    entries[0].grab_focus ();
+  }
+}
+
+public class Contacts.EditorPropertyRow : ListBoxRow {
+  public bool is_empty { get; set; default = true; }
+  public bool is_removed { get; set; default = false; }
+  public string ptype { get; private set; }
+  public Box container;
+  public Box header;
+  public Revealer revealer;
+
+  construct {
+    this.revealer = new Revealer ();
+    //TODO: bind orientation property to available space
+    var box = new Box (Orientation.VERTICAL, 6);
+    box.set_valign (Align.START);
+    box.set_can_focus (false);
+    this.container = new Box (Orientation.HORIZONTAL, 6);
+    this.container.set_can_focus (false);
+    this.header = new Box (Orientation.HORIZONTAL, 6);
+    this.header.set_can_focus (false);
+    box.pack_start (this.header);
+    box.pack_end (this.container);
+    this.set_activatable (false);
+    this.set_selectable (false);
+    this.set_can_focus (false);
+    box.margin = 12;
+    this.revealer.add (box);
+    add (this.revealer);
+    this.get_style_context ().add_class ("editor-property-row");
+    this.revealer.bind_property ("reveal-child", this, "is-removed", BindingFlags.INVERT_BOOLEAN);
+  }
+
+  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 seperator 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 ();
+    });
+  }
+
+  public void show_with_animation (bool animate = true) {
+    if (!animate) {
+      var duration = this.revealer.get_transition_duration ();
+      this.revealer.set_reveal_child (true);
+      this.revealer.set_transition_duration (duration);
+      this.show_all ();
+    } else {
+      this.show_all ();
+      this.revealer.set_reveal_child (true);
+    }
+  }
+
+  public void add_base_label (string label) {
+    var title_label = new Label (label);
+    title_label.set_hexpand (false);
+    title_label.set_halign (Align.START);
+    title_label.margin_end = 6;
+    this.header.pack_start (title_label);
+  }
+
+  public void add_base_combo (Set<AbstractFieldDetails> set, string label, TypeSet combo_type, 
AbstractFieldDetails details) {
+    var title_label = new Label (label);
+    title_label.set_halign (Align.START);
+    this.header.pack_start (title_label);
+    TypeCombo combo = new TypeCombo (combo_type);
+    combo.set_hexpand (false);
+    combo.set_active_from_field_details (details);
+    this.header.pack_start (combo);
+
+    combo.changed.connect (() => {
+      combo.active_descriptor.save_to_field_details(details);
+      // Workaround: we shouldn't do a manual signal
+      (set as FakeHashSet).changed ();
+      debug ("Property phone changed");
+    });
+  }
+
+  //FIXME: create only one add_base_entry
+  public void add_base_entry_email (Set<AbstractFieldDetails> set,
+                                    EmailFieldDetails details,
+                                    string placeholder) {
+    var value_entry = new Entry ();
+    value_entry.set_input_purpose (InputPurpose.EMAIL);
+    value_entry.placeholder_text = placeholder;
+    value_entry.set_text (details.value);
+    value_entry.set_hexpand (true);
+    this.container.pack_start (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
+      (set as FakeHashSet).changed ();
+      debug ("Property email changed");
+      this.is_empty = value_entry.get_text () == "";
+    });
+  }
+
+  public void add_base_entry_phone (Set<AbstractFieldDetails> set,
+                                    PhoneFieldDetails details,
+                                    string placeholder) {
+    var value_entry = new Entry ();
+    value_entry.set_input_purpose (InputPurpose.PHONE);
+    value_entry.placeholder_text = placeholder;
+    value_entry.set_text (details.value);
+    value_entry.set_hexpand (true);
+    this.container.pack_start (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
+      (set as FakeHashSet).changed ();
+      debug ("Property type changed");
+
+      this.is_empty = value_entry.get_text () == "";
+    });
+  }
+
+  public void add_base_entry_url (Set<AbstractFieldDetails> set,
+                                  UrlFieldDetails details,
+                                  string placeholder) {
+    var value_entry = new Entry ();
+    value_entry.placeholder_text = placeholder;
+    value_entry.set_input_purpose (InputPurpose.URL);
+    value_entry.set_text (details.value);
+    value_entry.set_hexpand (true);
+    this.container.pack_start (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
+      (set as FakeHashSet).changed ();
+      debug ("Property type changed");
+
+      this.is_empty = value_entry.get_text () == "";
+    });
+  }
+
+  public void add_base_delete (Set<AbstractFieldDetails> set, AbstractFieldDetails details) {
+    var delete_button = new Button.from_icon_name ("user-trash-symbolic");
+    delete_button.get_accessible ().set_name (_("Delete field"));
+    delete_button.set_valign (Align.START);
+    this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
+    this.container.pack_end (delete_button, false);
+
+
+    delete_button.clicked.connect (() => {
+      debug ("Property removed");
+      this.remove ();
+      set.remove (details);
+    });
+  }
+}
+
+/**
+ * A widget representing a property of a persona in the editor {@link Contact}.
+ * We can have more then one property in one properity e.g. Emails therefore we need to return a List
+ */
+public class Contacts.EditorProperty : 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) {
+      if (s == property_name) {
+        this.writeable = true;
+        break;
+      }
+    }
+
+    create_for_property (persona, property_name, only_new);
+  }
+
+  void create_for_property (Persona p, string prop_name, bool only_new) {
+    switch (prop_name) {
+      case "email-addresses":
+        var details = p as EmailDetails;
+        if (details != null) {
+          var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
+          if (!only_new)
+            foreach (var email in emails) {
+              add (create_for_email (details.email_addresses, email));
+            }
+          if (this.writeable)
+            add (create_for_email (details.email_addresses));
+        }
+        break;
+      case "phone-numbers":
+        var details = p as PhoneDetails;
+        if (details != null) {
+          var phones = Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
+          if (!only_new)
+            foreach (var phone in phones) {
+              add (create_for_phone (details.phone_numbers, phone));
+            }
+          if (this.writeable)
+            add (create_for_phone (details.phone_numbers));
+        }
+        break;
+      case "urls":
+        var details = p as UrlDetails;
+        if (details != null) {
+          var urls = Utils.sort_fields<UrlFieldDetails>(details.urls);
+          if (!only_new)
+            foreach (var url in urls) {
+              add (create_for_url (details.urls, url));
+            }
+          add (create_for_url (details.urls));
+        }
+        break;
+      case "nickname":
+        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;
+        if (birthday_details != null && !only_new) {
+          add (create_for_birthday (birthday_details));
+        }
+        break;
+      case "notes":
+        var note_details = p as NoteDetails;
+        if (note_details != null) {
+          if (!only_new)
+            foreach (var note in note_details.notes) {
+              add (create_for_note (note_details.notes, note));
+            }
+          if (this.writeable)
+            add (create_for_note (note_details.notes));
+        }
+        break;
+      case "postal-addresses":
+        var address_details = p as PostalAddressDetails;
+        if (address_details != null) {
+          if (!only_new)
+            foreach (var addr in address_details.postal_addresses) {
+              add (create_for_address (address_details.postal_addresses, addr));
+            }
+          if (this.writeable)
+            add (create_for_address (address_details.postal_addresses));
+        }
+        break;
+    }
+  }
+
+  private EditorPropertyRow create_for_email (Set<AbstractFieldDetails> set, EmailFieldDetails? details = 
null) {
+    if (details == null) {
+      var parameters = new HashMultiMap<string, string> ();
+      parameters["type"] = "PERSONAL";
+      var new_details = new EmailFieldDetails ("", parameters);
+      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);
+
+    box.sensitive = this.writeable;
+    return box;
+  }
+
+  private EditorPropertyRow create_for_phone (Set<AbstractFieldDetails> set, PhoneFieldDetails? details = 
null) {
+    if (details == null) {
+      var parameters = new HashMultiMap<string, string> ();
+      parameters["type"] = "CELL";
+      var new_details = new PhoneFieldDetails ("", parameters);
+      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;
+    return box;
+  }
+
+  // TODO: add support for different types of urls
+  private EditorPropertyRow create_for_url (Set<AbstractFieldDetails> set, UrlFieldDetails? details = null) {
+    if (details == null) {
+      var parameters = new HashMultiMap<string, string> ();
+      parameters["type"] = "PERSONAL";
+      var new_details = new UrlFieldDetails ("", parameters);
+      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://exmaple.com";));
+    box.add_base_delete (set, details);
+
+    box.sensitive = this.writeable;
+    return box;
+  }
+
+  private EditorPropertyRow create_for_nick (NameDetails details) {
+    var box = new EditorPropertyRow ("nickname");
+    box.add_base_label (_("Nickname"));
+
+    var value_entry = new Entry ();
+    value_entry.set_text (details.nickname);
+    value_entry.set_hexpand (true);
+    box.container.pack_start (value_entry);
+
+    value_entry.changed.connect (() => {
+      details.nickname = value_entry.get_text ();
+      debug ("Nickname changed");
+      box.is_empty = value_entry.get_text () == "";
+    });
+
+    box.sensitive = this.writeable;
+    return box;
+  }
+
+  // TODO: support different types of nodes
+  private EditorPropertyRow create_for_note (Set<NoteFieldDetails> set, NoteFieldDetails? details = null) {
+    if (details == null) {
+      var parameters = new HashMultiMap<string, string> ();
+      parameters["type"] = "PERSONAL";
+      var new_details = new NoteFieldDetails ("", parameters);
+      set.add(new_details);
+      details = new_details;
+    }
+    var box = new EditorPropertyRow ("notes");
+    box.add_base_label (_("Note"));
+
+    var sw = new ScrolledWindow (null, null);
+    sw.set_shadow_type (ShadowType.OUT);
+    sw.set_size_request (-1, 100);
+    var value_text = new TextView ();
+    value_text.get_buffer ().set_text (details.value);
+    value_text.set_hexpand (true);
+    sw.add (value_text);
+    box.container.pack_start (sw);
+
+    box.add_base_delete (set, details);
+
+    value_text.get_buffer ().changed.connect (() => {
+      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);
+      // Workaround: we shouldn't do a manual signal
+      (set as FakeHashSet).changed ();
+      debug ("Property changed");
+      box.is_empty = details.value == "";
+    });
+
+    box.sensitive = this.writeable;
+    return box;
+  }
+
+  private EditorPropertyRow create_for_birthday (BirthdayDetails? details) {
+    DateTime date;
+    if (details.birthday == null) {
+      date = new DateTime.now ();
+    } else {
+      date = details.birthday;
+    }
+
+    var box = new EditorPropertyRow ("birthday");
+    box.add_base_label (_("Birthday"));
+
+    var button = new Button.with_label (_("Set Birthday"));
+    box.container.pack_start (button);
+
+    button.clicked.connect (() => {
+      Window parent_window = button.get_toplevel () as Window;
+      if (parent_window != null) {
+        var dialog = new BirthdayEditor (parent_window, date);
+
+        dialog.changed.connect (() => {
+          if (dialog.is_set) {
+            details.birthday = dialog.get_birthday ();
+            button.set_label (details.birthday.to_local ().format ("%x"));
+            box.is_empty = false;
+          }
+        });
+        dialog.show_all ();
+      }
+    });
+
+    box.is_empty = details.birthday == null;
+
+    var delete_button = new Button.from_icon_name ("user-trash-symbolic");
+    delete_button.get_accessible ().set_name (_("Delete field"));
+    delete_button.set_valign (Align.START);
+    box.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
+    box.container.pack_end (delete_button, false);
+
+    delete_button.clicked.connect (() => {
+      debug ("Birthday removed");
+      details.birthday = null;
+      box.is_empty = true;
+      button.set_label (_("Set Birthday"));
+    });
+
+    box.sensitive = this.writeable;
+    return box;
+  }
+
+  private EditorPropertyRow create_for_address (Set<PostalAddressFieldDetails> set,
+                                                PostalAddressFieldDetails? details = null) {
+    if (details == null) {
+      var parameters = new HashMultiMap<string, string> ();
+      parameters["type"] = "HOME";
+      var address = new PostalAddress(null, null, null, null, null, null, null, null, null);
+      var new_details = new PostalAddressFieldDetails (address, parameters);
+      set.add(new_details);
+      details = new_details;
+    }
+    var box = new EditorPropertyRow ("postal-addresses");
+    box.add_base_combo (set, _("Address"), TypeSet.general, details);
+
+    var value_address = new AddressEditor (details);
+    box.container.pack_start (value_address);
+
+    box.add_base_delete (set, details);
+
+    value_address.changed.connect (() => {
+      // Workaround: we shouldn't do a manual signal
+      (set as FakeHashSet).changed ();
+      debug ("Address changed");
+      box.is_empty = value_address.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 471a16c..5b8b50e 100644
--- a/src/contacts-fake-persona-store.vala
+++ b/src/contacts-fake-persona-store.vala
@@ -41,13 +41,13 @@ public class Contacts.FakePersonaStore : PersonaStore {
   }
 
   public override Map<string, Persona> personas {
-      get { return this._personas_ro; }
+    get { return this._personas_ro; }
   }
 
   public override MaybeBool can_add_personas { get { return MaybeBool.FALSE; } }
   public override MaybeBool can_alias_personas { get { return MaybeBool.FALSE; } }
   public override MaybeBool can_group_personas { get { return MaybeBool.FALSE; } }
-  public override MaybeBool can_remove_personas { get { return MaybeBool.FALSE; } }
+  public override MaybeBool can_remove_personas { get { return MaybeBool.TRUE; } }
   public override bool is_prepared  { get { return true; } }
   public override bool is_quiescent  { get { return true; } }
   private string[] _always_writeable_properties = {};
@@ -68,193 +68,521 @@ public class Contacts.FakePersonaStore : PersonaStore {
  * The FakePersona is used as a placeholder till we get the real persona from folks
  * It needs to implement all Details we support so that we don't loise any information
  */
+const string BACKEND_NAME = "fake-store";
+
 public class Contacts.FakePersona : Persona,
-  AvatarDetails,
-  BirthdayDetails,
-  EmailDetails,
-  ImDetails,
-  NameDetails,
-  NoteDetails,
-  PhoneDetails,
-  UrlDetails,
-  PostalAddressDetails
+AvatarDetails,
+BirthdayDetails,
+EmailDetails,
+ImDetails,
+NameDetails,
+NoteDetails,
+PhoneDetails,
+UrlDetails,
+PostalAddressDetails
 {
   private HashTable<string, Value?> properties;
   // Keep track of the persona in the actual store
-  private weak Persona real_persona { get; set; default = null; }
+  public weak Persona real_persona { get; set; default = null; }
 
+  private string[] _writeable_properties = {};
   private const string[] _linkable_properties = {};
-  private const string[] _writeable_properties = {};
   public override string[] linkable_properties {
     get { return _linkable_properties; }
   }
 
   public override string[] writeable_properties {
-    get { return _writeable_properties; }
+    get {
+      return this._writeable_properties;
+    }
   }
 
-  [CCode (notify = false)]
-  public LoadableIcon? avatar
-  {
-    get { unowned Value? value = this.properties.get ("avatar");
-      if (value == null)
-        return null;
+  private ArrayList<string> _changed_properties;
+
+  construct {
+    this._changed_properties = new ArrayList<string> ();
+  }
+
+  public LoadableIcon? avatar {
+    get {
+      unowned Value? value = this.properties.get ("avatar");
       return (LoadableIcon?) value;
     }
-    set {}
+    set {
+      this.properties.set ("avatar", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public string full_name
-  {
-    get { unowned Value? value = this.properties.get ("full-name");
+  public async void change_avatar (LoadableIcon? avatar) throws PropertyError {
+    this.avatar = avatar;
+  }
+
+  public string full_name {
+    get {
+      unowned Value? value = this.properties.get ("full-name");
       if (value == null)
         return "";
-      return value.get_string (); }
-    set {}
+      return value.get_string ();
+    }
+    set {
+      this.properties.set ("full-name", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public string nickname
-  {
-    get { unowned Value? value = this.properties.get ("nickname");
+  public string nickname {
+    get {
+      unowned Value? value = this.properties.get ("nickname");
       if (value == null)
         return "";
-      return value.get_string (); }
-    set {}
+      return value.get_string ();
+    }
+    set {
+      this.properties.set ("nickname", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public StructuredName? structured_name
-  {
+  //TODO: implement structured_name
+  public StructuredName? structured_name {
     get { return null; }
     set {}
   }
 
-  [CCode (notify = false)]
-  public Set<PhoneFieldDetails> phone_numbers
-  {
-    get { unowned Value? value = this.properties.get ("phone-numbers");
+  public Set<PhoneFieldDetails> phone_numbers {
+    get {
+      unowned Value? value = this.properties.get ("phone-numbers");
       if (value == null) {
         var new_value = Value (typeof (Set));
-        new_value.set_object (new HashSet<PhoneFieldDetails> ());
+        var set = new FakeHashSet<PhoneFieldDetails> ();
+        new_value.set_object (set);
+        set.changed.connect (() => { notify_property ("phone-numbers"); });
         this.properties.set ("phone-numbers", new_value);
         value = this.properties.get ("phone-numbers");
       }
       return (Set<PhoneFieldDetails>) value;
     }
-
-    set {}
+    set {
+      this.properties.set ("phone-numbers", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public Set<UrlFieldDetails> urls
-  {
-    get { unowned Value? value = this.properties.get ("urls");
+  public Set<UrlFieldDetails> urls {
+    get { 
+      unowned Value? value = this.properties.get ("urls");
       if (value == null) {
         var new_value = Value (typeof (Set));
-        new_value.set_object (new HashSet<UrlFieldDetails> ());
+        var set = new FakeHashSet<UrlFieldDetails> ();
+        new_value.set_object (set);
+        set.changed.connect (() => { notify_property ("urls"); });
         this.properties.set ("urls", new_value);
-        value = this.properties.get ("urls");
+        value = new_value;
       }
       return (Set<UrlFieldDetails>) value;
     }
-
-    set {}
+    set {
+      this.properties.set ("urls", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public Set<PostalAddressFieldDetails> postal_addresses
-  {
-    get { unowned Value? value = this.properties.get ("urls");
+  public Set<PostalAddressFieldDetails> postal_addresses {
+    get {
+      unowned Value? value = this.properties.get ("postal-addresses");
       if (value == null) {
         var new_value = Value (typeof (Set));
-        new_value.set_object (new HashSet<PostalAddressFieldDetails> ());
-        this.properties.set ("urls", new_value);
+        var set = new FakeHashSet<PostalAddressFieldDetails> ();
+        new_value.set_object (set);
+        set.changed.connect (() => { notify_property ("postal-addresses"); });
+        this.properties.set ("postal-addresses", new_value);
         value = new_value;
       }
-
       return (Set<PostalAddressFieldDetails>) value;
     }
-
-    set {}
+    set {
+      this.properties.set ("postal-addresses", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public Set<NoteFieldDetails> notes
-  {
-    get { unowned Value? value = this.properties.get ("notes");
+  public Set<NoteFieldDetails> notes {
+    get {
+      unowned Value? value = this.properties.get ("notes");
       if (value == null) {
         var new_value = Value (typeof (Set));
-        new_value.set_object (new HashSet<NoteFieldDetails> ());
+        var set = new FakeHashSet<NoteFieldDetails> ();
+        new_value.set_object (set);
+        set.changed.connect (() => { notify_property ("notes"); });
         this.properties.set ("notes", new_value);
         value = new_value;
       }
       return (Set<NoteFieldDetails>) value;
     }
-
-    set {}
+    set {
+      this.properties.set ("notes", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public DateTime? birthday
-  {
+  public DateTime? birthday {
     get { unowned Value? value = this.properties.get ("birthday");
       if (value == null)
         return null;
       return (DateTime) value;
     }
-    set {}
+    set {
+      this.properties.set ("birthday", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public string? calendar_event_id
-  {
+  //TODO implement calender_event_id
+  public string? calendar_event_id {
     get { return null; }
     set {}
   }
 
-  [CCode (notify = false)]
-  public MultiMap<string,ImFieldDetails> im_addresses
-  {
-    get { unowned Value? value = this.properties.get ("im-addresses");
+  public MultiMap<string,ImFieldDetails> im_addresses {
+    get {
+      unowned Value? value = this.properties.get ("im-addresses");
       if (value == null) {
         var new_value = Value (typeof (MultiMap));
-        new_value.set_object (new HashMultiMap<string, ImFieldDetails> ());
+        var set = new FakeHashMultiMap<string, ImFieldDetails> ();
+        new_value.set_object (set);
         this.properties.set ("im-addresses", new_value);
+        set.changed.connect (() => { notify_property ("im-addresses"); });
         value = new_value;
       }
-
       return (MultiMap<string, ImFieldDetails>) value;
     }
-
-    set {}
+    set {
+      this.properties.set ("im-addresses", value);
+    }
   }
 
-  [CCode (notify = false)]
-  public Set<EmailFieldDetails> email_addresses
-  {
-    get { unowned Value? value = this.properties.get ("email-addresses");
+  public Set<EmailFieldDetails> email_addresses {
+    get {
+      unowned Value? value = this.properties.get ("email-addresses");
       if (value == null) {
         var new_value = Value (typeof (Set));
-        new_value.set_object (new HashSet<EmailFieldDetails> ());
+        var set = new FakeHashSet<EmailFieldDetails> ();
+        set.changed.connect (() => { notify_property ("email-addresses"); });
+        new_value.set_object (set);
         this.properties.set ("email-addresses", new_value);
         value = new_value;
       }
-
       return (Set<EmailFieldDetails>) value;
     }
-    set {}
+    set {
+      this.properties.set ("email-addresses", value);
+    }
   }
 
-  public FakePersona (PersonaStore store, HashTable<string, Value?> details) {
-    //TODO: use correct data to fill the object
-    Object (display_id: "display-id-fake-persona",
-            uid: "uid-fake-persona",
-            iid: "iid",
+  public FakePersona (PersonaStore store, string[] writeable_properties, HashTable<string, Value?> details) {
+    var id = Uuid.string_random();
+    var uid = Folks.Persona.build_uid (BACKEND_NAME, store.id, id);
+    var iid = "%s:%s".printf (store.id, id);
+    Object (display_id: iid,
+            uid: uid,
+            iid: iid,
             store: store,
             is_user: false);
 
     this.properties = details;
+    this._writeable_properties = writeable_properties;
+  }
+
+  public FakePersona.from_real (Persona persona) {
+    var details = new HashTable<string, Value?> (str_hash, str_equal);
+    this (FakePersonaStore.the_store (), persona.writeable_properties, details);
+    // FIXME: get all properties not only writable properties
+    var props = persona.writeable_properties;
+    foreach (var prop in props) {
+      get_property_from_real (persona, prop);
+    }
+
+    this.real_persona = persona;
+    // FIXME: we are adding property changes also for things we don't care about e.g. individual
+    this.notify.connect((obj, ps) => {
+      add_to_changed_properties(ps.name);
+    });
+  }
+
+  private void get_property_from_real (Persona persona, string property_name) {
+    // TODO Implement the interface for the commented properties
+    switch (property_name) {
+      case "alias":
+        //alias = (persona as AliasDetails).alias;
+        break;
+      case "avatar":
+        avatar = (persona as AvatarDetails).avatar;
+        break;
+      case "birthday":
+        birthday = (persona as BirthdayDetails).birthday;
+        break;
+      case "calendar-event-id":
+        calendar_event_id = (persona as BirthdayDetails).calendar_event_id;
+        break;
+      case "email-addresses":
+        foreach (var e in (persona as EmailDetails).email_addresses) {
+          email_addresses.add (new EmailFieldDetails (e.value, e.parameters));
+        }
+        break;
+      case "is-favourite":
+        //is_favourite = (persona as FavouriteDetails).is_favourite;
+        break;
+      case "gender":
+        //gender = (persona as GenderDetails).gender;
+        break;
+      case "groups":
+        //groups = (persona as GroupDetails).groups;
+        break;
+      case "im-addresses":
+        im_addresses = (persona as ImDetails).im_addresses;
+        break;
+      case "local-ids":
+        //local_ids = (persona as LocalIdDetails).local_ids;
+        break;
+      case "structured-name":
+        structured_name = (persona as NameDetails).structured_name;
+        break;
+      case "full-name":
+        full_name = (persona as NameDetails).full_name;
+        break;
+      case "nickname":
+        nickname = (persona as NameDetails).nickname;
+        break;
+      case "notes":
+        foreach (var e in (persona as NoteDetails).notes) {
+          notes.add (new NoteFieldDetails (e.value, e.parameters, e.id));
+        }
+        break;
+      case "phone-numbers":
+        foreach (var e in (persona as PhoneDetails).phone_numbers) {
+          phone_numbers.add (new PhoneFieldDetails (e.value, e.parameters));
+        }
+        break;
+      case "postal-addresses":
+        foreach (var e in (persona as PostalAddressDetails).postal_addresses) {
+          postal_addresses.add (new PostalAddressFieldDetails (e.value, e.parameters));
+        }
+        break;
+      case "roles":
+        //roles (persona as RoleDetails).roles;
+        break;
+      case "urls":
+        foreach (var e in (persona as UrlDetails).urls) {
+          urls.add (new UrlFieldDetails (e.value, e.parameters));
+        }
+        break;
+      case "web-service-addresses":
+        //web_service_addresses.add_all((persona as WebServiceDetails).web_service_addresses);
+        break;
+      default:
+        debug ("Unknown property '%s' in FakePersona.get_property_from_real().", property_name);
+        break;
+    }
+  }
+
+  private void add_to_changed_properties (string property_name) {
+    debug ("Property: %s was added to the changed property list", property_name);
+    if (!this._changed_properties.contains(property_name))
+      this._changed_properties.add (property_name);
+  }
+
+  public HashTable<string, Value?>  get_details () {
+    return this.properties;
+  }
+
+  public async void apply_changes_to_real () {
+    if (this.real_persona == null) {
+      warning ("No real persona to apply changes from fake persona");
+      return;
+    }
+    foreach (var prop in _changed_properties) {
+      if (properties.contains (prop)) {
+        try {
+        yield set_persona_property (this.real_persona, prop, properties.get (prop));
+        } catch (Error e) {
+          error ("Couldn't write property: %s", e.message);
+        }
+      }
+    }
+  }
+
+  private static async void set_persona_property (Persona persona,
+                                          string property_name, Value new_value) throws PropertyError, 
IndividualAggregatorError, PropertyError {
+    switch (property_name) {
+      case "alias":
+        yield (persona as AliasDetails).change_alias ((string) new_value);
+        break;
+      case "avatar":
+        yield (persona as AvatarDetails).change_avatar ((LoadableIcon?) new_value);
+        break;
+      case "birthday":
+        yield (persona as BirthdayDetails).change_birthday ((DateTime?) new_value);
+        break;
+      case "calendar-event-id":
+        yield (persona as BirthdayDetails).change_calendar_event_id ((string?) new_value);
+        break;
+      case "email-addresses":
+        var original = (Set<EmailFieldDetails>) new_value;
+        var copy = new HashSet<EmailFieldDetails> ();
+        foreach (var e in original) {
+          if (e.value != null && e.value != "")
+            copy.add (new EmailFieldDetails (e.value, e.parameters));
+        }
+        yield (persona as EmailDetails).change_email_addresses (copy);
+        break;
+      case "is-favourite":
+        yield (persona as FavouriteDetails).change_is_favourite ((bool) new_value);
+        break;
+      case "gender":
+        yield (persona as GenderDetails).change_gender ((Gender) new_value);
+        break;
+      case "groups":
+        yield (persona as GroupDetails).change_groups ((Set<string>) new_value);
+        break;
+      case "im-addresses":
+        yield (persona as ImDetails).change_im_addresses ((MultiMap<string, ImFieldDetails>) new_value);
+        break;
+      case "local-ids":
+        yield (persona as LocalIdDetails).change_local_ids ((Set<string>) new_value);
+        break;
+      case "structured-name":
+        yield (persona as NameDetails).change_structured_name ((StructuredName?) new_value);
+        break;
+      case "full-name":
+        yield (persona as NameDetails).change_full_name ((string) new_value);
+        break;
+      case "nickname":
+        yield (persona as NameDetails).change_nickname ((string) new_value);
+        break;
+      case "notes":
+        var original = (Set<NoteFieldDetails>) new_value;
+        var copy = new HashSet<NoteFieldDetails> ();
+        foreach (var e in original) {
+          if (e.value != null && e.value != "")
+            copy.add (new NoteFieldDetails (e.value, e.parameters));
+        }
+        yield (persona as NoteDetails).change_notes (copy);
+        break;
+      case "phone-numbers":
+        var original = (Set<PhoneFieldDetails>) new_value;
+        var copy = new HashSet<PhoneFieldDetails> ();
+        foreach (var e in original) {
+          if (e.value != null && e.value != "")
+            copy.add (new PhoneFieldDetails (e.value, e.parameters));
+        }
+        yield (persona as PhoneDetails).change_phone_numbers (copy);
+        break;
+      case "postal-addresses":
+        var original = (Set<PostalAddressFieldDetails>) new_value;
+        var copy = new HashSet<PostalAddressFieldDetails> ();
+        foreach (var e in original) {
+          // TODO: make sure that the Postal Address isn't empty
+          if (e.value != null)
+            copy.add (new PostalAddressFieldDetails (e.value, e.parameters));
+        }
+        yield (persona as PostalAddressDetails).change_postal_addresses (copy);
+        break;
+      case "roles":
+        yield (persona as RoleDetails).change_roles ((Set<RoleFieldDetails>) new_value);
+        break;
+      case "urls":
+        var original = (Set<UrlFieldDetails>) new_value;
+        var copy = new HashSet<UrlFieldDetails> ();
+        foreach (var e in original) {
+          if (e.value != null && e.value != "")
+            copy.add (new UrlFieldDetails (e.value, e.parameters));
+        }
+        yield (persona as UrlDetails).change_urls (copy);
+        break;
+      case "web-service-addresses":
+        yield (persona as WebServiceDetails).change_web_service_addresses ((MultiMap<string, 
WebServiceFieldDetails>) new_value);
+        break;
+      default:
+        critical ("Unknown property '%s' in Contact.set_persona_property().", property_name);
+        break;
+    }
+  }
+}
+
+/**
+ * A FakeIndividual
+ */
+public class Contacts.FakeIndividual : Individual {
+  public weak Individual real_individual { get; set; default = null; }
+  public weak FakePersona primary_persona { get; set; default = null; }
+  public FakeIndividual (Set<FakePersona>? personas) {
+    base (personas);
+    foreach (var p in personas) {
+      // Keep track of the main persona
+      if (Contacts.Utils.persona_is_main (p) || personas.size == 1)
+        primary_persona = p;
+    }
+  }
+
+  public FakeIndividual.from_real (Individual individual) {
+    var fake_personas = new HashSet<FakePersona> ();
+    foreach (var p in individual.personas) {
+      var fake_p = new FakePersona.from_real (p);
+      // Keep track of the main persona
+      if (Contacts.Utils.persona_is_main (p) || individual.personas.size == 1)
+        primary_persona = fake_p;
+      fake_personas.add (fake_p);
+    }
+    this (fake_personas);
+    this.real_individual = individual;
+  }
+
+  public async void apply_changes_to_real () {
+    if (this.real_individual == null) {
+      warning ("No real individual to apply changes from fake individual");
+      return;
+    }
+
+    foreach (var p in this.personas) {
+        var fake_persona = p as FakePersona;
+        if (fake_persona != null) {
+          yield fake_persona.apply_changes_to_real ();
+        }
+      }
+  }
+}
+
+/**
+ * This is the same as Gee.HashSet but adds a changed/added/removed signals
+ */
+public class Contacts.FakeHashSet<T> : Gee.HashSet<T> {
+  public signal void changed ();
+  public signal void added ();
+  public signal void removed ();
+
+  public FakeHashSet () {
+    base ();
+  }
+
+  public override bool add (T element) {
+    var res = base.add (element);
+    if (res) {
+      added();
+      changed ();
+    }
+    return res;
+  }
+
+  public override bool remove (T element) {
+    var res = base.remove (element);
+    if (res) {
+      removed();
+      changed ();
+    }
+    return res;
+  }
+}
+
+/**
+ * This is the same as Gee.HashMultiMap but adds a changed signal
+ */
+public class Contacts.FakeHashMultiMap<K, T> : Gee.HashMultiMap<K, T> {
+  public signal void changed ();
+
+  public FakeHashMultiMap () {
+    base ();
   }
 }
diff --git a/src/contacts-linking.vala b/src/contacts-linking.vala
index d5ddd2f..ba641c5 100644
--- a/src/contacts-linking.vala
+++ b/src/contacts-linking.vala
@@ -33,6 +33,7 @@ namespace Contacts {
 
     /* Link individuals */
     public async void do (LinkedList<Individual> individuals) {
+      print ("LINK\n");
       var personas_to_link = new HashSet<Persona> ();
       foreach (var i in individuals) {
         var saved_personas = new HashSet<Persona> ();
@@ -42,10 +43,11 @@ namespace Contacts {
         }
         this.personas_to_link.add (saved_personas);
       }
-
-      // We don't need to unlink the individuals because we are using every persona
-      yield link_personas(this.store, this.store.aggregator, personas_to_link);
-
+      try {
+        yield this.store.aggregator.link_personas (personas_to_link);
+      } catch (Error e) {
+        error ("Coulnd't link contacts: %s", e.message);
+      }
       finished = true;
     }
 
@@ -55,54 +57,44 @@ namespace Contacts {
         .first_match(() => {return true;}).individual;
       yield store.aggregator.unlink_individual (individual);
       foreach (var personas in personas_to_link) {
-        yield link_personas (this.store, this.store.aggregator, personas);
+        try {
+          yield this.store.aggregator.link_personas (personas);
+        } catch (Error e) {
+          error ("Coulnd't link contacts: %s", e.message);
+        }
       }
     }
   }
 
   public class UnLinkOperation : Object {
     private weak Store store;
+
+    private HashSet<Persona> personas;
+
     public UnLinkOperation(Store store) {
       this.store = store;
+      this.personas = new HashSet<Persona> ();
     }
 
     /* Remove a personas from individual */
-    public async void do (Individual main, Set<Persona> personas_to_remove) {
-      var personas_to_keep = new HashSet<Persona> ();
+    public async void do (Individual main) {
       foreach (var persona in main.personas)
-        if (!personas_to_remove.contains (persona))
-          personas_to_keep.add (persona);
+          personas.add (persona);
 
       try {
         yield store.aggregator.unlink_individual (main);
       } catch (Error e) {
-        debug ("Couldn't link personas");
+        error ("Coulnd't link contacts: %s", e.message);
       }
-      yield link_personas(this.store, this.store.aggregator, personas_to_keep);
     }
 
     /* Undo the unlinking */
     public async void undo () {
+      try {
+        yield this.store.aggregator.link_personas (personas);
+      } catch (Error e) {
+        error ("Coulnd't link contacts: %s", e.message);
+      }
     }
   }
-
- /* Workaround: link_personas creates a new persona in the primary-store,
-  * For some reason we can't change the primary-store directly,
-  * but we can change the gsettings property.
-  * Before linking we set the primary-store to be "key-file"
-  * that the linking persona isn't written to a real store
-  */
-  private async void link_personas (Store store, IndividualAggregator aggregator, Set<Persona> personas) {
-    var settings = new GLib.Settings ("org.freedesktop.folks");
-    var default_store = settings.get_string ("primary-store");
-    settings.set_string ("primary-store", "key-file:relationships.ini");
-    try {
-      yield aggregator.link_personas (personas);
-    } catch (Error e) {
-      debug ("%s", e.message);
-    }
-
-    // Rest primary-store
-    settings.set_string ("primary-store", default_store);
-  }
 }
diff --git a/src/contacts-store.vala b/src/contacts-store.vala
index 4f1568f..e232be2 100644
--- a/src/contacts-store.vala
+++ b/src/contacts-store.vala
@@ -101,8 +101,8 @@ public class Contacts.Store : GLib.Object {
   }
 
   public void add_no_suggest_link (Individual a, Individual b) {
-    var persona1 = Contacts.Utils.get_personas_for_display(a).to_array ()[0];
-    var persona2 = Contacts.Utils.get_personas_for_display(b).to_array ()[0];
+    var persona1 = a.personas.to_array ()[0];
+    var persona2 = b.personas.to_array ()[0];
     dont_suggest_link.set (persona1.uid, persona2.uid);
     write_dont_suggest_db ();
   }
@@ -148,6 +148,8 @@ public class Contacts.Store : GLib.Object {
       }
     }
 
+    debug ("Individuals changed: %d old, %d new", to_add.size, to_remove.size);
+
     // Add new individuals
     foreach (var i in to_add) {
       if (i.personas.size > 0)
@@ -173,7 +175,7 @@ public class Contacts.Store : GLib.Object {
         callback();
       });
       yield;
-      this.disconnect (signal_id);
+      disconnect (signal_id);
     }
 
     Individual? matched = null;
@@ -198,11 +200,11 @@ public class Contacts.Store : GLib.Object {
     try {
       yield account_manager.prepare_async (null);
 
-      account_manager.account_enabled.connect (this.check_account_caps);
-      account_manager.account_disabled.connect (this.check_account_caps);
+      account_manager.account_enabled.connect (check_account_caps);
+      account_manager.account_disabled.connect (check_account_caps);
 
       foreach (var account in account_manager.dup_valid_accounts ())
-        yield this.check_account_caps (account);
+        yield check_account_caps (account);
     } catch (GLib.Error e) {
       warning ("Unable to check accounts caps %s", e.message);
     }
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index f25c3a6..1e95fea 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -164,6 +164,18 @@ namespace Contacts.Utils {
     return stores;
   }
 
+  public PersonaStore[] get_eds_address_books_from_backend (BackendStore backend_store) {
+    PersonaStore[] stores = {};
+    foreach (var backend in backend_store.enabled_backends.values) {
+      foreach (var persona_store in backend.persona_stores.values) {
+        if (persona_store.type_id == "eds") {
+          stores += persona_store;
+        }
+      }
+    }
+    return stores;
+  }
+
   public PersonaStore? get_key_file_address_book (Store contacts_store) {
     foreach (var backend in contacts_store.backend_store.enabled_backends.values) {
       foreach (var persona_store in backend.persona_stores.values) {
@@ -186,6 +198,15 @@ namespace Contacts.Utils {
     dialog.destroy();
   }
 
+  public bool persona_is_main (Persona persona) {
+    var store = persona.store;
+    if (!store.is_primary_store)
+      return false;
+
+    // Mark google contacts not in "My Contacts" as non-main
+    return !persona_is_google_other (persona);
+  }
+
   public bool has_main_persona (Individual individual) {
     var result = false;
     foreach (var p in individual.personas) {
diff --git a/src/contacts-window.vala b/src/contacts-window.vala
index 0ea8f1a..e409478 100644
--- a/src/contacts-window.vala
+++ b/src/contacts-window.vala
@@ -22,6 +22,14 @@ using Folks;
 
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-window.ui")]
 public class Contacts.Window : Gtk.ApplicationWindow {
+
+  private const GLib.ActionEntry[] action_entries = {
+    { "edit-contact",     edit_contact     },
+    { "share-contact",    share_contact    },
+    { "unlink-contact",   unlink_contact   },
+    { "delete-contact",   delete_contact   }
+  };
+
   [GtkChild]
   private Leaflet header;
   [GtkChild]
@@ -41,8 +49,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   [GtkChild]
   private Overlay notification_overlay;
   [GtkChild]
-  private Button add_button;
-  [GtkChild]
   private Button select_cancel_button;
   [GtkChild]
   private MenuButton hamburger_menu_button;
@@ -51,10 +57,14 @@ public class Contacts.Window : Gtk.ApplicationWindow {
   [GtkChild]
   private ModelButton sort_on_surname_button;
   [GtkChild]
+  private MenuButton contact_menu_button;
+  [GtkChild]
   private ToggleButton favorite_button;
   private bool ignore_favorite_button_toggled;
   [GtkChild]
-  private Button edit_button;
+  private Button unlink_button;
+  [GtkChild]
+  private Button add_button;
   [GtkChild]
   private Button cancel_button;
   [GtkChild]
@@ -91,6 +101,10 @@ public class Contacts.Window : Gtk.ApplicationWindow {
       store: contacts_store
     );
 
+    SimpleActionGroup actions = new SimpleActionGroup ();
+    actions.add_action_entries (action_entries, this);
+    this.insert_action_group ("window", actions);
+
     this.settings = settings;
     this.sort_on_firstname_button.clicked.connect (() => {
       this.settings.sort_on_surname = false;
@@ -166,10 +180,6 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     this.contact_pane = new ContactPane (this, this.store);
     this.contact_pane.visible = true;
     this.contact_pane.hexpand = true;
-    this.contact_pane.will_delete.connect ( (individual) => {
-        this.list_pane.hide_contact (individual);
-        delete_contacts (new ArrayList<Individual>.wrap ({ individual }));
-     });
     this.contact_pane.contacts_linked.connect (contact_pane_contacts_linked_cb);
     this.contact_pane.display_name_changed.connect ((display_name) => {
       this.right_header.title = display_name;
@@ -213,9 +223,9 @@ public class Contacts.Window : Gtk.ApplicationWindow {
         = (this.state == UiState.NORMAL || this.state == UiState.SHOWING);
 
     // UI when showing a contact
-    this.edit_button.visible
-        = this.favorite_button.visible
-        = (this.state == UiState.SHOWING);
+    this.contact_menu_button.visible
+      = this.favorite_button.visible
+      = (this.state == UiState.SHOWING);
 
     // Selecting UI
     this.select_cancel_button.visible = (this.state == UiState.SELECTING);
@@ -247,8 +257,11 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     show_list_pane ();
   }
 
-  [GtkCallback]
-  private void on_edit_button_clicked () {
+  private void share_contact () {
+    debug ("Share isn't implemented, yet");
+  }
+
+  private void edit_contact () {
     if (this.contact_pane.individual == null)
       return;
 
@@ -256,7 +269,7 @@ public class Contacts.Window : Gtk.ApplicationWindow {
 
     var name = this.contact_pane.individual.display_name;
     this.right_header.title = _("Editing %s").printf (name);
-    this.contact_pane.start_editing ();
+    this.contact_pane.edit_contact ();
   }
 
   [GtkCallback]
@@ -264,26 +277,57 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     // Don't change the contact being favorite while switching between the two of them
     if (this.ignore_favorite_button_toggled)
       return;
+    if (this.contact_pane.individual == null)
+      return;
 
     var is_fav = this.contact_pane.individual.is_favourite;
     this.contact_pane.individual.is_favourite = !is_fav;
   }
 
-  private void stop_editing (bool drop_changes = false) {
-    if (this.state == UiState.CREATING) {
-      show_list_pane ();
+  private void unlink_contact () {
+    var individual = this.contact_pane.individual;
+    if (individual == null)
+      return;
+
+    set_shown_contact (null);
+    this.state = UiState.NORMAL;
+
+    var operation = new UnLinkOperation (this.store);
+    operation.do.begin (individual);
 
-      if (drop_changes) {
-        this.contact_pane.stop_editing (drop_changes);
-      } else {
-        this.contact_pane.create_contact.begin ();
+    var b = new Button.with_mnemonic (_("_Undo"));
+    var notification = new InAppNotification (_("Contacts unlinked"), b);
+
+    /* signal handlers */
+    b.clicked.connect ( () => {
+        /* here, we will link the thing in question */
+        operation.undo.begin ();
+        notification.dismiss ();
+      });
+
+    add_notification (notification);
+  }
+
+  private void delete_contact () {
+    var individual = this.contact_pane.individual;
+    if (individual == null)
+      return;
+
+    this.list_pane.hide_contact (individual);
+    delete_contacts (new ArrayList<Individual>.wrap ({ individual }));
+  }
+
+  private void stop_editing (bool cancel = false) {
+    if (this.state == UiState.CREATING) {
+      if (cancel) {
+        show_list_pane ();
       }
       this.state = UiState.NORMAL;
     } else {
       show_contact_pane ();
-      this.contact_pane.stop_editing (drop_changes);
       this.state = UiState.SHOWING;
     }
+    this.contact_pane.stop_editing (cancel);
 
     if (this.contact_pane.individual != null) {
       this.right_header.title = this.contact_pane.individual.display_name;
@@ -372,6 +416,13 @@ public class Contacts.Window : Gtk.ApplicationWindow {
     this.select_cancel_button.clicked.connect (() => { this.state = UiState.NORMAL; });
     this.done_button.clicked.connect (() => stop_editing ());
     this.cancel_button.clicked.connect (() => stop_editing (true));
+
+    this.contact_pane.notify["individual"].connect (() => {
+      var individual = this.contact_pane.individual;
+      if (individual == null)
+        return;
+      this.unlink_button.set_visible (individual.personas.size > 1);
+    });
   }
 
   [GtkCallback]
diff --git a/src/meson.build b/src/meson.build
index 2592e74..c663c61 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -74,22 +74,23 @@ libcontacts_dep = declare_dependency(
 
 # The gnome-contacts binary
 contacts_vala_sources = files(
+  'contacts-addressbook-list.vala',
   'contacts-accounts-list.vala',
   'contacts-app.vala',
   'contacts-avatar.vala',
   'contacts-avatar-selector.vala',
   'contacts-contact-editor.vala',
-  'contacts-contact-form.vala',
   'contacts-contact-list.vala',
   'contacts-contact-pane.vala',
   'contacts-contact-sheet.vala',
   'contacts-crop-cheese-dialog.vala',
+  'contacts-editor-persona.vala',
+  'contacts-editor-property.vala',
   'contacts-in-app-notification.vala',
   'contacts-link-suggestion-grid.vala',
   'contacts-linked-personas-dialog.vala',
   'contacts-linking.vala',
   'contacts-list-pane.vala',
-  'contacts-max-width-bin.vala',
   'contacts-settings.vala',
   'contacts-setup-window.vala',
   'contacts-type-combo.vala',



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