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