blob: 520ef8668a0d975a1b84439aef3854779e22e0df [file] [log] [blame]
Alexandre Lision8521baa2015-03-13 11:08:00 -04001/*
Alexandre Lision9fe374b2016-01-06 10:17:31 -05002 * Copyright (C) 2015-2016 Savoir-faire Linux Inc.
Alexandre Lision8521baa2015-03-13 11:08:00 -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.
Alexandre Lision8521baa2015-03-13 11:08:00 -040018 */
Alexandre Lision4ba18022015-04-23 12:17:40 -040019#import "HistoryVC.h"
Alexandre Lision5855b6a2015-02-03 11:31:05 -050020
Alexandre Lision34e738e2015-09-24 14:13:23 -040021//Qt
Alexandre Lision7837e4f2015-03-23 09:58:12 -040022#import <QSortFilterProxyModel>
Alexandre Lisiond5229f32015-11-16 11:17:41 -050023#import <QtMacExtras/qmacfunctions.h>
24#import <QPixmap>
Alexandre Lision34e738e2015-09-24 14:13:23 -040025
26//LRC
27#import <categorizedhistorymodel.h>
Alexandre Lisionc5148052015-03-04 15:10:35 -050028#import <callmodel.h>
29#import <call.h>
Alexandre Lision2db8f472015-07-22 15:05:46 -040030#import <person.h>
Alexandre Lisionc5148052015-03-04 15:10:35 -050031#import <contactmethod.h>
Alexandre Lisiond5229f32015-11-16 11:17:41 -050032#import <globalinstances.h>
Anthony Léonard60970852017-07-21 11:33:41 -040033#import <personmodel.h>
34#import <peerprofilecollection.h>
Alexandre Lision5855b6a2015-02-03 11:31:05 -050035
Alexandre Lisionc5148052015-03-04 15:10:35 -050036#import "QNSTreeController.h"
Alexandre Lision2db8f472015-07-22 15:05:46 -040037#import "PersonLinkerVC.h"
Alexandre Lision4e280d62015-09-09 15:56:30 -040038#import "views/HoverTableRowView.h"
Alexandre Lisiond5229f32015-11-16 11:17:41 -050039#import "delegates/ImageManipulationDelegate.h"
Alexandre Lisionc5148052015-03-04 15:10:35 -050040
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040041@interface HistoryVC() <NSPopoverDelegate, KeyboardShortcutDelegate, ContactLinkedDelegate> {
Alexandre Lision5855b6a2015-02-03 11:31:05 -050042
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040043 QNSTreeController *treeController;
44 IBOutlet RingOutlineView *historyView;
45 QSortFilterProxyModel *historyProxyModel;
46 NSPopover* addToContactPopover;
47}
Alexandre Lision2db8f472015-07-22 15:05:46 -040048
Alexandre Lisionc5148052015-03-04 15:10:35 -050049@end
Alexandre Lision5855b6a2015-02-03 11:31:05 -050050
Alexandre Lision4ba18022015-04-23 12:17:40 -040051@implementation HistoryVC
Alexandre Lision5855b6a2015-02-03 11:31:05 -050052
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040053// Tags for Views
Alexandre Lisiond5229f32015-11-16 11:17:41 -050054NSInteger const DIRECTION_TAG = 100;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040055NSInteger const DISPLAYNAME_TAG = 200;
56NSInteger const DETAILS_TAG = 300;
Alexandre Lisiond5229f32015-11-16 11:17:41 -050057NSInteger const PHOTO_TAG = 400;
Alexandre Lision5855b6a2015-02-03 11:31:05 -050058
Alexandre Lision5855b6a2015-02-03 11:31:05 -050059- (void)awakeFromNib
60{
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040061 NSLog(@"INIT HVC");
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040062 historyProxyModel = new QSortFilterProxyModel(&CategorizedHistoryModel::instance());
63 historyProxyModel->setSourceModel(&CategorizedHistoryModel::instance());
Alexandre Lision7837e4f2015-03-23 09:58:12 -040064 historyProxyModel->setSortRole(static_cast<int>(Call::Role::Date));
65 historyProxyModel->sort(0,Qt::DescendingOrder);
66 treeController = [[QNSTreeController alloc] initWithQModel:historyProxyModel];
Alexandre Lision5855b6a2015-02-03 11:31:05 -050067
68 [treeController setAvoidsEmptySelection:NO];
69 [treeController setChildrenKeyPath:@"children"];
70
Alexandre Lisionc5148052015-03-04 15:10:35 -050071 [historyView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
72 [historyView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
73 [historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
74 [historyView setTarget:self];
Alexandre Lision3b0bd332015-03-15 18:43:07 -040075 [historyView setDoubleAction:@selector(placeHistoryCall:)];
Alexandre Lision2db8f472015-07-22 15:05:46 -040076 [historyView setContextMenuDelegate:self];
77 [historyView setShortcutsDelegate:self];
Alexandre Lision5855b6a2015-02-03 11:31:05 -050078
Alexandre Lisiond3aa3ad2015-10-23 14:28:41 -040079 QObject::connect(&CallModel::instance(),
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040080 &CategorizedHistoryModel::dataChanged,
81 [=](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
82 [historyView reloadDataForRowIndexes:
83 [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(topLeft.row(), bottomRight.row() + 1)]
84 columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, historyView.tableColumns.count)]];
85 });
Alexandre Lision5855b6a2015-02-03 11:31:05 -050086}
87
Alexandre Lision54b0fae2015-08-04 15:19:01 -040088- (void) dealloc
89{
90 delete historyProxyModel;
91}
92
Alexandre Lision3b0bd332015-03-15 18:43:07 -040093- (void)placeHistoryCall:(id)sender
Alexandre Lisionc5148052015-03-04 15:10:35 -050094{
95 if([[treeController selectedNodes] count] > 0) {
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040096 auto item = [treeController selectedNodes][0];
97 QModelIndex qIdx = [treeController toQIdx:item];
Alexandre Lision2db8f472015-07-22 15:05:46 -040098 if (!qIdx.parent().isValid()) {
Alexandre Lision4dfcafc2015-08-20 12:43:23 -040099 if ([historyView isItemExpanded:item]) {
100 [[historyView animator] collapseItem:item];
101 } else
102 [[historyView animator] expandItem:item];
Alexandre Lision2db8f472015-07-22 15:05:46 -0400103 return;
104 }
Alexandre Lision7837e4f2015-03-23 09:58:12 -0400105 QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod);
Alexandre Lisionc5148052015-03-04 15:10:35 -0500106 ContactMethod* m = qvariant_cast<ContactMethod*>(var);
107 if(m){
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500108 auto c = CallModel::instance().dialingCall();
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400109 c->setPeerContactMethod(m);
Alexandre Lisionc5148052015-03-04 15:10:35 -0500110 c << Call::Action::ACCEPT;
Alexandre Lision89edc6a2015-11-09 11:30:47 -0500111 CallModel::instance().selectCall(c);
Alexandre Lisionc5148052015-03-04 15:10:35 -0500112 }
113 }
114}
115
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500116#pragma mark - NSOutlineViewDelegate methods
117
118// -------------------------------------------------------------------------------
119// shouldSelectItem:item
120// -------------------------------------------------------------------------------
121- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
122{
123 return YES;
124}
125
126// -------------------------------------------------------------------------------
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500127// shouldEditTableColumn:tableColumn:item
128//
129// Decide to allow the edit of the given outline view "item".
130// -------------------------------------------------------------------------------
131- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
132{
133 return NO;
134}
135
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400136- (NSImage*) image:(NSImage*) img withTintedWithColor:(NSColor *)tint
137{
138 if (tint) {
139 [img lockFocus];
140 [tint set];
141 NSRect imageRect = {NSZeroPoint, [img size]};
142 NSRectFillUsingOperation(imageRect, NSCompositeSourceAtop);
143 [img unlockFocus];
144 }
145 return img;
146}
147
148/* View Based OutlineView: See the delegate method -tableView:viewForTableColumn:row: in NSTableView.
149 */
150- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500151{
Alexandre Lisionc5148052015-03-04 15:10:35 -0500152 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
Alexandre Lisionc5148052015-03-04 15:10:35 -0500153
Alexandre Lision4e280d62015-09-09 15:56:30 -0400154 NSTableCellView* result;
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400155 if(!qIdx.parent().isValid()) {
156 result = [outlineView makeViewWithIdentifier:@"CategoryCell" owner:outlineView];
Alexandre Lision4e280d62015-09-09 15:56:30 -0400157
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400158 } else {
159 result = [outlineView makeViewWithIdentifier:@"HistoryCell" owner:outlineView];
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500160 NSImageView* directionView = [result viewWithTag:DIRECTION_TAG];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400161
162 if (qvariant_cast<Call::Direction>(qIdx.data((int)Call::Role::Direction)) == Call::Direction::INCOMING) {
163 if (qvariant_cast<Boolean>(qIdx.data((int) Call::Role::Missed))) {
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500164 [directionView setImage:[self image:[NSImage imageNamed:@"ic_call_missed"] withTintedWithColor:[NSColor redColor]]];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400165 } else {
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500166 [directionView setImage:[self image:[NSImage imageNamed:@"ic_call_received"]
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400167 withTintedWithColor:[NSColor colorWithCalibratedRed:116/255.0 green:179/255.0 blue:93/255.0 alpha:1.0]]];
168 }
169 } else {
170 if (qvariant_cast<Boolean>(qIdx.data((int) Call::Role::Missed))) {
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500171 [directionView setImage:[self image:[NSImage imageNamed:@"ic_call_missed"] withTintedWithColor:[NSColor redColor]]];
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400172 } else {
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500173 [directionView setImage:[self image:[NSImage imageNamed:@"ic_call_made"]
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400174 withTintedWithColor:[NSColor colorWithCalibratedRed:116/255.0 green:179/255.0 blue:93/255.0 alpha:1.0]]];
175 }
176 }
177
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500178 auto call = qvariant_cast<Call*>(qIdx.data((int)Call::Role::Object));
179
180 NSImageView* photoView = [result viewWithTag:PHOTO_TAG];
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400181 [photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
Alexandre Lisiond5229f32015-11-16 11:17:41 -0500182
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400183 NSTextField* details = [result viewWithTag:DETAILS_TAG];
184 [details setStringValue:qIdx.data((int)Call::Role::FormattedDate).toString().toNSString()];
185 }
186
187 NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
188 [displayName setStringValue:qIdx.data(Qt::DisplayRole).toString().toNSString()];
189
190 return result;
191}
192
193- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
194{
195 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
196 if(!qIdx.parent().isValid()) {
197 return 35.0;
198 } else {
199 return 48.0;
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500200 }
201}
202
Alexandre Lision4e280d62015-09-09 15:56:30 -0400203/* View Based OutlineView: See the delegate method -tableView:rowViewForRow: in NSTableView.
204 */
205- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500206{
Alexandre Lision4e280d62015-09-09 15:56:30 -0400207 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
208
209 HoverTableRowView* result = [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
210 if(!qIdx.parent().isValid()) {
211 [result setHighlightable:NO];
212 } else
213 [result setHighlightable:YES];
214
215 return result;
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500216}
217
Alexandre Lision2db8f472015-07-22 15:05:46 -0400218#pragma mark - ContextMenuDelegate
219
Alexandre Lision12946a72016-03-08 13:39:34 -0500220- (NSMenu*) contextualMenuForIndex:(NSTreeNode*) item
Alexandre Lision2db8f472015-07-22 15:05:46 -0400221{
Alexandre Lision12946a72016-03-08 13:39:34 -0500222
223 QModelIndex qIdx = [treeController toQIdx:item];
224 if (!qIdx.isValid()) {
225 return nil;
226 }
227
228 const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
229 if (qIdx.parent().isValid() && var.isValid()) {
230 if (auto call = var.value<Call *>()) {
231 auto contactmethod = call->peerContactMethod();
232 if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
233 NSMenu *theMenu = [[NSMenu alloc]
234 initWithTitle:@""];
Kateryna Kostiuk9dd759c2017-07-11 12:06:55 -0400235 NSMenuItem* addContactItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
236 action:@selector(addContactForRow:)
237 keyEquivalent:@""];
238
239 [addContactItem setRepresentedObject:item];
240 [theMenu addItem:addContactItem];
Alexandre Lision12946a72016-03-08 13:39:34 -0500241 return theMenu;
Alexandre Lision2db8f472015-07-22 15:05:46 -0400242 }
243 }
244 }
245 return nil;
246}
247
248- (void) addToContact
249{
250 ContactMethod* contactmethod = nullptr;
251 if([[treeController selectedNodes] count] > 0) {
252 QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
253 const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
254 if (qIdx.parent().isValid() && var.isValid()) {
255 if (auto call = var.value<Call *>()) {
256 contactmethod = call->peerContactMethod();
257 }
258 }
259 }
260
Anthony Léonard60970852017-07-21 11:33:41 -0400261 // TODO: Uncomment to reuse contact name editing popover
262// if (addToContactPopover != nullptr) {
263// [addToContactPopover performClose:self];
264// addToContactPopover = NULL;
265// } else if (contactmethod) {
266// auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
267// [editorVC setMethodToLink:contactmethod];
268// [editorVC setContactLinkedDelegate:self];
269// addToContactPopover = [[NSPopover alloc] init];
270// [addToContactPopover setContentSize:editorVC.view.frame.size];
271// [addToContactPopover setContentViewController:editorVC];
272// [addToContactPopover setAnimates:YES];
273// [addToContactPopover setBehavior:NSPopoverBehaviorTransient];
274// [addToContactPopover setDelegate:self];
275//
276// [addToContactPopover showRelativeToRect:[historyView frameOfOutlineCellAtRow:[historyView selectedRow]] ofView:historyView preferredEdge:NSMaxXEdge];
277// }
Alexandre Lision2db8f472015-07-22 15:05:46 -0400278
Anthony Léonard60970852017-07-21 11:33:41 -0400279 auto* newPerson = new Person();
280 newPerson->setFormattedName(contactmethod->bestName());
281
282 Person::ContactMethods numbers;
283 numbers << contactmethod;
284 newPerson->setContactMethods(numbers);
285 contactmethod->setPerson(newPerson);
286
287 auto personCollections = PersonModel::instance().collections();
288 CollectionInterface *peerProfileCollection = nil;
289 foreach(auto collection, personCollections) {
290 if(dynamic_cast<PeerProfileCollection*>(collection))
291 peerProfileCollection = collection;
292 }
293 if(peerProfileCollection) {
294 PersonModel::instance().addNewPerson(newPerson, peerProfileCollection);
Alexandre Lision2db8f472015-07-22 15:05:46 -0400295 }
296}
297
Kateryna Kostiuk9dd759c2017-07-11 12:06:55 -0400298- (void) addContactForRow:(id) sender
299{
300 NSInteger row = [historyView rowForItem:[sender representedObject]];
301 if(row < 0) {
302 return;
303 }
304 [historyView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
305 [self addToContact];
306}
307
Alexandre Lision2db8f472015-07-22 15:05:46 -0400308#pragma mark - NSPopOverDelegate
309
310- (void)popoverDidClose:(NSNotification *)notification
311{
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400312 if (addToContactPopover != nullptr) {
313 [addToContactPopover performClose:self];
314 addToContactPopover = NULL;
Alexandre Lision2db8f472015-07-22 15:05:46 -0400315 }
316}
317
318#pragma mark - ContactLinkedDelegate
319
320- (void)contactLinked
321{
Alexandre Lision4dfcafc2015-08-20 12:43:23 -0400322 if (addToContactPopover != nullptr) {
323 [addToContactPopover performClose:self];
324 addToContactPopover = NULL;
Alexandre Lision2db8f472015-07-22 15:05:46 -0400325 }
326}
327
328#pragma mark - KeyboardShortcutDelegate
329
330- (void) onAddShortcut
331{
332 if([[treeController selectedNodes] count] > 0) {
333 QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
334 const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
335 if (qIdx.parent().isValid() && var.isValid()) {
336 if (auto call = var.value<Call *>()) {
337 auto contactmethod = call->peerContactMethod();
338 if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
339 [self addToContact];
340 }
341 }
342 }
343 }
344}
345
Alexandre Lision5855b6a2015-02-03 11:31:05 -0500346@end