blob: 24ae5d90a8092b58763a07734d77e3d43133b975 [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
47 __unsafe_unretained IBOutlet IconButton* backButton;
48 __unsafe_unretained IBOutlet NSTextField* messageField;
49 QVector<ContactMethod*> contactMethods;
50 NSMutableString* textSelection;
51
52 QNSTreeController* treeController;
53
54 __unsafe_unretained IBOutlet NSView* sendPanel;
55 __unsafe_unretained IBOutlet NSTextField* conversationTitle;
56 __unsafe_unretained IBOutlet NSTextField* emptyConversationPlaceHolder;
57 __unsafe_unretained IBOutlet IconButton* sendButton;
58 __unsafe_unretained IBOutlet NSOutlineView* conversationView;
59 __unsafe_unretained IBOutlet NSPopUpButton* contactMethodsPopupButton;
60}
61
Alexandre Lision83180df2016-01-18 11:32:20 -050062@property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager;
63
Alexandre Lision0f66bd32016-01-18 11:30:45 -050064@end
65
66@implementation ConversationVC
67
68- (void)viewDidLoad {
69 [super viewDidLoad];
70 // Do view setup here.
71 [self.view setWantsLayer:YES];
72 [self.view setLayer:[CALayer layer]];
73 [self.view.layer setBackgroundColor:[NSColor ringGreyHighlight].CGColor];
74 [self.view.layer setCornerRadius:5.0f];
75
76 [sendPanel setWantsLayer:YES];
77 [sendPanel setLayer:[CALayer layer]];
Alexandre Lision83180df2016-01-18 11:32:20 -050078 _selectionManager = [[INDSequentialTextSelectionManager alloc] init];
Alexandre Lision0f66bd32016-01-18 11:30:45 -050079
80 [self setupChat];
81
82}
83
84- (void) initFrame
85{
86 [self.view setFrame:self.view.superview.bounds];
87 [self.view setHidden:YES];
88 self.view.layer.position = self.view.frame.origin;
89}
90
91- (void) setupChat
92{
93 QObject::connect(RecentModel::instance().selectionModel(),
94 &QItemSelectionModel::currentChanged,
95 [=](const QModelIndex &current, const QModelIndex &previous) {
96
97 contactMethods = RecentModel::instance().getContactMethods(current);
98 if (contactMethods.isEmpty()) {
99 return ;
100 }
101
Alexandre Lision83180df2016-01-18 11:32:20 -0500102 [self.selectionManager unregisterAllTextViews];
103
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500104 [contactMethodsPopupButton removeAllItems];
105 for (auto cm : contactMethods) {
106 [contactMethodsPopupButton addItemWithTitle:cm->uri().toNSString()];
107 }
108
109 [contactMethodsPopupButton setEnabled:(contactMethods.length() > 1)];
110
111 [emptyConversationPlaceHolder setHidden:NO];
112 // Select first cm
113 [contactMethodsPopupButton selectItemAtIndex:0];
114 [self itemChanged:contactMethodsPopupButton];
115
116 NSString* localizedTitle = current.data((int)Ring::Role::Name).toString().toNSString();
117 [conversationTitle setStringValue:localizedTitle];
118
119 });
120}
121
122- (IBAction)sendMessage:(id)sender
123{
124 /* make sure there is text to send */
125 NSString* text = self.message;
126 if (text && text.length > 0) {
127 QMap<QString, QString> messages;
128 messages["text/plain"] = QString::fromNSString(text);
129 contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->sendOfflineTextMessage(messages);
130 self.message = @"";
131 }
132}
133
134- (IBAction)placeCall:(id)sender
135{
136 if(auto cm = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])) {
137 auto c = CallModel::instance().dialingCall();
138 c->setPeerContactMethod(cm);
139 c << Call::Action::ACCEPT;
140 CallModel::instance().selectCall(c);
141 }
142}
143
144# pragma mark private IN/OUT animations
145
146-(void) animateIn
147{
148 CGRect frame = CGRectOffset(self.view.superview.bounds, -self.view.superview.bounds.size.width, 0);
149 [self.view setHidden:NO];
150
151 [CATransaction begin];
152 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
153 [animation setFromValue:[NSValue valueWithPoint:frame.origin]];
154 [animation setToValue:[NSValue valueWithPoint:self.view.superview.bounds.origin]];
155 [animation setDuration:0.2f];
156 [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]];
157 [self.view.layer addAnimation:animation forKey:animation.keyPath];
158
159 [CATransaction commit];
160}
161
162-(IBAction) animateOut:(id)sender
163{
164 if(self.view.frame.origin.x < 0) {
165 return;
166 }
167
168 CGRect frame = CGRectOffset(self.view.frame, -self.view.frame.size.width, 0);
169 [CATransaction begin];
170 CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
171 [animation setFromValue:[NSValue valueWithPoint:self.view.frame.origin]];
172 [animation setToValue:[NSValue valueWithPoint:frame.origin]];
173 [animation setDuration:0.2f];
174 [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
175
176 [CATransaction setCompletionBlock:^{
177 [self.view setHidden:YES];
178 }];
179 [self.view.layer addAnimation:animation forKey:animation.keyPath];
180
181 [self.view.layer setPosition:frame.origin];
182 [CATransaction commit];
183}
184
185#pragma mark - NSOutlineViewDelegate methods
186
187- (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item;
188{
189 return YES;
190}
191
192- (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item
193{
194 return YES;
195}
196
197- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
198{
199 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
200 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
201 IMTableCellView* result;
202
203 if (dir == Media::Media::Direction::IN) {
204 result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self];
205 } else {
206 result = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self];
207 }
208
209 [result setup];
210
211 NSMutableAttributedString* msgAttString =
212 [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
213 attributes:[self messageAttributesFor:qIdx]];
214
215 NSAttributedString* timestampAttrString =
216 [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
217 attributes:[self timestampAttributesFor:qIdx]];
218
219
220 CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width);
221 finalWidth = MIN(finalWidth + 30, result.frame.size.width - result.photoView.frame.size.width - 30);
222
223 [msgAttString appendAttributedString:timestampAttrString];
224 [[result.msgView textStorage] appendAttributedString:msgAttString];
225 [result.msgView checkTextInDocument:nil];
226 [result.msgView setWantsLayer:YES];
227 result.msgView.layer.cornerRadius = 5.0f;
228
229 [result updateWidthConstraint:finalWidth];
230
231 Person* p = qvariant_cast<Person*>(qIdx.data((int)Person::Role::Object));
232 QVariant photo = GlobalInstances::pixmapManipulator().contactPhoto(p, QSize(50,50));
233 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(photo))];
234
235 return result;
236}
237
238- (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
239{
Alexandre Lision83180df2016-01-18 11:32:20 -0500240 if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) {
241 [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue];
242 }
243
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500244 if (auto txtRecording = contactMethods.at([contactMethodsPopupButton indexOfSelectedItem])->textRecording()) {
245 [emptyConversationPlaceHolder setHidden:txtRecording->instantMessagingModel()->rowCount() > 0];
246 txtRecording->setAllRead();
247 }
248}
249
250- (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item
251{
252 QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)];
253
254 double someWidth = outlineView.frame.size.width;
255
256 NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()]
257 attributes:[self messageAttributesFor:qIdx]];
258 NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString:
259 qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString()
260 attributes:[self timestampAttributesFor:qIdx]];
261
262 [msgAttString appendAttributedString:timestampAttrString];
263
264 NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT);
265 NSTextView *tv = [[NSTextView alloc] initWithFrame:frame];
266 [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink];
267 [tv setAutomaticLinkDetectionEnabled:YES];
268 [[tv textStorage] setAttributedString:msgAttString];
269 [tv sizeToFit];
270
271 double height = tv.frame.size.height + 20;
272
273 return MAX(height, 60.0f);
274}
275
276#pragma mark - Text formatting
277
278- (NSMutableDictionary*) timestampAttributesFor:(QModelIndex) qIdx
279{
280 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
281 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
282
283 if (dir == Media::Media::Direction::IN) {
284 attrs[NSForegroundColorAttributeName] = [NSColor grayColor];
285 } else {
286 attrs[NSForegroundColorAttributeName] = [NSColor whiteColor];
287 }
288
289 NSFont* systemFont = [NSFont systemFontOfSize:12.0f];
290 attrs[NSFontAttributeName] = systemFont;
291 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
292
293 return attrs;
294}
295
296- (NSMutableDictionary*) messageAttributesFor:(QModelIndex) qIdx
297{
298 auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction));
299 NSMutableDictionary* attrs = [NSMutableDictionary dictionary];
300
301 if (dir == Media::Media::Direction::IN) {
302 attrs[NSForegroundColorAttributeName] = [NSColor blackColor];
303 } else {
304 attrs[NSForegroundColorAttributeName] = [NSColor whiteColor];
305 }
306
307 NSFont* systemFont = [NSFont systemFontOfSize:14.0f];
308 attrs[NSFontAttributeName] = systemFont;
309 attrs[NSParagraphStyleAttributeName] = [self paragraphStyle];
310
311 return attrs;
312}
313
314- (NSParagraphStyle*) paragraphStyle
315{
316 /*
317 The only way to instantiate an NSMutableParagraphStyle is to mutably copy an
318 NSParagraphStyle. And since we don't have an existing NSParagraphStyle available
319 to copy, we use the default one.
320
321 The default values supplied by the default NSParagraphStyle are:
322 Alignment NSNaturalTextAlignment
323 Tab stops 12 left-aligned tabs, spaced by 28.0 points
324 Line break mode NSLineBreakByWordWrapping
325 All others 0.0
326 */
327 NSMutableParagraphStyle* aMutableParagraphStyle =
328 [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
329
330 // Now adjust our NSMutableParagraphStyle formatting to be whatever we want.
331 // The numeric values below are in points (72 points per inch)
332 [aMutableParagraphStyle setAlignment:NSLeftTextAlignment];
333 [aMutableParagraphStyle setLineSpacing:1.5];
334 [aMutableParagraphStyle setParagraphSpacing:5.0];
335 [aMutableParagraphStyle setHeadIndent:5.0];
336 [aMutableParagraphStyle setTailIndent:-5.0];
337 [aMutableParagraphStyle setFirstLineHeadIndent:5.0];
338 [aMutableParagraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
339 return aMutableParagraphStyle;
340}
341
342#pragma mark - NSTextFieldDelegate
343
344- (BOOL)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
345{
346 if (commandSelector == @selector(insertNewline:) && self.message.length > 0) {
347 [self sendMessage:nil];
348 return YES;
349 }
350 return NO;
351}
352
353#pragma mark - NSPopUpButton item selection
354
355- (IBAction)itemChanged:(id)sender {
356 NSInteger index = [(NSPopUpButton *)sender indexOfSelectedItem];
357
358 if (auto txtRecording = contactMethods.at(index)->textRecording()) {
359 treeController = [[QNSTreeController alloc] initWithQModel:txtRecording->instantMessagingModel()];
360 [treeController setAvoidsEmptySelection:NO];
361 [treeController setChildrenKeyPath:@"children"];
362 [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
363 [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
364 [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
365 }
366
367 [conversationView scrollToEndOfDocument:nil];
368}
369
370
371@end