Commit d431155b authored by Olivier Soldano's avatar Olivier Soldano Committed by Kateryna Kostiuk

implement new conversation list

This patch is an implementation of the conversation list using the
conversation model recently introduced in LRC.
 - Unused HistoryVC and PersonsVC are removed from the code base as we
   are switching to a one list design.
 - Setting a conversation model on SmartListVC switches the displayed
   list.
 - Actions such as selecting a conversation, double clicking, call
   button and searching are also implemented (from the LRC point of
   view which may not yet be visible for user as of this patch).
 - As the new view is based on NSTableView rather than NSOutlineView,
   a RingTableView class is introduced based on RingOutlineView code.
 - The call button at the right of the search field is removed.

Change-Id: I668f102f435048d3c85efd22d1ab31b395139215
Reviewed-by: Kateryna Kostiuk's avatarKateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com>
parent 49cb2918
......@@ -130,10 +130,6 @@ SET(ringclient_CONTROLLERS
src/VideoPrefsVC.mm
src/VideoPrefsVC.h
src/GeneralPrefsVC.h
src/HistoryVC.mm
src/HistoryVC.h
src/PersonsVC.mm
src/PersonsVC.h
src/ChatVC.mm
src/ChatVC.h
src/PersonLinkerVC.mm
......@@ -190,6 +186,8 @@ SET(ringclient_VIEWS
src/views/ITProgressIndicator.h
src/views/RingOutlineView.mm
src/views/RingOutlineView.h
src/views/RingTableView.mm
src/views/RingTableView.h
src/views/RingIDField.mm
src/views/RingIDField.h
src/views/HoverTableRowView.mm
......@@ -230,7 +228,8 @@ SET(ringclient_OTHERS
src/delegates/ImageManipulationDelegate.mm
src/delegates/ImageManipulationDelegate.h
src/AccountSelectionManager.h
src/AccountSelectionManager.mm)
src/AccountSelectionManager.mm
src/utils.h)
SET(ringclient_XIBS
......
/*
* Copyright (C) 2015-2016 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
*
* 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 "HistoryVC.h"
//Qt
#import <QSortFilterProxyModel>
#import <QtMacExtras/qmacfunctions.h>
#import <QPixmap>
//LRC
#import <categorizedhistorymodel.h>
#import <callmodel.h>
#import <call.h>
#import <person.h>
#import <contactmethod.h>
#import <globalinstances.h>
#import <personmodel.h>
#import <peerprofilecollection.h>
#import "QNSTreeController.h"
#import "PersonLinkerVC.h"
#import "views/HoverTableRowView.h"
#import "delegates/ImageManipulationDelegate.h"
@interface HistoryVC() <NSPopoverDelegate, KeyboardShortcutDelegate, ContactLinkedDelegate> {
QNSTreeController *treeController;
IBOutlet RingOutlineView *historyView;
QSortFilterProxyModel *historyProxyModel;
NSPopover* addToContactPopover;
}
@end
@implementation HistoryVC
// Tags for Views
NSInteger const DIRECTION_TAG = 100;
NSInteger const DISPLAYNAME_TAG = 200;
NSInteger const DETAILS_TAG = 300;
NSInteger const PHOTO_TAG = 400;
- (void)awakeFromNib
{
NSLog(@"INIT HVC");
historyProxyModel = new QSortFilterProxyModel(&CategorizedHistoryModel::instance());
historyProxyModel->setSourceModel(&CategorizedHistoryModel::instance());
historyProxyModel->setSortRole(static_cast<int>(Call::Role::Date));
historyProxyModel->sort(0,Qt::DescendingOrder);
treeController = [[QNSTreeController alloc] initWithQModel:historyProxyModel];
[treeController setAvoidsEmptySelection:NO];
[treeController setChildrenKeyPath:@"children"];
[historyView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[historyView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
[historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[historyView setTarget:self];
[historyView setDoubleAction:@selector(placeHistoryCall:)];
[historyView setContextMenuDelegate:self];
[historyView setShortcutsDelegate:self];
QObject::connect(&CallModel::instance(),
&CategorizedHistoryModel::dataChanged,
[=](const QModelIndex &topLeft, const QModelIndex &bottomRight) {
[historyView reloadDataForRowIndexes:
[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(topLeft.row(), bottomRight.row() + 1)]
columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, historyView.tableColumns.count)]];
});
}
- (void) dealloc
{
delete historyProxyModel;
}
- (void)placeHistoryCall:(id)sender
{
if([[treeController selectedNodes] count] > 0) {
auto item = [treeController selectedNodes][0];
QModelIndex qIdx = [treeController toQIdx:item];
if (!qIdx.parent().isValid()) {
if ([historyView isItemExpanded:item]) {
[[historyView animator] collapseItem:item];
} else
[[historyView animator] expandItem:item];
return;
}
QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod);
ContactMethod* m = qvariant_cast<ContactMethod*>(var);
if(m){
auto c = CallModel::instance().dialingCall();
c->setPeerContactMethod(m);
c << Call::Action::ACCEPT;
CallModel::instance().selectCall(c);
}
}
}
#pragma mark - NSOutlineViewDelegate methods
// -------------------------------------------------------------------------------
// shouldSelectItem:item
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
{
return YES;
}
// -------------------------------------------------------------------------------
// shouldEditTableColumn:tableColumn:item
//
// Decide to allow the edit of the given outline view "item".
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
return NO;
}
- (NSImage*) image:(NSImage*) img withTintedWithColor:(NSColor *)tint
{
if (tint) {
[img lockFocus];
[tint set];
NSRect imageRect = {NSZeroPoint, [img size]};
NSRectFillUsingOperation(imageRect, NSCompositeSourceAtop);
[img unlockFocus];
}
return img;
}
/* View Based OutlineView: See the delegate method -tableView:viewForTableColumn:row: in NSTableView.
*/
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
NSTableCellView* result;
if(!qIdx.parent().isValid()) {
result = [outlineView makeViewWithIdentifier:@"CategoryCell" owner:outlineView];
} else {
result = [outlineView makeViewWithIdentifier:@"HistoryCell" owner:outlineView];
NSImageView* directionView = [result viewWithTag:DIRECTION_TAG];
if (qvariant_cast<Call::Direction>(qIdx.data((int)Call::Role::Direction)) == Call::Direction::INCOMING) {
if (qvariant_cast<Boolean>(qIdx.data((int) Call::Role::Missed))) {
[directionView setImage:[self image:[NSImage imageNamed:@"ic_call_missed"] withTintedWithColor:[NSColor redColor]]];
} else {
[directionView setImage:[self image:[NSImage imageNamed:@"ic_call_received"]
withTintedWithColor:[NSColor colorWithCalibratedRed:116/255.0 green:179/255.0 blue:93/255.0 alpha:1.0]]];
}
} else {
if (qvariant_cast<Boolean>(qIdx.data((int) Call::Role::Missed))) {
[directionView setImage:[self image:[NSImage imageNamed:@"ic_call_missed"] withTintedWithColor:[NSColor redColor]]];
} else {
[directionView setImage:[self image:[NSImage imageNamed:@"ic_call_made"]
withTintedWithColor:[NSColor colorWithCalibratedRed:116/255.0 green:179/255.0 blue:93/255.0 alpha:1.0]]];
}
}
auto call = qvariant_cast<Call*>(qIdx.data((int)Call::Role::Object));
NSImageView* photoView = [result viewWithTag:PHOTO_TAG];
[photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
NSTextField* details = [result viewWithTag:DETAILS_TAG];
[details setStringValue:qIdx.data((int)Call::Role::FormattedDate).toString().toNSString()];
}
NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
[displayName setStringValue:qIdx.data(Qt::DisplayRole).toString().toNSString()];
return result;
}
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
if(!qIdx.parent().isValid()) {
return 35.0;
} else {
return 48.0;
}
}
/* View Based OutlineView: See the delegate method -tableView:rowViewForRow: in NSTableView.
*/
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
HoverTableRowView* result = [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
if(!qIdx.parent().isValid()) {
[result setHighlightable:NO];
} else
[result setHighlightable:YES];
return result;
}
#pragma mark - ContextMenuDelegate
- (NSMenu*) contextualMenuForIndex:(NSTreeNode*) item
{
QModelIndex qIdx = [treeController toQIdx:item];
if (!qIdx.isValid()) {
return nil;
}
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
auto contactmethod = call->peerContactMethod();
if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
NSMenu *theMenu = [[NSMenu alloc]
initWithTitle:@""];
NSMenuItem* addContactItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Add to contacts", @"Contextual menu action")
action:@selector(addContactForRow:)
keyEquivalent:@""];
[addContactItem setRepresentedObject:item];
[theMenu addItem:addContactItem];
return theMenu;
}
}
}
return nil;
}
- (void) addToContact
{
ContactMethod* contactmethod = nullptr;
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
contactmethod = call->peerContactMethod();
}
}
}
// TODO: Uncomment to reuse contact name editing popover
// if (addToContactPopover != nullptr) {
// [addToContactPopover performClose:self];
// addToContactPopover = NULL;
// } else if (contactmethod) {
// auto* editorVC = [[PersonLinkerVC alloc] initWithNibName:@"PersonLinker" bundle:nil];
// [editorVC setMethodToLink:contactmethod];
// [editorVC setContactLinkedDelegate:self];
// addToContactPopover = [[NSPopover alloc] init];
// [addToContactPopover setContentSize:editorVC.view.frame.size];
// [addToContactPopover setContentViewController:editorVC];
// [addToContactPopover setAnimates:YES];
// [addToContactPopover setBehavior:NSPopoverBehaviorTransient];
// [addToContactPopover setDelegate:self];
//
// [addToContactPopover showRelativeToRect:[historyView frameOfOutlineCellAtRow:[historyView selectedRow]] ofView:historyView preferredEdge:NSMaxXEdge];
// }
auto* newPerson = new Person();
newPerson->setFormattedName(contactmethod->bestName());
Person::ContactMethods numbers;
numbers << contactmethod;
newPerson->setContactMethods(numbers);
contactmethod->setPerson(newPerson);
auto personCollections = PersonModel::instance().collections();
CollectionInterface *peerProfileCollection = nil;
foreach(auto collection, personCollections) {
if(dynamic_cast<PeerProfileCollection*>(collection))
peerProfileCollection = collection;
}
if(peerProfileCollection) {
PersonModel::instance().addNewPerson(newPerson, peerProfileCollection);
}
}
- (void) addContactForRow:(id) sender
{
NSInteger row = [historyView rowForItem:[sender representedObject]];
if(row < 0) {
return;
}
[historyView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[self addToContact];
}
#pragma mark - NSPopOverDelegate
- (void)popoverDidClose:(NSNotification *)notification
{
if (addToContactPopover != nullptr) {
[addToContactPopover performClose:self];
addToContactPopover = NULL;
}
}
#pragma mark - ContactLinkedDelegate
- (void)contactLinked
{
if (addToContactPopover != nullptr) {
[addToContactPopover performClose:self];
addToContactPopover = NULL;
}
}
#pragma mark - KeyboardShortcutDelegate
- (void) onAddShortcut
{
if([[treeController selectedNodes] count] > 0) {
QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]];
const auto& var = qIdx.data(static_cast<int>(Call::Role::Object));
if (qIdx.parent().isValid() && var.isValid()) {
if (auto call = var.value<Call *>()) {
auto contactmethod = call->peerContactMethod();
if (!contactmethod->contact() || contactmethod->contact()->isPlaceHolder()) {
[self addToContact];
}
}
}
}
}
@end
/*
* Copyright (C) 2015-2016 Savoir-faire Linux Inc.
* Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
*
* 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 "PersonsVC.h"
//Qt
#import <QSortFilterProxyModel>
#import <QtMacExtras/qmacfunctions.h>
#import <QPixmap>
//LRC
#import <person.h>
#import <personmodel.h>
#import <callmodel.h>
#import <contactmethod.h>
#import <categorizedcontactmodel.h>
#import <globalinstances.h>
#import "backends/AddressBookBackend.h"
#import "QNSTreeController.h"
#import "delegates/ImageManipulationDelegate.h"
#import "views/HoverTableRowView.h"
#import "views/ContextualTableCellView.h"
#import <AddressBook/AddressBook.h>
class ReachablePersonModel : public QSortFilterProxyModel
{
public:
ReachablePersonModel(QAbstractItemModel* parent) : QSortFilterProxyModel(parent)
{
setSourceModel(parent);
}
virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
{
return sourceModel()->index(source_row,0,source_parent).flags() & Qt::ItemIsEnabled;
}
};
@interface PersonsVC () {
QNSTreeController *treeController;
__unsafe_unretained IBOutlet RingOutlineView *personsView;
QSortFilterProxyModel *contactProxyModel;
}
@end
@implementation PersonsVC
// Tags for views
NSInteger const IMAGE_TAG = 100;
NSInteger const DISPLAYNAME_TAG = 200;
NSInteger const DETAILS_TAG = 300;
NSInteger const CALL_BUTTON_TAG = 400;
-(void) awakeFromNib
{
NSLog(@"INIT PersonsVC");
contactProxyModel = new ReachablePersonModel(&CategorizedContactModel::instance());
contactProxyModel->setSortRole(static_cast<int>(Qt::DisplayRole));
contactProxyModel->sort(0,Qt::AscendingOrder);
treeController = [[QNSTreeController alloc] initWithQModel:contactProxyModel];
[treeController setAvoidsEmptySelection:NO];
[treeController setChildrenKeyPath:@"children"];
[personsView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
[personsView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
[personsView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
[personsView setTarget:self];
[personsView setDoubleAction:@selector(callContact:)];
[personsView setContextMenuDelegate:self];
CategorizedContactModel::instance().setUnreachableHidden(YES);
}
- (void) dealloc
{
delete contactProxyModel;
}
- (IBAction)callContact:(id)sender
{
if([[treeController selectedNodes] count] > 0) {
auto item = [treeController selectedNodes][0];
QModelIndex qIdx = [treeController toQIdx:item];
ContactMethod* m = nil;
if (!qIdx.parent().isValid()) {
if ([personsView isItemExpanded:item]) {
[[personsView animator] collapseItem:item];
} else
[[personsView animator] expandItem:item];
return;
}
if(((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 2) {
// Person
QVariant var = qIdx.data((int)Person::Role::Object);
if (var.isValid()) {
Person *c = var.value<Person*>();
if (c->phoneNumbers().size() > 0) {
m = c->phoneNumbers().first();
}
}
} else if (((NSTreeNode*)[treeController selectedNodes][0]).indexPath.length == 3) {
//ContactMethod
QVariant var = qIdx.data(static_cast<int>(ContactMethod::Role::Object));
if (var.isValid()) {
m = var.value<ContactMethod *>();
}
}
if(m){
Call* c = CallModel::instance().dialingCall();
c->setPeerContactMethod(m);
c << Call::Action::ACCEPT;
CallModel::instance().selectCall(c);
}
}
}
#pragma mark - NSOutlineViewDelegate methods
// -------------------------------------------------------------------------------
// shouldSelectItem:item
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
if(!qIdx.isValid())
return NO;
return YES;
}
// -------------------------------------------------------------------------------
// shouldEditTableColumn:tableColumn:item
//
// Decide to allow the edit of the given outline view "item".
// -------------------------------------------------------------------------------
- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
return NO;
}
/* View Based OutlineView: See the delegate method -tableView:viewForTableColumn:row: in NSTableView.
*/
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
NSTableCellView *result;
NSString* displayNameString = qIdx.data(Qt::DisplayRole).toString().toNSString();
if(!qIdx.parent().isValid()) {
result = [outlineView makeViewWithIdentifier:@"LetterCell" owner:outlineView];
[result setWantsLayer:YES];
[result setLayer:[CALayer layer]];
} else if(((NSTreeNode*)item).indexPath.length == 2) {
result = [outlineView makeViewWithIdentifier:@"PersonCell" owner:outlineView];
NSImageView* photoView = [result viewWithTag:IMAGE_TAG];
Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
if(displayNameString.length == 0 && p) {
displayNameString = qIdx.data((int)Person::Role::IdOfLastCMUsed).toString().toNSString();
}
[photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
[((ContextualTableCellView*) result) setContextualsControls:[NSMutableArray arrayWithObject:[result viewWithTag:CALL_BUTTON_TAG]]];
[((ContextualTableCellView*) result) setShouldBlurParentView:NO];
NSTextField* details = [result viewWithTag:DETAILS_TAG];
if (p && p->phoneNumbers().size() > 0)
[details setStringValue:p->phoneNumbers().first()->uri().toNSString()];
} else {
result = [outlineView makeViewWithIdentifier:@"ContactMethodCell" owner:outlineView];
}
NSTextField* displayName = [result viewWithTag:DISPLAYNAME_TAG];
[displayName setStringValue:displayNameString];
return result;
}
/* View Based OutlineView: See the delegate method -tableView:rowViewForRow: in NSTableView.
*/
- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
HoverTableRowView* result = [outlineView makeViewWithIdentifier:@"HoverRowView" owner:nil];
if(!qIdx.parent().isValid()) {
[result setHighlightable:NO];
} else
[result setHighlightable:YES];
return result;
}
- (void)startCallForRow:(id)sender {
NSInteger row = [personsView rowForView:sender];
[personsView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
[self callContact:nil];
}
// -------------------------------------------------------------------------------
// outlineViewSelectionDidChange:notification
// -------------------------------------------------------------------------------
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{
}
- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
{
QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
return (((NSTreeNode*)item).indexPath.length == 2) ? 60.0 : 20.0;
}