Plugins/WebKit Message View/AIWebKitMessageViewController.m
author Zachary West <zacw@adium.im>
Mon Nov 02 18:30:22 2009 -0500 (2009-11-02)
changeset 2852 7ff6b3f336d6
parent 2532 700469827eee
child 2853 a2f78c3401b9
permissions -rw-r--r--
Instead of inserting a <hr/> when we lose focus, which ends up breaking more than you'd expect, add a message class for the next message. Fixes #13300.

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