From 58276bcbc18bea0c9f9ec8d8f6996a93940e1242 Mon Sep 17 00:00:00 2001 From: Kateryna Kostiuk Date: Wed, 7 Jun 2017 08:50:48 -0400 Subject: [PATCH] UI: update chat view UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now view used for sending and receiving messages during the call (ChatView) is the same as for regular ConversationView. Also this commit fix two problems: 1)remove black imprints of text views, that appeared after window resizing 2)set status "read" for messages arriving during call, so they don't appear in SmartList as unread. Change-Id: I6d0cb79878395d28cfc93491a9d4cab42ed89192 Reviewed-by: Anthony LĂ©onard --- CMakeLists.txt | 7 +- src/ChatVC.mm | 82 +++++------- src/ConversationVC.mm | 184 +++----------------------- src/MessagesVC.h | 33 +++++ src/MessagesVC.mm | 228 +++++++++++++++++++++++++++++++++ src/views/IMTableCellView.h | 2 + src/views/IMTableCellView.mm | 14 +- src/views/MessageBubbleView.h | 34 +++++ src/views/MessageBubbleView.mm | 80 ++++++++++++ ui/Base.lproj/Conversation.xib | 66 +++++++--- ui/Base.lproj/CurrentCall.xib | 217 +++++++++++++++++++++++-------- 11 files changed, 654 insertions(+), 293 deletions(-) create mode 100644 src/MessagesVC.h create mode 100644 src/MessagesVC.mm create mode 100644 src/views/MessageBubbleView.h create mode 100644 src/views/MessageBubbleView.mm diff --git a/CMakeLists.txt b/CMakeLists.txt index d3d77c2b..d972954d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,6 +168,8 @@ SET(ringclient_CONTROLLERS src/SendContactRequestWC.mm src/AccBannedContactsVC.h src/AccBannedContactsVC.mm + src/MessagesVC.h + src/MessagesVC.mm ) SET(ringclient_BACKENDS @@ -202,7 +204,10 @@ SET(ringclient_VIEWS src/views/ContactRequestCellView.h src/views/ContactRequestCellView.mm src/views/RoundedTextField.h - src/views/RoundedTextField.mm) + src/views/RoundedTextField.mm + src/views/MessageBubbleView.h + src/views/MessageBubbleView.mm +) SET(ringclient_OTHERS src/main.mm diff --git a/src/ChatVC.mm b/src/ChatVC.mm index c3712ab4..7655f16c 100644 --- a/src/ChatVC.mm +++ b/src/ChatVC.mm @@ -27,6 +27,8 @@ #import #import +#import "MessagesVC.h" + @interface MediaConnectionsHolder : NSObject @property QMetaObject::Connection newMediaAdded; @@ -36,20 +38,26 @@ @implementation MediaConnectionsHolder + @end -@interface ChatVC () +@interface ChatVC () +{ + IBOutlet MessagesVC* messagesViewVC; +} -@property (unsafe_unretained) IBOutlet NSTextView *chatView; @property (unsafe_unretained) IBOutlet NSTextField *messageField; @property (unsafe_unretained) IBOutlet NSButton *sendButton; + @property MediaConnectionsHolder* mediaHolder; @end @implementation ChatVC -@synthesize messageField,chatView,sendButton, mediaHolder; + + +@synthesize messageField,sendButton, mediaHolder; - (void)awakeFromNib { @@ -57,7 +65,7 @@ [self.view setWantsLayer:YES]; [self.view setLayer:[CALayer layer]]; - [self.view.layer setBackgroundColor:[NSColor blackColor].CGColor]; + [self.view.layer setBackgroundColor:[NSColor controlColor].CGColor]; mediaHolder = [[MediaConnectionsHolder alloc] init]; @@ -66,11 +74,7 @@ [=](const QModelIndex ¤t, const QModelIndex &previous) { [self setupChat]; }); - - // Override default style to add interline space - NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; - paragraphStyle.lineSpacing = 8; - [chatView setDefaultParagraphStyle:paragraphStyle]; + messagesViewVC.delegate = self; } @@ -101,58 +105,34 @@ if (media->type() == Media::Media::Type::TEXT) { QObject::disconnect(mediaHolder.newMediaAdded); [self parseChatModel:((Media::Text*)media)->recording()->instantMessagingModel()]; - } }); } } -- (void) parseChatModel:(QAbstractItemModel *)model -{ - QObject::disconnect(mediaHolder.newMessage); - [self.messageField setStringValue:@""]; - self.message = @""; - [self.chatView.textStorage.mutableString setString:@""]; - - /* put all the messages in the im model into the text view */ - for (int row = 0; row < model->rowCount(); ++row) { - [self appendNewMessage:model->index(row, 0)]; - } +#pragma mark - MessagesVC delegate - /* append new messages */ - mediaHolder.newMessage = QObject::connect(model, - &QAbstractItemModel::rowsInserted, - [self, model] (const QModelIndex &parent, int first, int last) { - for (int row = first; row <= last; ++row) { - [self appendNewMessage:model->index(row, 0, parent)]; - } - }); -} +-(void) newMessageAdded { -- (void) appendNewMessage:(const QModelIndex&) msgIdx -{ - if (!msgIdx.isValid()) + QModelIndex callIdx = CallModel::instance().selectionModel()->currentIndex(); + if (!callIdx.isValid()) return; + Call* call = CallModel::instance().getCall(callIdx); + if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::IN)) { + Media::Text *text = call->firstMedia(Media::Media::Direction::IN); + auto textRecording = text->recording(); + textRecording->setAllRead(); + } else if (call->hasMedia(Media::Media::Type::TEXT, Media::Media::Direction::OUT)) { + Media::Text *text = call->firstMedia(Media::Media::Direction::OUT); + auto textRecording = text->recording(); + textRecording->setAllRead(); + } +} - NSString* message = msgIdx.data(Qt::DisplayRole).value().toNSString(); - NSString* author = msgIdx.data((int)Media::TextRecording::Role::AuthorDisplayname).value().toNSString(); - - NSMutableAttributedString* attr = [[NSMutableAttributedString alloc] initWithString: - [NSString stringWithFormat:@"%@: %@\n",author, message]]; - - // put in bold type author name - [attr applyFontTraits:NSBoldFontMask range: NSMakeRange(0, [author length])]; - - [[chatView textStorage] appendAttributedString:attr]; - - // reapply paragraph style on all the text - NSRange range = NSMakeRange(0,[chatView textStorage].length); - [[self.chatView textStorage] addAttribute:NSParagraphStyleAttributeName - value:chatView.defaultParagraphStyle - range:range]; - - [chatView scrollRangeToVisible:NSMakeRange([[chatView string] length], 0)]; +- (void) parseChatModel:(QAbstractItemModel *)model +{ + [messagesViewVC setUpViewWithModel:model]; } - (void) takeFocus diff --git a/src/ConversationVC.mm b/src/ConversationVC.mm index fb3d9964..5739e0f3 100644 --- a/src/ConversationVC.mm +++ b/src/ConversationVC.mm @@ -43,17 +43,17 @@ #import "PhoneDirectoryModel.h" #import "account.h" #import "AvailableAccountModel.h" +#import "MessagesVC.h" #import -@interface ConversationVC () { +@interface ConversationVC () { __unsafe_unretained IBOutlet NSTextField* messageField; QVector contactMethods; NSMutableString* textSelection; - QNSTreeController* treeController; QMetaObject::Connection contactMethodChanged; ContactMethod* selectedContactMethod; SendContactRequestWC* sendRequestWC; @@ -62,11 +62,10 @@ __unsafe_unretained IBOutlet NSTextField* conversationTitle; __unsafe_unretained IBOutlet NSTextField* emptyConversationPlaceHolder; __unsafe_unretained IBOutlet IconButton* sendButton; - __unsafe_unretained IBOutlet NSOutlineView* conversationView; __unsafe_unretained IBOutlet NSPopUpButton* contactMethodsPopupButton; + IBOutlet MessagesVC* messagesViewVC; } -@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager; @end @@ -82,7 +81,6 @@ [sendPanel setWantsLayer:YES]; [sendPanel setLayer:[CALayer layer]]; - _selectionManager = [[INDSequentialTextSelectionManager alloc] init]; [self setupChat]; @@ -106,8 +104,6 @@ return ; } - [self.selectionManager unregisterAllTextViews]; - [contactMethodsPopupButton removeAllItems]; for (auto cm : contactMethods) { [contactMethodsPopupButton addItemWithTitle:cm->uri().toNSString()]; @@ -145,8 +141,8 @@ } - (IBAction)backPressed:(id)sender { - [conversationView setDelegate:nil]; RecentModel::instance().selectionModel()->clearCurrentIndex(); + messagesViewVC.delegate = nil; } - (IBAction)openSendContactRequestWindow:(id)sender @@ -199,158 +195,6 @@ [CATransaction commit]; } -#pragma mark - NSOutlineViewDelegate methods - -- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; -{ - return YES; -} - -- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item -{ - return YES; -} - -- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item -{ - QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; - auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); - IMTableCellView* result; - - if (dir == Media::Media::Direction::IN) { - result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self]; - } else { - result = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self]; - } - - [result setup]; - - NSMutableAttributedString* msgAttString = - [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] - attributes:[self messageAttributesFor:qIdx]]; - - NSAttributedString* timestampAttrString = - [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() - attributes:[self timestampAttributesFor:qIdx]]; - - - CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width); - finalWidth = MIN(finalWidth + 30, result.frame.size.width - result.photoView.frame.size.width - 30); - - [msgAttString appendAttributedString:timestampAttrString]; - [[result.msgView textStorage] appendAttributedString:msgAttString]; - [result.msgView checkTextInDocument:nil]; - [result.msgView setWantsLayer:YES]; - result.msgView.layer.cornerRadius = 5.0f; - - [result updateWidthConstraint:finalWidth]; - [result.photoView setImage:QtMac::toNSImage(qvariant_cast(qIdx.data(Qt::DecorationRole)))]; - return result; -} - -- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row -{ - if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) { - [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue]; - } - - if (auto txtRecording = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->textRecording()) { - [emptyConversationPlaceHolder setHidden:txtRecording->instantMessagingModel()->rowCount() > 0]; - txtRecording->setAllRead(); - } -} - -- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item -{ - QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; - - double someWidth = outlineView.frame.size.width; - - NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] - attributes:[self messageAttributesFor:qIdx]]; - NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString: - qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() - attributes:[self timestampAttributesFor:qIdx]]; - - [msgAttString appendAttributedString:timestampAttrString]; - - NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT); - NSTextView *tv = [[NSTextView alloc] initWithFrame:frame]; - [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink]; - [tv setAutomaticLinkDetectionEnabled:YES]; - [[tv textStorage] setAttributedString:msgAttString]; - [tv sizeToFit]; - - double height = tv.frame.size.height + 20; - - return MAX(height, 60.0f); -} - -#pragma mark - Text formatting - -- (NSMutableDictionary*) timestampAttributesFor:(QModelIndex) qIdx -{ - auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); - NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; - - if (dir == Media::Media::Direction::IN) { - attrs[NSForegroundColorAttributeName] = [NSColor grayColor]; - } else { - attrs[NSForegroundColorAttributeName] = [NSColor whiteColor]; - } - - NSFont* systemFont = [NSFont systemFontOfSize:12.0f]; - attrs[NSFontAttributeName] = systemFont; - attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; - - return attrs; -} - -- (NSMutableDictionary*) messageAttributesFor:(QModelIndex) qIdx -{ - auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); - NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; - - if (dir == Media::Media::Direction::IN) { - attrs[NSForegroundColorAttributeName] = [NSColor blackColor]; - } else { - attrs[NSForegroundColorAttributeName] = [NSColor whiteColor]; - } - - NSFont* systemFont = [NSFont systemFontOfSize:14.0f]; - attrs[NSFontAttributeName] = systemFont; - attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; - - return attrs; -} - -- (NSParagraphStyle*) paragraphStyle -{ - /* - The only way to instantiate an NSMutableParagraphStyle is to mutably copy an - NSParagraphStyle. And since we don't have an existing NSParagraphStyle available - to copy, we use the default one. - - The default values supplied by the default NSParagraphStyle are: - Alignment NSNaturalTextAlignment - Tab stops 12 left-aligned tabs, spaced by 28.0 points - Line break mode NSLineBreakByWordWrapping - All others 0.0 - */ - NSMutableParagraphStyle* aMutableParagraphStyle = - [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; - - // Now adjust our NSMutableParagraphStyle formatting to be whatever we want. - // The numeric values below are in points (72 points per inch) - [aMutableParagraphStyle setAlignment:NSLeftTextAlignment]; - [aMutableParagraphStyle setLineSpacing:1.5]; - [aMutableParagraphStyle setParagraphSpacing:5.0]; - [aMutableParagraphStyle setHeadIndent:5.0]; - [aMutableParagraphStyle setTailIndent:-5.0]; - [aMutableParagraphStyle setFirstLineHeadIndent:5.0]; - [aMutableParagraphStyle setLineBreakMode:NSLineBreakByWordWrapping]; - return aMutableParagraphStyle; -} #pragma mark - NSTextFieldDelegate @@ -378,17 +222,19 @@ }); if (auto txtRecording = selectedContactMethod->textRecording()) { - treeController = [[QNSTreeController alloc] initWithQModel:txtRecording->instantMessagingModel()]; - [treeController setAvoidsEmptySelection:NO]; - [treeController setChildrenKeyPath:@"children"]; - [conversationView setDelegate:self]; - [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; - [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil]; - [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; + messagesViewVC.delegate = self; + [messagesViewVC setUpViewWithModel:txtRecording->instantMessagingModel()]; } - - [conversationView scrollToEndOfDocument:nil]; } +#pragma mark - MessagesVC delegate + +-(void) newMessageAdded { + + if (auto txtRecording = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->textRecording()) { + [emptyConversationPlaceHolder setHidden:txtRecording->instantMessagingModel()->rowCount() > 0]; + txtRecording->setAllRead(); + } +} @end diff --git a/src/MessagesVC.h b/src/MessagesVC.h new file mode 100644 index 00000000..6b5430fd --- /dev/null +++ b/src/MessagesVC.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015-2017 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * 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 + +@protocol MessagesVCDelegate + +-(void)newMessageAdded; + +@end + +@interface MessagesVC : NSViewController + +-(void)setUpViewWithModel: (QAbstractItemModel*) model; +@property (retain, nonatomic) id delegate; + +@end diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm new file mode 100644 index 00000000..a38ba672 --- /dev/null +++ b/src/MessagesVC.mm @@ -0,0 +1,228 @@ +/* + * Copyright (C) 2015-2017 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * 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 +#import +#import +#import + +#import +#import +#import +#import +#import + +#import "MessagesVC.h" +#import "QNSTreeController.h" +#import "views/IMTableCellView.h" +#import "views/MessageBubbleView.h" +#import "INDSequentialTextSelectionManager.h" + +@interface MessagesVC () { + + QNSTreeController* treeController; + __unsafe_unretained IBOutlet NSOutlineView* conversationView; + +} + +@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager; + +@end + +@implementation MessagesVC +QAbstractItemModel* currentModel; + +-(void)setUpViewWithModel: (QAbstractItemModel*) model { + + _selectionManager = [[INDSequentialTextSelectionManager alloc] init]; + + [self.selectionManager unregisterAllTextViews]; + + treeController = [[QNSTreeController alloc] initWithQModel:model]; + [treeController setAvoidsEmptySelection:NO]; + [treeController setChildrenKeyPath:@"children"]; + [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; + [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil]; + [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; + [conversationView scrollToEndOfDocument:nil]; + currentModel = model; +} + +#pragma mark - NSOutlineViewDelegate methods + +- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; +{ + return YES; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + return YES; +} + +- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + if(!qIdx.isValid()) { + return [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self]; + } + auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); + IMTableCellView* result; + + if (dir == Media::Media::Direction::IN) { + result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self]; + } else { + result = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self]; + } + + // check if the message first in incoming or outgoing messages sequence + Boolean isFirstInSequence = true; + int row = qIdx.row() - 1; + if(row >= 0) { + QModelIndex index = currentModel->index(row, 0); + if(index.isValid()) { + auto dirOld = qvariant_cast(index.data((int)Media::TextRecording::Role::Direction)); + isFirstInSequence = !(dirOld == dir); + } + } + [result.photoView setHidden:!isFirstInSequence]; + result.msgBackground.needPointer = isFirstInSequence; + [result setup]; + + NSMutableAttributedString* msgAttString = + [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] + attributes:[self messageAttributesFor:qIdx]]; + + NSAttributedString* timestampAttrString = + [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() + attributes:[self timestampAttributesFor:qIdx]]; + + + CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width); + + finalWidth = MIN(finalWidth + 30, outlineView.frame.size.width * 0.7); + + NSString* msgString = qIdx.data((int)Qt::DisplayRole).toString().toNSString(); + NSString* dateString = qIdx.data((int)Qt::DisplayRole).toString().toNSString(); + + [msgAttString appendAttributedString:timestampAttrString]; + [[result.msgView textStorage] appendAttributedString:msgAttString]; + [result.msgView checkTextInDocument:nil]; + [result updateWidthConstraint:finalWidth]; + [result.photoView setImage:QtMac::toNSImage(qvariant_cast(qIdx.data(Qt::DecorationRole)))]; + return result; +} + +- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row +{ + if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) { + [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue]; + } + [self.delegate newMessageAdded]; +} + +- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item +{ + QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; + double someWidth = outlineView.frame.size.width * 0.7; + + NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] + attributes:[self messageAttributesFor:qIdx]]; + NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString: + qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() + attributes:[self timestampAttributesFor:qIdx]]; + + [msgAttString appendAttributedString:timestampAttrString]; + + NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT); + NSTextView *tv = [[NSTextView alloc] initWithFrame:frame]; + [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink]; + [tv setAutomaticLinkDetectionEnabled:YES]; + [[tv textStorage] setAttributedString:msgAttString]; + [tv sizeToFit]; + + double height = tv.frame.size.height + 10; + return MAX(height, 50.0f); +} + +#pragma mark - Text formatting + +- (NSMutableDictionary*) timestampAttributesFor:(QModelIndex) qIdx +{ + auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); + NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; + + if (dir == Media::Media::Direction::IN) { + attrs[NSForegroundColorAttributeName] = [NSColor grayColor]; + } else { + attrs[NSForegroundColorAttributeName] = [NSColor whiteColor]; + } + + NSFont* systemFont = [NSFont systemFontOfSize:12.0f]; + attrs[NSFontAttributeName] = systemFont; + attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; + + return attrs; +} + +- (NSMutableDictionary*) messageAttributesFor:(QModelIndex) qIdx +{ + auto dir = qvariant_cast(qIdx.data((int)Media::TextRecording::Role::Direction)); + NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; + + if (dir == Media::Media::Direction::IN) { + attrs[NSForegroundColorAttributeName] = [NSColor blackColor]; + } else { + attrs[NSForegroundColorAttributeName] = [NSColor whiteColor]; + } + + NSFont* systemFont = [NSFont systemFontOfSize:14.0f]; + attrs[NSFontAttributeName] = systemFont; + attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; + + return attrs; +} + +- (NSParagraphStyle*) paragraphStyle +{ + /* + The only way to instantiate an NSMutableParagraphStyle is to mutably copy an + NSParagraphStyle. And since we don't have an existing NSParagraphStyle available + to copy, we use the default one. + + The default values supplied by the default NSParagraphStyle are: + Alignment NSNaturalTextAlignment + Tab stops 12 left-aligned tabs, spaced by 28.0 points + Line break mode NSLineBreakByWordWrapping + All others 0.0 + */ + NSMutableParagraphStyle* aMutableParagraphStyle = + [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + + // Now adjust our NSMutableParagraphStyle formatting to be whatever we want. + // The numeric values below are in points (72 points per inch) + [aMutableParagraphStyle setLineSpacing:1.5]; + [aMutableParagraphStyle setParagraphSpacing:5.0]; + [aMutableParagraphStyle setHeadIndent:5.0]; + [aMutableParagraphStyle setTailIndent:-5.0]; + [aMutableParagraphStyle setFirstLineHeadIndent:5.0]; + return aMutableParagraphStyle; +} + +@end diff --git a/src/views/IMTableCellView.h b/src/views/IMTableCellView.h index 68fd8391..110dfb69 100644 --- a/src/views/IMTableCellView.h +++ b/src/views/IMTableCellView.h @@ -18,11 +18,13 @@ */ #import +#import "MessageBubbleView.h" @interface IMTableCellView : NSTableCellView @property (nonatomic, strong) IBOutlet NSImageView* photoView; @property (nonatomic, strong) IBOutlet NSTextView* msgView; +@property (nonatomic, strong) IBOutlet MessageBubbleView* msgBackground; - (void) setup; - (void) updateWidthConstraint:(CGFloat) newWidth; diff --git a/src/views/IMTableCellView.mm b/src/views/IMTableCellView.mm index a9d94979..4b8bd07a 100644 --- a/src/views/IMTableCellView.mm +++ b/src/views/IMTableCellView.mm @@ -29,13 +29,23 @@ - (void) setup { if ([self.identifier isEqualToString:@"RightMessageView"]) { - [self.msgView setBackgroundColor:[NSColor ringBlue]]; + self.msgBackground.pointerDirection = RIGHT; + self.msgBackground.bgColor = [NSColor ringBlue]; + + } + else { + self.msgBackground.pointerDirection = LEFT; + self.msgBackground.bgColor = [NSColor whiteColor]; } + [self.msgView setBackgroundColor:[NSColor clearColor]]; [self.msgView setString:@""]; [self.msgView setAutoresizingMask:NSViewWidthSizable]; + [self.msgView setAutoresizingMask:NSViewHeightSizable]; + [self.msgBackground setAutoresizingMask:NSViewWidthSizable]; + [self.msgBackground setAutoresizingMask:NSViewHeightSizable]; [self.msgView setEnabledTextCheckingTypes:NSTextCheckingTypeLink]; [self.msgView setAutomaticLinkDetectionEnabled:YES]; -} + } - (void) updateWidthConstraint:(CGFloat) newWidth { diff --git a/src/views/MessageBubbleView.h b/src/views/MessageBubbleView.h new file mode 100644 index 00000000..7b87c859 --- /dev/null +++ b/src/views/MessageBubbleView.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015-2017 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * 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 + +typedef NS_ENUM(NSInteger, PointerDirection) { + LEFT = 0, + RIGHT, + BLOCK, +}; + +@interface MessageBubbleView: NSView + +@property NSColor* bgColor; +@property Boolean needPointer; +@property enum PointerDirection pointerDirection; + +@end diff --git a/src/views/MessageBubbleView.mm b/src/views/MessageBubbleView.mm new file mode 100644 index 00000000..1634c8cd --- /dev/null +++ b/src/views/MessageBubbleView.mm @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015-2017 Savoir-faire Linux Inc. + * Author: Kateryna Kostiuk + * + * 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 "MessageBubbleView.h" +#import +#import +#import "NSColor+RingTheme.h" + +@implementation MessageBubbleView + +- (void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + CGContextRef context = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort]; + CGContextSetRGBFillColor(context, 1, 1, 1, 1); + CGFloat radius = 6; + CGFloat minx = CGRectGetMinX(dirtyRect), midx = CGRectGetMidX(dirtyRect), maxx = CGRectGetMaxX(dirtyRect); + CGFloat miny = CGRectGetMinY(dirtyRect), midy = CGRectGetMidY(dirtyRect), maxy = CGRectGetMaxY(dirtyRect); + + CGMutablePathRef outlinePath = CGPathCreateMutable(); + + if (self.pointerDirection == LEFT) + { + minx += 6; + CGPathMoveToPoint(outlinePath, nil, midx, miny); + CGPathAddArcToPoint(outlinePath, nil, maxx, miny, maxx, midy, radius); + CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, midx, maxy, radius); + CGPathAddArcToPoint(outlinePath, nil, minx, maxy, minx, midy, radius); + if(self.needPointer) { + CGPathAddLineToPoint(outlinePath, nil, minx, maxy - 20); + CGPathAddLineToPoint(outlinePath, nil, minx - 6, maxy - 15); + CGPathAddLineToPoint(outlinePath, nil, minx, maxy - 10); + } + + CGPathAddArcToPoint(outlinePath, nil, minx, miny, midx, miny, radius); + CGPathCloseSubpath(outlinePath); + } + else + { + maxx-=6; + CGPathMoveToPoint(outlinePath, nil, midx, miny); + CGPathAddArcToPoint(outlinePath, nil, minx, miny, minx, midy, radius); + CGPathAddArcToPoint(outlinePath, nil, minx, maxy, midx, maxy, radius); + CGPathAddArcToPoint(outlinePath, nil, maxx, maxy, maxx, midy, radius); + if(self.needPointer) { + CGPathAddLineToPoint(outlinePath, nil, maxx, maxy - 20); + CGPathAddLineToPoint(outlinePath, nil, maxx + 6, maxy - 15); + CGPathAddLineToPoint(outlinePath, nil, maxx, maxy - 10); + } + CGPathAddArcToPoint(outlinePath, nil, maxx, miny, midx, miny, radius); + CGPathCloseSubpath(outlinePath); + } + CGContextSetShadowWithColor(context, CGSizeMake(0,1), 1, [NSColor lightGrayColor].CGColor); + CGContextAddPath(context, outlinePath); + CGContextFillPath(context); + + CGContextAddPath(context, outlinePath); + CGContextClip(context); + if(self.bgColor) { + CGContextSetFillColorWithColor(context, self.bgColor.CGColor); + CGContextStrokePath(context); + NSRectFill(dirtyRect); + } +} +@end diff --git a/ui/Base.lproj/Conversation.xib b/ui/Base.lproj/Conversation.xib index bfe24a9a..64b70fb6 100644 --- a/ui/Base.lproj/Conversation.xib +++ b/ui/Base.lproj/Conversation.xib @@ -10,9 +10,9 @@ - + @@ -58,26 +58,37 @@ - + - - + + + + + + + + + + + + + - - - - - - + + + + + + @@ -87,25 +98,37 @@ - + - - + + + + + + + + + + + + + - - - - - + + + + + + @@ -114,7 +137,7 @@ - + @@ -305,6 +328,11 @@ Gw + + + + + diff --git a/ui/Base.lproj/CurrentCall.xib b/ui/Base.lproj/CurrentCall.xib index bb9e6597..56d568ea 100644 --- a/ui/Base.lproj/CurrentCall.xib +++ b/ui/Base.lproj/CurrentCall.xib @@ -1,7 +1,8 @@ - - + + - + + @@ -42,21 +43,21 @@ - + - + - + - + - + @@ -146,7 +147,7 @@ - + - + @@ -705,47 +830,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - + + + + + + - - @@ -753,6 +853,11 @@ + + + + + @@ -768,8 +873,8 @@ - + @@ -780,6 +885,7 @@ + + @@ -799,6 +907,7 @@ + @@ -807,6 +916,7 @@