Plugins/WebKit Message View/AIWebKitMessageViewController.m
author Zachary West <zacw@adium.im>
Tue Nov 10 10:56:52 2009 -0500 (2009-11-10)
changeset 2766 67be3e959a78
parent 2764 4b03c62063a5
child 2790 6de98889c273
permissions -rw-r--r--
Don't base focus application on content tracking, since status messages aren't tracked. Base it on whether or not the content is post-processed (i.e., logged). Refs #13356.
     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 		if (content.postProcessContent && adium.interfaceController.activeChat != content.chat) {
   685 			if (nextMessageFocus) {
   686 				[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
   687 				
   688 				// Add a class for "first content to lose focus"
   689 				[content addDisplayClass:@"firstFocus"];
   690 				
   691 				nextMessageFocus = NO;
   692 			}
   693 
   694 			// Add a class for "this content received while out of focus"
   695 			[content addDisplayClass:@"focus"];
   696 		}
   697 		
   698 		//Add the content object
   699 		[self _appendContent:content 
   700 					 similar:similar
   701    willAddMoreContentObjects:willAddMoreContentObjects
   702 		  replaceLastContent:replaceLastContent];
   703 	}
   704 
   705 	[previousContent release]; previousContent = [content retain];
   706 }
   707 
   708 /*!
   709  * @brief Append a content object
   710  */
   711 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
   712 {
   713 	[webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
   714 																					similar:contentIsSimilar
   715 																  willAddMoreContentObjects:willAddMoreContentObjects
   716 																		 replaceLastContent:replaceLastContent]];
   717 
   718 	NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
   719 }
   720 
   721 #pragma mark Topics
   722 /*!
   723  * @brief Force a topic update.
   724  *
   725  * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
   726  */
   727 - (void)updateTopic
   728 {
   729 	NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
   730 	
   731 	AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
   732 													withSource:chat.topicSetter
   733 												   destination:nil
   734 														  date:[NSDate date]
   735 													   message:topic];
   736 	
   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];
   739 	
   740 	[self enqueueContentObject:contentTopic];
   741 }
   742 
   743 //WebView Delegates ----------------------------------------------------------------------------------------------------
   744 #pragma mark Webview delegates
   745 
   746 - (void)webViewIsReady{
   747 	webViewIsReady = YES;
   748 	[self setupMarkedScroller];
   749 	[self setIsGroupChat:chat.isGroupChat];
   750 	[self processQueuedContent];
   751 }
   752 
   753 - (void)openImage:(id)sender
   754 {
   755 	NSURL	*imageURL = [sender representedObject];
   756 	[[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
   757 }
   758 
   759 - (void)saveImageAs:(id)sender
   760 {
   761 	NSURL		*imageURL = [sender representedObject];
   762 	NSString	*path = [imageURL path];
   763 	
   764 	NSSavePanel *savePanel = [NSSavePanel savePanel];
   765 	[savePanel beginSheetForDirectory:nil
   766 								 file:[path lastPathComponent]
   767 					   modalForWindow:[webView window]
   768 						modalDelegate:self
   769 					   didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
   770 						  contextInfo:[imageURL retain]];
   771 }
   772 
   773 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode  contextInfo:(void  *)contextInfo
   774 {
   775 	NSURL	*imageURL = (NSURL *)contextInfo;
   776 
   777 	if (returnCode ==  NSOKButton) {
   778 		[[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
   779 												toPath:[[sheet URL] absoluteString]
   780 												 error:NULL];
   781 	}
   782 	
   783 	[imageURL release];
   784 }
   785 
   786 /*!
   787  * @brief Append our own menu items to the webview's contextual menus
   788  */
   789 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
   790 {
   791 	NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
   792 	AIListContact	*chatListObject = chat.listObject.parentContact;
   793 	NSMenuItem		*menuItem;
   794 
   795 	//Remove default items we don't want
   796 	if (webViewMenuItems) {
   797 
   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];
   808 			} else {
   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];					
   812 			}
   813 		}
   814 	}
   815 	
   816 	NSURL	*imageURL;
   817 	if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
   818 		//This is an image		
   819 		if (!webViewMenuItems) {
   820 			webViewMenuItems = [NSMutableArray array];
   821 		}
   822 		
   823 		menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
   824 											  target:self
   825 											  action:@selector(openImage:)
   826 									   keyEquivalent:@""
   827 								   representedObject:imageURL];
   828 		[webViewMenuItems addObject:menuItem];
   829 		[menuItem release];
   830 		menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
   831 											  target:self
   832 											  action:@selector(saveImageAs:)
   833 									   keyEquivalent:@""
   834 								   representedObject:imageURL];
   835 		[webViewMenuItems addObject:menuItem];
   836 		[menuItem release];		
   837 		
   838 		/*
   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)
   845 		 */
   846 			
   847 	}
   848 
   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];
   855 			}
   856 			
   857 			[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   858 		}
   859 	} else {
   860 		webViewMenuItems = [NSMutableArray array];
   861 	}
   862 
   863 	NSMenu *originalMenu = nil;
   864 	
   865 	if (chatListObject) {
   866 		NSArray *locations;
   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];
   874 		} else {
   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];
   882 		}
   883 		
   884 		originalMenu = [adium.menuController contextualMenuWithLocations:locations
   885 														   forListObject:chatListObject
   886 																  inChat:chat];
   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]
   891 																 forChat:chat];
   892 	}
   893 	
   894 	[webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
   895 	[originalMenu removeAllItems];
   896 
   897 	if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
   898 		[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   899 
   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")
   902 										  target:self
   903 										  action:@selector(clearView)
   904 								   keyEquivalent:@""];
   905 	[webViewMenuItems addObject:menuItem];
   906 	[menuItem release];
   907 	
   908 	return webViewMenuItems;
   909 }
   910 
   911 /*!
   912  * @brief Add ourself to the window script object bridge when it's safe to do so
   913  */
   914 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
   915 {
   916     [[webView windowScriptObject] setValue:self forKey:@"client"];
   917 }
   918 
   919 //Dragging delegate ----------------------------------------------------------------------------------------------------
   920 #pragma mark Dragging delegate
   921 /*!
   922  * @brief If possible, return the first NSTextView in the message view's responder chain
   923  *
   924  * This is used for drag and drop behavior.
   925  */
   926 - (NSTextView *)textView
   927 {
   928 	id	responder = [webView nextResponder];
   929 	
   930 	//Walkin the responder chain looking for an NSTextView
   931 	while (responder &&
   932 		  ![responder isKindOfClass:[NSTextView class]]) {
   933 		responder = [responder nextResponder];
   934 	}
   935 	
   936 	return responder;
   937 }
   938 
   939 /*!
   940  * @brief Dragging entered
   941  */
   942 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   943 {
   944 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   945 
   946 	return ([pasteboard availableTypeFromArray:draggedTypes] ?
   947 		   NSDragOperationCopy :
   948 		   NSDragOperationNone);
   949 }
   950 
   951 /*!
   952 * @brief Dragging updated
   953  */
   954 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   955 {
   956 	return [self draggingEntered:sender];
   957 }
   958 
   959 /*!
   960  * @brief Handle a drag onto the webview
   961  * 
   962  * If we're getting a non-image file, we can handle it immediately.  Otherwise, the drag is the textView's problem.
   963  */
   964 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   965 {
   966 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   967 	BOOL			success = NO;
   968 	
   969 	if ([self shouldHandleDragWithPasteboard:pasteboard]) {
   970 		
   971 		//Not an image but it is a file - send it immediately as a file transfer
   972 		NSArray			*files = [pasteboard propertyListForType:NSFilenamesPboardType];
   973 		NSString		*path;
   974 		for (path in files) {
   975 			AIListObject *listObject = chat.listObject;
   976 			if (listObject) {
   977 				[adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
   978 			}
   979 		}
   980 		success = YES;
   981 		
   982 	} else {
   983 		NSTextView *textView = [self textView];
   984 		if (textView) {
   985 			[[webView window] makeFirstResponder:textView]; //Make it first responder
   986 			success = [textView performDragOperation:sender];
   987 		}
   988 	}
   989 	
   990 	return success;
   991 }
   992 
   993 /*!
   994  * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
   995  */
   996 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   997 {
   998 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   999 	BOOL	success = YES;
  1000 	
  1001 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {	
  1002 		NSTextView *textView = [self textView];
  1003 		if (textView) {
  1004 			success = [textView prepareForDragOperation:sender];
  1005 		}
  1006 	}
  1007 	
  1008 	return success;
  1009 }
  1010 	
  1011 /*!
  1012  * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
  1013  */
  1014 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
  1015 {
  1016 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1017 	
  1018 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {
  1019 		NSTextView *textView = [self textView];
  1020 		if (textView) {
  1021 			[textView concludeDragOperation:sender];
  1022 		}
  1023 	}
  1024 }
  1025 
  1026 /*!
  1027  * @brief Handle drags of content we recognize
  1028  */
  1029 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
  1030 {
  1031 	/*
  1032 	return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
  1033 			[pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
  1034 	 */
  1035 	return NO;
  1036 }
  1037 
  1038 
  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
  1043 /*!
  1044  * @brief Update icon masks when participating list objects change
  1045  *
  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.
  1049  */
  1050 - (void)participatingListObjectsChanged:(NSNotification *)notification
  1051 {
  1052 	NSArray			*participatingListObjects = [chat containedObjects];
  1053 	
  1054 	[[NSNotificationCenter defaultCenter] removeObserver:self
  1055 										  name:ListObject_AttributesChanged
  1056 										object:nil];
  1057 	
  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];
  1062 		}
  1063 		
  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]];
  1070 		}
  1071 	}
  1072 	
  1073 	//Also observe our account
  1074 	if (chat.account) {
  1075 		[[NSNotificationCenter defaultCenter] addObserver:self
  1076 									   selector:@selector(listObjectAttributesChanged:) 
  1077 										   name:ListObject_AttributesChanged
  1078 										 object:chat.account];
  1079 	}
  1080 
  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];
  1087 		}
  1088 	}
  1089 }
  1090 
  1091 /*!
  1092  * @brief Update icon masks when source or destination changes
  1093  */
  1094 - (void)sourceOrDestinationChanged:(NSNotification *)notification
  1095 {
  1096 	//Update the participating contacts
  1097 	[self participatingListObjectsChanged:nil];
  1098 	
  1099 	//And update the source account
  1100 	[self updateUserIconForObject:chat.account];
  1101 	
  1102 	[self updateServiceIcon];
  1103 }
  1104 
  1105 /*!
  1106  * @brief Update the icon when a list object's icon attributes change
  1107  */
  1108 - (void)listObjectAttributesChanged:(NSNotification *)notification
  1109 {
  1110     AIListObject	*inObject = [notification object];
  1111     NSSet			*keys = [[notification userInfo] objectForKey:@"Keys"];
  1112 	
  1113 	if ([keys containsObject:KEY_USER_ICON]) {
  1114 		if (inObject) {
  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;
  1120 			} else {
  1121 				/*
  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.
  1124 				 */
  1125 				
  1126 				for (AIListContact *participatingListObject in chat) {
  1127 					if ([participatingListObject parentContact] == inObject) {
  1128 						actualObject = participatingListObject;
  1129 						break;
  1130 					}
  1131 				}
  1132 			}
  1133 
  1134 			if (actualObject) {
  1135 				[self userIconForObjectDidChange:actualObject];
  1136 			}
  1137 
  1138 		} else {
  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];
  1142 		}
  1143 	}
  1144 }
  1145 
  1146 - (void)userIconForObjectDidChange:(AIListObject *)inObject
  1147 {
  1148 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1149 										 [(AIListContact *)inObject parentContact] :
  1150 										 inObject);
  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];
  1161 		} else {
  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.
  1164 			 */
  1165 		}
  1166 	}
  1167 	
  1168 	[self updateUserIconForObject:iconSourceObject];
  1169 }
  1170 
  1171 /*!
  1172  * @brief Remove all references to *this* chat using cached icons for an object
  1173  *
  1174  * If this is the last chat utilizing the cached icon, it will be deleted.
  1175  *
  1176  * @param inObject The object
  1177  */
  1178 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
  1179 {
  1180 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1181 										 [(AIListContact *)inObject parentContact] :
  1182 										 inObject);
  1183 	NSString		*path;
  1184 	
  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];
  1191 
  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];
  1198 	}
  1199 
  1200 	[objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
  1201 }
  1202 
  1203 /*!
  1204  * @brief Remove all references to *this* chat using cached icons for all involved objects
  1205  */
  1206 - (void)releaseAllCachedIcons
  1207 {
  1208 	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
  1209 		[self releaseCurrentWebKitUserIconForObject:listObject];
  1210 	}
  1211 }
  1212 
  1213 /*!
  1214  * @brief Generate an updated masked user icon for the passed list object
  1215  */
  1216 - (void)updateUserIconForObject:(AIListObject *)inObject
  1217 {
  1218 	AIListObject		*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1219 											 [(AIListContact *)inObject parentContact] :
  1220 											 inObject);
  1221 	NSImage				*userIcon;
  1222 	NSString			*oldWebKitUserIconPath = nil;
  1223 	NSString			*webKitUserIconPath = nil;
  1224 	NSImage				*webKitUserIcon;
  1225 	
  1226 	/*
  1227 	 * We probably already have a userIcon waiting for us, the active display icon; use that
  1228 	 * rather than loading one from disk.
  1229 	 */
  1230 	if (!(userIcon = [iconSourceObject userIcon])) {
  1231 		//If that's not the case, try using the UserIconPath
  1232 		NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
  1233 		if (userIconPath)
  1234 			userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
  1235 	}
  1236 
  1237 	if (userIcon) {
  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
  1246 						fraction:1.0];
  1247 			[webKitUserIcon unlockFocus];
  1248 		} else {
  1249 			//Otherwise, just use the icon as-is
  1250 			webKitUserIcon = userIcon;
  1251 		}
  1252 
  1253 		oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
  1254 		webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1255 
  1256 		if (!webKitUserIconPath) {
  1257 			/* If the image doesn't know a path to use, write it out and set it.
  1258 			 *
  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.
  1261 			 *
  1262 			 * Only write out the icon if the object doesn't already have one
  1263 			 */				
  1264 			webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
  1265 			if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
  1266 													 atomically:YES]) {
  1267 				[iconSourceObject setValue:webKitUserIconPath
  1268 										   forProperty:KEY_WEBKIT_USER_ICON
  1269 										   notify:NO];				
  1270 			}			
  1271 		}
  1272 
  1273 		//Make sure it's known that this user has been handled
  1274 		if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
  1275 			[objectsWithUserIconsArray addObject:iconSourceObject];
  1276 
  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];
  1281 		}
  1282 		
  1283 		if (!webKitUserIconPath) webKitUserIconPath = @"";
  1284 
  1285 		//Update existing images
  1286 		AILogWithSignature(@"Updating %@ to %@", oldWebKitUserIconPath, webKitUserIconPath);
  1287 
  1288 		if (oldWebKitUserIconPath &&
  1289 			![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
  1290 			DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1291 			NSUInteger imagesCount = [images length];
  1292 
  1293 			webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
  1294 
  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];
  1300 				}
  1301 			}
  1302 		}
  1303 
  1304 		[objectIconPathDict setObject:webKitUserIconPath
  1305 							   forKey:iconSourceObject.internalObjectID];
  1306 	}
  1307 }
  1308 
  1309 - (void)updateServiceIcon
  1310 {
  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:)])
  1314 		return; 
  1315 	DOMNodeList  *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
  1316 	NSUInteger imagesCount = [serviceIconImages length];
  1317 	
  1318 	NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID 
  1319 																type:AIServiceIconLarge];
  1320 	
  1321 	for (NSInteger i = 0; i < imagesCount; i++) {
  1322 		DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
  1323 		[img setSrc:serviceIconPath];
  1324 	}	
  1325 }
  1326 
  1327 - (void)customEmoticonUpdated:(NSNotification *)inNotification
  1328 {
  1329 	DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1330 	NSUInteger imagesCount = [images length];
  1331 
  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];
  1341 			
  1342 			if ([[img className] isEqualToString:@"emoticon"] &&
  1343 				[[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
  1344 				[img setSrc:path];
  1345 				[img setWidth:emoticonSize.width];
  1346 				[img setHeight:emoticonSize.height];
  1347 				updatedImage = YES;
  1348 			}
  1349 		}
  1350 		NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
  1351 																	 withArguments:nil];
  1352 		if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
  1353 
  1354 		if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat" 
  1355 															  withArguments:[NSArray arrayWithObject:shouldScroll]];
  1356 	}
  1357 }
  1358 
  1359 /*!
  1360  * @brief Returns the path the background image given a unique ID
  1361  */
  1362 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
  1363 {
  1364 	NSString	*filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
  1365 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1366 }
  1367 
  1368 /*!
  1369  * @brief Returns the path to the list object's masked user icon
  1370  */
  1371 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
  1372 {
  1373 	NSString	*filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
  1374 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1375 }
  1376 
  1377 #pragma mark File Transfer
  1378 
  1379 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
  1380 {
  1381 	ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
  1382 	ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
  1383 
  1384 	if (tc) {
  1385 		AIFileTransferAction a;
  1386 		if ([action isEqualToString:@"SaveAs"])
  1387 			a = AISaveFileAs;
  1388 		else if ([action isEqualToString:@"Cancel"]) 
  1389 			a = AICancel;
  1390 		else
  1391 			a = AISaveFile;
  1392 		
  1393 		[tc handleFileTransferAction:a];
  1394 	}
  1395 }
  1396 
  1397 #pragma mark Topic editing
  1398 - (void)editingDidComplete:(DOMRange *)range
  1399 {
  1400 	DOMNode *node = range.startContainer;
  1401 	DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
  1402 	
  1403 	NSString *topicChange = nil;
  1404 		
  1405 	if (node == topicEdit || node.parentNode == topicEdit) {
  1406 		topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];		
  1407 		
  1408 		NSTextView *textView = [self textView];
  1409 		if (textView) {
  1410 			[[webView window] makeFirstResponder:textView]; //Make it first responder
  1411 		}
  1412 		
  1413 		// Update the topic div in case the user doesn't have permission to change it.
  1414 		[self updateTopic];
  1415 		
  1416 		// Tell the chat to set the topic.
  1417 		[chat setTopic:topicChange];
  1418 	}
  1419 }
  1420 
  1421 #pragma mark Marked Scroller
  1422 - (JVMarkedScroller *)markedScroller
  1423 {
  1424 	WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
  1425 	NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
  1426 	
  1427 	JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
  1428 	return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
  1429 }
  1430 
  1431 - (void)setupMarkedScroller
  1432 {
  1433 	WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
  1434 	NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
  1435 	
  1436 	[scrollView setHasHorizontalScroller:NO];
  1437 
  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];
  1446 	}
  1447 }
  1448 
  1449 - (NSNumber *)currentOffsetHeight
  1450 {
  1451 	// We use the body's height to determine our mark location.
  1452 	return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
  1453 }
  1454 
  1455 - (void)markCurrentLocation
  1456 {
  1457 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
  1458 }
  1459 
  1460 #define PREF_KEY_FOCUS_LINE	@"Draw Focus Lines"
  1461 
  1462 - (void)markForFocusChange
  1463 {
  1464 	// We use the current Chat element's height to determine our mark location.
  1465 	[self.markedScroller removeMarkWithIdentifier:@"focus"];
  1466 	
  1467 	// The next message being inserted needs to add a mark.
  1468 	nextMessageFocus = YES;
  1469 	
  1470 	DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
  1471 	DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
  1472 	for (NSUInteger i = 0; i < nodeList.length; i++)
  1473 	{
  1474 		node = (DOMHTMLElement *)[nodeList item:i];
  1475 		classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
  1476 		
  1477 		[classes removeObject:@"focus"];
  1478 		[classes removeObject:@"firstFocus"];
  1479 		
  1480 		node.className = [classes componentsJoinedByString:@" "];
  1481 		
  1482 		[classes release];
  1483 	}
  1484 	
  1485 }
  1486 
  1487 - (void)addMark
  1488 {
  1489 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
  1490 }
  1491 
  1492 - (void)jumpToPreviousMark
  1493 {
  1494 	[self.markedScroller jumpToPreviousMark:nil];
  1495 }
  1496 
  1497 - (BOOL)previousMarkExists
  1498 {
  1499 	return [self.markedScroller previousMarkExists];
  1500 }
  1501 
  1502 - (void)jumpToNextMark
  1503 {
  1504 	[self.markedScroller jumpToNextMark:nil];	
  1505 }
  1506 
  1507 - (BOOL)nextMarkExists
  1508 {
  1509 	return [self.markedScroller nextMarkExists];	
  1510 }
  1511 
  1512 - (void)jumpToFocusMark
  1513 {
  1514 	[self.markedScroller jumpToFocusMark:nil];
  1515 }
  1516 
  1517 - (BOOL)focusMarkExists
  1518 {
  1519 	return [self.markedScroller focusMarkExists];
  1520 }
  1521 
  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.
  1524 */
  1525 
  1526 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
  1527 {
  1528 	if (
  1529 		sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
  1530 		sel_isEqual(aSelector, @selector(debugLog:)) ||
  1531 		sel_isEqual(aSelector, @selector(zoomImage:))
  1532 	)
  1533 		return NO;
  1534 	
  1535 	return YES;
  1536 }
  1537 
  1538 /*
  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:
  1542  *
  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.
  1546  */
  1547 + (NSString *)webScriptNameForSelector:(SEL)aSelector
  1548 {
  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";
  1552 	return @"";
  1553 }
  1554 
  1555 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
  1556 {
  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])];
  1568 	else 
  1569 		return NO;
  1570 	
  1571 	[img setClassName:className];
  1572 	[[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
  1573 
  1574 	return YES;
  1575 }
  1576 
  1577 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
  1578 
  1579 //gets the source of the html page, for debugging
  1580 - (NSString *)webviewSource
  1581 {
  1582 	return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
  1583 }
  1584 
  1585 /*!
  1586  * @brief Set the HTML content for the "Chat" area.
  1587  */
  1588 - (void)setChatContentSource:(NSString *)source
  1589 {
  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:)
  1593 				   withObject:source
  1594 				   afterDelay:0];
  1595 	} else {
  1596 		// Add the old "Chat" element to the window.
  1597 		[(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
  1598 
  1599 		NSString	*scrollToBottomScript;		
  1600 		if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
  1601 			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
  1602 		}
  1603 	}
  1604 }
  1605 
  1606 /*!
  1607  * @brief Get the HTML content for the "Chat" area.
  1608  */
  1609 - (NSString *)chatContentSource
  1610 {
  1611 	return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
  1612 }
  1613 
  1614 /*!
  1615  * @brief The unique name for this style of "content source"
  1616  */
  1617 - (NSString *)contentSourceName
  1618 {
  1619 	return [[[messageStyle bundle] bundlePath] lastPathComponent];
  1620 }
  1621 
  1622 @end