blob: 7d1f7f4826c4322f7e7dc32ab6f3e21903c778a2 [file] [log] [blame]
Alexandre Lision0f66bd32016-01-18 11:30:45 -05001/*
2 * Copyright (C) 2016 Savoir-faire Linux Inc.
3 * Author: Alexandre Lision <alexandre.lision@savoirfairelinux.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20#import "ConversationVC.h"
21
22#import <QItemSelectionModel>
23#import <qstring.h>
24#import <QPixmap>
25#import <QtMacExtras/qmacfunctions.h>
26
27#import <media/media.h>
28#import <recentmodel.h>
29#import <person.h>
30#import <contactmethod.h>
31#import <media/text.h>
32#import <media/textrecording.h>
33#import <callmodel.h>
34#import <globalinstances.h>
35
36#import "views/IconButton.h"
37#import "views/IMTableCellView.h"
38#import "views/NSColor+RingTheme.h"
39#import "QNSTreeController.h"
Alexandre Lision83180df2016-01-18 11:32:20 -050040#import "INDSequentialTextSelectionManager.h"
Alexandre Lision0f66bd32016-01-18 11:30:45 -050041#import "delegates/ImageManipulationDelegate.h"
42
43#import <QuartzCore/QuartzCore.h>
44
45@interface ConversationVC () <NSOutlineViewDelegate> {
46
Alexandre Lision0f66bd32016-01-18 11:30:45 -050047 __unsafe_unretained IBOutlet NSTextField* messageField;
48 QVector<ContactMethod*> contactMethods;
49 NSMutableString* textSelection;
50
51 QNSTreeController* treeController;
52
53 __unsafe_unretained IBOutlet NSView* sendPanel;
54 __unsafe_unretained IBOutlet NSTextField* conversationTitle;
55 __unsafe_unretained IBOutlet NSTextField* emptyConversationPlaceHolder;
56 __unsafe_unretained IBOutlet IconButton* sendButton;
57 __unsafe_unretained IBOutlet NSOutlineView* conversationView;
58 __unsafe_unretained IBOutlet NSPopUpButton* contactMethodsPopupButton;
59}
60
Alexandre Lision83180df2016-01-18 11:32:20 -050061@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
62
Alexandre Lision0f66bd32016-01-18 11:30:45 -050063@end
64
65@implementation ConversationVC
66
67- (void)viewDidLoad {
68 [super viewDidLoad];
69 // Do view setup here.
70 [self.view setWantsLayer:YES];
71 [self.view setLayer:[CALayer layer]];
72 [self.view.layer setBackgroundColor:[NSColor ringGreyHighlight].CGColor];
73 [self.view.layer setCornerRadius:5.0f];
74
75 [sendPanel setWantsLayer:YES];
76 [sendPanel setLayer:[CALayer layer]];
Alexandre Lision83180df2016-01-18 11:32:20 -050077 _selectionManager = [[INDSequentialTextSelectionManager alloc] init];
Alexandre Lision0f66bd32016-01-18 11:30:45 -050078
79 [self setupChat];
80
81}
82
83- (void) initFrame
84{
85 [self.view setFrame:self.view.superview.bounds];
86 [self.view setHidden:YES];
87 self.view.layer.position = self.view.frame.origin;
88}
89
90- (void) setupChat
91{
92 QObject::connect(RecentModel::instance().selectionModel(),
93 &QItemSelectionModel::currentChanged,
94 [=](const QModelIndex &current, const QModelIndex &previous) {
95
96 contactMethods = RecentModel::instance().getContactMethods(current);
97 if (contactMethods.isEmpty()) {
98 return ;
99 }
100
Alexandre Lision83180df2016-01-18 11:32:20 -0500101 [self.selectionManager unregisterAllTextViews];
102
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500103 [contactMethodsPopupButton removeAllItems];
104 for (auto cm : contactMethods) {
105 [contactMethodsPopupButton addItemWithTitle:cm->uri().toNSString()];
106 }
107
108 [contactMethodsPopupButton setEnabled:(contactMethods.length() > 1)];
109
110 [emptyConversationPlaceHolder setHidden:NO];
111 // Select first cm
112 [contactMethodsPopupButton selectItemAtIndex:0];
113 [self itemChanged:contactMethodsPopupButton];
114
115 NSString* localizedTitle = current.data((int)Ring::Role::Name).toString().toNSString();
116 [conversationTitle setStringValue:localizedTitle];
117
118 });
119}
120
121- (IBAction)sendMessage:(id)sender
122{
123 /* make sure there is text to send */
124 NSString* text = self.message;
125 if (text && text.length > 0) {
126 QMap<QString, QString> messages;
127 messages["text/plain"] = QString::fromNSString(text);
128 contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->sendOfflineTextMessage(messages);
129 self.message = @"";
130 }
131}
132
133- (IBAction)placeCall:(id)sender
134{
135 if(auto cm = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])) {
136 auto c = CallModel::instance().dialingCall();
137 c->setPeerContactMethod(cm);
138 c << Call::Action::ACCEPT;
139 CallModel::instance().selectCall(c);
140 }
141}
142
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500143- (IBAction)backPressed:(id)sender {
144 [conversationView setDelegate:nil];
145 RecentModel::instance().selectionModel()->clearCurrentIndex();
146}
147
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500148# pragma mark private IN/OUT animations
149
150-(void) animateIn
151{
152 CGRect frame = CGRectOffset(self.view.superview.bounds, -self.view.superview.bounds.size.width, 0);
153 [self.view setHidden:NO];
154
155 [CATransaction begin];
156 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
157 [animation setFromValue:[NSValue valueWithPoint:frame.origin]];
158 [animation setToValue:[NSValue valueWithPoint:self.view.superview.bounds.origin]];
159 [animation setDuration:0.2f];
160 [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]];
161 [self.view.layer addAnimation:animation forKey:animation.keyPath];
162
163 [CATransaction commit];
164}
165
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500166-(void) animateOut
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500167{
168 if(self.view.frame.origin.x < 0) {
169 return;
170 }
171
172 CGRect frame = CGRectOffset(self.view.frame, -self.view.frame.size.width, 0);
173 [CATransaction begin];
174 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
175 [animation setFromValue:[NSValue valueWithPoint:self.view.frame.origin]];
176 [animation setToValue:[NSValue valueWithPoint:frame.origin]];
177 [animation setDuration:0.2f];
178 [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
179
180 [CATransaction setCompletionBlock:^{
181 [self.view setHidden:YES];
182 }];
183 [self.view.layer addAnimation:animation forKey:animation.keyPath];
184
185 [self.view.layer setPosition:frame.origin];
186 [CATransaction commit];
187}
188
189#pragma mark - NSOutlineViewDelegate methods
190
191- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
192{
193 return YES;
194}
195
196- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
197{
198 return YES;
199}
200
201- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
202{
203 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
204 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
205 IMTableCellView* result;
206
207 if (dir == Media::Media::Direction::IN) {
208 result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self];
209 } else {
210 result = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self];
211 }
212
213 [result setup];
214
215 NSMutableAttributedString* msgAttString =
216 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
217 attributes:[self messageAttributesFor:qIdx]];
218
219 NSAttributedString* timestampAttrString =
220 [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
221 attributes:[self timestampAttributesFor:qIdx]];
222
223
224 CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
225 finalWidth = MIN(finalWidth + 30, result.frame.size.width - result.photoView.frame.size.width - 30);
226
227 [msgAttString appendAttributedString:timestampAttrString];
228 [[result.msgView textStorage] appendAttributedString:msgAttString];
229 [result.msgView checkTextInDocument:nil];
230 [result.msgView setWantsLayer:YES];
231 result.msgView.layer.cornerRadius = 5.0f;
232
233 [result updateWidthConstraint:finalWidth];
234
235 Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
236 QVariant photo = GlobalInstances::pixmapManipulator().contactPhoto(p, QSize(50,50));
237 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))];
238
239 return result;
240}
241
242- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
243{
Alexandre Lision83180df2016-01-18 11:32:20 -0500244 if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) {
245 [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
246 }
247
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500248 if (auto txtRecording = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->textRecording()) {
249 [emptyConversationPlaceHolder setHidden:txtRecording->instantMessagingModel()->rowCount() > 0];
250 txtRecording->setAllRead();
251 }
252}
253
254- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
255{
256 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
257
258 double someWidth = outlineView.frame.size.width;
259
260 NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
261 attributes:[self messageAttributesFor:qIdx]];
262 NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString:
263 qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
264 attributes:[self timestampAttributesFor:qIdx]];
265
266 [msgAttString appendAttributedString:timestampAttrString];
267
268 NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
269 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
270 [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
271 [tv setAutomaticLinkDetectionEnabled:YES];
272 [[tv textStorage] setAttributedString:msgAttString];
273 [tv sizeToFit];
274
275 double height = tv.frame.size.height + 20;
276
277 return MAX(height, 60.0f);
278}
279
280#pragma mark - Text formatting
281
282- (NSMutableDictionary*) timestampAttributesFor:(QModelIndex) qIdx
283{
284 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
285 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
286
287 if (dir == Media::Media::Direction::IN) {
288 attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
289 } else {
290 attrs[NSForegroundColorAttributeName] = [NSColor whiteColor];
291 }
292
293 NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
294 attrs[NSFontAttributeName] = systemFont;
295 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
296
297 return attrs;
298}
299
300- (NSMutableDictionary*) messageAttributesFor:(QModelIndex) qIdx
301{
302 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
303 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
304
305 if (dir == Media::Media::Direction::IN) {
306 attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
307 } else {
308 attrs[NSForegroundColorAttributeName] = [NSColor whiteColor];
309 }
310
311 NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
312 attrs[NSFontAttributeName] = systemFont;
313 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
314
315 return attrs;
316}
317
318- (NSParagraphStyle*) paragraphStyle
319{
320 /*
321 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
322 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
323 to copy, we use the default one.
324
325 The default values supplied by the default NSParagraphStyle are:
326 Alignment NSNaturalTextAlignment
327 Tab stops 12 left-aligned tabs, spaced by 28.0 points
328 Line break mode NSLineBreakByWordWrapping
329 All others 0.0
330 */
331 NSMutableParagraphStyle* aMutableParagraphStyle =
332 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
333
334 // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
335 // The numeric values below are in points (72 points per inch)
336 [aMutableParagraphStyle setAlignment:NSLeftTextAlignment];
337 [aMutableParagraphStyle setLineSpacing:1.5];
338 [aMutableParagraphStyle setParagraphSpacing:5.0];
339 [aMutableParagraphStyle setHeadIndent:5.0];
340 [aMutableParagraphStyle setTailIndent:-5.0];
341 [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
342 [aMutableParagraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
343 return aMutableParagraphStyle;
344}
345
346#pragma mark - NSTextFieldDelegate
347
348- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
349{
350 if (commandSelector == @selector(insertNewline:) && self.message.length > 0) {
351 [self sendMessage:nil];
352 return YES;
353 }
354 return NO;
355}
356
357#pragma mark - NSPopUpButton item selection
358
359- (IBAction)itemChanged:(id)sender {
360 NSInteger index = [(NSPopUpButton *)sender indexOfSelectedItem];
361
362 if (auto txtRecording = contactMethods.at(index)->textRecording()) {
363 treeController = [[QNSTreeController alloc] initWithQModel:txtRecording->instantMessagingModel()];
364 [treeController setAvoidsEmptySelection:NO];
365 [treeController setChildrenKeyPath:@"children"];
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500366 [conversationView setDelegate:self];
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500367 [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
368 [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
369 [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
370 }
371
372 [conversationView scrollToEndOfDocument:nil];
373}
374
375
376@end