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.
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIWebKitMessageViewController.h"
18 #import "AIWebKitMessageViewStyle.h"
19 #import "AIWebKitMessageViewPlugin.h"
20 #import "ESWebKitMessageViewPreferences.h"
21 #import "AIWebKitDelegate.h"
22 #import "ESFileTransferRequestPromptController.h"
24 #import <Adium/AIContactControllerProtocol.h>
25 #import <Adium/AIContentControllerProtocol.h>
26 #import <Adium/AIMenuControllerProtocol.h>
27 #import <Adium/AIFileTransferControllerProtocol.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIChat.h>
30 #import <Adium/AIContentTopic.h>
31 #import <Adium/AIContentContext.h>
32 #import <Adium/AIContentObject.h>
33 #import <Adium/AIContentEvent.h>
34 #import <Adium/AIEmoticon.h>
35 #import <Adium/AIListContact.h>
36 #import <Adium/AIMetaContact.h>
37 #import <Adium/AIListObject.h>
38 #import <Adium/AIService.h>
39 #import <Adium/ESFileTransfer.h>
40 #import <Adium/ESTextAndButtonsWindowController.h>
41 #import <Adium/AIHTMLDecoder.h>
42 #import <AIUtilities/AIArrayAdditions.h>
43 #import <AIUtilities/AIColorAdditions.h>
44 #import <AIUtilities/AIDateFormatterAdditions.h>
45 #import <AIUtilities/AIFileManagerAdditions.h>
46 #import <AIUtilities/AIImageAdditions.h>
47 #import <AIUtilities/AIMenuAdditions.h>
48 #import <AIUtilities/AIMutableStringAdditions.h>
49 #import <AIUtilities/AIPasteboardAdditions.h>
50 #import <AIUtilities/AIStringAdditions.h>
51 #import <AIUtilities/AIAttributedStringAdditions.h>
52 #import <AIUtilities/JVMarkedScroller.h>
53 #import <objc/objc-runtime.h>
55 #define KEY_WEBKIT_CHATS_USING_CACHED_ICON @"WebKit:Chats Using Cached Icon"
57 #define USE_FASTER_BUT_BUGGY_WEBKIT_PREFERENCE_CHANGE_HANDLING FALSE
59 #define TEMPORARY_FILE_PREFIX @"TEMP"
61 @interface AIWebKitMessageViewController ()
62 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin;
64 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent;
65 - (void)_updateWebViewForCurrentPreferences;
66 - (void)_updateVariantWithoutPrimingView;
67 - (void)processQueuedContent;
68 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects;
69 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent;
71 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID;
72 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject;
73 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject;
74 - (void)releaseAllCachedIcons;
75 - (void)updateUserIconForObject:(AIListObject *)inObject;
76 - (void)userIconForObjectDidChange:(AIListObject *)inObject;
77 - (void)updateServiceIcon;
80 - (void)participatingListObjectsChanged:(NSNotification *)notification;
81 - (void)sourceOrDestinationChanged:(NSNotification *)notification;
82 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard;
83 - (void)enqueueContentObject:(AIContentObject *)contentObject;
84 - (void)debugLog:(NSString *)message;
85 - (void)processQueuedContent;
86 - (NSString *)webviewSource;
87 - (void) setIsGroupChat:(BOOL) flag;
89 - (void)setupMarkedScroller;
90 - (JVMarkedScroller *)markedScroller;
91 - (NSNumber *)currentOffsetHeight;
92 - (void)markCurrentLocation;
95 @interface DOMDocument (FutureWebKitPublicMethodsIKnow)
96 - (DOMNodeList *)getElementsByClassName:(NSString *)className;
97 - (DOMNodeList *)querySelectorAll:(NSString *)selectors; // We require 10.5.8/Safari 4, all is well!
100 static NSArray *draggedTypes = nil;
102 @implementation AIWebKitMessageViewController
104 + (AIWebKitMessageViewController *)messageDisplayControllerForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
106 return [[[self alloc] initForChat:inChat withPlugin:inPlugin] autorelease];
109 - (id)initForChat:(AIChat *)inChat withPlugin:(AIWebKitMessageViewPlugin *)inPlugin
112 if ((self = [super init])) {
115 delegateProxy = [AIWebKitDelegate sharedWebKitDelegate];
117 chat = [inChat retain];
118 plugin = [inPlugin retain];
119 contentQueue = [[NSMutableArray alloc] init];
120 objectIconPathDict = [[NSMutableDictionary alloc] init];
121 objectsWithUserIconsArray = [[NSMutableArray alloc] init];
122 shouldReflectPreferenceChanges = NO;
123 storedContentObjects = nil;
125 //Observe preference changes.
126 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_REGULAR_MESSAGE_DISPLAY];
127 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_GROUP_MESSAGE_DISPLAY];
128 [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
130 //Set ourselves up initially.
131 [self _updateWebViewForCurrentPreferences];
133 //Observe participants list changes
134 [[NSNotificationCenter defaultCenter] addObserver:self
135 selector:@selector(participatingListObjectsChanged:)
136 name:Chat_ParticipatingListObjectsChanged
139 //Observe source/destination changes
140 [[NSNotificationCenter defaultCenter] addObserver:self
141 selector:@selector(sourceOrDestinationChanged:)
142 name:Chat_SourceChanged
144 [[NSNotificationCenter defaultCenter] addObserver:self
145 selector:@selector(sourceOrDestinationChanged:)
146 name:Chat_DestinationChanged
149 //Observe content additons
150 [[NSNotificationCenter defaultCenter] addObserver:self
151 selector:@selector(contentObjectAdded:)
152 name:Content_ContentObjectAdded
154 [[NSNotificationCenter defaultCenter] addObserver:self
155 selector:@selector(chatDidFinishAddingUntrackedContent:)
156 name:Content_ChatDidFinishAddingUntrackedContent
159 [[NSNotificationCenter defaultCenter] addObserver:self
160 selector:@selector(customEmoticonUpdated:)
161 name:@"AICustomEmoticonUpdated"
168 - (void)messageViewIsClosing
170 [webView stopLoading:nil];
172 //Stop observing the webview, since it may attempt callbacks shortly after we dealloc
173 [delegateProxy removeDelegate:self];
175 /* The windowScriptObject retained self when we set it as the client in -[AIWebKitMessageViewController _initWebView]...
176 * Unfortunately, (as of 10.4.9) it won't actually release self until the webView deallocates. We'll do removeWebScriptKey:
177 * now in case that works properly later, and do the release of webView here rather than in dealloc to work around the bug.
179 [[webView windowScriptObject] removeWebScriptKey:@"client"];
181 //Release the web view
182 [webView release]; webView = nil;
190 [self releaseAllCachedIcons];
192 [plugin release]; plugin = nil;
193 [objectsWithUserIconsArray release]; objectsWithUserIconsArray = nil;
194 [objectIconPathDict release]; objectIconPathDict = nil;
196 //Stop any delayed requests and remove all observers
197 [NSObject cancelPreviousPerformRequestsWithTarget:self];
198 [adium.preferenceController unregisterPreferenceObserver:self];
199 [[NSNotificationCenter defaultCenter] removeObserver:self];
201 //Clean up style/variant info
202 [messageStyle release]; messageStyle = nil;
203 [activeStyle release]; activeStyle = nil;
204 [activeVariant release]; activeVariant = nil;
205 [preferenceGroup release]; preferenceGroup = nil;
207 //Cleanup content processing
208 [contentQueue release]; contentQueue = nil;
209 [storedContentObjects release]; storedContentObjects = nil;
210 [previousContent release]; previousContent = nil;
213 [chat release]; chat = nil;
215 //Release the marked scroller
216 [self.markedScroller release];
221 - (void)setShouldReflectPreferenceChanges:(BOOL)inValue
223 shouldReflectPreferenceChanges = inValue;
225 //We'll want to start storing content objects if we're needing to reflect preference changes
226 if (shouldReflectPreferenceChanges) {
227 if (!storedContentObjects) {
228 storedContentObjects = [[NSMutableArray alloc] init];
231 [storedContentObjects release]; storedContentObjects = nil;
235 - (void)adiumPrint:(id)sender
237 WebPreferences* prefs = [webView preferences];
238 [prefs setShouldPrintBackgrounds:YES];
240 [[[[webView mainFrame] frameView] documentView] print:sender];
243 //WebView --------------------------------------------------------------------------------------------------
246 @synthesize messageStyle, messageView = webView;
248 - (NSView *)messageScrollView
250 return [[webView mainFrame] frameView];
254 * @brief Apply preference changes to our webview
256 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key object:(AIListObject *)object
257 preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
259 // First time won't occur because preferenceGroup is not yet set. Don't run on the assumption that it is, end early.
260 if (!preferenceGroup)
263 if ([group isEqualToString:preferenceGroup]) {
264 #if USE_FASTER_BUT_BUGGY_WEBKIT_PREFERENCE_CHANGE_HANDLING
265 NSString *variantKey = [plugin styleSpecificKey:@"Variant" forStyle:activeStyle];
266 //Variant changes we can apply immediately. All other changes require us to reload the view
267 if (!firstTime && [key isEqualToString:variantKey]) {
268 [activeVariant release]; activeVariant = [[prefDict objectForKey:variantKey] retain];
269 [self _updateVariantWithoutPrimingView];
271 } else if (shouldReflectPreferenceChanges) {
272 //Ignore changes related to our background image cache. These keys are used for storage only and aren't
273 //something we need to update in response to. All other display changes we update our view for.
274 if (![key isEqualToString:@"BackgroundCacheUniqueID"] &&
275 ![key isEqualToString:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]] &&
276 ![key isEqualToString:KEY_CURRENT_WEBKIT_STYLE_PATH]) {
277 [self _updateWebViewForCurrentPreferences];
281 if (shouldReflectPreferenceChanges) {
282 //Ignore changes related to our background image cache. These keys are used for storage only and aren't
283 //something we need to update in response to. All other display changes we update our view for.
284 if (![key isEqualToString:@"BackgroundCacheUniqueID"] &&
285 ![key isEqualToString:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]] &&
286 (![key isEqualToString:KEY_CURRENT_WEBKIT_STYLE_PATH] || shouldReflectPreferenceChanges)) {
287 if (!isUpdatingWebViewForCurrentPreferences) {
288 isUpdatingWebViewForCurrentPreferences = YES;
289 [self _updateWebViewForCurrentPreferences];
290 isUpdatingWebViewForCurrentPreferences = NO;
297 if (([group isEqualToString:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES] && shouldReflectPreferenceChanges)) {
298 //If the background image changes, wipe the cache and update for the new image
299 [adium.preferenceController setPreference:nil
300 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
301 group:preferenceGroup];
302 if (!isUpdatingWebViewForCurrentPreferences) {
303 isUpdatingWebViewForCurrentPreferences = YES;
304 [self _updateWebViewForCurrentPreferences];
305 isUpdatingWebViewForCurrentPreferences = NO;
311 * @brief Initialiaze the web view
316 webView = [[ESWebView alloc] initWithFrame:NSMakeRect(0,0,100,100) //Arbitrary frame
319 [webView setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
320 [delegateProxy addDelegate:self forView:webView];
321 [webView setMaintainsBackForwardList:NO];
324 draggedTypes = [[NSArray alloc] initWithObjects:
325 NSFilenamesPboardType,
326 AIiTunesTrackPboardType,
331 NSFileContentsPboardType,
334 NSPostScriptPboardType,
337 [webView registerForDraggedTypes:draggedTypes];
341 * @brief Updates our webview to the current preferences, priming the view
343 - (void)_updateWebViewForCurrentPreferences
346 [messageStyle autorelease]; messageStyle = nil;
347 [activeStyle release]; activeStyle = nil;
348 [activeVariant release]; activeVariant = nil;
350 //Load the message style
351 messageStyle = [[plugin currentMessageStyleForChat:chat] retain];
352 activeStyle = [[[messageStyle bundle] bundleIdentifier] retain];
353 preferenceGroup = [[plugin preferenceGroupForChat:chat] retain];
355 [webView setPreferencesIdentifier:[NSString stringWithFormat:@"%@-%@",
356 activeStyle, preferenceGroup]];
358 //Get the prefered variant (or the default if a prefered is not available)
359 activeVariant = [[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Variant" forStyle:activeStyle]
360 group:preferenceGroup] retain];
361 if (!activeVariant) activeVariant = [[messageStyle defaultVariant] retain];
362 if (!activeVariant) {
363 /* If the message style doesn't specify a default variant, choose the first one.
364 * Note: Old styles (styleVersion < 3) will always report a variant for defaultVariant.
366 NSArray *availableVariants = [messageStyle availableVariants];
367 if ([availableVariants count]) {
368 activeVariant = [[availableVariants objectAtIndex:0] retain];
372 NSDictionary *prefDict = [adium.preferenceController preferencesForGroup:preferenceGroup];
374 //Update message style behavior: XXX move this somewhere not per-chat
375 [messageStyle setShowUserIcons:[[prefDict objectForKey:KEY_WEBKIT_SHOW_USER_ICONS] boolValue]];
376 [messageStyle setShowHeader:[[prefDict objectForKey:KEY_WEBKIT_SHOW_HEADER] boolValue]];
377 [messageStyle setUseCustomNameFormat:[[prefDict objectForKey:KEY_WEBKIT_USE_NAME_FORMAT] boolValue]];
378 [messageStyle setNameFormat:[[prefDict objectForKey:KEY_WEBKIT_NAME_FORMAT] integerValue]];
379 [messageStyle setDateFormat:[prefDict objectForKey:KEY_WEBKIT_TIME_STAMP_FORMAT]];
380 [messageStyle setShowIncomingMessageColors:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_COLORS] boolValue]];
381 [messageStyle setShowIncomingMessageFonts:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_FONTS] boolValue]];
383 //Custom background image
384 //Webkit wants to load these from disk, but we have it stuffed in a plist. So we'll write it out as an image
385 //into the cache and have webkit fetch from there.
386 NSString *cachePath = nil;
387 if ([[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"UseCustomBackground" forStyle:activeStyle]
388 group:preferenceGroup] boolValue]) {
389 cachePath = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
390 group:preferenceGroup];
391 if (!cachePath || ![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
392 NSData *backgroundImage = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Background" forStyle:activeStyle]
393 group:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
395 if (backgroundImage) {
396 //Generate a unique cache ID for this image
397 NSInteger uniqueID = [[adium.preferenceController preferenceForKey:@"BackgroundCacheUniqueID"
398 group:preferenceGroup] integerValue] + 1;
399 [adium.preferenceController setPreference:[NSNumber numberWithInteger:uniqueID]
400 forKey:@"BackgroundCacheUniqueID"
401 group:preferenceGroup];
403 //Cache the image under that unique ID
404 //Since we prefix the filename with TEMP, Adium will automatically clean it up on quit
405 cachePath = [self _webKitBackgroundImagePathForUniqueID:uniqueID];
406 [backgroundImage writeToFile:cachePath atomically:YES];
408 //Remember where we cached it
409 [adium.preferenceController setPreference:cachePath
410 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
411 group:preferenceGroup];
413 cachePath = @""; //No custom image found
417 [messageStyle setCustomBackgroundColor:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundColor" forStyle:activeStyle]
418 group:preferenceGroup] representedColor]];
420 [messageStyle setCustomBackgroundColor:nil];
423 [messageStyle setCustomBackgroundPath:cachePath];
424 [messageStyle setCustomBackgroundType:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundType" forStyle:activeStyle]
425 group:preferenceGroup] integerValue]];
427 BOOL isBackgroundTransparent = [[self messageStyle] isBackgroundTransparent];
428 [webView setTransparent:isBackgroundTransparent];
429 NSWindow *win = [webView window];
431 [win setOpaque:!isBackgroundTransparent];
433 //Update webview font settings
434 NSString *fontFamily = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontFamily" forStyle:activeStyle]
435 group:preferenceGroup];
436 [webView setFontFamily:(fontFamily ? fontFamily : [messageStyle defaultFontFamily])];
438 NSNumber *fontSize = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontSize" forStyle:activeStyle]
439 group:preferenceGroup];
440 [[webView preferences] setDefaultFontSize:[(fontSize ? fontSize : [messageStyle defaultFontSize]) integerValue]];
442 NSNumber *minSize = [adium.preferenceController preferenceForKey:KEY_WEBKIT_MIN_FONT_SIZE
443 group:preferenceGroup];
444 [[webView preferences] setMinimumFontSize:(minSize ? [minSize integerValue] : 1)];
446 //Update our icons before doing any loading
447 [self sourceOrDestinationChanged:nil];
449 //Prime the webview with the new style/variant and settings, and re-insert all our content back into the view
450 [self _primeWebViewAndReprocessContent:YES];
454 * @brief Updates our webview to the currently active varient without refreshing the view
456 - (void)_updateVariantWithoutPrimingView
458 //We can only change the variant if the web view is ready. If it's not ready we wait a bit and try again.
459 if (webViewIsReady) {
460 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForChangingVariant:activeVariant]];
462 [self performSelector:@selector(_updateVariantWithoutPrimingView) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
467 * @brief Clears the view from displayed messages
469 * Implements the method defined in protocol AIMessageDisplayController
473 [self _primeWebViewAndReprocessContent:NO];
474 [self.markedScroller removeAllMarks];
475 [previousContent release];
476 previousContent = nil;
477 nextMessageFocus = NO;
478 [chat clearUnviewedContentCount];
482 * @brief Primes our webview to the currently active style and variant
484 * The webview won't be ready right away, so we flag it as not ready and set ourself as the frame load delegate so
485 * it will let us know when it's good to go. If reprocessContent is NO, all content in the view will be lost.
487 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent
491 //Hack: this will re-set us for all the delegates, but that shouldn't matter
492 [delegateProxy addDelegate:self forView:webView];
493 [[webView mainFrame] loadHTMLString:[messageStyle baseTemplateWithVariant:activeVariant chat:chat] baseURL:nil];
495 if(chat.isGroupChat && chat.supportsTopic) {
496 // Force a topic update, so we set our topic appropriately.
500 if (reprocessContent) {
501 NSArray *currentContentQueue;
503 //Keep the array of objects waiting to be added, if necessary, to append them after our currently displayed ones
504 currentContentQueue = ([contentQueue count] ?
505 [contentQueue copy] :
508 //Start from an empty content queue
509 [contentQueue removeAllObjects];
511 //Add our stored content objects to the content queue
512 [contentQueue addObjectsFromArray:storedContentObjects];
513 [storedContentObjects removeAllObjects];
515 //Add the old content queue back in if necessary
516 if (currentContentQueue) {
517 [contentQueue addObjectsFromArray:currentContentQueue];
518 [currentContentQueue release];
521 //We're still holding onto the previousContent from before, which is no longer accurate. Release it.
522 [previousContent release]; previousContent = nil;
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
529 * If/when we support transforming chats to/from groupchats we'll need to observe that and call this as appropriate
531 - (void) setIsGroupChat:(BOOL) flag
533 DOMHTMLElement *chatElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"];
534 NSMutableString *chatClassName = [[[chatElement className] mutableCopy] autorelease];
536 [chatClassName replaceOccurrencesOfString:@" groupchat"
538 options:NSLiteralSearch
539 range:NSMakeRange(0, [chatClassName length])];
541 [chatClassName appendString:@" groupchat"];
542 [chatElement setClassName:chatClassName];
545 //Content --------------------------------------------------------------------------------------------------------------
548 * @brief Append new content to our processing queue
550 - (void)contentObjectAdded:(NSNotification *)notification
552 AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
553 [self enqueueContentObject:contentObject];
556 - (void)enqueueContentObject:(AIContentObject *)contentObject
558 [contentQueue addObject:contentObject];
560 /* Immediately update our display if the content requires it.
561 * This is NO, for example, when we receive an entire block of message history content so that we can avoid scrolling
564 if ([contentObject displayContentImmediately]) {
565 [self processQueuedContent];
570 * @brief Our chat finished adding untracked content
572 - (void)chatDidFinishAddingUntrackedContent:(NSNotification *)notification
574 [self processQueuedContent];
578 * @brief Append new content to our processing queueProcess any content in the queuee
580 - (void)processQueuedContent
582 /* If the webview isn't ready, assume we have at least one piece of content left to display */
583 NSUInteger contentQueueCount = 1;
584 NSUInteger objectsAdded = 0;
586 if (webViewIsReady) {
587 contentQueueCount = contentQueue.count;
589 while (contentQueueCount > 0) {
590 BOOL willAddMoreContent = (contentQueueCount > 1);
592 //Display the content
593 AIContentObject *content = [contentQueue objectAtIndex:0];
594 [self _processContentObject:content
595 willAddMoreContentObjects:willAddMoreContent];
597 //If we are going to reflect preference changes, store this content object
598 if (shouldReflectPreferenceChanges) {
599 [storedContentObjects addObject:content];
602 //Remove the content we just displayed from the queue
603 [contentQueue removeObjectAtIndex: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
612 if (objectsAdded > 1) {
613 NSString *scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects];
615 if (scrollToBottomScript) {
616 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
620 //If there is still content to process (the webview wasn't ready), we'll try again after a brief delay
621 if (contentQueueCount) {
622 [self performSelector:@selector(processQueuedContent) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
627 * @brief Process and then append a content object
629 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects
631 AIContentEvent *dateSeparator = nil;
632 BOOL replaceLastContent = NO;
635 If the day has changed since our last message (or if there was no previous message and
636 we are about to display context), insert a date line.
638 if ((!previousContent && [content isKindOfClass:[AIContentContext class]]) ||
639 (![content isFromSameDayAsContent:previousContent])) {
641 NSString *dateMessage = [[NSDateFormatter localizedDateFormatter] stringFromDate:content.date];
643 dateSeparator = [AIContentEvent statusInChat:content.chat
644 withSource:content.chat.listObject
645 destination:content.chat.account
647 message:[[[NSAttributedString alloc] initWithString:dateMessage
648 attributes:[adium.contentController defaultFormattingAttributes]] autorelease]
649 withType:@"date_separator"];
651 if ([content isKindOfClass:[AIContentContext class]])
652 [dateSeparator addDisplayClass:@"history"];
654 //Add the date header
655 [self _appendContent:dateSeparator
657 willAddMoreContentObjects:YES
658 replaceLastContent:NO];
659 [previousContent release]; previousContent = [dateSeparator retain];
662 BOOL similar = (previousContent && [content isSimilarToContent:previousContent] && ![content isKindOfClass:[ESFileTransfer class]]);
663 if ([previousContent isKindOfClass:[AIContentStatus class]] && [content isKindOfClass:[AIContentStatus class]] &&
664 [[(AIContentStatus *)previousContent coalescingKey] isEqualToString:[(AIContentStatus *)content coalescingKey]]) {
666 replaceLastContent = YES;
669 if ([content.type isEqualToString:CONTENT_TOPIC_TYPE]) {
670 DOMHTMLElement *topicElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topic"];
672 if (((AIContentTopic *)content).actuallyBlank) {
673 content.message = [NSAttributedString stringWithString:@""];
676 [topicElement setTitle:content.message.string];
678 [topicElement setInnerHTML:[messageStyle completedTemplateForContent:content similar:similar]];
680 // Mark the current location (the start of this element) if it's a mention.
681 if (content.trackContent && [content.displayClasses containsObject:@"mention"]) {
682 [self markCurrentLocation];
685 if (content.postProcessContent && adium.interfaceController.activeChat != content.chat) {
686 if (nextMessageFocus) {
687 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
689 // Add a class for "first content to lose focus"
690 [content addDisplayClass:@"firstFocus"];
692 nextMessageFocus = NO;
695 // Add a class for "this content received while out of focus"
696 [content addDisplayClass:@"focus"];
699 //Add the content object
700 [self _appendContent:content
702 willAddMoreContentObjects:willAddMoreContentObjects
703 replaceLastContent:replaceLastContent];
706 [previousContent release]; previousContent = [content retain];
710 * @brief Append a content object
712 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
714 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
715 similar:contentIsSimilar
716 willAddMoreContentObjects:willAddMoreContentObjects
717 replaceLastContent:replaceLastContent]];
719 NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
724 * @brief Force a topic update.
726 * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
730 NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
732 AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
733 withSource:chat.topicSetter
738 // In case this topic is blank, we have to filter this ourself; the content controller will drop it.
739 contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
741 [self enqueueContentObject:contentTopic];
744 //WebView Delegates ----------------------------------------------------------------------------------------------------
745 #pragma mark Webview delegates
747 - (void)webViewIsReady{
748 webViewIsReady = YES;
749 [self setupMarkedScroller];
750 [self setIsGroupChat:chat.isGroupChat];
751 [self processQueuedContent];
754 - (void)openImage:(id)sender
756 NSURL *imageURL = [sender representedObject];
757 [[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
760 - (void)saveImageAs:(id)sender
762 NSURL *imageURL = [sender representedObject];
763 NSString *path = [imageURL path];
765 NSSavePanel *savePanel = [NSSavePanel savePanel];
766 [savePanel beginSheetForDirectory:nil
767 file:[path lastPathComponent]
768 modalForWindow:[webView window]
770 didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
771 contextInfo:[imageURL retain]];
774 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
776 NSURL *imageURL = (NSURL *)contextInfo;
778 if (returnCode == NSOKButton) {
779 [[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
780 toPath:[[sheet URL] absoluteString]
788 * @brief Append our own menu items to the webview's contextual menus
790 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
792 NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
793 AIListContact *chatListObject = chat.listObject.parentContact;
794 NSMenuItem *menuItem;
796 //Remove default items we don't want
797 if (webViewMenuItems) {
799 for (menuItem in defaultMenuItems) {
800 NSInteger tag = [menuItem tag];
801 if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
802 (tag == WebMenuItemTagDownloadLinkToDisk) ||
803 (tag == WebMenuItemTagOpenImageInNewWindow) ||
804 (tag == WebMenuItemTagDownloadImageToDisk) ||
805 (tag == WebMenuItemTagOpenFrameInNewWindow) ||
806 (tag == WebMenuItemTagStop) ||
807 (tag == WebMenuItemTagReload)) {
808 [webViewMenuItems removeObjectIdenticalTo:menuItem];
810 //This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
811 if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
812 [webViewMenuItems removeObjectIdenticalTo:menuItem];
818 if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
820 if (!webViewMenuItems) {
821 webViewMenuItems = [NSMutableArray array];
824 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
826 action:@selector(openImage:)
828 representedObject:imageURL];
829 [webViewMenuItems addObject:menuItem];
831 menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
833 action:@selector(saveImageAs:)
835 representedObject:imageURL];
836 [webViewMenuItems addObject:menuItem];
840 NSString *imgClass = [img className];
841 //being very careful to only get user icons... a better way would be to put a class "usericon" on the img, but I haven't worked out how to do that, so we test for the name of the person in the src, and that it's not an emoticon or direct connect image.
842 if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
843 [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
844 [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
845 [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
850 if (webViewMenuItems) {
851 //Add a separator item if items already exist in webViewMenuItems
852 if ([webViewMenuItems count]) {
853 // If the first item is a separator item, remove it.
854 if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
855 [webViewMenuItems removeObjectAtIndex:0];
858 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
861 webViewMenuItems = [NSMutableArray array];
864 NSMenu *originalMenu = nil;
866 if (chatListObject) {
868 if ([chatListObject isIntentionallyNotAStranger]) {
869 locations = [NSArray arrayWithObjects:
870 [NSNumber numberWithInteger:Context_Contact_Manage],
871 [NSNumber numberWithInteger:Context_Contact_Action],
872 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
873 [NSNumber numberWithInteger:Context_Contact_ChatAction],
874 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
876 locations = [NSArray arrayWithObjects:
877 [NSNumber numberWithInteger:Context_Contact_Manage],
878 [NSNumber numberWithInteger:Context_Contact_Action],
879 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
880 [NSNumber numberWithInteger:Context_Contact_ChatAction],
881 [NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
882 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
885 originalMenu = [adium.menuController contextualMenuWithLocations:locations
886 forListObject:chatListObject
888 } else if(chat.isGroupChat) {
889 originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
890 [NSNumber numberWithInteger:Context_GroupChat_Manage],
891 [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
895 [webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
896 [originalMenu removeAllItems];
898 if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
899 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
901 //Present an option to clear the display
902 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
904 action:@selector(clearView)
906 [webViewMenuItems addObject:menuItem];
909 return webViewMenuItems;
913 * @brief Add ourself to the window script object bridge when it's safe to do so
915 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
917 [[webView windowScriptObject] setValue:self forKey:@"client"];
920 //Dragging delegate ----------------------------------------------------------------------------------------------------
921 #pragma mark Dragging delegate
923 * @brief If possible, return the first NSTextView in the message view's responder chain
925 * This is used for drag and drop behavior.
927 - (NSTextView *)textView
929 id responder = [webView nextResponder];
931 //Walkin the responder chain looking for an NSTextView
933 ![responder isKindOfClass:[NSTextView class]]) {
934 responder = [responder nextResponder];
941 * @brief Dragging entered
943 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
945 NSPasteboard *pasteboard = [sender draggingPasteboard];
947 return ([pasteboard availableTypeFromArray:draggedTypes] ?
948 NSDragOperationCopy :
949 NSDragOperationNone);
953 * @brief Dragging updated
955 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
957 return [self draggingEntered:sender];
961 * @brief Handle a drag onto the webview
963 * If we're getting a non-image file, we can handle it immediately. Otherwise, the drag is the textView's problem.
965 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
967 NSPasteboard *pasteboard = [sender draggingPasteboard];
970 if ([self shouldHandleDragWithPasteboard:pasteboard]) {
972 //Not an image but it is a file - send it immediately as a file transfer
973 NSArray *files = [pasteboard propertyListForType:NSFilenamesPboardType];
975 for (path in files) {
976 AIListObject *listObject = chat.listObject;
978 [adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
984 NSTextView *textView = [self textView];
986 [[webView window] makeFirstResponder:textView]; //Make it first responder
987 success = [textView performDragOperation:sender];
995 * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
997 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
999 NSPasteboard *pasteboard = [sender draggingPasteboard];
1002 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
1003 NSTextView *textView = [self textView];
1005 success = [textView prepareForDragOperation:sender];
1013 * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
1015 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1017 NSPasteboard *pasteboard = [sender draggingPasteboard];
1019 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
1020 NSTextView *textView = [self textView];
1022 [textView concludeDragOperation:sender];
1028 * @brief Handle drags of content we recognize
1030 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
1033 return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
1034 [pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
1040 //User Icon masking --------------------------------------------------------------------------------------------------
1041 //We allow messaage styles to specify masks for user icons. This could be user to round the corners of user icons
1042 //or other related effects.
1043 #pragma mark User icon masking
1045 * @brief Update icon masks when participating list objects change
1047 * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
1048 * When the list changes, remove the observers we had in place before and add observers for each object in the list
1049 * so we never observe for contacts not in the chat.
1051 - (void)participatingListObjectsChanged:(NSNotification *)notification
1053 NSArray *participatingListObjects = [chat containedObjects];
1055 [[NSNotificationCenter defaultCenter] removeObserver:self
1056 name:ListObject_AttributesChanged
1059 for (AIListObject *listObject in participatingListObjects) {
1060 //Update the mask for any user which just entered the chat
1061 if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
1062 [self updateUserIconForObject:listObject];
1065 //In the future, watch for changes on the parent object, since that's the icon we display
1066 if ([listObject isKindOfClass:[AIListContact class]]) {
1067 [[NSNotificationCenter defaultCenter] addObserver:self
1068 selector:@selector(listObjectAttributesChanged:)
1069 name:ListObject_AttributesChanged
1070 object:[(AIListContact *)listObject parentContact]];
1074 //Also observe our account
1076 [[NSNotificationCenter defaultCenter] addObserver:self
1077 selector:@selector(listObjectAttributesChanged:)
1078 name:ListObject_AttributesChanged
1079 object:chat.account];
1082 //Remove the cache for any object no longer in the chat
1083 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1084 if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
1085 (![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
1086 !(listObject == chat.account)) {
1087 [self releaseCurrentWebKitUserIconForObject:listObject];
1093 * @brief Update icon masks when source or destination changes
1095 - (void)sourceOrDestinationChanged:(NSNotification *)notification
1097 //Update the participating contacts
1098 [self participatingListObjectsChanged:nil];
1100 //And update the source account
1101 [self updateUserIconForObject:chat.account];
1103 [self updateServiceIcon];
1107 * @brief Update the icon when a list object's icon attributes change
1109 - (void)listObjectAttributesChanged:(NSNotification *)notification
1111 AIListObject *inObject = [notification object];
1112 NSSet *keys = [[notification userInfo] objectForKey:@"Keys"];
1114 if ([keys containsObject:KEY_USER_ICON]) {
1116 AIListObject *actualObject = nil;
1117 AILogWithSignature(@"%@'s icon changed", inObject);
1118 if (chat.account == inObject) {
1119 //The account is the object actually in the chat
1120 actualObject = inObject;
1123 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
1124 * be displaying as changed.
1127 for (AIListContact *participatingListObject in chat) {
1128 if ([participatingListObject parentContact] == inObject) {
1129 actualObject = participatingListObject;
1136 [self userIconForObjectDidChange:actualObject];
1140 AILogWithSignature(@"nil object's icon changed");
1141 //We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
1142 [self sourceOrDestinationChanged:nil];
1147 - (void)userIconForObjectDidChange:(AIListObject *)inObject
1149 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1150 [(AIListContact *)inObject parentContact] :
1152 NSString *currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1153 if (currentIconPath) {
1154 NSString *objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1155 if (objectsKnownIconPath &&
1156 [currentIconPath isEqualToString:objectsKnownIconPath]) {
1157 //We're the first one to get to this object! We get to delete the old path and remove the reference to it
1158 [[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
1159 [iconSourceObject setValue:nil
1160 forProperty:KEY_WEBKIT_USER_ICON
1161 notify:NotifyNever];
1163 /* Some other instance beat us to the punch. The object's KEY_WEBKIT_USER_ICON is right, since it doesn't match our
1164 * internally tracked path.
1169 [self updateUserIconForObject:iconSourceObject];
1173 * @brief Remove all references to *this* chat using cached icons for an object
1175 * If this is the last chat utilizing the cached icon, it will be deleted.
1177 * @param inObject The object
1179 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
1181 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1182 [(AIListContact *)inObject parentContact] :
1186 NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
1187 chatsUsingCachedIcon--;
1188 [iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
1189 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1190 notify:NotifyNever];
1191 [objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
1193 if ((chatsUsingCachedIcon <= 0) &&
1194 (path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
1195 [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
1196 [iconSourceObject setValue:nil
1197 forProperty:KEY_WEBKIT_USER_ICON
1198 notify:NotifyNever];
1201 [objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
1205 * @brief Remove all references to *this* chat using cached icons for all involved objects
1207 - (void)releaseAllCachedIcons
1209 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1210 [self releaseCurrentWebKitUserIconForObject:listObject];
1215 * @brief Generate an updated masked user icon for the passed list object
1217 - (void)updateUserIconForObject:(AIListObject *)inObject
1219 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1220 [(AIListContact *)inObject parentContact] :
1223 NSString *oldWebKitUserIconPath = nil;
1224 NSString *webKitUserIconPath = nil;
1225 NSImage *webKitUserIcon;
1228 * We probably already have a userIcon waiting for us, the active display icon; use that
1229 * rather than loading one from disk.
1231 if (!(userIcon = [iconSourceObject userIcon])) {
1232 //If that's not the case, try using the UserIconPath
1233 NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
1235 userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
1239 if ([messageStyle userIconMask]) {
1240 //Apply the mask if the style has one
1241 //XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
1242 webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
1243 [webKitUserIcon lockFocus];
1244 [userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
1245 fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
1246 operation:NSCompositeSourceIn
1248 [webKitUserIcon unlockFocus];
1250 //Otherwise, just use the icon as-is
1251 webKitUserIcon = userIcon;
1254 oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1255 webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1257 if (!webKitUserIconPath) {
1258 /* If the image doesn't know a path to use, write it out and set it.
1260 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
1261 * any animation, which is good since animation in the message view is slow and annoying.
1263 * Only write out the icon if the object doesn't already have one
1265 webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
1266 if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
1268 [iconSourceObject setValue:webKitUserIconPath
1269 forProperty:KEY_WEBKIT_USER_ICON
1272 AILogWithSignature(@"Warning: Could not write out icon to %@", webKitUserIconPath);
1276 //Make sure it's known that this user has been handled
1277 if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
1278 [objectsWithUserIconsArray addObject:iconSourceObject];
1280 //Keep track of this chat using the icon
1281 [iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
1282 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1283 notify:NotifyNever];
1286 if (!webKitUserIconPath) webKitUserIconPath = @"";
1288 if ([webView mainFrameDocument]) {
1289 //Update existing images if the webView has loaded and has a main frame
1290 if (oldWebKitUserIconPath &&
1291 ![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
1293 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1294 NSUInteger imagesCount = [images length];
1296 webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
1298 for (NSInteger i = 0; i < imagesCount; i++) {
1299 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1300 NSString *currentSrc = [img getAttribute:@"src"];
1301 if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
1302 [img setSrc:webKitUserIconPath];
1307 [objectIconPathDict setObject:webKitUserIconPath
1308 forKey:iconSourceObject.internalObjectID];
1310 /* Otherwise, try to again in a moment. We've already done the heavy lifting
1311 * such as writing out the icon, so it's cheap to recurse.
1313 [self performSelector:@selector(updateUserIconForObject:)
1320 - (void)updateServiceIcon
1322 DOMDocument *doc = [webView mainFrameDocument];
1323 //Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
1324 if(![doc respondsToSelector:@selector(getElementsByClassName:)])
1326 DOMNodeList *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
1327 NSUInteger imagesCount = [serviceIconImages length];
1329 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID
1330 type:AIServiceIconLarge];
1332 for (NSInteger i = 0; i < imagesCount; i++) {
1333 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
1334 [img setSrc:serviceIconPath];
1338 - (void)customEmoticonUpdated:(NSNotification *)inNotification
1340 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1341 NSUInteger imagesCount = [images length];
1343 if (imagesCount > 0) {
1344 AIEmoticon *emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
1345 NSString *textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
1346 NSString *path = [emoticon path];
1347 NSSize emoticonSize = [[emoticon image] size];
1348 BOOL updatedImage = NO;
1349 path = [[NSURL fileURLWithPath:path] absoluteString];
1350 for (NSInteger i = 0; i < imagesCount; i++) {
1351 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1353 if ([[img className] isEqualToString:@"emoticon"] &&
1354 [[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
1356 [img setWidth:emoticonSize.width];
1357 [img setHeight:emoticonSize.height];
1361 NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
1363 if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
1365 if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat"
1366 withArguments:[NSArray arrayWithObject:shouldScroll]];
1371 * @brief Returns the path the background image given a unique ID
1373 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
1375 NSString *filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
1376 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1380 * @brief Returns the path to the list object's masked user icon
1382 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
1384 NSString *filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
1385 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1388 #pragma mark File Transfer
1390 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
1392 ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
1393 ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
1396 AIFileTransferAction a;
1397 if ([action isEqualToString:@"SaveAs"])
1399 else if ([action isEqualToString:@"Cancel"])
1404 [tc handleFileTransferAction:a];
1408 #pragma mark Topic editing
1409 - (void)editingDidComplete:(DOMRange *)range
1411 DOMNode *node = range.startContainer;
1412 DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
1414 NSString *topicChange = nil;
1416 if (node == topicEdit || node.parentNode == topicEdit) {
1417 topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];
1419 NSTextView *textView = [self textView];
1421 [[webView window] makeFirstResponder:textView]; //Make it first responder
1424 // Update the topic div in case the user doesn't have permission to change it.
1427 // Tell the chat to set the topic.
1428 [chat setTopic:topicChange];
1432 #pragma mark Marked Scroller
1433 - (JVMarkedScroller *)markedScroller
1435 WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
1436 NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
1438 JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
1439 return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
1442 - (void)setupMarkedScroller
1444 WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
1445 NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
1447 [scrollView setHasHorizontalScroller:NO];
1449 JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
1450 if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
1451 NSRect scrollerFrame = [[scrollView verticalScroller] frame];
1452 NSScroller *oldScroller = scroller;
1453 scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
1454 [scroller setFloatValue:oldScroller.floatValue];
1455 [scroller setKnobProportion:oldScroller.knobProportion];
1456 [scrollView setVerticalScroller:scroller];
1460 - (NSNumber *)currentOffsetHeight
1462 // We use the body's height to determine our mark location.
1463 return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
1466 - (void)markCurrentLocation
1468 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
1471 #define PREF_KEY_FOCUS_LINE @"Draw Focus Lines"
1473 - (void)markForFocusChange
1475 // We use the current Chat element's height to determine our mark location.
1476 [self.markedScroller removeMarkWithIdentifier:@"focus"];
1478 // The next message being inserted needs to add a mark.
1479 nextMessageFocus = YES;
1481 DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
1482 DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
1483 for (NSUInteger i = 0; i < nodeList.length; i++)
1485 node = (DOMHTMLElement *)[nodeList item:i];
1486 classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
1488 [classes removeObject:@"focus"];
1489 [classes removeObject:@"firstFocus"];
1491 node.className = [classes componentsJoinedByString:@" "];
1500 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
1503 - (void)jumpToPreviousMark
1505 [self.markedScroller jumpToPreviousMark:nil];
1508 - (BOOL)previousMarkExists
1510 return [self.markedScroller previousMarkExists];
1513 - (void)jumpToNextMark
1515 [self.markedScroller jumpToNextMark:nil];
1518 - (BOOL)nextMarkExists
1520 return [self.markedScroller nextMarkExists];
1523 - (void)jumpToFocusMark
1525 [self.markedScroller jumpToFocusMark:nil];
1528 - (BOOL)focusMarkExists
1530 return [self.markedScroller focusMarkExists];
1533 #pragma mark JS Bridging
1534 /*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
1537 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
1540 sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
1541 sel_isEqual(aSelector, @selector(debugLog:)) ||
1542 sel_isEqual(aSelector, @selector(zoomImage:))
1550 * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
1551 * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
1552 * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
1554 * Any colon (“:”)in the Objective-C selector is replaced by an underscore (“_”).
1555 * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
1556 * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
1558 + (NSString *)webScriptNameForSelector:(SEL)aSelector
1560 if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
1561 if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
1562 if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
1566 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
1568 NSMutableString *className = [[[img className] mutableCopy] autorelease];
1569 if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
1570 [className replaceOccurrencesOfString:@"fullSizeImage"
1571 withString:@"scaledToFitImage"
1572 options:NSLiteralSearch
1573 range:NSMakeRange(0, [className length])];
1574 else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
1575 [className replaceOccurrencesOfString:@"scaledToFitImage"
1576 withString:@"fullSizeImage"
1577 options:NSLiteralSearch
1578 range:NSMakeRange(0, [className length])];
1582 [img setClassName:className];
1583 [[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
1588 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
1590 //gets the source of the html page, for debugging
1591 - (NSString *)webviewSource
1593 return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
1597 * @brief Set the HTML content for the "Chat" area.
1599 - (void)setChatContentSource:(NSString *)source
1601 if (!webViewIsReady) {
1602 // If the webview isn't ready yet, wait a very short amount of time before trying again
1603 [self performSelector:@selector(setChatContentSource:)
1607 // Add the old "Chat" element to the window.
1608 [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
1610 NSString *scrollToBottomScript;
1611 if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
1612 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
1618 * @brief Get the HTML content for the "Chat" area.
1620 - (NSString *)chatContentSource
1622 return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
1626 * @brief The unique name for this style of "content source"
1628 - (NSString *)contentSourceName
1630 return [[[messageStyle bundle] bundlePath] lastPathComponent];