blob: 77cf1ebfc34dc0e4cc70753618f79d38799bef2e [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 Lisiond14bda32015-10-13 11:34:29 -040032#import <itemdataroles.h>
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040033#import <person.h>
34#import <contactmethod.h>
35#import <globalinstances.h>
36
37#import "QNSTreeController.h"
38#import "delegates/ImageManipulationDelegate.h"
Alexandre Lision4e280d62015-09-09 15:56:30 -040039#import "views/HoverTableRowView.h"
Alexandre Lision61db3552015-10-22 19:12:52 -040040#import "PersonLinkerVC.h"
41#import "views/RingOutlineView.h"
Alexandre Lision4e280d62015-09-09 15:56:30 -040042#import "views/ContextualTableCellView.h"
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040043
Alexandre Lision61db3552015-10-22 19:12:52 -040044@interface SmartViewVC () <NSOutlineViewDelegate, NSPopoverDelegate, ContextMenuDelegate, ContactLinkedDelegate, KeyboardShortcutDelegate> {
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040045 BOOL isShowingContacts;
46 QNSTreeController *treeController;
Alexandre Lision61db3552015-10-22 19:12:52 -040047 NSPopover* addToContactPopover;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040048
49 //UI elements
Alexandre Lision61db3552015-10-22 19:12:52 -040050 __unsafe_unretained IBOutlet RingOutlineView* smartView;
51 __unsafe_unretained IBOutlet NSSearchField* searchField;
52 __unsafe_unretained IBOutlet NSButton* showContactsButton;
53 __unsafe_unretained IBOutlet NSButton* showHistoryButton;
54 __unsafe_unretained IBOutlet NSTabView* tabbar;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040055}
56
57@end
58
59@implementation SmartViewVC
60
61// Tags for views
62NSInteger const IMAGE_TAG = 100;
63NSInteger const DISPLAYNAME_TAG = 200;
64NSInteger const DETAILS_TAG = 300;
65NSInteger const CALL_BUTTON_TAG = 400;
66NSInteger const TXT_BUTTON_TAG = 500;
67
68- (void)awakeFromNib
69{
70 NSLog(@"INIT SmartView VC");
71
72 isShowingContacts = false;
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040073 treeController = [[QNSTreeController alloc] initWithQModel:RecentModel::instance().peopleProxy()];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040074
75 [treeController setAvoidsEmptySelection:NO];
76 [treeController setChildrenKeyPath:@"children"];
77
78 [smartView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
79 [smartView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
80 [smartView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
81 [smartView setTarget:self];
Alexandre Lision89edc6a2015-11-09 11:30:47 -050082 [smartView setAction:@selector(selectRow:)];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040083 [smartView setDoubleAction:@selector(placeCall:)];
84
Alexandre Lision61db3552015-10-22 19:12:52 -040085 [smartView setContextMenuDelegate:self];
86 [smartView setShortcutsDelegate:self];
87
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040088 QObject::connect(RecentModel::instance().peopleProxy(),
Alexandre Lisionee098462015-10-22 17:22:50 -040089 &QAbstractItemModel::dataChanged,
90 [self](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
91 for(int row = topLeft.row() ; row <= bottomRight.row() ; ++row)
92 {
93 [smartView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row]
94 columnIndexes:[NSIndexSet indexSetWithIndex:0]];
95 }
96 });
97
Alexandre Lision89edc6a2015-11-09 11:30:47 -050098 QObject::connect(RecentModel::instance().selectionModel(),
99 &QItemSelectionModel::currentChanged,
100 [=](const QModelIndex &current, const QModelIndex &previous) {
101 if(!current.isValid())
102 return;
103
104 auto proxyIdx = RecentModel::instance().peopleProxy()->mapFromSource(current);
105 if (proxyIdx.isValid()) {
106 [treeController setSelectionQModelIndex:proxyIdx];
107
108 [showContactsButton setState:NO];
109 isShowingContacts = NO;
110 [showHistoryButton setState:NO];
111 [tabbar selectTabViewItemAtIndex:0];
112 [smartView scrollRowToVisible:proxyIdx.row()];
113 }
114 });
115
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400116 [self.view setWantsLayer:YES];
117 [self.view setLayer:[CALayer layer]];
118 [self.view.layer setBackgroundColor:[NSColor whiteColor].CGColor];
119
120 [searchField setWantsLayer:YES];
121 [searchField setLayer:[CALayer layer]];
122 [searchField.layer setBackgroundColor:[NSColor colorWithCalibratedRed:0.949 green:0.949 blue:0.949 alpha:0.9].CGColor];
123}
124
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500125-(void) selectRow:(id)sender
126{
127 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
128 auto proxyIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
129 RecentModel::instance().selectionModel()->setCurrentIndex(proxyIdx, QItemSelectionModel::ClearAndSelect);
130}
131
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400132- (void)placeCall:(id)sender
133{
134 QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
135 ContactMethod* m = nil;
136
137 // Double click on an ongoing call
138 if (qIdx.parent().isValid()) {
139 return;
140 }
141
142 if([[treeController selectedNodes] count] > 0) {
143 QVariant var = qIdx.data((int)Call::Role::ContactMethod);
144 m = qvariant_cast<ContactMethod*>(var);
145 if (!m) {
146 // test if it is a person
147 QVariant var = qIdx.data((int)Person::Role::Object);
148 if (var.isValid()) {
149 Person *c = var.value<Person*>();
150 if (c->phoneNumbers().size() > 0) {
151 m = c->phoneNumbers().first();
152 }
153 }
154 }
155 }
156
157 // Before calling check if we properly extracted a contact method and that
158 // there is NOT already an ongoing call for this index (e.g: no children for this node)
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400159 if(m && !RecentModel::instance().peopleProxy()->index(0, 0, qIdx).isValid()){
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500160 auto c = CallModel::instance().dialingCall();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400161 c->setPeerContactMethod(m);
162 c << Call::Action::ACCEPT;
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500163 CallModel::instance().selectCall(c);
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400164 }
165}
166
167- (IBAction)showHistory:(NSButton*)sender {
168 if (isShowingContacts) {
169 [showContactsButton setState:NO];
170 isShowingContacts = NO;
171 [tabbar selectTabViewItemAtIndex:1];
172 } else if ([sender state] == NSOffState) {
173 [tabbar selectTabViewItemAtIndex:0];
174 } else {
175 [tabbar selectTabViewItemAtIndex:1];
176 }
177}
178
179- (IBAction)showContacts:(NSButton*)sender {
180 if (isShowingContacts) {
181 [showContactsButton setState:NO];
182 [tabbar selectTabViewItemAtIndex:0];
183 } else {
184 [showHistoryButton setState:![sender state]];
185 [tabbar selectTabViewItemAtIndex:2];
186 }
187
188 isShowingContacts = [sender state];
189}
190
191#pragma mark - NSOutlineViewDelegate methods
192
193// -------------------------------------------------------------------------------
194// shouldSelectItem:item
195// -------------------------------------------------------------------------------
196- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
197{
198 return YES;
199}
200
201// -------------------------------------------------------------------------------
202// shouldEditTableColumn:tableColumn:item
203//
204// Decide to allow the edit of the given outline view "item".
205// -------------------------------------------------------------------------------
206- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
207{
208 return NO;
209}
210
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400211// -------------------------------------------------------------------------------
212// outlineViewSelectionDidChange:notification
213// -------------------------------------------------------------------------------
214- (void)outlineViewSelectionDidChange:(NSNotification *)notification
215{
216 if ([treeController selectedNodes].count <= 0) {
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500217 RecentModel::instance().selectionModel()->clearCurrentIndex();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400218 return;
219 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400220}
221
222/* View Based OutlineView: See the delegate method -tableView:viewForTableColumn:row: in NSTableView.
223 */
224- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
225{
226 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
227 NSTableCellView *result;
228 if (!qIdx.parent().isValid()) {
229 result = [outlineView makeViewWithIdentifier:@"MainCell" owner:outlineView];
230 NSTextField* details = [result viewWithTag:DETAILS_TAG];
231
Alexandre Lision4e280d62015-09-09 15:56:30 -0400232 [((ContextualTableCellView*) result) setContextualsControls:[NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]]];
233
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400234 if (auto call = RecentModel::instance().getActiveCall(RecentModel::instance().peopleProxy()->mapToSource(qIdx))) {
Alexandre Lisiond14bda32015-10-13 11:34:29 -0400235 [details setStringValue:call->roleData((int)Ring::Role::FormattedState).toString().toNSString()];
Alexandre Lision21666f32015-09-22 17:04:36 -0400236 [((ContextualTableCellView*) result) setActiveState:YES];
237 } else {
Alexandre Lisiond14bda32015-10-13 11:34:29 -0400238 [details setStringValue:qIdx.data((int)Ring::Role::FormattedLastUsed).toString().toNSString()];
Alexandre Lision21666f32015-09-22 17:04:36 -0400239 [((ContextualTableCellView*) result) setActiveState:NO];
240 }
241
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400242 } else {
243 result = [outlineView makeViewWithIdentifier:@"CallCell" owner:outlineView];
244 NSTextField* details = [result viewWithTag:DETAILS_TAG];
245
246 [details setStringValue:qIdx.data((int)Call::Role::HumanStateName).toString().toNSString()];
247 }
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400248
249 NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
250 [displayName setStringValue:qIdx.data(Qt::DisplayRole).toString().toNSString()];
251 NSImageView* photoView = [result viewWithTag:IMAGE_TAG];
252 Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500253 QVariant photo = GlobalInstances::pixmapManipulator().contactPhoto(p, QSize(50,50));
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400254 [photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))];
255 return result;
256}
257
258- (IBAction)callClickedAtRow:(id)sender {
259 NSInteger row = [smartView rowForView:sender];
260 [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
261 [self placeCall:nil];
262}
263
264- (IBAction)hangUpClickedAtRow:(id)sender {
265 NSInteger row = [smartView rowForView:sender];
266 [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400267 CallModel::instance().getCall(CallModel::instance().selectionModel()->currentIndex()) << Call::Action::REFUSE;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400268}
269
270/* View Based OutlineView: See the delegate method -tableView:rowViewForRow: in NSTableView.
Alexandre Lision4e280d62015-09-09 15:56:30 -0400271*/
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400272- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
273{
Alexandre Lision4e280d62015-09-09 15:56:30 -0400274 return [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400275}
Alexandre Lision4e280d62015-09-09 15:56:30 -0400276
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400277
278/* View Based OutlineView: This delegate method can be used to know when a new 'rowView' has been added to the table. At this point, you can choose to add in extra views, or modify any properties on 'rowView'.
279 */
280- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
281{
282
283}
284
285/* View Based OutlineView: This delegate method can be used to know when 'rowView' has been removed from the table. The removed 'rowView' may be reused by the table so any additionally inserted views should be removed at this point. A 'row' parameter is included. 'row' will be '-1' for rows that are being deleted from the table and no longer have a valid row, otherwise it will be the valid row that is being removed due to it being moved off screen.
286 */
287- (void)outlineView:(NSOutlineView *)outlineView didRemoveRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
288{
289
290}
291
292- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
293{
294 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
295 return (((NSTreeNode*)item).indexPath.length == 1) ? 60.0 : 45.0;
296}
297
298- (void) placeCallFromSearchField
299{
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400300 Call* c = CallModel::instance().dialingCall();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400301 // check for a valid ring hash
302 NSCharacterSet *hexSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"];
303 BOOL valid = [[[searchField stringValue] stringByTrimmingCharactersInSet:hexSet] isEqualToString:@""];
304
305 if(valid && searchField.stringValue.length == 40) {
306 c->setDialNumber(QString::fromNSString([NSString stringWithFormat:@"ring:%@",[searchField stringValue]]));
307 } else {
308 c->setDialNumber(QString::fromNSString([searchField stringValue]));
309 }
310
311 c << Call::Action::ACCEPT;
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400312
313 [searchField setStringValue:@""];
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400314 RecentModel::instance().peopleProxy()->
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400315 setFilterRegExp(QRegExp(QString::fromNSString([searchField stringValue]), Qt::CaseInsensitive, QRegExp::FixedString));
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400316}
317
Alexandre Lision61db3552015-10-22 19:12:52 -0400318- (void) addToContact
319{
320 if ([treeController selectedNodes].count == 0)
321 return;
322
323 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
324 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
325 auto contactmethod = RecentModel::instance().getContactMethods(originIdx);
326 if (contactmethod.isEmpty())
327 return;
328
329 if (addToContactPopover != nullptr) {
330 [addToContactPopover performClose:self];
331 addToContactPopover = NULL;
332 } else if (contactmethod.first()) {
333 auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
334 [editorVC setMethodToLink:contactmethod.first()];
335 [editorVC setContactLinkedDelegate:self];
336 addToContactPopover = [[NSPopover alloc] init];
337 [addToContactPopover setContentSize:editorVC.view.frame.size];
338 [addToContactPopover setContentViewController:editorVC];
339 [addToContactPopover setAnimates:YES];
340 [addToContactPopover setBehavior:NSPopoverBehaviorTransient];
341 [addToContactPopover setDelegate:self];
342
343 [addToContactPopover showRelativeToRect:[smartView frameOfCellAtColumn:0 row:[smartView selectedRow]]
344 ofView:smartView preferredEdge:NSMaxXEdge];
345 }
346}
347
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400348#pragma NSTextFieldDelegate
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400349
350- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
351{
352 if (commandSelector == @selector(insertNewline:)) {
353 if([[searchField stringValue] isNotEqualTo:@""]) {
354 [self placeCallFromSearchField];
355 return YES;
356 }
357 }
358
359 return NO;
360}
361
Alexandre Lision61db3552015-10-22 19:12:52 -0400362#pragma mark - NSPopOverDelegate
363
364- (void)popoverDidClose:(NSNotification *)notification
365{
366 if (addToContactPopover != nullptr) {
367 [addToContactPopover performClose:self];
368 addToContactPopover = NULL;
369 }
370}
371
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400372- (void)controlTextDidChange:(NSNotification *) notification
373{
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -0400374 RecentModel::instance().peopleProxy()->
Alexandre Lisionbf0385e2015-10-22 17:36:28 -0400375 setFilterRegExp(QRegExp(QString::fromNSString([searchField stringValue]), Qt::CaseInsensitive, QRegExp::FixedString));
376}
377
Alexandre Lision61db3552015-10-22 19:12:52 -0400378#pragma mark - ContactLinkedDelegate
379
380- (void)contactLinked
381{
382 if (addToContactPopover != nullptr) {
383 [addToContactPopover performClose:self];
384 addToContactPopover = NULL;
385 }
386}
387
388#pragma mark - KeyboardShortcutDelegate
389
390- (void) onAddShortcut
391{
392 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
393 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
394 auto contactmethods = RecentModel::instance().getContactMethods(originIdx);
395 if (contactmethods.isEmpty())
396 return;
397
398 auto contactmethod = contactmethods.first();
399 if (contactmethod && (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder())) {
400 [self addToContact];
401 }
402}
403
404#pragma mark - ContextMenuDelegate
405
406- (NSMenu*) contextualMenuForIndex:(NSIndexPath*) path
407{
408 auto qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
409 auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
410 auto contactmethods = RecentModel::instance().getContactMethods(originIdx);
411 if (contactmethods.isEmpty())
412 return nil;
413
414 auto cm = contactmethods.first();
415 if (!cm->contact() || cm->contact()->isPlaceHolder()) {
416 NSMenu *theMenu = [[NSMenu alloc]
417 initWithTitle:@""];
418 [theMenu insertItemWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
419 action:@selector(addToContact)
420 keyEquivalent:@"a"
421 atIndex:0];
422 return theMenu;
423 }
424 return nil;
425}
426
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400427@end