Set the focus element for all elements when out of focus. Fixes #13356.
This also fixes setting the mark location correctly, as well as sets the first non-focused message as having class "firstFocus".
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIWebKitMessageViewController.h"
18 #import "AIWebKitMessageViewStyle.h"
19 #import "AIWebKitMessageViewPlugin.h"
20 #import "ESWebKitMessageViewPreferences.h"
21 #import "AIWebKitDelegate.h"
22 #import "ESFileTransferRequestPromptController.h"
24 #import <Adium/AIContactControllerProtocol.h>
25 #import <Adium/AIContentControllerProtocol.h>
26 #import <Adium/AIMenuControllerProtocol.h>
27 #import <Adium/AIFileTransferControllerProtocol.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIChat.h>
30 #import <Adium/AIContentTopic.h>
31 #import <Adium/AIContentContext.h>
32 #import <Adium/AIContentObject.h>
33 #import <Adium/AIContentEvent.h>
34 #import <Adium/AIEmoticon.h>
35 #import <Adium/AIListContact.h>
36 #import <Adium/AIMetaContact.h>
37 #import <Adium/AIListObject.h>
38 #import <Adium/AIService.h>
39 #import <Adium/ESFileTransfer.h>
40 #import <Adium/ESTextAndButtonsWindowController.h>
41 #import <Adium/AIHTMLDecoder.h>
42 #import <AIUtilities/AIArrayAdditions.h>
43 #import <AIUtilities/AIColorAdditions.h>
44 #import <AIUtilities/AIDateFormatterAdditions.h>
45 #import <AIUtilities/AIFileManagerAdditions.h>
46 #import <AIUtilities/AIImageAdditions.h>
47 #import <AIUtilities/AIMenuAdditions.h>
48 #import <AIUtilities/AIMutableStringAdditions.h>
49 #import <AIUtilities/AIPasteboardAdditions.h>
50 #import <AIUtilities/AIStringAdditions.h>
51 #import <AIUtilities/AIAttributedStringAdditions.h>
52 #import <AIUtilities/JVMarkedScroller.h>
53 #import <objc/objc-runtime.h>
55 #define KEY_WEBKIT_CHATS_USING_CACHED_ICON @"WebKit:Chats Using Cached Icon"
57 #define USE_FASTER_BUT_BUGGY_WEBKIT_PREFERENCE_CHANGE_HANDLING FALSE
59 #define TEMPORARY_FILE_PREFIX @"TEMP"
61 @interface AIWebKitMessageViewController ()
62 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin;
64 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent;
65 - (void)_updateWebViewForCurrentPreferences;
66 - (void)_updateVariantWithoutPrimingView;
67 - (void)processQueuedContent;
68 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects;
69 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent;
71 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID;
72 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject;
73 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject;
74 - (void)releaseAllCachedIcons;
75 - (void)updateUserIconForObject:(AIListObject *)inObject;
76 - (void)userIconForObjectDidChange:(AIListObject *)inObject;
77 - (void)updateServiceIcon;
80 - (void)participatingListObjectsChanged:(NSNotification *)notification;
81 - (void)sourceOrDestinationChanged:(NSNotification *)notification;
82 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard;
83 - (void)enqueueContentObject:(AIContentObject *)contentObject;
84 - (void)debugLog:(NSString *)message;
85 - (void)processQueuedContent;
86 - (NSString *)webviewSource;
87 - (void) setIsGroupChat:(BOOL) flag;
89 - (void)setupMarkedScroller;
90 - (JVMarkedScroller *)markedScroller;
91 - (NSNumber *)currentOffsetHeight;
92 - (void)markCurrentLocation;
95 @interface DOMDocument (FutureWebKitPublicMethodsIKnow)
96 - (DOMNodeList *)getElementsByClassName:(NSString *)className;
97 - (DOMNodeList *)querySelectorAll:(NSString *)selectors; // We require 10.5.8/Safari 4, all is well!
100 static NSArray *draggedTypes = nil;
102 @implementation AIWebKitMessageViewController
104 + (AIWebKitMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
106 return [[[self alloc] initForChat:inChat withPlugin:inPlugin] autorelease];
109 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
112 if ((self = [super init])) {
115 delegateProxy = [AIWebKitDelegate sharedWebKitDelegate];
117 chat = [inChat retain];
118 plugin = [inPlugin retain];
119 contentQueue = [[NSMutableArray alloc] init];
120 objectIconPathDict = [[NSMutableDictionary alloc] init];
121 objectsWithUserIconsArray = [[NSMutableArray alloc] init];
122 shouldReflectPreferenceChanges = NO;
123 storedContentObjects = nil;
125 //Observe preference changes.
126 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_REGULAR_MESSAGE_DISPLAY];
127 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_GROUP_MESSAGE_DISPLAY];
128 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
130 //Set ourselves up initially.
131 [self _updateWebViewForCurrentPreferences];
133 //Observe participants list changes
134 [[NSNotificationCenter defaultCenter] addObserver:self
135 selector:@selector(participatingListObjectsChanged:)
136 name:Chat_ParticipatingListObjectsChanged
139 //Observe source/destination changes
140 [[NSNotificationCenter defaultCenter] addObserver:self
141 selector:@selector(sourceOrDestinationChanged:)
142 name:Chat_SourceChanged
144 [[NSNotificationCenter defaultCenter] addObserver:self
145 selector:@selector(sourceOrDestinationChanged:)
146 name:Chat_DestinationChanged
149 //Observe content additons
150 [[NSNotificationCenter defaultCenter] addObserver:self
151 selector:@selector(contentObjectAdded:)
152 name:Content_ContentObjectAdded
154 [[NSNotificationCenter defaultCenter] addObserver:self
155 selector:@selector(chatDidFinishAddingUntrackedContent:)
156 name:Content_ChatDidFinishAddingUntrackedContent
159 [[NSNotificationCenter defaultCenter] addObserver:self
160 selector:@selector(customEmoticonUpdated:)
161 name:@"AICustomEmoticonUpdated"
168 - (void)messageViewIsClosing
170 [webView stopLoading:nil];
172 //Stop observing the webview, since it may attempt callbacks shortly after we dealloc
173 [delegateProxy removeDelegate:self];
175 /* The windowScriptObject retained self when we set it as the client in -[AIWebKitMessageViewController _initWebView]...
176 * Unfortunately, (as of 10.4.9) it won't actually release self until the webView deallocates. We'll do removeWebScriptKey:
177 * now in case that works properly later, and do the release of webView here rather than in dealloc to work around the bug.
179 [[webView windowScriptObject] removeWebScriptKey:@"client"];
181 //Release the web view
182 [webView release]; webView = nil;
190 [self releaseAllCachedIcons];
192 [plugin release]; plugin = nil;
193 [objectsWithUserIconsArray release]; objectsWithUserIconsArray = nil;
194 [objectIconPathDict release]; objectIconPathDict = nil;
196 //Stop any delayed requests and remove all observers
197 [NSObject cancelPreviousPerformRequestsWithTarget:self];
198 [adium.preferenceController unregisterPreferenceObserver:self];
199 [[NSNotificationCenter defaultCenter] removeObserver:self];
201 //Clean up style/variant info
202 [messageStyle release]; messageStyle = nil;
203 [activeStyle release]; activeStyle = nil;
204 [activeVariant release]; activeVariant = nil;
205 [preferenceGroup release]; preferenceGroup = nil;
207 //Cleanup content processing
208 [contentQueue release]; contentQueue = nil;
209 [storedContentObjects release]; storedContentObjects = nil;
210 [previousContent release]; previousContent = nil;
213 [chat release]; chat = nil;
215 //Release the marked scroller
216 [self.markedScroller release];
221 - (void)setShouldReflectPreferenceChanges:(BOOL)inValue
223 shouldReflectPreferenceChanges = inValue;
225 //We'll want to start storing content objects if we're needing to reflect preference changes
226 if (shouldReflectPreferenceChanges) {
227 if (!storedContentObjects) {
228 storedContentObjects = [[NSMutableArray alloc] init];
231 [storedContentObjects release]; storedContentObjects = nil;
235 - (void)adiumPrint:(id)sender
237 WebPreferences* prefs = [webView preferences];
238 [prefs setShouldPrintBackgrounds:YES];
240 [[[[webView mainFrame] frameView] documentView] print:sender];
243 //WebView --------------------------------------------------------------------------------------------------
246 @synthesize messageStyle, messageView = webView;
248 - (NSView *)messageScrollView
250 return [[webView mainFrame] frameView];
254 * @brief Apply preference changes to our webview
256 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
257 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
259 // First time won't occur because preferenceGroup is not yet set. Don't run on the assumption that it is, end early.
260 if (!preferenceGroup)
263 if ([group isEqualToString:preferenceGroup]) {
264 #if USE_FASTER_BUT_BUGGY_WEBKIT_PREFERENCE_CHANGE_HANDLING
265 NSString *variantKey = [plugin styleSpecificKey:@"Variant" forStyle:activeStyle];
266 //Variant changes we can apply immediately. All other changes require us to reload the view
267 if (!firstTime && [key isEqualToString:variantKey]) {
268 [activeVariant release]; activeVariant = [[prefDict objectForKey:variantKey] retain];
269 [self _updateVariantWithoutPrimingView];
271 } else if (shouldReflectPreferenceChanges) {
272 //Ignore changes related to our background image cache. These keys are used for storage only and aren't
273 //something we need to update in response to. All other display changes we update our view for.
274 if (![key isEqualToString:@"BackgroundCacheUniqueID"] &&
275 ![key isEqualToString:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]] &&
276 ![key isEqualToString:KEY_CURRENT_WEBKIT_STYLE_PATH]) {
277 [self _updateWebViewForCurrentPreferences];
281 if (shouldReflectPreferenceChanges) {
282 //Ignore changes related to our background image cache. These keys are used for storage only and aren't
283 //something we need to update in response to. All other display changes we update our view for.
284 if (![key isEqualToString:@"BackgroundCacheUniqueID"] &&
285 ![key isEqualToString:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]] &&
286 (![key isEqualToString:KEY_CURRENT_WEBKIT_STYLE_PATH] || shouldReflectPreferenceChanges)) {
287 if (!isUpdatingWebViewForCurrentPreferences) {
288 isUpdatingWebViewForCurrentPreferences = YES;
289 [self _updateWebViewForCurrentPreferences];
290 isUpdatingWebViewForCurrentPreferences = NO;
297 if (([group isEqualToString:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES] && shouldReflectPreferenceChanges)) {
298 //If the background image changes, wipe the cache and update for the new image
299 [adium.preferenceController setPreference:nil
300 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
301 group:preferenceGroup];
302 if (!isUpdatingWebViewForCurrentPreferences) {
303 isUpdatingWebViewForCurrentPreferences = YES;
304 [self _updateWebViewForCurrentPreferences];
305 isUpdatingWebViewForCurrentPreferences = NO;
311 * @brief Initialiaze the web view
316 webView = [[ESWebView alloc] initWithFrame:NSMakeRect(0,0,100,100) //Arbitrary frame
319 [webView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
320 [delegateProxy addDelegate:self forView:webView];
321 [webView setMaintainsBackForwardList:NO];
324 draggedTypes = [[NSArray alloc] initWithObjects:
325 NSFilenamesPboardType,
326 AIiTunesTrackPboardType,
331 NSFileContentsPboardType,
334 NSPostScriptPboardType,
337 [webView registerForDraggedTypes:draggedTypes];
341 * @brief Updates our webview to the current preferences, priming the view
343 - (void)_updateWebViewForCurrentPreferences
346 [messageStyle autorelease]; messageStyle = nil;
347 [activeStyle release]; activeStyle = nil;
348 [activeVariant release]; activeVariant = nil;
350 //Load the message style
351 messageStyle = [[plugin currentMessageStyleForChat:chat] retain];
352 activeStyle = [[[messageStyle bundle] bundleIdentifier] retain];
353 preferenceGroup = [[plugin preferenceGroupForChat:chat] retain];
355 [webView setPreferencesIdentifier:activeStyle];
357 //Get the prefered variant (or the default if a prefered is not available)
358 activeVariant = [[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Variant" forStyle:activeStyle]
359 group:preferenceGroup] retain];
360 if (!activeVariant) activeVariant = [[messageStyle defaultVariant] retain];
361 if (!activeVariant) {
362 /* If the message style doesn't specify a default variant, choose the first one.
363 * Note: Old styles (styleVersion < 3) will always report a variant for defaultVariant.
365 NSArray *availableVariants = [messageStyle availableVariants];
366 if ([availableVariants count]) {
367 activeVariant = [[availableVariants objectAtIndex:0] retain];
371 NSDictionary *prefDict = [adium.preferenceController preferencesForGroup:preferenceGroup];
373 //Update message style behavior: XXX move this somewhere not per-chat
374 [messageStyle setShowUserIcons:[[prefDict objectForKey:KEY_WEBKIT_SHOW_USER_ICONS] boolValue]];
375 [messageStyle setShowHeader:[[prefDict objectForKey:KEY_WEBKIT_SHOW_HEADER] boolValue]];
376 [messageStyle setUseCustomNameFormat:[[prefDict objectForKey:KEY_WEBKIT_USE_NAME_FORMAT] boolValue]];
377 [messageStyle setNameFormat:[[prefDict objectForKey:KEY_WEBKIT_NAME_FORMAT] integerValue]];
378 [messageStyle setDateFormat:[prefDict objectForKey:KEY_WEBKIT_TIME_STAMP_FORMAT]];
379 [messageStyle setShowIncomingMessageColors:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_COLORS] boolValue]];
380 [messageStyle setShowIncomingMessageFonts:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_FONTS] boolValue]];
382 //Custom background image
383 //Webkit wants to load these from disk, but we have it stuffed in a plist. So we'll write it out as an image
384 //into the cache and have webkit fetch from there.
385 NSString *cachePath = nil;
386 if ([[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"UseCustomBackground" forStyle:activeStyle]
387 group:preferenceGroup] boolValue]) {
388 cachePath = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
389 group:preferenceGroup];
390 if (!cachePath || ![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
391 NSData *backgroundImage = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Background" forStyle:activeStyle]
392 group:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
394 if (backgroundImage) {
395 //Generate a unique cache ID for this image
396 NSInteger uniqueID = [[adium.preferenceController preferenceForKey:@"BackgroundCacheUniqueID"
397 group:preferenceGroup] integerValue] + 1;
398 [adium.preferenceController setPreference:[NSNumber numberWithInteger:uniqueID]
399 forKey:@"BackgroundCacheUniqueID"
400 group:preferenceGroup];
402 //Cache the image under that unique ID
403 //Since we prefix the filename with TEMP, Adium will automatically clean it up on quit
404 cachePath = [self _webKitBackgroundImagePathForUniqueID:uniqueID];
405 [backgroundImage writeToFile:cachePath atomically:YES];
407 //Remember where we cached it
408 [adium.preferenceController setPreference:cachePath
409 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
410 group:preferenceGroup];
412 cachePath = @""; //No custom image found
416 [messageStyle setCustomBackgroundColor:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundColor" forStyle:activeStyle]
417 group:preferenceGroup] representedColor]];
419 [messageStyle setCustomBackgroundColor:nil];
422 [messageStyle setCustomBackgroundPath:cachePath];
423 [messageStyle setCustomBackgroundType:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundType" forStyle:activeStyle]
424 group:preferenceGroup] integerValue]];
426 BOOL isBackgroundTransparent = [[self messageStyle] isBackgroundTransparent];
427 [webView setTransparent:isBackgroundTransparent];
428 NSWindow *win = [webView window];
430 [win setOpaque:!isBackgroundTransparent];
432 //Update webview font settings
433 NSString *fontFamily = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontFamily" forStyle:activeStyle]
434 group:preferenceGroup];
435 [webView setFontFamily:(fontFamily ? fontFamily : [messageStyle defaultFontFamily])];
437 NSNumber *fontSize = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontSize" forStyle:activeStyle]
438 group:preferenceGroup];
439 [[webView preferences] setDefaultFontSize:[(fontSize ? fontSize : [messageStyle defaultFontSize]) integerValue]];
441 NSNumber *minSize = [adium.preferenceController preferenceForKey:KEY_WEBKIT_MIN_FONT_SIZE
442 group:preferenceGroup];
443 [[webView preferences] setMinimumFontSize:(minSize ? [minSize integerValue] : 1)];
445 //Update our icons before doing any loading
446 [self sourceOrDestinationChanged:nil];
448 //Prime the webview with the new style/variant and settings, and re-insert all our content back into the view
449 [self _primeWebViewAndReprocessContent:YES];
453 * @brief Updates our webview to the currently active varient without refreshing the view
455 - (void)_updateVariantWithoutPrimingView
457 //We can only change the variant if the web view is ready. If it's not ready we wait a bit and try again.
458 if (webViewIsReady) {
459 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForChangingVariant:activeVariant]];
461 [self performSelector:@selector(_updateVariantWithoutPrimingView) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
466 * @brief Clears the view from displayed messages
468 * Implements the method defined in protocol AIMessageDisplayController
472 [self _primeWebViewAndReprocessContent:NO];
473 [self.markedScroller removeAllMarks];
474 [previousContent release];
475 previousContent = nil;
476 nextMessageFocus = NO;
477 [chat clearUnviewedContentCount];
481 * @brief Primes our webview to the currently active style and variant
483 * The webview won't be ready right away, so we flag it as not ready and set ourself as the frame load delegate so
484 * it will let us know when it's good to go. If reprocessContent is NO, all content in the view will be lost.
486 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent
490 //Hack: this will re-set us for all the delegates, but that shouldn't matter
491 [delegateProxy addDelegate:self forView:webView];
492 [[webView mainFrame] loadHTMLString:[messageStyle baseTemplateWithVariant:activeVariant chat:chat] baseURL:nil];
494 if(chat.isGroupChat && chat.supportsTopic) {
495 // Force a topic update, so we set our topic appropriately.
499 if (reprocessContent) {
500 NSArray *currentContentQueue;
502 //Keep the array of objects waiting to be added, if necessary, to append them after our currently displayed ones
503 currentContentQueue = ([contentQueue count] ?
504 [contentQueue copy] :
507 //Start from an empty content queue
508 [contentQueue removeAllObjects];
510 //Add our stored content objects to the content queue
511 [contentQueue addObjectsFromArray:storedContentObjects];
512 [storedContentObjects removeAllObjects];
514 //Add the old content queue back in if necessary
515 if (currentContentQueue) {
516 [contentQueue addObjectsFromArray:currentContentQueue];
517 [currentContentQueue release];
520 //We're still holding onto the previousContent from before, which is no longer accurate. Release it.
521 [previousContent release]; previousContent = nil;
526 * @brief Sets the class 'groupchat' on the #Chat element, to allow styles to modify their appearance based on whether we're in a groupchat
528 * If/when we support transforming chats to/from groupchats we'll need to observe that and call this as appropriate
530 - (void) setIsGroupChat:(BOOL) flag
532 DOMHTMLElement *chatElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"];
533 NSMutableString *chatClassName = [[[chatElement className] mutableCopy] autorelease];
535 [chatClassName replaceOccurrencesOfString:@" groupchat"
537 options:NSLiteralSearch
538 range:NSMakeRange(0, [chatClassName length])];
540 [chatClassName appendString:@" groupchat"];
541 [chatElement setClassName:chatClassName];
544 //Content --------------------------------------------------------------------------------------------------------------
547 * @brief Append new content to our processing queue
549 - (void)contentObjectAdded:(NSNotification *)notification
551 AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
552 [self enqueueContentObject:contentObject];
555 - (void)enqueueContentObject:(AIContentObject *)contentObject
557 [contentQueue addObject:contentObject];
559 /* Immediately update our display if the content requires it.
560 * This is NO, for example, when we receive an entire block of message history content so that we can avoid scrolling
563 if ([contentObject displayContentImmediately]) {
564 [self processQueuedContent];
569 * @brief Our chat finished adding untracked content
571 - (void)chatDidFinishAddingUntrackedContent:(NSNotification *)notification
573 [self processQueuedContent];
577 * @brief Append new content to our processing queueProcess any content in the queuee
579 - (void)processQueuedContent
581 /* If the webview isn't ready, assume we have at least one piece of content left to display */
582 NSUInteger contentQueueCount = 1;
583 NSUInteger objectsAdded = 0;
585 if (webViewIsReady) {
586 contentQueueCount = contentQueue.count;
588 while (contentQueueCount > 0) {
589 BOOL willAddMoreContent = (contentQueueCount > 1);
591 //Display the content
592 AIContentObject *content = [contentQueue objectAtIndex:0];
593 [self _processContentObject:content
594 willAddMoreContentObjects:willAddMoreContent];
596 //If we are going to reflect preference changes, store this content object
597 if (shouldReflectPreferenceChanges) {
598 [storedContentObjects addObject:content];
601 //Remove the content we just displayed from the queue
602 [contentQueue removeObjectAtIndex:0];
608 /* If we added two or more objects, we may want to scroll to the bottom now, having not done it as each object
611 if (objectsAdded > 1) {
612 NSString *scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects];
614 if (scrollToBottomScript) {
615 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
619 //If there is still content to process (the webview wasn't ready), we'll try again after a brief delay
620 if (contentQueueCount) {
621 [self performSelector:@selector(processQueuedContent) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
626 * @brief Process and then append a content object
628 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects
630 AIContentEvent *dateSeparator = nil;
631 BOOL replaceLastContent = NO;
634 If the day has changed since our last message (or if there was no previous message and
635 we are about to display context), insert a date line.
637 if ((!previousContent && [content isKindOfClass:[AIContentContext class]]) ||
638 (![content isFromSameDayAsContent:previousContent])) {
640 NSString *dateMessage = [[NSDateFormatter localizedDateFormatter] stringFromDate:content.date];
642 dateSeparator = [AIContentEvent statusInChat:content.chat
643 withSource:content.chat.listObject
644 destination:content.chat.account
646 message:[[[NSAttributedString alloc] initWithString:dateMessage
647 attributes:[adium.contentController defaultFormattingAttributes]] autorelease]
648 withType:@"date_separator"];
650 if ([content isKindOfClass:[AIContentContext class]])
651 [dateSeparator addDisplayClass:@"history"];
653 //Add the date header
654 [self _appendContent:dateSeparator
656 willAddMoreContentObjects:YES
657 replaceLastContent:NO];
658 [previousContent release]; previousContent = [dateSeparator retain];
661 BOOL similar = (previousContent && [content isSimilarToContent:previousContent] && ![content isKindOfClass:[ESFileTransfer class]]);
662 if ([previousContent isKindOfClass:[AIContentStatus class]] && [content isKindOfClass:[AIContentStatus class]] &&
663 [[(AIContentStatus *)previousContent coalescingKey] isEqualToString:[(AIContentStatus *)content coalescingKey]]) {
665 replaceLastContent = YES;
668 if ([content.type isEqualToString:CONTENT_TOPIC_TYPE]) {
669 DOMHTMLElement *topicElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topic"];
671 if (((AIContentTopic *)content).actuallyBlank) {
672 content.message = [NSAttributedString stringWithString:@""];
675 [topicElement setTitle:content.message.string];
677 [topicElement setInnerHTML:[messageStyle completedTemplateForContent:content similar:similar]];
679 // Mark the current location (the start of this element) if it's a mention.
680 if (content.trackContent && [content.displayClasses containsObject:@"mention"]) {
681 [self markCurrentLocation];
684 if (adium.interfaceController.activeChat != content.chat && [content.type isEqualToString:CONTENT_MESSAGE_TYPE]) {
685 if (nextMessageFocus) {
686 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
688 // Add a class for "first message to lose focus"
689 [content addDisplayClass:@"firstFocus"];
691 nextMessageFocus = NO;
694 // Add a class for "this message received while out of focus"
695 [content addDisplayClass:@"focus"];
698 //Add the content object
699 [self _appendContent:content
701 willAddMoreContentObjects:willAddMoreContentObjects
702 replaceLastContent:replaceLastContent];
705 [previousContent release]; previousContent = [content retain];
709 * @brief Append a content object
711 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
713 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
714 similar:contentIsSimilar
715 willAddMoreContentObjects:willAddMoreContentObjects
716 replaceLastContent:replaceLastContent]];
718 NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
723 * @brief Force a topic update.
725 * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
729 NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
731 AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
732 withSource:chat.topicSetter
737 // In case this topic is blank, we have to filter this ourself; the content controller will drop it.
738 contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
740 [self enqueueContentObject:contentTopic];
743 //WebView Delegates ----------------------------------------------------------------------------------------------------
744 #pragma mark Webview delegates
746 - (void)webViewIsReady{
747 webViewIsReady = YES;
748 [self setupMarkedScroller];
749 [self setIsGroupChat:chat.isGroupChat];
750 [self processQueuedContent];
753 - (void)openImage:(id)sender
755 NSURL *imageURL = [sender representedObject];
756 [[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
759 - (void)saveImageAs:(id)sender
761 NSURL *imageURL = [sender representedObject];
762 NSString *path = [imageURL path];
764 NSSavePanel *savePanel = [NSSavePanel savePanel];
765 [savePanel beginSheetForDirectory:nil
766 file:[path lastPathComponent]
767 modalForWindow:[webView window]
769 didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
770 contextInfo:[imageURL retain]];
773 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
775 NSURL *imageURL = (NSURL *)contextInfo;
777 if (returnCode == NSOKButton) {
778 [[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
779 toPath:[[sheet URL] absoluteString]
787 * @brief Append our own menu items to the webview's contextual menus
789 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
791 NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
792 AIListContact *chatListObject = chat.listObject.parentContact;
793 NSMenuItem *menuItem;
795 //Remove default items we don't want
796 if (webViewMenuItems) {
798 for (menuItem in defaultMenuItems) {
799 NSInteger tag = [menuItem tag];
800 if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
801 (tag == WebMenuItemTagDownloadLinkToDisk) ||
802 (tag == WebMenuItemTagOpenImageInNewWindow) ||
803 (tag == WebMenuItemTagDownloadImageToDisk) ||
804 (tag == WebMenuItemTagOpenFrameInNewWindow) ||
805 (tag == WebMenuItemTagStop) ||
806 (tag == WebMenuItemTagReload)) {
807 [webViewMenuItems removeObjectIdenticalTo:menuItem];
809 //This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
810 if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
811 [webViewMenuItems removeObjectIdenticalTo:menuItem];
817 if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
819 if (!webViewMenuItems) {
820 webViewMenuItems = [NSMutableArray array];
823 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
825 action:@selector(openImage:)
827 representedObject:imageURL];
828 [webViewMenuItems addObject:menuItem];
830 menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
832 action:@selector(saveImageAs:)
834 representedObject:imageURL];
835 [webViewMenuItems addObject:menuItem];
839 NSString *imgClass = [img className];
840 //being very careful to only get user icons... a better way would be to put a class "usericon" on the img, but I haven't worked out how to do that, so we test for the name of the person in the src, and that it's not an emoticon or direct connect image.
841 if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
842 [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
843 [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
844 [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
849 if (webViewMenuItems) {
850 //Add a separator item if items already exist in webViewMenuItems
851 if ([webViewMenuItems count]) {
852 // If the first item is a separator item, remove it.
853 if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
854 [webViewMenuItems removeObjectAtIndex:0];
857 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
860 webViewMenuItems = [NSMutableArray array];
863 NSMenu *originalMenu = nil;
865 if (chatListObject) {
867 if ([chatListObject isIntentionallyNotAStranger]) {
868 locations = [NSArray arrayWithObjects:
869 [NSNumber numberWithInteger:Context_Contact_Manage],
870 [NSNumber numberWithInteger:Context_Contact_Action],
871 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
872 [NSNumber numberWithInteger:Context_Contact_ChatAction],
873 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
875 locations = [NSArray arrayWithObjects:
876 [NSNumber numberWithInteger:Context_Contact_Manage],
877 [NSNumber numberWithInteger:Context_Contact_Action],
878 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
879 [NSNumber numberWithInteger:Context_Contact_ChatAction],
880 [NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
881 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
884 originalMenu = [adium.menuController contextualMenuWithLocations:locations
885 forListObject:chatListObject
887 } else if(chat.isGroupChat) {
888 originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
889 [NSNumber numberWithInteger:Context_GroupChat_Manage],
890 [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
894 [webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
895 [originalMenu removeAllItems];
897 if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
898 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
900 //Present an option to clear the display
901 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
903 action:@selector(clearView)
905 [webViewMenuItems addObject:menuItem];
908 return webViewMenuItems;
912 * @brief Add ourself to the window script object bridge when it's safe to do so
914 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
916 [[webView windowScriptObject] setValue:self forKey:@"client"];
919 //Dragging delegate ----------------------------------------------------------------------------------------------------
920 #pragma mark Dragging delegate
922 * @brief If possible, return the first NSTextView in the message view's responder chain
924 * This is used for drag and drop behavior.
926 - (NSTextView *)textView
928 id responder = [webView nextResponder];
930 //Walkin the responder chain looking for an NSTextView
932 ![responder isKindOfClass:[NSTextView class]]) {
933 responder = [responder nextResponder];
940 * @brief Dragging entered
942 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
944 NSPasteboard *pasteboard = [sender draggingPasteboard];
946 return ([pasteboard availableTypeFromArray:draggedTypes] ?
947 NSDragOperationCopy :
948 NSDragOperationNone);
952 * @brief Dragging updated
954 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
956 return [self draggingEntered:sender];
960 * @brief Handle a drag onto the webview
962 * If we're getting a non-image file, we can handle it immediately. Otherwise, the drag is the textView's problem.
964 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
966 NSPasteboard *pasteboard = [sender draggingPasteboard];
969 if ([self shouldHandleDragWithPasteboard:pasteboard]) {
971 //Not an image but it is a file - send it immediately as a file transfer
972 NSArray *files = [pasteboard propertyListForType:NSFilenamesPboardType];
974 for (path in files) {
975 AIListObject *listObject = chat.listObject;
977 [adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
983 NSTextView *textView = [self textView];
985 [[webView window] makeFirstResponder:textView]; //Make it first responder
986 success = [textView performDragOperation:sender];
994 * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
996 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
998 NSPasteboard *pasteboard = [sender draggingPasteboard];
1001 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
1002 NSTextView *textView = [self textView];
1004 success = [textView prepareForDragOperation:sender];
1012 * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
1014 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1016 NSPasteboard *pasteboard = [sender draggingPasteboard];
1018 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
1019 NSTextView *textView = [self textView];
1021 [textView concludeDragOperation:sender];
1027 * @brief Handle drags of content we recognize
1029 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
1032 return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
1033 [pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
1039 //User Icon masking --------------------------------------------------------------------------------------------------
1040 //We allow messaage styles to specify masks for user icons. This could be user to round the corners of user icons
1041 //or other related effects.
1042 #pragma mark User icon masking
1044 * @brief Update icon masks when participating list objects change
1046 * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
1047 * When the list changes, remove the observers we had in place before and add observers for each object in the list
1048 * so we never observe for contacts not in the chat.
1050 - (void)participatingListObjectsChanged:(NSNotification *)notification
1052 NSArray *participatingListObjects = [chat containedObjects];
1054 [[NSNotificationCenter defaultCenter] removeObserver:self
1055 name:ListObject_AttributesChanged
1058 for (AIListObject *listObject in participatingListObjects) {
1059 //Update the mask for any user which just entered the chat
1060 if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
1061 [self updateUserIconForObject:listObject];
1064 //In the future, watch for changes on the parent object, since that's the icon we display
1065 if ([listObject isKindOfClass:[AIListContact class]]) {
1066 [[NSNotificationCenter defaultCenter] addObserver:self
1067 selector:@selector(listObjectAttributesChanged:)
1068 name:ListObject_AttributesChanged
1069 object:[(AIListContact *)listObject parentContact]];
1073 //Also observe our account
1075 [[NSNotificationCenter defaultCenter] addObserver:self
1076 selector:@selector(listObjectAttributesChanged:)
1077 name:ListObject_AttributesChanged
1078 object:chat.account];
1081 //Remove the cache for any object no longer in the chat
1082 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1083 if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
1084 (![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
1085 !(listObject == chat.account)) {
1086 [self releaseCurrentWebKitUserIconForObject:listObject];
1092 * @brief Update icon masks when source or destination changes
1094 - (void)sourceOrDestinationChanged:(NSNotification *)notification
1096 //Update the participating contacts
1097 [self participatingListObjectsChanged:nil];
1099 //And update the source account
1100 [self updateUserIconForObject:chat.account];
1102 [self updateServiceIcon];
1106 * @brief Update the icon when a list object's icon attributes change
1108 - (void)listObjectAttributesChanged:(NSNotification *)notification
1110 AIListObject *inObject = [notification object];
1111 NSSet *keys = [[notification userInfo] objectForKey:@"Keys"];
1113 if ([keys containsObject:KEY_USER_ICON]) {
1115 AIListObject *actualObject = nil;
1116 AILogWithSignature(@"%@'s icon changed", inObject);
1117 if (chat.account == inObject) {
1118 //The account is the object actually in the chat
1119 actualObject = inObject;
1122 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
1123 * be displaying as changed.
1126 for (AIListContact *participatingListObject in chat) {
1127 if ([participatingListObject parentContact] == inObject) {
1128 actualObject = participatingListObject;
1135 [self userIconForObjectDidChange:actualObject];
1139 AILogWithSignature(@"nil object's icon changed");
1140 //We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
1141 [self sourceOrDestinationChanged:nil];
1146 - (void)userIconForObjectDidChange:(AIListObject *)inObject
1148 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1149 [(AIListContact *)inObject parentContact] :
1151 NSString *currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1152 if (currentIconPath) {
1153 NSString *objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1154 if (objectsKnownIconPath &&
1155 [currentIconPath isEqualToString:objectsKnownIconPath]) {
1156 //We're the first one to get to this object! We get to delete the old path and remove the reference to it
1157 [[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
1158 [iconSourceObject setValue:nil
1159 forProperty:KEY_WEBKIT_USER_ICON
1160 notify:NotifyNever];
1162 /* Some other instance beat us to the punch. The object's KEY_WEBKIT_USER_ICON is right, since it doesn't match our
1163 * internally tracked path.
1168 [self updateUserIconForObject:iconSourceObject];
1172 * @brief Remove all references to *this* chat using cached icons for an object
1174 * If this is the last chat utilizing the cached icon, it will be deleted.
1176 * @param inObject The object
1178 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
1180 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1181 [(AIListContact *)inObject parentContact] :
1185 NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
1186 chatsUsingCachedIcon--;
1187 [iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
1188 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1189 notify:NotifyNever];
1190 [objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
1192 if ((chatsUsingCachedIcon <= 0) &&
1193 (path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
1194 [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
1195 [iconSourceObject setValue:nil
1196 forProperty:KEY_WEBKIT_USER_ICON
1197 notify:NotifyNever];
1200 [objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
1204 * @brief Remove all references to *this* chat using cached icons for all involved objects
1206 - (void)releaseAllCachedIcons
1208 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1209 [self releaseCurrentWebKitUserIconForObject:listObject];
1214 * @brief Generate an updated masked user icon for the passed list object
1216 - (void)updateUserIconForObject:(AIListObject *)inObject
1218 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1219 [(AIListContact *)inObject parentContact] :
1222 NSString *oldWebKitUserIconPath = nil;
1223 NSString *webKitUserIconPath = nil;
1224 NSImage *webKitUserIcon;
1227 * We probably already have a userIcon waiting for us, the active display icon; use that
1228 * rather than loading one from disk.
1230 if (!(userIcon = [iconSourceObject userIcon])) {
1231 //If that's not the case, try using the UserIconPath
1232 NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
1234 userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
1238 if ([messageStyle userIconMask]) {
1239 //Apply the mask if the style has one
1240 //XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
1241 webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
1242 [webKitUserIcon lockFocus];
1243 [userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
1244 fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
1245 operation:NSCompositeSourceIn
1247 [webKitUserIcon unlockFocus];
1249 //Otherwise, just use the icon as-is
1250 webKitUserIcon = userIcon;
1253 oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1254 webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1256 if (!webKitUserIconPath) {
1257 /* If the image doesn't know a path to use, write it out and set it.
1259 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
1260 * any animation, which is good since animation in the message view is slow and annoying.
1262 * Only write out the icon if the object doesn't already have one
1264 webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
1265 if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
1267 [iconSourceObject setValue:webKitUserIconPath
1268 forProperty:KEY_WEBKIT_USER_ICON
1273 //Make sure it's known that this user has been handled
1274 if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
1275 [objectsWithUserIconsArray addObject:iconSourceObject];
1277 //Keep track of this chat using the icon
1278 [iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
1279 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1280 notify:NotifyNever];
1283 if (!webKitUserIconPath) webKitUserIconPath = @"";
1285 //Update existing images
1286 AILogWithSignature(@"Updating %@ to %@", oldWebKitUserIconPath, webKitUserIconPath);
1288 if (oldWebKitUserIconPath &&
1289 ![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
1290 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1291 NSUInteger imagesCount = [images length];
1293 webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
1295 for (NSInteger i = 0; i < imagesCount; i++) {
1296 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1297 NSString *currentSrc = [img getAttribute:@"src"];
1298 if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
1299 [img setSrc:webKitUserIconPath];
1304 [objectIconPathDict setObject:webKitUserIconPath
1305 forKey:iconSourceObject.internalObjectID];
1309 - (void)updateServiceIcon
1311 DOMDocument *doc = [webView mainFrameDocument];
1312 //Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
1313 if(![doc respondsToSelector:@selector(getElementsByClassName:)])
1315 DOMNodeList *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
1316 NSUInteger imagesCount = [serviceIconImages length];
1318 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID
1319 type:AIServiceIconLarge];
1321 for (NSInteger i = 0; i < imagesCount; i++) {
1322 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
1323 [img setSrc:serviceIconPath];
1327 - (void)customEmoticonUpdated:(NSNotification *)inNotification
1329 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1330 NSUInteger imagesCount = [images length];
1332 if (imagesCount > 0) {
1333 AIEmoticon *emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
1334 NSString *textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
1335 NSString *path = [emoticon path];
1336 NSSize emoticonSize = [[emoticon image] size];
1337 BOOL updatedImage = NO;
1338 path = [[NSURL fileURLWithPath:path] absoluteString];
1339 for (NSInteger i = 0; i < imagesCount; i++) {
1340 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1342 if ([[img className] isEqualToString:@"emoticon"] &&
1343 [[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
1345 [img setWidth:emoticonSize.width];
1346 [img setHeight:emoticonSize.height];
1350 NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
1352 if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
1354 if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat"
1355 withArguments:[NSArray arrayWithObject:shouldScroll]];
1360 * @brief Returns the path the background image given a unique ID
1362 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
1364 NSString *filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
1365 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1369 * @brief Returns the path to the list object's masked user icon
1371 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
1373 NSString *filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
1374 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1377 #pragma mark File Transfer
1379 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
1381 ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
1382 ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
1385 AIFileTransferAction a;
1386 if ([action isEqualToString:@"SaveAs"])
1388 else if ([action isEqualToString:@"Cancel"])
1393 [tc handleFileTransferAction:a];
1397 #pragma mark Topic editing
1398 - (void)editingDidComplete:(DOMRange *)range
1400 DOMNode *node = range.startContainer;
1401 DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
1403 NSString *topicChange = nil;
1405 if (node == topicEdit || node.parentNode == topicEdit) {
1406 topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];
1408 NSTextView *textView = [self textView];
1410 [[webView window] makeFirstResponder:textView]; //Make it first responder
1413 // Update the topic div in case the user doesn't have permission to change it.
1416 // Tell the chat to set the topic.
1417 [chat setTopic:topicChange];
1421 #pragma mark Marked Scroller
1422 - (JVMarkedScroller *)markedScroller
1424 WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
1425 NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
1427 JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
1428 return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
1431 - (void)setupMarkedScroller
1433 WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
1434 NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
1436 [scrollView setHasHorizontalScroller:NO];
1438 JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
1439 if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
1440 NSRect scrollerFrame = [[scrollView verticalScroller] frame];
1441 NSScroller *oldScroller = scroller;
1442 scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
1443 [scroller setFloatValue:oldScroller.floatValue];
1444 [scroller setKnobProportion:oldScroller.knobProportion];
1445 [scrollView setVerticalScroller:scroller];
1449 - (NSNumber *)currentOffsetHeight
1451 // We use the body's height to determine our mark location.
1452 return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
1455 - (void)markCurrentLocation
1457 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
1460 #define PREF_KEY_FOCUS_LINE @"Draw Focus Lines"
1462 - (void)markForFocusChange
1464 // We use the current Chat element's height to determine our mark location.
1465 [self.markedScroller removeMarkWithIdentifier:@"focus"];
1467 // The next message being inserted needs to add a mark.
1468 nextMessageFocus = YES;
1470 DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
1471 DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
1472 for (NSUInteger i = 0; i < nodeList.length; i++)
1474 node = (DOMHTMLElement *)[nodeList item:i];
1475 classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
1477 [classes removeObject:@"focus"];
1478 [classes removeObject:@"firstFocus"];
1480 node.className = [classes componentsJoinedByString:@" "];
1489 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
1492 - (void)jumpToPreviousMark
1494 [self.markedScroller jumpToPreviousMark:nil];
1497 - (BOOL)previousMarkExists
1499 return [self.markedScroller previousMarkExists];
1502 - (void)jumpToNextMark
1504 [self.markedScroller jumpToNextMark:nil];
1507 - (BOOL)nextMarkExists
1509 return [self.markedScroller nextMarkExists];
1512 - (void)jumpToFocusMark
1514 [self.markedScroller jumpToFocusMark:nil];
1517 - (BOOL)focusMarkExists
1519 return [self.markedScroller focusMarkExists];
1522 #pragma mark JS Bridging
1523 /*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
1526 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
1529 sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
1530 sel_isEqual(aSelector, @selector(debugLog:)) ||
1531 sel_isEqual(aSelector, @selector(zoomImage:))
1539 * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
1540 * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
1541 * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
1543 * Any colon (“:”)in the Objective-C selector is replaced by an underscore (“_”).
1544 * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
1545 * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
1547 + (NSString *)webScriptNameForSelector:(SEL)aSelector
1549 if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
1550 if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
1551 if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
1555 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
1557 NSMutableString *className = [[[img className] mutableCopy] autorelease];
1558 if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
1559 [className replaceOccurrencesOfString:@"fullSizeImage"
1560 withString:@"scaledToFitImage"
1561 options:NSLiteralSearch
1562 range:NSMakeRange(0, [className length])];
1563 else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
1564 [className replaceOccurrencesOfString:@"scaledToFitImage"
1565 withString:@"fullSizeImage"
1566 options:NSLiteralSearch
1567 range:NSMakeRange(0, [className length])];
1571 [img setClassName:className];
1572 [[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
1577 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
1579 //gets the source of the html page, for debugging
1580 - (NSString *)webviewSource
1582 return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
1586 * @brief Set the HTML content for the "Chat" area.
1588 - (void)setChatContentSource:(NSString *)source
1590 if (!webViewIsReady) {
1591 // If the webview isn't ready yet, wait a very short amount of time before trying again
1592 [self performSelector:@selector(setChatContentSource:)
1596 // Add the old "Chat" element to the window.
1597 [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
1599 NSString *scrollToBottomScript;
1600 if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
1601 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
1607 * @brief Get the HTML content for the "Chat" area.
1609 - (NSString *)chatContentSource
1611 return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
1615 * @brief The unique name for this style of "content source"
1617 - (NSString *)contentSourceName
1619 return [[[messageStyle bundle] bundlePath] lastPathComponent];