blob: cce9ddd73381c0c7a3626118c5920ea804ea0856 [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
Alexandre Lision4baba4c2016-02-11 13:00:57 -050067- (void)loadView {
68 [super loadView];
Alexandre Lision0f66bd32016-01-18 11:30:45 -050069 // 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];
Alexandre Lision43e91bc2016-04-19 18:04:52 -0400234 [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))];
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500235 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"];
Alexandre Lision01cf5e32016-01-21 15:54:30 -0500362 [conversationView setDelegate:self];
Alexandre Lision0f66bd32016-01-18 11:30:45 -0500363 [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil];
364 [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil];
365 [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil];
366 }
367
368 [conversationView scrollToEndOfDocument:nil];
369}
370
371
372@end