UI: update chat view UI
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 <anthony.leonard@savoirfairelinux.com>
diff --git a/src/ChatVC.mm b/src/ChatVC.mm
index c3712ab..7655f16 100644
--- a/src/ChatVC.mm
+++ b/src/ChatVC.mm
@@ -27,6 +27,8 @@
#import <media/textrecording.h>
#import <callmodel.h>
+#import "MessagesVC.h"
+
@interface MediaConnectionsHolder : NSObject
@property QMetaObject::Connection newMediaAdded;
@@ -36,20 +38,26 @@
@implementation MediaConnectionsHolder
+
@end
-@interface ChatVC ()
+@interface ChatVC () <MessagesVCDelegate>
+{
+ 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:@""];
+#pragma mark - MessagesVC delegate
- /* 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)];
+-(void) newMessageAdded {
+
+ 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::Text>(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::Text>(Media::Media::Direction::OUT);
+ auto textRecording = text->recording();
+ textRecording->setAllRead();
}
-
- /* 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) appendNewMessage:(const QModelIndex&) msgIdx
+- (void) parseChatModel:(QAbstractItemModel *)model
+
{
- if (!msgIdx.isValid())
- return;
-
- NSString* message = msgIdx.data(Qt::DisplayRole).value<QString>().toNSString();
- NSString* author = msgIdx.data((int)Media::TextRecording::Role::AuthorDisplayname).value<QString>().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)];
-
+ [messagesViewVC setUpViewWithModel:model];
}
- (void) takeFocus
diff --git a/src/ConversationVC.mm b/src/ConversationVC.mm
index fb3d996..5739e0f 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 <QuartzCore/QuartzCore.h>
-@interface ConversationVC () <NSOutlineViewDelegate> {
+@interface ConversationVC () <NSOutlineViewDelegate, MessagesVCDelegate> {
__unsafe_unretained IBOutlet NSTextField* messageField;
QVector<ContactMethod*> 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<Media::Media::Direction>(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<QPixmap>(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<Media::Media::Direction>(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<Media::Media::Direction>(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 0000000..6b5430f
--- /dev/null
+++ b/src/MessagesVC.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@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 <Cocoa/Cocoa.h>
+
+@protocol MessagesVCDelegate
+
+-(void)newMessageAdded;
+
+@end
+
+@interface MessagesVC : NSViewController
+
+-(void)setUpViewWithModel: (QAbstractItemModel*) model;
+@property (retain, nonatomic) id <MessagesVCDelegate> delegate;
+
+@end
diff --git a/src/MessagesVC.mm b/src/MessagesVC.mm
new file mode 100644
index 0000000..a38ba67
--- /dev/null
+++ b/src/MessagesVC.mm
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@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 <QItemSelectionModel>
+#import <qstring.h>
+#import <QPixmap>
+#import <QtMacExtras/qmacfunctions.h>
+
+#import <media/media.h>
+#import <person.h>
+#import <media/text.h>
+#import <media/textrecording.h>
+#import <globalinstances.h>
+
+#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<Media::Media::Direction>(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<Media::Media::Direction>(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<QPixmap>(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<Media::Media::Direction>(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<Media::Media::Direction>(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 68fd839..110dfb6 100644
--- a/src/views/IMTableCellView.h
+++ b/src/views/IMTableCellView.h
@@ -18,11 +18,13 @@
*/
#import <Cocoa/Cocoa.h>
+#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 a9d9497..4b8bd07 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 0000000..7b87c85
--- /dev/null
+++ b/src/views/MessageBubbleView.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@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 <Cocoa/Cocoa.h>
+
+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 0000000..1634c8c
--- /dev/null
+++ b/src/views/MessageBubbleView.mm
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2015-2017 Savoir-faire Linux Inc.
+ * Author: Kateryna Kostiuk <kateryna.kostiuk@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 "MessageBubbleView.h"
+#import <CoreGraphics/CoreGraphics.h>
+#import <QuartzCore/QuartzCore.h>
+#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