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