|
David@0
|
1 |
/* |
|
David@0
|
2 |
* Adium is the legal property of its developers, whose names are listed in the copyright file included |
|
David@0
|
3 |
* with this source distribution. |
|
David@0
|
4 |
* |
|
David@0
|
5 |
* This program is free software; you can redistribute it and/or modify it under the terms of the GNU |
|
David@0
|
6 |
* General Public License as published by the Free Software Foundation; either version 2 of the License, |
|
David@0
|
7 |
* or (at your option) any later version. |
|
David@0
|
8 |
* |
|
David@0
|
9 |
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even |
|
David@0
|
10 |
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General |
|
David@0
|
11 |
* Public License for more details. |
|
David@0
|
12 |
* |
|
David@0
|
13 |
* You should have received a copy of the GNU General Public License along with this program; if not, |
|
David@0
|
14 |
* write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
|
David@0
|
15 |
*/ |
|
David@0
|
16 |
|
|
David@0
|
17 |
#import <Adium/AIChat.h> |
|
David@0
|
18 |
#import <Adium/AIAccount.h> |
|
David@559
|
19 |
#import <Adium/AIListObject.h> |
|
David@563
|
20 |
#import <Adium/AIListContact.h> |
|
David@0
|
21 |
#import <Adium/AIMessageEntryTextView.h> |
|
David@0
|
22 |
#import <Adium/ESFileWrapperExtension.h> |
|
David@0
|
23 |
#import <Adium/AITextAttachmentExtension.h> |
|
David@0
|
24 |
|
|
David@0
|
25 |
#import <Adium/AIMenuControllerProtocol.h> |
|
David@0
|
26 |
#import <Adium/AIContentControllerProtocol.h> |
|
David@0
|
27 |
#import <Adium/AIInterfaceControllerProtocol.h> |
|
David@0
|
28 |
#import <Adium/AIContentContext.h> |
|
David@0
|
29 |
|
|
David@0
|
30 |
#import <AIUtilities/AIApplicationAdditions.h> |
|
David@0
|
31 |
#import <AIUtilities/AIAttributedStringAdditions.h> |
|
David@0
|
32 |
#import <AIUtilities/AIColorAdditions.h> |
|
David@0
|
33 |
#import <AIUtilities/AITextAttributes.h> |
|
David@0
|
34 |
#import <AIUtilities/AIImageAdditions.h> |
|
David@0
|
35 |
#import <AIUtilities/AIFileManagerAdditions.h> |
|
David@0
|
36 |
#import <AIUtilities/AIPasteboardAdditions.h> |
|
zacw@1123
|
37 |
#import <AIUtilities/AIBezierPathAdditions.h> |
|
David@12
|
38 |
#import <Adium/AIContactControllerProtocol.h> |
|
David@0
|
39 |
|
|
David@0
|
40 |
|
|
David@0
|
41 |
#import <FriBidi/NSString-FBAdditions.h> |
|
David@0
|
42 |
|
|
David@0
|
43 |
#define MAX_HISTORY 25 //Number of messages to remember in history |
|
David@0
|
44 |
#define ENTRY_TEXTVIEW_PADDING 6 //Padding for auto-sizing |
|
David@0
|
45 |
|
|
David@0
|
46 |
#define KEY_DISABLE_TYPING_NOTIFICATIONS @"Disable Typing Notifications" |
|
David@0
|
47 |
|
|
David@0
|
48 |
#define KEY_SPELL_CHECKING @"Spell Checking Enabled" |
|
David@0
|
49 |
#define KEY_GRAMMAR_CHECKING @"Grammar Checking Enabled" |
|
David@0
|
50 |
#define PREF_GROUP_DUAL_WINDOW_INTERFACE @"Dual Window Interface" |
|
David@0
|
51 |
|
|
zacw@1123
|
52 |
#define INDICATOR_RIGHT_PADDING 2 // Padding between right side of the message view and the rightmost indicator |
|
David@0
|
53 |
|
|
David@0
|
54 |
#define PREF_GROUP_CHARACTER_COUNTER @"Character Counter" |
|
David@0
|
55 |
#define KEY_CHARACTER_COUNTER_ENABLED @"Character Counter Enabled" |
|
David@0
|
56 |
#define KEY_MAX_NUMBER_OF_CHARACTERS @"Maximum Number Of Characters" |
|
David@0
|
57 |
|
|
David@0
|
58 |
#define FILES_AND_IMAGES_TYPES [NSArray arrayWithObjects: \ |
|
David@0
|
59 |
NSFilenamesPboardType, AIiTunesTrackPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil] |
|
David@0
|
60 |
|
|
David@0
|
61 |
#define PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY [NSArray arrayWithObjects: \ |
|
David@0
|
62 |
NSRTFPboardType, NSStringPboardType, nil] |
|
David@0
|
63 |
|
|
David@0
|
64 |
/** |
|
David@0
|
65 |
* @class AISimpleTextView |
|
David@0
|
66 |
* @brief Just draws an attributed string. That's it. |
|
David@0
|
67 |
* |
|
David@0
|
68 |
* No really, it's dead simple. It just draws an attributed string in its bounds (which you set). That's it. |
|
David@0
|
69 |
* It's totally not even useful. |
|
David@0
|
70 |
*/ |
|
David@0
|
71 |
|
|
David@0
|
72 |
@implementation AISimpleTextView |
|
David@0
|
73 |
|
|
zacw@1123
|
74 |
@synthesize string; |
|
David@0
|
75 |
- (void)dealloc |
|
David@0
|
76 |
{ |
|
David@0
|
77 |
[string release]; |
|
David@0
|
78 |
[super dealloc]; |
|
David@0
|
79 |
} |
|
David@0
|
80 |
|
|
David@0
|
81 |
- (void)drawRect:(NSRect)rect |
|
David@0
|
82 |
{ |
|
zacw@1123
|
83 |
[string drawInRect:self.bounds]; |
|
David@0
|
84 |
} |
|
David@0
|
85 |
@end |
|
David@0
|
86 |
|
|
David@84
|
87 |
@interface AIMessageEntryTextView () |
|
David@0
|
88 |
- (void)_setPushIndicatorVisible:(BOOL)visible; |
|
David@0
|
89 |
- (void)positionPushIndicator; |
|
David@0
|
90 |
- (void)_resetCacheAndPostSizeChanged; |
|
David@0
|
91 |
|
|
David@0
|
92 |
- (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data; |
|
David@0
|
93 |
- (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment; |
|
David@0
|
94 |
- (void)addAttachmentOfPath:(NSString *)inPath; |
|
David@0
|
95 |
- (void)addAttachmentOfImage:(NSImage *)inImage; |
|
David@0
|
96 |
- (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard; |
|
David@0
|
97 |
|
|
David@0
|
98 |
- (void)setCharacterCounterVisible:(BOOL)visible; |
|
David@0
|
99 |
- (void)setCharacterCounterMaximum:(int)inMaxCharacters; |
|
zacw@847
|
100 |
- (void)setCharacterCounterPrefix:(NSString *)prefix; |
|
David@0
|
101 |
- (void)updateCharacterCounter; |
|
David@0
|
102 |
- (void)positionCharacterCounter; |
|
David@0
|
103 |
|
|
David@0
|
104 |
- (void)positionIndicators:(NSNotification *)notification; |
|
David@0
|
105 |
@end |
|
David@0
|
106 |
|
|
David@0
|
107 |
@interface NSMutableAttributedString (AIMessageEntryTextViewAdditions) |
|
David@0
|
108 |
- (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)inAttributes; |
|
David@0
|
109 |
@end |
|
David@0
|
110 |
|
|
David@0
|
111 |
@implementation AIMessageEntryTextView |
|
David@0
|
112 |
|
|
David@0
|
113 |
- (void)_initMessageEntryTextView |
|
David@0
|
114 |
{ |
|
David@0
|
115 |
associatedView = nil; |
|
David@0
|
116 |
chat = nil; |
|
David@0
|
117 |
pushIndicator = nil; |
|
David@0
|
118 |
pushPopEnabled = YES; |
|
David@0
|
119 |
historyEnabled = YES; |
|
David@0
|
120 |
clearOnEscape = NO; |
|
David@0
|
121 |
homeToStartOfLine = YES; |
|
David@0
|
122 |
resizing = NO; |
|
David@0
|
123 |
enableTypingNotifications = NO; |
|
David@0
|
124 |
historyArray = [[NSMutableArray alloc] initWithObjects:@"",nil]; |
|
David@0
|
125 |
pushArray = [[NSMutableArray alloc] init]; |
|
David@0
|
126 |
currentHistoryLocation = 0; |
|
David@0
|
127 |
[self setDrawsBackground:YES]; |
|
David@0
|
128 |
_desiredSizeCached = NSMakeSize(0,0); |
|
David@0
|
129 |
characterCounter = nil; |
|
zacw@847
|
130 |
characterCounterPrefix = nil; |
|
David@0
|
131 |
maxCharacters = 0; |
|
zacw@1123
|
132 |
savedTextColor = nil; |
|
David@0
|
133 |
|
|
David@0
|
134 |
if ([self respondsToSelector:@selector(setAllowsUndo:)]) { |
|
David@0
|
135 |
[self setAllowsUndo:YES]; |
|
David@0
|
136 |
} |
|
David@0
|
137 |
if ([self respondsToSelector:@selector(setAllowsDocumentBackgroundColorChange:)]) { |
|
David@0
|
138 |
[self setAllowsDocumentBackgroundColorChange:YES]; |
|
David@0
|
139 |
} |
|
David@0
|
140 |
|
|
David@0
|
141 |
[self setImportsGraphics:YES]; |
|
David@0
|
142 |
|
|
David@0
|
143 |
[[NSNotificationCenter defaultCenter] addObserver:self |
|
David@0
|
144 |
selector:@selector(textDidChange:) |
|
David@0
|
145 |
name:NSTextDidChangeNotification |
|
David@0
|
146 |
object:self]; |
|
David@0
|
147 |
[[NSNotificationCenter defaultCenter] addObserver:self |
|
David@0
|
148 |
selector:@selector(frameDidChange:) |
|
David@0
|
149 |
name:NSViewFrameDidChangeNotification |
|
David@0
|
150 |
object:self]; |
|
David@1109
|
151 |
[[NSNotificationCenter defaultCenter] addObserver:self |
|
David@0
|
152 |
selector:@selector(toggleMessageSending:) |
|
David@0
|
153 |
name:@"AIChatDidChangeCanSendMessagesNotification" |
|
David@0
|
154 |
object:chat]; |
|
David@1109
|
155 |
[[NSNotificationCenter defaultCenter] addObserver:self |
|
David@0
|
156 |
selector:@selector(contentObjectAdded:) |
|
David@0
|
157 |
name:Content_ContentObjectAdded |
|
David@0
|
158 |
object:nil]; |
|
David@0
|
159 |
|
|
David@95
|
160 |
[adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_DUAL_WINDOW_INTERFACE]; |
|
David@0
|
161 |
|
|
David@13
|
162 |
[[AIContactObserverManager sharedManager] registerListObjectObserver:self]; |
|
David@0
|
163 |
} |
|
David@0
|
164 |
|
|
David@0
|
165 |
//Init the text view |
|
David@0
|
166 |
- (id)initWithFrame:(NSRect)frameRect textContainer:(NSTextContainer *)aTextContainer |
|
David@0
|
167 |
{ |
|
David@0
|
168 |
if ((self = [super initWithFrame:frameRect textContainer:aTextContainer])) { |
|
David@0
|
169 |
[self _initMessageEntryTextView]; |
|
David@0
|
170 |
} |
|
David@0
|
171 |
|
|
David@0
|
172 |
return self; |
|
David@0
|
173 |
} |
|
David@0
|
174 |
|
|
David@0
|
175 |
- (id)initWithCoder:(NSCoder *)coder |
|
David@0
|
176 |
{ |
|
David@0
|
177 |
if ((self = [super initWithCoder:coder])) { |
|
David@0
|
178 |
[self _initMessageEntryTextView]; |
|
David@0
|
179 |
} |
|
David@0
|
180 |
|
|
David@0
|
181 |
return self; |
|
David@0
|
182 |
} |
|
David@0
|
183 |
|
|
David@0
|
184 |
- (void)dealloc |
|
David@0
|
185 |
{ |
|
zacw@844
|
186 |
if(chat.isGroupChat) { |
|
zacw@844
|
187 |
[chat removeObserver:self forKeyPath:@"Character Counter Max"]; |
|
zacw@847
|
188 |
[chat removeObserver:self forKeyPath:@"Character Counter Prefix"]; |
|
zacw@844
|
189 |
} |
|
zacw@844
|
190 |
|
|
David@1109
|
191 |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
|
David@95
|
192 |
[adium.preferenceController unregisterPreferenceObserver:self]; |
|
David@13
|
193 |
[[AIContactObserverManager sharedManager] unregisterListObjectObserver:self]; |
|
David@0
|
194 |
|
|
zacw@1123
|
195 |
[savedTextColor release]; |
|
zacw@1639
|
196 |
[characterCounter release]; |
|
zacw@847
|
197 |
[characterCounterPrefix release]; |
|
David@0
|
198 |
[chat release]; |
|
David@0
|
199 |
[associatedView release]; |
|
David@0
|
200 |
[historyArray release]; historyArray = nil; |
|
David@0
|
201 |
[pushArray release]; pushArray = nil; |
|
David@0
|
202 |
|
|
David@0
|
203 |
[super dealloc]; |
|
David@0
|
204 |
} |
|
David@0
|
205 |
|
|
David@759
|
206 |
- (void) setDelegate:(id<AIMessageEntryTextViewDelegate>)del |
|
David@759
|
207 |
{ |
|
David@759
|
208 |
super.delegate = del; |
|
David@759
|
209 |
} |
|
David@759
|
210 |
|
|
David@759
|
211 |
- (id<AIMessageEntryTextViewDelegate>)delegate |
|
David@759
|
212 |
{ |
|
David@759
|
213 |
return super.delegate; |
|
David@759
|
214 |
} |
|
David@759
|
215 |
|
|
David@0
|
216 |
- (void)keyDown:(NSEvent *)inEvent |
|
David@0
|
217 |
{ |
|
David@0
|
218 |
NSString *charactersIgnoringModifiers = [inEvent charactersIgnoringModifiers]; |
|
David@0
|
219 |
|
|
David@0
|
220 |
if ([charactersIgnoringModifiers length]) { |
|
David@0
|
221 |
unichar inChar = [charactersIgnoringModifiers characterAtIndex:0]; |
|
David@0
|
222 |
unsigned int flags = [inEvent modifierFlags]; |
|
David@0
|
223 |
|
|
David@0
|
224 |
//We have to test ctrl before option, because otherwise we'd miss ctrl-option-* events |
|
David@0
|
225 |
if (pushPopEnabled && |
|
David@0
|
226 |
(flags & NSControlKeyMask) && !(flags & NSShiftKeyMask)) { |
|
David@0
|
227 |
if (inChar == NSUpArrowFunctionKey) { |
|
David@0
|
228 |
[self popContent]; |
|
David@0
|
229 |
} else if (inChar == NSDownArrowFunctionKey) { |
|
David@0
|
230 |
[self pushContent]; |
|
David@0
|
231 |
} else if (inChar == 's') { |
|
David@0
|
232 |
[self swapContent]; |
|
David@0
|
233 |
} else { |
|
David@0
|
234 |
[super keyDown:inEvent]; |
|
David@0
|
235 |
} |
|
David@0
|
236 |
|
|
David@0
|
237 |
} else if (historyEnabled && |
|
David@0
|
238 |
(flags & NSAlternateKeyMask) && !(flags & NSShiftKeyMask)) { |
|
David@0
|
239 |
if (inChar == NSUpArrowFunctionKey) { |
|
David@0
|
240 |
[self historyUp]; |
|
David@0
|
241 |
} else if (inChar == NSDownArrowFunctionKey) { |
|
David@0
|
242 |
[self historyDown]; |
|
David@0
|
243 |
} else { |
|
David@0
|
244 |
[super keyDown:inEvent]; |
|
David@0
|
245 |
} |
|
David@0
|
246 |
|
|
David@0
|
247 |
} else if (associatedView && |
|
David@0
|
248 |
(flags & NSCommandKeyMask) && !(flags & NSShiftKeyMask)) { |
|
David@0
|
249 |
if ((inChar == NSUpArrowFunctionKey || inChar == NSDownArrowFunctionKey) || |
|
David@0
|
250 |
(inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) || |
|
David@0
|
251 |
(inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) { |
|
David@0
|
252 |
//Pass the associatedView a keyDown event equivalent equal to inEvent except without the modifier flags |
|
David@0
|
253 |
[associatedView keyDown:[NSEvent keyEventWithType:[inEvent type] |
|
David@0
|
254 |
location:[inEvent locationInWindow] |
|
David@0
|
255 |
modifierFlags:0 |
|
David@0
|
256 |
timestamp:[inEvent timestamp] |
|
David@0
|
257 |
windowNumber:[inEvent windowNumber] |
|
David@0
|
258 |
context:[inEvent context] |
|
David@0
|
259 |
characters:[inEvent characters] |
|
David@0
|
260 |
charactersIgnoringModifiers:charactersIgnoringModifiers |
|
David@0
|
261 |
isARepeat:[inEvent isARepeat] |
|
David@0
|
262 |
keyCode:[inEvent keyCode]]]; |
|
David@0
|
263 |
} else { |
|
David@0
|
264 |
[super keyDown:inEvent]; |
|
David@0
|
265 |
} |
|
David@0
|
266 |
|
|
David@0
|
267 |
} else if (associatedView && |
|
David@0
|
268 |
(inChar == NSPageUpFunctionKey || inChar == NSPageDownFunctionKey)) { |
|
David@0
|
269 |
[associatedView keyDown:inEvent]; |
|
David@0
|
270 |
|
|
David@0
|
271 |
} else if (inChar == NSHomeFunctionKey || inChar == NSEndFunctionKey) { |
|
David@0
|
272 |
if (homeToStartOfLine) { |
|
David@0
|
273 |
NSRange newRange; |
|
David@0
|
274 |
|
|
David@0
|
275 |
if (flags & NSShiftKeyMask) { |
|
David@0
|
276 |
//With shift, select to the beginning/end of the line |
|
David@0
|
277 |
NSRange selectedRange = [self selectedRange]; |
|
David@0
|
278 |
if (inChar == NSHomeFunctionKey) { |
|
David@0
|
279 |
//Home: from 0 to the current location |
|
David@0
|
280 |
newRange.location = 0; |
|
David@0
|
281 |
newRange.length = selectedRange.location; |
|
David@0
|
282 |
} else { |
|
David@0
|
283 |
//End: from current location to the end |
|
David@0
|
284 |
newRange.location = selectedRange.location; |
|
David@0
|
285 |
newRange.length = [[self string] length] - newRange.location; |
|
David@0
|
286 |
} |
|
David@0
|
287 |
|
|
David@0
|
288 |
} else { |
|
David@0
|
289 |
newRange.location = ((inChar == NSHomeFunctionKey) ? 0 : [[self string] length]); |
|
David@0
|
290 |
newRange.length = 0; |
|
David@0
|
291 |
} |
|
David@0
|
292 |
|
|
David@0
|
293 |
[self setSelectedRange:newRange]; |
|
David@0
|
294 |
|
|
David@0
|
295 |
} else { |
|
David@0
|
296 |
//If !homeToStartOfLine, pass the keypress to our associated view. |
|
David@0
|
297 |
if (associatedView) { |
|
David@0
|
298 |
[associatedView keyDown:inEvent]; |
|
David@0
|
299 |
} else { |
|
David@0
|
300 |
[super keyDown:inEvent]; |
|
David@0
|
301 |
} |
|
David@0
|
302 |
} |
|
David@0
|
303 |
|
|
David@0
|
304 |
} else if (inChar == NSTabCharacter) { |
|
Evan@160
|
305 |
if ([self.delegate respondsToSelector:@selector(textViewShouldTabComplete:)] && |
|
Evan@160
|
306 |
[self.delegate textViewShouldTabComplete:self]) { |
|
David@0
|
307 |
[self complete:nil]; |
|
David@0
|
308 |
} else { |
|
David@0
|
309 |
[super keyDown:inEvent]; |
|
David@0
|
310 |
} |
|
David@0
|
311 |
|
|
David@0
|
312 |
} else { |
|
David@0
|
313 |
[super keyDown:inEvent]; |
|
David@0
|
314 |
} |
|
David@0
|
315 |
} else { |
|
David@0
|
316 |
[super keyDown:inEvent]; |
|
David@0
|
317 |
} |
|
David@0
|
318 |
} |
|
David@0
|
319 |
|
|
David@0
|
320 |
//Text changed |
|
David@0
|
321 |
- (void)textDidChange:(NSNotification *)notification |
|
David@0
|
322 |
{ |
|
David@0
|
323 |
//Update typing status |
|
David@0
|
324 |
if (enableTypingNotifications) { |
|
David@95
|
325 |
[adium.contentController userIsTypingContentForChat:chat hasEnteredText:[[self textStorage] length] > 0]; |
|
David@0
|
326 |
} |
|
David@0
|
327 |
|
|
David@0
|
328 |
//Hide any existing contact list tooltip when we begin typing |
|
David@100
|
329 |
[adium.interfaceController showTooltipForListObject:nil atScreenPoint:NSZeroPoint onWindow:nil]; |
|
David@0
|
330 |
|
|
David@0
|
331 |
//Reset cache and resize |
|
David@0
|
332 |
[self _resetCacheAndPostSizeChanged]; |
|
David@0
|
333 |
|
|
David@0
|
334 |
//Update the character counter |
|
David@0
|
335 |
if (characterCounter) { |
|
David@0
|
336 |
[self updateCharacterCounter]; |
|
David@0
|
337 |
} |
|
David@0
|
338 |
} |
|
David@0
|
339 |
|
|
David@0
|
340 |
/*! |
|
David@0
|
341 |
* @brief Clear any link attribute in the current typing attributes |
|
David@0
|
342 |
* |
|
David@0
|
343 |
* Any link attribute is removed. All other typing attributes are unchanged. |
|
David@0
|
344 |
*/ |
|
David@0
|
345 |
- (void)clearLinkAttribute |
|
David@0
|
346 |
{ |
|
David@0
|
347 |
NSDictionary *typingAttributes = [self typingAttributes]; |
|
David@0
|
348 |
|
|
David@0
|
349 |
if ([typingAttributes objectForKey:NSLinkAttributeName]) { |
|
David@0
|
350 |
NSMutableDictionary *newTypingAttributes = [typingAttributes mutableCopy]; |
|
David@0
|
351 |
|
|
David@0
|
352 |
[newTypingAttributes removeObjectForKey:NSLinkAttributeName]; |
|
David@0
|
353 |
[self setTypingAttributes:newTypingAttributes]; |
|
David@0
|
354 |
|
|
David@0
|
355 |
[newTypingAttributes release]; |
|
David@0
|
356 |
} |
|
David@0
|
357 |
} |
|
David@0
|
358 |
|
|
David@0
|
359 |
/*! |
|
David@0
|
360 |
* @brief The user pressed escape: clear our text view in response |
|
David@0
|
361 |
*/ |
|
David@0
|
362 |
- (void)cancelOperation:(id)sender |
|
David@0
|
363 |
{ |
|
David@0
|
364 |
if (clearOnEscape) { |
|
David@0
|
365 |
NSUndoManager *undoManager = [self undoManager]; |
|
David@0
|
366 |
[undoManager registerUndoWithTarget:self |
|
David@0
|
367 |
selector:@selector(setAttributedString:) |
|
David@0
|
368 |
object:[[[self textStorage] copy] autorelease]]; |
|
David@0
|
369 |
[undoManager setActionName:AILocalizedString(@"Clear", nil)]; |
|
David@0
|
370 |
|
|
David@0
|
371 |
[self setString:@""]; |
|
David@0
|
372 |
[self clearLinkAttribute]; |
|
David@0
|
373 |
} |
|
David@0
|
374 |
|
|
Evan@160
|
375 |
if ([self.delegate respondsToSelector:@selector(textViewDidCancel:)]) { |
|
Evan@160
|
376 |
[self.delegate textViewDidCancel:self]; |
|
David@0
|
377 |
} |
|
David@0
|
378 |
} |
|
David@0
|
379 |
|
|
David@0
|
380 |
|
|
David@0
|
381 |
//Configure ------------------------------------------------------------------------------------------------------------ |
|
David@0
|
382 |
#pragma mark Configure |
|
David@759
|
383 |
@synthesize clearOnEscape, homeToStartOfLine, associatedView; |
|
David@0
|
384 |
|
|
David@0
|
385 |
- (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key |
|
David@0
|
386 |
object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime |
|
David@0
|
387 |
{ |
|
David@426
|
388 |
if ((!object || (object == chat.account)) && |
|
David@0
|
389 |
[group isEqualToString:GROUP_ACCOUNT_STATUS] && |
|
David@0
|
390 |
(!key || [key isEqualToString:KEY_DISABLE_TYPING_NOTIFICATIONS])) { |
|
David@426
|
391 |
enableTypingNotifications = ![[chat.account preferenceForKey:KEY_DISABLE_TYPING_NOTIFICATIONS |
|
David@0
|
392 |
group:GROUP_ACCOUNT_STATUS] boolValue]; |
|
David@0
|
393 |
} |
|
David@0
|
394 |
|
|
David@0
|
395 |
if (!object && |
|
David@0
|
396 |
[group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE] && |
|
David@0
|
397 |
(!key || [key isEqualToString:KEY_SPELL_CHECKING])) { |
|
David@0
|
398 |
[self setContinuousSpellCheckingEnabled:[[prefDict objectForKey:KEY_SPELL_CHECKING] boolValue]]; |
|
David@0
|
399 |
} |
|
David@0
|
400 |
|
|
David@72
|
401 |
if (!object && |
|
David@72
|
402 |
[group isEqualToString:PREF_GROUP_DUAL_WINDOW_INTERFACE] && |
|
David@72
|
403 |
(!key || [key isEqualToString:KEY_GRAMMAR_CHECKING])) { |
|
David@72
|
404 |
[self setGrammarCheckingEnabled:[[prefDict objectForKey:KEY_GRAMMAR_CHECKING] boolValue]]; |
|
David@0
|
405 |
} |
|
David@0
|
406 |
} |
|
David@0
|
407 |
|
|
David@0
|
408 |
//Adium Text Entry ----------------------------------------------------------------------------------------------------- |
|
David@0
|
409 |
#pragma mark Adium Text Entry |
|
David@0
|
410 |
|
|
David@0
|
411 |
/*! |
|
David@0
|
412 |
* @brief Toggle whether message sending is enabled based on a notification. The notification object is the AIChat of the appropriate message entry view |
|
David@0
|
413 |
*/ |
|
David@0
|
414 |
- (void)toggleMessageSending:(NSNotification *)not |
|
David@0
|
415 |
{ |
|
David@0
|
416 |
//XXX - We really should query the AIChat about this, but AIChat's "can't send" is really designed for handling offline, not banned. Bringing up the offline messaging dialog when banned would make no sense. |
|
David@0
|
417 |
[self setSendingEnabled:[[[not userInfo] objectForKey:@"TypingEnabled"] boolValue]]; |
|
David@0
|
418 |
} |
|
David@0
|
419 |
|
|
David@0
|
420 |
/*! |
|
David@0
|
421 |
* @brief Are we available for sending? |
|
David@0
|
422 |
*/ |
|
David@0
|
423 |
- (BOOL)availableForSending |
|
David@0
|
424 |
{ |
|
David@500
|
425 |
return self.sendingEnabled; |
|
David@0
|
426 |
} |
|
David@0
|
427 |
|
|
David@0
|
428 |
//Set our string, preserving the selected range |
|
David@0
|
429 |
- (void)setAttributedString:(NSAttributedString *)inAttributedString |
|
David@0
|
430 |
{ |
|
David@0
|
431 |
int length = [inAttributedString length]; |
|
David@0
|
432 |
NSRange oldRange = [self selectedRange]; |
|
David@0
|
433 |
|
|
David@0
|
434 |
//Change our string |
|
David@0
|
435 |
[[self textStorage] setAttributedString:inAttributedString]; |
|
David@0
|
436 |
|
|
David@0
|
437 |
//Restore the old selected range |
|
David@0
|
438 |
if (oldRange.location < length) { |
|
David@0
|
439 |
if (oldRange.location + oldRange.length <= length) { |
|
David@0
|
440 |
[self setSelectedRange:oldRange]; |
|
David@0
|
441 |
} else { |
|
David@0
|
442 |
[self setSelectedRange:NSMakeRange(oldRange.location, length - oldRange.location)]; |
|
David@0
|
443 |
} |
|
David@0
|
444 |
} |
|
David@0
|
445 |
|
|
David@0
|
446 |
//Notify everyone that our text changed |
|
David@0
|
447 |
[[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self]; |
|
David@0
|
448 |
} |
|
David@0
|
449 |
|
|
David@0
|
450 |
//Set our string (plain text) |
|
David@0
|
451 |
- (void)setString:(NSString *)string |
|
David@0
|
452 |
{ |
|
David@0
|
453 |
[super setString:string]; |
|
David@0
|
454 |
|
|
David@0
|
455 |
//Notify everyone that our text changed |
|
David@0
|
456 |
[[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification object:self]; |
|
David@0
|
457 |
} |
|
David@0
|
458 |
|
|
David@0
|
459 |
//Set our typing format |
|
David@0
|
460 |
- (void)setTypingAttributes:(NSDictionary *)attrs |
|
David@0
|
461 |
{ |
|
David@0
|
462 |
[super setTypingAttributes:attrs]; |
|
David@0
|
463 |
|
|
David@0
|
464 |
[self setInsertionPointColor:[[attrs objectForKey:NSBackgroundColorAttributeName] contrastingColor]]; |
|
David@0
|
465 |
} |
|
David@0
|
466 |
|
|
David@0
|
467 |
#pragma mark Pasting |
|
David@0
|
468 |
|
|
David@0
|
469 |
- (BOOL)handlePasteAsRichText |
|
David@0
|
470 |
{ |
|
David@0
|
471 |
NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard]; |
|
David@0
|
472 |
BOOL handledPaste = NO; |
|
David@0
|
473 |
|
|
David@0
|
474 |
//Types is ordered by the preference for handling of the data; enumerating it lets us allow the sending application's hints to be followed. |
|
Evan@166
|
475 |
for (NSString *type in generalPasteboard.types) { |
|
David@0
|
476 |
if ([type isEqualToString:NSRTFDPboardType]) { |
|
David@0
|
477 |
NSData *data = [generalPasteboard dataForType:NSRTFDPboardType]; |
|
David@0
|
478 |
[self insertText:[self attributedStringWithAITextAttachmentExtensionsFromRTFDData:data]]; |
|
David@0
|
479 |
handledPaste = YES; |
|
David@0
|
480 |
|
|
David@0
|
481 |
} else if ([PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY containsObject:type]) { |
|
David@0
|
482 |
//When we hit a type we should let the superclass handle, break without doing anything |
|
David@0
|
483 |
break; |
|
David@0
|
484 |
|
|
David@0
|
485 |
} else if ([FILES_AND_IMAGES_TYPES containsObject:type]) { |
|
David@0
|
486 |
[self addAttachmentsFromPasteboard:generalPasteboard]; |
|
David@0
|
487 |
handledPaste = YES; |
|
David@0
|
488 |
} |
|
David@0
|
489 |
|
|
Evan@166
|
490 |
if (handledPaste) break; |
|
Evan@166
|
491 |
|
|
David@0
|
492 |
} |
|
David@0
|
493 |
|
|
David@0
|
494 |
return handledPaste; |
|
David@0
|
495 |
} |
|
David@0
|
496 |
|
|
David@0
|
497 |
//Paste as rich text without altering our typing attributes |
|
David@0
|
498 |
- (void)pasteAsRichText:(id)sender |
|
David@0
|
499 |
{ |
|
David@0
|
500 |
NSDictionary *attributes = [[self typingAttributes] copy]; |
|
David@0
|
501 |
|
|
David@0
|
502 |
if (![self handlePasteAsRichText]) { |
|
David@0
|
503 |
[self paste:sender]; |
|
David@0
|
504 |
} |
|
David@0
|
505 |
|
|
David@0
|
506 |
if (attributes) { |
|
David@0
|
507 |
[self setTypingAttributes:attributes]; |
|
David@0
|
508 |
} |
|
David@0
|
509 |
|
|
David@0
|
510 |
[attributes release]; |
|
David@0
|
511 |
|
|
David@0
|
512 |
[self scrollRangeToVisible:[self selectedRange]]; |
|
David@0
|
513 |
} |
|
David@0
|
514 |
|
|
David@0
|
515 |
- (void)pasteAsPlainTextWithTraits:(id)sender |
|
David@0
|
516 |
{ |
|
David@0
|
517 |
NSDictionary *attributes = [[self typingAttributes] copy]; |
|
David@0
|
518 |
|
|
David@0
|
519 |
NSPasteboard *generalPasteboard = [NSPasteboard generalPasteboard]; |
|
David@0
|
520 |
NSString *type; |
|
David@0
|
521 |
|
|
David@0
|
522 |
NSArray *supportedTypes = |
|
David@0
|
523 |
[NSArray arrayWithObjects:NSURLPboardType, NSRTFDPboardType, NSRTFPboardType, NSHTMLPboardType, NSStringPboardType, |
|
David@0
|
524 |
NSFilenamesPboardType, NSTIFFPboardType, NSPDFPboardType, NSPICTPboardType, nil]; |
|
David@0
|
525 |
|
|
David@0
|
526 |
type = [[NSPasteboard generalPasteboard] availableTypeFromArray:supportedTypes]; |
|
David@0
|
527 |
|
|
David@0
|
528 |
if ([type isEqualToString:NSRTFPboardType] || |
|
David@0
|
529 |
[type isEqualToString:NSRTFDPboardType] || |
|
David@0
|
530 |
[type isEqualToString:NSHTMLPboardType] || |
|
David@0
|
531 |
[type isEqualToString:NSStringPboardType]) { |
|
David@0
|
532 |
NSData *data; |
|
David@0
|
533 |
|
|
David@0
|
534 |
@try { |
|
David@0
|
535 |
data = [generalPasteboard dataForType:type]; |
|
David@0
|
536 |
} @catch (NSException *localException) { |
|
David@0
|
537 |
data = nil; |
|
David@0
|
538 |
} |
|
David@0
|
539 |
|
|
David@0
|
540 |
//Failed. Try again with the string type. |
|
David@0
|
541 |
if (!data && ![type isEqualToString:NSStringPboardType]) { |
|
David@0
|
542 |
if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) { |
|
David@0
|
543 |
type = NSStringPboardType; |
|
David@0
|
544 |
@try { |
|
David@0
|
545 |
data = [generalPasteboard dataForType:type]; |
|
David@0
|
546 |
} @catch (NSException *localException) { |
|
David@0
|
547 |
data = nil; |
|
David@0
|
548 |
} |
|
David@0
|
549 |
} |
|
David@0
|
550 |
} |
|
David@0
|
551 |
|
|
David@0
|
552 |
if (!data) { |
|
David@0
|
553 |
//We still didn't get valid data... maybe super can handle it |
|
David@0
|
554 |
@try { |
|
David@0
|
555 |
[self paste:sender]; |
|
David@0
|
556 |
} @catch (NSException *localException) { |
|
David@0
|
557 |
NSBeep(); |
|
David@0
|
558 |
return; |
|
David@0
|
559 |
} |
|
David@0
|
560 |
} |
|
David@0
|
561 |
|
|
David@0
|
562 |
NSMutableAttributedString *attributedString; |
|
David@0
|
563 |
|
|
David@0
|
564 |
if ([type isEqualToString:NSStringPboardType]) { |
|
David@0
|
565 |
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; |
|
David@0
|
566 |
attributedString = [[NSMutableAttributedString alloc] initWithString:string |
|
David@0
|
567 |
attributes:[self typingAttributes]]; |
|
David@0
|
568 |
[string release]; |
|
David@0
|
569 |
|
|
David@0
|
570 |
} else { |
|
David@0
|
571 |
@try { |
|
David@0
|
572 |
if ([type isEqualToString:NSRTFPboardType]) { |
|
David@0
|
573 |
attributedString = [[NSMutableAttributedString alloc] initWithRTF:data |
|
David@0
|
574 |
documentAttributes:NULL]; |
|
David@0
|
575 |
} else if ([type isEqualToString:NSRTFDPboardType]) { |
|
David@0
|
576 |
attributedString = [[NSMutableAttributedString alloc] initWithRTFD:data |
|
David@0
|
577 |
documentAttributes:NULL]; |
|
David@0
|
578 |
} else /* NSHTMLPboardType */ { |
|
David@0
|
579 |
attributedString = [[NSMutableAttributedString alloc] initWithHTML:data |
|
David@0
|
580 |
documentAttributes:NULL]; |
|
David@0
|
581 |
} |
|
David@0
|
582 |
} @catch (NSException *localException) { |
|
David@0
|
583 |
//Error while reading the RTF or HTML data, which can happen. Fall back on plain text |
|
David@0
|
584 |
if ([[[NSPasteboard generalPasteboard] types] containsObject:NSStringPboardType]) { |
|
David@0
|
585 |
data = [generalPasteboard dataForType:NSStringPboardType]; |
|
David@0
|
586 |
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; |
|
David@0
|
587 |
attributedString = [[NSMutableAttributedString alloc] initWithString:string |
|
David@0
|
588 |
attributes:[self typingAttributes]]; |
|
David@0
|
589 |
[string release]; |
|
David@0
|
590 |
} else { |
|
David@0
|
591 |
attributedString = nil; |
|
David@0
|
592 |
} |
|
David@0
|
593 |
} |
|
David@0
|
594 |
|
|
David@0
|
595 |
if (!attributedString) { |
|
David@0
|
596 |
NSBeep(); |
|
David@0
|
597 |
return; |
|
David@0
|
598 |
} |
|
David@0
|
599 |
|
|
David@0
|
600 |
[attributedString convertForPasteWithTraitsUsingAttributes:[self typingAttributes]]; |
|
David@0
|
601 |
} |
|
David@0
|
602 |
|
|
David@0
|
603 |
NSRange selectedRange = [self selectedRange]; |
|
David@0
|
604 |
NSTextStorage *textStorage = [self textStorage]; |
|
David@0
|
605 |
|
|
David@0
|
606 |
//Prepare the undo operation |
|
David@0
|
607 |
NSUndoManager *undoManager = [self undoManager]; |
|
David@0
|
608 |
[[undoManager prepareWithInvocationTarget:textStorage] |
|
David@0
|
609 |
replaceCharactersInRange:NSMakeRange(selectedRange.location, [attributedString length]) |
|
David@0
|
610 |
withAttributedString:[textStorage attributedSubstringFromRange:selectedRange]]; |
|
David@0
|
611 |
[undoManager setActionName:AILocalizedString(@"Paste", nil)]; |
|
David@0
|
612 |
|
|
David@0
|
613 |
//Perform the paste |
|
David@0
|
614 |
[textStorage replaceCharactersInRange:selectedRange |
|
David@0
|
615 |
withAttributedString:attributedString]; |
|
David@0
|
616 |
// Align our text properly (only need to if the first character was changed) |
|
David@0
|
617 |
if (selectedRange.location == 0) |
|
David@0
|
618 |
[self setBaseWritingDirection:[[textStorage string] baseWritingDirection]]; |
|
David@0
|
619 |
//Notify that we changed our text |
|
David@0
|
620 |
[[NSNotificationCenter defaultCenter] postNotificationName:NSTextDidChangeNotification |
|
David@0
|
621 |
object:self]; |
|
David@0
|
622 |
[attributedString release]; |
|
David@0
|
623 |
|
|
David@0
|
624 |
} else if ([FILES_AND_IMAGES_TYPES containsObject:type] || |
|
David@0
|
625 |
[type isEqualToString:NSURLPboardType]) { |
|
David@0
|
626 |
if (![self handlePasteAsRichText]) { |
|
David@0
|
627 |
[self paste:sender]; |
|
David@0
|
628 |
} |
|
David@0
|
629 |
|
|
David@0
|
630 |
} else { |
|
David@0
|
631 |
//If we didn't handle it yet, let super try to deal with it |
|
David@0
|
632 |
[self paste:sender]; |
|
David@0
|
633 |
} |
|
David@0
|
634 |
|
|
David@0
|
635 |
if (attributes) { |
|
David@0
|
636 |
[self setTypingAttributes:attributes]; |
|
David@0
|
637 |
} |
|
David@0
|
638 |
|
|
David@0
|
639 |
[attributes release]; |
|
David@0
|
640 |
|
|
David@0
|
641 |
[self scrollRangeToVisible:[self selectedRange]]; |
|
David@0
|
642 |
} |
|
David@0
|
643 |
|
|
David@0
|
644 |
#pragma mark Deletion |
|
David@0
|
645 |
|
|
David@0
|
646 |
- (void)deleteBackward:(id)sender |
|
David@0
|
647 |
{ |
|
David@0
|
648 |
//Perform the delete |
|
David@0
|
649 |
[super deleteBackward:sender]; |
|
David@0
|
650 |
|
|
David@0
|
651 |
//If we are now an empty string, and we still have a link active, clear the link |
|
David@0
|
652 |
if ([[self textStorage] length] == 0) { |
|
David@0
|
653 |
[self clearLinkAttribute]; |
|
David@0
|
654 |
} |
|
David@0
|
655 |
} |
|
David@0
|
656 |
|
|
David@0
|
657 |
//Contact menu --------------------------------------------------------------------------------------------------------- |
|
David@0
|
658 |
#pragma mark Contact menu |
|
David@0
|
659 |
//Set and return the selected chat (to auto-configure the contact menu) |
|
David@0
|
660 |
- (void)setChat:(AIChat *)inChat |
|
David@0
|
661 |
{ |
|
David@0
|
662 |
if (chat != inChat) { |
|
zacw@844
|
663 |
if(chat.isGroupChat) { |
|
zacw@844
|
664 |
[chat removeObserver:self forKeyPath:@"Character Counter Max"]; |
|
zacw@847
|
665 |
[chat removeObserver:self forKeyPath:@"Character Counter Prefix"]; |
|
zacw@844
|
666 |
} |
|
zacw@844
|
667 |
|
|
David@0
|
668 |
[chat release]; |
|
zacw@1682
|
669 |
chat = [inChat retain]; |
|
David@0
|
670 |
|
|
zacw@1682
|
671 |
// We only need to update our observation state for group chats. |
|
zacw@807
|
672 |
if(chat.isGroupChat) { |
|
zacw@807
|
673 |
[chat addObserver:self |
|
zacw@807
|
674 |
forKeyPath:@"Character Counter Max" |
|
zacw@807
|
675 |
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial) |
|
zacw@807
|
676 |
context:NULL]; |
|
zacw@847
|
677 |
|
|
zacw@847
|
678 |
[chat addObserver:self |
|
zacw@847
|
679 |
forKeyPath:@"Character Counter Prefix" |
|
zacw@847
|
680 |
options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial) |
|
zacw@847
|
681 |
context:NULL]; |
|
zacw@807
|
682 |
} |
|
zacw@1682
|
683 |
|
|
zacw@1682
|
684 |
//Observe preferences changes for typing enable/disable |
|
zacw@1682
|
685 |
[adium.preferenceController registerPreferenceObserver:self forGroup:GROUP_ACCOUNT_STATUS]; |
|
David@0
|
686 |
} |
|
zacw@1682
|
687 |
|
|
zacw@1682
|
688 |
//Set up the character counter for this chat's list object. |
|
zacw@1682
|
689 |
//This is done regardless of a chat changing because destination changes need to trigger this. |
|
zacw@1682
|
690 |
if(!chat.isGroupChat) { |
|
zacw@1682
|
691 |
[self setCharacterCounterMaximum:[chat.listObject integerValueForProperty:@"Character Counter Max"]]; |
|
zacw@1682
|
692 |
[self setCharacterCounterVisible:([chat.listObject valueForProperty:@"Character Counter Max"] != nil)]; |
|
zacw@1682
|
693 |
[self setCharacterCounterPrefix:[chat.listObject valueForProperty:@"Character Counter Prefix"]]; |
|
zacw@1682
|
694 |
|
|
zacw@1682
|
695 |
[self updateCharacterCounter]; |
|
zacw@1682
|
696 |
} |
|
David@0
|
697 |
} |
|
David@0
|
698 |
- (AIChat *)chat{ |
|
David@0
|
699 |
return chat; |
|
David@0
|
700 |
} |
|
David@0
|
701 |
|
|
David@0
|
702 |
//Return the selected list object (to auto-configure the contact menu) |
|
David@0
|
703 |
- (AIListContact *)listObject |
|
David@0
|
704 |
{ |
|
David@426
|
705 |
return chat.listObject; |
|
David@0
|
706 |
} |
|
David@0
|
707 |
|
|
David@0
|
708 |
- (AIListContact *)preferredListObject |
|
David@0
|
709 |
{ |
|
David@0
|
710 |
return [chat preferredListObject]; |
|
David@0
|
711 |
} |
|
David@0
|
712 |
|
|
David@0
|
713 |
//Auto Sizing ---------------------------------------------------------------------------------------------------------- |
|
David@0
|
714 |
#pragma mark Auto-sizing |
|
David@0
|
715 |
//Returns our desired size |
|
David@0
|
716 |
- (NSSize)desiredSize |
|
David@0
|
717 |
{ |
|
David@0
|
718 |
if (_desiredSizeCached.width == 0) { |
|
David@0
|
719 |
float textHeight; |
|
David@0
|
720 |
if ([[self textStorage] length] != 0) { |
|
David@0
|
721 |
//If there is text in this view, let the container tell us its height |
|
David@0
|
722 |
|
|
David@0
|
723 |
//Force glyph generation. We must do this or usedRectForTextContainer might only return a rect for a |
|
David@0
|
724 |
//portion of our text. |
|
David@0
|
725 |
[[self layoutManager] glyphRangeForTextContainer:[self textContainer]]; |
|
David@0
|
726 |
|
|
David@0
|
727 |
textHeight = [[self layoutManager] usedRectForTextContainer:[self textContainer]].size.height; |
|
David@0
|
728 |
} else { |
|
David@0
|
729 |
//Otherwise, we use the current typing attributes to guess what the height of a line should be |
|
David@0
|
730 |
textHeight = [NSAttributedString stringHeightForAttributes:[self typingAttributes]]; |
|
David@0
|
731 |
} |
|
David@0
|
732 |
|
|
David@0
|
733 |
/* When we called glyphRangeForTextContainer, we may have triggered re-entry via |
|
David@0
|
734 |
* -[self setFrame:] --> -[self frameDidChange:] --> -[self _resetCacheAndPostSizeChanged] |
|
David@0
|
735 |
* in which case the second entry through the loop (the future relative to our conversation in this comment) got the correct desired size. |
|
David@0
|
736 |
* In the present, an *old* value is in textHeight. We don't want to use that. Jumping gigawatts! |
|
David@0
|
737 |
*/ |
|
David@0
|
738 |
if (_desiredSizeCached.width == 0) { |
|
David@0
|
739 |
_desiredSizeCached = NSMakeSize([self frame].size.width, textHeight + ENTRY_TEXTVIEW_PADDING); |
|
David@0
|
740 |
} |
|
David@0
|
741 |
} |
|
David@0
|
742 |
|
|
David@0
|
743 |
return _desiredSizeCached; |
|
David@0
|
744 |
} |
|
David@0
|
745 |
|
|
David@0
|
746 |
//Reset the desired size cache when our frame changes |
|
David@0
|
747 |
- (void)frameDidChange:(NSNotification *)notification |
|
David@0
|
748 |
{ |
|
David@0
|
749 |
//resetCacheAndPostSizeChanged can get us right back to here, resulting in an infinite loop if we're not careful |
|
David@0
|
750 |
if (!resizing) { |
|
David@0
|
751 |
resizing = YES; |
|
David@0
|
752 |
[self _resetCacheAndPostSizeChanged]; |
|
David@0
|
753 |
resizing = NO; |
|
David@0
|
754 |
} |
|
David@0
|
755 |
} |
|
David@0
|
756 |
|
|
David@0
|
757 |
//Reset the desired size cache and post a size changed notification. Call after the text's dimensions change |
|
David@0
|
758 |
- (void)_resetCacheAndPostSizeChanged |
|
David@0
|
759 |
{ |
|
David@0
|
760 |
//Reset the size cache |
|
David@0
|
761 |
_desiredSizeCached = NSMakeSize(0,0); |
|
David@0
|
762 |
|
|
David@0
|
763 |
//Post notification if size changed |
|
David@0
|
764 |
if (!NSEqualSizes([self desiredSize], lastPostedSize)) { |
|
David@0
|
765 |
lastPostedSize = [self desiredSize]; |
|
David@0
|
766 |
[[NSNotificationCenter defaultCenter] postNotificationName:AIViewDesiredSizeDidChangeNotification object:self]; |
|
David@0
|
767 |
} |
|
David@0
|
768 |
} |
|
David@0
|
769 |
|
|
David@0
|
770 |
//Paging --------------------------------------------------------------------------------------------------------------- |
|
David@0
|
771 |
#pragma mark Paging |
|
David@0
|
772 |
//Page up or down in the message view |
|
David@0
|
773 |
- (void)scrollPageUp:(id)sender |
|
David@0
|
774 |
{ |
|
David@0
|
775 |
if (associatedView && [associatedView respondsToSelector:@selector(pageUp:)]) { |
|
David@0
|
776 |
[associatedView pageUp:nil]; |
|
David@0
|
777 |
} else { |
|
David@0
|
778 |
[super scrollPageUp:sender]; |
|
David@0
|
779 |
} |
|
David@0
|
780 |
} |
|
David@0
|
781 |
- (void)scrollPageDown:(id)sender |
|
David@0
|
782 |
{ |
|
David@0
|
783 |
if (associatedView && [associatedView respondsToSelector:@selector(pageDown:)]) { |
|
David@0
|
784 |
[associatedView pageDown:nil]; |
|
David@0
|
785 |
} else { |
|
David@0
|
786 |
[super scrollPageDown:sender]; |
|
David@0
|
787 |
} |
|
David@0
|
788 |
} |
|
David@0
|
789 |
|
|
David@0
|
790 |
|
|
David@0
|
791 |
//History -------------------------------------------------------------------------------------------------------------- |
|
David@0
|
792 |
#pragma mark History |
|
David@759
|
793 |
@synthesize historyEnabled; |
|
David@0
|
794 |
|
|
David@0
|
795 |
//Move up through the history |
|
David@0
|
796 |
- (void)historyUp |
|
David@0
|
797 |
{ |
|
David@0
|
798 |
if (currentHistoryLocation == 0) { |
|
David@0
|
799 |
//Store current message |
|
David@0
|
800 |
[historyArray replaceObjectAtIndex:0 withObject:[[[self textStorage] copy] autorelease]]; |
|
David@0
|
801 |
} |
|
David@0
|
802 |
|
|
David@0
|
803 |
if (currentHistoryLocation < [historyArray count]-1) { |
|
David@0
|
804 |
//Move up |
|
David@0
|
805 |
currentHistoryLocation++; |
|
David@0
|
806 |
|
|
David@0
|
807 |
//Display history |
|
David@0
|
808 |
[self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]]; |
|
David@0
|
809 |
} |
|
David@0
|
810 |
} |
|
David@0
|
811 |
|
|
David@0
|
812 |
//Move down through history |
|
David@0
|
813 |
- (void)historyDown |
|
David@0
|
814 |
{ |
|
David@0
|
815 |
if (currentHistoryLocation > 0) { |
|
David@0
|
816 |
//Move down |
|
David@0
|
817 |
currentHistoryLocation--; |
|
David@0
|
818 |
|
|
David@0
|
819 |
//Display history |
|
David@0
|
820 |
[self setAttributedString:[historyArray objectAtIndex:currentHistoryLocation]]; |
|
David@0
|
821 |
} |
|
David@0
|
822 |
} |
|
David@0
|
823 |
|
|
David@0
|
824 |
//Update history when content is sent |
|
David@0
|
825 |
- (IBAction)sendContent:(id)sender |
|
David@0
|
826 |
{ |
|
David@0
|
827 |
NSAttributedString *textStorage = [self textStorage]; |
|
David@0
|
828 |
|
|
David@0
|
829 |
//Add to history if there is text being sent |
|
David@0
|
830 |
[historyArray insertObject:[[textStorage copy] autorelease] atIndex:1]; |
|
David@0
|
831 |
if ([historyArray count] > MAX_HISTORY) { |
|
David@0
|
832 |
[historyArray removeLastObject]; |
|
David@0
|
833 |
} |
|
David@0
|
834 |
|
|
David@0
|
835 |
currentHistoryLocation = 0; //Move back to bottom of history |
|
David@0
|
836 |
|
|
David@0
|
837 |
//Send the content |
|
David@0
|
838 |
[super sendContent:sender]; |
|
David@0
|
839 |
|
|
David@0
|
840 |
//Clear the undo/redo stack as it makes no sense to carry between sends (the history is for that) |
|
David@0
|
841 |
[[self undoManager] removeAllActions]; |
|
David@0
|
842 |
} |
|
David@0
|
843 |
|
|
David@0
|
844 |
//Populate the history with messages from the message history |
|
David@0
|
845 |
- (void)contentObjectAdded:(NSNotification *)notification |
|
David@0
|
846 |
{ |
|
David@812
|
847 |
AIContentObject *content = [notification.userInfo objectForKey:@"AIContentObject"]; |
|
David@0
|
848 |
|
|
David@813
|
849 |
if (self.chat == content.chat && ([content.type isEqualToString:CONTENT_CONTEXT_TYPE]) && content.isOutgoing) { |
|
David@0
|
850 |
//Populate the history with messages from us |
|
David@812
|
851 |
[historyArray insertObject:content.message atIndex:1]; |
|
David@813
|
852 |
if (historyArray.count > MAX_HISTORY) { |
|
David@0
|
853 |
[historyArray removeLastObject]; |
|
David@0
|
854 |
} |
|
David@0
|
855 |
} |
|
David@0
|
856 |
} |
|
David@0
|
857 |
|
|
David@0
|
858 |
//Push and Pop --------------------------------------------------------------------------------------------------------- |
|
David@0
|
859 |
#pragma mark Push and Pop |
|
David@0
|
860 |
//Enable/Disable push-pop |
|
David@0
|
861 |
- (void)setPushPopEnabled:(BOOL)inBool |
|
David@0
|
862 |
{ |
|
David@0
|
863 |
pushPopEnabled = inBool; |
|
David@0
|
864 |
} |
|
David@0
|
865 |
|
|
David@0
|
866 |
//Push out of the message entry field |
|
David@0
|
867 |
- (void)pushContent |
|
David@0
|
868 |
{ |
|
David@0
|
869 |
if ([[self textStorage] length] != 0 && pushPopEnabled) { |
|
David@0
|
870 |
[pushArray addObject:[[[self textStorage] copy] autorelease]]; |
|
David@0
|
871 |
[self setString:@""]; |
|
David@0
|
872 |
[self _setPushIndicatorVisible:YES]; |
|
David@0
|
873 |
} |
|
David@0
|
874 |
} |
|
David@0
|
875 |
|
|
David@0
|
876 |
//Pop into the message entry field |
|
David@0
|
877 |
- (void)popContent |
|
David@0
|
878 |
{ |
|
David@0
|
879 |
if ([pushArray count] && pushPopEnabled) { |
|
David@0
|
880 |
[self setAttributedString:[pushArray lastObject]]; |
|
David@0
|
881 |
[self setSelectedRange:NSMakeRange([[self textStorage] length], 0)]; //selection to end |
|
David@0
|
882 |
[pushArray removeLastObject]; |
|
David@0
|
883 |
if ([pushArray count] == 0) { |
|
David@0
|
884 |
[self _setPushIndicatorVisible:NO]; |
|
David@0
|
885 |
} |
|
David@0
|
886 |
} |
|
David@0
|
887 |
} |
|
David@0
|
888 |
|
|
David@0
|
889 |
//Swap current content |
|
David@0
|
890 |
- (void)swapContent |
|
David@0
|
891 |
{ |
|
David@0
|
892 |
if (pushPopEnabled) { |
|
David@0
|
893 |
NSAttributedString *tempMessage = [[[self textStorage] copy] autorelease]; |
|
David@0
|
894 |
|
|
David@0
|
895 |
if ([pushArray count]) { |
|
David@0
|
896 |
[self popContent]; |
|
David@0
|
897 |
} else { |
|
David@0
|
898 |
[self setString:@""]; |
|
David@0
|
899 |
} |
|
David@0
|
900 |
|
|
David@0
|
901 |
if (tempMessage && [tempMessage length] != 0) { |
|
David@0
|
902 |
[pushArray addObject:tempMessage]; |
|
David@0
|
903 |
[self _setPushIndicatorVisible:YES]; |
|
David@0
|
904 |
} |
|
David@0
|
905 |
} |
|
David@0
|
906 |
} |
|
David@0
|
907 |
|
|
David@0
|
908 |
//Push indicator |
|
David@0
|
909 |
- (void)_setPushIndicatorVisible:(BOOL)visible |
|
David@0
|
910 |
{ |
|
David@0
|
911 |
static NSImage *pushIndicatorImage = nil; |
|
David@0
|
912 |
|
|
David@0
|
913 |
// |
|
David@0
|
914 |
if (!pushIndicatorImage) pushIndicatorImage = [[NSImage imageNamed:@"stackImage" forClass:[self class]] retain]; |
|
David@0
|
915 |
|
|
David@0
|
916 |
if (visible && !pushIndicatorVisible) { |
|
David@0
|
917 |
pushIndicatorVisible = visible; |
|
David@0
|
918 |
|
|
David@0
|
919 |
//Push text over to make room for indicator |
|
David@0
|
920 |
NSSize size = [self frame].size; |
|
David@0
|
921 |
size.width -= ([pushIndicatorImage size].width); |
|
David@0
|
922 |
[self setFrameSize:size]; |
|
David@0
|
923 |
|
|
David@0
|
924 |
// Make the indicator and set its action. It is a button with no border. |
|
David@0
|
925 |
pushIndicator = [[NSButton alloc] initWithFrame: |
|
David@0
|
926 |
NSMakeRect(0, 0, [pushIndicatorImage size].width, [pushIndicatorImage size].height)]; |
|
David@0
|
927 |
[pushIndicator setButtonType:NSMomentaryPushButton]; |
|
David@0
|
928 |
[pushIndicator setAutoresizingMask:(NSViewMinXMargin)]; |
|
David@0
|
929 |
[pushIndicator setImage:pushIndicatorImage]; |
|
David@0
|
930 |
[pushIndicator setImagePosition:NSImageOnly]; |
|
David@0
|
931 |
[pushIndicator setBezelStyle:NSRegularSquareBezelStyle]; |
|
David@0
|
932 |
[pushIndicator setBordered:NO]; |
|
David@0
|
933 |
[[self superview] addSubview:pushIndicator]; |
|
David@0
|
934 |
[pushIndicator setTarget:self]; |
|
David@0
|
935 |
[pushIndicator setAction:@selector(popContent)]; |
|
David@0
|
936 |
|
|
David@0
|
937 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]]; |
|
David@0
|
938 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]]; |
|
David@0
|
939 |
|
|
David@0
|
940 |
[self positionPushIndicator]; //Set the indicators initial position |
|
David@0
|
941 |
|
|
David@0
|
942 |
} else if (!visible && pushIndicatorVisible) { |
|
David@0
|
943 |
pushIndicatorVisible = visible; |
|
David@0
|
944 |
|
|
David@0
|
945 |
//Push text back |
|
David@0
|
946 |
NSSize size = [self frame].size; |
|
David@0
|
947 |
size.width += [pushIndicatorImage size].width; |
|
David@0
|
948 |
[self setFrameSize:size]; |
|
David@0
|
949 |
|
|
David@0
|
950 |
//Unsubcribe, if necessary. |
|
David@0
|
951 |
if (!characterCounter) { |
|
David@0
|
952 |
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]]; |
|
David@0
|
953 |
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]]; |
|
David@0
|
954 |
} |
|
David@0
|
955 |
//Remove indicator |
|
David@0
|
956 |
[pushIndicator removeFromSuperview]; |
|
David@0
|
957 |
[pushIndicator release]; pushIndicator = nil; |
|
David@0
|
958 |
|
|
David@0
|
959 |
[self positionPushIndicator]; |
|
David@0
|
960 |
} |
|
David@0
|
961 |
} |
|
David@0
|
962 |
|
|
David@0
|
963 |
//Reposition the push indicator into lower right corner |
|
David@0
|
964 |
- (void)positionPushIndicator |
|
David@0
|
965 |
{ |
|
David@0
|
966 |
NSRect visRect = [[self superview] bounds]; |
|
David@0
|
967 |
NSRect indFrame = [pushIndicator frame]; |
|
David@0
|
968 |
float counterPadding = characterCounter ? NSWidth([characterCounter frame]) : 0; |
|
David@0
|
969 |
[pushIndicator setFrameOrigin:NSMakePoint(NSMaxX(visRect) - NSWidth(indFrame) - INDICATOR_RIGHT_PADDING - counterPadding, |
|
zacw@1123
|
970 |
NSMidY([self frame]) - NSHeight(indFrame)/2)]; |
|
David@0
|
971 |
[[self enclosingScrollView] setNeedsDisplay:YES]; |
|
David@0
|
972 |
} |
|
David@0
|
973 |
|
|
David@0
|
974 |
#pragma mark Indicators Positioning |
|
David@0
|
975 |
|
|
David@0
|
976 |
/** |
|
David@0
|
977 |
* @brief Dispatch for both indicators to observe bounds & frame changes of their superview |
|
David@0
|
978 |
* |
|
David@0
|
979 |
* Stupid that this is necessary, but you can only remove an entire object from a notification center's observer list, |
|
David@0
|
980 |
* not on a per-method basis. |
|
David@0
|
981 |
*/ |
|
David@0
|
982 |
- (void)positionIndicators:(NSNotification *)notification |
|
David@0
|
983 |
{ |
|
David@0
|
984 |
if (pushIndicatorVisible) |
|
David@0
|
985 |
[self positionPushIndicator]; |
|
David@0
|
986 |
if (characterCounter) |
|
David@0
|
987 |
[self positionCharacterCounter]; |
|
David@0
|
988 |
} |
|
David@0
|
989 |
|
|
David@0
|
990 |
#pragma mark Character Counter |
|
David@0
|
991 |
|
|
David@0
|
992 |
/** |
|
David@0
|
993 |
* @brief Makes the character counter for this view visible. |
|
David@0
|
994 |
*/ |
|
David@0
|
995 |
- (void)setCharacterCounterVisible:(BOOL)visible |
|
David@0
|
996 |
{ |
|
David@0
|
997 |
if (visible && !characterCounter) { |
|
David@0
|
998 |
characterCounter = [[AISimpleTextView alloc] initWithFrame:NSZeroRect]; |
|
zacw@2521
|
999 |
[characterCounter setAutoresizingMask:(NSViewMinXMargin|NSViewWidthSizable)]; |
|
David@0
|
1000 |
|
|
David@0
|
1001 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewBoundsDidChangeNotification object:[self superview]]; |
|
David@0
|
1002 |
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(positionIndicators:) name:NSViewFrameDidChangeNotification object:[self superview]]; |
|
David@0
|
1003 |
|
|
David@0
|
1004 |
[self updateCharacterCounter]; |
|
David@0
|
1005 |
[[self superview] addSubview:characterCounter]; |
|
David@0
|
1006 |
|
|
David@0
|
1007 |
} else if (!visible && characterCounter) { |
|
David@0
|
1008 |
[characterCounter removeFromSuperview]; |
|
David@0
|
1009 |
|
|
David@0
|
1010 |
// Make sure to resize this view back to the right size. |
|
David@0
|
1011 |
NSSize size = [self frame].size; |
|
David@0
|
1012 |
size.width += NSWidth([characterCounter frame]); |
|
David@0
|
1013 |
[self setFrameSize:size]; |
|
David@0
|
1014 |
|
|
David@0
|
1015 |
//Unsubscribe, if necessary. |
|
David@0
|
1016 |
if (!pushIndicatorVisible) { |
|
David@0
|
1017 |
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewBoundsDidChangeNotification object:[self superview]]; |
|
David@0
|
1018 |
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:[self superview]]; |
|
David@0
|
1019 |
} |
|
David@0
|
1020 |
|
|
David@0
|
1021 |
[characterCounter release]; |
|
David@0
|
1022 |
characterCounter = nil; |
|
David@0
|
1023 |
|
|
David@0
|
1024 |
// Reposition the push indicator, if necessary. |
|
David@0
|
1025 |
if (pushIndicatorVisible) |
|
David@0
|
1026 |
[self positionPushIndicator]; |
|
David@0
|
1027 |
|
|
David@0
|
1028 |
[[self enclosingScrollView] setNeedsDisplay:YES]; |
|
David@0
|
1029 |
} |
|
David@0
|
1030 |
} |
|
David@0
|
1031 |
|
|
zacw@847
|
1032 |
/*! |
|
zacw@847
|
1033 |
* @brief Set the prefix for the character count. |
|
zacw@847
|
1034 |
*/ |
|
zacw@847
|
1035 |
- (void)setCharacterCounterPrefix:(NSString *)prefix |
|
zacw@847
|
1036 |
{ |
|
zacw@847
|
1037 |
if(prefix != characterCounterPrefix) { |
|
zacw@847
|
1038 |
[characterCounterPrefix release]; |
|
zacw@847
|
1039 |
characterCounterPrefix = [prefix retain]; |
|
zacw@847
|
1040 |
} |
|
zacw@847
|
1041 |
} |
|
zacw@847
|
1042 |
|
|
David@0
|
1043 |
/** |
|
David@0
|
1044 |
* @brief Set the number of characters the character counter should count down from. |
|
David@0
|
1045 |
*/ |
|
David@0
|
1046 |
- (void)setCharacterCounterMaximum:(int)inMaxCharacters |
|
David@0
|
1047 |
{ |
|
David@0
|
1048 |
maxCharacters = inMaxCharacters; |
|
David@0
|
1049 |
|
|
David@0
|
1050 |
if (characterCounter) |
|
David@0
|
1051 |
[self updateCharacterCounter]; |
|
David@0
|
1052 |
} |
|
David@0
|
1053 |
|
|
David@0
|
1054 |
/** |
|
David@0
|
1055 |
* @brief Update the character counter and resize this view to make space if the counter's bounds change. |
|
David@0
|
1056 |
*/ |
|
David@0
|
1057 |
- (void)updateCharacterCounter |
|
David@0
|
1058 |
{ |
|
David@0
|
1059 |
NSRect visRect = [[self superview] bounds]; |
|
zacw@1123
|
1060 |
|
|
Peter@1078
|
1061 |
NSString *inputString = [self.chat.account encodedAttributedString:[self textStorage] forListObject:self.chat.listObject]; |
|
Peter@1078
|
1062 |
int currentCount = (maxCharacters - [inputString length]); |
|
zacw@1119
|
1063 |
|
|
zacw@1140
|
1064 |
if(maxCharacters && currentCount < 0) { |
|
zacw@1123
|
1065 |
savedTextColor = [[self textColor] retain]; |
|
zacw@1119
|
1066 |
|
|
zacw@1119
|
1067 |
[self setBackgroundColor:[NSColor colorWithCalibratedHue:0.983 |
|
zacw@1119
|
1068 |
saturation:0.43 |
|
zacw@1119
|
1069 |
brightness:0.99 |
|
zacw@1119
|
1070 |
alpha:1.0]]; |
|
zacw@1119
|
1071 |
|
|
zacw@1123
|
1072 |
[self.enclosingScrollView setBackgroundColor:[NSColor colorWithCalibratedHue:0.983 |
|
zacw@1123
|
1073 |
saturation:0.43 |
|
zacw@1123
|
1074 |
brightness:0.99 |
|
zacw@1123
|
1075 |
alpha:1.0]]; |
|
zacw@1119
|
1076 |
} else { |
|
zacw@1123
|
1077 |
if (savedTextColor) { |
|
zacw@1123
|
1078 |
[self setTextColor:savedTextColor]; |
|
zacw@1123
|
1079 |
savedTextColor = nil; |
|
zacw@1119
|
1080 |
} |
|
zacw@1119
|
1081 |
|
|
zacw@1119
|
1082 |
[self setBackgroundColor:[NSColor controlBackgroundColor]]; |
|
zacw@1123
|
1083 |
[self.enclosingScrollView setBackgroundColor:[NSColor controlBackgroundColor]]; |
|
zacw@1119
|
1084 |
} |
|
zacw@847
|
1085 |
|
|
zacw@847
|
1086 |
NSString *counterText = [NSString stringWithFormat:@"%d", currentCount]; |
|
zacw@847
|
1087 |
|
|
zacw@847
|
1088 |
if (characterCounterPrefix) { |
|
zacw@847
|
1089 |
counterText = [NSString stringWithFormat:@"%@%@", characterCounterPrefix, counterText]; |
|
zacw@847
|
1090 |
} |
|
zacw@847
|
1091 |
|
|
zacw@847
|
1092 |
NSAttributedString *label = [[NSAttributedString alloc] initWithString:counterText |
|
David@95
|
1093 |
attributes:[adium.contentController defaultFormattingAttributes]]; |
|
David@0
|
1094 |
[characterCounter setString:label]; |
|
zacw@1123
|
1095 |
[characterCounter setFrameSize:label.size]; |
|
David@0
|
1096 |
[label release]; |
|
David@0
|
1097 |
|
|
David@0
|
1098 |
//Reposition the character counter. |
|
David@0
|
1099 |
[self positionCharacterCounter]; |
|
David@0
|
1100 |
|
|
David@0
|
1101 |
//Shift the text entry view over as necessary. |
|
David@0
|
1102 |
float indent = 0; |
|
David@0
|
1103 |
if (pushIndicatorVisible || characterCounter) { |
|
David@0
|
1104 |
float pushIndicatorX = pushIndicator ? NSMinX([pushIndicator frame]) : NSMaxX([self bounds]); |
|
David@0
|
1105 |
float characterCounterX = characterCounter ? NSMinX([characterCounter frame]) : NSMaxX([self bounds]); |
|
David@0
|
1106 |
indent = NSWidth(visRect) - fminf(pushIndicatorX, characterCounterX); |
|
David@0
|
1107 |
} |
|
David@0
|
1108 |
[self setFrameSize:NSMakeSize(NSWidth(visRect) - indent, NSHeight([self frame]))]; |
|
David@0
|
1109 |
|
|
David@0
|
1110 |
//Reposition the push indicator if necessary. |
|
David@0
|
1111 |
if (pushIndicatorVisible) |
|
David@0
|
1112 |
[self positionPushIndicator]; |
|
David@0
|
1113 |
|
|
David@0
|
1114 |
[[self enclosingScrollView] setNeedsDisplay:YES]; |
|
David@0
|
1115 |
} |
|
David@0
|
1116 |
|
|
David@0
|
1117 |
/** |
|
David@0
|
1118 |
* @brief Keeps the character counter in the bottom right corner. |
|
David@0
|
1119 |
*/ |
|
David@0
|
1120 |
- (void)positionCharacterCounter |
|
David@0
|
1121 |
{ |
|
David@0
|
1122 |
NSRect visRect = [[self superview] bounds]; |
|
sholt@2589
|
1123 |
NSSize counterSize = characterCounter.string.size; |
|
David@0
|
1124 |
|
|
David@0
|
1125 |
//NSMaxY([self frame]) is necessary because visRect's height changes after you start typing. No idea why. |
|
sholt@2589
|
1126 |
[characterCounter setFrameOrigin:NSMakePoint(NSMaxX(visRect) - counterSize.width - INDICATOR_RIGHT_PADDING, |
|
sholt@2589
|
1127 |
NSMidY([self frame]) - (counterSize.height)/2)]; |
|
sholt@2589
|
1128 |
[characterCounter setFrameSize:counterSize]; |
|
David@0
|
1129 |
[[self enclosingScrollView] setNeedsDisplay:YES]; |
|
David@0
|
1130 |
} |
|
David@0
|
1131 |
|
|
zacw@807
|
1132 |
#pragma mark List Object Observer / Chat KVO |
|
David@0
|
1133 |
|
|
David@0
|
1134 |
- (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent |
|
David@0
|
1135 |
{ |
|
David@426
|
1136 |
if ((inObject == chat.listObject) && |
|
zacw@847
|
1137 |
(!inModifiedKeys || [inModifiedKeys containsObject:@"Character Counter Max"] || [inModifiedKeys containsObject:@"Character Counter Prefix"])) { |
|
David@0
|
1138 |
[self setCharacterCounterMaximum:[inObject integerValueForProperty:@"Character Counter Max"]]; |
|
David@0
|
1139 |
[self setCharacterCounterVisible:([inObject valueForProperty:@"Character Counter Max"] != nil)]; |
|
zacw@847
|
1140 |
[self setCharacterCounterPrefix:[inObject valueForProperty:@"Character Counter Prefix"]]; |
|
zacw@847
|
1141 |
|
|
zacw@847
|
1142 |
[self updateCharacterCounter]; |
|
David@0
|
1143 |
} |
|
David@0
|
1144 |
|
|
David@0
|
1145 |
return nil; |
|
David@0
|
1146 |
} |
|
David@0
|
1147 |
|
|
zacw@807
|
1148 |
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context |
|
zacw@807
|
1149 |
{ |
|
zacw@847
|
1150 |
if(object == chat && ([keyPath isEqualToString:@"Character Counter Max"] || [keyPath isEqualToString:@"Character Counter Prefix"])) { |
|
zacw@807
|
1151 |
[self setCharacterCounterMaximum:[chat integerValueForProperty:@"Character Counter Max"]]; |
|
zacw@847
|
1152 |
[self setCharacterCounterVisible:([chat valueForProperty:@"Character Counter Max"] != nil)]; |
|
zacw@847
|
1153 |
[self setCharacterCounterPrefix:[chat valueForProperty:@"Character Counter Prefix"]]; |
|
zacw@847
|
1154 |
|
|
zacw@847
|
1155 |
[self updateCharacterCounter]; |
|
zacw@807
|
1156 |
} |
|
zacw@807
|
1157 |
} |
|
David@0
|
1158 |
|
|
David@0
|
1159 |
#pragma mark Contextual Menus |
|
David@0
|
1160 |
|
|
David@0
|
1161 |
- (NSMenu *)menuForEvent:(NSEvent *)theEvent |
|
David@0
|
1162 |
{ |
|
David@0
|
1163 |
NSMenu *contextualMenu = nil; |
|
David@0
|
1164 |
|
|
David@0
|
1165 |
NSArray *itemsArray = nil; |
|
David@0
|
1166 |
BOOL addedOurLinkItems = NO; |
|
David@0
|
1167 |
|
|
David@0
|
1168 |
if ((contextualMenu = [super menuForEvent:theEvent])) { |
|
David@0
|
1169 |
contextualMenu = [[contextualMenu copy] autorelease]; |
|
Evan@166
|
1170 |
|
|
David@0
|
1171 |
NSMenuItem *editLinkItem = nil; |
|
Evan@166
|
1172 |
for (NSMenuItem *menuItem in contextualMenu.itemArray) { |
|
David@0
|
1173 |
if ([[menuItem title] rangeOfString:AILocalizedString(@"Edit Link", nil)].location != NSNotFound) { |
|
David@0
|
1174 |
editLinkItem = menuItem; |
|
David@0
|
1175 |
break; |
|
David@0
|
1176 |
} |
|
David@0
|
1177 |
} |
|
David@0
|
1178 |
|
|
David@0
|
1179 |
if (editLinkItem) { |
|
David@0
|
1180 |
//There was an Edit Link item. Remove it, and add out own link editing items in its place. |
|
David@0
|
1181 |
int editIndex = [contextualMenu indexOfItem:editLinkItem]; |
|
David@0
|
1182 |
[contextualMenu removeItem:editLinkItem]; |
|
David@0
|
1183 |
|
|
David@100
|
1184 |
NSMenu *linkItemsMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObject: |
|
David@433
|
1185 |
[NSNumber numberWithInt:Context_TextView_LinkEditing]]]; |
|
David@0
|
1186 |
|
|
Evan@166
|
1187 |
for (NSMenuItem *menuItem in linkItemsMenu.itemArray) { |
|
David@0
|
1188 |
[contextualMenu insertItem:[[menuItem copy] autorelease] atIndex:editIndex++]; |
|
David@0
|
1189 |
} |
|
David@0
|
1190 |
|
|
David@0
|
1191 |
addedOurLinkItems = YES; |
|
David@0
|
1192 |
} |
|
David@0
|
1193 |
} else { |
|
David@0
|
1194 |
contextualMenu = [[[NSMenu alloc] init] autorelease]; |
|
David@0
|
1195 |
} |
|
David@0
|
1196 |
|
|
David@0
|
1197 |
//Retrieve the items which should be added to the bottom of the default menu |
|
David@0
|
1198 |
NSArray *locationArray = (addedOurLinkItems ? |
|
David@0
|
1199 |
[NSArray arrayWithObject:[NSNumber numberWithInt:Context_TextView_Edit]] : |
|
David@0
|
1200 |
[NSArray arrayWithObjects:[NSNumber numberWithInt:Context_TextView_LinkEditing], |
|
David@0
|
1201 |
[NSNumber numberWithInt:Context_TextView_Edit], nil]); |
|
David@433
|
1202 |
NSMenu *adiumMenu = [adium.menuController contextualMenuWithLocations:locationArray]; |
|
David@0
|
1203 |
itemsArray = [adiumMenu itemArray]; |
|
David@0
|
1204 |
|
|
David@0
|
1205 |
if ([itemsArray count] > 0) { |
|
David@0
|
1206 |
[contextualMenu addItem:[NSMenuItem separatorItem]]; |
|
David@0
|
1207 |
int i = [(NSMenu *)contextualMenu numberOfItems]; |
|
Evan@166
|
1208 |
for (NSMenuItem *menuItem in itemsArray) { |
|
David@0
|
1209 |
//We're going to be copying; call menu needs update now since it won't be called later. |
|
David@0
|
1210 |
NSMenu *submenu = [menuItem submenu]; |
|
David@0
|
1211 |
NSMenuItem *menuItemCopy = [[menuItem copy] autorelease]; |
|
David@0
|
1212 |
if (submenu && [submenu respondsToSelector:@selector(delegate)]) { |
|
David@0
|
1213 |
[[menuItemCopy submenu] setDelegate:[submenu delegate]]; |
|
David@0
|
1214 |
} |
|
David@0
|
1215 |
|
|
David@0
|
1216 |
[contextualMenu insertItem:menuItemCopy atIndex:i++]; |
|
David@0
|
1217 |
} |
|
David@0
|
1218 |
} |
|
David@0
|
1219 |
|
|
David@0
|
1220 |
return contextualMenu; |
|
David@0
|
1221 |
} |
|
David@0
|
1222 |
|
|
David@0
|
1223 |
#pragma mark Drag and drop |
|
David@0
|
1224 |
|
|
David@0
|
1225 |
/*An NSTextView which has setImportsGraphics:YES as of 10.5 gets the following drag types by default: |
|
David@0
|
1226 |
"NeXT RTFD pasteboard type", |
|
David@0
|
1227 |
"NeXT Rich Text Format v1.0 pasteboard type", |
|
David@0
|
1228 |
"Apple HTML pasteboard type", |
|
David@0
|
1229 |
NSFilenamesPboardType, |
|
David@0
|
1230 |
"CorePasteboardFlavorType 0x6D6F6F76", |
|
David@0
|
1231 |
"Apple PDF pasteboard type", |
|
David@0
|
1232 |
"NeXT TIFF v4.0 pasteboard type", |
|
David@0
|
1233 |
"Apple PICT pasteboard type", |
|
David@0
|
1234 |
"NeXT Encapsulated PostScript v1.2 pasteboard type", |
|
David@0
|
1235 |
"Apple PNG pasteboard type", |
|
David@0
|
1236 |
WebURLsWithTitlesPboardType, |
|
David@0
|
1237 |
"CorePasteboardFlavorType 0x75726C20", |
|
David@0
|
1238 |
"Apple URL pasteboard type", |
|
David@0
|
1239 |
NSStringPboardType, |
|
David@0
|
1240 |
"NSColor pasteboard type", |
|
David@0
|
1241 |
"NeXT font pasteboard type", |
|
David@0
|
1242 |
"NeXT ruler pasteboard type", |
|
David@0
|
1243 |
*/ |
|
David@0
|
1244 |
|
|
David@0
|
1245 |
- (NSArray *)acceptableDragTypes; |
|
David@0
|
1246 |
{ |
|
David@0
|
1247 |
NSMutableArray *dragTypes; |
|
David@0
|
1248 |
|
|
David@0
|
1249 |
dragTypes = [NSMutableArray arrayWithArray:[super acceptableDragTypes]]; |
|
David@0
|
1250 |
[dragTypes addObject:AIiTunesTrackPboardType]; |
|
David@0
|
1251 |
|
|
David@0
|
1252 |
return dragTypes; |
|
David@0
|
1253 |
} |
|
David@0
|
1254 |
|
|
David@0
|
1255 |
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender |
|
David@0
|
1256 |
{ |
|
David@0
|
1257 |
NSPasteboard *pasteboard = [sender draggingPasteboard]; |
|
David@0
|
1258 |
|
|
David@0
|
1259 |
if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES]) |
|
David@0
|
1260 |
return NSDragOperationCopy; |
|
David@0
|
1261 |
else |
|
David@0
|
1262 |
return [super draggingEntered:sender]; |
|
David@0
|
1263 |
} |
|
David@0
|
1264 |
|
|
David@0
|
1265 |
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender |
|
David@0
|
1266 |
{ |
|
David@0
|
1267 |
NSPasteboard *pasteboard = [sender draggingPasteboard]; |
|
David@0
|
1268 |
|
|
David@0
|
1269 |
if ([pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES]) |
|
David@0
|
1270 |
return NSDragOperationCopy; |
|
David@0
|
1271 |
else |
|
David@0
|
1272 |
return [super draggingUpdated:sender]; |
|
David@0
|
1273 |
} |
|
David@0
|
1274 |
|
|
David@0
|
1275 |
//We don't need to prepare for the types we are handling in performDragOperation: below |
|
David@0
|
1276 |
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender |
|
David@0
|
1277 |
{ |
|
David@0
|
1278 |
NSPasteboard *pasteboard = [sender draggingPasteboard]; |
|
David@0
|
1279 |
NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES]; |
|
David@0
|
1280 |
NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY]; |
|
David@0
|
1281 |
BOOL allowDragOperation; |
|
David@0
|
1282 |
|
|
David@0
|
1283 |
if (type && !superclassType) { |
|
David@0
|
1284 |
// XXX - This shouldn't let you insert into a view for which the delegate says NO to some sort of check. |
|
David@0
|
1285 |
allowDragOperation = YES; |
|
David@0
|
1286 |
} else { |
|
David@0
|
1287 |
allowDragOperation = [super prepareForDragOperation:sender]; |
|
David@0
|
1288 |
} |
|
David@0
|
1289 |
|
|
David@0
|
1290 |
return (allowDragOperation); |
|
David@0
|
1291 |
} |
|
David@0
|
1292 |
|
|
David@0
|
1293 |
//No conclusion is needed for the types we are handling in performDragOperation: below |
|
David@0
|
1294 |
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender |
|
David@0
|
1295 |
{ |
|
David@0
|
1296 |
NSPasteboard *pasteboard = [sender draggingPasteboard]; |
|
David@0
|
1297 |
NSString *type = [pasteboard availableTypeFromArray:FILES_AND_IMAGES_TYPES]; |
|
David@0
|
1298 |
NSString *superclassType = [pasteboard availableTypeFromArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY]; |
|
David@0
|
1299 |
|
|
David@0
|
1300 |
|
|
David@0
|
1301 |
|
|
David@0
|
1302 |
if (!type || superclassType) { |
|
David@0
|
1303 |
[super concludeDragOperation:sender]; |
|
David@0
|
1304 |
} |
|
David@0
|
1305 |
} |
|
David@0
|
1306 |
|
|
David@0
|
1307 |
- (void)addAttachmentsFromPasteboard:(NSPasteboard *)pasteboard |
|
David@0
|
1308 |
{ |
|
David@0
|
1309 |
NSString *availableType; |
|
David@0
|
1310 |
if ((availableType = [pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSFilenamesPboardType, AIiTunesTrackPboardType, nil]])) { |
|
David@0
|
1311 |
//The pasteboard points to one or more files on disc. Use them directly. |
|
David@0
|
1312 |
NSArray *files = nil; |
|
David@0
|
1313 |
if ([availableType isEqualToString:NSFilenamesPboardType]) { |
|
David@0
|
1314 |
files = [pasteboard propertyListForType:NSFilenamesPboardType]; |
|
David@0
|
1315 |
|
|
David@0
|
1316 |
} else if ([availableType isEqualToString:AIiTunesTrackPboardType]) { |
|
David@0
|
1317 |
files = [pasteboard filesFromITunesDragPasteboard]; |
|
David@0
|
1318 |
} |
|
David@0
|
1319 |
|
|
David@0
|
1320 |
NSString *path; |
|
David@75
|
1321 |
for (path in files) { |
|
David@0
|
1322 |
[self addAttachmentOfPath:path]; |
|
David@0
|
1323 |
} |
|
David@0
|
1324 |
|
|
David@0
|
1325 |
} else { |
|
David@0
|
1326 |
//The pasteboard contains image data with no corresponding file. |
|
David@0
|
1327 |
NSImage *image = [[NSImage alloc] initWithPasteboard:pasteboard]; |
|
David@0
|
1328 |
[self addAttachmentOfImage:image]; |
|
David@0
|
1329 |
[image release]; |
|
David@0
|
1330 |
} |
|
David@0
|
1331 |
} |
|
David@0
|
1332 |
|
|
David@0
|
1333 |
//The textView's method of inserting into the view is insufficient; we can do better. |
|
David@0
|
1334 |
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender |
|
David@0
|
1335 |
{ |
|
David@0
|
1336 |
NSPasteboard *pasteboard = [sender draggingPasteboard]; |
|
David@0
|
1337 |
BOOL success = NO; |
|
David@0
|
1338 |
|
|
David@0
|
1339 |
NSString *myType = [[pasteboard types] firstObjectCommonWithArray:FILES_AND_IMAGES_TYPES]; |
|
David@0
|
1340 |
NSString *superclassType = [[pasteboard types] firstObjectCommonWithArray:PASS_TO_SUPERCLASS_DRAG_TYPE_ARRAY]; |
|
David@0
|
1341 |
|
|
David@0
|
1342 |
if (myType && |
|
David@0
|
1343 |
(!superclassType || ([[pasteboard types] indexOfObject:myType] < [[pasteboard types] indexOfObject:superclassType]))) { |
|
David@0
|
1344 |
[self addAttachmentsFromPasteboard:pasteboard]; |
|
David@0
|
1345 |
|
|
David@0
|
1346 |
success = YES; |
|
David@0
|
1347 |
} else { |
|
David@0
|
1348 |
success = [super performDragOperation:sender]; |
|
David@0
|
1349 |
|
|
David@0
|
1350 |
} |
|
David@0
|
1351 |
|
|
David@0
|
1352 |
return success; |
|
David@0
|
1353 |
} |
|
David@0
|
1354 |
|
|
David@0
|
1355 |
#pragma mark Spell Checking |
|
David@0
|
1356 |
|
|
David@0
|
1357 |
/*! |
|
David@0
|
1358 |
* @brief Spell checking was toggled |
|
David@0
|
1359 |
* |
|
David@0
|
1360 |
* Set our preference, as we toggle spell checking globally when it is changed locally |
|
David@0
|
1361 |
*/ |
|
David@0
|
1362 |
- (void)toggleContinuousSpellChecking:(id)sender |
|
David@0
|
1363 |
{ |
|
David@0
|
1364 |
[super toggleContinuousSpellChecking:sender]; |
|
David@0
|
1365 |
|
|
David@95
|
1366 |
[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isContinuousSpellCheckingEnabled]] |
|
David@0
|
1367 |
forKey:KEY_SPELL_CHECKING |
|
David@0
|
1368 |
group:PREF_GROUP_DUAL_WINDOW_INTERFACE]; |
|
David@0
|
1369 |
} |
|
David@0
|
1370 |
|
|
David@0
|
1371 |
/*! |
|
David@0
|
1372 |
* @brief Grammar checking was toggled |
|
David@0
|
1373 |
* |
|
David@0
|
1374 |
* Set our preference, as we toggle grammar checking globally when it is changed locally |
|
David@0
|
1375 |
*/ |
|
David@0
|
1376 |
- (void)toggleGrammarChecking:(id)sender |
|
David@0
|
1377 |
{ |
|
David@0
|
1378 |
[super toggleGrammarChecking:sender]; |
|
David@0
|
1379 |
|
|
David@95
|
1380 |
[adium.preferenceController setPreference:[NSNumber numberWithBool:[self isGrammarCheckingEnabled]] |
|
David@0
|
1381 |
forKey:KEY_GRAMMAR_CHECKING |
|
David@0
|
1382 |
group:PREF_GROUP_DUAL_WINDOW_INTERFACE]; |
|
David@0
|
1383 |
} |
|
David@0
|
1384 |
|
|
David@0
|
1385 |
|
|
David@0
|
1386 |
#pragma mark Writing Direction |
|
David@0
|
1387 |
- (void)toggleBaseWritingDirection:(id)sender |
|
David@0
|
1388 |
{ |
|
David@0
|
1389 |
if ([self baseWritingDirection] == NSWritingDirectionRightToLeft) { |
|
David@0
|
1390 |
[self setBaseWritingDirection:NSWritingDirectionLeftToRight]; |
|
David@0
|
1391 |
} else { |
|
David@0
|
1392 |
[self setBaseWritingDirection:NSWritingDirectionRightToLeft]; |
|
David@0
|
1393 |
} |
|
David@0
|
1394 |
|
|
David@0
|
1395 |
//Apply it immediately |
|
David@0
|
1396 |
[self setBaseWritingDirection:[self baseWritingDirection] |
|
David@0
|
1397 |
range:NSMakeRange(0, [[self textStorage] length])]; |
|
David@0
|
1398 |
} |
|
David@0
|
1399 |
|
|
David@0
|
1400 |
#pragma mark Attachments |
|
David@0
|
1401 |
/*! |
|
David@0
|
1402 |
* @brief Add an attachment of the file at inPath at the current insertion point |
|
David@0
|
1403 |
* |
|
David@0
|
1404 |
* @param inPath The full path, whose contents will not be loaded into memory at this time |
|
David@0
|
1405 |
*/ |
|
David@0
|
1406 |
- (void)addAttachmentOfPath:(NSString *)inPath |
|
David@0
|
1407 |
{ |
|
David@0
|
1408 |
if ([[inPath pathExtension] caseInsensitiveCompare:@"textClipping"] == NSOrderedSame) { |
|
David@0
|
1409 |
inPath = [inPath stringByAppendingString:@"/..namedfork/rsrc"]; |
|
David@0
|
1410 |
|
|
David@0
|
1411 |
NSData *data = [NSData dataWithContentsOfFile:inPath]; |
|
David@0
|
1412 |
if (data) { |
|
David@0
|
1413 |
data = [data subdataWithRange:NSMakeRange(260, [data length] - 260)]; |
|
David@0
|
1414 |
|
|
David@0
|
1415 |
NSAttributedString *clipping = [[[NSAttributedString alloc] initWithRTF:data documentAttributes:nil] autorelease]; |
|
David@0
|
1416 |
if (clipping) { |
|
David@0
|
1417 |
NSDictionary *attributes = [[self typingAttributes] copy]; |
|
David@0
|
1418 |
|
|
David@0
|
1419 |
[self insertText:clipping]; |
|
David@0
|
1420 |
|
|
David@0
|
1421 |
if (attributes) { |
|
David@0
|
1422 |
[self setTypingAttributes:attributes]; |
|
David@0
|
1423 |
} |
|
David@0
|
1424 |
|
|
David@0
|
1425 |
[attributes release]; |
|
David@0
|
1426 |
} |
|
David@0
|
1427 |
} |
|
David@0
|
1428 |
|
|
David@0
|
1429 |
} else { |
|
David@0
|
1430 |
AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init]; |
|
David@0
|
1431 |
[attachment setPath:inPath]; |
|
David@0
|
1432 |
[attachment setString:[inPath lastPathComponent]]; |
|
David@0
|
1433 |
[attachment setShouldSaveImageForLogging:YES]; |
|
David@0
|
1434 |
|
|
David@0
|
1435 |
//Insert an attributed string into the text at the current insertion point |
|
David@0
|
1436 |
[self insertText:[self attributedStringWithTextAttachmentExtension:attachment]]; |
|
David@0
|
1437 |
|
|
David@0
|
1438 |
[attachment release]; |
|
David@0
|
1439 |
} |
|
David@0
|
1440 |
} |
|
David@0
|
1441 |
|
|
David@0
|
1442 |
/*! |
|
David@0
|
1443 |
* @brief Add an attachment of inImage at the current insertion point |
|
David@0
|
1444 |
*/ |
|
David@0
|
1445 |
- (void)addAttachmentOfImage:(NSImage *)inImage |
|
David@0
|
1446 |
{ |
|
David@0
|
1447 |
AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init]; |
|
David@0
|
1448 |
|
|
David@0
|
1449 |
[attachment setImage:inImage]; |
|
David@0
|
1450 |
[attachment setShouldSaveImageForLogging:YES]; |
|
David@0
|
1451 |
|
|
David@0
|
1452 |
//Insert an attributed string into the text at the current insertion point |
|
David@0
|
1453 |
[self insertText:[self attributedStringWithTextAttachmentExtension:attachment]]; |
|
David@0
|
1454 |
|
|
David@0
|
1455 |
[attachment release]; |
|
David@0
|
1456 |
} |
|
David@0
|
1457 |
|
|
David@0
|
1458 |
/*! |
|
David@0
|
1459 |
* @brief Generate an NSAttributedString which contains attachment and displays it using attachment's iconImage |
|
David@0
|
1460 |
*/ |
|
David@0
|
1461 |
- (NSAttributedString *)attributedStringWithTextAttachmentExtension:(AITextAttachmentExtension *)attachment |
|
David@0
|
1462 |
{ |
|
David@0
|
1463 |
NSTextAttachmentCell *cell = [[NSTextAttachmentCell alloc] initImageCell:[attachment iconImage]]; |
|
David@0
|
1464 |
|
|
David@0
|
1465 |
[attachment setHasAlternate:NO]; |
|
David@0
|
1466 |
[attachment setAttachmentCell:cell]; |
|
David@0
|
1467 |
[cell release]; |
|
David@0
|
1468 |
|
|
David@0
|
1469 |
return [NSAttributedString attributedStringWithAttachment:attachment]; |
|
David@0
|
1470 |
} |
|
David@0
|
1471 |
|
|
David@0
|
1472 |
/*! |
|
David@0
|
1473 |
* @brief Given RTFD data, return an NSAttributedString whose attachments are all AITextAttachmentExtension objects |
|
David@0
|
1474 |
*/ |
|
David@0
|
1475 |
- (NSAttributedString *)attributedStringWithAITextAttachmentExtensionsFromRTFDData:(NSData *)data |
|
David@0
|
1476 |
{ |
|
David@0
|
1477 |
NSMutableAttributedString *attributedString = [[[NSMutableAttributedString alloc] initWithRTFD:data |
|
David@0
|
1478 |
documentAttributes:NULL] autorelease]; |
|
David@0
|
1479 |
if ([attributedString length] && [attributedString containsAttachments]) { |
|
David@0
|
1480 |
int currentLocation = 0; |
|
David@0
|
1481 |
NSRange attachmentRange; |
|
David@0
|
1482 |
|
|
David@0
|
1483 |
NSString *attachmentCharacterString = [NSString stringWithFormat:@"%C",NSAttachmentCharacter]; |
|
David@0
|
1484 |
|
|
David@0
|
1485 |
//Find each attachment |
|
David@0
|
1486 |
attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString |
|
David@0
|
1487 |
options:0 |
|
David@0
|
1488 |
range:NSMakeRange(currentLocation, |
|
David@0
|
1489 |
[attributedString length] - currentLocation)]; |
|
David@0
|
1490 |
while (attachmentRange.length != 0) { |
|
David@0
|
1491 |
//Found an attachment in at attachmentRange.location |
|
David@0
|
1492 |
NSTextAttachment *attachment = [attributedString attribute:NSAttachmentAttributeName |
|
David@0
|
1493 |
atIndex:attachmentRange.location |
|
David@0
|
1494 |
effectiveRange:nil]; |
|
David@0
|
1495 |
|
|
David@0
|
1496 |
//If it's not already an AITextAttachmentExtension, make it into one |
|
David@0
|
1497 |
if (![attachment isKindOfClass:[AITextAttachmentExtension class]]) { |
|
David@0
|
1498 |
NSAttributedString *replacement; |
|
David@0
|
1499 |
NSFileWrapper *fileWrapper = [attachment fileWrapper]; |
|
David@0
|
1500 |
NSString *destinationPath; |
|
David@0
|
1501 |
NSString *preferredName = [fileWrapper preferredFilename]; |
|
David@0
|
1502 |
|
|
David@0
|
1503 |
//Get a unique folder within our temporary directory |
|
David@0
|
1504 |
destinationPath = [NSTemporaryDirectory() stringByAppendingPathComponent:[[NSProcessInfo processInfo] globallyUniqueString]]; |
|
David@472
|
1505 |
[[NSFileManager defaultManager] createDirectoryAtPath:destinationPath withIntermediateDirectories:YES attributes:nil error:NULL]; |
|
David@0
|
1506 |
destinationPath = [destinationPath stringByAppendingPathComponent:preferredName]; |
|
David@0
|
1507 |
|
|
David@0
|
1508 |
//Write the file out to it |
|
David@0
|
1509 |
[fileWrapper writeToFile:destinationPath |
|
David@0
|
1510 |
atomically:NO |
|
David@0
|
1511 |
updateFilenames:NO]; |
|
David@0
|
1512 |
|
|
David@0
|
1513 |
//Now create an AITextAttachmentExtension pointing to it |
|
David@0
|
1514 |
AITextAttachmentExtension *attachment = [[AITextAttachmentExtension alloc] init]; |
|
David@0
|
1515 |
[attachment setPath:destinationPath]; |
|
David@0
|
1516 |
[attachment setString:preferredName]; |
|
David@0
|
1517 |
[attachment setShouldSaveImageForLogging:YES]; |
|
David@0
|
1518 |
|
|
David@0
|
1519 |
//Insert an attributed string into the text at the current insertion point |
|
David@0
|
1520 |
replacement = [self attributedStringWithTextAttachmentExtension:attachment]; |
|
David@0
|
1521 |
[attachment release]; |
|
David@0
|
1522 |
|
|
David@0
|
1523 |
//Remove the NSTextAttachment, replacing it the AITextAttachmentExtension |
|
David@0
|
1524 |
[attributedString replaceCharactersInRange:attachmentRange |
|
David@0
|
1525 |
withAttributedString:replacement]; |
|
David@0
|
1526 |
|
|
David@0
|
1527 |
attachmentRange.length = [replacement length]; |
|
David@0
|
1528 |
} |
|
David@0
|
1529 |
|
|
David@0
|
1530 |
currentLocation = attachmentRange.location + attachmentRange.length; |
|
David@0
|
1531 |
|
|
David@0
|
1532 |
|
|
David@0
|
1533 |
//Find the next attachment |
|
David@0
|
1534 |
attachmentRange = [[attributedString string] rangeOfString:attachmentCharacterString |
|
David@0
|
1535 |
options:0 |
|
David@0
|
1536 |
range:NSMakeRange(currentLocation, |
|
David@0
|
1537 |
[attributedString length] - currentLocation)]; |
|
David@0
|
1538 |
} |
|
David@0
|
1539 |
} |
|
David@0
|
1540 |
|
|
David@0
|
1541 |
return attributedString; |
|
David@0
|
1542 |
} |
|
David@0
|
1543 |
|
|
David@0
|
1544 |
- (void)changeDocumentBackgroundColor:(id)sender |
|
David@0
|
1545 |
{ |
|
David@0
|
1546 |
NSColor *backgroundColor = [sender color]; |
|
David@0
|
1547 |
NSRange selectedRange = [self selectedRange]; |
|
David@0
|
1548 |
|
|
David@0
|
1549 |
[[self textStorage] addAttribute:NSBackgroundColorAttributeName |
|
David@0
|
1550 |
value:backgroundColor |
|
David@0
|
1551 |
range:selectedRange]; |
|
David@0
|
1552 |
[[self textStorage] addAttribute:AIBodyColorAttributeName |
|
David@0
|
1553 |
value:backgroundColor |
|
David@0
|
1554 |
range:selectedRange]; |
|
David@0
|
1555 |
|
|
David@0
|
1556 |
NSMutableDictionary *typingAttributes = [[self typingAttributes] mutableCopy]; |
|
David@0
|
1557 |
[typingAttributes setObject:backgroundColor forKey:AIBodyColorAttributeName]; |
|
David@0
|
1558 |
[typingAttributes setObject:backgroundColor forKey:NSBackgroundColorAttributeName]; |
|
David@0
|
1559 |
[self setTypingAttributes:typingAttributes]; |
|
David@0
|
1560 |
[typingAttributes release]; |
|
David@0
|
1561 |
|
|
David@0
|
1562 |
[[self textStorage] edited:NSTextStorageEditedAttributes |
|
David@0
|
1563 |
range:selectedRange |
|
David@0
|
1564 |
changeInLength:0]; |
|
David@0
|
1565 |
} |
|
David@0
|
1566 |
|
|
David@0
|
1567 |
- (void)insertText:(id)aString |
|
David@0
|
1568 |
{ |
|
David@0
|
1569 |
[super insertText:aString]; |
|
David@0
|
1570 |
// Auto set the writing direction based on our content |
|
David@0
|
1571 |
[self setBaseWritingDirection:[[[self textStorage] string] baseWritingDirection]]; |
|
David@0
|
1572 |
} |
|
David@0
|
1573 |
|
|
David@0
|
1574 |
@end |
|
David@0
|
1575 |
|
|
David@0
|
1576 |
@implementation NSMutableAttributedString (AIMessageEntryTextViewAdditions) |
|
David@0
|
1577 |
- (void)convertForPasteWithTraitsUsingAttributes:(NSDictionary *)typingAttributes; |
|
David@0
|
1578 |
{ |
|
David@0
|
1579 |
NSRange fullRange = NSMakeRange(0, [self length]); |
|
David@0
|
1580 |
|
|
David@0
|
1581 |
//Remove non-trait attributes |
|
David@0
|
1582 |
if ([typingAttributes objectForKey:NSBackgroundColorAttributeName]) { |
|
David@0
|
1583 |
[self addAttribute:NSBackgroundColorAttributeName |
|
David@0
|
1584 |
value:[typingAttributes objectForKey:NSBackgroundColorAttributeName] |
|
David@0
|
1585 |
range:fullRange]; |
|
David@0
|
1586 |
|
|
David@0
|
1587 |
} else { |
|
David@0
|
1588 |
[self removeAttribute:NSBackgroundColorAttributeName range:fullRange]; |
|
David@0
|
1589 |
} |
|
David@0
|
1590 |
|
|
David@0
|
1591 |
if ([typingAttributes objectForKey:NSForegroundColorAttributeName]) { |
|
David@0
|
1592 |
[self addAttribute:NSForegroundColorAttributeName |
|
David@0
|
1593 |
value:[typingAttributes objectForKey:NSForegroundColorAttributeName] |
|
David@0
|
1594 |
range:fullRange]; |
|
David@0
|
1595 |
|
|
David@0
|
1596 |
} else { |
|
David@0
|
1597 |
[self removeAttribute:NSForegroundColorAttributeName range:fullRange]; |
|
David@0
|
1598 |
} |
|
David@0
|
1599 |
|
|
David@0
|
1600 |
if ([typingAttributes objectForKey:NSParagraphStyleAttributeName]) { |
|
David@0
|
1601 |
[self addAttribute:NSParagraphStyleAttributeName |
|
David@0
|
1602 |
value:[typingAttributes objectForKey:NSParagraphStyleAttributeName] |
|
David@0
|
1603 |
range:fullRange]; |
|
David@0
|
1604 |
|
|
David@0
|
1605 |
} else { |
|
David@0
|
1606 |
[self removeAttribute:NSParagraphStyleAttributeName range:fullRange]; |
|
David@0
|
1607 |
} |
|
David@0
|
1608 |
|
|
David@0
|
1609 |
[self removeAttribute:NSBaselineOffsetAttributeName range:fullRange]; |
|
David@0
|
1610 |
[self removeAttribute:NSCursorAttributeName range:fullRange]; |
|
David@0
|
1611 |
[self removeAttribute:NSExpansionAttributeName range:fullRange]; |
|
David@0
|
1612 |
[self removeAttribute:NSKernAttributeName range:fullRange]; |
|
David@0
|
1613 |
[self removeAttribute:NSLigatureAttributeName range:fullRange]; |
|
David@0
|
1614 |
[self removeAttribute:NSObliquenessAttributeName range:fullRange]; |
|
David@0
|
1615 |
[self removeAttribute:NSShadowAttributeName range:fullRange]; |
|
David@0
|
1616 |
[self removeAttribute:NSStrokeWidthAttributeName range:fullRange]; |
|
David@0
|
1617 |
|
|
David@0
|
1618 |
NSRange searchRange = NSMakeRange(0, fullRange.length); |
|
David@0
|
1619 |
NSFontManager *fontManager = [NSFontManager sharedFontManager]; |
|
David@0
|
1620 |
NSFont *myFont = [typingAttributes objectForKey:NSFontAttributeName]; |
|
David@0
|
1621 |
|
|
David@0
|
1622 |
while (searchRange.location < fullRange.length) { |
|
David@0
|
1623 |
NSFont *font; |
|
David@0
|
1624 |
NSRange effectiveRange; |
|
David@0
|
1625 |
font = [self attribute:NSFontAttributeName |
|
David@0
|
1626 |
atIndex:searchRange.location |
|
David@0
|
1627 |
longestEffectiveRange:&effectiveRange |
|
David@0
|
1628 |
inRange:searchRange]; |
|
David@0
|
1629 |
|
|
David@0
|
1630 |
if (font) { |
|
David@0
|
1631 |
NSFontTraitMask thisFontTraits = [fontManager traitsOfFont:font]; |
|
David@0
|
1632 |
NSFontTraitMask traits = 0; |
|
David@0
|
1633 |
|
|
David@0
|
1634 |
if (thisFontTraits & NSBoldFontMask) { |
|
David@0
|
1635 |
traits |= NSBoldFontMask; |
|
David@0
|
1636 |
} else { |
|
David@0
|
1637 |
traits |= NSUnboldFontMask; |
|
David@0
|
1638 |
} |
|
David@0
|
1639 |
|
|
David@0
|
1640 |
if (thisFontTraits & NSItalicFontMask) { |
|
David@0
|
1641 |
traits |= NSItalicFontMask; |
|
David@0
|
1642 |
} else { |
|
David@0
|
1643 |
traits |= NSUnitalicFontMask; |
|
David@0
|
1644 |
} |
|
David@0
|
1645 |
|
|
David@0
|
1646 |
font = [fontManager fontWithFamily:[myFont familyName] |
|
David@0
|
1647 |
traits:traits |
|
David@0
|
1648 |
weight:[fontManager weightOfFont:myFont] |
|
David@0
|
1649 |
size:[myFont pointSize]]; |
|
David@0
|
1650 |
|
|
David@0
|
1651 |
if (font) { |
|
David@0
|
1652 |
[self addAttribute:NSFontAttributeName |
|
David@0
|
1653 |
value:font |
|
David@0
|
1654 |
range:effectiveRange]; |
|
David@0
|
1655 |
} |
|
David@0
|
1656 |
} |
|
David@0
|
1657 |
|
|
David@0
|
1658 |
searchRange.location = effectiveRange.location + effectiveRange.length; |
|
David@0
|
1659 |
searchRange.length = fullRange.length - searchRange.location; |
|
David@0
|
1660 |
} |
|
David@0
|
1661 |
|
|
David@0
|
1662 |
//Replace attachments with nothing! Absolutely nothing! |
|
David@0
|
1663 |
[self convertAttachmentsToStringsUsingPlaceholder:@""]; |
|
David@0
|
1664 |
} |
|
David@0
|
1665 |
|
|
David@0
|
1666 |
|
|
David@0
|
1667 |
|
|
David@0
|
1668 |
|
|
David@0
|
1669 |
@end |