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.
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:activeStyle];
357 //Get the prefered variant (or the default if a prefered is not available)
358 activeVariant = [[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Variant" forStyle:activeStyle]
359 group:preferenceGroup] retain];
360 if (!activeVariant) activeVariant = [[messageStyle defaultVariant] retain];
361 if (!activeVariant) {
362 /* If the message style doesn't specify a default variant, choose the first one.
363 * Note: Old styles (styleVersion < 3) will always report a variant for defaultVariant.
365 NSArray *availableVariants = [messageStyle availableVariants];
366 if ([availableVariants count]) {
367 activeVariant = [[availableVariants objectAtIndex:0] retain];
371 NSDictionary *prefDict = [adium.preferenceController preferencesForGroup:preferenceGroup];
373 //Update message style behavior: XXX move this somewhere not per-chat
374 [messageStyle setShowUserIcons:[[prefDict objectForKey:KEY_WEBKIT_SHOW_USER_ICONS] boolValue]];
375 [messageStyle setShowHeader:[[prefDict objectForKey:KEY_WEBKIT_SHOW_HEADER] boolValue]];
376 [messageStyle setUseCustomNameFormat:[[prefDict objectForKey:KEY_WEBKIT_USE_NAME_FORMAT] boolValue]];
377 [messageStyle setNameFormat:[[prefDict objectForKey:KEY_WEBKIT_NAME_FORMAT] integerValue]];
378 [messageStyle setDateFormat:[prefDict objectForKey:KEY_WEBKIT_TIME_STAMP_FORMAT]];
379 [messageStyle setShowIncomingMessageColors:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_COLORS] boolValue]];
380 [messageStyle setShowIncomingMessageFonts:[[prefDict objectForKey:KEY_WEBKIT_SHOW_MESSAGE_FONTS] boolValue]];
382 //Custom background image
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
384 //into the cache and have webkit fetch from there.
385 NSString *cachePath = nil;
386 if ([[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"UseCustomBackground" forStyle:activeStyle]
387 group:preferenceGroup] boolValue]) {
388 cachePath = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
389 group:preferenceGroup];
390 if (!cachePath || ![[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
391 NSData *backgroundImage = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"Background" forStyle:activeStyle]
392 group:PREF_GROUP_WEBKIT_BACKGROUND_IMAGES];
394 if (backgroundImage) {
395 //Generate a unique cache ID for this image
396 NSInteger uniqueID = [[adium.preferenceController preferenceForKey:@"BackgroundCacheUniqueID"
397 group:preferenceGroup] integerValue] + 1;
398 [adium.preferenceController setPreference:[NSNumber numberWithInteger:uniqueID]
399 forKey:@"BackgroundCacheUniqueID"
400 group:preferenceGroup];
402 //Cache the image under that unique ID
403 //Since we prefix the filename with TEMP, Adium will automatically clean it up on quit
404 cachePath = [self _webKitBackgroundImagePathForUniqueID:uniqueID];
405 [backgroundImage writeToFile:cachePath atomically:YES];
407 //Remember where we cached it
408 [adium.preferenceController setPreference:cachePath
409 forKey:[plugin styleSpecificKey:@"BackgroundCachePath" forStyle:activeStyle]
410 group:preferenceGroup];
412 cachePath = @""; //No custom image found
416 [messageStyle setCustomBackgroundColor:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundColor" forStyle:activeStyle]
417 group:preferenceGroup] representedColor]];
419 [messageStyle setCustomBackgroundColor:nil];
422 [messageStyle setCustomBackgroundPath:cachePath];
423 [messageStyle setCustomBackgroundType:[[adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"BackgroundType" forStyle:activeStyle]
424 group:preferenceGroup] integerValue]];
426 BOOL isBackgroundTransparent = [[self messageStyle] isBackgroundTransparent];
427 [webView setTransparent:isBackgroundTransparent];
428 NSWindow *win = [webView window];
430 [win setOpaque:!isBackgroundTransparent];
432 //Update webview font settings
433 NSString *fontFamily = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontFamily" forStyle:activeStyle]
434 group:preferenceGroup];
435 [webView setFontFamily:(fontFamily ? fontFamily : [messageStyle defaultFontFamily])];
437 NSNumber *fontSize = [adium.preferenceController preferenceForKey:[plugin styleSpecificKey:@"FontSize" forStyle:activeStyle]
438 group:preferenceGroup];
439 [[webView preferences] setDefaultFontSize:[(fontSize ? fontSize : [messageStyle defaultFontSize]) integerValue]];
441 NSNumber *minSize = [adium.preferenceController preferenceForKey:KEY_WEBKIT_MIN_FONT_SIZE
442 group:preferenceGroup];
443 [[webView preferences] setMinimumFontSize:(minSize ? [minSize integerValue] : 1)];
445 //Update our icons before doing any loading
446 [self sourceOrDestinationChanged:nil];
448 //Prime the webview with the new style/variant and settings, and re-insert all our content back into the view
449 [self _primeWebViewAndReprocessContent:YES];
453 * @brief Updates our webview to the currently active varient without refreshing the view
455 - (void)_updateVariantWithoutPrimingView
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.
458 if (webViewIsReady) {
459 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForChangingVariant:activeVariant]];
461 [self performSelector:@selector(_updateVariantWithoutPrimingView) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
466 * @brief Clears the view from displayed messages
468 * Implements the method defined in protocol AIMessageDisplayController
472 [self _primeWebViewAndReprocessContent:NO];
473 [self.markedScroller removeAllMarks];
474 [previousContent release];
475 previousContent = nil;
476 nextMessageFocus = NO;
477 [chat clearUnviewedContentCount];
481 * @brief Primes our webview to the currently active style and variant
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
484 * it will let us know when it's good to go. If reprocessContent is NO, all content in the view will be lost.
486 - (void)_primeWebViewAndReprocessContent:(BOOL)reprocessContent
490 //Hack: this will re-set us for all the delegates, but that shouldn't matter
491 [delegateProxy addDelegate:self forView:webView];
492 [[webView mainFrame] loadHTMLString:[messageStyle baseTemplateWithVariant:activeVariant chat:chat] baseURL:nil];
494 if(chat.isGroupChat && chat.supportsTopic) {
495 // Force a topic update, so we set our topic appropriately.
499 if (reprocessContent) {
500 NSArray *currentContentQueue;
502 //Keep the array of objects waiting to be added, if necessary, to append them after our currently displayed ones
503 currentContentQueue = ([contentQueue count] ?
504 [contentQueue copy] :
507 //Start from an empty content queue
508 [contentQueue removeAllObjects];
510 //Add our stored content objects to the content queue
511 [contentQueue addObjectsFromArray:storedContentObjects];
512 [storedContentObjects removeAllObjects];
514 //Add the old content queue back in if necessary
515 if (currentContentQueue) {
516 [contentQueue addObjectsFromArray:currentContentQueue];
517 [currentContentQueue release];
520 //We're still holding onto the previousContent from before, which is no longer accurate. Release it.
521 [previousContent release]; previousContent = nil;
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
528 * If/when we support transforming chats to/from groupchats we'll need to observe that and call this as appropriate
530 - (void) setIsGroupChat:(BOOL) flag
532 DOMHTMLElement *chatElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"];
533 NSMutableString *chatClassName = [[[chatElement className] mutableCopy] autorelease];
535 [chatClassName replaceOccurrencesOfString:@" groupchat"
537 options:NSLiteralSearch
538 range:NSMakeRange(0, [chatClassName length])];
540 [chatClassName appendString:@" groupchat"];
541 [chatElement setClassName:chatClassName];
544 //Content --------------------------------------------------------------------------------------------------------------
547 * @brief Append new content to our processing queue
549 - (void)contentObjectAdded:(NSNotification *)notification
551 AIContentObject *contentObject = [[notification userInfo] objectForKey:@"AIContentObject"];
552 [self enqueueContentObject:contentObject];
555 - (void)enqueueContentObject:(AIContentObject *)contentObject
557 [contentQueue addObject:contentObject];
559 /* Immediately update our display if the content requires it.
560 * This is NO, for example, when we receive an entire block of message history content so that we can avoid scrolling
563 if ([contentObject displayContentImmediately]) {
564 [self processQueuedContent];
569 * @brief Our chat finished adding untracked content
571 - (void)chatDidFinishAddingUntrackedContent:(NSNotification *)notification
573 [self processQueuedContent];
577 * @brief Append new content to our processing queueProcess any content in the queuee
579 - (void)processQueuedContent
581 /* If the webview isn't ready, assume we have at least one piece of content left to display */
582 NSUInteger contentQueueCount = 1;
583 NSUInteger objectsAdded = 0;
585 if (webViewIsReady) {
586 contentQueueCount = contentQueue.count;
588 while (contentQueueCount > 0) {
589 BOOL willAddMoreContent = (contentQueueCount > 1);
591 //Display the content
592 AIContentObject *content = [contentQueue objectAtIndex:0];
593 [self _processContentObject:content
594 willAddMoreContentObjects:willAddMoreContent];
596 //If we are going to reflect preference changes, store this content object
597 if (shouldReflectPreferenceChanges) {
598 [storedContentObjects addObject:content];
601 //Remove the content we just displayed from the queue
602 [contentQueue removeObjectAtIndex: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
611 if (objectsAdded > 1) {
612 NSString *scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects];
614 if (scrollToBottomScript) {
615 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
619 //If there is still content to process (the webview wasn't ready), we'll try again after a brief delay
620 if (contentQueueCount) {
621 [self performSelector:@selector(processQueuedContent) withObject:nil afterDelay:NEW_CONTENT_RETRY_DELAY];
626 * @brief Process and then append a content object
628 - (void)_processContentObject:(AIContentObject *)content willAddMoreContentObjects:(BOOL)willAddMoreContentObjects
630 AIContentEvent *dateSeparator = nil;
631 BOOL replaceLastContent = NO;
634 If the day has changed since our last message (or if there was no previous message and
635 we are about to display context), insert a date line.
637 if ((!previousContent && [content isKindOfClass:[AIContentContext class]]) ||
638 (![content isFromSameDayAsContent:previousContent])) {
640 NSString *dateMessage = [[NSDateFormatter localizedDateFormatter] stringFromDate:content.date];
642 dateSeparator = [AIContentEvent statusInChat:content.chat
643 withSource:content.chat.listObject
644 destination:content.chat.account
646 message:[[[NSAttributedString alloc] initWithString:dateMessage
647 attributes:[adium.contentController defaultFormattingAttributes]] autorelease]
648 withType:@"date_separator"];
650 if ([content isKindOfClass:[AIContentContext class]])
651 [dateSeparator addDisplayClass:@"history"];
653 //Add the date header
654 [self _appendContent:dateSeparator
656 willAddMoreContentObjects:YES
657 replaceLastContent:NO];
658 [previousContent release]; previousContent = [dateSeparator retain];
661 BOOL similar = (previousContent && [content isSimilarToContent:previousContent] && ![content isKindOfClass:[ESFileTransfer class]]);
662 if ([previousContent isKindOfClass:[AIContentStatus class]] && [content isKindOfClass:[AIContentStatus class]] &&
663 [[(AIContentStatus *)previousContent coalescingKey] isEqualToString:[(AIContentStatus *)content coalescingKey]]) {
665 replaceLastContent = YES;
668 if ([content.type isEqualToString:CONTENT_TOPIC_TYPE]) {
669 DOMHTMLElement *topicElement = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topic"];
671 if (((AIContentTopic *)content).actuallyBlank) {
672 content.message = [NSAttributedString stringWithString:@""];
675 [topicElement setTitle:content.message.string];
677 [topicElement setInnerHTML:[messageStyle completedTemplateForContent:content similar:similar]];
679 // Mark the current location (the start of this element) if it's a mention.
680 if (content.trackContent && [content.displayClasses containsObject:@"mention"]) {
681 [self markCurrentLocation];
684 // Set it as a focus if appropriate.
685 if (nextMessageFocus && [content.type isEqualToString:CONTENT_MESSAGE_TYPE]) {
686 [content addDisplayClass:@"focus"];
687 nextMessageFocus = NO;
690 //Add the content object
691 [self _appendContent:content
693 willAddMoreContentObjects:willAddMoreContentObjects
694 replaceLastContent:replaceLastContent];
697 [previousContent release]; previousContent = [content retain];
701 * @brief Append a content object
703 - (void)_appendContent:(AIContentObject *)content similar:(BOOL)contentIsSimilar willAddMoreContentObjects:(BOOL)willAddMoreContentObjects replaceLastContent:(BOOL)replaceLastContent
705 [webView stringByEvaluatingJavaScriptFromString:[messageStyle scriptForAppendingContent:content
706 similar:contentIsSimilar
707 willAddMoreContentObjects:willAddMoreContentObjects
708 replaceLastContent:replaceLastContent]];
710 NSAccessibilityPostNotification(webView, NSAccessibilityValueChangedNotification);
715 * @brief Force a topic update.
717 * We have to filter this ourself because, if the topic is blank, the content controller will never show it to us.
721 NSAttributedString *topic = [NSAttributedString stringWithString:(chat.topic ?: @"")];
723 AIContentTopic *contentTopic = [AIContentTopic topicInChat:chat
724 withSource:chat.topicSetter
729 // In case this topic is blank, we have to filter this ourself; the content controller will drop it.
730 contentTopic.message = [adium.contentController filterAttributedString:topic usingFilterType:AIFilterDisplay direction:AIFilterIncoming context:contentTopic];
732 [self enqueueContentObject:contentTopic];
735 //WebView Delegates ----------------------------------------------------------------------------------------------------
736 #pragma mark Webview delegates
738 - (void)webViewIsReady{
739 webViewIsReady = YES;
740 [self setupMarkedScroller];
741 [self setIsGroupChat:chat.isGroupChat];
742 [self processQueuedContent];
745 - (void)openImage:(id)sender
747 NSURL *imageURL = [sender representedObject];
748 [[NSWorkspace sharedWorkspace] openFile:[imageURL path]];
751 - (void)saveImageAs:(id)sender
753 NSURL *imageURL = [sender representedObject];
754 NSString *path = [imageURL path];
756 NSSavePanel *savePanel = [NSSavePanel savePanel];
757 [savePanel beginSheetForDirectory:nil
758 file:[path lastPathComponent]
759 modalForWindow:[webView window]
761 didEndSelector:@selector(savePanelDidEnd:returnCode:contextInfo:)
762 contextInfo:[imageURL retain]];
765 - (void)savePanelDidEnd:(NSSavePanel *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
767 NSURL *imageURL = (NSURL *)contextInfo;
769 if (returnCode == NSOKButton) {
770 [[NSFileManager defaultManager] copyItemAtPath:[imageURL absoluteString]
771 toPath:[[sheet URL] absoluteString]
779 * @brief Append our own menu items to the webview's contextual menus
781 - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems
783 NSMutableArray *webViewMenuItems = [[defaultMenuItems mutableCopy] autorelease];
784 AIListContact *chatListObject = chat.listObject.parentContact;
785 NSMenuItem *menuItem;
787 //Remove default items we don't want
788 if (webViewMenuItems) {
790 for (menuItem in defaultMenuItems) {
791 NSInteger tag = [menuItem tag];
792 if ((tag == WebMenuItemTagOpenLinkInNewWindow) ||
793 (tag == WebMenuItemTagDownloadLinkToDisk) ||
794 (tag == WebMenuItemTagOpenImageInNewWindow) ||
795 (tag == WebMenuItemTagDownloadImageToDisk) ||
796 (tag == WebMenuItemTagOpenFrameInNewWindow) ||
797 (tag == WebMenuItemTagStop) ||
798 (tag == WebMenuItemTagReload)) {
799 [webViewMenuItems removeObjectIdenticalTo:menuItem];
801 //This isn't as nice; there's no tag available. Use the localization from WebKit to look at the title.
802 if ([[menuItem title] isEqualToString:NSLocalizedStringFromTableInBundle(@"Open Link", nil, [NSBundle bundleForClass:[WebView class]], nil)])
803 [webViewMenuItems removeObjectIdenticalTo:menuItem];
809 if ((imageURL = [element objectForKey:WebElementImageURLKey])) {
811 if (!webViewMenuItems) {
812 webViewMenuItems = [NSMutableArray array];
815 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Open Image", nil)
817 action:@selector(openImage:)
819 representedObject:imageURL];
820 [webViewMenuItems addObject:menuItem];
822 menuItem = [[NSMenuItem alloc] initWithTitle:[AILocalizedString(@"Save Image As", nil) stringByAppendingEllipsis]
824 action:@selector(saveImageAs:)
826 representedObject:imageURL];
827 [webViewMenuItems addObject:menuItem];
831 NSString *imgClass = [img className];
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.
833 if([[img getAttribute:@"src"] rangeOfString:internalObjectID].location != NSNotFound &&
834 [imgClass rangeOfString:@"emoticon"].location == NSNotFound &&
835 [imgClass rangeOfString:@"fullSizeImage"].location == NSNotFound &&
836 [imgClass rangeOfString:@"scaledToFitImage"].location == NSNotFound)
841 if (webViewMenuItems) {
842 //Add a separator item if items already exist in webViewMenuItems
843 if ([webViewMenuItems count]) {
844 // If the first item is a separator item, remove it.
845 if ([[webViewMenuItems objectAtIndex:0] isSeparatorItem]) {
846 [webViewMenuItems removeObjectAtIndex:0];
849 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
852 webViewMenuItems = [NSMutableArray array];
855 NSMenu *originalMenu = nil;
857 if (chatListObject) {
859 if ([chatListObject isIntentionallyNotAStranger]) {
860 locations = [NSArray arrayWithObjects:
861 [NSNumber numberWithInteger:Context_Contact_Manage],
862 [NSNumber numberWithInteger:Context_Contact_Action],
863 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
864 [NSNumber numberWithInteger:Context_Contact_ChatAction],
865 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
867 locations = [NSArray arrayWithObjects:
868 [NSNumber numberWithInteger:Context_Contact_Manage],
869 [NSNumber numberWithInteger:Context_Contact_Action],
870 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
871 [NSNumber numberWithInteger:Context_Contact_ChatAction],
872 [NSNumber numberWithInteger:Context_Contact_Stranger_ChatAction],
873 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
876 originalMenu = [adium.menuController contextualMenuWithLocations:locations
877 forListObject:chatListObject
879 } else if(chat.isGroupChat) {
880 originalMenu = [adium.menuController contextualMenuWithLocations:[NSArray arrayWithObjects:
881 [NSNumber numberWithInteger:Context_GroupChat_Manage],
882 [NSNumber numberWithInteger:Context_GroupChat_Action], nil]
886 [webViewMenuItems addObjectsFromArray:originalMenu.itemArray];
887 [originalMenu removeAllItems];
889 if (webViewMenuItems.count > 0 && ![[webViewMenuItems objectAtIndex:webViewMenuItems.count-1] isSeparatorItem])
890 [webViewMenuItems addObject:[NSMenuItem separatorItem]];
892 //Present an option to clear the display
893 menuItem = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Clear Display", "Clears the display window for the currently open message window")
895 action:@selector(clearView)
897 [webViewMenuItems addObject:menuItem];
900 return webViewMenuItems;
904 * @brief Add ourself to the window script object bridge when it's safe to do so
906 - (void)webView:(WebView *)sender didClearWindowObject:(WebScriptObject *)windowObject forFrame:(WebFrame *)frame
908 [[webView windowScriptObject] setValue:self forKey:@"client"];
911 //Dragging delegate ----------------------------------------------------------------------------------------------------
912 #pragma mark Dragging delegate
914 * @brief If possible, return the first NSTextView in the message view's responder chain
916 * This is used for drag and drop behavior.
918 - (NSTextView *)textView
920 id responder = [webView nextResponder];
922 //Walkin the responder chain looking for an NSTextView
924 ![responder isKindOfClass:[NSTextView class]]) {
925 responder = [responder nextResponder];
932 * @brief Dragging entered
934 - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
936 NSPasteboard *pasteboard = [sender draggingPasteboard];
938 return ([pasteboard availableTypeFromArray:draggedTypes] ?
939 NSDragOperationCopy :
940 NSDragOperationNone);
944 * @brief Dragging updated
946 - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
948 return [self draggingEntered:sender];
952 * @brief Handle a drag onto the webview
954 * If we're getting a non-image file, we can handle it immediately. Otherwise, the drag is the textView's problem.
956 - (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
958 NSPasteboard *pasteboard = [sender draggingPasteboard];
961 if ([self shouldHandleDragWithPasteboard:pasteboard]) {
963 //Not an image but it is a file - send it immediately as a file transfer
964 NSArray *files = [pasteboard propertyListForType:NSFilenamesPboardType];
966 for (path in files) {
967 AIListObject *listObject = chat.listObject;
969 [adium.fileTransferController sendFile:path toListContact:(AIListContact *)listObject];
975 NSTextView *textView = [self textView];
977 [[webView window] makeFirstResponder:textView]; //Make it first responder
978 success = [textView performDragOperation:sender];
986 * @brief Pass on the prepareForDragOperation if it's not one we're handling in this class
988 - (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
990 NSPasteboard *pasteboard = [sender draggingPasteboard];
993 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
994 NSTextView *textView = [self textView];
996 success = [textView prepareForDragOperation:sender];
1004 * @brief Pass on the concludeDragOperation if it's not one we're handling in this class
1006 - (void)concludeDragOperation:(id <NSDraggingInfo>)sender
1008 NSPasteboard *pasteboard = [sender draggingPasteboard];
1010 if (![self shouldHandleDragWithPasteboard:pasteboard]) {
1011 NSTextView *textView = [self textView];
1013 [textView concludeDragOperation:sender];
1019 * @brief Handle drags of content we recognize
1021 - (BOOL)shouldHandleDragWithPasteboard:(NSPasteboard *)pasteboard
1024 return (![pasteboard availableTypeFromArray:[NSArray arrayWithObjects:NSTIFFPboardType,NSPDFPboardType,NSPICTPboardType,nil]] &&
1025 [pasteboard availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]]);
1031 //User Icon masking --------------------------------------------------------------------------------------------------
1032 //We allow messaage styles to specify masks for user icons. This could be user to round the corners of user icons
1033 //or other related effects.
1034 #pragma mark User icon masking
1036 * @brief Update icon masks when participating list objects change
1038 * We want to observe attributesChanged: notifications for all objects which are participating in our chat.
1039 * When the list changes, remove the observers we had in place before and add observers for each object in the list
1040 * so we never observe for contacts not in the chat.
1042 - (void)participatingListObjectsChanged:(NSNotification *)notification
1044 NSArray *participatingListObjects = [chat containedObjects];
1046 [[NSNotificationCenter defaultCenter] removeObserver:self
1047 name:ListObject_AttributesChanged
1050 for (AIListObject *listObject in participatingListObjects) {
1051 //Update the mask for any user which just entered the chat
1052 if (![objectsWithUserIconsArray containsObjectIdenticalTo:listObject]) {
1053 [self updateUserIconForObject:listObject];
1056 //In the future, watch for changes on the parent object, since that's the icon we display
1057 if ([listObject isKindOfClass:[AIListContact class]]) {
1058 [[NSNotificationCenter defaultCenter] addObserver:self
1059 selector:@selector(listObjectAttributesChanged:)
1060 name:ListObject_AttributesChanged
1061 object:[(AIListContact *)listObject parentContact]];
1065 //Also observe our account
1067 [[NSNotificationCenter defaultCenter] addObserver:self
1068 selector:@selector(listObjectAttributesChanged:)
1069 name:ListObject_AttributesChanged
1070 object:chat.account];
1073 //Remove the cache for any object no longer in the chat
1074 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1075 if ((![listObject isKindOfClass:[AIMetaContact class]] || (![participatingListObjects firstObjectCommonWithArray:[(AIMetaContact *)listObject containedObjects]])) &&
1076 (![listObject isKindOfClass:[AIListContact class]] || ![participatingListObjects containsObjectIdenticalTo:(AIListContact *)listObject]) &&
1077 !(listObject == chat.account)) {
1078 [self releaseCurrentWebKitUserIconForObject:listObject];
1084 * @brief Update icon masks when source or destination changes
1086 - (void)sourceOrDestinationChanged:(NSNotification *)notification
1088 //Update the participating contacts
1089 [self participatingListObjectsChanged:nil];
1091 //And update the source account
1092 [self updateUserIconForObject:chat.account];
1094 [self updateServiceIcon];
1098 * @brief Update the icon when a list object's icon attributes change
1100 - (void)listObjectAttributesChanged:(NSNotification *)notification
1102 AIListObject *inObject = [notification object];
1103 NSSet *keys = [[notification userInfo] objectForKey:@"Keys"];
1105 if ([keys containsObject:KEY_USER_ICON]) {
1107 AIListObject *actualObject = nil;
1108 AILogWithSignature(@"%@'s icon changed", inObject);
1109 if (chat.account == inObject) {
1110 //The account is the object actually in the chat
1111 actualObject = inObject;
1114 * We are notified of a change to the metacontact's icon. Find the contact inside the chat which we will
1115 * be displaying as changed.
1118 for (AIListContact *participatingListObject in chat) {
1119 if ([participatingListObject parentContact] == inObject) {
1120 actualObject = participatingListObject;
1127 [self userIconForObjectDidChange:actualObject];
1131 AILogWithSignature(@"nil object's icon changed");
1132 //We don't know what changed, if anything, that is relevant to our chat. Update source and destination icons.
1133 [self sourceOrDestinationChanged:nil];
1138 - (void)userIconForObjectDidChange:(AIListObject *)inObject
1140 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1141 [(AIListContact *)inObject parentContact] :
1143 NSString *currentIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1144 if (currentIconPath) {
1145 NSString *objectsKnownIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1146 if (objectsKnownIconPath &&
1147 [currentIconPath isEqualToString:objectsKnownIconPath]) {
1148 //We're the first one to get to this object! We get to delete the old path and remove the reference to it
1149 [[NSFileManager defaultManager] removeItemAtPath:currentIconPath error:NULL];
1150 [iconSourceObject setValue:nil
1151 forProperty:KEY_WEBKIT_USER_ICON
1152 notify:NotifyNever];
1154 /* Some other instance beat us to the punch. The object's KEY_WEBKIT_USER_ICON is right, since it doesn't match our
1155 * internally tracked path.
1160 [self updateUserIconForObject:iconSourceObject];
1164 * @brief Remove all references to *this* chat using cached icons for an object
1166 * If this is the last chat utilizing the cached icon, it will be deleted.
1168 * @param inObject The object
1170 - (void)releaseCurrentWebKitUserIconForObject:(AIListObject *)inObject
1172 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1173 [(AIListContact *)inObject parentContact] :
1177 NSInteger chatsUsingCachedIcon = [iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON];
1178 chatsUsingCachedIcon--;
1179 [iconSourceObject setValue:[NSNumber numberWithInteger:chatsUsingCachedIcon]
1180 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1181 notify:NotifyNever];
1182 [objectsWithUserIconsArray removeObjectIdenticalTo:iconSourceObject];
1184 if ((chatsUsingCachedIcon <= 0) &&
1185 (path = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON])) {
1186 [[NSFileManager defaultManager] removeItemAtPath:path error:NULL];
1187 [iconSourceObject setValue:nil
1188 forProperty:KEY_WEBKIT_USER_ICON
1189 notify:NotifyNever];
1192 [objectIconPathDict removeObjectForKey:iconSourceObject.internalObjectID];
1196 * @brief Remove all references to *this* chat using cached icons for all involved objects
1198 - (void)releaseAllCachedIcons
1200 for (AIListObject *listObject in [[objectsWithUserIconsArray copy] autorelease]) {
1201 [self releaseCurrentWebKitUserIconForObject:listObject];
1206 * @brief Generate an updated masked user icon for the passed list object
1208 - (void)updateUserIconForObject:(AIListObject *)inObject
1210 AIListObject *iconSourceObject = ([inObject isKindOfClass:[AIListContact class]] ?
1211 [(AIListContact *)inObject parentContact] :
1214 NSString *oldWebKitUserIconPath = nil;
1215 NSString *webKitUserIconPath = nil;
1216 NSImage *webKitUserIcon;
1219 * We probably already have a userIcon waiting for us, the active display icon; use that
1220 * rather than loading one from disk.
1222 if (!(userIcon = [iconSourceObject userIcon])) {
1223 //If that's not the case, try using the UserIconPath
1224 NSString *userIconPath = [iconSourceObject valueForProperty:@"UserIconPath"];
1226 userIcon = [[[NSImage alloc] initWithContentsOfFile:userIconPath] autorelease];
1230 if ([messageStyle userIconMask]) {
1231 //Apply the mask if the style has one
1232 //XXX Using multiple styles at once, one of which has a user icon mask, would lead to odd behavior
1233 webKitUserIcon = [[[messageStyle userIconMask] copy] autorelease];
1234 [webKitUserIcon lockFocus];
1235 [userIcon drawInRect:NSMakeRect(0,0,[webKitUserIcon size].width,[webKitUserIcon size].height)
1236 fromRect:NSMakeRect(0,0,[userIcon size].width,[userIcon size].height)
1237 operation:NSCompositeSourceIn
1239 [webKitUserIcon unlockFocus];
1241 //Otherwise, just use the icon as-is
1242 webKitUserIcon = userIcon;
1245 oldWebKitUserIconPath = [objectIconPathDict objectForKey:iconSourceObject.internalObjectID];
1246 webKitUserIconPath = [iconSourceObject valueForProperty:KEY_WEBKIT_USER_ICON];
1248 if (!webKitUserIconPath) {
1249 /* If the image doesn't know a path to use, write it out and set it.
1251 * Writing the icon out is necessary for webkit to be able to use it; it also guarantees that there won't be
1252 * any animation, which is good since animation in the message view is slow and annoying.
1254 * Only write out the icon if the object doesn't already have one
1256 webKitUserIconPath = [self _webKitUserIconPathForObject:iconSourceObject];
1257 if ([[webKitUserIcon PNGRepresentation] writeToFile:webKitUserIconPath
1259 [iconSourceObject setValue:webKitUserIconPath
1260 forProperty:KEY_WEBKIT_USER_ICON
1265 //Make sure it's known that this user has been handled
1266 if (![objectsWithUserIconsArray containsObjectIdenticalTo:iconSourceObject]) {
1267 [objectsWithUserIconsArray addObject:iconSourceObject];
1269 //Keep track of this chat using the icon
1270 [iconSourceObject setValue:[NSNumber numberWithInteger:([iconSourceObject integerValueForProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON] + 1)]
1271 forProperty:KEY_WEBKIT_CHATS_USING_CACHED_ICON
1272 notify:NotifyNever];
1275 if (!webKitUserIconPath) webKitUserIconPath = @"";
1277 //Update existing images
1278 AILogWithSignature(@"Updating %@ to %@", oldWebKitUserIconPath, webKitUserIconPath);
1280 if (oldWebKitUserIconPath &&
1281 ![oldWebKitUserIconPath isEqualToString:webKitUserIconPath]) {
1282 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1283 NSUInteger imagesCount = [images length];
1285 webKitUserIconPath = [[webKitUserIconPath copy] autorelease];
1287 for (NSInteger i = 0; i < imagesCount; i++) {
1288 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1289 NSString *currentSrc = [img getAttribute:@"src"];
1290 if (currentSrc && ([currentSrc rangeOfString:oldWebKitUserIconPath].location != NSNotFound)) {
1291 [img setSrc:webKitUserIconPath];
1296 [objectIconPathDict setObject:webKitUserIconPath
1297 forKey:iconSourceObject.internalObjectID];
1301 - (void)updateServiceIcon
1303 DOMDocument *doc = [webView mainFrameDocument];
1304 //Old WebKits don't support this... if someone feels like doing it the slower way here, feel free
1305 if(![doc respondsToSelector:@selector(getElementsByClassName:)])
1307 DOMNodeList *serviceIconImages = [doc getElementsByClassName:@"serviceIcon"];
1308 NSUInteger imagesCount = [serviceIconImages length];
1310 NSString *serviceIconPath = [AIServiceIcons pathForServiceIconForServiceID:chat.account.service.serviceID
1311 type:AIServiceIconLarge];
1313 for (NSInteger i = 0; i < imagesCount; i++) {
1314 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[serviceIconImages item:i];
1315 [img setSrc:serviceIconPath];
1319 - (void)customEmoticonUpdated:(NSNotification *)inNotification
1321 DOMNodeList *images = [[webView mainFrameDocument] getElementsByTagName:@"img"];
1322 NSUInteger imagesCount = [images length];
1324 if (imagesCount > 0) {
1325 AIEmoticon *emoticon = [[inNotification userInfo] objectForKey:@"AIEmoticon"];
1326 NSString *textEquivalent = [[emoticon textEquivalents] objectAtIndex:0];
1327 NSString *path = [emoticon path];
1328 NSSize emoticonSize = [[emoticon image] size];
1329 BOOL updatedImage = NO;
1330 path = [[NSURL fileURLWithPath:path] absoluteString];
1331 for (NSInteger i = 0; i < imagesCount; i++) {
1332 DOMHTMLImageElement *img = (DOMHTMLImageElement *)[images item:i];
1334 if ([[img className] isEqualToString:@"emoticon"] &&
1335 [[img getAttribute:@"alt"] isEqualToString:textEquivalent]) {
1337 [img setWidth:emoticonSize.width];
1338 [img setHeight:emoticonSize.height];
1342 NSNumber *shouldScroll = [[webView windowScriptObject] callWebScriptMethod:@"nearBottom"
1344 if (!shouldScroll) shouldScroll = [NSNumber numberWithBool:NO];
1346 if (updatedImage) [[webView windowScriptObject] callWebScriptMethod:@"alignChat"
1347 withArguments:[NSArray arrayWithObject:shouldScroll]];
1352 * @brief Returns the path the background image given a unique ID
1354 - (NSString *)_webKitBackgroundImagePathForUniqueID:(NSInteger)uniqueID
1356 NSString *filename = [NSString stringWithFormat:@"%@-WebkitBGImage-%ld.png", TEMPORARY_FILE_PREFIX, (long)uniqueID];
1357 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1361 * @brief Returns the path to the list object's masked user icon
1363 - (NSString *)_webKitUserIconPathForObject:(AIListObject *)inObject
1365 NSString *filename = [NSString stringWithFormat:@"%@-%@%@.png", TEMPORARY_FILE_PREFIX, inObject.internalObjectID, [NSString randomStringOfLength:5]];
1366 return [[adium cachesPath] stringByAppendingPathComponent:filename];
1369 #pragma mark File Transfer
1371 - (void)handleAction:(NSString *)action forFileTransfer:(NSString *)fileTransferID
1373 ESFileTransfer *fileTransfer = [ESFileTransfer existingFileTransferWithID:fileTransferID];
1374 ESFileTransferRequestPromptController *tc = [fileTransfer fileTransferRequestPromptController];
1377 AIFileTransferAction a;
1378 if ([action isEqualToString:@"SaveAs"])
1380 else if ([action isEqualToString:@"Cancel"])
1385 [tc handleFileTransferAction:a];
1389 #pragma mark Topic editing
1390 - (void)editingDidComplete:(DOMRange *)range
1392 DOMNode *node = range.startContainer;
1393 DOMHTMLElement *topicEdit = (DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"topicEdit"];
1395 NSString *topicChange = nil;
1397 if (node == topicEdit || node.parentNode == topicEdit) {
1398 topicChange = [[AIHTMLDecoder decodeHTML:[topicEdit innerHTML]] string];
1400 NSTextView *textView = [self textView];
1402 [[webView window] makeFirstResponder:textView]; //Make it first responder
1405 // Update the topic div in case the user doesn't have permission to change it.
1408 // Tell the chat to set the topic.
1409 [chat setTopic:topicChange];
1413 #pragma mark Marked Scroller
1414 - (JVMarkedScroller *)markedScroller
1416 WebFrame *contentFrame = [webView.mainFrame findFrameNamed:@"_current"];
1417 NSScrollView *scrollView = contentFrame.frameView.documentView.enclosingScrollView;
1419 JVMarkedScroller *scroller = (JVMarkedScroller *)scrollView.verticalScroller;
1420 return ([scroller isKindOfClass:[JVMarkedScroller class]]) ? scroller : nil;
1423 - (void)setupMarkedScroller
1425 WebFrame *contentFrame = [[webView mainFrame] findFrameNamed:@"_current"];
1426 NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
1428 [scrollView setHasHorizontalScroller:NO];
1430 JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
1431 if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
1432 NSRect scrollerFrame = [[scrollView verticalScroller] frame];
1433 NSScroller *oldScroller = scroller;
1434 scroller = [[[JVMarkedScroller alloc] initWithFrame:scrollerFrame] autorelease];
1435 [scroller setFloatValue:oldScroller.floatValue];
1436 [scroller setKnobProportion:oldScroller.knobProportion];
1437 [scrollView setVerticalScroller:scroller];
1441 - (NSNumber *)currentOffsetHeight
1443 // We use the body's height to determine our mark location.
1444 return [(DOMElement *)[(DOMHTMLDocument *)webView.mainFrameDocument body] valueForKey:@"scrollHeight"];
1447 - (void)markCurrentLocation
1449 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue]];
1452 #define PREF_KEY_FOCUS_LINE @"Draw Focus Lines"
1454 - (void)markForFocusChange
1456 JVMarkedScroller *scroller = self.markedScroller;
1458 // We use the current Chat element's height to determine our mark location.
1459 [scroller removeMarkWithIdentifier:@"focus"];
1460 [scroller addMarkAt:[self.currentOffsetHeight integerValue] withIdentifier:@"focus" withColor:[NSColor redColor]];
1462 nextMessageFocus = YES;
1464 DOMNodeList *nodeList = [webView.mainFrameDocument querySelectorAll:@".focus"];
1465 DOMHTMLElement *node = nil; NSMutableArray *classes = nil;
1466 for (NSUInteger i = 0; i < nodeList.length; i++)
1468 node = (DOMHTMLElement *)[nodeList item:i];
1469 classes = [[node.className componentsSeparatedByString:@" "] mutableCopy];
1471 [classes removeObject:@"focus"];
1473 node.className = [classes componentsJoinedByString:@" "];
1478 nextMessageFocus = YES;
1483 [self.markedScroller addMarkAt:[self.currentOffsetHeight integerValue] withColor:[NSColor greenColor]];
1486 - (void)jumpToPreviousMark
1488 [self.markedScroller jumpToPreviousMark:nil];
1491 - (BOOL)previousMarkExists
1493 return [self.markedScroller previousMarkExists];
1496 - (void)jumpToNextMark
1498 [self.markedScroller jumpToNextMark:nil];
1501 - (BOOL)nextMarkExists
1503 return [self.markedScroller nextMarkExists];
1506 - (void)jumpToFocusMark
1508 [self.markedScroller jumpToFocusMark:nil];
1511 - (BOOL)focusMarkExists
1513 return [self.markedScroller focusMarkExists];
1516 #pragma mark JS Bridging
1517 /*See http://developer.apple.com/documentation/AppleApplications/Conceptual/SafariJSProgTopics/Tasks/ObjCFromJavaScript.html#//apple_ref/doc/uid/30001215 for more information.
1520 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
1523 sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:)) ||
1524 sel_isEqual(aSelector, @selector(debugLog:)) ||
1525 sel_isEqual(aSelector, @selector(zoomImage:))
1533 * This method returns the name to be used in the scripting environment for the selector specified by aSelector.
1534 * It is your responsibility to ensure that the returned name is unique to the script invoking this method.
1535 * If this method returns nil or you do not implement it, the default name for the selector will be constructed as follows:
1537 * Any colon (“:”)in the Objective-C selector is replaced by an underscore (“_”).
1538 * Any underscore in the Objective-C selector is prefixed with a dollar sign (“$”).
1539 * Any dollar sign in the Objective-C selector is prefixed with another dollar sign.
1541 + (NSString *)webScriptNameForSelector:(SEL)aSelector
1543 if (sel_isEqual(aSelector, @selector(handleAction:forFileTransfer:))) return @"handleFileTransfer";
1544 if (sel_isEqual(aSelector, @selector(debugLog:))) return @"debugLog";
1545 if (sel_isEqual(aSelector, @selector(zoomImage:))) return @"zoomImage";
1549 - (BOOL)zoomImage:(DOMHTMLImageElement *)img
1551 NSMutableString *className = [[[img className] mutableCopy] autorelease];
1552 if ([className rangeOfString:@"fullSizeImage"].location != NSNotFound)
1553 [className replaceOccurrencesOfString:@"fullSizeImage"
1554 withString:@"scaledToFitImage"
1555 options:NSLiteralSearch
1556 range:NSMakeRange(0, [className length])];
1557 else if ([className rangeOfString:@"scaledToFitImage"].location != NSNotFound)
1558 [className replaceOccurrencesOfString:@"scaledToFitImage"
1559 withString:@"fullSizeImage"
1560 options:NSLiteralSearch
1561 range:NSMakeRange(0, [className length])];
1565 [img setClassName:className];
1566 [[webView windowScriptObject] callWebScriptMethod:@"alignChat" withArguments:[NSArray arrayWithObject:[NSNumber numberWithBool:YES]]];
1571 - (void)debugLog:(NSString *)message { NSLog(@"%@", message); }
1573 //gets the source of the html page, for debugging
1574 - (NSString *)webviewSource
1576 return [(DOMHTMLHtmlElement *)[[[webView mainFrameDocument] getElementsByTagName:@"html"] item:0] outerHTML];
1580 * @brief Set the HTML content for the "Chat" area.
1582 - (void)setChatContentSource:(NSString *)source
1584 if (!webViewIsReady) {
1585 // If the webview isn't ready yet, wait a very short amount of time before trying again
1586 [self performSelector:@selector(setChatContentSource:)
1590 // Add the old "Chat" element to the window.
1591 [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] setOuterHTML:source];
1593 NSString *scrollToBottomScript;
1594 if ((scrollToBottomScript = [messageStyle scriptForScrollingAfterAddingMultipleContentObjects])) {
1595 [webView stringByEvaluatingJavaScriptFromString:scrollToBottomScript];
1601 * @brief Get the HTML content for the "Chat" area.
1603 - (NSString *)chatContentSource
1605 return [(DOMHTMLElement *)[[webView mainFrameDocument] getElementById:@"Chat"] outerHTML];
1609 * @brief The unique name for this style of "content source"
1611 - (NSString *)contentSourceName
1613 return [[[messageStyle bundle] bundlePath] lastPathComponent];