contacts: create or update contacts

Add ability to create a new contact with an unknow uri, or link it to an
existing contact.
This is presented in a popover, either from an history entry, or during
a call with an unknown URI.

Issue: #78236
Change-Id: I22fa416b9f5c7a6eceb6f2ea47bb30aa251cda54
diff --git a/src/CurrentCallVC.mm b/src/CurrentCallVC.mm
index 52b6481..1c37d9a 100644
--- a/src/CurrentCallVC.mm
+++ b/src/CurrentCallVC.mm
@@ -41,9 +41,11 @@
 #import <video/previewmanager.h>
 #import <video/renderer.h>
 #import <media/text.h>
+#import <person.h>
 
 #import "views/ITProgressIndicator.h"
 #import "views/CallView.h"
+#import "PersonLinkerVC.h"
 
 @interface RendererConnectionsHolder : NSObject
 
@@ -57,7 +59,7 @@
 
 @end
 
-@interface CurrentCallVC ()
+@interface CurrentCallVC () <NSPopoverDelegate, ContactLinkedDelegate>
 
 @property (unsafe_unretained) IBOutlet NSTextField *personLabel;
 @property (unsafe_unretained) IBOutlet NSTextField *stateLabel;
@@ -67,6 +69,7 @@
 @property (unsafe_unretained) IBOutlet NSButton *pickUpButton;
 @property (unsafe_unretained) IBOutlet NSButton *muteAudioButton;
 @property (unsafe_unretained) IBOutlet NSButton *muteVideoButton;
+@property (unsafe_unretained) IBOutlet NSButton *addContactButton;
 
 @property (unsafe_unretained) IBOutlet ITProgressIndicator *loadingIndicator;
 
@@ -76,6 +79,7 @@
 @property (unsafe_unretained) IBOutlet NSButton *chatButton;
 
 @property (strong) IBOutlet NSPopover *qualityPopOver;
+@property (strong) NSPopover* addToContactPopover;
 
 @property QHash<int, NSButton*> actionHash;
 
@@ -132,9 +136,16 @@
 -(void) updateCall
 {
     QModelIndex callIdx = CallModel::instance()->selectionModel()->currentIndex();
+    if (!callIdx.isValid()) {
+        return;
+    }
     [personLabel setStringValue:callIdx.data(Qt::DisplayRole).toString().toNSString()];
     [timeSpentLabel setStringValue:callIdx.data((int)Call::Role::Length).toString().toNSString()];
 
+    auto contactmethod = qvariant_cast<Call*>(callIdx.data(static_cast<int>(Call::Role::Object)))->peerContactMethod();
+    BOOL shouldShow = (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder());
+    [self.addContactButton setHidden:!shouldShow];
+
     Call::State state = callIdx.data((int)Call::Role::State).value<Call::State>();
     [loadingIndicator setHidden:YES];
     [stateLabel setStringValue:callIdx.data((int)Call::Role::HumanStateName).toString().toNSString()];
@@ -525,6 +536,27 @@
 
 #pragma mark - Button methods
 
+- (IBAction)addToContact:(NSButton*) sender {
+    auto contactmethod = CallModel::instance()->getCall(CallModel::instance()->selectionModel()->currentIndex())->peerContactMethod();
+
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    } else if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
+        auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
+        [editorVC setMethodToLink:contactmethod];
+        [editorVC setContactLinkedDelegate:self];
+        self.addToContactPopover = [[NSPopover alloc] init];
+        [self.addToContactPopover setContentSize:editorVC.view.frame.size];
+        [self.addToContactPopover setContentViewController:editorVC];
+        [self.addToContactPopover setAnimates:YES];
+        [self.addToContactPopover setBehavior:NSPopoverBehaviorTransient];
+        [self.addToContactPopover setDelegate:self];
+
+        [self.addToContactPopover showRelativeToRect:sender.bounds ofView:sender preferredEdge:NSMaxXEdge];
+    }
+}
+
 - (IBAction)hangUp:(id)sender {
     CallModel::instance()->getCall(CallModel::instance()->selectionModel()->currentIndex()) << Call::Action::REFUSE;
 }
@@ -568,6 +600,26 @@
     [self.qualityPopOver showRelativeToRect:[sender bounds] ofView:sender preferredEdge:NSMaxXEdge];
 }
 
+#pragma mark - NSPopOverDelegate
+
+- (void)popoverDidClose:(NSNotification *)notification
+{
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    }
+}
+
+#pragma mark - ContactLinkedDelegate
+
+- (void)contactLinked
+{
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    }
+}
+
 #pragma mark - NSSplitViewDelegate
 
 /* Return YES if the subview should be collapsed because the user has double-clicked on an adjacent divider. If a split view has a delegate, and the delegate responds to this message, it will be sent once for the subview before a divider when the user double-clicks on that divider, and again for the subview after the divider, but only if the delegate returned YES when sent -splitView:canCollapseSubview: for the subview in question. When the delegate indicates that both subviews should be collapsed NSSplitView's behavior is undefined.
diff --git a/src/HistoryVC.h b/src/HistoryVC.h
index fe6b7b4..7e4da5a 100644
--- a/src/HistoryVC.h
+++ b/src/HistoryVC.h
@@ -31,8 +31,9 @@
 #define HISTORYVIEWCONTROLLER_H
 
 #import <Cocoa/Cocoa.h>
+#import "views/RingOutlineView.h"
 
-@interface HistoryVC : NSViewController <NSOutlineViewDelegate> {
+@interface HistoryVC : NSViewController <NSOutlineViewDelegate, ContextMenuDelegate> {
 
 }
 
diff --git a/src/HistoryVC.mm b/src/HistoryVC.mm
index acb41c4..40ed7e9 100644
--- a/src/HistoryVC.mm
+++ b/src/HistoryVC.mm
@@ -33,20 +33,24 @@
 #import <QSortFilterProxyModel>
 #import <callmodel.h>
 #import <call.h>
+#import <person.h>
 #import <contactmethod.h>
 #import <localhistorycollection.h>
 
 #import "QNSTreeController.h"
+#import "PersonLinkerVC.h"
 
 #define COLUMNID_DAY			@"DayColumn"	// the single column name in our outline view
 #define COLUMNID_CONTACTMETHOD	@"ContactMethodColumn"	// the single column name in our outline view
 #define COLUMNID_DATE			@"DateColumn"	// the single column name in our outline view
 
-@interface HistoryVC()
+@interface HistoryVC() <NSPopoverDelegate, KeyboardShortcutDelegate, ContactLinkedDelegate>
 
 @property QNSTreeController *treeController;
-@property (assign) IBOutlet NSOutlineView *historyView;
+@property (assign) IBOutlet RingOutlineView *historyView;
 @property QSortFilterProxyModel *historyProxyModel;
+@property (strong) NSPopover* addToContactPopover;
+
 @end
 
 @implementation HistoryVC
@@ -58,7 +62,6 @@
 {
     if (self = [super initWithCoder:aDecoder]) {
         NSLog(@"INIT HVC");
-
     }
     return self;
 }
@@ -79,6 +82,8 @@
     [historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
     [historyView setTarget:self];
     [historyView setDoubleAction:@selector(placeHistoryCall:)];
+    [historyView setContextMenuDelegate:self];
+    [historyView setShortcutsDelegate:self];
 
     CategorizedHistoryModel::instance()->addCollection<LocalHistoryCollection>(LoadOptions::FORCE_ENABLED);
 }
@@ -87,6 +92,9 @@
 {
     if([[treeController selectedNodes] count] > 0) {
         QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        if (!qIdx.parent().isValid()) {
+            return;
+        }
         QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod);
         ContactMethod* m = qvariant_cast<ContactMethod*>(var);
         if(m){
@@ -174,4 +182,98 @@
     //NSLog(@"outlineViewSelectionDidChange!!");
 }
 
+#pragma mark - ContextMenuDelegate
+
+- (NSMenu*) contextualMenuForIndex:(NSIndexPath*) path
+{
+    if([[treeController selectedNodes] count] > 0) {
+        QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
+        if (qIdx.parent().isValid() && var.isValid()) {
+            if (auto call = var.value<Call *>()) {
+                auto contactmethod = call->peerContactMethod();
+                if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
+                    NSMenu *theMenu = [[NSMenu alloc]
+                                       initWithTitle:@""];
+                    [theMenu insertItemWithTitle:@"Add to contact"
+                                          action:@selector(addToContact)
+                                   keyEquivalent:@"a"
+                                         atIndex:0];
+                    return theMenu;
+                }
+            }
+        }
+    }
+    return nil;
+}
+
+- (void) addToContact
+{
+    ContactMethod* contactmethod = nullptr;
+    if([[treeController selectedNodes] count] > 0) {
+        QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
+        if (qIdx.parent().isValid() && var.isValid()) {
+            if (auto call = var.value<Call *>()) {
+                contactmethod = call->peerContactMethod();
+            }
+        }
+    }
+
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    } else if (contactmethod) {
+        auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonEditor" bundle:nil];
+        [editorVC setMethodToLink:contactmethod];
+        [editorVC setContactLinkedDelegate:self];
+        self.addToContactPopover = [[NSPopover alloc] init];
+        [self.addToContactPopover setContentSize:editorVC.view.frame.size];
+        [self.addToContactPopover setContentViewController:editorVC];
+        [self.addToContactPopover setAnimates:YES];
+        [self.addToContactPopover setBehavior:NSPopoverBehaviorTransient];
+        [self.addToContactPopover setDelegate:self];
+
+        [self.addToContactPopover showRelativeToRect:[historyView frameOfOutlineCellAtRow:[historyView selectedRow]] ofView:historyView preferredEdge:NSMaxXEdge];
+    }
+}
+
+#pragma mark - NSPopOverDelegate
+
+- (void)popoverDidClose:(NSNotification *)notification
+{
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    }
+}
+
+#pragma mark - ContactLinkedDelegate
+
+- (void)contactLinked
+{
+    if (self.addToContactPopover != nullptr) {
+        [self.addToContactPopover performClose:self];
+        self.addToContactPopover = NULL;
+    }
+}
+
+#pragma mark - KeyboardShortcutDelegate
+
+- (void) onAddShortcut
+{
+    if([[treeController selectedNodes] count] > 0) {
+        QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
+        if (qIdx.parent().isValid() && var.isValid()) {
+            if (auto call = var.value<Call *>()) {
+                auto contactmethod = call->peerContactMethod();
+                if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
+                    [self addToContact];
+                }
+            }
+        }
+    }
+}
+
 @end
diff --git a/src/PersonLinkerVC.h b/src/PersonLinkerVC.h
new file mode 100644
index 0000000..4c2edef
--- /dev/null
+++ b/src/PersonLinkerVC.h
@@ -0,0 +1,54 @@
+/*
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+@protocol ContactLinkedDelegate;
+@protocol ContactLinkedDelegate
+
+@optional
+
+-(void) contactLinked;
+
+@end
+
+class ContactMethod;
+
+@interface PersonLinkerVC : NSViewController <NSOutlineViewDelegate>
+
+@property ContactMethod* const methodToLink;
+
+/*
+ * Delegate to inform about completion of the linking process between
+ * a ContactMethod and a Person.
+ */
+@property (nonatomic) id <ContactLinkedDelegate> contactLinkedDelegate;
+
+@end
diff --git a/src/PersonLinkerVC.mm b/src/PersonLinkerVC.mm
new file mode 100644
index 0000000..291a4d4
--- /dev/null
+++ b/src/PersonLinkerVC.mm
@@ -0,0 +1,298 @@
+/*
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#import "PersonLinkerVC.h"
+
+//Qt
+#import <QtMacExtras/qmacfunctions.h>
+#import <QPixmap>
+
+//LRC
+#import <person.h>
+#import <personmodel.h>
+#import <contactmethod.h>
+#import <numbercategorymodel.h>
+
+#import "QNSTreeController.h"
+#import "delegates/ImageManipulationDelegate.h"
+#import "views/PersonCell.h"
+
+#define FIRSTNAME_TAG   1
+#define LASTNAME_TAG    2
+
+#define COLUMNID_NAME @"NameColumn"
+
+class OnlyPersonProxyModel : public QSortFilterProxyModel
+{
+public:
+    OnlyPersonProxyModel(QAbstractItemModel* parent) : QSortFilterProxyModel(parent)
+    {
+        setSourceModel(parent);
+    }
+    virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
+    {
+        bool match = filterRegExp().indexIn(sourceModel()->index(source_row,0,source_parent).data(Qt::DisplayRole).toString()) != -1;
+        //qDebug() << "FILTERING" << sourceModel()->index(source_row,0,source_parent) << "match:" << match;
+        return match && !sourceModel()->index(source_row,0,source_parent).parent().isValid();
+    }
+};
+
+@interface PersonLinkerVC () <NSTextFieldDelegate, NSComboBoxDelegate, NSComboBoxDataSource>
+
+@property QSortFilterProxyModel* contactProxyModel;
+@property QNSTreeController* treeController;
+
+
+@property (unsafe_unretained) IBOutlet NSTextField *contactMethodLabel;
+@property (unsafe_unretained) IBOutlet NSOutlineView *personsView;
+@property (unsafe_unretained) IBOutlet NSTextField *firstNameField;
+@property (unsafe_unretained) IBOutlet NSTextField *lastNameField;
+@property (unsafe_unretained) IBOutlet NSButton *createNewContactButton;
+@property (unsafe_unretained) IBOutlet NSComboBox *categoryComboBox;
+@property (strong) IBOutlet NSView *createContactSubview;
+@property (unsafe_unretained) IBOutlet NSView *linkToExistingSubview;
+
+
+@end
+
+@implementation PersonLinkerVC
+@synthesize treeController;
+@synthesize personsView;
+@synthesize contactProxyModel;
+@synthesize contactMethodLabel;
+@synthesize categoryComboBox, firstNameField, lastNameField;
+@synthesize createContactSubview, linkToExistingSubview, createNewContactButton;
+
+-(void) awakeFromNib
+{
+    NSLog(@"INIT PersonLinkerVC");
+
+    [firstNameField setTag:FIRSTNAME_TAG];
+    [lastNameField setTag:LASTNAME_TAG];
+
+    [categoryComboBox selectItemAtIndex:0];
+
+    contactProxyModel = new OnlyPersonProxyModel(PersonModel::instance());
+    contactProxyModel->setSortRole(static_cast<int>(Qt::DisplayRole));
+    contactProxyModel->sort(0,Qt::AscendingOrder);
+    contactProxyModel->setFilterRole(Qt::DisplayRole);
+    treeController = [[QNSTreeController alloc] initWithQModel:contactProxyModel];
+
+    [treeController setAvoidsEmptySelection:NO];
+    [treeController setChildrenKeyPath:@"children"];
+
+    [personsView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
+    [personsView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
+    [personsView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
+    [personsView setTarget:self];
+    [personsView setDoubleAction:@selector(addToContact:)];
+
+    [contactMethodLabel setStringValue:self.methodToLink->uri().toNSString()];
+}
+
+- (IBAction)addToContact:(id)sender
+{
+    /* get the selected number category */
+    const auto& idx = NumberCategoryModel::instance()->index([categoryComboBox indexOfSelectedItem]);
+    if (idx.isValid()) {
+        auto category = NumberCategoryModel::instance()->getCategory(idx.data().toString());
+        self.methodToLink->setCategory(category);
+    }
+
+    if([[treeController selectedNodes] count] > 0) {
+        QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
+        ContactMethod* m = nil;
+        if(((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 1) {
+            // Person
+            QVariant var = qIdx.data((int)Person::Role::Object);
+            if (var.isValid()) {
+                Person *p = var.value<Person*>();
+                Person::ContactMethods cms = p->phoneNumbers();
+                cms.append(self.methodToLink);
+                p->setContactMethods(cms);
+                self.methodToLink->setPerson(p);
+                p->save();
+                [self.contactLinkedDelegate contactLinked];
+            }
+        }
+    }
+}
+
+- (void) dealloc
+{
+    // No ARC for c++ pointers
+    delete contactProxyModel;
+}
+
+- (IBAction)presentNewContactForm:(id)sender {
+    [createContactSubview setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
+    //[createContactSubview setBounds:linkToExistingSubview.bounds];
+    [createContactSubview setFrame:linkToExistingSubview.frame];
+    [linkToExistingSubview setHidden:YES];
+    [self.view addSubview:createContactSubview];
+
+    [[[NSApplication sharedApplication] mainWindow] makeFirstResponder:firstNameField];
+    [firstNameField setNextKeyView:lastNameField];
+    [lastNameField setNextKeyView:createNewContactButton];
+    [createNewContactButton setNextKeyView:firstNameField];
+}
+
+- (IBAction)createContact:(id)sender
+{
+    /* get the selected number category */
+    const auto& idx = NumberCategoryModel::instance()->index([categoryComboBox indexOfSelectedItem]);
+    if (idx.isValid()) {
+        auto category = NumberCategoryModel::instance()->getCategory(idx.data().toString());
+        self.methodToLink->setCategory(category);
+    }
+
+    /* create a new person */
+    Person *p = new Person();
+    p->setFirstName(QString::fromNSString(firstNameField.stringValue));
+    p->setFamilyName(QString::fromNSString(lastNameField.stringValue));
+    p->setFormattedName(QString::fromNSString([[NSString alloc] initWithFormat:@"%@ %@", firstNameField.stringValue, lastNameField.stringValue]));
+    /* associate the new person with the contact method */
+    Person::ContactMethods numbers;
+    numbers << self.methodToLink;
+    p->setContactMethods(numbers);
+    self.methodToLink->setPerson(p);
+    PersonModel::instance()->addNewPerson(p);
+    [self.contactLinkedDelegate contactLinked];
+}
+
+#pragma mark - NSOutlineViewDelegate methods
+
+// -------------------------------------------------------------------------------
+//	shouldSelectItem:item
+// -------------------------------------------------------------------------------
+- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
+{
+    QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
+    if(!qIdx.isValid())
+        return NO;
+
+    if(qIdx.parent().isValid()) {
+        return NO;
+    } else {
+        return YES;
+    }
+}
+
+// -------------------------------------------------------------------------------
+//	dataCellForTableColumn:tableColumn:item
+// -------------------------------------------------------------------------------
+- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item
+{
+    QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
+    PersonCell *returnCell = [tableColumn dataCell];
+    return returnCell;
+}
+
+// -------------------------------------------------------------------------------
+//	shouldEditTableColumn:tableColumn:item
+//
+//	Decide to allow the edit of the given outline view "item".
+// -------------------------------------------------------------------------------
+- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
+{
+    return NO;
+}
+
+// -------------------------------------------------------------------------------
+//	outlineView:willDisplayCell:forTableColumn:item
+// -------------------------------------------------------------------------------
+- (void)outlineView:(NSOutlineView *)olv willDisplayCell:(NSCell*)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
+{
+    QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
+    if(!qIdx.isValid()) {
+        [((PersonCell *)cell) setPersonImage:nil];
+        return;
+    }
+
+    if ([[tableColumn identifier] isEqualToString:COLUMNID_NAME])
+    {
+        PersonCell *pCell = (PersonCell *)cell;
+        [pCell setPersonImage:nil];
+        if(!qIdx.parent().isValid()) {
+            pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString();
+                Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
+                QVariant photo = ImageManipulationDelegate::instance()->contactPhoto(p, QSize(35,35));
+                [pCell setPersonImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))];
+        } else {
+            pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString();
+
+        }
+    }
+}
+
+// -------------------------------------------------------------------------------
+//	outlineViewSelectionDidChange:notification
+// -------------------------------------------------------------------------------
+
+- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
+{
+    return 45.0;
+}
+
+#pragma mark - NSTextFieldDelegate
+
+- (void)controlTextDidChange:(NSNotification *) notification
+{
+    if ([notification.object tag] == FIRSTNAME_TAG || [notification.object tag] == LASTNAME_TAG) {
+        NSTextView *textView = notification.userInfo[@"NSFieldEditor"];
+        BOOL enableCreate = textView.textStorage.string.length > 0;
+        [createNewContactButton setEnabled:enableCreate];
+    } else {
+        NSTextView *textView = notification.userInfo[@"NSFieldEditor"];
+        contactProxyModel->setFilterRegExp(QRegExp(QString::fromNSString(textView.textStorage.string), Qt::CaseInsensitive, QRegExp::FixedString));
+        [personsView scrollToBeginningOfDocument:nil];
+    }
+}
+
+#pragma mark - NSComboBoxDelegate
+
+- (void)comboBoxSelectionDidChange:(NSNotification*) notification
+{
+    [(NSComboBox *)[notification object] indexOfSelectedItem];
+}
+
+#pragma mark - NSComboBoxDatasource
+
+- (NSInteger)numberOfItemsInComboBox:(NSComboBox *)aComboBox
+{
+    return NumberCategoryModel::instance()->rowCount();
+}
+
+- (id)comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(NSInteger)index
+{
+    return NumberCategoryModel::instance()->index(index).data().toString().toNSString();
+}
+
+@end
diff --git a/src/PersonsVC.mm b/src/PersonsVC.mm
index f1d1c26..68687a2 100644
--- a/src/PersonsVC.mm
+++ b/src/PersonsVC.mm
@@ -30,15 +30,19 @@
 
 #import "PersonsVC.h"
 
-#import <personmodel.h>
-#import <callmodel.h>
-#import <categorizedcontactmodel.h>
+
+//Qt
 #import <QSortFilterProxyModel>
-#import <person.h>
-#import <contactmethod.h>
 #import <QtMacExtras/qmacfunctions.h>
 #import <QPixmap>
 
+//LRC
+#import <person.h>
+#import <personmodel.h>
+#import <callmodel.h>
+#import <contactmethod.h>
+#import <categorizedcontactmodel.h>
+
 #import "backends/AddressBookBackend.h"
 #import "QNSTreeController.h"
 #import "delegates/ImageManipulationDelegate.h"
@@ -92,8 +96,6 @@
     [personsView setDoubleAction:@selector(callContact:)];
 
     CategorizedContactModel::instance()->setUnreachableHidden(YES);
-    PersonModel::instance()->addCollection<AddressBookBackend>(LoadOptions::FORCE_ENABLED);
-
 }
 
 - (IBAction)callContact:(id)sender
@@ -227,9 +229,6 @@
 - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
 {
     QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
-    if(!qIdx.isValid())
-        return 0.0f;
-
     if(!qIdx.parent().isValid()) {
         return 20.0;
     } else {
@@ -237,5 +236,4 @@
     }
 }
 
-
 @end
diff --git a/src/QNSTreeController.h b/src/QNSTreeController.h
index 735fe56..0539c6e 100644
--- a/src/QNSTreeController.h
+++ b/src/QNSTreeController.h
@@ -35,14 +35,12 @@
 
 @interface QNSTreeController : NSTreeController {
 
-QAbstractItemModel *privateQModel;
-NSMutableArray* topNodes;
-
+    QAbstractItemModel *privateQModel;
 }
 
-- (void*)connect;
 - (id) initWithQModel:(QAbstractItemModel*) model;
 - (QModelIndex) toQIdx:(NSTreeNode*) node;
+- (QModelIndex) indexPathtoQIdx:(NSIndexPath*) path;
 
 @end
 
diff --git a/src/QNSTreeController.mm b/src/QNSTreeController.mm
index f7697fb..60c91d1 100644
--- a/src/QNSTreeController.mm
+++ b/src/QNSTreeController.mm
@@ -29,6 +29,8 @@
  */
 #import "QNSTreeController.h"
 
+#import <QDebug>
+
 @interface Node : NSObject {
     NSMutableArray *children;
 }
@@ -43,9 +45,14 @@
     return self;
 }
 
-- (void) addChild:(Node*) child
+- (void) addChild:(Node*) child AtIndex:(NSUInteger) idx
 {
-    [children addObject:child];
+    [children insertObject:child atIndex:idx];
+}
+
+- (NSMutableArray*) children
+{
+    return children;
 }
 
 @end
@@ -58,18 +65,33 @@
     self = [super init];
     self->privateQModel = model;
 
-    topNodes = [[NSMutableArray alloc] init];
+    NSMutableArray* nodes = [[NSMutableArray alloc] init];
     [self connect];
 
-    [self populate];
+    [self populate:nodes];
 
-    return [self initWithContent:topNodes];
+    return [self initWithContent:nodes];
 }
 
--(void) populate
+-(void) populate:(NSMutableArray*) nodes
 {
-    for (int i =0 ; i < self->privateQModel->rowCount() ; ++i){
-        [topNodes insertObject:[[Node alloc] init] atIndex:i];
+    for (int i = 0 ; i < self->privateQModel->rowCount() ; ++i) {
+        Node* n = [[Node alloc] init];
+        //qDebug() << "POUPL TOP:"<< self->privateQModel->index(i, 0) ;
+        [self populateChild:[n children] withParent:self->privateQModel->index(i, 0)];
+        [nodes insertObject:n atIndex:i];
+    }
+}
+
+- (void) populateChild:(NSMutableArray*) nodes withParent:(QModelIndex)qIdx
+{
+    if (!qIdx.isValid())
+        return;
+    for (int i = 0 ; i < self->privateQModel->rowCount(qIdx) ; ++i) {
+        Node* n = [[Node alloc] init];
+        //qDebug() << "POPUL CHILD:"<< self->privateQModel->index(i, 0, qIdx) ;
+        [self populateChild:[n children] withParent:self->privateQModel->index(i, 0, qIdx)];
+        [nodes insertObject:n atIndex:i];
     }
 }
 
@@ -78,24 +100,26 @@
     return self->privateQModel->flags(self->privateQModel->index(0, 0)) | Qt::ItemIsEditable;
 }
 
-- (QModelIndex) toQIdx:(NSTreeNode*) node
+- (QModelIndex) indexPathtoQIdx:(NSIndexPath*) path
 {
-    NSIndexPath* idx = node.indexPath;
-    NSUInteger myArray[[idx length]];
-    [idx getIndexes:myArray];
+    NSUInteger myArray[[path length]];
+    [path getIndexes:myArray];
     QModelIndex toReturn;
 
-    for (int i = 0; i < idx.length; ++i) {
+    for (int i = 0; i < path.length; ++i) {
         toReturn = self->privateQModel->index(myArray[i], 0, toReturn);
     }
 
     return toReturn;
 }
 
-- (void) insertChildAtQIndex:(QModelIndex) qIdx
+- (QModelIndex) toQIdx:(NSTreeNode*) node
 {
-    Node* child = [[Node alloc] init];
+    return [self indexPathtoQIdx:node.indexPath];
+}
 
+- (NSIndexPath*) qIdxToNSIndexPath:(QModelIndex) qIdx
+{
     QModelIndex tmp = qIdx.parent();
     NSMutableArray* allIndexes = [NSMutableArray array];
     while (tmp.isValid()) {
@@ -108,7 +132,27 @@
     for (int i = 0 ; i < allIndexes.count ; ++i) {
         indexes[i] = [[allIndexes objectAtIndex:i] intValue];
     }
-    [self insertObject:child atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:allIndexes.count]];
+    return [[NSIndexPath alloc] initWithIndexes:indexes length:allIndexes.count];
+}
+
+- (void) insertNodeAtQIndex:(QModelIndex) qIdx
+{
+    NSIndexPath* path = [self qIdxToNSIndexPath:qIdx];
+    //qDebug() << "insertNodeAt" << qIdx;
+    //NSLog(@"insertNodeAt index: %@", path);
+    if (path.length == 1 && [path indexAtPosition:0] <= [[self arrangedObjects] count])
+        [self insertObject:[[Node alloc] init] atArrangedObjectIndexPath:path];
+    else if (path.length > 1)
+        [self insertObject:[[Node alloc] init] atArrangedObjectIndexPath:path];
+}
+
+- (void) removeNodeAtQIndex:(QModelIndex) qIdx
+{
+    NSIndexPath* path = [self qIdxToNSIndexPath:qIdx];
+    if ([self.arrangedObjects descendantNodeAtIndexPath:path]) {
+        //NSLog(@"removeNodeAt index: %@", path);
+        [self removeObjectAtArrangedObjectIndexPath:path];
+    }
 }
 
 - (void)connect
@@ -116,17 +160,14 @@
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::rowsInserted,
                      [=](const QModelIndex & parent, int first, int last) {
-                         for( int row = first; row <= last; row++) {
-                             if(!parent.isValid()) {
-                                 //Inserting topnode
-                                 Node* n = [[Node alloc] init];
-                                 [self insertObject:n atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndex:row]];
-                             } else {
-                                 [self insertChildAtQIndex:self->privateQModel->index(row, 0, parent)];
-                             }
+                         for( int row = first; row <= last; ++row) {
+                             //qDebug() << "INSERTING:"<< self->privateQModel->index(row, 0, parent) ;
+                             if(!self->privateQModel->index(row, 0, parent).isValid())
+                                 continue;
+
+                             [self insertNodeAtQIndex:self->privateQModel->index(row, 0, parent)];
                          }
-                     }
-                     );
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::rowsAboutToBeMoved,
@@ -137,8 +178,7 @@
                          for( int row = sourceStart; row <= sourceEnd; row++) {
                              //TODO
                          }
-                     }
-                     );
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::rowsMoved,
@@ -149,45 +189,37 @@
                          for( int row = sourceStart; row <= sourceEnd; row++) {
                              //TODO
                          }
-                     }
-                     );
+                         [self rearrangeObjects];
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::rowsAboutToBeRemoved,
-                     [=](const QModelIndex & parent, int first, int last) {
-                         NSLog(@"rows about to be removed");
-                     }
-                     );
+                     [self](const QModelIndex & parent, int first, int last) {
+                         for( int row = first; row <= last; row++) {
+                             //qDebug() << "REMOVING:"<< self->privateQModel->index(row, 0, parent) ;
+                             if (!self->privateQModel->index(row, 0, parent).isValid())
+                                 continue;
+
+                             [self removeNodeAtQIndex:self->privateQModel->index(row, 0, parent)];
+                         }
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::rowsRemoved,
-                     [=](const QModelIndex & parent, int first, int last) {
-                         //NSLog(@"rows removed");
-                         for( int row = first; row <= last; row++) {
-                             if(parent.isValid())
-                             {
-                                 //Removing leaf
-                                 NSUInteger indexes[] = { (NSUInteger)parent.row(), (NSUInteger)row};
-                                 [self removeObjectAtArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:2]];
-                             } else
-                             {
-                                 [self removeObjectAtArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndex:row]];
-                             }
-                         }
-                     }
-                     );
+                     [self](const QModelIndex& parent, int first, int last) {
+
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::layoutChanged,
-                     [=]() {
+                     [self]() {
                          //NSLog(@"layout changed");
-                     }
-                     );
+                         [self rearrangeObjects];
+                     });
 
     QObject::connect(self->privateQModel,
                      &QAbstractItemModel::dataChanged,
-                     [=](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
-                         //NSLog(@"data changed");
+                     [self](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
                          for(int row = topLeft.row() ; row <= bottomRight.row() ; ++row)
                          {
                              QModelIndex tmpIdx = self->privateQModel->index(row, 0);
@@ -197,6 +229,7 @@
                                      [self insertObject:n atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndex:row]];
                              }
                          }
+                         [self rearrangeObjects];
                      });
 }
 
diff --git a/src/RingWindowController.mm b/src/RingWindowController.mm
index 60f2426..c96b3e9 100644
--- a/src/RingWindowController.mm
+++ b/src/RingWindowController.mm
@@ -34,11 +34,14 @@
 #import <callmodel.h>
 #import <account.h>
 #import <call.h>
+#import <personmodel.h>
 
 #import "AppDelegate.h"
 #import "Constants.h"
 #import "CurrentCallVC.h"
 
+#import "backends/AddressBookBackend.h"
+
 @interface RingWindowController ()
 
 @property NSSearchField* callField;
@@ -67,6 +70,8 @@
     [callView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
     [[currentVC view] setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
 
+
+    PersonModel::instance()->addCollection<AddressBookBackend>(LoadOptions::FORCE_ENABLED);
     [callView addSubview:[self.currentVC view]];
     [currentVC initFrame];
 
diff --git a/src/backends/AddressBookBackend.h b/src/backends/AddressBookBackend.h
index 74a24c4..dc3a5cc 100644
--- a/src/backends/AddressBookBackend.h
+++ b/src/backends/AddressBookBackend.h
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2004-2015 Savoir-Faire Linux Inc.
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
  *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
@@ -27,13 +27,14 @@
  *  shall include the source code for the parts of OpenSSL used as well
  *  as that of the covered work.
  */
-#ifndef ADDRESSBOOKBACKEND_H
-#define ADDRESSBOOKBACKEND_H
 
 #include <collectioninterface.h>
 #include <collectioneditor.h>
 
 class Person;
+@class ABPerson;
+@class NSMutableArray;
+@class NSNotification;
 
 template<typename T> class CollectionMediator;
 
@@ -53,10 +54,15 @@
     virtual QByteArray id       () const override;
     virtual FlagPack<SupportedFeatures>  supportedFeatures() const override;
 
+    bool addNewPerson(Person *item);
+    bool removePerson(NSString* uid);
+
 private:
     CollectionMediator<Person>*  m_pMediator;
+    NSMutableArray* observers;
+
+    void handleNotification(NSNotification* ns);
+    Person* abPersonToPerson(ABPerson* ab);
 
     void asyncLoad(int startingPoint);
-};
-
-#endif // ADDRESSBOOKBACKEND_H
+};
\ No newline at end of file
diff --git a/src/backends/AddressBookBackend.mm b/src/backends/AddressBookBackend.mm
index f98e773..59da421 100644
--- a/src/backends/AddressBookBackend.mm
+++ b/src/backends/AddressBookBackend.mm
@@ -1,5 +1,5 @@
 /*
- *  Copyright (C) 2004-2015 Savoir-Faire Linux Inc.
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
  *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
@@ -29,6 +29,7 @@
  */
 #import "AddressBookBackend.h"
 
+//Cocoa
 #import <AddressBook/AddressBook.h>
 
 //Qt
@@ -46,6 +47,7 @@
 #import <account.h>
 #import <person.h>
 #import <contactmethod.h>
+#import <personmodel.h>
 
 /**
  *
@@ -81,7 +83,7 @@
     virtual bool save       ( const Person* item ) override;
     virtual bool remove     ( const Person* item ) override;
     virtual bool edit       ( Person*       item ) override;
-    virtual bool addNew     ( const Person* item ) override;
+    virtual bool addNew     ( Person* item ) override;
     virtual bool addExisting( const Person* item ) override;
 
 private:
@@ -105,40 +107,95 @@
 AddressBookBackend::AddressBookBackend(CollectionMediator<Person>* mediator) :
 CollectionInterface(new AddressBookEditor(mediator,this)),m_pMediator(mediator)
 {
+    ::id addressBookObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kABDatabaseChangedNotification
+                                     object:nil
+                                     queue:[NSOperationQueue mainQueue]
+                                     usingBlock:^(NSNotification *note) {
+                                         handleNotification(note);
+                                     }];
 
+    ::id externalAddressBookObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kABDatabaseChangedExternallyNotification
+                                                                                  object:nil
+                                                                                   queue:[NSOperationQueue mainQueue]
+                                                                              usingBlock:^(NSNotification *note) {
+                                                                                  handleNotification(note);
+                                                                              }];
+
+    observers = [[NSArray alloc] initWithObjects:addressBookObserver, externalAddressBookObserver, nil];
+}
+
+void AddressBookBackend::handleNotification(NSNotification* ns)
+{
+    for (NSString* r in ns.userInfo[kABInsertedRecords]) {
+        ABRecord* inserted = [[ABAddressBook sharedAddressBook] recordForUniqueId:r];
+        if (inserted && [[[ABAddressBook sharedAddressBook] recordClassFromUniqueId:r] containsString:@"ABPerson"]) {
+            editor<Person>()->addExisting(this->abPersonToPerson(inserted));
+        }
+    }
+
+    for (NSString* r in ns.userInfo[kABUpdatedRecords]) {
+        NSLog(@"Updated record : %@", r);
+        if ([[[ABAddressBook sharedAddressBook] recordClassFromUniqueId:r] containsString:@"ABPerson"]) {
+            Person* toUpdate = PersonModel::instance()->getPersonByUid([r UTF8String]);
+            if (toUpdate) {
+                ABPerson* updated = [[ABAddressBook sharedAddressBook] recordForUniqueId:r];
+                toUpdate->updateFromVCard(QByteArray::fromNSData(updated.vCardRepresentation));
+            } else
+                editor<Person>()->addExisting(this->abPersonToPerson([[ABAddressBook sharedAddressBook] recordForUniqueId:r]));
+        }
+    }
+
+    for (NSString* r in ns.userInfo[kABDeletedRecords]) {
+        NSLog(@"Deleted person: %@", r);
+        removePerson(r);
+    }
 }
 
 AddressBookBackend::~AddressBookBackend()
 {
-
+    for (::id observer in this->observers)
+        [[NSNotificationCenter defaultCenter] removeObserver:observer];
 }
 
 void AddressBookEditor::savePerson(QTextStream& stream, const Person* Person)
 {
-
     qDebug() << "Saving Person!";
 }
 
 bool AddressBookEditor::regenFile(const Person* toIgnore)
 {
-    QDir dir(QString('/'));
-    dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString());
-
-
     return false;
 }
 
-bool AddressBookEditor::save(const Person* Person)
+bool AddressBookEditor::save(const Person* person)
 {
-    //if (Person->collection()->editor<Person>() != this)
-    //    return addNew(Person);
+    // first get the existing person
+    ABPerson* toSave = [[ABAddressBook sharedAddressBook] recordForUniqueId:[[NSString alloc] initWithUTF8String:person->uid().data()]];
 
-    return regenFile(nullptr);
+    // create its new reprresentation
+    ABPerson* newVCard = [[ABPerson alloc] initWithVCardRepresentation:person->toVCard().toNSData()];
+
+    if (toSave) {
+        // i.e. *all* potential properties
+        for (NSString* property in [ABPerson properties]) {
+            // if the property doesn't exist in the address book, value will be nil
+            id value = [newVCard valueForProperty:property];
+            if (value && [property isNotEqualTo:kABUIDProperty]) {
+                NSError* error;
+                if (![toSave setValue:value forProperty:property error:&error] || error) {
+                    NSLog(@"Error saving property %@ for person %@ : %@", property, toSave, [error localizedDescription]);
+                    return false;
+                }
+            }
+        }
+    }
+    return [[ABAddressBook sharedAddressBook] save];
 }
 
 bool AddressBookEditor::remove(const Person* item)
 {
-    return regenFile(item);
+    mediator()->removeItem(item);
+    return false;
 }
 
 bool AddressBookEditor::edit( Person* item)
@@ -147,12 +204,10 @@
     return false;
 }
 
-bool AddressBookEditor::addNew(const Person* Person)
+bool AddressBookEditor::addNew( Person* item)
 {
-    QDir dir(QString('/'));
-    dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString());
-
-    return false;
+    bool ret = m_pCollection->addNewPerson(item);
+    return ret;
 }
 
 bool AddressBookEditor::addExisting(const Person* item)
@@ -167,12 +222,12 @@
     return m_lItems;
 }
 
-QString AddressBookBackend::name () const
+QString AddressBookBackend::name() const
 {
     return QObject::tr("AddressBook backend");
 }
 
-QString AddressBookBackend::category () const
+QString AddressBookBackend::category() const
 {
     return QObject::tr("Persons");
 }
@@ -192,7 +247,7 @@
     QTimer::singleShot(100, [=] {
         asyncLoad(0);
     });
-     return false;
+    return false;
 }
 
 void AddressBookBackend::asyncLoad(int startingPoint)
@@ -205,18 +260,7 @@
 
         ABPerson* abPerson = ((ABPerson*)[everyone objectAtIndex:i]);
 
-        Person* person = new Person(QByteArray::fromNSData(abPerson.vCardRepresentation),
-                                    Person::Encoding::vCard,
-                                    this);
-
-        if(abPerson.imageData)
-            person->setPhoto(QVariant(QPixmap::fromImage(QImage::fromData(QByteArray::fromNSData((abPerson.imageData))))));
-
-        if([person->formattedName().toNSString() isEqualToString:@""]   &&
-           [person->secondName().toNSString() isEqualToString:@""]     &&
-           [person->firstName().toNSString() isEqualToString:@""]) {
-            continue;
-        }
+        Person* person = this->abPersonToPerson(abPerson);
 
         person->setCollection(this);
 
@@ -228,23 +272,51 @@
             asyncLoad(endPoint);
         });
     }
-
 }
 
+Person* AddressBookBackend::abPersonToPerson(ABPerson* ab)
+{
+    Person* person = new Person(QByteArray::fromNSData(ab.vCardRepresentation),
+                                Person::Encoding::vCard,
+                                this);
+    if(ab.imageData)
+        person->setPhoto(QVariant(QPixmap::fromImage(QImage::fromData(QByteArray::fromNSData((ab.imageData))))));
+
+    person->setUid([[ab uniqueId] UTF8String]);
+    return person;
+}
 
 bool AddressBookBackend::reload()
 {
     return false;
 }
 
+bool AddressBookBackend::addNewPerson(Person *item)
+{
+    ABAddressBook *book = [ABAddressBook sharedAddressBook];
+    ABPerson* person = [[ABPerson alloc] initWithVCardRepresentation:item->toVCard().toNSData()];
+    [book addRecord:person];
+    return [book save];
+}
+
+bool AddressBookBackend::removePerson(NSString* uid)
+{
+    auto found = PersonModel::instance()->getPersonByUid([uid UTF8String]);
+    if (found) {
+        deactivate(found);
+        editor<Person>()->remove(found);
+        return true;
+    }
+    return false;
+}
+
 FlagPack<AddressBookBackend::SupportedFeatures> AddressBookBackend::supportedFeatures() const
 {
-    return (FlagPack<SupportedFeatures>) (
-                                                     CollectionInterface::SupportedFeatures::NONE  |
-                                                     CollectionInterface::SupportedFeatures::LOAD  |
-                                                     CollectionInterface::SupportedFeatures::CLEAR |
-                                                     CollectionInterface::SupportedFeatures::REMOVE|
-                                                     CollectionInterface::SupportedFeatures::ADD   );
+    return (FlagPack<SupportedFeatures>) (CollectionInterface::SupportedFeatures::NONE  |
+                                          CollectionInterface::SupportedFeatures::LOAD  |
+                                          CollectionInterface::SupportedFeatures::CLEAR |
+                                          CollectionInterface::SupportedFeatures::REMOVE|
+                                          CollectionInterface::SupportedFeatures::ADD   );
 }
 
 bool AddressBookBackend::clear()
diff --git a/src/views/RingOutlineView.h b/src/views/RingOutlineView.h
new file mode 100644
index 0000000..119d6e0
--- /dev/null
+++ b/src/views/RingOutlineView.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#import <Cocoa/Cocoa.h>
+
+@protocol ContextMenuDelegate;
+@protocol ContextMenuDelegate
+
+@required
+
+- (NSMenu*) contextualMenuForIndex:(NSIndexPath*) path;
+
+@end
+
+@protocol KeyboardShortcutDelegate;
+@protocol KeyboardShortcutDelegate
+
+@optional
+
+/**
+ *  This shortcut has to respond to cmd (⌘) + a
+ */
+- (void) onAddShortcut;
+
+@end
+
+@interface RingOutlineView : NSOutlineView
+
+@property (nonatomic,weak) id <ContextMenuDelegate>         contextMenuDelegate;
+@property (nonatomic,weak) id <KeyboardShortcutDelegate>    shortcutsDelegate;
+
+@end
diff --git a/src/views/RingOutlineView.mm b/src/views/RingOutlineView.mm
new file mode 100644
index 0000000..fcb994a
--- /dev/null
+++ b/src/views/RingOutlineView.mm
@@ -0,0 +1,68 @@
+/*
+ *  Copyright (C) 2015 Savoir-faire Linux Inc.
+ *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ *
+ *  Additional permission under GNU GPL version 3 section 7:
+ *
+ *  If you modify this program, or any covered work, by linking or
+ *  combining it with the OpenSSL project's OpenSSL library (or a
+ *  modified version of that library), containing parts covered by the
+ *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
+ *  grants you additional permission to convey the resulting work.
+ *  Corresponding Source for a non-source form of such a combination
+ *  shall include the source code for the parts of OpenSSL used as well
+ *  as that of the covered work.
+ */
+
+#import "RingOutlineView.h"
+
+@implementation RingOutlineView
+
+- (NSMenu*)menuForEvent:(NSEvent*)evt
+{
+    NSPoint pt = [self convertPoint:[evt locationInWindow] fromView:nil];
+    int rowIdx = [self rowAtPoint:pt];
+    int colIdx = [self columnAtPoint:pt];
+    if (self.contextMenuDelegate && rowIdx >= 0 && colIdx >= 0) {
+        NSUInteger indexes[2] = {static_cast<NSUInteger>(rowIdx), static_cast<NSUInteger>(colIdx)};
+        NSIndexPath* path = [NSIndexPath indexPathWithIndexes:indexes length:2];
+        return [self.contextMenuDelegate contextualMenuForIndex:path];
+    }
+    return nil;
+}
+
+- (void)keyDown:(NSEvent *)theEvent
+{
+    // Handle the Tab key
+    if ([[theEvent characters] characterAtIndex:0] == NSTabCharacter) {
+        if (([theEvent modifierFlags] & NSShiftKeyMask) != NSShiftKeyMask) {
+            [[self window] selectKeyViewFollowingView:self];
+        } else {
+            [[self window] selectKeyViewPrecedingView:self];
+        }
+    }
+    else if (([theEvent modifierFlags] & NSCommandKeyMask) == NSCommandKeyMask) {
+        if (self.shortcutsDelegate) {
+            if ([[theEvent characters] characterAtIndex:0] == 'a') {
+                [self.shortcutsDelegate onAddShortcut];
+            }
+        }
+    } else
+        [super keyDown:theEvent];
+}
+
+@end