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