blob: 96ef3ece8f767ca6b704a91be6625513852c465f [file] [log] [blame]
Alexandre Lision4dfcafc2015-08-20 12:43:23 -04001/*
Alexandre Lision9fe374b2016-01-06 10:17:31 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Alexandre Lision4dfcafc2015-08-20 12:43:23 -04003 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#import "SmartViewVC.h"
21
22//Qt
23#import <QtMacExtras/qmacfunctions.h>
24#import <QPixmap>
25#import <QIdentityProxyModel>
26#import <QItemSelectionModel>
27
28//LRC
29#import <recentmodel.h>
30#import <callmodel.h>
31#import <call.h>
Alexandre Lision45c18672016-11-02 15:24:45 -040032#import <uri.h>
Alexandre Lisiond14bda32015-10-13 11:34:29 -040033#import <itemdataroles.h>
Alexandre Lision45c18672016-11-02 15:24:45 -040034#import <namedirectory.h>
35#import <accountmodel.h>
36#import <account.h>
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040037#import <person.h>
38#import <contactmethod.h>
39#import <globalinstances.h>
Alexandre Lision0f66bd32016-01-18 11:30:45 -050040#import <phonedirectorymodel.h>
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040041
42#import "QNSTreeController.h"
43#import "delegates/ImageManipulationDelegate.h"
Alexandre Lision4e280d62015-09-09 15:56:30 -040044#import "views/HoverTableRowView.h"
Alexandre Lision61db3552015-10-22 19:12:52 -040045#import "PersonLinkerVC.h"
Alexandre Lision4baba4c2016-02-11 13:00:57 -050046#import "views/IconButton.h"
Alexandre Lision61db3552015-10-22 19:12:52 -040047#import "views/RingOutlineView.h"
Alexandre Lision4e280d62015-09-09 15:56:30 -040048#import "views/ContextualTableCellView.h"
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040049
Alexandre Lision61db3552015-10-22 19:12:52 -040050@interface SmartViewVC () <NSOutlineViewDelegate, NSPopoverDelegate, ContextMenuDelegate, ContactLinkedDelegate, KeyboardShortcutDelegate> {
Alexandre Lision0f66bd32016-01-18 11:30:45 -050051
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040052 QNSTreeController *treeController;
Alexandre Lision61db3552015-10-22 19:12:52 -040053 NSPopover* addToContactPopover;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040054
55 //UI elements
Alexandre Lision61db3552015-10-22 19:12:52 -040056 __unsafe_unretained IBOutlet RingOutlineView* smartView;
57 __unsafe_unretained IBOutlet NSSearchField* searchField;
Alexandre Lision61db3552015-10-22 19:12:52 -040058 __unsafe_unretained IBOutlet NSTabView* tabbar;
Alexandre Lision45c18672016-11-02 15:24:45 -040059
60 /* Pending ring usernames lookup for the search entry */
61 QMetaObject::Connection usernameLookupConnection;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040062}
63
64@end
65
66@implementation SmartViewVC
67
68// Tags for views
Alexandre Lision57914fa2016-02-10 14:52:24 -050069NSInteger const IMAGE_TAG = 100;
70NSInteger const DISPLAYNAME_TAG = 200;
71NSInteger const DETAILS_TAG = 300;
72NSInteger const CALL_BUTTON_TAG = 400;
73NSInteger const TXT_BUTTON_TAG = 500;
74NSInteger const CANCEL_BUTTON_TAG = 600;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040075
76- (void)awakeFromNib
77{
78 NSLog(@"INIT SmartView VC");
79
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040080 treeController = [[QNSTreeController alloc] initWithQModel:RecentModel::instance().peopleProxy()];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040081 [treeController setAvoidsEmptySelection:NO];
82 [treeController setChildrenKeyPath:@"children"];
83
84 [smartView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
85 [smartView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
86 [smartView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
87 [smartView setTarget:self];
Alexandre Lision89edc6a2015-11-09 11:30:47 -050088 [smartView setAction:@selector(selectRow:)];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040089 [smartView setDoubleAction:@selector(placeCall:)];
90
Alexandre Lision61db3552015-10-22 19:12:52 -040091 [smartView setContextMenuDelegate:self];
92 [smartView setShortcutsDelegate:self];
93
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040094 QObject::connect(RecentModel::instance().peopleProxy(),
Alexandre Lisionee098462015-10-22 17:22:50 -040095 &QAbstractItemModel::dataChanged,
96 [self](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
97 for(int row = topLeft.row() ; row <= bottomRight.row() ; ++row)
98 {
99 [smartView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row]
100 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
101 }
102 });
103
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500104 QObject::connect(RecentModel::instance().selectionModel(),
105 &QItemSelectionModel::currentChanged,
106 [=](const QModelIndex &current, const QModelIndex &previous) {
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500107 if(!current.isValid()) {
108 [smartView deselectAll:nil];
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500109 return;
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500110 }
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500111
112 auto proxyIdx = RecentModel::instance().peopleProxy()->mapFromSource(current);
113 if (proxyIdx.isValid()) {
114 [treeController setSelectionQModelIndex:proxyIdx];
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500115 [tabbar selectTabViewItemAtIndex:0];
116 [smartView scrollRowToVisible:proxyIdx.row()];
117 }
118 });
119
Alexandre Lision25fc82a2016-05-08 17:30:40 -0400120 QObject::connect(RecentModel::instance().peopleProxy(),
121 &QAbstractItemModel::rowsInserted,
122 [=](const QModelIndex &parent, int first, int last) {
123 Q_UNUSED(parent)
124 Q_UNUSED(first)
125 Q_UNUSED(last)
126 [smartView scrollRowToVisible:0];
127 });
128
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400129 [self.view setWantsLayer:YES];
130 [self.view setLayer:[CALayer layer]];
131 [self.view.layer setBackgroundColor:[NSColor whiteColor].CGColor];
132
133 [searchField setWantsLayer:YES];
134 [searchField setLayer:[CALayer layer]];
135 [searchField.layer setBackgroundColor:[NSColor colorWithCalibratedRed:0.949 green:0.949 blue:0.949 alpha:0.9].CGColor];
136}
137
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500138-(void) selectRow:(id)sender
139{
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500140 if ([treeController selectedNodes].count == 0) {
141 RecentModel::instance().selectionModel()->clearCurrentIndex();
142 return;
143 }
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500144 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
145 auto proxyIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
146 RecentModel::instance().selectionModel()->setCurrentIndex(proxyIdx, QItemSelectionModel::ClearAndSelect);
147}
148
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400149- (void)placeCall:(id)sender
150{
151 QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
152 ContactMethod* m = nil;
153
154 // Double click on an ongoing call
155 if (qIdx.parent().isValid()) {
156 return;
157 }
158
159 if([[treeController selectedNodes] count] > 0) {
160 QVariant var = qIdx.data((int)Call::Role::ContactMethod);
161 m = qvariant_cast<ContactMethod*>(var);
162 if (!m) {
163 // test if it is a person
164 QVariant var = qIdx.data((int)Person::Role::Object);
165 if (var.isValid()) {
166 Person *c = var.value<Person*>();
167 if (c->phoneNumbers().size() > 0) {
168 m = c->phoneNumbers().first();
169 }
170 }
171 }
172 }
173
174 // Before calling check if we properly extracted a contact method and that
175 // there is NOT already an ongoing call for this index (e.g: no children for this node)
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400176 if(m && !RecentModel::instance().peopleProxy()->index(0, 0, qIdx).isValid()){
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500177 auto c = CallModel::instance().dialingCall();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400178 c->setPeerContactMethod(m);
179 c << Call::Action::ACCEPT;
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500180 CallModel::instance().selectCall(c);
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400181 }
182}
183
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500184- (IBAction)showHistory:(NSButton*)sender
185{
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500186 [tabbar selectTabViewItemAtIndex:1];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400187}
188
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500189- (IBAction)showContacts:(NSButton*)sender
190{
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500191 [tabbar selectTabViewItemAtIndex:2];
192}
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500193
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500194- (IBAction)showSmartlist:(NSButton*)sender
195{
196 [tabbar selectTabViewItemAtIndex:0];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400197}
198
199#pragma mark - NSOutlineViewDelegate methods
200
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400201- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
202{
203 return YES;
204}
205
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400206- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
207{
208 return NO;
209}
210
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400211- (void)outlineViewSelectionDidChange:(NSNotification *)notification
212{
213 if ([treeController selectedNodes].count <= 0) {
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500214 RecentModel::instance().selectionModel()->clearCurrentIndex();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400215 return;
216 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400217}
218
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400219- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
220{
Alexandre Lisiona4bb0512016-04-19 18:04:31 -0400221 QModelIndex proxyIdx = [treeController toQIdx:((NSTreeNode*)item)];
222 QModelIndex qIdx = RecentModel::instance().peopleProxy()->mapToSource(proxyIdx);
223
224 NSTableCellView* result;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400225 if (!qIdx.parent().isValid()) {
226 result = [outlineView makeViewWithIdentifier:@"MainCell" owner:outlineView];
227 NSTextField* details = [result viewWithTag:DETAILS_TAG];
228
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500229 NSMutableArray* controls = [NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]];
230 [((ContextualTableCellView*) result) setContextualsControls:controls];
Alexandre Lision4e280d62015-09-09 15:56:30 -0400231
Alexandre Lisiona4bb0512016-04-19 18:04:31 -0400232 if (auto call = RecentModel::instance().getActiveCall(qIdx)) {
Alexandre Lisiond14bda32015-10-13 11:34:29 -0400233 [details setStringValue:call->roleData((int)Ring::Role::FormattedState).toString().toNSString()];
Alexandre Lision21666f32015-09-22 17:04:36 -0400234 [((ContextualTableCellView*) result) setActiveState:YES];
235 } else {
Alexandre Lisiond14bda32015-10-13 11:34:29 -0400236 [details setStringValue:qIdx.data((int)Ring::Role::FormattedLastUsed).toString().toNSString()];
Alexandre Lision21666f32015-09-22 17:04:36 -0400237 [((ContextualTableCellView*) result) setActiveState:NO];
238 }
239
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500240 NSTextField* unreadCount = [result viewWithTag:TXT_BUTTON_TAG];
241 int unread = qIdx.data((int)Ring::Role::UnreadTextMessageCount).toInt();
242 [unreadCount setHidden:(unread == 0)];
243 [unreadCount.layer setCornerRadius:5.0f];
244 [unreadCount setStringValue:qIdx.data((int)Ring::Role::UnreadTextMessageCount).toString().toNSString()];
245
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400246 } else {
247 result = [outlineView makeViewWithIdentifier:@"CallCell" owner:outlineView];
Alexandre Lision57914fa2016-02-10 14:52:24 -0500248 NSMutableArray* controls = [NSMutableArray arrayWithObject:[result viewWithTag:CANCEL_BUTTON_TAG]];
249 [((ContextualTableCellView*) result) setContextualsControls:controls];
250 [((ContextualTableCellView*) result) setActiveState:NO];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400251 NSTextField* details = [result viewWithTag:DETAILS_TAG];
252
253 [details setStringValue:qIdx.data((int)Call::Role::HumanStateName).toString().toNSString()];
254 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400255
256 NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500257 [displayName setStringValue:qIdx.data((int)Ring::Role::Name).toString().toNSString()];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400258 NSImageView* photoView = [result viewWithTag:IMAGE_TAG];
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400259
260 [photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400261 return result;
262}
263
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500264- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
265{
266 return [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
267}
268
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400269- (IBAction)callClickedAtRow:(id)sender {
270 NSInteger row = [smartView rowForView:sender];
271 [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
272 [self placeCall:nil];
273}
274
275- (IBAction)hangUpClickedAtRow:(id)sender {
276 NSInteger row = [smartView rowForView:sender];
Alexandre Lision57914fa2016-02-10 14:52:24 -0500277 id callNode = [smartView itemAtRow:row];
278 auto callIdx = [treeController toQIdx:((NSTreeNode*)callNode)];
279
280 if (callIdx.isValid()) {
281 auto call = RecentModel::instance().getActiveCall(RecentModel::instance().peopleProxy()->mapToSource(callIdx));
282 call << Call::Action::REFUSE;
283 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400284}
285
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400286- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
287{
288 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
Alexandre Lision57914fa2016-02-10 14:52:24 -0500289 return (((NSTreeNode*)item).indexPath.length == 1) ? 60.0 : 50.0;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400290}
291
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500292- (IBAction)placeCallFromSearchField:(id)sender
293{
294 if ([searchField stringValue].length == 0) {
295 return;
296 }
Alexandre Lision45c18672016-11-02 15:24:45 -0400297 [self processSearchFieldInputAndStartCall:YES];
298}
299
300- (void) startCallFromURI:(const URI&) uri
301{
302 auto cm = PhoneDirectoryModel::instance().getNumber(uri);
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500303 auto c = CallModel::instance().dialingCall();
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500304 c->setPeerContactMethod(cm);
305 c << Call::Action::ACCEPT;
306 CallModel::instance().selectCall(c);
307}
308
Alexandre Lision45c18672016-11-02 15:24:45 -0400309- (void) startConversationFromURI:(const URI&) uri
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400310{
Alexandre Lision45c18672016-11-02 15:24:45 -0400311 auto cm = PhoneDirectoryModel::instance().getNumber(uri);
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500312 time_t currentTime;
313 ::time(&currentTime);
314 cm->setLastUsed(currentTime);
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500315 auto proxyIdx = RecentModel::instance().peopleProxy()->mapToSource(RecentModel::instance().peopleProxy()->index(0, 0));
316 RecentModel::instance().selectionModel()->setCurrentIndex(proxyIdx, QItemSelectionModel::ClearAndSelect);
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400317}
318
Alexandre Lision45c18672016-11-02 15:24:45 -0400319- (void) displayErrorModalWithTitle:(NSString*) title WithMessage:(NSString*) message
320{
321 NSAlert* alert = [NSAlert alertWithMessageText:title
322 defaultButton:@"Ok"
323 alternateButton:nil
324 otherButton:nil
325 informativeTextWithFormat:message];
326
327 [alert beginSheetModalForWindow:self.view.window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
328}
329
330- (void) processSearchFieldInputAndStartCall:(BOOL) shouldCall
331{
332 auto ringAccountList = AccountModel::instance().getAccountsByProtocol(Account::Protocol::RING);
333 auto sipAccountList = AccountModel::instance().getAccountsByProtocol(Account::Protocol::SIP);
334 BOOL hasValidRingAccount = NO;
335 BOOL hasValidSIPAccount = NO;
336
337 NSString* noValidAccountTitle = NSLocalizedString(@"No valid account available",
338 @"Alert dialog title");
339 NSString* noValidAccountMessage = NSLocalizedString(@"Make sure you have at least one valid account",
340 @"Alert dialo message");
341
342 Q_FOREACH(auto account, ringAccountList) {
343 if (account->isEnabled() && account->registrationState() == Account::RegistrationState::READY) {
344 hasValidRingAccount = YES;
345 }
346 }
347
348 Q_FOREACH(auto account, sipAccountList) {
349 if (account->isEnabled() && account->registrationState() == Account::RegistrationState::READY) {
350 hasValidSIPAccount = YES;
351 }
352 }
353
354 const auto* numberEntered = [searchField stringValue];
355 URI uri = URI(numberEntered.UTF8String);
Alexandre Lisionf909fb12016-11-03 12:35:36 -0400356 [self clearSearchField];
Alexandre Lision45c18672016-11-02 15:24:45 -0400357
358 if (hasValidRingAccount) {
359 if (uri.protocolHint() == URI::ProtocolHint::RING) {
360 // If it is a RingID start the conversation or the call
361 if (shouldCall) {
362 [self startCallFromURI:uri];
363 } else {
364 [self startConversationFromURI:uri];
365 }
366 } else {
367 // If it's not a ringID and the user has a valid Ring account do a search on the blockchain
368 // If the user wants to make SIP calls, he can unregister his ring account
369 QString usernameToLookup = uri.userinfo();
370 QObject::disconnect(usernameLookupConnection);
371 usernameLookupConnection = QObject::connect(&NameDirectory::instance(),
372 &NameDirectory::registeredNameFound,
373 [self, usernameToLookup, hasValidSIPAccount, shouldCall] (const Account* account, NameDirectory::LookupStatus status, const QString& address, const QString& name) {
374 if (usernameToLookup.compare(name) != 0) {
375 //That is not our lookup.
376 return;
377 }
378
379 switch(status) {
380 case NameDirectory::LookupStatus::SUCCESS: {
381 URI uri = URI("ring:" + address);
382 if (shouldCall) {
383 [self startCallFromURI:uri];
384 } else {
385 [self startConversationFromURI:uri];
386 }
387 break;
388 }
389 case NameDirectory::LookupStatus::INVALID_NAME:
390 case NameDirectory::LookupStatus::ERROR:
391 case NameDirectory::LookupStatus::NOT_FOUND: {
392 // The lookup on the blockchain has failed for this user
393 // Check if the user has a valid sip account to use instead
394 if (hasValidSIPAccount) {
395 if (shouldCall) {
396 [self startCallFromURI:usernameToLookup];
397 } else {
398 [self startConversationFromURI:usernameToLookup];
399 }
400 } else {
401 [self displayErrorModalWithTitle:NSLocalizedString(@"Entered name not found",
402 @"Alert dialog title")
403 WithMessage:NSLocalizedString(@"The username you entered do not match a RingID on the network",
404 @"Alert dialog title")];
405 }
406 break;
407 }
408 }
Alexandre Lisionf909fb12016-11-03 12:35:36 -0400409 });
410
Alexandre Lision45c18672016-11-02 15:24:45 -0400411 NameDirectory::instance().lookupName(nullptr, QString(), usernameToLookup);
412 }
413 } else if (hasValidSIPAccount) {
414 if (uri.protocolHint() == URI::ProtocolHint::RING) {
415 // If it is a RingID and no valid account is available, present error
416 [self displayErrorModalWithTitle:noValidAccountTitle
417 WithMessage:noValidAccountMessage];
Alexandre Lisionf909fb12016-11-03 12:35:36 -0400418 return;
Alexandre Lision45c18672016-11-02 15:24:45 -0400419 }
420 if (shouldCall) {
421 [self startCallFromURI:uri];
422 } else {
423 [self startConversationFromURI:uri];
424 }
425 } else {
426 [self displayErrorModalWithTitle:noValidAccountTitle
427 WithMessage:noValidAccountMessage];
428 }
429}
430
Alexandre Lisionf909fb12016-11-03 12:35:36 -0400431- (void) clearSearchField
432{
433 [searchField setStringValue:@""];
434 RecentModel::instance().peopleProxy()->setFilterWildcard(QString::fromNSString([searchField stringValue]));
435}
436
Alexandre Lision61db3552015-10-22 19:12:52 -0400437- (void) addToContact
438{
439 if ([treeController selectedNodes].count == 0)
440 return;
441
442 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
443 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
444 auto contactmethod = RecentModel::instance().getContactMethods(originIdx);
445 if (contactmethod.isEmpty())
446 return;
447
448 if (addToContactPopover != nullptr) {
449 [addToContactPopover performClose:self];
450 addToContactPopover = NULL;
451 } else if (contactmethod.first()) {
452 auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
453 [editorVC setMethodToLink:contactmethod.first()];
454 [editorVC setContactLinkedDelegate:self];
455 addToContactPopover = [[NSPopover alloc] init];
456 [addToContactPopover setContentSize:editorVC.view.frame.size];
457 [addToContactPopover setContentViewController:editorVC];
458 [addToContactPopover setAnimates:YES];
459 [addToContactPopover setBehavior:NSPopoverBehaviorTransient];
460 [addToContactPopover setDelegate:self];
461
462 [addToContactPopover showRelativeToRect:[smartView frameOfCellAtColumn:0 row:[smartView selectedRow]]
463 ofView:smartView preferredEdge:NSMaxXEdge];
464 }
465}
466
Alexandre Lision72a669e2016-09-14 17:52:16 -0400467/**
468 Copy a NSString in the general Pasteboard
469
470 @param sender the NSObject containing the represented object to copy
471 */
472- (void) copyStringToPasteboard:(id) sender
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400473{
474 NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
475 [pasteBoard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
476 [pasteBoard setString:[sender representedObject] forType:NSStringPboardType];
477}
478
479- (void) callNumber:(id) sender
480{
481 Call* c = CallModel::instance().dialingCall();
482 c->setDialNumber(QString::fromNSString([sender representedObject]));
483 c << Call::Action::ACCEPT;
484}
485
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400486#pragma NSTextFieldDelegate
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400487
488- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
489{
490 if (commandSelector == @selector(insertNewline:)) {
491 if([[searchField stringValue] isNotEqualTo:@""]) {
Alexandre Lision45c18672016-11-02 15:24:45 -0400492 [self processSearchFieldInputAndStartCall:NO];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400493 return YES;
494 }
495 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400496 return NO;
497}
498
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500499- (void)controlTextDidChange:(NSNotification *) notification
500{
Alexandre Lisionac4f5b62016-02-17 13:53:12 -0500501 RecentModel::instance().peopleProxy()->setFilterWildcard(QString::fromNSString([searchField stringValue]));
502}
503
Alexandre Lision61db3552015-10-22 19:12:52 -0400504#pragma mark - NSPopOverDelegate
505
506- (void)popoverDidClose:(NSNotification *)notification
507{
508 if (addToContactPopover != nullptr) {
509 [addToContactPopover performClose:self];
510 addToContactPopover = NULL;
511 }
512}
513
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400514
Alexandre Lision61db3552015-10-22 19:12:52 -0400515#pragma mark - ContactLinkedDelegate
516
517- (void)contactLinked
518{
519 if (addToContactPopover != nullptr) {
520 [addToContactPopover performClose:self];
521 addToContactPopover = NULL;
522 }
523}
524
525#pragma mark - KeyboardShortcutDelegate
526
527- (void) onAddShortcut
528{
529 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
530 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
531 auto contactmethods = RecentModel::instance().getContactMethods(originIdx);
532 if (contactmethods.isEmpty())
533 return;
534
535 auto contactmethod = contactmethods.first();
536 if (contactmethod && (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder())) {
537 [self addToContact];
538 }
539}
540
541#pragma mark - ContextMenuDelegate
542
Alexandre Lision12946a72016-03-08 13:39:34 -0500543- (NSMenu*) contextualMenuForIndex:(NSTreeNode*) item
Alexandre Lision61db3552015-10-22 19:12:52 -0400544{
Alexandre Lision12946a72016-03-08 13:39:34 -0500545 auto qIdx = [treeController toQIdx:item];
546
547 if (!qIdx.isValid()) {
548 return nil;
549 }
550
Alexandre Lision61db3552015-10-22 19:12:52 -0400551 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
552 auto contactmethods = RecentModel::instance().getContactMethods(originIdx);
553 if (contactmethods.isEmpty())
554 return nil;
555
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400556 NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@""];
557
558 if (contactmethods.size() == 1
559 && !contactmethods.first()->contact()
560 || contactmethods.first()->contact()->isPlaceHolder()) {
561
Alexandre Lision61db3552015-10-22 19:12:52 -0400562 [theMenu insertItemWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
563 action:@selector(addToContact)
564 keyEquivalent:@"a"
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400565 atIndex:theMenu.itemArray.count];
Alexandre Lision72a669e2016-09-14 17:52:16 -0400566 } else if (auto person = contactmethods.first()->contact()) {
567 NSMenuItem* copyNameItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy name", @"Contextual menu action")
568 action:@selector(copyStringToPasteboard:)
569 keyEquivalent:@""];
570
571 [copyNameItem setRepresentedObject:person->formattedName().toNSString()];
572 [theMenu addItem:copyNameItem];
Alexandre Lision61db3552015-10-22 19:12:52 -0400573 }
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400574
575 NSMenu* copySubmenu = [[NSMenu alloc] init];
576 NSMenu* callSubmenu = [[NSMenu alloc] init];
577
Alexandre Lision72a669e2016-09-14 17:52:16 -0400578 for (auto cm : contactmethods) {
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400579 NSMenuItem* tmpCopyItem = [[NSMenuItem alloc] initWithTitle:cm->uri().toNSString()
Alexandre Lision72a669e2016-09-14 17:52:16 -0400580 action:@selector(copyStringToPasteboard:)
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400581 keyEquivalent:@""];
582
583 [tmpCopyItem setRepresentedObject:cm->uri().toNSString()];
584 [copySubmenu addItem:tmpCopyItem];
585
586 NSMenuItem* tmpCallItem = [[NSMenuItem alloc] initWithTitle:cm->uri().toNSString()
587 action:@selector(callNumber:)
588 keyEquivalent:@""];
589 [tmpCallItem setRepresentedObject:cm->uri().toNSString()];
590 [callSubmenu addItem:tmpCallItem];
591 }
592
Alexandre Lision72a669e2016-09-14 17:52:16 -0400593 NSMenuItem* copyNumberItem = [[NSMenuItem alloc] init];
594 [copyNumberItem setTitle:NSLocalizedString(@"Copy number", @"Contextual menu action")];
595 [copyNumberItem setSubmenu:copySubmenu];
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400596
597 NSMenuItem* callItems = [[NSMenuItem alloc] init];
598 [callItems setTitle:NSLocalizedString(@"Call number", @"Contextual menu action")];
599 [callItems setSubmenu:callSubmenu];
600
Alexandre Lision72a669e2016-09-14 17:52:16 -0400601 [theMenu insertItem:copyNumberItem atIndex:theMenu.itemArray.count];
Alexandre Lisiond7bf2882015-10-22 22:54:32 -0400602 [theMenu insertItem:[NSMenuItem separatorItem] atIndex:theMenu.itemArray.count];
603 [theMenu insertItem:callItems atIndex:theMenu.itemArray.count];
604
605 return theMenu;
Alexandre Lision61db3552015-10-22 19:12:52 -0400606}
607
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400608@end