Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2015-2017 Savoir-faire Linux Inc. |
| 3 | * Author: Kateryna Kostiuk <kateryna.kostiuk@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 <QItemSelectionModel> |
| 21 | #import <qstring.h> |
| 22 | #import <QPixmap> |
| 23 | #import <QtMacExtras/qmacfunctions.h> |
| 24 | |
| 25 | #import <media/media.h> |
| 26 | #import <person.h> |
| 27 | #import <media/text.h> |
| 28 | #import <media/textrecording.h> |
| 29 | #import <globalinstances.h> |
| 30 | |
| 31 | #import "MessagesVC.h" |
| 32 | #import "QNSTreeController.h" |
| 33 | #import "views/IMTableCellView.h" |
| 34 | #import "views/MessageBubbleView.h" |
| 35 | #import "INDSequentialTextSelectionManager.h" |
| 36 | |
| 37 | @interface MessagesVC () { |
| 38 | |
| 39 | QNSTreeController* treeController; |
| 40 | __unsafe_unretained IBOutlet NSOutlineView* conversationView; |
| 41 | |
| 42 | } |
| 43 | |
| 44 | @property (nonatomic, strong, readonly) INDSequentialTextSelectionManager* selectionManager; |
| 45 | |
| 46 | @end |
| 47 | |
| 48 | @implementation MessagesVC |
| 49 | QAbstractItemModel* currentModel; |
| 50 | |
| 51 | -(void)setUpViewWithModel: (QAbstractItemModel*) model { |
| 52 | |
| 53 | _selectionManager = [[INDSequentialTextSelectionManager alloc] init]; |
| 54 | |
| 55 | [self.selectionManager unregisterAllTextViews]; |
| 56 | |
| 57 | treeController = [[QNSTreeController alloc] initWithQModel:model]; |
| 58 | [treeController setAvoidsEmptySelection:NO]; |
| 59 | [treeController setChildrenKeyPath:@"children"]; |
| 60 | [conversationView bind:@"content" toObject:treeController withKeyPath:@"arrangedObjects" options:nil]; |
| 61 | [conversationView bind:@"sortDescriptors" toObject:treeController withKeyPath:@"sortDescriptors" options:nil]; |
| 62 | [conversationView bind:@"selectionIndexPaths" toObject:treeController withKeyPath:@"selectionIndexPaths" options:nil]; |
| 63 | [conversationView scrollToEndOfDocument:nil]; |
| 64 | currentModel = model; |
| 65 | } |
| 66 | |
| 67 | #pragma mark - NSOutlineViewDelegate methods |
| 68 | |
| 69 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldSelectItem:(id)item; |
| 70 | { |
| 71 | return YES; |
| 72 | } |
| 73 | |
| 74 | - (BOOL)outlineView:(NSOutlineView *)outlineView shouldEditTableColumn:(NSTableColumn *)tableColumn item:(id)item |
| 75 | { |
| 76 | return YES; |
| 77 | } |
| 78 | |
| 79 | - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item |
| 80 | { |
| 81 | QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; |
| 82 | if(!qIdx.isValid()) { |
| 83 | return [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self]; |
| 84 | } |
| 85 | auto dir = qvariant_cast<Media::Media::Direction>(qIdx.data((int)Media::TextRecording::Role::Direction)); |
| 86 | IMTableCellView* result; |
| 87 | |
| 88 | if (dir == Media::Media::Direction::IN) { |
| 89 | result = [outlineView makeViewWithIdentifier:@"LeftMessageView" owner:self]; |
| 90 | } else { |
| 91 | result = [outlineView makeViewWithIdentifier:@"RightMessageView" owner:self]; |
| 92 | } |
| 93 | |
| 94 | // check if the message first in incoming or outgoing messages sequence |
| 95 | Boolean isFirstInSequence = true; |
| 96 | int row = qIdx.row() - 1; |
| 97 | if(row >= 0) { |
| 98 | QModelIndex index = currentModel->index(row, 0); |
| 99 | if(index.isValid()) { |
| 100 | auto dirOld = qvariant_cast<Media::Media::Direction>(index.data((int)Media::TextRecording::Role::Direction)); |
| 101 | isFirstInSequence = !(dirOld == dir); |
| 102 | } |
| 103 | } |
| 104 | [result.photoView setHidden:!isFirstInSequence]; |
| 105 | result.msgBackground.needPointer = isFirstInSequence; |
| 106 | [result setup]; |
| 107 | |
| 108 | NSMutableAttributedString* msgAttString = |
| 109 | [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 110 | attributes:[self messageAttributes]]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 111 | |
| 112 | NSAttributedString* timestampAttrString = |
| 113 | [[NSAttributedString alloc] initWithString:qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 114 | attributes:[self timestampAttributes]]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 115 | |
| 116 | |
| 117 | CGFloat finalWidth = MAX(msgAttString.size.width, timestampAttrString.size.width); |
| 118 | |
| 119 | finalWidth = MIN(finalWidth + 30, outlineView.frame.size.width * 0.7); |
| 120 | |
| 121 | NSString* msgString = qIdx.data((int)Qt::DisplayRole).toString().toNSString(); |
| 122 | NSString* dateString = qIdx.data((int)Qt::DisplayRole).toString().toNSString(); |
| 123 | |
| 124 | [msgAttString appendAttributedString:timestampAttrString]; |
| 125 | [[result.msgView textStorage] appendAttributedString:msgAttString]; |
| 126 | [result.msgView checkTextInDocument:nil]; |
| 127 | [result updateWidthConstraint:finalWidth]; |
| 128 | [result.photoView setImage:QtMac::toNSImage(qvariant_cast<QPixmap>(qIdx.data(Qt::DecorationRole)))]; |
| 129 | return result; |
| 130 | } |
| 131 | |
| 132 | - (void)outlineView:(NSOutlineView *)outlineView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row |
| 133 | { |
| 134 | if (IMTableCellView* cellView = [outlineView viewAtColumn:0 row:row makeIfNecessary:NO]) { |
| 135 | [self.selectionManager registerTextView:cellView.msgView withUniqueIdentifier:@(row).stringValue]; |
| 136 | } |
| 137 | [self.delegate newMessageAdded]; |
| 138 | } |
| 139 | |
| 140 | - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item |
| 141 | { |
| 142 | QModelIndex qIdx = [treeController toQIdx:((NSTreeNode*)item)]; |
| 143 | double someWidth = outlineView.frame.size.width * 0.7; |
| 144 | |
| 145 | NSMutableAttributedString* msgAttString = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@\n",qIdx.data((int)Qt::DisplayRole).toString().toNSString()] |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 146 | attributes:[self messageAttributes]]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 147 | NSAttributedString *timestampAttrString = [[NSAttributedString alloc] initWithString: |
| 148 | qIdx.data((int)Media::TextRecording::Role::FormattedDate).toString().toNSString() |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 149 | attributes:[self timestampAttributes]]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 150 | |
| 151 | [msgAttString appendAttributedString:timestampAttrString]; |
| 152 | |
| 153 | NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT); |
| 154 | NSTextView *tv = [[NSTextView alloc] initWithFrame:frame]; |
| 155 | [tv setEnabledTextCheckingTypes:NSTextCheckingTypeLink]; |
| 156 | [tv setAutomaticLinkDetectionEnabled:YES]; |
| 157 | [[tv textStorage] setAttributedString:msgAttString]; |
| 158 | [tv sizeToFit]; |
| 159 | |
| 160 | double height = tv.frame.size.height + 10; |
| 161 | return MAX(height, 50.0f); |
| 162 | } |
| 163 | |
| 164 | #pragma mark - Text formatting |
| 165 | |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 166 | - (NSMutableDictionary*) timestampAttributes |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 167 | { |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 168 | NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 169 | attrs[NSForegroundColorAttributeName] = [NSColor grayColor]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 170 | NSFont* systemFont = [NSFont systemFontOfSize:12.0f]; |
| 171 | attrs[NSFontAttributeName] = systemFont; |
| 172 | attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; |
| 173 | |
| 174 | return attrs; |
| 175 | } |
| 176 | |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 177 | - (NSMutableDictionary*) messageAttributes |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 178 | { |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 179 | NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; |
Kateryna Kostiuk | 3308987 | 2017-07-14 16:43:59 -0400 | [diff] [blame] | 180 | attrs[NSForegroundColorAttributeName] = [NSColor blackColor]; |
Kateryna Kostiuk | 58276bc | 2017-06-07 08:50:48 -0400 | [diff] [blame] | 181 | NSFont* systemFont = [NSFont systemFontOfSize:14.0f]; |
| 182 | attrs[NSFontAttributeName] = systemFont; |
| 183 | attrs[NSParagraphStyleAttributeName] = [self paragraphStyle]; |
| 184 | |
| 185 | return attrs; |
| 186 | } |
| 187 | |
| 188 | - (NSParagraphStyle*) paragraphStyle |
| 189 | { |
| 190 | /* |
| 191 | The only way to instantiate an NSMutableParagraphStyle is to mutably copy an |
| 192 | NSParagraphStyle. And since we don't have an existing NSParagraphStyle available |
| 193 | to copy, we use the default one. |
| 194 | |
| 195 | The default values supplied by the default NSParagraphStyle are: |
| 196 | Alignment NSNaturalTextAlignment |
| 197 | Tab stops 12 left-aligned tabs, spaced by 28.0 points |
| 198 | Line break mode NSLineBreakByWordWrapping |
| 199 | All others 0.0 |
| 200 | */ |
| 201 | NSMutableParagraphStyle* aMutableParagraphStyle = |
| 202 | [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; |
| 203 | |
| 204 | // Now adjust our NSMutableParagraphStyle formatting to be whatever we want. |
| 205 | // The numeric values below are in points (72 points per inch) |
| 206 | [aMutableParagraphStyle setLineSpacing:1.5]; |
| 207 | [aMutableParagraphStyle setParagraphSpacing:5.0]; |
| 208 | [aMutableParagraphStyle setHeadIndent:5.0]; |
| 209 | [aMutableParagraphStyle setTailIndent:-5.0]; |
| 210 | [aMutableParagraphStyle setFirstLineHeadIndent:5.0]; |
| 211 | return aMutableParagraphStyle; |
| 212 | } |
| 213 | |
| 214 | @end |