Plugins/WebKit Message View/AIWebKitMessageViewController.m
author Evan Schoenberg
Mon Nov 23 14:41:32 2009 -0600 (2009-11-23)
changeset 2951 9232899b7ef5
parent 2939 9f11b96baf1d
child 2954 31f6f4c62d78
permissions -rw-r--r--
For our `WebView`s, use a preferences identifier of activeStyle-preferenceGroup. This uniques group chats from one-on-one chats when their preferences are being stored separately. The WebView preference identifier is used only transiently in our usage to allow all WebViews to update when one changes (via the preferences); this doesn't change our actual storage and retrieval of the prefs. Fixes #12810 by implementing Zac's suggestion there.
     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:[NSString stringWithFormat:@"%@-%@",
   356 									   activeStyle, preferenceGroup]];
   357 
   358 	//Get the prefered variant (or the default if a prefered is not available)
   359 	activeVariant = [[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Variant" forStyle:activeStyle]
   360 															  group:preferenceGroup] retain];
   361 	if (!activeVariant) activeVariant = [[messageStyle defaultVariant] retain];
   362 	if (!activeVariant) {
   363 		/* If the message style doesn't specify a default variant, choose the first one.
   364 		 * Note: Old styles (styleVersion < 3) will always report a variant for defaultVariant.
   365 		 */
   366 		NSArray *availableVariants = [messageStyle availableVariants];
   367 		if ([availableVariants count]) {
   368 			activeVariant = [[availableVariants objectAtIndex:0] retain];
   369 		}
   370 	}
   371 	
   372 	NSDictionary *prefDict = [adium.preferenceController preferencesForGroup:preferenceGroup];
   373 
   374 	//Update message style behavior: XXX move this somewhere not per-chat
   375 	[messageStyle setShowUserIcons:[[prefDict objectForKey:KEY_WEBKIT_SHOW_USER_ICONS] boolValue]];
   376 	[messageStyle setShowHeader:[[prefDict objectForKey:KEY_WEBKIT_SHOW_HEADER] boolValue]];
   377 	[messageStyle setUseCustomNameFormat:[[prefDict objectForKey:KEY_WEBKIT_USE_NAME_FORMAT] boolValue]];
   378 	[messageStyle setNameFormat:[[prefDict objectForKey:KEY_WEBKIT_NAME_FORMAT] integerValue]];
   379 	[messageStyle setDateFormat:[prefDict objectForKey:KEY_WEBKIT_TIME_STAMP_FORMAT]];
   380 	[messageStyle setShowIncomingMessageColors:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_COLORS] boolValue]];
   381 	[messageStyle setShowIncomingMessageFonts:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_FONTS] boolValue]];
   382 	
   383 	//Custom background image
   384 	//Webkit wants to load these from disk, but we have it stuffed in a plist.  So we'll write it out as an image
   385 	//into the cache and have webkit fetch from there.
   386 	NSString	*cachePath = nil;
   387 	if ([[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"UseCustomBackground" forStyle:activeStyle]
   388 												  group:preferenceGroup] boolValue]) {
   389 		cachePath = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
   390 															 group:preferenceGroup];
   391 		if (!cachePath || ![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
   392 			NSData	*backgroundImage = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Background" forStyle:activeStyle]
   393 																				group:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
   394 			
   395 			if (backgroundImage) {
   396 				//Generate a unique cache ID for this image
   397 				NSInteger	uniqueID = [[adium.preferenceController preferenceForKey:@"BackgroundCacheUniqueID"
   398 																		 group:preferenceGroup] integerValue] + 1;
   399 				[adium.preferenceController setPreference:[NSNumber numberWithInteger:uniqueID]
   400 													 forKey:@"BackgroundCacheUniqueID"
   401 													  group:preferenceGroup];
   402 				
   403 				//Cache the image under that unique ID
   404 				//Since we prefix the filename with TEMP, Adium will automatically clean it up on quit
   405 				cachePath = [self _webKitBackgroundImagePathForUniqueID:uniqueID];
   406 				[backgroundImage writeToFile:cachePath atomically:YES];
   407 
   408 				//Remember where we cached it
   409 				[adium.preferenceController setPreference:cachePath
   410 													 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
   411 													  group:preferenceGroup];
   412 			} else {
   413 				cachePath = @""; //No custom image found
   414 			}
   415 		}
   416 		
   417 		[messageStyle setCustomBackgroundColor:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundColor" forStyle:activeStyle]
   418 																						 group:preferenceGroup] representedColor]];
   419 	} else {
   420 		[messageStyle setCustomBackgroundColor:nil];
   421 	}
   422 
   423 	[messageStyle setCustomBackgroundPath:cachePath];
   424 	[messageStyle setCustomBackgroundType:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundType" forStyle:activeStyle]
   425 																					group:preferenceGroup] integerValue]];
   426 	
   427 	BOOL isBackgroundTransparent = [[self messageStyle] isBackgroundTransparent];
   428 	[webView setTransparent:isBackgroundTransparent];
   429 	NSWindow *win = [webView window];
   430 	if(win)
   431 		[win setOpaque:!isBackgroundTransparent];
   432 
   433 	//Update webview font settings
   434 	NSString	*fontFamily = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontFamily" forStyle:activeStyle]
   435 																	group:preferenceGroup];
   436 	[webView setFontFamily:(fontFamily ? fontFamily : [messageStyle defaultFontFamily])];
   437 	
   438 	NSNumber	*fontSize = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontSize" forStyle:activeStyle]
   439 																  group:preferenceGroup];
   440 	[[webView preferences] setDefaultFontSize:[(fontSize ? fontSize : [messageStyle defaultFontSize]) integerValue]];
   441 	
   442 	NSNumber	*minSize = [adium.preferenceController preferenceForKey:KEY_WEBKIT_MIN_FONT_SIZE
   443 																 group:preferenceGroup];
   444 	[[webView preferences] setMinimumFontSize:(minSize ? [minSize integerValue] : 1)];
   445 
   446 	//Update our icons before doing any loading
   447 	[self sourceOrDestinationChanged:nil];
   448 
   449 	//Prime the webview with the new style/variant and settings, and re-insert all our content back into the view
   450 	[self _primeWebViewAndReprocessContent:YES];	
   451 }
   452 
   453 /*!
   454  * @brief Updates our webview to the currently active varient without refreshing the view
   455  */
   456 - (void)_updateVariantWithoutPrimingView
   457 {
   458 	//We can only change the variant if the web view is ready.  If it's not ready we wait a bit and try again.
   459 	if (webViewIsReady) {
   460 		[webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForChangingVariant:activeVariant]];			
   461 	} else {
   462 		[self performSelector:@selector(_updateVariantWithoutPrimingView) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
   463 	}
   464 }
   465 
   466 /*!
   467  *	@brief Clears the view from displayed messages
   468  *
   469  *	Implements the method defined in protocol AIMessageDisplayController
   470  */
   471 - (void)clearView
   472 {
   473 	[self _primeWebViewAndReprocessContent:NO];
   474 	[self.markedScroller removeAllMarks];
   475 	[previousContent release];
   476 	previousContent = nil;
   477 	nextMessageFocus = NO;
   478 	[chat clearUnviewedContentCount];
   479 }
   480 
   481 /*!
   482  * @brief Primes our webview to the currently active style and variant
   483  *
   484  * The webview won't be ready right away, so we flag it as not ready and set ourself as the frame load delegate so
   485  * it will let us know when it's good to go.  If reprocessContent is NO, all content in the view will be lost.
   486  */
   487 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent
   488 {
   489 	webViewIsReady = NO;
   490 
   491 	//Hack: this will re-set us for all the delegates, but that shouldn't matter
   492 	[delegateProxy addDelegate:self forView:webView];
   493 	[[webView mainFrame] loadHTMLString:[messageStyle baseTemplateWithVariant:activeVariant chat:chat] baseURL:nil];
   494 
   495 	if(chat.isGroupChat && chat.supportsTopic) {
   496 		// Force a topic update, so we set our topic appropriately.
   497 		[self updateTopic];
   498 	}
   499 	
   500 	if (reprocessContent) {
   501 		NSArray	*currentContentQueue;
   502 		
   503 		//Keep the array of objects waiting to be added, if necessary, to append them after our currently displayed ones
   504 		currentContentQueue = ([contentQueue count] ?
   505 							   [contentQueue copy] :
   506 							   nil);
   507 
   508 		//Start from an empty content queue
   509 		[contentQueue removeAllObjects];
   510 
   511 		//Add our stored content objects to the content queue
   512 		[contentQueue addObjectsFromArray:storedContentObjects];
   513 		[storedContentObjects removeAllObjects];
   514 
   515 		//Add the old content queue back in if necessary
   516 		if (currentContentQueue) {
   517 			[contentQueue addObjectsFromArray:currentContentQueue];
   518 			[currentContentQueue release];
   519 		}
   520 
   521 		//We're still holding onto the previousContent from before, which is no longer accurate. Release it.
   522 		[previousContent release]; previousContent = nil;
   523 	}
   524 }
   525 
   526 /*!
   527  * @brief Sets the class 'groupchat' on the #Chat element, to allow styles to modify their appearance based on whether we're in a groupchat
   528  *
   529  * If/when we support transforming chats to/from groupchats we'll need to observe that and call this as appropriate
   530  */
   531 - (void) setIsGroupChat:(BOOL) flag
   532 {
   533 	DOMHTMLElement *chatElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"];
   534 	NSMutableString *chatClassName = [[[chatElement className] mutableCopy] autorelease];
   535 	if (flag == NO)
   536 		[chatClassName replaceOccurrencesOfString:@" groupchat"
   537 									   withString:@""
   538 										  options:NSLiteralSearch
   539 											range:NSMakeRange(0, [chatClassName length])];
   540 	else
   541 		[chatClassName appendString:@" groupchat"];
   542 	[chatElement setClassName:chatClassName];
   543 }
   544 
   545 //Content --------------------------------------------------------------------------------------------------------------
   546 #pragma mark Content
   547 /*!
   548  * @brief Append new content to our processing queue
   549  */
   550 - (void)contentObjectAdded:(NSNotification *)notification
   551 {
   552 	AIContentObject	*contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
   553 	[self enqueueContentObject:contentObject];
   554 }
   555 
   556 - (void)enqueueContentObject:(AIContentObject *)contentObject
   557 {
   558 	[contentQueue addObject:contentObject];
   559 	
   560 	/* Immediately update our display if the content requires it.
   561 	* This is NO, for example, when we receive an entire block of message history content so that we can avoid scrolling
   562 	* after each one.
   563 	*/
   564 	if ([contentObject displayContentImmediately]) {
   565 		[self processQueuedContent];
   566 	}
   567 }
   568 
   569 /*!
   570  * @brief Our chat finished adding untracked content
   571  */
   572 - (void)chatDidFinishAddingUntrackedContent:(NSNotification *)notification
   573 {
   574 	[self processQueuedContent];	
   575 }
   576 
   577 /*!
   578  * @brief Append new content to our processing queueProcess any content in the queuee
   579  */
   580 - (void)processQueuedContent
   581 {
   582 	/* If the webview isn't ready, assume we have at least one piece of content left to display */
   583 	NSUInteger	contentQueueCount = 1;
   584 	NSUInteger	objectsAdded = 0;
   585 	
   586 	if (webViewIsReady) {
   587 		contentQueueCount = contentQueue.count;
   588 
   589 		while (contentQueueCount > 0) {
   590 			BOOL willAddMoreContent = (contentQueueCount > 1);
   591 			
   592 			//Display the content
   593 			AIContentObject *content = [contentQueue objectAtIndex:0];
   594 			[self _processContentObject:content 
   595 			  willAddMoreContentObjects:willAddMoreContent];
   596 
   597 			//If we are going to reflect preference changes, store this content object
   598 			if (shouldReflectPreferenceChanges) {
   599 				[storedContentObjects addObject:content];
   600 			}
   601 
   602 			//Remove the content we just displayed from the queue
   603 			[contentQueue removeObjectAtIndex:0];
   604 			objectsAdded++;
   605 			contentQueueCount--;
   606 		}
   607 	}
   608 	
   609 	/* If we added two or more objects, we may want to scroll to the bottom now, having not done it as each object
   610 	 * was added.
   611 	 */
   612 	if (objectsAdded > 1) {
   613 		NSString	*scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects];
   614 		
   615 		if (scrollToBottomScript) {
   616 			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
   617 		}
   618 	}
   619 	
   620 	//If there is still content to process (the webview wasn't ready), we'll try again after a brief delay
   621 	if (contentQueueCount) {
   622 		[self performSelector:@selector(processQueuedContent) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
   623 	}
   624 }
   625 
   626 /*!
   627  * @brief Process and then append a content object
   628  */
   629 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects
   630 {
   631 	AIContentEvent	*dateSeparator = nil;
   632 	BOOL			replaceLastContent = NO;
   633 
   634 	/*
   635 	 If the day has changed since our last message (or if there was no previous message and 
   636 	 we are about to display context), insert a date line.
   637 	 */
   638 	if ((!previousContent && [content isKindOfClass:[AIContentContext class]]) ||
   639 	   (![content isFromSameDayAsContent:previousContent])) {
   640 		
   641 		NSString *dateMessage = [[NSDateFormatter localizedDateFormatter] stringFromDate:content.date];
   642 		
   643 		dateSeparator = [AIContentEvent statusInChat:content.chat
   644 										  withSource:content.chat.listObject
   645 										 destination:content.chat.account
   646 												date:content.date
   647 											 message:[[[NSAttributedString alloc] initWithString:dateMessage
   648 																					  attributes:[adium.contentController defaultFormattingAttributes]] autorelease]
   649 											withType:@"date_separator"];
   650 
   651 		if ([content isKindOfClass:[AIContentContext class]])
   652 			[dateSeparator addDisplayClass:@"history"];
   653 
   654 		//Add the date header
   655 		[self _appendContent:dateSeparator 
   656 					 similar:NO
   657 			willAddMoreContentObjects:YES
   658 		  replaceLastContent:NO];
   659 		[previousContent release]; previousContent = [dateSeparator retain];
   660 	}
   661 	
   662 	BOOL similar = (previousContent && [content isSimilarToContent:previousContent] && ![content isKindOfClass:[ESFileTransfer class]]);
   663 	if ([previousContent isKindOfClass:[AIContentStatus class]] && [content isKindOfClass:[AIContentStatus class]] &&
   664 		[[(AIContentStatus *)previousContent coalescingKey] isEqualToString:[(AIContentStatus *)content coalescingKey]]) {
   665 		similar = NO;
   666 		replaceLastContent = YES;
   667 	}
   668 
   669 	if ([content.type isEqualToString:CONTENT_TOPIC_TYPE]) {
   670 		DOMHTMLElement *topicElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topic"];
   671 		
   672 		if (((AIContentTopic *)content).actuallyBlank) {
   673 			content.message = [NSAttributedString stringWithString:@""];
   674 		}
   675 		
   676 		[topicElement setTitle:content.message.string];
   677 		
   678 		[topicElement setInnerHTML:[messageStyle completedTemplateForContent:content similar:similar]];
   679 	} else {
   680 		// Mark the current location (the start of this element) if it's a mention.
   681 		if (content.trackContent && [content.displayClasses containsObject:@"mention"]) {
   682 			[self markCurrentLocation];
   683 		}
   684 		
   685 		if (content.postProcessContent && adium.interfaceController.activeChat != content.chat) {
   686 			if (nextMessageFocus) {
   687 				[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
   688 				
   689 				// Add a class for "first content to lose focus"
   690 				[content addDisplayClass:@"firstFocus"];
   691 				
   692 				nextMessageFocus = NO;
   693 			}
   694 
   695 			// Add a class for "this content received while out of focus"
   696 			[content addDisplayClass:@"focus"];
   697 		}
   698 		
   699 		//Add the content object
   700 		[self _appendContent:content 
   701 					 similar:similar
   702    willAddMoreContentObjects:willAddMoreContentObjects
   703 		  replaceLastContent:replaceLastContent];
   704 	}
   705 
   706 	[previousContent release]; previousContent = [content retain];
   707 }
   708 
   709 /*!
   710  * @brief Append a content object
   711  */
   712 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
   713 {
   714 	[webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
   715 																					similar:contentIsSimilar
   716 																  willAddMoreContentObjects:willAddMoreContentObjects
   717 																		 replaceLastContent:replaceLastContent]];
   718 
   719 	NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
   720 }
   721 
   722 #pragma mark Topics
   723 /*!
   724  * @brief Force a topic update.
   725  *
   726  * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
   727  */
   728 - (void)updateTopic
   729 {
   730 	NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
   731 	
   732 	AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
   733 													withSource:chat.topicSetter
   734 												   destination:nil
   735 														  date:[NSDate date]
   736 													   message:topic];
   737 	
   738 	// In case this topic is blank, we have to filter this ourself; the content controller will drop it.
   739 	contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
   740 	
   741 	[self enqueueContentObject:contentTopic];
   742 }
   743 
   744 //WebView Delegates ----------------------------------------------------------------------------------------------------
   745 #pragma mark Webview delegates
   746 
   747 - (void)webViewIsReady{
   748 	webViewIsReady = YES;
   749 	[self setupMarkedScroller];
   750 	[self setIsGroupChat:chat.isGroupChat];
   751 	[self processQueuedContent];
   752 }
   753 
   754 - (void)openImage:(id)sender
   755 {
   756 	NSURL	*imageURL = [sender representedObject];
   757 	[[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
   758 }
   759 
   760 - (void)saveImageAs:(id)sender
   761 {
   762 	NSURL		*imageURL = [sender representedObject];
   763 	NSString	*path = [imageURL path];
   764 	
   765 	NSSavePanel *savePanel = [NSSavePanel savePanel];
   766 	[savePanel beginSheetForDirectory:nil
   767 								 file:[path lastPathComponent]
   768 					   modalForWindow:[webView window]
   769 						modalDelegate:self
   770 					   didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
   771 						  contextInfo:[imageURL retain]];
   772 }
   773 
   774 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode  contextInfo:(void  *)contextInfo
   775 {
   776 	NSURL	*imageURL = (NSURL *)contextInfo;
   777 
   778 	if (returnCode ==  NSOKButton) {
   779 		[[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
   780 												toPath:[[sheet URL] absoluteString]
   781 												 error:NULL];
   782 	}
   783 	
   784 	[imageURL release];
   785 }
   786 
   787 /*!
   788  * @brief Append our own menu items to the webview's contextual menus
   789  */
   790 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
   791 {
   792 	NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
   793 	AIListContact	*chatListObject = chat.listObject.parentContact;
   794 	NSMenuItem		*menuItem;
   795 
   796 	//Remove default items we don't want
   797 	if (webViewMenuItems) {
   798 
   799 		for (menuItem in defaultMenuItems) {
   800 			NSInteger tag = [menuItem tag];
   801 			if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
   802 				(tag == WebMenuItemTagDownloadLinkToDisk) ||
   803 				(tag == WebMenuItemTagOpenImageInNewWindow) ||
   804 				(tag == WebMenuItemTagDownloadImageToDisk) ||
   805 				(tag == WebMenuItemTagOpenFrameInNewWindow) ||
   806 				(tag == WebMenuItemTagStop) ||
   807 				(tag == WebMenuItemTagReload)) {
   808 				[webViewMenuItems removeObjectIdenticalTo:menuItem];
   809 			} else {
   810 				//This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
   811 				if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
   812 					[webViewMenuItems removeObjectIdenticalTo:menuItem];					
   813 			}
   814 		}
   815 	}
   816 	
   817 	NSURL	*imageURL;
   818 	if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
   819 		//This is an image		
   820 		if (!webViewMenuItems) {
   821 			webViewMenuItems = [NSMutableArray array];
   822 		}
   823 		
   824 		menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
   825 											  target:self
   826 											  action:@selector(openImage:)
   827 									   keyEquivalent:@""
   828 								   representedObject:imageURL];
   829 		[webViewMenuItems addObject:menuItem];
   830 		[menuItem release];
   831 		menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
   832 											  target:self
   833 											  action:@selector(saveImageAs:)
   834 									   keyEquivalent:@""
   835 								   representedObject:imageURL];
   836 		[webViewMenuItems addObject:menuItem];
   837 		[menuItem release];		
   838 		
   839 		/*
   840 		NSString *imgClass = [img className];
   841 		//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.
   842 		if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
   843 		   [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
   844 		   [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
   845 		   [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
   846 		 */
   847 			
   848 	}
   849 
   850 	if (webViewMenuItems) {		
   851 		//Add a separator item if items already exist in webViewMenuItems
   852 		if ([webViewMenuItems count]) {
   853 			// If the first item is a separator item, remove it.
   854 			if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
   855 				[webViewMenuItems removeObjectAtIndex:0];
   856 			}
   857 			
   858 			[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   859 		}
   860 	} else {
   861 		webViewMenuItems = [NSMutableArray array];
   862 	}
   863 
   864 	NSMenu *originalMenu = nil;
   865 	
   866 	if (chatListObject) {
   867 		NSArray *locations;
   868 		if ([chatListObject isIntentionallyNotAStranger]) {
   869 			locations = [NSArray arrayWithObjects:
   870 				[NSNumber numberWithInteger:Context_Contact_Manage],
   871 				[NSNumber numberWithInteger:Context_Contact_Action],
   872 				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
   873 				[NSNumber numberWithInteger:Context_Contact_ChatAction],
   874 				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
   875 		} else {
   876 			locations = [NSArray arrayWithObjects:
   877 				[NSNumber numberWithInteger:Context_Contact_Manage],
   878 				[NSNumber numberWithInteger:Context_Contact_Action],
   879 				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
   880 				[NSNumber numberWithInteger:Context_Contact_ChatAction],
   881 				[NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
   882 				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
   883 		}
   884 		
   885 		originalMenu = [adium.menuController contextualMenuWithLocations:locations
   886 														   forListObject:chatListObject
   887 																  inChat:chat];
   888 	} else if(chat.isGroupChat) {
   889 		originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
   890 																		  [NSNumber numberWithInteger:Context_GroupChat_Manage],
   891 																		  [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
   892 																 forChat:chat];
   893 	}
   894 	
   895 	[webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
   896 	[originalMenu removeAllItems];
   897 
   898 	if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
   899 		[webViewMenuItems addObject:[NSMenuItem separatorItem]];
   900 
   901 	//Present an option to clear the display
   902 	menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
   903 										  target:self
   904 										  action:@selector(clearView)
   905 								   keyEquivalent:@""];
   906 	[webViewMenuItems addObject:menuItem];
   907 	[menuItem release];
   908 	
   909 	return webViewMenuItems;
   910 }
   911 
   912 /*!
   913  * @brief Add ourself to the window script object bridge when it's safe to do so
   914  */
   915 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
   916 {
   917     [[webView windowScriptObject] setValue:self forKey:@"client"];
   918 }
   919 
   920 //Dragging delegate ----------------------------------------------------------------------------------------------------
   921 #pragma mark Dragging delegate
   922 /*!
   923  * @brief If possible, return the first NSTextView in the message view's responder chain
   924  *
   925  * This is used for drag and drop behavior.
   926  */
   927 - (NSTextView *)textView
   928 {
   929 	id	responder = [webView nextResponder];
   930 	
   931 	//Walkin the responder chain looking for an NSTextView
   932 	while (responder &&
   933 		  ![responder isKindOfClass:[NSTextView class]]) {
   934 		responder = [responder nextResponder];
   935 	}
   936 	
   937 	return responder;
   938 }
   939 
   940 /*!
   941  * @brief Dragging entered
   942  */
   943 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
   944 {
   945 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   946 
   947 	return ([pasteboard availableTypeFromArray:draggedTypes] ?
   948 		   NSDragOperationCopy :
   949 		   NSDragOperationNone);
   950 }
   951 
   952 /*!
   953 * @brief Dragging updated
   954  */
   955 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
   956 {
   957 	return [self draggingEntered:sender];
   958 }
   959 
   960 /*!
   961  * @brief Handle a drag onto the webview
   962  * 
   963  * If we're getting a non-image file, we can handle it immediately.  Otherwise, the drag is the textView's problem.
   964  */
   965 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
   966 {
   967 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
   968 	BOOL			success = NO;
   969 	
   970 	if ([self shouldHandleDragWithPasteboard:pasteboard]) {
   971 		
   972 		//Not an image but it is a file - send it immediately as a file transfer
   973 		NSArray			*files = [pasteboard propertyListForType:NSFilenamesPboardType];
   974 		NSString		*path;
   975 		for (path in files) {
   976 			AIListObject *listObject = chat.listObject;
   977 			if (listObject) {
   978 				[adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
   979 			}
   980 		}
   981 		success = YES;
   982 		
   983 	} else {
   984 		NSTextView *textView = [self textView];
   985 		if (textView) {
   986 			[[webView window] makeFirstResponder:textView]; //Make it first responder
   987 			success = [textView performDragOperation:sender];
   988 		}
   989 	}
   990 	
   991 	return success;
   992 }
   993 
   994 /*!
   995  * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
   996  */
   997 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
   998 {
   999 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1000 	BOOL	success = YES;
  1001 	
  1002 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {	
  1003 		NSTextView *textView = [self textView];
  1004 		if (textView) {
  1005 			success = [textView prepareForDragOperation:sender];
  1006 		}
  1007 	}
  1008 	
  1009 	return success;
  1010 }
  1011 	
  1012 /*!
  1013  * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
  1014  */
  1015 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
  1016 {
  1017 	NSPasteboard	*pasteboard = [sender draggingPasteboard];
  1018 	
  1019 	if (![self shouldHandleDragWithPasteboard:pasteboard]) {
  1020 		NSTextView *textView = [self textView];
  1021 		if (textView) {
  1022 			[textView concludeDragOperation:sender];
  1023 		}
  1024 	}
  1025 }
  1026 
  1027 /*!
  1028  * @brief Handle drags of content we recognize
  1029  */
  1030 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
  1031 {
  1032 	/*
  1033 	return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
  1034 			[pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
  1035 	 */
  1036 	return NO;
  1037 }
  1038 
  1039 
  1040 //User Icon masking --------------------------------------------------------------------------------------------------
  1041 //We allow messaage styles to specify masks for user icons.  This could be user to round the corners of user icons 
  1042 //or other related effects.
  1043 #pragma mark User icon masking
  1044 /*!
  1045  * @brief Update icon masks when participating list objects change
  1046  *
  1047  * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
  1048  * When the list changes, remove the observers we had in place before and add observers for each object in the list
  1049  * so we never observe for contacts not in the chat.
  1050  */
  1051 - (void)participatingListObjectsChanged:(NSNotification *)notification
  1052 {
  1053 	NSArray			*participatingListObjects = [chat containedObjects];
  1054 	
  1055 	[[NSNotificationCenter defaultCenter] removeObserver:self
  1056 										  name:ListObject_AttributesChanged
  1057 										object:nil];
  1058 	
  1059 	for (AIListObject *listObject in participatingListObjects) {
  1060 		//Update the mask for any user which just entered the chat
  1061 		if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
  1062 			[self updateUserIconForObject:listObject];
  1063 		}
  1064 		
  1065 		//In the future, watch for changes on the parent object, since that's the icon we display
  1066 		if ([listObject isKindOfClass:[AIListContact class]]) {
  1067 			[[NSNotificationCenter defaultCenter] addObserver:self
  1068 										   selector:@selector(listObjectAttributesChanged:) 
  1069 											   name:ListObject_AttributesChanged
  1070 											 object:[(AIListContact *)listObject parentContact]];
  1071 		}
  1072 	}
  1073 	
  1074 	//Also observe our account
  1075 	if (chat.account) {
  1076 		[[NSNotificationCenter defaultCenter] addObserver:self
  1077 									   selector:@selector(listObjectAttributesChanged:) 
  1078 										   name:ListObject_AttributesChanged
  1079 										 object:chat.account];
  1080 	}
  1081 
  1082 	//Remove the cache for any object no longer in the chat
  1083 	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
  1084 		if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
  1085 			(![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
  1086 			!(listObject == chat.account)) {
  1087 			[self releaseCurrentWebKitUserIconForObject:listObject];
  1088 		}
  1089 	}
  1090 }
  1091 
  1092 /*!
  1093  * @brief Update icon masks when source or destination changes
  1094  */
  1095 - (void)sourceOrDestinationChanged:(NSNotification *)notification
  1096 {
  1097 	//Update the participating contacts
  1098 	[self participatingListObjectsChanged:nil];
  1099 	
  1100 	//And update the source account
  1101 	[self updateUserIconForObject:chat.account];
  1102 	
  1103 	[self updateServiceIcon];
  1104 }
  1105 
  1106 /*!
  1107  * @brief Update the icon when a list object's icon attributes change
  1108  */
  1109 - (void)listObjectAttributesChanged:(NSNotification *)notification
  1110 {
  1111     AIListObject	*inObject = [notification object];
  1112     NSSet			*keys = [[notification userInfo] objectForKey:@"Keys"];
  1113 	
  1114 	if ([keys containsObject:KEY_USER_ICON]) {
  1115 		if (inObject) {
  1116 			AIListObject	*actualObject = nil;
  1117 			AILogWithSignature(@"%@'s icon changed", inObject);
  1118 			if (chat.account == inObject) {
  1119 				//The account is the object actually in the chat
  1120 				actualObject = inObject;
  1121 			} else {
  1122 				/*
  1123 				 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
  1124 				 * be displaying as changed.
  1125 				 */
  1126 				
  1127 				for (AIListContact *participatingListObject in chat) {
  1128 					if ([participatingListObject parentContact] == inObject) {
  1129 						actualObject = participatingListObject;
  1130 						break;
  1131 					}
  1132 				}
  1133 			}
  1134 
  1135 			if (actualObject) {
  1136 				[self userIconForObjectDidChange:actualObject];
  1137 			}
  1138 
  1139 		} else {
  1140 			AILogWithSignature(@"nil object's icon changed");
  1141 			//We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
  1142 			[self sourceOrDestinationChanged:nil];
  1143 		}
  1144 	}
  1145 }
  1146 
  1147 - (void)userIconForObjectDidChange:(AIListObject *)inObject
  1148 {
  1149 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1150 										 [(AIListContact *)inObject parentContact] :
  1151 										 inObject);
  1152 	NSString		*currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
  1153 	if (currentIconPath) {
  1154 		NSString	*objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1155 		if (objectsKnownIconPath &&
  1156 			[currentIconPath isEqualToString:objectsKnownIconPath]) {
  1157 			//We're the first one to get to this object!  We get to delete the old path and remove the reference to it
  1158 			[[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
  1159 			[iconSourceObject setValue:nil
  1160 									   forProperty:KEY_WEBKIT_USER_ICON
  1161 									   notify:NotifyNever];
  1162 		} else {
  1163 			/* Some other instance beat us to the punch. The object's KEY_WEBKIT_USER_ICON is right, since it doesn't match our
  1164 			 * internally tracked path.
  1165 			 */
  1166 		}
  1167 	}
  1168 	
  1169 	[self updateUserIconForObject:iconSourceObject];
  1170 }
  1171 
  1172 /*!
  1173  * @brief Remove all references to *this* chat using cached icons for an object
  1174  *
  1175  * If this is the last chat utilizing the cached icon, it will be deleted.
  1176  *
  1177  * @param inObject The object
  1178  */
  1179 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
  1180 {
  1181 	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1182 										 [(AIListContact *)inObject parentContact] :
  1183 										 inObject);
  1184 	NSString		*path;
  1185 	
  1186 	NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
  1187 	chatsUsingCachedIcon--;
  1188 	[iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
  1189 					   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
  1190 					   notify:NotifyNever];
  1191 	[objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
  1192 
  1193 	if ((chatsUsingCachedIcon <= 0) &&
  1194 		(path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
  1195 		[[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
  1196 		[iconSourceObject setValue:nil
  1197 								   forProperty:KEY_WEBKIT_USER_ICON
  1198 								   notify:NotifyNever];
  1199 	}
  1200 
  1201 	[objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
  1202 }
  1203 
  1204 /*!
  1205  * @brief Remove all references to *this* chat using cached icons for all involved objects
  1206  */
  1207 - (void)releaseAllCachedIcons
  1208 {
  1209 	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
  1210 		[self releaseCurrentWebKitUserIconForObject:listObject];
  1211 	}
  1212 }
  1213 
  1214 /*!
  1215  * @brief Generate an updated masked user icon for the passed list object
  1216  */
  1217 - (void)updateUserIconForObject:(AIListObject *)inObject
  1218 {
  1219 	AIListObject		*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
  1220 											 [(AIListContact *)inObject parentContact] :
  1221 											 inObject);
  1222 	NSImage				*userIcon;
  1223 	NSString			*oldWebKitUserIconPath = nil;
  1224 	NSString			*webKitUserIconPath = nil;
  1225 	NSImage				*webKitUserIcon;
  1226 	
  1227 	/*
  1228 	 * We probably already have a userIcon waiting for us, the active display icon; use that
  1229 	 * rather than loading one from disk.
  1230 	 */
  1231 	if (!(userIcon = [iconSourceObject userIcon])) {
  1232 		//If that's not the case, try using the UserIconPath
  1233 		NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
  1234 		if (userIconPath)
  1235 			userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
  1236 	}
  1237 
  1238 	if (userIcon) {
  1239 		if ([messageStyle userIconMask]) {
  1240 			//Apply the mask if the style has one
  1241 			//XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
  1242 			webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
  1243 			[webKitUserIcon lockFocus];
  1244 			[userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
  1245 						fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
  1246 					   operation:NSCompositeSourceIn
  1247 						fraction:1.0];
  1248 			[webKitUserIcon unlockFocus];
  1249 		} else {
  1250 			//Otherwise, just use the icon as-is
  1251 			webKitUserIcon = userIcon;
  1252 		}
  1253 
  1254 		oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];		
  1255 		webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
  1256 		
  1257 		if (!webKitUserIconPath) {
  1258 			/* If the image doesn't know a path to use, write it out and set it.
  1259 			 *
  1260 			 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
  1261 			 * any animation, which is good since animation in the message view is slow and annoying.
  1262 			 *
  1263 			 * Only write out the icon if the object doesn't already have one
  1264 			 */				
  1265 			webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
  1266 			if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
  1267 													 atomically:YES]) {
  1268 				[iconSourceObject setValue:webKitUserIconPath
  1269 										   forProperty:KEY_WEBKIT_USER_ICON
  1270 										   notify:NO];
  1271 			} else {
  1272 				AILogWithSignature(@"Warning: Could not write out icon to %@", webKitUserIconPath);
  1273 			}
  1274 		}
  1275 
  1276 		//Make sure it's known that this user has been handled
  1277 		if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
  1278 			[objectsWithUserIconsArray addObject:iconSourceObject];
  1279 
  1280 			//Keep track of this chat using the icon
  1281 			[iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
  1282 									   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
  1283 									   notify:NotifyNever];
  1284 		}
  1285 		
  1286 		if (!webKitUserIconPath) webKitUserIconPath = @"";
  1287 
  1288 		if ([webView mainFrameDocument]) {
  1289 			//Update existing images if the webView has loaded and has a main frame
  1290 			if (oldWebKitUserIconPath &&
  1291 				![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
  1292 				
  1293 				DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1294 				NSUInteger imagesCount = [images length];
  1295 
  1296 				webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
  1297 
  1298 				for (NSInteger i = 0; i < imagesCount; i++) {
  1299 					DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
  1300 					NSString *currentSrc = [img getAttribute:@"src"];
  1301 					if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
  1302 						[img setSrc:webKitUserIconPath];
  1303 					}
  1304 				}
  1305 			}
  1306 			
  1307 			[objectIconPathDict setObject:webKitUserIconPath
  1308 								   forKey:iconSourceObject.internalObjectID];
  1309 		} else {
  1310 			/* Otherwise, try to again in a moment. We've already done the heavy lifting
  1311 			 * such as writing out the icon, so it's cheap to recurse.
  1312 			 */			
  1313 			[self performSelector:@selector(updateUserIconForObject:)
  1314 					   withObject:inObject
  1315 					   afterDelay:1];
  1316 		}
  1317 	}
  1318 }
  1319 
  1320 - (void)updateServiceIcon
  1321 {
  1322 	DOMDocument *doc = [webView mainFrameDocument];
  1323 	//Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
  1324 	if(![doc respondsToSelector:@selector(getElementsByClassName:)])
  1325 		return; 
  1326 	DOMNodeList  *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
  1327 	NSUInteger imagesCount = [serviceIconImages length];
  1328 	
  1329 	NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID 
  1330 																type:AIServiceIconLarge];
  1331 	
  1332 	for (NSInteger i = 0; i < imagesCount; i++) {
  1333 		DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
  1334 		[img setSrc:serviceIconPath];
  1335 	}	
  1336 }
  1337 
  1338 - (void)customEmoticonUpdated:(NSNotification *)inNotification
  1339 {
  1340 	DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
  1341 	NSUInteger imagesCount = [images length];
  1342 
  1343 	if (imagesCount > 0) {
  1344 		AIEmoticon	*emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
  1345 		NSString	*textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
  1346 		NSString	*path = [emoticon path];
  1347 		NSSize		emoticonSize = [[emoticon image] size];
  1348 		BOOL		updatedImage = NO;
  1349 		path = [[NSURL fileURLWithPath:path] absoluteString];
  1350 		for (NSInteger i = 0; i < imagesCount; i++) {
  1351 			DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
  1352 			
  1353 			if ([[img className] isEqualToString:@"emoticon"] &&
  1354 				[[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
  1355 				[img setSrc:path];
  1356 				[img setWidth:emoticonSize.width];
  1357 				[img setHeight:emoticonSize.height];
  1358 				updatedImage = YES;
  1359 			}
  1360 		}
  1361 		NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
  1362 																	 withArguments:nil];
  1363 		if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
  1364 
  1365 		if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat" 
  1366 															  withArguments:[NSArray arrayWithObject:shouldScroll]];
  1367 	}
  1368 }
  1369 
  1370 /*!
  1371  * @brief Returns the path the background image given a unique ID
  1372  */
  1373 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
  1374 {
  1375 	NSString	*filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
  1376 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1377 }
  1378 
  1379 /*!
  1380  * @brief Returns the path to the list object's masked user icon
  1381  */
  1382 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
  1383 {
  1384 	NSString	*filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
  1385 	return [[adium cachesPath] stringByAppendingPathComponent:filename];
  1386 }
  1387 
  1388 #pragma mark File Transfer
  1389 
  1390 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
  1391 {
  1392 	ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
  1393 	ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
  1394 
  1395 	if (tc) {
  1396 		AIFileTransferAction a;
  1397 		if ([action isEqualToString:@"SaveAs"])
  1398 			a = AISaveFileAs;
  1399 		else if ([action isEqualToString:@"Cancel"]) 
  1400 			a = AICancel;
  1401 		else
  1402 			a = AISaveFile;
  1403 		
  1404 		[tc handleFileTransferAction:a];
  1405 	}
  1406 }
  1407 
  1408 #pragma mark Topic editing
  1409 - (void)editingDidComplete:(DOMRange *)range
  1410 {
  1411 	DOMNode *node = range.startContainer;
  1412 	DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
  1413 	
  1414 	NSString *topicChange = nil;
  1415 		
  1416 	if (node == topicEdit || node.parentNode == topicEdit) {
  1417 		topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];		
  1418 		
  1419 		NSTextView *textView = [self textView];
  1420 		if (textView) {
  1421 			[[webView window] makeFirstResponder:textView]; //Make it first responder
  1422 		}
  1423 		
  1424 		// Update the topic div in case the user doesn't have permission to change it.
  1425 		[self updateTopic];
  1426 		
  1427 		// Tell the chat to set the topic.
  1428 		[chat setTopic:topicChange];
  1429 	}
  1430 }
  1431 
  1432 #pragma mark Marked Scroller
  1433 - (JVMarkedScroller *)markedScroller
  1434 {
  1435 	WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
  1436 	NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
  1437 	
  1438 	JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
  1439 	return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
  1440 }
  1441 
  1442 - (void)setupMarkedScroller
  1443 {
  1444 	WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
  1445 	NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
  1446 	
  1447 	[scrollView setHasHorizontalScroller:NO];
  1448 
  1449 	JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
  1450 	if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
  1451 		NSRect scrollerFrame = [[scrollView verticalScroller] frame];
  1452 		NSScroller *oldScroller = scroller;
  1453 		scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
  1454 		[scroller setFloatValue:oldScroller.floatValue];
  1455 		[scroller setKnobProportion:oldScroller.knobProportion];
  1456 		[scrollView setVerticalScroller:scroller];
  1457 	}
  1458 }
  1459 
  1460 - (NSNumber *)currentOffsetHeight
  1461 {
  1462 	// We use the body's height to determine our mark location.
  1463 	return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
  1464 }
  1465 
  1466 - (void)markCurrentLocation
  1467 {
  1468 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
  1469 }
  1470 
  1471 #define PREF_KEY_FOCUS_LINE	@"Draw Focus Lines"
  1472 
  1473 - (void)markForFocusChange
  1474 {
  1475 	// We use the current Chat element's height to determine our mark location.
  1476 	[self.markedScroller removeMarkWithIdentifier:@"focus"];
  1477 	
  1478 	// The next message being inserted needs to add a mark.
  1479 	nextMessageFocus = YES;
  1480 	
  1481 	DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
  1482 	DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
  1483 	for (NSUInteger i = 0; i < nodeList.length; i++)
  1484 	{
  1485 		node = (DOMHTMLElement *)[nodeList item:i];
  1486 		classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
  1487 		
  1488 		[classes removeObject:@"focus"];
  1489 		[classes removeObject:@"firstFocus"];
  1490 		
  1491 		node.className = [classes componentsJoinedByString:@" "];
  1492 		
  1493 		[classes release];
  1494 	}
  1495 	
  1496 }
  1497 
  1498 - (void)addMark
  1499 {
  1500 	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
  1501 }
  1502 
  1503 - (void)jumpToPreviousMark
  1504 {
  1505 	[self.markedScroller jumpToPreviousMark:nil];
  1506 }
  1507 
  1508 - (BOOL)previousMarkExists
  1509 {
  1510 	return [self.markedScroller previousMarkExists];
  1511 }
  1512 
  1513 - (void)jumpToNextMark
  1514 {
  1515 	[self.markedScroller jumpToNextMark:nil];	
  1516 }
  1517 
  1518 - (BOOL)nextMarkExists
  1519 {
  1520 	return [self.markedScroller nextMarkExists];	
  1521 }
  1522 
  1523 - (void)jumpToFocusMark
  1524 {
  1525 	[self.markedScroller jumpToFocusMark:nil];
  1526 }
  1527 
  1528 - (BOOL)focusMarkExists
  1529 {
  1530 	return [self.markedScroller focusMarkExists];
  1531 }
  1532 
  1533 #pragma mark JS Bridging
  1534 /*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
  1535 */
  1536 
  1537 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
  1538 {
  1539 	if (
  1540 		sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
  1541 		sel_isEqual(aSelector, @selector(debugLog:)) ||
  1542 		sel_isEqual(aSelector, @selector(zoomImage:))
  1543 	)
  1544 		return NO;
  1545 	
  1546 	return YES;
  1547 }
  1548 
  1549 /*
  1550  * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
  1551  * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
  1552  * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
  1553  *
  1554  * Any colon (:)in the Objective-C selector is replaced by an underscore (_).
  1555  * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
  1556  * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
  1557  */
  1558 + (NSString *)webScriptNameForSelector:(SEL)aSelector
  1559 {
  1560 	if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
  1561 	if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
  1562 	if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
  1563 	return @"";
  1564 }
  1565 
  1566 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
  1567 {
  1568 	NSMutableString *className = [[[img className] mutableCopy] autorelease];
  1569 	if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
  1570 		[className replaceOccurrencesOfString:@"fullSizeImage"
  1571 								   withString:@"scaledToFitImage"
  1572 									  options:NSLiteralSearch
  1573 										range:NSMakeRange(0, [className length])];
  1574 	else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
  1575 		[className replaceOccurrencesOfString:@"scaledToFitImage"
  1576 								   withString:@"fullSizeImage"
  1577 									  options:NSLiteralSearch
  1578 										range:NSMakeRange(0, [className length])];
  1579 	else 
  1580 		return NO;
  1581 	
  1582 	[img setClassName:className];
  1583 	[[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
  1584 
  1585 	return YES;
  1586 }
  1587 
  1588 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
  1589 
  1590 //gets the source of the html page, for debugging
  1591 - (NSString *)webviewSource
  1592 {
  1593 	return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
  1594 }
  1595 
  1596 /*!
  1597  * @brief Set the HTML content for the "Chat" area.
  1598  */
  1599 - (void)setChatContentSource:(NSString *)source
  1600 {
  1601 	if (!webViewIsReady) {
  1602 		// If the webview isn't ready yet, wait a very short amount of time before trying again
  1603 		[self performSelector:@selector(setChatContentSource:)
  1604 				   withObject:source
  1605 				   afterDelay:0];
  1606 	} else {
  1607 		// Add the old "Chat" element to the window.
  1608 		[(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
  1609 
  1610 		NSString	*scrollToBottomScript;		
  1611 		if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
  1612 			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
  1613 		}
  1614 	}
  1615 }
  1616 
  1617 /*!
  1618  * @brief Get the HTML content for the "Chat" area.
  1619  */
  1620 - (NSString *)chatContentSource
  1621 {
  1622 	return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
  1623 }
  1624 
  1625 /*!
  1626  * @brief The unique name for this style of "content source"
  1627  */
  1628 - (NSString *)contentSourceName
  1629 {
  1630 	return [[[messageStyle bundle] bundlePath] lastPathComponent];
  1631 }
  1632 
  1633 @end