diff --git a/CMakeLists.txt b/CMakeLists.txt index 4854681397e7844c397d6d30592f88cf4b22d4b2..b797ec04a1fb2adcc9e93e79251ec38a687d57c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,10 +16,12 @@ ADD_DEFINITIONS("-std=c++11") PROJECT(${PROJ_NAME}) FIND_PACKAGE(Qt5Core REQUIRED) +FIND_PACKAGE(Qt5MacExtras REQUIRED) FIND_PACKAGE(Qt5Widgets REQUIRED) FIND_PACKAGE(LibRingClient REQUIRED) INCLUDE_DIRECTORIES(SYSTEM ${Qt5Core_INCLUDE_DIRS}) +INCLUDE_DIRECTORIES(SYSTEM ${Qt5MacExtras_INCLUDE_DIRS}) INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}) INCLUDE_DIRECTORIES( ${CMAKE_CURRENT_SOURCE_DIR}) INCLUDE_DIRECTORIES(${LIB_RING_CLIENT_INCLUDE_DIR}) @@ -48,9 +50,14 @@ SET(ringclient_SRCS src/GeneralPrefsVC.mm src/RingWizardWC.mm src/HistoryViewController.mm + src/PersonsVC.mm + src/delegates/ImageManipulationDelegate.mm + + src/views/PersonCell.mm src/views/CallView.mm + src/backends/AddressBookBackend.mm src/backends/MinimalHistoryBackend.mm) SET(ringclient_XIBS @@ -89,9 +96,14 @@ SET(ringclient_HDRS src/HistoryViewController.h src/RingWizardWC.h src/QNSTreeController.h + src/PersonsVC.h + + src/delegates/ImageManipulationDelegate.h + src/views/PersonCell.h src/views/CallView.h + src/backends/AddressBookBackend.h src/backends/MinimalHistoryBackend.h) # Icons @@ -134,6 +146,7 @@ ADD_EXECUTABLE(${PROJ_NAME} MACOSX_BUNDLE TARGET_LINK_LIBRARIES( ${PROJ_NAME} ${LIB_RING_CLIENT_LIBRARY} ${Qt5Core_LIBRARIES} + ${Qt5MacExtras_LIBRARIES} ${Qt5Widgets_LIBRARIES} ) diff --git a/src/HistoryViewController.mm b/src/HistoryViewController.mm index 5ca5dada255eab747adb442068d605963435d144..60d05b0e7f05b7fa1351c9a78931c7ac7fbea64d 100644 --- a/src/HistoryViewController.mm +++ b/src/HistoryViewController.mm @@ -78,15 +78,14 @@ [historyView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil]; [historyView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; [historyView setTarget:self]; - [historyView setDoubleAction:@selector(placeCall:)]; + [historyView setDoubleAction:@selector(placeHistoryCall:)]; CategorizedHistoryModel::instance()->addCollection<MinimalHistoryBackend>(LoadOptions::FORCE_ENABLED); } -- (void)placeCall:(id)sender +- (void)placeHistoryCall:(id)sender { if([[treeController selectedNodes] count] > 0) { - Call* c = CallModel::instance()->dialingCall(); QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]]; QVariant var = historyProxyModel->data(qIdx, (int)Call::Role::ContactMethod); ContactMethod* m = qvariant_cast<ContactMethod*>(var); diff --git a/src/PersonsVC.h b/src/PersonsVC.h new file mode 100644 index 0000000000000000000000000000000000000000..9d735717235252b211ab8b260ae4155868b1e85c --- /dev/null +++ b/src/PersonsVC.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#import <Cocoa/Cocoa.h> + +@interface PersonsVC : NSViewController <NSOutlineViewDelegate> + +@end diff --git a/src/PersonsVC.mm b/src/PersonsVC.mm new file mode 100644 index 0000000000000000000000000000000000000000..bf9e6b40eb83edc06d3707840e159449bff67775 --- /dev/null +++ b/src/PersonsVC.mm @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#import "PersonsVC.h" + +#import <personmodel.h> +#import <callmodel.h> +#import <categorizedcontactmodel.h> +#import <QSortFilterProxyModel> +#import <person.h> +#import <contactmethod.h> +#import <QtMacExtras/qmacfunctions.h> +#import <QPixmap> + +#import "backends/AddressBookBackend.h" +#import "QNSTreeController.h" +#import "delegates/ImageManipulationDelegate.h" +#import "views/PersonCell.h" + +#define COLUMNID_NAME @"NameColumn" + +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 () + +@property NSTreeController *treeController; +@property (assign) IBOutlet NSOutlineView *personsView; +@property QSortFilterProxyModel *contactProxyModel; + +@end + +@implementation PersonsVC +@synthesize treeController; +@synthesize personsView; +@synthesize contactProxyModel; + +-(void) awakeFromNib +{ + new ImageManipulationDelegate(); + 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:)]; + + CategorizedContactModel::instance()->setUnreachableHidden(YES); + PersonModel::instance()->addCollection<AddressBookBackend>(LoadOptions::FORCE_ENABLED); + +} + +- (IBAction)callContact:(id)sender +{ + if([[treeController selectedNodes] count] > 0) { + QModelIndex qIdx = [treeController toQIdx:[treeController selectedNodes][0]]; + ContactMethod* m = nil; + 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() == 1) { + 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->setDialNumber(m); + c << Call::Action::ACCEPT; + } + } +} + +#pragma mark - NSOutlineViewDelegate methods + +// ------------------------------------------------------------------------------- +// shouldSelectItem:item +// ------------------------------------------------------------------------------- +- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + if(!qIdx.isValid()) + return NO; + + if(!qIdx.parent().isValid()) { + return NO; + } else { + return YES; + } +} + +// ------------------------------------------------------------------------------- +// dataCellForTableColumn:tableColumn:item +// ------------------------------------------------------------------------------- +- (NSCell *)outlineView:(NSOutlineView *)outlineView dataCellForTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + PersonCell *returnCell = [tableColumn dataCell]; + if(!qIdx.isValid()) + return returnCell; + + if(!qIdx.parent().isValid()) { + [returnCell setDrawsBackground:YES]; + [returnCell setBackgroundColor:[NSColor selectedControlColor]]; + } else { + [returnCell setDrawsBackground:NO]; + } + + return returnCell; +} + +// ------------------------------------------------------------------------------- +// textShouldEndEditing:fieldEditor +// ------------------------------------------------------------------------------- +- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor +{ + if ([[fieldEditor string] length] == 0) + { + // don't allow empty node names + return NO; + } + else + { + 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; +} + +// ------------------------------------------------------------------------------- +// outlineView:willDisplayCell:forTableColumn:item +// ------------------------------------------------------------------------------- +- (void)outlineView:(NSOutlineView *)olv willDisplayCell:(NSCell*)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + if(!qIdx.isValid()) + return; + + if ([[tableColumn identifier] isEqualToString:COLUMNID_NAME]) + { + PersonCell *pCell = (PersonCell *)cell; + [pCell setPersonImage:nil]; + if(!qIdx.parent().isValid()) { + pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString(); + } else { + pCell.title = qIdx.data(Qt::DisplayRole).toString().toNSString(); + if(((NSTreeNode*)item).indexPath.length == 2) { + Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object)); + QVariant photo = ImageManipulationDelegate::instance()->contactPhoto(p, QSize(35,35)); + [pCell setPersonImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))]; + } + } + } +} + +// ------------------------------------------------------------------------------- +// outlineViewSelectionDidChange:notification +// ------------------------------------------------------------------------------- +- (void)outlineViewSelectionDidChange:(NSNotification *)notification +{ + +} + +- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + if(!qIdx.isValid()) + return 0.0f; + + if(!qIdx.parent().isValid()) { + return 20.0; + } else { + return 45.0; + } +} + + +@end diff --git a/src/QNSTreeController.mm b/src/QNSTreeController.mm index 75533c25b9a676f57cabeda803a54e305b61b846..eca4884808c02cde1dbd492513d435d738c5fd3d 100644 --- a/src/QNSTreeController.mm +++ b/src/QNSTreeController.mm @@ -84,13 +84,33 @@ NSUInteger myArray[[idx length]]; [idx getIndexes:myArray]; QModelIndex toReturn; - if(idx.length == 2) - toReturn = self->privateQModel->index(myArray[1], 0, self->privateQModel->index(myArray[0], 0)); - else - toReturn = self->privateQModel->index(myArray[0], 0); + + for (int i = 0; i < idx.length; ++i) { + toReturn = self->privateQModel->index(myArray[i], 0, toReturn); + } + return toReturn; } +- (void) insertChildAtQIndex:(QModelIndex) qIdx +{ + Node* child = [[Node alloc] init]; + + QModelIndex tmp = qIdx.parent(); + NSMutableArray* allIndexes = [NSMutableArray array]; + while (tmp.isValid()) { + [allIndexes insertObject:@(tmp.row()) atIndex:0]; + tmp = tmp.parent(); + } + [allIndexes insertObject:@(qIdx.row()) atIndex:allIndexes.count]; + + NSUInteger indexes[allIndexes.count]; + for (int i = 0 ; i < allIndexes.count ; ++i) { + indexes[i] = [[allIndexes objectAtIndex:i] intValue]; + } + [self insertObject:child atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:allIndexes.count]]; +} + - (void)connect { QObject::connect(self->privateQModel, @@ -102,9 +122,7 @@ Node* n = [[Node alloc] init]; [self insertObject:n atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndex:row]]; } else { - Node* child = [[Node alloc] init]; - NSUInteger indexes[] = { (NSUInteger)parent.row(), (NSUInteger)row}; - [self insertObject:child atArrangedObjectIndexPath:[[NSIndexPath alloc] initWithIndexes:indexes length:2]]; + [self insertChildAtQIndex:self->privateQModel->index(row, 0, parent)]; } } } diff --git a/src/backends/AddressBookBackend.h b/src/backends/AddressBookBackend.h new file mode 100644 index 0000000000000000000000000000000000000000..7856851927da81939ad531cf00fb37d1a92e9df0 --- /dev/null +++ b/src/backends/AddressBookBackend.h @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ +#ifndef ADDRESSBOOKBACKEND_H +#define ADDRESSBOOKBACKEND_H + +#include <collectioninterface.h> +#include <collectioneditor.h> + +class Person; + +template<typename T> class CollectionMediator; + +class AddressBookBackend : public CollectionInterface +{ +public: + explicit AddressBookBackend(CollectionMediator<Person>* mediator); + virtual ~AddressBookBackend(); + + virtual bool load() override; + virtual bool reload() override; + virtual bool clear() override; + virtual QString name () const override; + virtual QString category () const override; + virtual QVariant icon () const override; + virtual bool isEnabled() const override; + virtual QByteArray id () const override; + virtual SupportedFeatures supportedFeatures() const override; + +private: + CollectionMediator<Person>* m_pMediator; + + void asyncLoad(int startingPoint); +}; + +#endif // ADDRESSBOOKBACKEND_H diff --git a/src/backends/AddressBookBackend.mm b/src/backends/AddressBookBackend.mm new file mode 100644 index 0000000000000000000000000000000000000000..d2073c003bcf144e75e2dbcc182be5ebea506348 --- /dev/null +++ b/src/backends/AddressBookBackend.mm @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall import the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ +#import "AddressBookBackend.h" + +#import <AddressBook/AddressBook.h> + +//Qt +#import <QtCore/QFile> +#import <QtCore/QDir> +#import <QtCore/QHash> +#import <QtWidgets/QApplication> +#import <QtCore/QStandardPaths> +#import <QTimer> +#import <QtGlobal> + +//Ring +#import <Person.h> +#import <account.h> +#import <person.h> +#import <contactmethod.h> + +/** + * + *kABFirstNameProperty + kABLastNameProperty + kABFirstNamePhoneticProperty + kABLastNamePhoneticProperty + kABBirthdayProperty + kABOrganizationProperty + kABJobTitleProperty + kABHomePageProperty + kABEmailProperty + kABAddressProperty + kABPhoneProperty + kABAIMInstantProperty + kABJabberInstantProperty + kABMSNInstantProperty + kABYahooInstantProperty + kABICQInstantProperty + kABNoteProperty + kABMiddleNameProperty + kABMiddleNamePhoneticProperty + kABTitleProperty + kABSuffixProperty + kABNicknameProperty + kABMaidenNameProperty + */ + +class AddressBookEditor : public CollectionEditor<Person> +{ +public: + AddressBookEditor(CollectionMediator<Person>* m, AddressBookBackend* parent); + virtual bool save ( const Person* item ) override; + virtual bool remove ( const Person* item ) override; + virtual bool edit ( Person* item ) override; + virtual bool addNew ( const Person* item ) override; + virtual bool addExisting( const Person* item ) override; + +private: + virtual QVector<Person*> items() const override; + + //Helpers + void savePerson(QTextStream& stream, const Person* Person); + bool regenFile(const Person* toIgnore); + + //Attributes + QVector<Person*> m_lItems; + AddressBookBackend* m_pCollection; +}; + +AddressBookEditor::AddressBookEditor(CollectionMediator<Person>* m, AddressBookBackend* parent) : +CollectionEditor<Person>(m),m_pCollection(parent) +{ + +} + +AddressBookBackend::AddressBookBackend(CollectionMediator<Person>* mediator) : +CollectionInterface(new AddressBookEditor(mediator,this)),m_pMediator(mediator) +{ + +} + +AddressBookBackend::~AddressBookBackend() +{ + +} + +void AddressBookEditor::savePerson(QTextStream& stream, const Person* Person) +{ + + qDebug() << "Saving Person!"; +} + +bool AddressBookEditor::regenFile(const Person* toIgnore) +{ + QDir dir(QString('/')); + dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString()); + + + return false; +} + +bool AddressBookEditor::save(const Person* Person) +{ + //if (Person->collection()->editor<Person>() != this) + // return addNew(Person); + + return regenFile(nullptr); +} + +bool AddressBookEditor::remove(const Person* item) +{ + return regenFile(item); +} + +bool AddressBookEditor::edit( Person* item) +{ + Q_UNUSED(item) + return false; +} + +bool AddressBookEditor::addNew(const Person* Person) +{ + QDir dir(QString('/')); + dir.mkpath(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QLatin1Char('/') + QString()); + + return false; +} + +bool AddressBookEditor::addExisting(const Person* item) +{ + m_lItems << const_cast<Person*>(item); + mediator()->addItem(item); + return true; +} + +QVector<Person*> AddressBookEditor::items() const +{ + return m_lItems; +} + +QString AddressBookBackend::name () const +{ + return QObject::tr("AddressBook backend"); +} + +QString AddressBookBackend::category () const +{ + return QObject::tr("Persons"); +} + +QVariant AddressBookBackend::icon() const +{ + return QVariant(); +} + +bool AddressBookBackend::isEnabled() const +{ + return true; +} + +bool AddressBookBackend::load() +{ + QTimer::singleShot(100, [=] { + asyncLoad(0); + }); + return false; +} + +void AddressBookBackend::asyncLoad(int startingPoint) +{ + ABAddressBook *book = [ABAddressBook sharedAddressBook]; + NSArray *everyone = [book people]; + int endPoint = qMin(startingPoint + 10, (int)everyone.count); + + for (int i = startingPoint; i < endPoint; ++i) { + + Person* person = new Person(QByteArray::fromNSData(((ABPerson*)[everyone objectAtIndex:i]).vCardRepresentation), + Person::Encoding::vCard, + this); + if([person->formattedName().toNSString() isEqualToString:@""] && + [person->secondName().toNSString() isEqualToString:@""] && + [person->firstName().toNSString() isEqualToString:@""]) { + continue; + } + person->setCollection(this); + + editor<Person>()->addExisting(person); + } + + if(endPoint < everyone.count) { + QTimer::singleShot(100, [=] { + asyncLoad(endPoint); + }); + } + +} + + +bool AddressBookBackend::reload() +{ + return false; +} + +CollectionInterface::SupportedFeatures AddressBookBackend::supportedFeatures() const +{ + return (CollectionInterface::SupportedFeatures) ( + CollectionInterface::SupportedFeatures::NONE | + CollectionInterface::SupportedFeatures::LOAD | + CollectionInterface::SupportedFeatures::CLEAR | + CollectionInterface::SupportedFeatures::REMOVE| + CollectionInterface::SupportedFeatures::ADD ); +} + +bool AddressBookBackend::clear() +{ + /* TODO: insert confirm dialog? */ + return true; +} + +QByteArray AddressBookBackend::id() const +{ + return "abb"; +} diff --git a/src/delegates/ImageManipulationDelegate.h b/src/delegates/ImageManipulationDelegate.h new file mode 100644 index 0000000000000000000000000000000000000000..2f711aea1c4086cef01ef37fa75ec869c602953d --- /dev/null +++ b/src/delegates/ImageManipulationDelegate.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ +#ifndef IMAGEMANIPULATION_H +#define IMAGEMANIPULATION_H + +#import <QuartzCore/QuartzCore.h> + +#import <delegates/pixmapmanipulationdelegate.h> +#import <call.h> + +class Person; +class QPixmap; + +class ImageManipulationDelegate : public PixmapManipulationDelegate { + +public: + ImageManipulationDelegate(); + QVariant contactPhoto(Person* c, const QSize& size, bool displayPresence = true) override; + virtual QByteArray toByteArray(const QVariant& pxm) override; + virtual QVariant personPhoto(const QByteArray& data, const QString& type = "PNG") override; + +private: + //Helper + QPixmap drawDefaultUserPixmap(const QSize& size, bool displayPresence, bool isPresent); + CGImageRef resizeCGImage(CGImageRef image, const QSize& size); + + +}; + +#endif // IMAGEMANIPULATION_H diff --git a/src/delegates/ImageManipulationDelegate.mm b/src/delegates/ImageManipulationDelegate.mm new file mode 100644 index 0000000000000000000000000000000000000000..658785fede797dc6bcf43920f4f168171e7dafe0 --- /dev/null +++ b/src/delegates/ImageManipulationDelegate.mm @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall import the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ +#import "ImageManipulationDelegate.h" + +#import <Cocoa/Cocoa.h> +#import <Foundation/Foundation.h> + +//Qt +#import <QSize> +#import <QBuffer> +#import <QtGui/QColor> +#import <QtGui/QPainter> +#import <QtGui/QBitmap> +#import <QtWidgets/QApplication> +#import <QtGui/QImage> +#import <QtMacExtras/qmacfunctions.h> +#import <QtGui/QPalette> + +//Ring +#import <person.h> +#import <contactmethod.h> +#import <presencestatusmodel.h> +#import <securityvalidationmodel.h> +#import <collectioninterface.h> +#import <useractionmodel.h> +#import <QStandardPaths> + +ImageManipulationDelegate::ImageManipulationDelegate() : PixmapManipulationDelegate() +{ + +} + +QVariant ImageManipulationDelegate::contactPhoto(Person* c, const QSize& size, bool displayPresence) { + const int radius = (size.height() > 35) ? 7 : 5; + + QPixmap pxm; + if (c->photo().isValid()) { + QPixmap contactPhoto((qvariant_cast<QPixmap>(c->photo())).scaledToWidth(size.height()-6)); + pxm = QPixmap(size); + pxm.fill(Qt::transparent); + QPainter painter(&pxm); + + //Clear the pixmap + painter.setCompositionMode(QPainter::CompositionMode_Clear); + painter.fillRect(0,0,size.width(),size.height(),QBrush(Qt::white)); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + + //Add corner radius to the Pixmap + QRect pxRect = contactPhoto.rect(); + QBitmap mask(pxRect.size()); + QPainter customPainter(&mask); + customPainter.setRenderHint (QPainter::Antialiasing, true ); + customPainter.fillRect (pxRect , Qt::white ); + customPainter.setBackground (Qt::black ); + customPainter.setBrush (Qt::black ); + customPainter.drawRoundedRect(pxRect,radius,radius); + contactPhoto.setMask(mask); + painter.drawPixmap(3,3,contactPhoto); + painter.setBrush(Qt::NoBrush); + painter.setPen(Qt::white); + painter.setRenderHint (QPainter::Antialiasing, true ); + painter.setCompositionMode(QPainter::CompositionMode_SourceIn); + painter.drawRoundedRect(3,3,pxm.height()-6,pxm.height()-6,radius,radius); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + + } + else { + pxm = drawDefaultUserPixmap(size, false, false); + } + + return pxm; +} + +QVariant ImageManipulationDelegate::personPhoto(const QByteArray& data, const QString& type) +{ + QImage image; + //For now, ENCODING is only base64 and image type PNG or JPG + const bool ret = image.loadFromData(QByteArray::fromBase64(data),type.toLatin1()); + if (!ret) + qDebug() << "vCard image loading failed"; + + return QPixmap::fromImage(image); +} + +QByteArray ImageManipulationDelegate::toByteArray(const QVariant& pxm) +{ + //Preparation of our QPixmap + QByteArray bArray; + QBuffer buffer(&bArray); + buffer.open(QIODevice::WriteOnly); + + //PNG ? + (qvariant_cast<QPixmap>(pxm)).save(&buffer, "PNG"); + buffer.close(); + + return bArray; +} + +QPixmap ImageManipulationDelegate::drawDefaultUserPixmap(const QSize& size, bool displayPresence, bool isPresent) { + + QPixmap pxm(size); + pxm.fill(Qt::transparent); + QPainter painter(&pxm); + + // create the image somehow, load from file, draw into it... + CGImageSourceRef source; + + source = CGImageSourceCreateWithData((CFDataRef)[[NSImage imageNamed:@"NSUser"] TIFFRepresentation], NULL); + CGImageRef maskRef = CGImageSourceCreateImageAtIndex(source, 0, NULL); + painter.drawPixmap(3,3,QtMac::fromCGImageRef(resizeCGImage(maskRef, size))); + + return pxm; +} + +CGImageRef ImageManipulationDelegate::resizeCGImage(CGImageRef image, const QSize& size) { + // create context, keeping original image properties + CGColorSpaceRef colorspace = CGImageGetColorSpace(image); + CGContextRef context = CGBitmapContextCreate(NULL, size.width(), size.height(), + CGImageGetBitsPerComponent(image), + CGImageGetBytesPerRow(image), + colorspace, + CGImageGetAlphaInfo(image)); + CGColorSpaceRelease(colorspace); + + + if(context == NULL) + return nil; + + // draw image to context (resizing it) + CGContextDrawImage(context, CGRectMake(0, 0, size.width(), size.height()), image); + // extract resulting image from context + CGImageRef imgRef = CGBitmapContextCreateImage(context); + CGContextRelease(context); + + return imgRef; +} \ No newline at end of file diff --git a/src/views/PersonCell.h b/src/views/PersonCell.h new file mode 100644 index 0000000000000000000000000000000000000000..34aeecdbc48348f42dd6cac8ea5f976d3f73398c --- /dev/null +++ b/src/views/PersonCell.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#import <Cocoa/Cocoa.h> + + +@interface PersonCell : NSTextFieldCell +{ + BOOL mIsEditingOrSelecting; + +} +@property NSImage *personImage; +@property NSString* primaryText; +@property NSString* secondaryText; + +@end diff --git a/src/views/PersonCell.mm b/src/views/PersonCell.mm new file mode 100644 index 0000000000000000000000000000000000000000..9b6bede5f98fec99a2f7c2eb31fc9402789cbe92 --- /dev/null +++ b/src/views/PersonCell.mm @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2004-2015 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. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this program, or any covered work, by linking or + * combining it with the OpenSSL project's OpenSSL library (or a + * modified version of that library), containing parts covered by the + * terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc. + * grants you additional permission to convey the resulting work. + * Corresponding Source for a non-source form of such a combination + * shall include the source code for the parts of OpenSSL used as well + * as that of the covered work. + */ + +#import "PersonCell.h" + +#define kImageOriginXOffset 3 +#define kImageOriginYOffset 1 + +#define kTextOriginXOffset 2 +#define kTextOriginYOffset 2 +#define kTextHeightAdjust 4 + +@implementation PersonCell + +// ------------------------------------------------------------------------------- +// initTextCell:aString +// ------------------------------------------------------------------------------- +- (instancetype)initTextCell:(NSString *)aString +{ + self = [super initTextCell:aString]; + if (self != nil) + { + // we want a smaller font + [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + } + return self; +} + +// ------------------------------------------------------------------------------- +// copyWithZone:zone +// ------------------------------------------------------------------------------- +- (id)copyWithZone:(NSZone *)zone +{ + PersonCell *cell = (PersonCell *)[super copyWithZone:zone]; + cell.personImage = self.personImage; + return cell; +} + +// ------------------------------------------------------------------------------- +// titleRectForBounds:cellRect +// +// Returns the proper bound for the cell's title while being edited +// ------------------------------------------------------------------------------- +- (NSRect)titleRectForBounds:(NSRect)cellRect +{ + // the cell has an image: draw the normal item cell + NSSize imageSize; + NSRect imageFrame; + + imageSize = [self.personImage size]; + NSDivideRect(cellRect, &imageFrame, &cellRect, 3 + imageSize.width, NSMinXEdge); + + imageFrame.origin.x += kImageOriginXOffset; + imageFrame.origin.y -= kImageOriginYOffset; + imageFrame.size = imageSize; + + imageFrame.origin.y += ceil((cellRect.size.height - imageFrame.size.height) / 2); + + NSRect newFrame = cellRect; + newFrame.origin.x += kTextOriginXOffset; + newFrame.origin.y += kTextOriginYOffset; + newFrame.size.height -= kTextHeightAdjust; + + return newFrame; +} + +// ------------------------------------------------------------------------------- +// editWithFrame:inView:editor:delegate:event +// ------------------------------------------------------------------------------- +- (void)editWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject event:(NSEvent *)theEvent +{ + NSRect textFrame = [self titleRectForBounds:aRect]; + [super editWithFrame:textFrame inView:controlView editor:textObj delegate:anObject event:theEvent]; +} + +// ------------------------------------------------------------------------------- +// selectWithFrame:inView:editor:delegate:event:start:length +// ------------------------------------------------------------------------------- +- (void)selectWithFrame:(NSRect)aRect inView:(NSView *)controlView editor:(NSText *)textObj delegate:(id)anObject start:(NSInteger)selStart length:(NSInteger)selLength +{ + NSRect textFrame = [self titleRectForBounds:aRect]; + [super selectWithFrame:textFrame inView:controlView editor:textObj delegate:anObject start:selStart length:selLength]; +} + +// ------------------------------------------------------------------------------- +// drawWithFrame:cellFrame:controlView +// ------------------------------------------------------------------------------- +- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView +{ + NSRect newCellFrame = cellFrame; + + if (self.personImage != nil) + { + NSSize imageSize; + NSRect imageFrame; + + imageSize = [self.personImage size]; + NSDivideRect(newCellFrame, &imageFrame, &newCellFrame, imageSize.width, NSMinXEdge); + if ([self drawsBackground]) + { + [[self backgroundColor] set]; + NSRectFill(imageFrame); + } + + imageFrame.origin.y += 2; + imageFrame.size = imageSize; + + [self.personImage drawInRect:imageFrame + fromRect:NSZeroRect + operation:NSCompositeSourceOver + fraction:1.0 + respectFlipped:YES + hints:nil]; + } + + [super drawWithFrame:newCellFrame inView:controlView]; +} + +- (NSRect)drawingRectForBounds:(NSRect)theRect +{ + // Get the parent's idea of where we should draw + NSRect newRect = [super drawingRectForBounds:theRect]; + + // When the text field is being + // edited or selected, we have to turn off the magic because it screws up + // the configuration of the field editor. We sneak around this by + // intercepting selectWithFrame and editWithFrame and sneaking a + // reduced, centered rect in at the last minute. + if (mIsEditingOrSelecting == NO) + { + // Get our ideal size for current text + NSSize textSize = [self cellSizeForBounds:theRect]; + + // Center that in the proposed rect + float heightDelta = newRect.size.height - textSize.height; + if (heightDelta > 0) + { + newRect.size.height -= heightDelta; + newRect.origin.y += (heightDelta / 2); + } + } + return newRect; +} + +// ------------------------------------------------------------------------------- +// cellSize +// ------------------------------------------------------------------------------- +- (NSSize)cellSize +{ + NSSize cellSize = [super cellSize]; + cellSize.width += (self.personImage ? [self.personImage size].width : 0) + 3; + return cellSize; +} + +@end diff --git a/ui/RingWindow.xib b/ui/RingWindow.xib index df2e1367af117c21785b20e77406bc90e8b0cb7c..17975d94e08896a322fbc4eecca517a6cec241d2 100644 --- a/ui/RingWindow.xib +++ b/ui/RingWindow.xib @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="no"?> -<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="14C1510" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> +<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="6751" systemVersion="13F1066" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies> <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="6751"/> </dependencies> @@ -17,25 +17,25 @@ <windowCollectionBehavior key="collectionBehavior" fullScreenPrimary="YES"/> <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/> <rect key="contentRect" x="196" y="240" width="1053" height="626"/> - <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/> + <rect key="screenRect" x="0.0" y="0.0" width="1680" height="1050"/> <view key="contentView" autoresizesSubviews="NO" id="se5-gp-TjO"> <rect key="frame" x="0.0" y="0.0" width="1053" height="626"/> <autoresizingMask key="autoresizingMask"/> <subviews> <scrollView focusRingType="none" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bqQ-DB-Z0g"> - <rect key="frame" x="20" y="461" width="300" height="145"/> - <clipView key="contentView" focusRingType="none" misplaced="YES" id="1so-Pz-QB2"> + <rect key="frame" x="20" y="461" width="295" height="145"/> + <clipView key="contentView" focusRingType="none" id="1so-Pz-QB2"> <rect key="frame" x="1" y="17" width="238" height="117"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> <outlineView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" headerView="nuM-bu-a6l" indentationPerLevel="16" outlineTableColumn="VDO-Cu-h8f" id="zcl-pp-rGb"> - <rect key="frame" x="0.0" y="0.0" width="298" height="19"/> + <rect key="frame" x="0.0" y="0.0" width="293" height="19"/> <autoresizingMask key="autoresizingMask"/> <size key="intercellSpacing" width="3" height="2"/> <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> <tableColumns> - <tableColumn identifier="ConversationsColumn" width="295" minWidth="40" maxWidth="1000" id="VDO-Cu-h8f"> + <tableColumn identifier="ConversationsColumn" width="290" minWidth="40" maxWidth="1000" id="VDO-Cu-h8f"> <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left" title="Conversations"> <font key="font" metaFont="smallSystem"/> <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> @@ -57,7 +57,7 @@ <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> </clipView> <constraints> - <constraint firstAttribute="width" constant="300" id="eCz-d8-35b"/> + <constraint firstAttribute="width" constant="295" id="eCz-d8-35b"/> <constraint firstAttribute="height" constant="145" id="lWc-BG-yhJ"/> </constraints> <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="Fze-JQ-8rU"> @@ -73,18 +73,18 @@ <autoresizingMask key="autoresizingMask"/> </tableHeaderView> </scrollView> - <tabView focusRingType="none" ambiguous="YES" misplaced="YES" type="bottomTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="xXW-iq-GcP"> + <tabView focusRingType="none" type="bottomTabsBezelBorder" translatesAutoresizingMaskIntoConstraints="NO" id="xXW-iq-GcP"> <rect key="frame" x="13" y="14" width="309" height="443"/> <font key="font" metaFont="system"/> <tabViewItems> <tabViewItem label="History" identifier="1" id="Wi9-Zd-O1N"> - <view key="view" ambiguous="YES" id="h2E-qI-upQ"> - <rect key="frame" x="10" y="7" width="289" height="405"/> + <view key="view" id="h2E-qI-upQ"> + <rect key="frame" x="10" y="7" width="295" height="405"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> - <scrollView ambiguous="YES" misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9uI-D5-KRt"> + <scrollView misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="19" horizontalPageScroll="10" verticalLineScroll="19" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9uI-D5-KRt"> <rect key="frame" x="-3" y="0.0" width="295" height="408"/> - <clipView key="contentView" ambiguous="YES" misplaced="YES" id="eME-fQ-3QX"> + <clipView key="contentView" misplaced="YES" id="eME-fQ-3QX"> <rect key="frame" x="1" y="17" width="238" height="117"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <subviews> @@ -142,6 +142,9 @@ </subviews> <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> </clipView> + <constraints> + <constraint firstAttribute="width" constant="295" id="x0a-p6-WiG"/> + </constraints> <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="9To-im-dx7"> <rect key="frame" x="1" y="119" width="223" height="15"/> <autoresizingMask key="autoresizingMask"/> @@ -157,10 +160,70 @@ </scrollView> </subviews> <constraints> - <constraint firstAttribute="trailing" secondItem="9uI-D5-KRt" secondAttribute="trailing" constant="3" id="7Oo-Z3-JeE"/> - <constraint firstItem="9uI-D5-KRt" firstAttribute="top" secondItem="h2E-qI-upQ" secondAttribute="top" constant="3" id="Fu4-zc-WvO"/> - <constraint firstItem="9uI-D5-KRt" firstAttribute="leading" secondItem="h2E-qI-upQ" secondAttribute="leading" constant="3" id="mO1-Tn-dFB"/> - <constraint firstAttribute="bottom" secondItem="9uI-D5-KRt" secondAttribute="bottom" id="mRP-eB-8tk"/> + <constraint firstItem="9uI-D5-KRt" firstAttribute="leading" secondItem="h2E-qI-upQ" secondAttribute="leading" id="IKi-Kl-zdl"/> + <constraint firstItem="9uI-D5-KRt" firstAttribute="top" secondItem="h2E-qI-upQ" secondAttribute="top" id="SNf-F0-6HX"/> + <constraint firstAttribute="trailing" secondItem="9uI-D5-KRt" secondAttribute="trailing" id="adg-yP-eVY"/> + <constraint firstAttribute="bottom" secondItem="9uI-D5-KRt" secondAttribute="bottom" id="nLK-Pj-h7B"/> + </constraints> + </view> + </tabViewItem> + <tabViewItem label="Contacts" identifier="" id="Zbi-X6-DLT"> + <view key="view" id="sag-tS-7Jw"> + <rect key="frame" x="10" y="7" width="289" height="405"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <scrollView focusRingType="none" misplaced="YES" autohidesScrollers="YES" horizontalLineScroll="48" horizontalPageScroll="10" verticalLineScroll="48" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rJv-ju-DFe"> + <rect key="frame" x="-3" y="5" width="295" height="403"/> + <clipView key="contentView" focusRingType="none" misplaced="YES" id="S00-xr-jYM"> + <rect key="frame" x="1" y="0.0" width="238" height="134"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <outlineView focusRingType="none" verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" multipleSelection="NO" autosaveColumns="NO" rowHeight="46" indentationPerLevel="16" autoresizesOutlineColumn="YES" outlineTableColumn="8Ve-L0-o7V" id="Hrg-Fe-uGq"> + <rect key="frame" x="0.0" y="0.0" width="293" height="48"/> + <autoresizingMask key="autoresizingMask"/> + <size key="intercellSpacing" width="3" height="2"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/> + <color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/> + <tableColumns> + <tableColumn identifier="NameColumn" width="289.7734375" minWidth="40" maxWidth="1000" id="8Ve-L0-o7V"> + <tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border" alignment="left"> + <font key="font" metaFont="system"/> + <color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/> + </tableHeaderCell> + <textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" alignment="left" title="Text Cell" id="GAa-pQ-t4g" customClass="PersonCell"> + <font key="font" metaFont="system"/> + <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </textFieldCell> + <tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/> + </tableColumn> + </tableColumns> + <connections> + <outlet property="delegate" destination="9RF-6W-vEW" id="5bm-gu-2q9"/> + </connections> + </outlineView> + </subviews> + <color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/> + </clipView> + <constraints> + <constraint firstAttribute="width" constant="295" id="2Ia-36-f20"/> + </constraints> + <scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="YES" id="TGe-ww-q1j"> + <rect key="frame" x="1" y="119" width="223" height="15"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + <scroller key="verticalScroller" hidden="YES" verticalHuggingPriority="750" horizontal="NO" id="dD5-3G-tUh"> + <rect key="frame" x="224" y="17" width="15" height="102"/> + <autoresizingMask key="autoresizingMask"/> + </scroller> + </scrollView> + </subviews> + <constraints> + <constraint firstAttribute="bottom" secondItem="rJv-ju-DFe" secondAttribute="bottom" constant="5" id="84Q-se-prS"/> + <constraint firstItem="rJv-ju-DFe" firstAttribute="leading" secondItem="sag-tS-7Jw" secondAttribute="leading" id="FHh-Sy-7VP"/> + <constraint firstItem="rJv-ju-DFe" firstAttribute="top" secondItem="sag-tS-7Jw" secondAttribute="top" id="VST-yM-sIz"/> + <constraint firstAttribute="trailing" secondItem="rJv-ju-DFe" secondAttribute="trailing" id="rZG-K9-Bc8"/> </constraints> </view> </tabViewItem> @@ -189,7 +252,7 @@ <connections> <outlet property="delegate" destination="-2" id="0bl-1N-AYu"/> </connections> - <point key="canvasLocation" x="530" y="361"/> + <point key="canvasLocation" x="529.5" y="361"/> </window> <viewController id="jzj-dD-ryc" customClass="HistoryViewController"> <connections> @@ -204,8 +267,11 @@ <outlet property="view" destination="tSW-YT-asL" id="fv5-ly-rk8"/> </connections> </viewController> + <viewController id="9RF-6W-vEW" customClass="PersonsVC"> + <connections> + <outlet property="personsView" destination="Hrg-Fe-uGq" id="nMb-nK-g6a"/> + <outlet property="view" destination="rJv-ju-DFe" id="A8H-6d-bW2"/> + </connections> + </viewController> </objects> - <resources> - <image name="NSActionTemplate" width="14" height="14"/> - </resources> </document>