Plugins/WebKit Message View/AIWebKitMessageViewController.m
author Zachary West <zacw@adium.im>
Tue Nov 10 10:56:52 2009 -0500 (2009-11-10)
changeset 2766 67be3e959a78
parent 2764 4b03c62063a5
child 2790 6de98889c273
permissions -rw-r--r--
Don't base focus application on content tracking, since status messages aren't tracked. Base it on whether or not the content is post-processed (i.e., logged). Refs #13356.
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@2728
    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@2728
   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@2766
   679
		// Mark the current location (the start of this element) if it's a mention.
zacw@2766
   680
		if (content.trackContent && [content.displayClasses containsObject:@"mention"]) {
zacw@2766
   681
			[self markCurrentLocation];
zacw@2766
   682
		}
zacw@2766
   683
		
zacw@2766
   684
		if (content.postProcessContent && adium.interfaceController.activeChat != content.chat) {
zacw@2766
   685
			if (nextMessageFocus) {
zacw@2766
   686
				[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
zacw@2766
   687
				
zacw@2766
   688
				// Add a class for "first content to lose focus"
zacw@2766
   689
				[content addDisplayClass:@"firstFocus"];
zacw@2766
   690
				
zacw@2766
   691
				nextMessageFocus = NO;
zacw@2729
   692
			}
zacw@2754
   693
zacw@2766
   694
			// Add a class for "this content received while out of focus"
zacw@2766
   695
			[content addDisplayClass:@"focus"];
zacw@2728
   696
		}
zacw@2728
   697
		
zacw@1292
   698
		//Add the content object
zacw@1292
   699
		[self _appendContent:content 
zacw@1292
   700
					 similar:similar
zacw@1292
   701
   willAddMoreContentObjects:willAddMoreContentObjects
zacw@1292
   702
		  replaceLastContent:replaceLastContent];
zacw@1292
   703
	}
zacw@1536
   704
David@0
   705
	[previousContent release]; previousContent = [content retain];
David@0
   706
}
David@0
   707
David@0
   708
/*!
David@0
   709
 * @brief Append a content object
David@0
   710
 */
David@0
   711
- (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
David@0
   712
{
David@0
   713
	[webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
David@0
   714
																					similar:contentIsSimilar
David@0
   715
																  willAddMoreContentObjects:willAddMoreContentObjects
David@0
   716
																		 replaceLastContent:replaceLastContent]];
David@0
   717
David@0
   718
	NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
David@0
   719
}
David@0
   720
zacw@1487
   721
#pragma mark Topics
zacw@1487
   722
/*!
zacw@1487
   723
 * @brief Force a topic update.
zacw@1487
   724
 *
zacw@1487
   725
 * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
zacw@1487
   726
 */
zacw@1487
   727
- (void)updateTopic
zacw@1487
   728
{
zacw@1690
   729
	NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
zacw@1487
   730
	
zacw@1487
   731
	AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
zacw@1487
   732
													withSource:chat.topicSetter
zacw@1487
   733
												   destination:nil
zacw@1487
   734
														  date:[NSDate date]
zacw@1487
   735
													   message:topic];
zacw@1487
   736
	
zacw@1487
   737
	// In case this topic is blank, we have to filter this ourself; the content controller will drop it.
zacw@1487
   738
	contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
zacw@1487
   739
	
zacw@1487
   740
	[self enqueueContentObject:contentTopic];
zacw@1487
   741
}
David@0
   742
David@0
   743
//WebView Delegates ----------------------------------------------------------------------------------------------------
David@0
   744
#pragma mark Webview delegates
David@0
   745
David@0
   746
- (void)webViewIsReady{
David@0
   747
	webViewIsReady = YES;
zacw@1534
   748
	[self setupMarkedScroller];
David@428
   749
	[self setIsGroupChat:chat.isGroupChat];
David@0
   750
	[self processQueuedContent];
David@0
   751
}
David@0
   752
David@0
   753
- (void)openImage:(id)sender
David@0
   754
{
David@0
   755
	NSURL	*imageURL = [sender representedObject];
David@0
   756
	[[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
David@0
   757
}
David@0
   758
David@0
   759
- (void)saveImageAs:(id)sender
David@0
   760
{
David@0
   761
	NSURL		*imageURL = [sender representedObject];
David@0
   762
	NSString	*path = [imageURL path];
David@0
   763
	
David@0
   764
	NSSavePanel *savePanel = [NSSavePanel savePanel];
David@0
   765
	[savePanel beginSheetForDirectory:nil
David@0
   766
								 file:[path lastPathComponent]
David@0
   767
					   modalForWindow:[webView window]
David@0
   768
						modalDelegate:self
David@0
   769
					   didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
David@0
   770
						  contextInfo:[imageURL retain]];
David@0
   771
}
David@0
   772
David@0
   773
- (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode  contextInfo:(void  *)contextInfo
David@0
   774
{
David@0
   775
	NSURL	*imageURL = (NSURL *)contextInfo;
David@0
   776
David@0
   777
	if (returnCode ==  NSOKButton) {
David@493
   778
		[[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
David@493
   779
												toPath:[[sheet URL] absoluteString]
David@493
   780
												 error:NULL];
David@0
   781
	}
David@0
   782
	
David@0
   783
	[imageURL release];
David@0
   784
}
David@0
   785
David@0
   786
/*!
David@0
   787
 * @brief Append our own menu items to the webview's contextual menus
David@0
   788
 */
David@0
   789
- (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
David@0
   790
{
David@0
   791
	NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
zacw@2132
   792
	AIListContact	*chatListObject = chat.listObject.parentContact;
David@0
   793
	NSMenuItem		*menuItem;
David@0
   794
David@0
   795
	//Remove default items we don't want
David@0
   796
	if (webViewMenuItems) {
David@0
   797
David@75
   798
		for (menuItem in defaultMenuItems) {
David@0
   799
			NSInteger tag = [menuItem tag];
David@0
   800
			if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
David@0
   801
				(tag == WebMenuItemTagDownloadLinkToDisk) ||
David@0
   802
				(tag == WebMenuItemTagOpenImageInNewWindow) ||
David@0
   803
				(tag == WebMenuItemTagDownloadImageToDisk) ||
David@0
   804
				(tag == WebMenuItemTagOpenFrameInNewWindow) ||
David@0
   805
				(tag == WebMenuItemTagStop) ||
David@0
   806
				(tag == WebMenuItemTagReload)) {
David@0
   807
				[webViewMenuItems removeObjectIdenticalTo:menuItem];
David@0
   808
			} else {
David@0
   809
				//This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
zacw@2182
   810
				if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
David@0
   811
					[webViewMenuItems removeObjectIdenticalTo:menuItem];					
David@0
   812
			}
David@0
   813
		}
David@0
   814
	}
David@0
   815
	
David@0
   816
	NSURL	*imageURL;
David@0
   817
	if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
David@0
   818
		//This is an image		
David@0
   819
		if (!webViewMenuItems) {
David@0
   820
			webViewMenuItems = [NSMutableArray array];
David@0
   821
		}
David@0
   822
		
David@0
   823
		menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
David@0
   824
											  target:self
David@0
   825
											  action:@selector(openImage:)
David@0
   826
									   keyEquivalent:@""
David@0
   827
								   representedObject:imageURL];
David@0
   828
		[webViewMenuItems addObject:menuItem];
David@0
   829
		[menuItem release];
David@0
   830
		menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
David@0
   831
											  target:self
David@0
   832
											  action:@selector(saveImageAs:)
David@0
   833
									   keyEquivalent:@""
David@0
   834
								   representedObject:imageURL];
David@0
   835
		[webViewMenuItems addObject:menuItem];
David@0
   836
		[menuItem release];		
David@0
   837
		
David@0
   838
		/*
David@0
   839
		NSString *imgClass = [img className];
David@0
   840
		//being very careful to only get user icons... a better way would be to put a class "usericon" on the img, but I haven't worked out how to do that, so we test for the name of the person in the src, and that it's not an emoticon or direct connect image.
David@0
   841
		if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
David@0
   842
		   [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
David@0
   843
		   [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
David@0
   844
		   [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
David@0
   845
		 */
David@0
   846
			
David@0
   847
	}
David@0
   848
zacw@1711
   849
	if (webViewMenuItems) {		
David@0
   850
		//Add a separator item if items already exist in webViewMenuItems
David@0
   851
		if ([webViewMenuItems count]) {
zacw@1711
   852
			// If the first item is a separator item, remove it.
zacw@1711
   853
			if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
zacw@1711
   854
				[webViewMenuItems removeObjectAtIndex:0];
zacw@1711
   855
			}
zacw@1711
   856
			
David@0
   857
			[webViewMenuItems addObject:[NSMenuItem separatorItem]];
David@0
   858
		}
David@0
   859
	} else {
David@0
   860
		webViewMenuItems = [NSMutableArray array];
David@0
   861
	}
David@0
   862
zacw@806
   863
	NSMenu *originalMenu = nil;
zacw@806
   864
	
David@0
   865
	if (chatListObject) {
David@0
   866
		NSArray *locations;
David@0
   867
		if ([chatListObject isIntentionallyNotAStranger]) {
David@0
   868
			locations = [NSArray arrayWithObjects:
David@0
   869
				[NSNumber numberWithInteger:Context_Contact_Manage],
David@0
   870
				[NSNumber numberWithInteger:Context_Contact_Action],
David@0
   871
				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
David@0
   872
				[NSNumber numberWithInteger:Context_Contact_ChatAction],
David@0
   873
				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
David@0
   874
		} else {
David@0
   875
			locations = [NSArray arrayWithObjects:
David@0
   876
				[NSNumber numberWithInteger:Context_Contact_Manage],
David@0
   877
				[NSNumber numberWithInteger:Context_Contact_Action],
David@0
   878
				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
David@0
   879
				[NSNumber numberWithInteger:Context_Contact_ChatAction],
David@0
   880
				[NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
David@0
   881
				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
David@0
   882
		}
David@0
   883
		
zacw@806
   884
		originalMenu = [adium.menuController contextualMenuWithLocations:locations
zacw@2098
   885
														   forListObject:chatListObject
zacw@2098
   886
																  inChat:chat];
zacw@806
   887
	} else if(chat.isGroupChat) {
zacw@1232
   888
		originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
zacw@1240
   889
																		  [NSNumber numberWithInteger:Context_GroupChat_Manage],
zacw@1240
   890
																		  [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
zacw@911
   891
																 forChat:chat];
zacw@806
   892
	}
zacw@806
   893
	
zacw@2098
   894
	[webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
zacw@2098
   895
	[originalMenu removeAllItems];
David@0
   896
zacw@892
   897
	if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
David@0
   898
		[webViewMenuItems addObject:[NSMenuItem separatorItem]];
David@0
   899
David@0
   900
	//Present an option to clear the display
David@0
   901
	menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
David@0
   902
										  target:self
David@0
   903
										  action:@selector(clearView)
David@0
   904
								   keyEquivalent:@""];
David@0
   905
	[webViewMenuItems addObject:menuItem];
David@0
   906
	[menuItem release];
David@0
   907
	
David@0
   908
	return webViewMenuItems;
David@0
   909
}
David@0
   910
David@0
   911
/*!
David@0
   912
 * @brief Add ourself to the window script object bridge when it's safe to do so
David@0
   913
 */
David@471
   914
- (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
David@0
   915
{
David@0
   916
    [[webView windowScriptObject] setValue:self forKey:@"client"];
David@0
   917
}
David@0
   918
David@0
   919
//Dragging delegate ----------------------------------------------------------------------------------------------------
David@0
   920
#pragma mark Dragging delegate
David@0
   921
/*!
David@0
   922
 * @brief If possible, return the first NSTextView in the message view's responder chain
David@0
   923
 *
David@0
   924
 * This is used for drag and drop behavior.
David@0
   925
 */
David@0
   926
- (NSTextView *)textView
David@0
   927
{
David@0
   928
	id	responder = [webView nextResponder];
David@0
   929
	
David@0
   930
	//Walkin the responder chain looking for an NSTextView
David@0
   931
	while (responder &&
David@0
   932
		  ![responder isKindOfClass:[NSTextView class]]) {
David@0
   933
		responder = [responder nextResponder];
David@0
   934
	}
David@0
   935
	
David@0
   936
	return responder;
David@0
   937
}
David@0
   938
David@0
   939
/*!
David@0
   940
 * @brief Dragging entered
David@0
   941
 */
David@0
   942
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
David@0
   943
{
David@0
   944
	NSPasteboard	*pasteboard = [sender draggingPasteboard];
David@0
   945
David@0
   946
	return ([pasteboard availableTypeFromArray:draggedTypes] ?
David@0
   947
		   NSDragOperationCopy :
David@0
   948
		   NSDragOperationNone);
David@0
   949
}
David@0
   950
David@0
   951
/*!
David@0
   952
* @brief Dragging updated
David@0
   953
 */
David@0
   954
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
David@0
   955
{
David@0
   956
	return [self draggingEntered:sender];
David@0
   957
}
David@0
   958
David@0
   959
/*!
David@0
   960
 * @brief Handle a drag onto the webview
David@0
   961
 * 
David@0
   962
 * If we're getting a non-image file, we can handle it immediately.  Otherwise, the drag is the textView's problem.
David@0
   963
 */
David@0
   964
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
David@0
   965
{
David@0
   966
	NSPasteboard	*pasteboard = [sender draggingPasteboard];
David@0
   967
	BOOL			success = NO;
David@0
   968
	
David@0
   969
	if ([self shouldHandleDragWithPasteboard:pasteboard]) {
David@0
   970
		
David@0
   971
		//Not an image but it is a file - send it immediately as a file transfer
David@0
   972
		NSArray			*files = [pasteboard propertyListForType:NSFilenamesPboardType];
David@0
   973
		NSString		*path;
David@75
   974
		for (path in files) {
David@426
   975
			AIListObject *listObject = chat.listObject;
David@0
   976
			if (listObject) {
David@100
   977
				[adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
David@0
   978
			}
David@0
   979
		}
David@0
   980
		success = YES;
David@0
   981
		
David@0
   982
	} else {
David@0
   983
		NSTextView *textView = [self textView];
David@0
   984
		if (textView) {
David@0
   985
			[[webView window] makeFirstResponder:textView]; //Make it first responder
David@0
   986
			success = [textView performDragOperation:sender];
David@0
   987
		}
David@0
   988
	}
David@0
   989
	
David@0
   990
	return success;
David@0
   991
}
David@0
   992
David@0
   993
/*!
David@0
   994
 * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
David@0
   995
 */
David@0
   996
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
David@0
   997
{
David@0
   998
	NSPasteboard	*pasteboard = [sender draggingPasteboard];
David@0
   999
	BOOL	success = YES;
David@0
  1000
	
David@0
  1001
	if (![self shouldHandleDragWithPasteboard:pasteboard]) {	
David@0
  1002
		NSTextView *textView = [self textView];
David@0
  1003
		if (textView) {
David@0
  1004
			success = [textView prepareForDragOperation:sender];
David@0
  1005
		}
David@0
  1006
	}
David@0
  1007
	
David@0
  1008
	return success;
David@0
  1009
}
David@0
  1010
	
David@0
  1011
/*!
David@0
  1012
 * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
David@0
  1013
 */
David@0
  1014
- (void)concludeDragOperation:(id <NSDraggingInfo>)sender
David@0
  1015
{
David@0
  1016
	NSPasteboard	*pasteboard = [sender draggingPasteboard];
David@0
  1017
	
David@0
  1018
	if (![self shouldHandleDragWithPasteboard:pasteboard]) {
David@0
  1019
		NSTextView *textView = [self textView];
David@0
  1020
		if (textView) {
David@0
  1021
			[textView concludeDragOperation:sender];
David@0
  1022
		}
David@0
  1023
	}
David@0
  1024
}
David@0
  1025
David@0
  1026
/*!
David@0
  1027
 * @brief Handle drags of content we recognize
David@0
  1028
 */
David@0
  1029
- (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
David@0
  1030
{
David@0
  1031
	/*
David@0
  1032
	return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
David@0
  1033
			[pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
David@0
  1034
	 */
David@0
  1035
	return NO;
David@0
  1036
}
David@0
  1037
David@0
  1038
David@0
  1039
//User Icon masking --------------------------------------------------------------------------------------------------
David@0
  1040
//We allow messaage styles to specify masks for user icons.  This could be user to round the corners of user icons 
David@0
  1041
//or other related effects.
David@0
  1042
#pragma mark User icon masking
David@0
  1043
/*!
David@0
  1044
 * @brief Update icon masks when participating list objects change
David@0
  1045
 *
David@0
  1046
 * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
David@0
  1047
 * When the list changes, remove the observers we had in place before and add observers for each object in the list
David@0
  1048
 * so we never observe for contacts not in the chat.
David@0
  1049
 */
David@0
  1050
- (void)participatingListObjectsChanged:(NSNotification *)notification
David@0
  1051
{
David@0
  1052
	NSArray			*participatingListObjects = [chat containedObjects];
David@0
  1053
	
David@1109
  1054
	[[NSNotificationCenter defaultCenter] removeObserver:self
David@0
  1055
										  name:ListObject_AttributesChanged
David@0
  1056
										object:nil];
David@0
  1057
	
David@75
  1058
	for (AIListObject *listObject in participatingListObjects) {
David@0
  1059
		//Update the mask for any user which just entered the chat
David@0
  1060
		if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
David@0
  1061
			[self updateUserIconForObject:listObject];
David@0
  1062
		}
David@0
  1063
		
David@0
  1064
		//In the future, watch for changes on the parent object, since that's the icon we display
David@0
  1065
		if ([listObject isKindOfClass:[AIListContact class]]) {
David@1109
  1066
			[[NSNotificationCenter defaultCenter] addObserver:self
David@0
  1067
										   selector:@selector(listObjectAttributesChanged:) 
David@0
  1068
											   name:ListObject_AttributesChanged
David@0
  1069
											 object:[(AIListContact *)listObject parentContact]];
David@0
  1070
		}
David@0
  1071
	}
David@0
  1072
	
David@0
  1073
	//Also observe our account
David@426
  1074
	if (chat.account) {
David@1109
  1075
		[[NSNotificationCenter defaultCenter] addObserver:self
David@0
  1076
									   selector:@selector(listObjectAttributesChanged:) 
David@0
  1077
										   name:ListObject_AttributesChanged
David@426
  1078
										 object:chat.account];
David@0
  1079
	}
David@0
  1080
David@0
  1081
	//Remove the cache for any object no longer in the chat
zacw@966
  1082
	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
David@0
  1083
		if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
David@0
  1084
			(![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
David@426
  1085
			!(listObject == chat.account)) {
David@0
  1086
			[self releaseCurrentWebKitUserIconForObject:listObject];
David@0
  1087
		}
David@0
  1088
	}
David@0
  1089
}
David@0
  1090
David@0
  1091
/*!
David@0
  1092
 * @brief Update icon masks when source or destination changes
David@0
  1093
 */
David@0
  1094
- (void)sourceOrDestinationChanged:(NSNotification *)notification
David@0
  1095
{
David@0
  1096
	//Update the participating contacts
David@0
  1097
	[self participatingListObjectsChanged:nil];
David@0
  1098
	
David@0
  1099
	//And update the source account
David@426
  1100
	[self updateUserIconForObject:chat.account];
David@0
  1101
	
David@0
  1102
	[self updateServiceIcon];
David@0
  1103
}
David@0
  1104
David@0
  1105
/*!
David@0
  1106
 * @brief Update the icon when a list object's icon attributes change
David@0
  1107
 */
David@0
  1108
- (void)listObjectAttributesChanged:(NSNotification *)notification
David@0
  1109
{
David@0
  1110
    AIListObject	*inObject = [notification object];
David@0
  1111
    NSSet			*keys = [[notification userInfo] objectForKey:@"Keys"];
David@0
  1112
	
David@0
  1113
	if ([keys containsObject:KEY_USER_ICON]) {
David@0
  1114
		if (inObject) {
David@0
  1115
			AIListObject	*actualObject = nil;
Evan@156
  1116
			AILogWithSignature(@"%@'s icon changed", inObject);
David@426
  1117
			if (chat.account == inObject) {
David@0
  1118
				//The account is the object actually in the chat
David@0
  1119
				actualObject = inObject;
David@0
  1120
			} else {
David@0
  1121
				/*
David@0
  1122
				 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
David@0
  1123
				 * be displaying as changed.
David@0
  1124
				 */
David@0
  1125
				
David@79
  1126
				for (AIListContact *participatingListObject in chat) {
David@0
  1127
					if ([participatingListObject parentContact] == inObject) {
David@0
  1128
						actualObject = participatingListObject;
David@0
  1129
						break;
David@0
  1130
					}
David@0
  1131
				}
David@0
  1132
			}
David@0
  1133
David@0
  1134
			if (actualObject) {
David@0
  1135
				[self userIconForObjectDidChange:actualObject];
David@0
  1136
			}
David@0
  1137
David@0
  1138
		} else {
Evan@156
  1139
			AILogWithSignature(@"nil object's icon changed");
David@0
  1140
			//We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
David@0
  1141
			[self sourceOrDestinationChanged:nil];
David@0
  1142
		}
David@0
  1143
	}
David@0
  1144
}
David@0
  1145
David@0
  1146
- (void)userIconForObjectDidChange:(AIListObject *)inObject
David@0
  1147
{
David@0
  1148
	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
David@0
  1149
										 [(AIListContact *)inObject parentContact] :
David@0
  1150
										 inObject);
David@838
  1151
	NSString		*currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
David@0
  1152
	if (currentIconPath) {
David@0
  1153
		NSString	*objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
David@0
  1154
		if (objectsKnownIconPath &&
David@0
  1155
			[currentIconPath isEqualToString:objectsKnownIconPath]) {
David@0
  1156
			//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
  1157
			[[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
David@0
  1158
			[iconSourceObject setValue:nil
David@0
  1159
									   forProperty:KEY_WEBKIT_USER_ICON
David@0
  1160
									   notify:NotifyNever];
David@0
  1161
		} else {
David@0
  1162
			/* 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
  1163
			 * internally tracked path.
David@0
  1164
			 */
David@0
  1165
		}
David@0
  1166
	}
David@0
  1167
	
David@0
  1168
	[self updateUserIconForObject:iconSourceObject];
David@0
  1169
}
David@0
  1170
David@0
  1171
/*!
David@0
  1172
 * @brief Remove all references to *this* chat using cached icons for an object
David@0
  1173
 *
David@0
  1174
 * If this is the last chat utilizing the cached icon, it will be deleted.
David@0
  1175
 *
David@0
  1176
 * @param inObject The object
David@0
  1177
 */
David@0
  1178
- (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
David@0
  1179
{
David@0
  1180
	AIListObject	*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
David@0
  1181
										 [(AIListContact *)inObject parentContact] :
David@0
  1182
										 inObject);
David@0
  1183
	NSString		*path;
David@0
  1184
	
David@393
  1185
	NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
David@0
  1186
	chatsUsingCachedIcon--;
David@0
  1187
	[iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
David@0
  1188
					   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
David@0
  1189
					   notify:NotifyNever];
David@0
  1190
	[objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
David@0
  1191
David@0
  1192
	if ((chatsUsingCachedIcon <= 0) &&
David@0
  1193
		(path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
David@471
  1194
		[[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
David@0
  1195
		[iconSourceObject setValue:nil
David@0
  1196
								   forProperty:KEY_WEBKIT_USER_ICON
David@0
  1197
								   notify:NotifyNever];
David@0
  1198
	}
David@0
  1199
David@838
  1200
	[objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
David@0
  1201
}
David@0
  1202
David@0
  1203
/*!
David@0
  1204
 * @brief Remove all references to *this* chat using cached icons for all involved objects
David@0
  1205
 */
David@0
  1206
- (void)releaseAllCachedIcons
David@0
  1207
{
catfish@1820
  1208
	for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
David@0
  1209
		[self releaseCurrentWebKitUserIconForObject:listObject];
David@0
  1210
	}
David@0
  1211
}
David@0
  1212
David@0
  1213
/*!
David@0
  1214
 * @brief Generate an updated masked user icon for the passed list object
David@0
  1215
 */
David@0
  1216
- (void)updateUserIconForObject:(AIListObject *)inObject
David@0
  1217
{
David@0
  1218
	AIListObject		*iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
David@0
  1219
											 [(AIListContact *)inObject parentContact] :
David@0
  1220
											 inObject);
David@0
  1221
	NSImage				*userIcon;
David@0
  1222
	NSString			*oldWebKitUserIconPath = nil;
David@0
  1223
	NSString			*webKitUserIconPath = nil;
David@0
  1224
	NSImage				*webKitUserIcon;
David@0
  1225
	
David@0
  1226
	/*
David@0
  1227
	 * We probably already have a userIcon waiting for us, the active display icon; use that
David@0
  1228
	 * rather than loading one from disk.
David@0
  1229
	 */
David@0
  1230
	if (!(userIcon = [iconSourceObject userIcon])) {
David@0
  1231
		//If that's not the case, try using the UserIconPath
David@478
  1232
		NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
David@478
  1233
		if (userIconPath)
David@478
  1234
			userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
David@0
  1235
	}
David@0
  1236
David@0
  1237
	if (userIcon) {
David@0
  1238
		if ([messageStyle userIconMask]) {
David@0
  1239
			//Apply the mask if the style has one
David@0
  1240
			//XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
David@0
  1241
			webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
David@0
  1242
			[webKitUserIcon lockFocus];
David@0
  1243
			[userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
David@0
  1244
						fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
David@0
  1245
					   operation:NSCompositeSourceIn
David@0
  1246
						fraction:1.0];
David@0
  1247
			[webKitUserIcon unlockFocus];
David@0
  1248
		} else {
David@0
  1249
			//Otherwise, just use the icon as-is
David@0
  1250
			webKitUserIcon = userIcon;
David@0
  1251
		}
David@0
  1252
David@838
  1253
		oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
David@0
  1254
		webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
David@0
  1255
David@0
  1256
		if (!webKitUserIconPath) {
David@0
  1257
			/* If the image doesn't know a path to use, write it out and set it.
David@0
  1258
			 *
David@0
  1259
			 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
David@0
  1260
			 * any animation, which is good since animation in the message view is slow and annoying.
David@0
  1261
			 *
David@0
  1262
			 * Only write out the icon if the object doesn't already have one
David@0
  1263
			 */				
David@0
  1264
			webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
David@0
  1265
			if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
David@0
  1266
													 atomically:YES]) {
David@0
  1267
				[iconSourceObject setValue:webKitUserIconPath
David@0
  1268
										   forProperty:KEY_WEBKIT_USER_ICON
David@0
  1269
										   notify:NO];				
David@0
  1270
			}			
David@0
  1271
		}
David@0
  1272
David@0
  1273
		//Make sure it's known that this user has been handled
David@0
  1274
		if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
David@0
  1275
			[objectsWithUserIconsArray addObject:iconSourceObject];
David@0
  1276
David@0
  1277
			//Keep track of this chat using the icon
David@393
  1278
			[iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
David@0
  1279
									   forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
David@0
  1280
									   notify:NotifyNever];
David@0
  1281
		}
David@0
  1282
		
David@0
  1283
		if (!webKitUserIconPath) webKitUserIconPath = @"";
David@0
  1284
David@0
  1285
		//Update existing images
Evan@156
  1286
		AILogWithSignature(@"Updating %@ to %@", oldWebKitUserIconPath, webKitUserIconPath);
Evan@156
  1287
David@0
  1288
		if (oldWebKitUserIconPath &&
David@0
  1289
			![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
David@370
  1290
			DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
David@0
  1291
			NSUInteger imagesCount = [images length];
David@0
  1292
David@0
  1293
			webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
David@0
  1294
David@0
  1295
			for (NSInteger i = 0; i < imagesCount; i++) {
David@0
  1296
				DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
David@0
  1297
				NSString *currentSrc = [img getAttribute:@"src"];
David@0
  1298
				if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
David@0
  1299
					[img setSrc:webKitUserIconPath];
David@0
  1300
				}
David@0
  1301
			}
David@0
  1302
		}
David@0
  1303
David@0
  1304
		[objectIconPathDict setObject:webKitUserIconPath
David@838
  1305
							   forKey:iconSourceObject.internalObjectID];
David@0
  1306
	}
David@0
  1307
}
David@0
  1308
David@0
  1309
- (void)updateServiceIcon
David@0
  1310
{
David@370
  1311
	DOMDocument *doc = [webView mainFrameDocument];
David@0
  1312
	//Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
David@0
  1313
	if(![doc respondsToSelector:@selector(getElementsByClassName:)])
David@0
  1314
		return; 
David@0
  1315
	DOMNodeList  *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
David@0
  1316
	NSUInteger imagesCount = [serviceIconImages length];
David@0
  1317
	
David@715
  1318
	NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID 
David@0
  1319
																type:AIServiceIconLarge];
David@0
  1320
	
David@0
  1321
	for (NSInteger i = 0; i < imagesCount; i++) {
David@0
  1322
		DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
David@0
  1323
		[img setSrc:serviceIconPath];
David@0
  1324
	}	
David@0
  1325
}
David@0
  1326
David@0
  1327
- (void)customEmoticonUpdated:(NSNotification *)inNotification
David@0
  1328
{
David@370
  1329
	DOMNodeList  *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
David@0
  1330
	NSUInteger imagesCount = [images length];
David@0
  1331
David@0
  1332
	if (imagesCount > 0) {
David@0
  1333
		AIEmoticon	*emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
David@0
  1334
		NSString	*textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
David@0
  1335
		NSString	*path = [emoticon path];
David@0
  1336
		NSSize		emoticonSize = [[emoticon image] size];
David@0
  1337
		BOOL		updatedImage = NO;
David@0
  1338
		path = [[NSURL fileURLWithPath:path] absoluteString];
David@0
  1339
		for (NSInteger i = 0; i < imagesCount; i++) {
David@0
  1340
			DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
David@0
  1341
			
David@0
  1342
			if ([[img className] isEqualToString:@"emoticon"] &&
David@0
  1343
				[[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
David@0
  1344
				[img setSrc:path];
David@0
  1345
				[img setWidth:emoticonSize.width];
David@0
  1346
				[img setHeight:emoticonSize.height];
David@0
  1347
				updatedImage = YES;
David@0
  1348
			}
David@0
  1349
		}
David@0
  1350
		NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
David@0
  1351
																	 withArguments:nil];
David@0
  1352
		if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
David@0
  1353
David@0
  1354
		if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat" 
David@0
  1355
															  withArguments:[NSArray arrayWithObject:shouldScroll]];
David@0
  1356
	}
David@0
  1357
}
David@0
  1358
David@0
  1359
/*!
David@0
  1360
 * @brief Returns the path the background image given a unique ID
David@0
  1361
 */
David@0
  1362
- (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
David@0
  1363
{
David@0
  1364
	NSString	*filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
David@0
  1365
	return [[adium cachesPath] stringByAppendingPathComponent:filename];
David@0
  1366
}
David@0
  1367
David@0
  1368
/*!
David@0
  1369
 * @brief Returns the path to the list object's masked user icon
David@0
  1370
 */
David@0
  1371
- (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
David@0
  1372
{
David@838
  1373
	NSString	*filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
David@0
  1374
	return [[adium cachesPath] stringByAppendingPathComponent:filename];
David@0
  1375
}
David@0
  1376
David@0
  1377
#pragma mark File Transfer
David@0
  1378
David@0
  1379
- (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
David@0
  1380
{
David@0
  1381
	ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
David@0
  1382
	ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
David@0
  1383
David@0
  1384
	if (tc) {
David@0
  1385
		AIFileTransferAction a;
David@0
  1386
		if ([action isEqualToString:@"SaveAs"])
David@0
  1387
			a = AISaveFileAs;
David@0
  1388
		else if ([action isEqualToString:@"Cancel"]) 
David@0
  1389
			a = AICancel;
David@0
  1390
		else
David@0
  1391
			a = AISaveFile;
David@0
  1392
		
David@0
  1393
		[tc handleFileTransferAction:a];
David@0
  1394
	}
David@0
  1395
}
David@0
  1396
zacw@1297
  1397
#pragma mark Topic editing
zacw@1297
  1398
- (void)editingDidComplete:(DOMRange *)range
zacw@1297
  1399
{
zacw@1297
  1400
	DOMNode *node = range.startContainer;
zacw@1298
  1401
	DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
zacw@1297
  1402
	
zacw@1297
  1403
	NSString *topicChange = nil;
zacw@1297
  1404
		
zacw@1298
  1405
	if (node == topicEdit || node.parentNode == topicEdit) {
zacw@1301
  1406
		topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];		
zacw@1301
  1407
		
zacw@1475
  1408
		NSTextView *textView = [self textView];
zacw@1475
  1409
		if (textView) {
zacw@1475
  1410
			[[webView window] makeFirstResponder:textView]; //Make it first responder
zacw@1475
  1411
		}
zacw@1475
  1412
		
zacw@1475
  1413
		// Update the topic div in case the user doesn't have permission to change it.
zacw@1487
  1414
		[self updateTopic];
zacw@1475
  1415
		
zacw@1301
  1416
		// Tell the chat to set the topic.
zacw@1301
  1417
		[chat setTopic:topicChange];
zacw@1297
  1418
	}
zacw@1297
  1419
}
zacw@1297
  1420
zacw@1534
  1421
#pragma mark Marked Scroller
zacw@1534
  1422
- (JVMarkedScroller *)markedScroller
zacw@1534
  1423
{
zacw@1534
  1424
	WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
zacw@1534
  1425
	NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
zacw@1534
  1426
	
zacw@1534
  1427
	JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
zacw@1534
  1428
	return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
zacw@1534
  1429
}
zacw@1534
  1430
zacw@1534
  1431
- (void)setupMarkedScroller
zacw@1534
  1432
{
zacw@1534
  1433
	WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
zacw@1534
  1434
	NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
zacw@1534
  1435
	
zacw@1534
  1436
	[scrollView setHasHorizontalScroller:NO];
zacw@1534
  1437
zacw@1534
  1438
	JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
zacw@1534
  1439
	if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
zacw@1534
  1440
		NSRect scrollerFrame = [[scrollView verticalScroller] frame];
zacw@1534
  1441
		NSScroller *oldScroller = scroller;
zacw@1534
  1442
		scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
David@1678
  1443
		[scroller setFloatValue:oldScroller.floatValue];
David@1678
  1444
		[scroller setKnobProportion:oldScroller.knobProportion];
zacw@1534
  1445
		[scrollView setVerticalScroller:scroller];
zacw@1534
  1446
	}
zacw@1534
  1447
}
zacw@1534
  1448
zacw@1543
  1449
- (NSNumber *)currentOffsetHeight
zacw@1543
  1450
{
zacw@1714
  1451
	// We use the body's height to determine our mark location.
catfish@2443
  1452
	return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
zacw@1543
  1453
}
zacw@1543
  1454
zacw@1534
  1455
- (void)markCurrentLocation
zacw@1534
  1456
{
catfish@2443
  1457
	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
zacw@1534
  1458
}
zacw@1534
  1459
zacw@1715
  1460
#define PREF_KEY_FOCUS_LINE	@"Draw Focus Lines"
zacw@1715
  1461
zacw@1537
  1462
- (void)markForFocusChange
zacw@1537
  1463
{
zacw@2754
  1464
	// We use the current Chat element's height to determine our mark location.
zacw@2754
  1465
	[self.markedScroller removeMarkWithIdentifier:@"focus"];
zacw@1714
  1466
	
zacw@2754
  1467
	// The next message being inserted needs to add a mark.
zacw@2728
  1468
	nextMessageFocus = YES;
zacw@2728
  1469
	
zacw@2728
  1470
	DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
zacw@2728
  1471
	DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
zacw@2728
  1472
	for (NSUInteger i = 0; i < nodeList.length; i++)
zacw@2728
  1473
	{
zacw@2728
  1474
		node = (DOMHTMLElement *)[nodeList item:i];
zacw@2728
  1475
		classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
zacw@2728
  1476
		
zacw@2728
  1477
		[classes removeObject:@"focus"];
zacw@2754
  1478
		[classes removeObject:@"firstFocus"];
zacw@2728
  1479
		
zacw@2728
  1480
		node.className = [classes componentsJoinedByString:@" "];
zacw@2728
  1481
		
zacw@2728
  1482
		[classes release];
zacw@1714
  1483
	}
zacw@1714
  1484
	
zacw@1543
  1485
}
zacw@1543
  1486
zacw@1543
  1487
- (void)addMark
zacw@1543
  1488
{
catfish@2443
  1489
	[self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
zacw@1543
  1490
}
zacw@1543
  1491
zacw@1543
  1492
- (void)jumpToPreviousMark
zacw@1543
  1493
{
catfish@2443
  1494
	[self.markedScroller jumpToPreviousMark:nil];
zacw@1543
  1495
}
zacw@1543
  1496
zacw@1581
  1497
- (BOOL)previousMarkExists
zacw@1581
  1498
{
catfish@2443
  1499
	return [self.markedScroller previousMarkExists];
zacw@1581
  1500
}
zacw@1581
  1501
zacw@1543
  1502
- (void)jumpToNextMark
zacw@1543
  1503
{
catfish@2443
  1504
	[self.markedScroller jumpToNextMark:nil];	
zacw@1543
  1505
}
zacw@1543
  1506
zacw@1581
  1507
- (BOOL)nextMarkExists
zacw@1581
  1508
{
catfish@2443
  1509
	return [self.markedScroller nextMarkExists];	
zacw@1581
  1510
}
zacw@1581
  1511
zacw@1543
  1512
- (void)jumpToFocusMark
zacw@1543
  1513
{
catfish@2443
  1514
	[self.markedScroller jumpToFocusMark:nil];
zacw@1581
  1515
}
zacw@1581
  1516
zacw@1581
  1517
- (BOOL)focusMarkExists
zacw@1581
  1518
{
catfish@2443
  1519
	return [self.markedScroller focusMarkExists];
zacw@1537
  1520
}
zacw@1537
  1521
David@0
  1522
#pragma mark JS Bridging
David@0
  1523
/*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
David@0
  1524
*/
David@0
  1525
David@0
  1526
+ (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
David@0
  1527
{
catfish@2305
  1528
	if (
catfish@2305
  1529
		sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
catfish@2305
  1530
		sel_isEqual(aSelector, @selector(debugLog:)) ||
catfish@2305
  1531
		sel_isEqual(aSelector, @selector(zoomImage:))
catfish@2305
  1532
	)
catfish@2305
  1533
		return NO;
catfish@2305
  1534
	
David@0
  1535
	return YES;
David@0
  1536
}
David@0
  1537
David@0
  1538
/*
David@0
  1539
 * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
David@0
  1540
 * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
David@0
  1541
 * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
David@0
  1542
 *
David@0
  1543
 * Any colon (:)in the Objective-C selector is replaced by an underscore (_).
David@0
  1544
 * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
David@0
  1545
 * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
David@0
  1546
 */
David@0
  1547
+ (NSString *)webScriptNameForSelector:(SEL)aSelector
David@0
  1548
{
catfish@2305
  1549
	if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
catfish@2305
  1550
	if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
catfish@2305
  1551
	if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
David@0
  1552
	return @"";
David@0
  1553
}
David@0
  1554
David@0
  1555
- (BOOL)zoomImage:(DOMHTMLImageElement *)img
David@0
  1556
{
David@0
  1557
	NSMutableString *className = [[[img className] mutableCopy] autorelease];
David@0
  1558
	if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
David@0
  1559
		[className replaceOccurrencesOfString:@"fullSizeImage"
David@0
  1560
								   withString:@"scaledToFitImage"
David@0
  1561
									  options:NSLiteralSearch
David@0
  1562
										range:NSMakeRange(0, [className length])];
David@0
  1563
	else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
David@0
  1564
		[className replaceOccurrencesOfString:@"scaledToFitImage"
David@0
  1565
								   withString:@"fullSizeImage"
David@0
  1566
									  options:NSLiteralSearch
David@0
  1567
										range:NSMakeRange(0, [className length])];
David@0
  1568
	else 
David@0
  1569
		return NO;
David@0
  1570
	
David@0
  1571
	[img setClassName:className];
David@0
  1572
	[[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
David@0
  1573
David@0
  1574
	return YES;
David@0
  1575
}
David@0
  1576
David@471
  1577
- (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
David@0
  1578
David@0
  1579
//gets the source of the html page, for debugging
David@0
  1580
- (NSString *)webviewSource
David@0
  1581
{
David@370
  1582
	return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
David@0
  1583
}
David@0
  1584
David@0
  1585
/*!
David@0
  1586
 * @brief Set the HTML content for the "Chat" area.
David@0
  1587
 */
David@0
  1588
- (void)setChatContentSource:(NSString *)source
David@0
  1589
{
David@0
  1590
	if (!webViewIsReady) {
David@0
  1591
		// If the webview isn't ready yet, wait a very short amount of time before trying again
David@0
  1592
		[self performSelector:@selector(setChatContentSource:)
David@0
  1593
				   withObject:source
David@599
  1594
				   afterDelay:0];
David@0
  1595
	} else {
David@0
  1596
		// Add the old "Chat" element to the window.
David@370
  1597
		[(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
David@0
  1598
David@0
  1599
		NSString	*scrollToBottomScript;		
David@0
  1600
		if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
David@0
  1601
			[webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
David@0
  1602
		}
David@0
  1603
	}
David@0
  1604
}
David@0
  1605
David@0
  1606
/*!
David@0
  1607
 * @brief Get the HTML content for the "Chat" area.
David@0
  1608
 */
David@0
  1609
- (NSString *)chatContentSource
David@0
  1610
{
David@370
  1611
	return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
David@0
  1612
}
David@0
  1613
David@0
  1614
/*!
David@0
  1615
 * @brief The unique name for this style of "content source"
David@0
  1616
 */
David@0
  1617
- (NSString *)contentSourceName
David@0
  1618
{
David@0
  1619
	return [[[messageStyle bundle] bundlePath] lastPathComponent];
David@0
  1620
}
David@0
  1621
David@0
  1622
@end