Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2015 Savoir-faire Linux Inc. |
| 3 | * 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 Lision | d14bda3 | 2015-10-13 11:34:29 -0400 | [diff] [blame] | 32 | #import <itemdataroles.h> |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 33 | #import <person.h> |
| 34 | #import <contactmethod.h> |
| 35 | #import <globalinstances.h> |
| 36 | |
| 37 | #import "QNSTreeController.h" |
| 38 | #import "delegates/ImageManipulationDelegate.h" |
Alexandre Lision | 4e280d6 | 2015-09-09 15:56:30 -0400 | [diff] [blame] | 39 | #import "views/HoverTableRowView.h" |
| 40 | #import "views/ContextualTableCellView.h" |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 41 | |
| 42 | @interface SmartViewVC () <NSOutlineViewDelegate> { |
| 43 | BOOL isShowingContacts; |
| 44 | QNSTreeController *treeController; |
| 45 | |
| 46 | //UI elements |
| 47 | __unsafe_unretained IBOutlet NSOutlineView *smartView; |
| 48 | __unsafe_unretained IBOutlet NSSearchField *searchField; |
| 49 | __unsafe_unretained IBOutlet NSButton *showContactsButton; |
| 50 | __unsafe_unretained IBOutlet NSButton *showHistoryButton; |
| 51 | __unsafe_unretained IBOutlet NSTabView *tabbar; |
| 52 | } |
| 53 | |
| 54 | @end |
| 55 | |
| 56 | @implementation SmartViewVC |
| 57 | |
| 58 | // Tags for views |
| 59 | NSInteger const IMAGE_TAG = 100; |
| 60 | NSInteger const DISPLAYNAME_TAG = 200; |
| 61 | NSInteger const DETAILS_TAG = 300; |
| 62 | NSInteger const CALL_BUTTON_TAG = 400; |
| 63 | NSInteger const TXT_BUTTON_TAG = 500; |
| 64 | |
| 65 | - (void)awakeFromNib |
| 66 | { |
| 67 | NSLog(@"INIT SmartView VC"); |
| 68 | |
| 69 | isShowingContacts = false; |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 70 | treeController = [[QNSTreeController alloc] initWithQModel:RecentModel::instance().peopleProxy()]; |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 71 | |
| 72 | [treeController setAvoidsEmptySelection:NO]; |
| 73 | [treeController setChildrenKeyPath:@"children"]; |
| 74 | |
| 75 | [smartView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; |
| 76 | [smartView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil]; |
| 77 | [smartView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; |
| 78 | [smartView setTarget:self]; |
| 79 | [smartView setDoubleAction:@selector(placeCall:)]; |
| 80 | |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 81 | QObject::connect(RecentModel::instance().peopleProxy(), |
Alexandre Lision | ee09846 | 2015-10-22 17:22:50 -0400 | [diff] [blame] | 82 | &QAbstractItemModel::dataChanged, |
| 83 | [self](const QModelIndex &topLeft, const QModelIndex &bottomRight) { |
| 84 | for(int row = topLeft.row() ; row <= bottomRight.row() ; ++row) |
| 85 | { |
| 86 | [smartView reloadDataForRowIndexes:[NSIndexSet indexSetWithIndex:row] |
| 87 | columnIndexes:[NSIndexSet indexSetWithIndex:0]]; |
| 88 | } |
| 89 | }); |
| 90 | |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 91 | [self.view setWantsLayer:YES]; |
| 92 | [self.view setLayer:[CALayer layer]]; |
| 93 | [self.view.layer setBackgroundColor:[NSColor whiteColor].CGColor]; |
| 94 | |
| 95 | [searchField setWantsLayer:YES]; |
| 96 | [searchField setLayer:[CALayer layer]]; |
| 97 | [searchField.layer setBackgroundColor:[NSColor colorWithCalibratedRed:0.949 green:0.949 blue:0.949 alpha:0.9].CGColor]; |
| 98 | } |
| 99 | |
| 100 | - (void)placeCall:(id)sender |
| 101 | { |
| 102 | QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]]; |
| 103 | ContactMethod* m = nil; |
| 104 | |
| 105 | // Double click on an ongoing call |
| 106 | if (qIdx.parent().isValid()) { |
| 107 | return; |
| 108 | } |
| 109 | |
| 110 | if([[treeController selectedNodes] count] > 0) { |
| 111 | QVariant var = qIdx.data((int)Call::Role::ContactMethod); |
| 112 | m = qvariant_cast<ContactMethod*>(var); |
| 113 | if (!m) { |
| 114 | // test if it is a person |
| 115 | QVariant var = qIdx.data((int)Person::Role::Object); |
| 116 | if (var.isValid()) { |
| 117 | Person *c = var.value<Person*>(); |
| 118 | if (c->phoneNumbers().size() > 0) { |
| 119 | m = c->phoneNumbers().first(); |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | // Before calling check if we properly extracted a contact method and that |
| 126 | // there is NOT already an ongoing call for this index (e.g: no children for this node) |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 127 | if(m && !RecentModel::instance().peopleProxy()->index(0, 0, qIdx).isValid()){ |
| 128 | Call* c = CallModel::instance().dialingCall(); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 129 | c->setPeerContactMethod(m); |
| 130 | c << Call::Action::ACCEPT; |
| 131 | |
Alexandre Lision | 21666f3 | 2015-09-22 17:04:36 -0400 | [diff] [blame] | 132 | [smartView deselectAll:nil]; |
| 133 | [smartView selectRowIndexes:[[NSIndexSet alloc] initWithIndex:0] byExtendingSelection:NO]; |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 134 | } |
| 135 | } |
| 136 | |
| 137 | - (IBAction)showHistory:(NSButton*)sender { |
| 138 | if (isShowingContacts) { |
| 139 | [showContactsButton setState:NO]; |
| 140 | isShowingContacts = NO; |
| 141 | [tabbar selectTabViewItemAtIndex:1]; |
| 142 | } else if ([sender state] == NSOffState) { |
| 143 | [tabbar selectTabViewItemAtIndex:0]; |
| 144 | } else { |
| 145 | [tabbar selectTabViewItemAtIndex:1]; |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | - (IBAction)showContacts:(NSButton*)sender { |
| 150 | if (isShowingContacts) { |
| 151 | [showContactsButton setState:NO]; |
| 152 | [tabbar selectTabViewItemAtIndex:0]; |
| 153 | } else { |
| 154 | [showHistoryButton setState:![sender state]]; |
| 155 | [tabbar selectTabViewItemAtIndex:2]; |
| 156 | } |
| 157 | |
| 158 | isShowingContacts = [sender state]; |
| 159 | } |
| 160 | |
| 161 | #pragma mark - NSOutlineViewDelegate methods |
| 162 | |
| 163 | // ------------------------------------------------------------------------------- |
| 164 | // shouldSelectItem:item |
| 165 | // ------------------------------------------------------------------------------- |
| 166 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; |
| 167 | { |
| 168 | return YES; |
| 169 | } |
| 170 | |
| 171 | // ------------------------------------------------------------------------------- |
| 172 | // shouldEditTableColumn:tableColumn:item |
| 173 | // |
| 174 | // Decide to allow the edit of the given outline view "item". |
| 175 | // ------------------------------------------------------------------------------- |
| 176 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item |
| 177 | { |
| 178 | return NO; |
| 179 | } |
| 180 | |
| 181 | |
| 182 | // ------------------------------------------------------------------------------- |
| 183 | // outlineViewSelectionDidChange:notification |
| 184 | // ------------------------------------------------------------------------------- |
| 185 | - (void)outlineViewSelectionDidChange:(NSNotification *)notification |
| 186 | { |
| 187 | if ([treeController selectedNodes].count <= 0) { |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 188 | CallModel::instance().selectionModel()->clearCurrentIndex(); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 189 | return; |
| 190 | } |
| 191 | |
| 192 | QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]]; |
| 193 | |
| 194 | // ask the tree controller for the current selection |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 195 | if (auto selected = RecentModel::instance().getActiveCall(RecentModel::instance().peopleProxy()->mapToSource(qIdx))) { |
| 196 | CallModel::instance().selectCall(selected); |
| 197 | } else if (auto selected = RecentModel::instance().getActiveCall(RecentModel::instance().peopleProxy()->mapToSource(qIdx.parent()))){ |
| 198 | CallModel::instance().selectCall(selected); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 199 | } else { |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 200 | CallModel::instance().selectionModel()->clearCurrentIndex(); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 201 | } |
| 202 | } |
| 203 | |
| 204 | /* View Based OutlineView: See the delegate method -tableView:viewForTableColumn:row: in NSTableView. |
| 205 | */ |
| 206 | - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item |
| 207 | { |
| 208 | QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; |
| 209 | NSTableCellView *result; |
| 210 | if (!qIdx.parent().isValid()) { |
| 211 | result = [outlineView makeViewWithIdentifier:@"MainCell" owner:outlineView]; |
| 212 | NSTextField* details = [result viewWithTag:DETAILS_TAG]; |
| 213 | |
Alexandre Lision | 4e280d6 | 2015-09-09 15:56:30 -0400 | [diff] [blame] | 214 | [((ContextualTableCellView*) result) setContextualsControls:[NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]]]; |
| 215 | |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 216 | if (auto call = RecentModel::instance().getActiveCall(RecentModel::instance().peopleProxy()->mapToSource(qIdx))) { |
Alexandre Lision | d14bda3 | 2015-10-13 11:34:29 -0400 | [diff] [blame] | 217 | [details setStringValue:call->roleData((int)Ring::Role::FormattedState).toString().toNSString()]; |
Alexandre Lision | 21666f3 | 2015-09-22 17:04:36 -0400 | [diff] [blame] | 218 | [((ContextualTableCellView*) result) setActiveState:YES]; |
| 219 | } else { |
Alexandre Lision | d14bda3 | 2015-10-13 11:34:29 -0400 | [diff] [blame] | 220 | [details setStringValue:qIdx.data((int)Ring::Role::FormattedLastUsed).toString().toNSString()]; |
Alexandre Lision | 21666f3 | 2015-09-22 17:04:36 -0400 | [diff] [blame] | 221 | [((ContextualTableCellView*) result) setActiveState:NO]; |
| 222 | } |
| 223 | |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 224 | } else { |
| 225 | result = [outlineView makeViewWithIdentifier:@"CallCell" owner:outlineView]; |
| 226 | NSTextField* details = [result viewWithTag:DETAILS_TAG]; |
| 227 | |
| 228 | [details setStringValue:qIdx.data((int)Call::Role::HumanStateName).toString().toNSString()]; |
| 229 | } |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 230 | |
| 231 | NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG]; |
| 232 | [displayName setStringValue:qIdx.data(Qt::DisplayRole).toString().toNSString()]; |
| 233 | NSImageView* photoView = [result viewWithTag:IMAGE_TAG]; |
| 234 | Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object)); |
Alexandre Lision | de0314b | 2015-09-02 15:45:21 -0400 | [diff] [blame] | 235 | QVariant photo = GlobalInstances::pixmapManipulator().contactPhoto(p, QSize(40,40)); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 236 | [photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))]; |
| 237 | return result; |
| 238 | } |
| 239 | |
| 240 | - (IBAction)callClickedAtRow:(id)sender { |
| 241 | NSInteger row = [smartView rowForView:sender]; |
| 242 | [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; |
| 243 | [self placeCall:nil]; |
| 244 | } |
| 245 | |
| 246 | - (IBAction)hangUpClickedAtRow:(id)sender { |
| 247 | NSInteger row = [smartView rowForView:sender]; |
| 248 | [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 249 | CallModel::instance().getCall(CallModel::instance().selectionModel()->currentIndex()) << Call::Action::REFUSE; |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 250 | } |
| 251 | |
| 252 | /* View Based OutlineView: See the delegate method -tableView:rowViewForRow: in NSTableView. |
Alexandre Lision | 4e280d6 | 2015-09-09 15:56:30 -0400 | [diff] [blame] | 253 | */ |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 254 | - (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item |
| 255 | { |
Alexandre Lision | 4e280d6 | 2015-09-09 15:56:30 -0400 | [diff] [blame] | 256 | return [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil]; |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 257 | } |
Alexandre Lision | 4e280d6 | 2015-09-09 15:56:30 -0400 | [diff] [blame] | 258 | |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 259 | |
| 260 | /* 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'. |
| 261 | */ |
| 262 | - (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row |
| 263 | { |
| 264 | |
| 265 | } |
| 266 | |
| 267 | /* 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. |
| 268 | */ |
| 269 | - (void)outlineView:(NSOutlineView *)outlineView didRemoveRowView:(NSTableRowView *)rowView forRow:(NSInteger)row |
| 270 | { |
| 271 | |
| 272 | } |
| 273 | |
| 274 | - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item |
| 275 | { |
| 276 | QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; |
| 277 | return (((NSTreeNode*)item).indexPath.length == 1) ? 60.0 : 45.0; |
| 278 | } |
| 279 | |
| 280 | - (void) placeCallFromSearchField |
| 281 | { |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 282 | Call* c = CallModel::instance().dialingCall(); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 283 | // check for a valid ring hash |
| 284 | NSCharacterSet *hexSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789abcdefABCDEF"]; |
| 285 | BOOL valid = [[[searchField stringValue] stringByTrimmingCharactersInSet:hexSet] isEqualToString:@""]; |
| 286 | |
| 287 | if(valid && searchField.stringValue.length == 40) { |
| 288 | c->setDialNumber(QString::fromNSString([NSString stringWithFormat:@"ring:%@",[searchField stringValue]])); |
| 289 | } else { |
| 290 | c->setDialNumber(QString::fromNSString([searchField stringValue])); |
| 291 | } |
| 292 | |
| 293 | c << Call::Action::ACCEPT; |
Alexandre Lision | bf0385e | 2015-10-22 17:36:28 -0400 | [diff] [blame] | 294 | |
| 295 | [searchField setStringValue:@""]; |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 296 | RecentModel::instance().peopleProxy()-> |
Alexandre Lision | bf0385e | 2015-10-22 17:36:28 -0400 | [diff] [blame] | 297 | setFilterRegExp(QRegExp(QString::fromNSString([searchField stringValue]), Qt::CaseInsensitive, QRegExp::FixedString)); |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 298 | } |
| 299 | |
Alexandre Lision | bf0385e | 2015-10-22 17:36:28 -0400 | [diff] [blame] | 300 | #pragma NSTextFieldDelegate |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 301 | |
| 302 | - (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector |
| 303 | { |
| 304 | if (commandSelector == @selector(insertNewline:)) { |
| 305 | if([[searchField stringValue] isNotEqualTo:@""]) { |
| 306 | [self placeCallFromSearchField]; |
| 307 | return YES; |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | return NO; |
| 312 | } |
| 313 | |
Alexandre Lision | bf0385e | 2015-10-22 17:36:28 -0400 | [diff] [blame] | 314 | - (void)controlTextDidChange:(NSNotification *) notification |
| 315 | { |
Alexandre Lision | d3aa3ad | 2015-10-23 14:28:41 -0400 | [diff] [blame] | 316 | RecentModel::instance().peopleProxy()-> |
Alexandre Lision | bf0385e | 2015-10-22 17:36:28 -0400 | [diff] [blame] | 317 | setFilterRegExp(QRegExp(QString::fromNSString([searchField stringValue]), Qt::CaseInsensitive, QRegExp::FixedString)); |
| 318 | } |
| 319 | |
Alexandre Lision | 4dfcafc | 2015-08-20 12:43:23 -0400 | [diff] [blame] | 320 | @end |