Plugins/WebKit Message View/AIWebKitMessageViewController.m
author Zachary West <zacw@adium.im>
Mon Nov 02 18:30:22 2009 -0500 (2009-11-02)
changeset 2852 7ff6b3f336d6
parent 2532 700469827eee
child 2853 a2f78c3401b9
permissions -rw-r--r--
Instead of inserting a <hr/> when we lose focus, which ends up breaking more than you'd expect, add a message class for the next message. Fixes #13300.

This removes the "Show Focus Lines" preference (always a good thing), and always inserts the mark in the scrollbar. It will be up to the style to implement the "focus" class to show the location. All previous messages of class "focus" will have "focus" removed when focus is lost again.
     1 /* 
     2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
     3  * with this source distribution.
     4  * 
     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.
     8  * 
     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.
    12  * 
    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.
    15  */
    16 
    17 #import "AIWebKitMessageViewController.h"
    18 #import "AIWebKitMessageViewStyle.h"
    19 #import "AIWebKitMessageViewPlugin.h"
    20 #import "ESWebKitMessageViewPreferences.h"
    21 #import "AIWebKitDelegate.h"
    22 #import "ESFileTransferRequestPromptController.h"
    23 #import "ESWebView.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>
    54 
    55 #define KEY_WEBKIT_CHATS_USING_CACHED_ICON @"WebKit:Chats Using Cached Icon"
    56 
    57 #define USE_FASTER_BUT_BUGGY_WEBKIT_PREFERENCE_CHANGE_HANDLING FALSE
    58 
    59 #define TEMPORARY_FILE_PREFIX	@"TEMP"
    60 
    61 @interface AIWebKitMessageViewController ()
    62 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin;
    63 - (void)_initWebView;
    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;
    70 
    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;
    78 - (void)updateTopic;
    79 
    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;
    88 
    89 - (void)setupMarkedScroller;
    90 - (JVMarkedScroller *)markedScroller;
    91 - (NSNumber *)currentOffsetHeight;
    92 - (void)markCurrentLocation;
    93 @end
    94 
    95 @interface DOMDocument (FutureWebKitPublicMethodsIKnow)
    96 - (DOMNodeList *)getElementsByClassName:(NSString *)className;
    97 - (DOMNodeList *)querySelectorAll:(NSString *)selectors; // We require 10.5.8/Safari 4, all is well!
    98 @end
    99 
   100 static NSArray *draggedTypes = nil;
   101 
   102 @implementation AIWebKitMessageViewController
   103 
   104 + (AIWebKitMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
   105 {
   106     return [[[self alloc] initForChat:inChat withPlugin:inPlugin] autorelease];
   107 }
   108 
   109 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
   110 {
   111     //init
   112     if ((self = [super init])) {		
   113 		[self _initWebView];
   114 		
   115 		delegateProxy = [AIWebKitDelegate sharedWebKitDelegate];
   116 		
   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;
   124 
   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];
   129 		
   130 		//Set ourselves up initially.
   131 		[self _updateWebViewForCurrentPreferences];
   132 		
   133 		//Observe participants list changes
   134 		[[NSNotificationCenter defaultCenter] addObserver:self 
   135 									   selector:@selector(participatingListObjectsChanged:)
   136 										   name:Chat_ParticipatingListObjectsChanged 
   137 										 object:inChat];
   138 
   139 		//Observe source/destination changes
   140 		[[NSNotificationCenter defaultCenter] addObserver:self 
   141 									   selector:@selector(sourceOrDestinationChanged:)
   142 										   name:Chat_SourceChanged 
   143 										 object:inChat];
   144 		[[NSNotificationCenter defaultCenter] addObserver:self 
   145 									   selector:@selector(sourceOrDestinationChanged:)
   146 										   name:Chat_DestinationChanged 
   147 										 object:inChat];
   148 		
   149 		//Observe content additons
   150 		[[NSNotificationCenter defaultCenter] addObserver:self 
   151 									   selector:@selector(contentObjectAdded:)
   152 										   name:Content_ContentObjectAdded 
   153 										 object:inChat];
   154 		[[NSNotificationCenter defaultCenter] addObserver:self 
   155 									   selector:@selector(chatDidFinishAddingUntrackedContent:)
   156 										   name:Content_ChatDidFinishAddingUntrackedContent 
   157 										 object:inChat];
   158 
   159 		[[NSNotificationCenter defaultCenter] addObserver:self
   160 									   selector:@selector(customEmoticonUpdated:)
   161 										   name:@"AICustomEmoticonUpdated"
   162 										 object:inChat];
   163 	}
   164 	
   165     return self;
   166 }
   167 
   168 - (void)messageViewIsClosing
   169 {
   170 	[webView stopLoading:nil];
   171 	
   172 	//Stop observing the webview, since it may attempt callbacks shortly after we dealloc
   173 	[delegateProxy removeDelegate:self];
   174 	
   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.
   178 	 */
   179 	[[webView windowScriptObject] removeWebScriptKey:@"client"];
   180 
   181 	//Release the web view
   182 	[webView release]; webView = nil;
   183 }
   184 
   185 /*!
   186  * @brief Deallocate
   187  */
   188 - (void)dealloc
   189 {
   190 	[self releaseAllCachedIcons];
   191 
   192 	[plugin release]; plugin = nil;
   193 	[objectsWithUserIconsArray release]; objectsWithUserIconsArray = nil;
   194 	[objectIconPathDict release]; objectIconPathDict = nil;
   195 
   196 	//Stop any delayed requests and remove all observers
   197 	[NSObject cancelPreviousPerformRequestsWithTarget:self];
   198 	[adium.preferenceController unregisterPreferenceObserver:self];
   199 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   200 	
   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;
   206 	
   207 	//Cleanup content processing
   208 	[contentQueue release]; contentQueue = nil;
   209 	[storedContentObjects release]; storedContentObjects = nil;
   210 	[previousContent release]; previousContent = nil;
   211 
   212 	//Release the chat
   213 	[chat release]; chat = nil;
   214 	
   215 	//Release the marked scroller
   216 	[self.markedScroller release];
   217 
   218 	[super dealloc];
   219 }
   220 
   221 - (void)setShouldReflectPreferenceChanges:(BOOL)inValue
   222 {
   223 	shouldReflectPreferenceChanges = inValue;
   224 
   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];
   229 		}
   230 	} else {
   231 		[storedContentObjects release]; storedContentObjects = nil;
   232 	}
   233 }
   234 
   235 - (void)adiumPrint:(id)sender
   236 {	
   237 	WebPreferences* prefs = [webView preferences];
   238 	[prefs setShouldPrintBackgrounds:YES];
   239 
   240 	[[[[webView mainFrame] frameView] documentView] print:sender];
   241 }
   242 
   243 //WebView --------------------------------------------------------------------------------------------------
   244 #pragma mark WebView
   245 
   246 @synthesize messageStyle, messageView = webView;
   247 
   248 - (NSView *)messageScrollView
   249 {
   250 	return [[webView mainFrame] frameView];
   251 }
   252 
   253 /*!
   254  * @brief Apply preference changes to our webview
   255  */
   256 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
   257 					preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
   258 {
   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)
   261 		return;
   262 	
   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];
   270 			
   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];
   278 			}
   279 		}
   280 #else
   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;
   291 				}
   292 			}
   293 		}
   294 #endif
   295 	}
   296 	
   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;
   306 		}
   307 	}	
   308 }
   309 
   310 /*!
   311  * @brief Initialiaze the web view
   312  */
   313 - (void)_initWebView
   314 {
   315 	//Create our webview
   316 	webView = [[ESWebView alloc] initWithFrame:NSMakeRect(0,0,100,100) //Arbitrary frame
   317 									 frameName:nil
   318 									 groupName:nil];
   319 	[webView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
   320 	[delegateProxy addDelegate:self forView:webView];
   321 	[webView setMaintainsBackForwardList:NO];
   322 
   323 	if (!draggedTypes) {
   324 		draggedTypes = [[NSArray alloc] initWithObjects:
   325 			NSFilenamesPboardType,
   326 			AIiTunesTrackPboardType,
   327 			NSTIFFPboardType,
   328 			NSPDFPboardType,
   329 			NSPICTPboardType,
   330 			NSHTMLPboardType,
   331 			NSFileContentsPboardType,
   332 			NSRTFPboardType,
   333 			NSStringPboardType,
   334 			NSPostScriptPboardType,
   335 			nil];
   336 	}
   337 	[webView registerForDraggedTypes:draggedTypes];
   338 }
   339 
   340 /*!
   341  * @brief Updates our webview to the current preferences, priming the view
   342  */
   343 - (void)_updateWebViewForCurrentPreferences
   344 {
   345 	//Cleanup first
   346 	[messageStyle autorelease]; messageStyle = nil;
   347 	[activeStyle release]; activeStyle = nil;
   348 	[activeVariant release]; activeVariant = nil;
   349 	
   350 	//Load the message style
   351 	messageStyle = [[plugin currentMessageStyleForChat:chat] retain];
   352 	activeStyle = [[[messageStyle bundle] bundleIdentifier] retain];
   353 	preferenceGroup = [[plugin preferenceGroupForChat:chat] retain];
   354 
   355 	[webView setPreferencesIdentifier:activeStyle];
   356 
   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.
   364 		 */
   365 		NSArray *availableVariants = [messageStyle availableVariants];
   366 		if ([availableVariants count]) {
   367 			activeVariant = [[availableVariants objectAtIndex:0] retain];
   368 		}
   369 	}
   370 	
   371 	NSDictionary *prefDict = [adium.preferenceController preferencesForGroup:preferenceGroup];
   372 
   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]];
   381 	
   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];
   393 			
   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];
   401 				
   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];
   406 
   407 				//Remember where we cached it
   408 				[adium.preferenceController setPreference:cachePath
   409 													 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
   410 													  group:preferenceGroup];
   411 			} else {
   412 				cachePath = @""; //No custom image found
   413 			}
   414 		}
   415 		
   416 		[messageStyle setCustomBackgroundColor:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundColor" forStyle:activeStyle]
   417 																						 group:preferenceGroup] representedColor]];
   418 	} else {
   419 		[messageStyle setCustomBackgroundColor:nil];
   420 	}
   421 
   422 	[messageStyle setCustomBackgroundPath:cachePath];
   423 	[messageStyle setCustomBackgroundType:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundType" forStyle:activeStyle]
   424 																					group:preferenceGroup] integerValue]];
   425 	
   426 	BOOL isBackgroundTransparent = [[self messageStyle] isBackgroundTransparent];
   427 	[webView setTransparent:isBackgroundTransparent];
   428 	NSWindow *win = [webView window];
   429 	if(win)
   430 		[win setOpaque:!isBackgroundTransparent];
   431 
   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])];
   436 	
   437 	NSNumber	*fontSize = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontSize" forStyle:activeStyle]
   438 																  group:preferenceGroup];
   439 	[[webView preferences] setDefaultFontSize:[(fontSize ? fontSize : [messageStyle defaultFontSize]) integerValue]];
   440 	
   441 	NSNumber	*minSize = [adium.preferenceController preferenceForKey:KEY_WEBKIT_MIN_FONT_SIZE
   442 																 group:preferenceGroup];
   443 	[[webView preferences] setMinimumFontSize:(minSize ? [minSize integerValue] : 1)];
   444 
   445 	//Update our icons before doing any loading
   446 	[self sourceOrDestinationChanged:nil];
   447 
   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];	
   450 }
   451 
   452 /*!
   453  * @brief Updates our webview to the currently active varient without refreshing the view
   454  */
   455 - (void)_updateVariantWithoutPrimingView
   456 {
   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]];			
   460 	} else {
   461 		[self performSelector:@selector(_updateVariantWithoutPrimingView) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
   462 	}
   463 }
   464 
   465 /*!
   466  *	@brief Clears the view from displayed messages
   467  *
   468  *	Implements the method defined in protocol AIMessageDisplayController
   469  */
   470 - (void)clearView
   471 {
   472 	[self _primeWebViewAndReprocessContent:NO];
   473 	[self.markedScroller removeAllMarks];
   474 	[previousContent release];
   475 	previousContent = nil;
   476 	nextMessageFocus = NO;
   477 	[chat clearUnviewedContentCount];
   478 }
   479 
   480 /*!
   481  * @brief Primes our webview to the currently active style and variant
   482  *
   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.
   485  */
   486 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent
   487 {
   488 	webViewIsReady = NO;
   489 
   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];
   493 
   494 	if(chat.isGroupChat && chat.supportsTopic) {
   495 		// Force a topic update, so we set our topic appropriately.
   496 		[self updateTopic];
   497 	}
   498 	
   499 	if (reprocessContent) {
   500 		NSArray	*currentContentQueue;
   501 		
   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] :
   505 							   nil);
   506 
   507 		//Start from an empty content queue
   508 		[contentQueue removeAllObjects];
   509 
   510 		//Add our stored content objects to the content queue
   511 		[contentQueue addObjectsFromArray:storedContentObjects];
   512 		[storedContentObjects removeAllObjects];
   513 
   514 		//Add the old content queue back in if necessary
   515 		if (currentContentQueue) {
   516 			[contentQueue addObjectsFromArray:currentContentQueue];
   517 			[currentContentQueue release];
   518 		}
   519 
   520 		//We're still holding onto the previousContent from before, which is no longer accurate. Release it.
   521 		[previousContent release]; previousContent = nil;
   522 	}
   523 }
   524 
   525 /*!
   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
   527  *
   528  * If/when we support transforming chats to/from groupchats we'll need to observe that and call this as appropriate
   529  */
   530 - (void) setIsGroupChat:(BOOL) flag
   531 {
   532 	DOMHTMLElement *chatElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"];
   533 	NSMutableString *chatClassName = [[[chatElement className] mutableCopy] autorelease];
   534 	if (flag == NO)
   535 		[chatClassName replaceOccurrencesOfString:@" groupchat"
   536 									   withString:@""
   537 										  options:NSLiteralSearch
   538 											range:NSMakeRange(0, [chatClassName length])];
   539 	else
   540 		[chatClassName appendString:@" groupchat"];
   541 	[chatElement setClassName:chatClassName];
   542 }
   543 
   544 //Content --------------------------------------------------------------------------------------------------------------
   545 #pragma mark Content
   546 /*!
   547  * @brief Append new content to our processing queue
   548  */
   549 - (void)contentObjectAdded:(NSNotification *)notification
   550 {
   551 	AIContentObject	*contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
   552 	[self enqueueContentObject:contentObject];
   553 }
   554 
   555 - (void)enqueueContentObject:(AIContentObject *)contentObject
   556 {
   557 	[contentQueue addObject:contentObject];
   558 	
   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
   561 	* after each one.
   562 	*/
   563 	if ([contentObject displayContentImmediately]) {
   564 		[self processQueuedContent];
   565 	}
   566 }
   567 
   568 /*!
   569  * @brief Our chat finished adding untracked content
   570  */
   571 - (void)chatDidFinishAddingUntrackedContent:(NSNotification *)notification
   572 {
   573 	[self processQueuedContent];	
   574 }
   575 
   576 /*!
   577  * @brief Append new content to our processing queueProcess any content in the queuee
   578  */
   579 - (void)processQueuedContent
   580 {
   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;
   584 	
   585 	if (webViewIsReady) {
   586 		contentQueueCount = contentQueue.count;
   587 
   588 		while (contentQueueCount > 0) {
   589 			BOOL willAddMoreContent = (contentQueueCount > 1);
   590 			
   591 			//Display the content
   592 			AIContentObject *content = [contentQueue objectAtIndex:0];
   593 			[self _processContentObject:content 
   594 			  willAddMoreContentObjects:willAddMoreContent];
   595 
   596 			//If we are going to reflect preference changes, store this content object
   597 			if (shouldReflectPreferenceChanges) {
   598 				[storedContentObjects addObject:content];
   599 			}
   600 
   601 			//Remove the content we just displayed from the queue
   602 			[contentQueue removeObjectAtIndex:0];
   603 			objectsAdded++;
   604 			contentQueueCount--;
   605 		}
   606 	}
   607 	
   608 	/* If we added two or more objects, we may want to scroll to the bottom now, having not done it as each object
   609 	 * was added.
   610 	 */
   611 	if (objectsAdded > 1) {
   612 		NSString	*scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects];
   613 		
   614 		if (scrollToBottomScript) {
   615 			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
   616 		}
   617 	}
   618 	
   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];
   622 	}
   623 }
   624 
   625 /*!
   626  * @brief Process and then append a content object
   627  */
   628 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects
   629 {
   630 	AIContentEvent	*dateSeparator = nil;
   631 	BOOL			replaceLastContent = NO;
   632 
   633 	/*
   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.
   636 	 */
   637 	if ((!previousContent && [content isKindOfClass:[AIContentContext class]]) ||
   638 	   (![content isFromSameDayAsContent:previousContent])) {
   639 		
   640 		NSString *dateMessage = [[NSDateFormatter localizedDateFormatter] stringFromDate:content.date];
   641 		
   642 		dateSeparator = [AIContentEvent statusInChat:content.chat
   643 										  withSource:content.chat.listObject
   644 										 destination:content.chat.account
   645 												date:content.date
   646 											 message:[[[NSAttributedString alloc] initWithString:dateMessage
   647 																					  attributes:[adium.contentController defaultFormattingAttributes]] autorelease]
   648 											withType:@"date_separator"];
   649 
   650 		if ([content isKindOfClass:[AIContentContext class]])
   651 			[dateSeparator addDisplayClass:@"history"];
   652 
   653 		//Add the date header
   654 		[self _appendContent:dateSeparator 
   655 					 similar:NO
   656 			willAddMoreContentObjects:YES
   657 		  replaceLastContent:NO];
   658 		[previousContent release]; previousContent = [dateSeparator retain];
   659 	}
   660 	
   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]]) {
   664 		similar = NO;
   665 		replaceLastContent = YES;
   666 	}
   667 
   668 	if ([content.type isEqualToString:CONTENT_TOPIC_TYPE]) {
   669 		DOMHTMLElement *topicElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topic"];
   670 		
   671 		if (((AIContentTopic *)content).actuallyBlank) {
   672 			content.message = [NSAttributedString stringWithString:@""];
   673 		}
   674 		
   675 		[topicElement setTitle:content.message.string];
   676 		
   677 		[topicElement setInnerHTML:[messageStyle completedTemplateForContent:content similar:similar]];
   678 	} else {
   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];
   682 		}
   683 		
   684 		// Set it as a focus if appropriate.
   685 		if (nextMessageFocus && [content.type isEqualToString:CONTENT_MESSAGE_TYPE]) {
   686 			[content addDisplayClass:@"focus"];
   687 			nextMessageFocus = NO;
   688 		}
   689 		
   690 		//Add the content object
   691 		[self _appendContent:content 
   692 					 similar:similar
   693    willAddMoreContentObjects:willAddMoreContentObjects
   694 		  replaceLastContent:replaceLastContent];
   695 	}
   696 
   697 	[previousContent release]; previousContent = [content retain];
   698 }
   699 
   700 /*!
   701  * @brief Append a content object
   702  */
   703 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
   704 {
   705 	[webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
   706 																					similar:contentIsSimilar
   707 																  willAddMoreContentObjects:willAddMoreContentObjects
   708 																		 replaceLastContent:replaceLastContent]];
   709 
   710 	NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
   711 }
   712 
   713 #pragma mark Topics
   714 /*!
   715  * @brief Force a topic update.
   716  *
   717  * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
   718  */
   719 - (void)updateTopic
   720 {
   721 	NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
   722 	
   723 	AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
   724 													withSource:chat.topicSetter
   725 												   destination:nil
   726 														  date:[NSDate date]
   727 													   message:topic];
   728 	
   729 	// In case this topic is blank, we have to filter this ourself; the content controller will drop it.
   730 	contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
   731 	
   732 	[self enqueueContentObject:contentTopic];
   733 }
   734 
   735 //WebView Delegates ----------------------------------------------------------------------------------------------------
   736 #pragma mark Webview delegates
   737 
   738 - (void)webViewIsReady{
   739 	webViewIsReady = YES;
   740 	[self setupMarkedScroller];
   741 	[self setIsGroupChat:chat.isGroupChat];
   742 	[self processQueuedContent];
   743 }
   744 
   745 - (void)openImage:(id)sender
   746 {
   747 	NSURL	*imageURL = [sender representedObject];
   748 	[[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
   749 }
   750 
   751 - (void)saveImageAs:(id)sender
   752 {
   753 	NSURL		*imageURL = [sender representedObject];
   754 	NSString	*path = [imageURL path];
   755 	
   756 	NSSavePanel *savePanel = [NSSavePanel savePanel];
   757 	[savePanel beginSheetForDirectory:nil
   758 								 file:[path lastPathComponent]
   759 					   modalForWindow:[webView window]
   760 						modalDelegate:self
   761 					   didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
   762 						  contextInfo:[imageURL retain]];
   763 }
   764 
   765 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode  contextInfo:(void  *)contextInfo
   766 {
   767 	NSURL	*imageURL = (NSURL *)contextInfo;
   768 
   769 	if (returnCode ==  NSOKButton) {
   770 		[[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
   771 												toPath:[[sheet URL] absoluteString]
   772 												 error:NULL];
   773 	}
   774 	
   775 	[imageURL release];
   776 }
   777 
   778 /*!
   779  * @brief Append our own menu items to the webview's contextual menus
   780  */
   781 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
   782 {
   783 	NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
   784 	AIListContact	*chatListObject = chat.listObject.parentContact;
   785 	NSMenuItem		*menuItem;
   786 
   787 	//Remove default items we don't want
   788 	if (webViewMenuItems) {
   789 
   790 		for (menuItem in defaultMenuItems) {
   791 			NSInteger tag = [menuItem tag];
   792 			if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
   793 				(tag == WebMenuItemTagDownloadLinkToDisk) ||
   794 				(tag == WebMenuItemTagOpenImageInNewWindow) ||
   795 				(tag == WebMenuItemTagDownloadImageToDisk) ||
   796 				(tag == WebMenuItemTagOpenFrameInNewWindow) ||
   797 				(tag == WebMenuItemTagStop) ||
   798 				(tag == WebMenuItemTagReload)) {
   799 				[webViewMenuItems removeObjectIdenticalTo:menuItem];
   800 			} else {
   801 				//This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
   802 				if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
   803 					[webViewMenuItems removeObjectIdenticalTo:menuItem];					
   804 			}
   805 		}
   806 	}
   807 	
   808 	NSURL	*imageURL;
   809 	if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
   810 		//This is an image		
   811 		if (!webViewMenuItems) {
   812 			webViewMenuItems = [NSMutableArray array];
   813 		}
   814 		
   815 		menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
   816 											  target:self
   817 											  action:@selector(openImage:)
   818 									   keyEquivalent:@""
   819 								   representedObject:imageURL];
   820 		[webViewMenuItems addObject:menuItem];
   821 		[menuItem release];
   822 		menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
   823 											  target:self
   824 											  action:@selector(saveImageAs:)
   825 									   keyEquivalent:@""
   826 								   representedObject:imageURL];
   827 		[webViewMenuItems addObject:menuItem];
   828 		[menuItem release];		
   829 		
   830 		/*
   831 		NSString *imgClass = [img className];
   832 		//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.
   833 		if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
   834 		   [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
   835 		   [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
   836 		   [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
   837 		 */
   838 			
   839 	}
   840 
   841 	if (webViewMenuItems) {		
   842 		//Add a separator item if items already exist in webViewMenuItems
   843 		if ([webViewMenuItems count]) {
   844 			// If the first item is a separator item, remove it.
   845 			if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
   846 				[webViewMenuItems removeObjectAtIndex:0];
   847 			}
   848 			
   849 			[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   850 		}
   851 	} else {
   852 		webViewMenuItems = [NSMutableArray array];
   853 	}
   854 
   855 	NSMenu *originalMenu = nil;
   856 	
   857 	if (chatListObject) {
   858 		NSArray *locations;
   859 		if ([chatListObject isIntentionallyNotAStranger]) {
   860 			locations = [NSArray arrayWithObjects:
   861 				[NSNumber numberWithInteger:Context_Contact_Manage],
   862 				[NSNumber numberWithInteger:Context_Contact_Action],
   863 				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
   864 				[NSNumber numberWithInteger:Context_Contact_ChatAction],
   865 				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
   866 		} else {
   867 			locations = [NSArray arrayWithObjects:
   868 				[NSNumber numberWithInteger:Context_Contact_Manage],
   869 				[NSNumber numberWithInteger:Context_Contact_Action],
   870 				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
   871 				[NSNumber numberWithInteger:Context_Contact_ChatAction],
   872 				[NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
   873 				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
   874 		}
   875 		
   876 		originalMenu = [adium.menuController contextualMenuWithLocations:locations
   877 														   forListObject:chatListObject
   878 																  inChat:chat];
   879 	} else if(chat.isGroupChat) {
   880 		originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
   881 																		  [NSNumber numberWithInteger:Context_GroupChat_Manage],
   882 																		  [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
   883 																 forChat:chat];
   884 	}
   885 	
   886 	[webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
   887 	[originalMenu removeAllItems];
   888 
   889 	if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
   890 		[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   891 
   892 	//Present an option to clear the display
   893 	menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
   894 										  target:self
   895 										  action:@selector(clearView)
   896 								   keyEquivalent:@""];
   897 	[webViewMenuItems addObject:menuItem];
   898 	[menuItem release];
   899 	
   900 	return webViewMenuItems;
   901 }
   902 
   903 /*!
   904  * @brief Add ourself to the window script object bridge when it's safe to do so
   905  */
   906 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
   907 {
   908     [[webView windowScriptObject] setValue:self forKey:@"client"];
   909 }
   910 
   911 //Dragging delegate ----------------------------------------------------------------------------------------------------
   912 #pragma mark Dragging delegate
   913 /*!
   914  * @brief If possible, return the first NSTextView in the message view's responder chain
   915  *
   916  * This is used for drag and drop behavior.
   917  */
   918 - (NSTextView *)textView
   919 {
   920 	id	responder = [webView nextResponder];
   921 	
   922 	//Walkin the responder chain looking for an NSTextView
   923 	while (responder &&
   924 		  ![responder isKindOfClass:[NSTextView class]]) {
   925 		responder = [responder nextResponder];
   926 	}
   927 	
   928 	return responder;
   929 }
   930 
   931 /*!
   932  * @brief Dragging entered
   933  */
   934 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   935 {
   936 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   937 
   938 	return ([pasteboard availableTypeFromArray:draggedTypes] ?
   939 		   NSDragOperationCopy :
   940 		   NSDragOperationNone);
   941 }
   942 
   943 /*!
   944 * @brief Dragging updated
   945  */
   946 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   947 {
   948 	return [self draggingEntered:sender];
   949 }
   950 
   951 /*!
   952  * @brief Handle a drag onto the webview
   953  * 
   954  * If we're getting a non-image file, we can handle it immediately.  Otherwise, the drag is the textView's problem.
   955  */
   956 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   957 {
   958 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   959 	BOOL			success = NO;
   960 	
   961 	if ([self shouldHandleDragWithPasteboard:pasteboard]) {
   962 		
   963 		//Not an image but it is a file - send it immediately as a file transfer
   964 		NSArray			*files = [pasteboard propertyListForType:NSFilenamesPboardType];
   965 		NSString		*path;
   966 		for (path in files) {
   967 			AIListObject *listObject = chat.listObject;
   968 			if (listObject) {
   969 				[adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
   970 			}
   971 		}
   972 		success = YES;
   973 		
   974 	} else {
   975 		NSTextView *textView = [self textView];
   976 		if (textView) {
   977 			[[webView window] makeFirstResponder:textView]; //Make it first responder
   978 			success = [textView performDragOperation:sender];
   979 		}
   980 	}
   981 	
   982 	return success;
   983 }
   984 
   985 /*!
   986  * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
   987  */
   988 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   989 {
   990 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   991 	BOOL	success = YES;
   992 	
   993 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {	
   994 		NSTextView *textView = [self textView];
   995 		if (textView) {
   996 			success = [textView prepareForDragOperation:sender];
   997 		}
   998 	}
   999 	
  1000 	return success;
  1001 }
  1002 	
  1003 /*!
  1004  * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
  1005  */
  1006 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
  1007 {
  1008 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1009 	
  1010 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {
  1011 		NSTextView *textView = [self textView];
  1012 		if (textView) {
  1013 			[textView concludeDragOperation:sender];
  1014 		}
  1015 	}
  1016 }
  1017 
  1018 /*!
  1019  * @brief Handle drags of content we recognize
  1020  */
  1021 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
  1022 {
  1023 	/*
  1024 	return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
  1025 			[pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
  1026 	 */
  1027 	return NO;
  1028 }
  1029 
  1030 
  1031 //User Icon masking --------------------------------------------------------------------------------------------------
  1032 //We allow messaage styles to specify masks for user icons.  This could be user to round the corners of user icons 
  1033 //or other related effects.
  1034 #pragma mark User icon masking
  1035 /*!
  1036  * @brief Update icon masks when participating list objects change
  1037  *
  1038  * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
  1039  * When the list changes, remove the observers we had in place before and add observers for each object in the list
  1040  * so we never observe for contacts not in the chat.
  1041  */
  1042 - (void)participatingListObjectsChanged:(NSNotification *)notification
  1043 {
  1044 	NSArray			*participatingListObjects = [chat containedObjects];
  1045 	
  1046 	[[NSNotificationCenter defaultCenter] removeObserver:self
  1047 										  name:ListObject_AttributesChanged
  1048 										object:nil];
  1049 	
  1050 	for (AIListObject *listObject in participatingListObjects) {
  1051 		//Update the mask for any user which just entered the chat
  1052 		if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
  1053 			[self updateUserIconForObject:listObject];
  1054 		}
  1055 		
  1056 		//In the future, watch for changes on the parent object, since that's the icon we display
  1057 		if ([listObject isKindOfClass:[AIListContact class]]) {
  1058 			[[NSNotificationCenter defaultCenter] addObserver:self
  1059 										   selector:@selector(listObjectAttributesChanged:) 
  1060 											   name:ListObject_AttributesChanged
  1061 											 object:[(AIListContact *)listObject parentContact]];
  1062 		}
  1063 	}
  1064 	
  1065 	//Also observe our account
  1066 	if (chat.account) {
  1067 		[[NSNotificationCenter defaultCenter] addObserver:self
  1068 									   selector:@selector(listObjectAttributesChanged:) 
  1069 										   name:ListObject_AttributesChanged
  1070 										 object:chat.account];
  1071 	}
  1072 
  1073 	//Remove the cache for any object no longer in the chat
  1074 	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
  1075 		if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
  1076 			(![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
  1077 			!(listObject == chat.account)) {
  1078 			[self releaseCurrentWebKitUserIconForObject:listObject];
  1079 		}
  1080 	}
  1081 }
  1082 
  1083 /*!
  1084  * @brief Update icon masks when source or destination changes
  1085  */
  1086 - (void)sourceOrDestinationChanged:(NSNotification *)notification
  1087 {
  1088 	//Update the participating contacts
  1089 	[self participatingListObjectsChanged:nil];
  1090 	
  1091 	//And update the source account
  1092 	[self updateUserIconForObject:chat.account];
  1093 	
  1094 	[self updateServiceIcon];
  1095 }
  1096 
  1097 /*!
  1098  * @brief Update the icon when a list object's icon attributes change
  1099  */
  1100 - (void)listObjectAttributesChanged:(NSNotification *)notification
  1101 {
  1102     AIListObject	*inObject = [notification object];
  1103     NSSet			*keys = [[notification userInfo] objectForKey:@"Keys"];
  1104 	
  1105 	if ([keys containsObject:KEY_USER_ICON]) {
  1106 		if (inObject) {
  1107 			AIListObject	*actualObject = nil;
  1108 			AILogWithSignature(@"%@'s icon changed", inObject);
  1109 			if (chat.account == inObject) {
  1110 				//The account is the object actually in the chat
  1111 				actualObject = inObject;
  1112 			} else {
  1113 				/*
  1114 				 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
  1115 				 * be displaying as changed.
  1116 				 */
  1117 				
  1118 				for (AIListContact *participatingListObject in chat) {
  1119 					if ([participatingListObject parentContact] == inObject) {
  1120 						actualObject = participatingListObject;
  1121 						break;
  1122 					}
  1123 				}
  1124 			}
  1125 
  1126 			if (actualObject) {
  1127 				[self userIconForObjectDidChange:actualObject];
  1128 			}
  1129 
  1130 		} else {
  1131 			AILogWithSignature(@"nil object's icon changed");
  1132 			//We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
  1133 			[self sourceOrDestinationChanged:nil];
  1134 		}
  1135 	}
  1136 }
  1137 
  1138 - (void)userIconForObjectDidChange:(AIListObject *)inObject
  1139 {
  1140 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1141 										 [(AIListContact *)inObject parentContact] :
  1142 										 inObject);
  1143 	NSString		*currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
  1144 	if (currentIconPath) {
  1145 		NSString	*objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1146 		if (objectsKnownIconPath &&
  1147 			[currentIconPath isEqualToString:objectsKnownIconPath]) {
  1148 			//We're the first one to get to this object!  We get to delete the old path and remove the reference to it
  1149 			[[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
  1150 			[iconSourceObject setValue:nil
  1151 									   forProperty:KEY_WEBKIT_USER_ICON
  1152 									   notify:NotifyNever];
  1153 		} else {
  1154 			/* Some other instance beat us to the punch. The object's KEY_WEBKIT_USER_ICON is right, since it doesn't match our
  1155 			 * internally tracked path.
  1156 			 */
  1157 		}
  1158 	}
  1159 	
  1160 	[self updateUserIconForObject:iconSourceObject];
  1161 }
  1162 
  1163 /*!
  1164  * @brief Remove all references to *this* chat using cached icons for an object
  1165  *
  1166  * If this is the last chat utilizing the cached icon, it will be deleted.
  1167  *
  1168  * @param inObject The object
  1169  */
  1170 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
  1171 {
  1172 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1173 										 [(AIListContact *)inObject parentContact] :
  1174 										 inObject);
  1175 	NSString		*path;
  1176 	
  1177 	NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
  1178 	chatsUsingCachedIcon--;
  1179 	[iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
  1180 					   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
  1181 					   notify:NotifyNever];
  1182 	[objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
  1183 
  1184 	if ((chatsUsingCachedIcon <= 0) &&
  1185 		(path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
  1186 		[[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
  1187 		[iconSourceObject setValue:nil
  1188 								   forProperty:KEY_WEBKIT_USER_ICON
  1189 								   notify:NotifyNever];
  1190 	}
  1191 
  1192 	[objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
  1193 }
  1194 
  1195 /*!
  1196  * @brief Remove all references to *this* chat using cached icons for all involved objects
  1197  */
  1198 - (void)releaseAllCachedIcons
  1199 {
  1200 	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
  1201 		[self releaseCurrentWebKitUserIconForObject:listObject];
  1202 	}
  1203 }
  1204 
  1205 /*!
  1206  * @brief Generate an updated masked user icon for the passed list object
  1207  */
  1208 - (void)updateUserIconForObject:(AIListObject *)inObject
  1209 {
  1210 	AIListObject		*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1211 											 [(AIListContact *)inObject parentContact] :
  1212 											 inObject);
  1213 	NSImage				*userIcon;
  1214 	NSString			*oldWebKitUserIconPath = nil;
  1215 	NSString			*webKitUserIconPath = nil;
  1216 	NSImage				*webKitUserIcon;
  1217 	
  1218 	/*
  1219 	 * We probably already have a userIcon waiting for us, the active display icon; use that
  1220 	 * rather than loading one from disk.
  1221 	 */
  1222 	if (!(userIcon = [iconSourceObject userIcon])) {
  1223 		//If that's not the case, try using the UserIconPath
  1224 		NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
  1225 		if (userIconPath)
  1226 			userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
  1227 	}
  1228 
  1229 	if (userIcon) {
  1230 		if ([messageStyle userIconMask]) {
  1231 			//Apply the mask if the style has one
  1232 			//XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
  1233 			webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
  1234 			[webKitUserIcon lockFocus];
  1235 			[userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
  1236 						fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
  1237 					   operation:NSCompositeSourceIn
  1238 						fraction:1.0];
  1239 			[webKitUserIcon unlockFocus];
  1240 		} else {
  1241 			//Otherwise, just use the icon as-is
  1242 			webKitUserIcon = userIcon;
  1243 		}
  1244 
  1245 		oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
  1246 		webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1247 
  1248 		if (!webKitUserIconPath) {
  1249 			/* If the image doesn't know a path to use, write it out and set it.
  1250 			 *
  1251 			 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
  1252 			 * any animation, which is good since animation in the message view is slow and annoying.
  1253 			 *
  1254 			 * Only write out the icon if the object doesn't already have one
  1255 			 */				
  1256 			webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
  1257 			if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
  1258 													 atomically:YES]) {
  1259 				[iconSourceObject setValue:webKitUserIconPath
  1260 										   forProperty:KEY_WEBKIT_USER_ICON
  1261 										   notify:NO];				
  1262 			}			
  1263 		}
  1264 
  1265 		//Make sure it's known that this user has been handled
  1266 		if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
  1267 			[objectsWithUserIconsArray addObject:iconSourceObject];
  1268 
  1269 			//Keep track of this chat using the icon
  1270 			[iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
  1271 									   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
  1272 									   notify:NotifyNever];
  1273 		}
  1274 		
  1275 		if (!webKitUserIconPath) webKitUserIconPath = @"";
  1276 
  1277 		//Update existing images
  1278 		AILogWithSignature(@"Updating %@ to %@", oldWebKitUserIconPath, webKitUserIconPath);
  1279 
  1280 		if (oldWebKitUserIconPath &&
  1281 			![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
  1282 			DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1283 			NSUInteger imagesCount = [images length];
  1284 
  1285 			webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
  1286 
  1287 			for (NSInteger i = 0; i < imagesCount; i++) {
  1288 				DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
  1289 				NSString *currentSrc = [img getAttribute:@"src"];
  1290 				if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
  1291 					[img setSrc:webKitUserIconPath];
  1292 				}
  1293 			}
  1294 		}
  1295 
  1296 		[objectIconPathDict setObject:webKitUserIconPath
  1297 							   forKey:iconSourceObject.internalObjectID];
  1298 	}
  1299 }
  1300 
  1301 - (void)updateServiceIcon
  1302 {
  1303 	DOMDocument *doc = [webView mainFrameDocument];
  1304 	//Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
  1305 	if(![doc respondsToSelector:@selector(getElementsByClassName:)])
  1306 		return; 
  1307 	DOMNodeList  *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
  1308 	NSUInteger imagesCount = [serviceIconImages length];
  1309 	
  1310 	NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID 
  1311 																type:AIServiceIconLarge];
  1312 	
  1313 	for (NSInteger i = 0; i < imagesCount; i++) {
  1314 		DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
  1315 		[img setSrc:serviceIconPath];
  1316 	}	
  1317 }
  1318 
  1319 - (void)customEmoticonUpdated:(NSNotification *)inNotification
  1320 {
  1321 	DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1322 	NSUInteger imagesCount = [images length];
  1323 
  1324 	if (imagesCount > 0) {
  1325 		AIEmoticon	*emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
  1326 		NSString	*textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
  1327 		NSString	*path = [emoticon path];
  1328 		NSSize		emoticonSize = [[emoticon image] size];
  1329 		BOOL		updatedImage = NO;
  1330 		path = [[NSURL fileURLWithPath:path] absoluteString];
  1331 		for (NSInteger i = 0; i < imagesCount; i++) {
  1332 			DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
  1333 			
  1334 			if ([[img className] isEqualToString:@"emoticon"] &&
  1335 				[[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
  1336 				[img setSrc:path];
  1337 				[img setWidth:emoticonSize.width];
  1338 				[img setHeight:emoticonSize.height];
  1339 				updatedImage = YES;
  1340 			}
  1341 		}
  1342 		NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
  1343 																	 withArguments:nil];
  1344 		if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
  1345 
  1346 		if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat" 
  1347 															  withArguments:[NSArray arrayWithObject:shouldScroll]];
  1348 	}
  1349 }
  1350 
  1351 /*!
  1352  * @brief Returns the path the background image given a unique ID
  1353  */
  1354 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
  1355 {
  1356 	NSString	*filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
  1357 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1358 }
  1359 
  1360 /*!
  1361  * @brief Returns the path to the list object's masked user icon
  1362  */
  1363 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
  1364 {
  1365 	NSString	*filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
  1366 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1367 }
  1368 
  1369 #pragma mark File Transfer
  1370 
  1371 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
  1372 {
  1373 	ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
  1374 	ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
  1375 
  1376 	if (tc) {
  1377 		AIFileTransferAction a;
  1378 		if ([action isEqualToString:@"SaveAs"])
  1379 			a = AISaveFileAs;
  1380 		else if ([action isEqualToString:@"Cancel"]) 
  1381 			a = AICancel;
  1382 		else
  1383 			a = AISaveFile;
  1384 		
  1385 		[tc handleFileTransferAction:a];
  1386 	}
  1387 }
  1388 
  1389 #pragma mark Topic editing
  1390 - (void)editingDidComplete:(DOMRange *)range
  1391 {
  1392 	DOMNode *node = range.startContainer;
  1393 	DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
  1394 	
  1395 	NSString *topicChange = nil;
  1396 		
  1397 	if (node == topicEdit || node.parentNode == topicEdit) {
  1398 		topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];		
  1399 		
  1400 		NSTextView *textView = [self textView];
  1401 		if (textView) {
  1402 			[[webView window] makeFirstResponder:textView]; //Make it first responder
  1403 		}
  1404 		
  1405 		// Update the topic div in case the user doesn't have permission to change it.
  1406 		[self updateTopic];
  1407 		
  1408 		// Tell the chat to set the topic.
  1409 		[chat setTopic:topicChange];
  1410 	}
  1411 }
  1412 
  1413 #pragma mark Marked Scroller
  1414 - (JVMarkedScroller *)markedScroller
  1415 {
  1416 	WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
  1417 	NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
  1418 	
  1419 	JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
  1420 	return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
  1421 }
  1422 
  1423 - (void)setupMarkedScroller
  1424 {
  1425 	WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
  1426 	NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
  1427 	
  1428 	[scrollView setHasHorizontalScroller:NO];
  1429 
  1430 	JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
  1431 	if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
  1432 		NSRect scrollerFrame = [[scrollView verticalScroller] frame];
  1433 		NSScroller *oldScroller = scroller;
  1434 		scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
  1435 		[scroller setFloatValue:oldScroller.floatValue];
  1436 		[scroller setKnobProportion:oldScroller.knobProportion];
  1437 		[scrollView setVerticalScroller:scroller];
  1438 	}
  1439 }
  1440 
  1441 - (NSNumber *)currentOffsetHeight
  1442 {
  1443 	// We use the body's height to determine our mark location.
  1444 	return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
  1445 }
  1446 
  1447 - (void)markCurrentLocation
  1448 {
  1449 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
  1450 }
  1451 
  1452 #define PREF_KEY_FOCUS_LINE	@"Draw Focus Lines"
  1453 
  1454 - (void)markForFocusChange
  1455 {
  1456 	JVMarkedScroller *scroller = self.markedScroller;
  1457 	
  1458 	// We use the current Chat element's height to determine our mark location.
  1459 	[scroller removeMarkWithIdentifier:@"focus"];
  1460 	[scroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];	
  1461 	
  1462 	nextMessageFocus = YES;
  1463 	
  1464 	DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
  1465 	DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
  1466 	for (NSUInteger i = 0; i < nodeList.length; i++)
  1467 	{
  1468 		node = (DOMHTMLElement *)[nodeList item:i];
  1469 		classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
  1470 		
  1471 		[classes removeObject:@"focus"];
  1472 		
  1473 		node.className = [classes componentsJoinedByString:@" "];
  1474 		
  1475 		[classes release];
  1476 	}
  1477 	
  1478 	nextMessageFocus = YES;
  1479 }
  1480 
  1481 - (void)addMark
  1482 {
  1483 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
  1484 }
  1485 
  1486 - (void)jumpToPreviousMark
  1487 {
  1488 	[self.markedScroller jumpToPreviousMark:nil];
  1489 }
  1490 
  1491 - (BOOL)previousMarkExists
  1492 {
  1493 	return [self.markedScroller previousMarkExists];
  1494 }
  1495 
  1496 - (void)jumpToNextMark
  1497 {
  1498 	[self.markedScroller jumpToNextMark:nil];	
  1499 }
  1500 
  1501 - (BOOL)nextMarkExists
  1502 {
  1503 	return [self.markedScroller nextMarkExists];	
  1504 }
  1505 
  1506 - (void)jumpToFocusMark
  1507 {
  1508 	[self.markedScroller jumpToFocusMark:nil];
  1509 }
  1510 
  1511 - (BOOL)focusMarkExists
  1512 {
  1513 	return [self.markedScroller focusMarkExists];
  1514 }
  1515 
  1516 #pragma mark JS Bridging
  1517 /*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
  1518 */
  1519 
  1520 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
  1521 {
  1522 	if (
  1523 		sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
  1524 		sel_isEqual(aSelector, @selector(debugLog:)) ||
  1525 		sel_isEqual(aSelector, @selector(zoomImage:))
  1526 	)
  1527 		return NO;
  1528 	
  1529 	return YES;
  1530 }
  1531 
  1532 /*
  1533  * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
  1534  * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
  1535  * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
  1536  *
  1537  * Any colon (:)in the Objective-C selector is replaced by an underscore (_).
  1538  * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
  1539  * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
  1540  */
  1541 + (NSString *)webScriptNameForSelector:(SEL)aSelector
  1542 {
  1543 	if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
  1544 	if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
  1545 	if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
  1546 	return @"";
  1547 }
  1548 
  1549 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
  1550 {
  1551 	NSMutableString *className = [[[img className] mutableCopy] autorelease];
  1552 	if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
  1553 		[className replaceOccurrencesOfString:@"fullSizeImage"
  1554 								   withString:@"scaledToFitImage"
  1555 									  options:NSLiteralSearch
  1556 										range:NSMakeRange(0, [className length])];
  1557 	else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
  1558 		[className replaceOccurrencesOfString:@"scaledToFitImage"
  1559 								   withString:@"fullSizeImage"
  1560 									  options:NSLiteralSearch
  1561 										range:NSMakeRange(0, [className length])];
  1562 	else 
  1563 		return NO;
  1564 	
  1565 	[img setClassName:className];
  1566 	[[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
  1567 
  1568 	return YES;
  1569 }
  1570 
  1571 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
  1572 
  1573 //gets the source of the html page, for debugging
  1574 - (NSString *)webviewSource
  1575 {
  1576 	return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
  1577 }
  1578 
  1579 /*!
  1580  * @brief Set the HTML content for the "Chat" area.
  1581  */
  1582 - (void)setChatContentSource:(NSString *)source
  1583 {
  1584 	if (!webViewIsReady) {
  1585 		// If the webview isn't ready yet, wait a very short amount of time before trying again
  1586 		[self performSelector:@selector(setChatContentSource:)
  1587 				   withObject:source
  1588 				   afterDelay:0];
  1589 	} else {
  1590 		// Add the old "Chat" element to the window.
  1591 		[(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
  1592 
  1593 		NSString	*scrollToBottomScript;		
  1594 		if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
  1595 			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
  1596 		}
  1597 	}
  1598 }
  1599 
  1600 /*!
  1601  * @brief Get the HTML content for the "Chat" area.
  1602  */
  1603 - (NSString *)chatContentSource
  1604 {
  1605 	return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
  1606 }
  1607 
  1608 /*!
  1609  * @brief The unique name for this style of "content source"
  1610  */
  1611 - (NSString *)contentSourceName
  1612 {
  1613 	return [[[messageStyle bundle] bundlePath] lastPathComponent];
  1614 }
  1615 
  1616 @end