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