SmartViewVC.mm 16.6 KB
Newer Older
1
/*
Alexandre Lision's avatar
Alexandre Lision committed
2
 *  Copyright (C) 2015-2016 Savoir-faire Linux Inc.
3
 *  Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
4 5
 *  Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
 *  Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
 *
 *  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.
 */

#import "SmartViewVC.h"

//Qt
#import <QtMacExtras/qmacfunctions.h>
#import <QPixmap>
#import <QIdentityProxyModel>
#import <QItemSelectionModel>

//LRC
#import <globalinstances.h>
32 33 34 35 36 37
#import <api/newaccountmodel.h>
#import <api/conversationmodel.h>
#import <api/account.h>
#import <api/contact.h>
#import <api/contactmodel.h>
#import <api/newcallmodel.h>
38 39 40

#import "QNSTreeController.h"
#import "delegates/ImageManipulationDelegate.h"
Alexandre Lision's avatar
Alexandre Lision committed
41
#import "views/HoverTableRowView.h"
42
#import "PersonLinkerVC.h"
43
#import "views/IconButton.h"
44
#import "views/RingTableView.h"
Alexandre Lision's avatar
Alexandre Lision committed
45
#import "views/ContextualTableCellView.h"
46
#import "utils.h"
47

48
@interface SmartViewVC () <NSTableViewDelegate, NSTableViewDataSource, NSPopoverDelegate, ContextMenuDelegate, ContactLinkedDelegate, KeyboardShortcutDelegate> {
49

50
    NSPopover* addToContactPopover;
51 52

    //UI elements
53
    __unsafe_unretained IBOutlet RingTableView* smartView;
54
    __unsafe_unretained IBOutlet NSSearchField* searchField;
55

56
    QMetaObject::Connection modelSortedConnection_, filterChangedConnection_, newConversationConnection_, conversationRemovedConnection_;
57 58 59

    lrc::api::ConversationModel* model_;
    std::string selectedUid_;
60 61 62 63 64 65
}

@end

@implementation SmartViewVC

66 67
@synthesize tabbar;

68
// Tags for views
69 70 71 72 73 74
NSInteger const IMAGE_TAG           = 100;
NSInteger const DISPLAYNAME_TAG     = 200;
NSInteger const DETAILS_TAG         = 300;
NSInteger const CALL_BUTTON_TAG     = 400;
NSInteger const TXT_BUTTON_TAG      = 500;
NSInteger const CANCEL_BUTTON_TAG   = 600;
75
NSInteger const RING_ID_LABEL       = 700;
76
NSInteger const PRESENCE_TAG        = 800;
77 78 79 80

- (void)awakeFromNib
{
    NSLog(@"INIT SmartView VC");
81 82
    //get selected account
    //encapsulate conversationmodel in local version
83 84 85 86

    [smartView setTarget:self];
    [smartView setDoubleAction:@selector(placeCall:)];

87 88 89
    [smartView setContextMenuDelegate:self];
    [smartView setShortcutsDelegate:self];

90
    [smartView setDataSource: self];
91

92 93 94 95 96 97 98 99 100
    [self.view setWantsLayer:YES];
    [self.view setLayer:[CALayer layer]];
    [self.view.layer setBackgroundColor:[NSColor whiteColor].CGColor];

    [searchField setWantsLayer:YES];
    [searchField setLayer:[CALayer layer]];
    [searchField.layer setBackgroundColor:[NSColor colorWithCalibratedRed:0.949 green:0.949 blue:0.949 alpha:0.9].CGColor];
}

101
- (void)placeCall:(id)sender
102
{
103 104 105 106 107 108
    NSInteger row;
    if (sender != nil && [sender clickedRow] != -1)
        row = [sender clickedRow];
    else if ([smartView selectedRow] != -1)
        row = [smartView selectedRow];
    else
109
        return;
110 111 112

    auto conv = model_->filteredConversation(row);
    model_->placeCall(conv.uid);
113 114
}

115
-(void) reloadData
116
{
117 118 119 120 121 122 123 124 125 126 127 128 129
    [smartView deselectAll:nil];
    [smartView reloadData];

    if (!selectedUid_.empty() && model_ != nil) {
        auto it = std::find_if(model_->allFilteredConversations().begin(), model_->allFilteredConversations().end(),
                          [self] (const lrc::api::conversation::Info& conv) {
                              return selectedUid_ == conv.uid;
                          });
        if (it != model_->allFilteredConversations().end()) {
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - model_->allFilteredConversations().begin())];
            [smartView selectRowIndexes:indexSet byExtendingSelection:NO];
        } else {
            selectedUid_.clear();
130 131 132
        }
    }

133 134 135
    [smartView scrollToBeginningOfDocument:nil];
}

136
- (BOOL)setConversationModel:(lrc::api::ConversationModel *)conversationModel
137 138 139
{
    if (model_ != conversationModel) {
        model_ = conversationModel;
140
        selectedUid_.clear(); // Clear selected conversation as the selected account is being changed
141 142
        [self reloadData];
        QObject::disconnect(modelSortedConnection_);
143
        QObject::disconnect(filterChangedConnection_);
144 145
        QObject::disconnect(newConversationConnection_);
        QObject::disconnect(conversationRemovedConnection_);
146 147 148 149 150 151 152 153 154
        if (model_ != nil) {
            modelSortedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::modelSorted,
                                                      [self] (){
                                                          [self reloadData];
                                                      });
            filterChangedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::filterChanged,
                                                        [self] (){
                                                            [self reloadData];
                                                        });
155 156 157 158 159 160 161 162
            newConversationConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::newConversation,
                                                          [self] (){
                                                              [self reloadData];
                                                          });
            conversationRemovedConnection_ = QObject::connect(model_, &lrc::api::ConversationModel::conversationRemoved,
                                                              [self] (){
                                                                  [self reloadData];
                                                              });
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
            model_->setFilter(""); // Reset the filter
        }
        [searchField setStringValue:@""];
        return YES;
    }
    return NO;
}

-(void)selectConversation:(const lrc::api::conversation::Info&)conv model:(lrc::api::ConversationModel*)model;
{
    auto& uid = conv.uid;
    if (selectedUid_ == uid)
        return;

    [self setConversationModel:model];

    if (model_ != nil) {
        auto it = std::find_if(model_->allFilteredConversations().begin(), model_->allFilteredConversations().end(),
                               [self] (const lrc::api::conversation::Info& conv) {
                                   return selectedUid_ == conv.uid;
                               });
        if (it != model_->allFilteredConversations().end()) {
            NSIndexSet* indexSet = [NSIndexSet indexSetWithIndex:(it - model_->allFilteredConversations().begin())];
            [smartView selectRowIndexes:indexSet byExtendingSelection:NO];
            selectedUid_ = uid;
188
        }
189 190 191
    }
}

192 193 194 195 196 197
-(void)deselect
{
    selectedUid_.clear();
    [smartView deselectAll:nil];
}

198
#pragma mark - NSTableViewDelegate methods
199

200
- (BOOL)tableView:(NSTableView *)tableView shouldSelectRow:(NSInteger)row
201 202 203 204
{
    return YES;
}

205
- (BOOL)tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
206 207 208 209
{
    return NO;
}

210
- (void)tableViewSelectionDidChange:(NSNotification *)notification
211
{
212 213 214
    NSInteger row = [notification.object selectedRow];

    if (row == -1)
215
        return;
216 217 218 219 220

    auto uid = model_->filteredConversation(row).uid;
    if (selectedUid_ != uid) {
        selectedUid_ = uid;
        model_->selectConversation(uid);
221 222 223
    }
}

224 225 226 227 228 229
- (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row
{
    return [tableView makeViewWithIdentifier:@"HoverRowView" owner:nil];
}

- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
230
{
231 232
    if (model_ == nil)
        return nil;
Alexandre Lision's avatar
Alexandre Lision committed
233

234
    auto conversation = model_->filteredConversation(row);
Alexandre Lision's avatar
Alexandre Lision committed
235
    NSTableCellView* result;
236

237 238
    result = [tableView makeViewWithIdentifier:@"MainCell" owner:tableView];
//    NSTextField* details = [result viewWithTag:DETAILS_TAG];
Alexandre Lision's avatar
Alexandre Lision committed
239

240 241 242
    NSMutableArray* controls = [NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]];
    [((ContextualTableCellView*) result) setContextualsControls:controls];
    [((ContextualTableCellView*) result) setShouldBlurParentView:YES];
243

244 245 246 247 248 249 250
//    if (auto call = RecentModel::instance().getActiveCall(qIdx)) {
//        [details setStringValue:call->roleData((int)Ring::Role::FormattedState).toString().toNSString()];
//        [((ContextualTableCellView*) result) setActiveState:YES];
//    } else {
//        [details setStringValue:qIdx.data((int)Ring::Role::FormattedLastUsed).toString().toNSString()];
//        [((ContextualTableCellView*) result) setActiveState:NO];
//    }
251

252 253 254
    NSTextField* unreadCount = [result viewWithTag:TXT_BUTTON_TAG];
    [unreadCount setHidden:(conversation.unreadMessages == 0)];
    [unreadCount setIntValue:conversation.unreadMessages];
255 256

    NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
257 258
    NSString* displayNameString = bestNameForConversation(conversation, *model_);
    NSString* displayIDString = bestIDForConversation(conversation, *model_);
259
    if(displayNameString.length == 0 || [displayNameString isEqualToString:displayIDString]) {
260
        NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL];
261
        [displayName setStringValue:displayIDString];
262
        [displayRingID setHidden:YES];
263 264 265 266 267
    }
    else {
        NSTextField* displayRingID = [result viewWithTag:RING_ID_LABEL];
        [displayName setStringValue:displayNameString];
        [displayRingID setStringValue:displayIDString];
268
        [displayRingID setHidden:NO];
269
    }
270
    NSImageView* photoView = [result viewWithTag:IMAGE_TAG];
271

272 273
    auto& imageManip = reinterpret_cast<Interfaces::ImageManipulationDelegate&>(GlobalInstances::pixmapManipulator());
    [photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(imageManip.conversationPhoto(conversation, model_->owner)))];
274 275

    NSView* presenceView = [result viewWithTag:PRESENCE_TAG];
276
    if (model_->owner.contactModel->getContact(conversation.participants[0]).isPresent) {
277 278 279 280
        [presenceView setHidden:NO];
    } else {
        [presenceView setHidden:YES];
    }
281 282 283
    return result;
}

284
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
285
{
286 287 288 289 290 291 292 293 294 295 296 297 298
    return 60.0;
}

#pragma mark - NSTableDataSource methods

- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
    if (tableView == smartView) {
        if (model_ != nullptr)
            return model_->allFilteredConversations().size();
    }

    return 0;
299 300
}

301
- (void)startCallForRow:(id)sender {
302 303 304 305 306 307 308
    NSInteger row = [smartView rowForView:sender];
    [smartView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
    [self placeCall:nil];
}

- (IBAction)hangUpClickedAtRow:(id)sender {
    NSInteger row = [smartView rowForView:sender];
309

310 311
    if (row == -1)
        return;
312

313 314
    auto conv = model_->filteredConversation(row);
    auto& callId = conv.callId;
315

316
    if (callId.empty())
317 318
        return;

319 320
    auto* callModel = model_->owner.callModel.get();
    callModel->hangUp(callId);
Alexandre Lision's avatar
Alexandre Lision committed
321
}
322

323 324 325 326 327 328 329 330 331 332 333
- (void) displayErrorModalWithTitle:(NSString*) title WithMessage:(NSString*) message
{
    NSAlert* alert = [NSAlert alertWithMessageText:title
                                     defaultButton:@"Ok"
                                   alternateButton:nil
                                       otherButton:nil
                         informativeTextWithFormat:message];

    [alert beginSheetModalForWindow:self.view.window modalDelegate:nil didEndSelector:NULL contextInfo:NULL];
}

334
- (void) processSearchFieldInput
335
{
336
    model_->setFilter(std::string([[searchField stringValue] UTF8String]));
337 338
}

339
-(const lrc::api::account::Info&) chosenAccount
340
{
341
    return model_->owner;
342 343
}

344 345 346
- (void) clearSearchField
{
    [searchField setStringValue:@""];
347
    [self processSearchFieldInput];
348 349
}

350 351 352 353 354 355
/**
 Copy a NSString in the general Pasteboard

 @param sender the NSObject containing the represented object to copy
 */
- (void) copyStringToPasteboard:(id) sender
356 357 358 359 360 361
{
    NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
    [pasteBoard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
    [pasteBoard setString:[sender representedObject] forType:NSStringPboardType];
}

Alexandre Lision's avatar
Alexandre Lision committed
362
#pragma NSTextFieldDelegate
363 364 365 366 367

- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
    if (commandSelector == @selector(insertNewline:)) {
        if([[searchField stringValue] isNotEqualTo:@""]) {
368
            [self processSearchFieldInput];
369 370 371 372 373 374
            return YES;
        }
    }
    return NO;
}

375 376
- (void)controlTextDidChange:(NSNotification *) notification
{
377
    [self processSearchFieldInput];
378 379
}

380 381 382 383 384 385 386 387 388 389
#pragma mark - NSPopOverDelegate

- (void)popoverDidClose:(NSNotification *)notification
{
    if (addToContactPopover != nullptr) {
        [addToContactPopover performClose:self];
        addToContactPopover = NULL;
    }
}

Alexandre Lision's avatar
Alexandre Lision committed
390

391 392 393 394 395 396 397 398 399 400 401 402 403 404
#pragma mark - ContactLinkedDelegate

- (void)contactLinked
{
    if (addToContactPopover != nullptr) {
        [addToContactPopover performClose:self];
        addToContactPopover = NULL;
    }
}

#pragma mark - KeyboardShortcutDelegate

- (void) onAddShortcut
{
405
    if ([smartView selectedRow] == -1)
406 407
        return;

408 409
    auto uid = model_->filteredConversation([smartView selectedRow]).uid;
    model_->makePermanent(uid);
410 411 412 413
}

#pragma mark - ContextMenuDelegate

414 415
#if 0
// TODO: Reimplement contextual menu with new models and behaviors
416
- (NSMenu*) contextualMenuForIndex:(NSTreeNode*) item
417
{
418 419 420 421 422 423
    auto qIdx = [treeController toQIdx:item];

    if (!qIdx.isValid()) {
        return nil;
    }

424 425 426 427 428
    auto originIdx = RecentModel::instance().peopleProxy()->mapToSource(qIdx);
    auto contactmethods = RecentModel::instance().getContactMethods(originIdx);
    if (contactmethods.isEmpty())
        return nil;

429 430 431 432 433 434
    NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@""];

    if (contactmethods.size() == 1
        && !contactmethods.first()->contact()
        || contactmethods.first()->contact()->isPlaceHolder()) {

435 436 437 438 439
        NSMenuItem* addContactItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
                                                                action:@selector(addContactForRow:)
                                                         keyEquivalent:@""];
        [addContactItem setRepresentedObject:item];
        [theMenu addItem:addContactItem];
440 441 442 443 444 445 446
    } else if (auto person = contactmethods.first()->contact()) {
        NSMenuItem* copyNameItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy name", @"Contextual menu action")
                                                             action:@selector(copyStringToPasteboard:)
                                                      keyEquivalent:@""];

        [copyNameItem setRepresentedObject:person->formattedName().toNSString()];
        [theMenu addItem:copyNameItem];
447
    }
448 449 450 451

    NSMenu* copySubmenu = [[NSMenu alloc] init];
    NSMenu* callSubmenu = [[NSMenu alloc] init];

452
    for (auto cm : contactmethods) {
453
        NSMenuItem* tmpCopyItem = [[NSMenuItem alloc] initWithTitle:cm->uri().toNSString()
454
                                                             action:@selector(copyStringToPasteboard:)
455 456 457 458 459 460 461 462 463 464 465 466
                                                      keyEquivalent:@""];

        [tmpCopyItem setRepresentedObject:cm->uri().toNSString()];
        [copySubmenu addItem:tmpCopyItem];

        NSMenuItem* tmpCallItem = [[NSMenuItem alloc] initWithTitle:cm->uri().toNSString()
                                                             action:@selector(callNumber:)
                                                      keyEquivalent:@""];
        [tmpCallItem setRepresentedObject:cm->uri().toNSString()];
        [callSubmenu addItem:tmpCallItem];
    }

467 468 469
    NSMenuItem* copyNumberItem = [[NSMenuItem alloc] init];
    [copyNumberItem setTitle:NSLocalizedString(@"Copy number", @"Contextual menu action")];
    [copyNumberItem setSubmenu:copySubmenu];
470 471 472 473 474

    NSMenuItem* callItems = [[NSMenuItem alloc] init];
    [callItems setTitle:NSLocalizedString(@"Call number", @"Contextual menu action")];
    [callItems setSubmenu:callSubmenu];

475
    [theMenu insertItem:copyNumberItem atIndex:theMenu.itemArray.count];
476 477 478 479
    [theMenu insertItem:[NSMenuItem separatorItem] atIndex:theMenu.itemArray.count];
    [theMenu insertItem:callItems atIndex:theMenu.itemArray.count];

    return theMenu;
480
}
481
#endif
482

483
@end