root/trunk/Views/JVStyleView.m

Revision 3765, 31.5 kB (checked in by timothy, 1 year ago)

Add some more checks to prevent the blank chat room bug.

Line 
1 #import "JVStyleView.h"
2 #import "JVMarkedScroller.h"
3 #import "JVChatTranscript.h"
4 #import "JVChatMessage.h"
5 #import "JVStyle.h"
6 #import "JVEmoticonSet.h"
7
8 #import <objc/objc-runtime.h>
9
10 NSString *JVStyleViewDidClearNotification = @"JVStyleViewDidClearNotification";
11 NSString *JVStyleViewDidChangeStylesNotification = @"JVStyleViewDidChangeStylesNotification";
12
13 @interface WebCoreCache
14 + (void) empty;
15 @end
16
17 #pragma mark -
18
19 @interface WebCache
20 + (void) empty;
21 @end
22
23 #pragma mark -
24
25 @interface WebView (WebViewLeopard)
26 - (void) setDrawsBackground:(BOOL) draws; // supported in 10.3.9/Tiger
27 - (BOOL) drawsBackground; // supported in 10.3.9/Tiger
28 - (void) setBackgroundColor:(NSColor *) color; // new in Safari 3/Leopard
29 - (void) setProhibitsMainFrameScrolling:(BOOL) prohibit; // new in Safari 3/Leopard
30 - (WebFrame *) selectedFrame;
31 @end
32
33 #pragma mark -
34
35 @interface WebView (WebViewPrivate)
36 - (WebFrame *) _frameForCurrentSelection;
37 @end
38
39 #pragma mark -
40
41 @interface DOMHTMLElement (DOMHTMLElementLeopard)
42 - (int) offsetTop;
43 - (int) offsetHeight;
44 - (int) scrollHeight;
45 @end
46
47 #pragma mark -
48
49 @interface NSScrollView (NSScrollViewWebKitPrivate)
50 - (void) setAllowsHorizontalScrolling:(BOOL) allow;
51 @end
52
53 #pragma mark -
54
55 @interface DOMHTMLElement (DOMHTMLElementExtras)
56 - (unsigned) childElementLength;
57 - (DOMNode *) childElementAtIndex:(unsigned) index;
58 - (int) integerForDOMProperty:(NSString *) property;
59 - (void) setInteger:(int) value forDOMProperty:(NSString *) property;
60 @end
61
62 #pragma mark -
63
64 @implementation DOMHTMLElement (DOMHTMLElementExtras)
65 - (unsigned) childElementLength {
66         unsigned length = 0;
67
68         DOMNode *node = [self firstChild];
69         while (node) {
70                 if( [node nodeType] == DOM_ELEMENT_NODE ) ++length;
71                 node = [node nextSibling];
72         }
73
74         return length;
75 }
76
77 - (DOMNode *) childElementAtIndex:(unsigned) index {
78         unsigned count = 0;
79         DOMNode *node = [self firstChild];
80         while (node) {
81                 if( [node nodeType] == DOM_ELEMENT_NODE && count++ == index )
82                         return node;
83                 node = [node nextSibling];
84         }
85
86         return nil;
87 }
88
89 - (int) integerForDOMProperty:(NSString *) property {
90         SEL selector = NSSelectorFromString( property );
91         if( [self respondsToSelector:selector] )
92                 return ((int(*)(id, SEL))objc_msgSend)(self, selector);
93         NSNumber *value = [self valueForKey:property];
94         if( [value isKindOfClass:[NSNumber class]] )
95                 return [value intValue];
96         return 0;
97 }
98
99 - (void) setInteger:(int) value forDOMProperty:(NSString *) property {
100         SEL selector = NSSelectorFromString( [@"set" stringByAppendingString:[property capitalizedString]] );
101         if( [self respondsToSelector:selector] ) {
102                 ((void(*)(id, SEL, int))objc_msgSend)(self, selector, value);
103         } else {
104                 NSNumber *number = [NSNumber numberWithInt:value];
105                 [self setValue:number forKey:property];
106         }
107 }
108 @end
109
110 #pragma mark -
111
112 @interface JVStyleView (JVStyleViewPrivate)
113 - (void) _resetDisplay;
114 - (void) _switchStyle;
115 - (void) _appendMessage:(NSString *) message;
116 - (void) _prependMessages:(NSString *) messages;
117 - (void) _styleError;
118 - (NSString *) _contentHTMLWithBody:(NSString *) html;
119 - (NSURL *) _baseURL;
120 - (unsigned) _visibleMessageCount;
121 - (int) _locationOfMessage:(JVChatMessage *) message;
122 - (int) _locationOfElementAtIndex:(unsigned) index;
123 - (void) _setupMarkedScroller;
124 @end
125
126 #pragma mark -
127
128 @implementation JVStyleView
129 - (id) initWithCoder:(NSCoder *) coder {
130         if( ( self = [super initWithCoder:coder] ) ) {
131                 _switchingStyles = NO;
132                 _forwarding = NO;
133                 _ready = NO;
134                 _contentFrameReady = NO;
135                 _requiresFullMessage = YES;
136                 _scrollbackLimit = 600;
137                 _transcript = nil;
138                 _style = nil;
139                 _styleVariant = nil;
140                 _styleParameters = [[NSMutableDictionary dictionary] retain];
141                 _emoticons = nil;
142                 _domDocument = nil;
143                 nextTextView = nil;
144         }
145
146         return self;
147 }
148
149 - (void) awakeFromNib {
150         [self setFrameLoadDelegate:self];
151         if( [self respondsToSelector:@selector(setProhibitsMainFrameScrolling:)] )
152                 [self setProhibitsMainFrameScrolling:YES];
153         [self performSelector:@selector( _reallyAwakeFromNib ) withObject:nil afterDelay:0.];
154 }
155
156 - (void) dealloc {
157         [[NSNotificationCenter defaultCenter] removeObserver:self name:JVStyleVariantChangedNotification object:nil];
158
159         [nextTextView release];
160         [_transcript release];
161         [_style release];
162         [_styleVariant release];
163         [_styleParameters release];
164         [_emoticons release];
165         [_mainDocument release];
166         [_domDocument release];
167         [_body release];
168         [_bodyTemplate release];
169
170         nextTextView = nil;
171         _transcript = nil;
172         _style = nil;
173         _styleVariant = nil;
174         _styleParameters = nil;
175         _emoticons = nil;
176         _mainDocument = nil;
177         _domDocument = nil;
178         _body = nil;
179         _bodyTemplate = nil;
180
181         [super dealloc];
182 }
183
184 #pragma mark -
185
186 - (void) forwardSelector:(SEL) selector withObject:(id) object {
187         if( [self nextTextView] ) {
188                 [[self window] makeFirstResponder:[self nextTextView]];
189                 [[self nextTextView] tryToPerform:selector with:object];
190         }
191 }
192
193 #pragma mark -
194
195 - (void) keyDown:(NSEvent *) event {
196         if( _forwarding ) return;
197         _forwarding = YES;
198         [self forwardSelector:@selector( keyDown: ) withObject:event];
199         _forwarding = NO;
200 }
201
202 - (void) pasteAsPlainText:(id) sender {
203         if( _forwarding ) return;
204         _forwarding = YES;
205         [self forwardSelector:@selector( pasteAsPlainText: ) withObject:sender];
206         _forwarding = NO;
207 }
208
209 - (void) pasteAsRichText:(id) sender {
210         if( _forwarding ) return;
211         _forwarding = YES;
212         [self forwardSelector:@selector( pasteAsRichText: ) withObject:sender];
213         _forwarding = NO;
214 }
215
216 #pragma mark -
217
218 - (NSTextView *) nextTextView {
219         return nextTextView;
220 }
221
222 - (void) setNextTextView:(NSTextView *) textView {
223         [nextTextView autorelease];
224         nextTextView = [textView retain];
225 }
226
227 #pragma mark -
228
229 - (void) setTranscript:(JVChatTranscript *) transcript {
230         [_transcript autorelease];
231         _transcript = [transcript retain];
232 }
233
234 - (JVChatTranscript *) transcript {
235         return _transcript;
236 }
237
238 #pragma mark -
239
240 - (void) setStyle:(JVStyle *) style {
241         [self setStyle:style withVariant:[style defaultVariantName]];
242 }
243
244 - (void) setStyle:(JVStyle *) style withVariant:(NSString *) variant {
245         if( [style isEqualTo:[self style]] ) {
246                 [self setStyleVariant:variant];
247                 return;
248         }
249
250         [_style autorelease];
251         _style = [style retain];
252
253         [_styleVariant autorelease];
254         _styleVariant = [variant copyWithZone:[self zone]];
255
256         // add single-quotes so that these are not interpreted as XPath expressions 
257         [_styleParameters setObject:@"'/tmp/'" forKey:@"buddyIconDirectory"]; 
258         [_styleParameters setObject:@"'.tif'" forKey:@"buddyIconExtension"];
259
260         NSString *timeFormatParameter = [NSString stringWithFormat:@"'%@'", [[NSUserDefaults standardUserDefaults] stringForKey:NSTimeFormatString]];
261         [_styleParameters setObject:timeFormatParameter forKey:@"timeFormat"];
262
263         [[NSNotificationCenter defaultCenter] removeObserver:self name:JVStyleVariantChangedNotification object:nil];
264         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _styleVariantChanged: ) name:JVStyleVariantChangedNotification object:style];
265
266         _switchingStyles = YES;
267         _requiresFullMessage = YES;
268
269         if( ! _ready ) return;
270
271         [self _resetDisplay];
272 }
273
274 - (JVStyle *) style {
275         return _style;
276 }
277
278 #pragma mark -
279
280 - (void) setStyleVariant:(NSString *) variant {
281         [_styleVariant autorelease];
282         _styleVariant = [variant copyWithZone:[self zone]];
283
284         if( _contentFrameReady ) {
285                 if( NSClassFromString( @"WebCoreCache" ) )
286                         [NSClassFromString( @"WebCoreCache" ) empty];
287                 else if( NSClassFromString( @"WebCache" ) )
288                         [NSClassFromString( @"WebCache" ) empty];
289
290                 NSString *styleSheetLocation = [[[self style] variantStyleSheetLocationWithName:_styleVariant] absoluteString];
291                 DOMHTMLLinkElement *element = (DOMHTMLLinkElement *)[_domDocument getElementById:@"variantStyle"];
292                 if( ! styleSheetLocation ) [element setHref:@""];
293                 else [element setHref:styleSheetLocation];
294
295                 [self performSelector:@selector( _checkForTransparantStyle ) withObject:nil afterDelay:0.];
296         } else {
297                 [self performSelector:_cmd withObject:variant afterDelay:0.5];
298         }
299 }
300
301 - (NSString *) styleVariant {
302         return _styleVariant;
303 }
304
305 #pragma mark -
306
307 - (void) setBodyTemplate:(NSString *) bodyTemplate {
308         [_bodyTemplate autorelease];
309         _bodyTemplate = [bodyTemplate retain];
310 }
311
312 - (NSString *) bodyTemplate {
313         return _bodyTemplate;
314 }
315
316 #pragma mark -
317
318 - (void) setStyleParameters:(NSDictionary *) parameters {
319         id old = _styleParameters;
320         _styleParameters = [parameters mutableCopyWithZone:[self zone]];
321         [old release];
322 }
323
324 - (NSDictionary *) styleParameters {
325         return [NSDictionary dictionaryWithDictionary:_styleParameters];
326 }
327
328 #pragma mark -
329
330 - (void) setEmoticons:(JVEmoticonSet *) emoticons {
331         [_emoticons autorelease];
332         _emoticons = [emoticons retain];
333
334         if( _contentFrameReady ) {
335                 if( NSClassFromString( @"WebCoreCache" ) )
336                         [NSClassFromString( @"WebCoreCache" ) empty];
337                 else if( NSClassFromString( @"WebCache" ) )
338                         [NSClassFromString( @"WebCache" ) empty];
339
340                 NSString *styleSheetLocation = [[[self emoticons] styleSheetLocation] absoluteString];
341                 DOMHTMLLinkElement *element = (DOMHTMLLinkElement *)[_domDocument getElementById:@"emoticonStyle"];
342                 if( ! styleSheetLocation ) [element setHref:@""];
343                 else [element setHref:styleSheetLocation];
344         } else {
345                 [self performSelector:_cmd withObject:emoticons afterDelay:0.5];
346         }
347 }
348
349 - (JVEmoticonSet *) emoticons {
350         return _emoticons;
351 }
352
353 #pragma mark -
354
355 - (void) setScrollbackLimit:(unsigned long) limit {
356         _scrollbackLimit = limit;
357 }
358
359 - (unsigned long) scrollbackLimit {
360         return _scrollbackLimit;
361 }
362
363 #pragma mark -
364
365 - (void) reloadCurrentStyle {
366         _switchingStyles = YES;
367         _requiresFullMessage = YES;
368         _rememberScrollPosition = YES;
369
370         if( NSClassFromString( @"WebCoreCache" ) )
371                 [NSClassFromString( @"WebCoreCache" ) empty];
372         else if( NSClassFromString( @"WebCache" ) )
373                 [NSClassFromString( @"WebCache" ) empty];
374
375         [self _resetDisplay];
376 }
377
378 - (void) clear {
379         _switchingStyles = NO;
380         _requiresFullMessage = YES;
381         [self _resetDisplay];
382 }
383
384 - (void) mark {
385         if( _contentFrameReady ) {
386                 DOMHTMLElement *elt = (DOMHTMLElement *)[_domDocument getElementById:@"mark"];
387                 if( elt ) [[elt parentNode] removeChild:elt];
388                 elt = (DOMHTMLElement *)[_domDocument createElement:@"hr"];
389                 [elt setAttribute:@"id" :@"mark"];
390                 [_body appendChild:elt];
391                 [self scrollToBottom];
392
393                 int location = [elt integerForDOMProperty:@"offsetTop"];
394
395                 JVMarkedScroller *scroller = [self verticalMarkedScroller];
396                 [scroller removeMarkWithIdentifier:@"mark"];
397                 [scroller addMarkAt:location withIdentifier:@"mark" withColor:[NSColor redColor]];
398
399                 _requiresFullMessage = YES;
400         } else {
401                 [self performSelector:_cmd withObject:nil afterDelay:0.5];
402         }
403 }
404
405 #pragma mark -
406
407 - (void) addBanner:(NSString *) name {
408         if( ! _mainFrameReady ) {
409                 [self performSelector:_cmd withObject:name afterDelay:0.];
410                 return;
411         }
412
413         NSString *shell = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:name ofType:@"html"] encoding:NSUTF8StringEncoding error:NULL];
414
415         DOMHTMLElement *element = (DOMHTMLElement *)[_mainDocument createElement:@"div"];
416         [element setClassName:@"banner"];
417         if( [shell length] ) [element setInnerHTML:shell];
418
419         [[_mainDocument body] insertBefore:element :[[_mainDocument body] firstChild]];
420 }
421
422 #pragma mark -
423
424 - (BOOL) appendChatMessage:(JVChatMessage *) message {
425         if( ! _contentFrameReady ) return YES; // don't schedule this to fire later since the transcript will be processed
426
427         if( _requiresFullMessage ) {
428                 DOMHTMLElement *replaceElement = (DOMHTMLElement *)[_domDocument getElementById:@"consecutiveInsert"];
429                 if( replaceElement ) _requiresFullMessage = NO; // a full message was assumed, but we can do a consecutive one
430         }
431
432         unsigned consecutiveOffset = [message consecutiveOffset];
433         NSString *result = nil;
434
435         if( _requiresFullMessage && consecutiveOffset > 0 ) {
436                 NSArray *elements = [[NSArray allocWithZone:nil] initWithObjects:message, nil];
437                 result = [[self style] transformChatTranscriptElements:elements withParameters:_styleParameters];
438                 [elements release];
439         } else {
440                 if( ! _requiresFullMessage && consecutiveOffset > 0 )
441                         [_styleParameters setObject:@"'yes'" forKey:@"consecutiveMessage"];
442                 result = [[self style] transformChatMessage:message withParameters:_styleParameters];
443                 [_styleParameters removeObjectForKey:@"consecutiveMessage"];
444         }
445
446         if( [result length] ) {
447                 [self _appendMessage:result];
448                 _requiresFullMessage = NO;
449         }
450
451         return ( [result length] ? YES : NO );
452 }
453
454 - (BOOL) appendChatTranscriptElement:(id <JVChatTranscriptElement>) element {
455         if( ! _contentFrameReady ) return YES; // don't schedule this to fire later since the transcript will be processed
456
457         NSString *result = [[self style] transformChatTranscriptElement:element withParameters:_styleParameters];
458
459         if( [result length] ) [self _appendMessage:result];
460
461         return ( [result length] ? YES : NO );
462 }
463
464 #pragma mark -
465
466 - (void) highlightMessage:(JVChatMessage *) message {
467 /*      DOMHTMLElement *element = (DOMHTMLElement *)[_domDocument getElementById:[message messageIdentifier]];
468         NSString *class = [element className];
469         if( [[element className] rangeOfString:@"searchHighlight"].location != NSNotFound ) return;
470         if( [class length] ) [element setClassName:[class stringByAppendingString:@" searchHighlight"]];
471         else [element setClassName:@"searchHighlight"];
472 */}
473
474 - (void) clearHighlightForMessage:(JVChatMessage *) message {
475 /*      DOMHTMLElement *element = (DOMHTMLElement *)[_domDocument getElementById:[message messageIdentifier]];
476         NSMutableString *class = [[[element className] mutableCopy] autorelease];
477         [class replaceOccurrencesOfString:@"searchHighlight" withString:@"" options:NSLiteralSearch range:NSMakeRange( 0, [class length] )];
478         [element setClassName:class];
479 */}
480
481 - (void) clearAllMessageHighlights {
482 //      [[self windowScriptObject] callWebScriptMethod:@"resetHighlightMessage" withArguments:[NSArray arrayWithObject:[NSNull null]]];
483 }
484
485 #pragma mark -
486
487 - (void) highlightString:(NSString *) string inMessage:(JVChatMessage *) message {
488         [[self windowScriptObject] callWebScriptMethod:@"searchHighlight" withArguments:[NSArray arrayWithObjects:[message messageIdentifier], string, nil]];
489 }
490
491 - (void) clearStringHighlightsForMessage:(JVChatMessage *) message {
492         [[self windowScriptObject] callWebScriptMethod:@"resetSearchHighlight" withArguments:[NSArray arrayWithObject:[message messageIdentifier]]];
493 }
494
495 - (void) clearAllStringHighlights {
496         [[self windowScriptObject] callWebScriptMethod:@"resetSearchHighlight" withArguments:[NSArray arrayWithObject:[NSNull null]]];
497 }
498
499 #pragma mark -
500
501 - (void) markScrollbarForMessage:(JVChatMessage *) message {
502         if( _switchingStyles || ! _contentFrameReady ) {
503                 [self performSelector:_cmd withObject:message afterDelay:0.5];
504                 return;
505         }
506
507         int loc = [self _locationOfMessage:message];
508         if( loc != NSNotFound ) [[self verticalMarkedScroller] addMarkAt:loc];
509 }
510
511 - (void) markScrollbarForMessage:(JVChatMessage *) message usingMarkIdentifier:(NSString *) identifier andColor:(NSColor *) color {
512         if( _switchingStyles || ! _contentFrameReady ) return; // can't queue, too many args. NSInvocation?
513
514         int loc = [self _locationOfMessage:message];
515         if( loc != NSNotFound ) [[self verticalMarkedScroller] addMarkAt:loc withIdentifier:identifier withColor:color];
516 }
517
518 - (void) markScrollbarForMessages:(NSArray *) messages {
519         if( _switchingStyles || ! _contentFrameReady ) {
520                 [self performSelector:_cmd withObject:messages afterDelay:0.5];
521                 return;
522         }
523
524         JVMarkedScroller *scroller = [self verticalMarkedScroller];
525         NSEnumerator *enumerator = [messages objectEnumerator];
526         JVChatMessage *message = nil;
527
528         while( ( message = [enumerator nextObject] ) ) {
529                 int loc = [self _locationOfMessage:message];
530                 if( loc != NSNotFound ) [scroller addMarkAt:loc];
531         }
532 }
533
534 #pragma mark -
535
536 - (void) clearScrollbarMarks {
537         JVMarkedScroller *scroller = [self verticalMarkedScroller];
538         [scroller removeAllMarks];
539         [scroller removeAllShadedAreas];
540 }
541
542 - (void) clearScrollbarMarksWithIdentifier:(NSString *) identifier {
543         [[self verticalMarkedScroller] removeMarkWithIdentifier:identifier];
544 }
545
546 #pragma mark -
547
548 - (void) webView:(WebView *) sender didFinishLoadForFrame:(WebFrame *) frame {
549         if( frame == [self mainFrame] ) {
550                 _mainFrameReady = YES;
551
552                 [_mainDocument autorelease];
553                 _mainDocument = (DOMHTMLDocument *)[[frame DOMDocument] retain];
554
555                 WebFrame *contentFrame = [[self mainFrame] findFrameNamed:@"content"];
556                 [contentFrame loadHTMLString:[self _contentHTMLWithBody:@""] baseURL:[self _baseURL]];
557         } else if( _mainFrameReady ) {
558                 if( [[[[[frame dataSource] response] URL] absoluteString] isEqualToString:@"about:blank"] ) {
559                         // this was a false content frame load, try again
560                         [frame loadHTMLString:[self _contentHTMLWithBody:@""] baseURL:[self _baseURL]];
561                         return;
562                 }
563
564                 [self performSelector:@selector( _contentFrameIsReady ) withObject:nil afterDelay:0.];
565         }
566 }
567
568 - (IBAction) copySelectionToFindPboard:(id) sender {
569         WebFrame *frame = nil;
570         if( [self respondsToSelector:@selector( selectedFrame )] )
571                 frame = [self selectedFrame];
572         else if( [self respondsToSelector:@selector( _frameForCurrentSelection )] )
573                 frame = [self _frameForCurrentSelection];
574
575         if( ! frame ) return;
576
577         NSString *selectedString = [(id <WebDocumentText>)[[frame frameView] documentView] selectedString];
578
579         if( [selectedString length] ) {
580                 NSPasteboard *pboard = [NSPasteboard pasteboardWithName:NSFindPboard];
581                 [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil];
582                 [pboard setString:selectedString forType:NSStringPboardType];
583         }
584 }
585
586 - (void) drawRect:(NSRect) rect {
587         if( ! [self drawsBackground] ) {
588                 [[NSColor clearColor] set];
589                 NSRectFill( rect ); // allows poking holes in the window with rgba background colors
590         }
591
592         [super drawRect:rect];
593 }
594
595 #pragma mark -
596 #pragma mark Highlight/Message Jumping
597
598 - (JVMarkedScroller *) verticalMarkedScroller {
599         WebFrame *contentFrame = [[self mainFrame] findFrameNamed:@"content"];
600         NSScrollView *scrollView = [[[contentFrame frameView] documentView] enclosingScrollView];
601         JVMarkedScroller *scroller = (JVMarkedScroller *)[scrollView verticalScroller];
602         if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] ) {
603                 [self _setupMarkedScroller];
604                 scroller = (JVMarkedScroller *)[scrollView verticalScroller];
605                 if( scroller && ! [scroller isMemberOfClass:[JVMarkedScroller class]] )
606                         return nil; // not sure, but somthing is wrong
607         }
608
609         return scroller;
610 }
611
612 - (IBAction) jumpToMark:(id) sender {
613     [[self verticalMarkedScroller] jumpToMarkWithIdentifier:@"mark"];
614 }
615
616 - (IBAction) jumpToPreviousHighlight:(id) sender {
617     [[self verticalMarkedScroller] jumpToPreviousMark:sender];
618 }
619
620 - (IBAction) jumpToNextHighlight:(id) sender {
621     [[self verticalMarkedScroller] jumpToNextMark:sender];
622 }
623
624 - (void) jumpToMessage:(JVChatMessage *) message {
625         unsigned loc = [self _locationOfMessage:message];
626         if( loc != NSNotFound ) {
627                 JVMarkedScroller *scroller = [self verticalMarkedScroller];
628                 int shift = [scroller shiftAmountToCenterAlign];
629                 [scroller setLocationOfCurrentMark:loc];
630
631                 [[_domDocument body] setInteger:( loc - shift ) forDOMProperty:@"scrollTop"];
632         }
633 }
634
635 - (void) scrollToBottom {
636         if( ! _contentFrameReady ) {
637                 [self performSelector:_cmd withObject:nil afterDelay:0.5];
638                 return;
639         }
640
641         DOMHTMLElement *body = [_domDocument body];
642         [body setInteger:[body integerForDOMProperty:@"scrollHeight"] forDOMProperty:@"scrollTop"];
643 }
644
645 - (BOOL) scrolledNearBottom {
646         WebFrame *contentFrame = [[self mainFrame] findFrameNamed:@"content"];
647         DOMHTMLElement *contentFrameElement = [contentFrame frameElement];
648         int frameHeight = [contentFrameElement integerForDOMProperty:@"offsetHeight"];
649
650         DOMHTMLElement *body = [_domDocument body];
651         int scrollHeight = [body integerForDOMProperty:@"scrollHeight"];
652         int scrollTop = [body integerForDOMProperty:@"scrollTop"];
653
654         // check if we are near the bottom 15 pixels of the chat area
655         return ( ( frameHeight + scrollTop ) >= ( scrollHeight - 15 ) );
656 }
657 @end
658
659 #pragma mark -
660
661 @implementation JVStyleView (JVStyleViewPrivate)
662 - (void) _checkForTransparantStyle {
663         DOMCSSStyleDeclaration *style = [self computedStyleForElement:_body pseudoElement:nil];
664         DOMCSSValue *value = [style getPropertyCSSValue:@"background-color"];
665         BOOL transparent = ( value && [[value cssText] rangeOfString:@"rgba"].location != NSNotFound );
666
667         if( [self respondsToSelector:@selector(setBackgroundColor:)] ) {
668                 if( transparent ) [self setBackgroundColor:[NSColor clearColor]]; // allows rgba backgrounds to see through to the Desktop
669                 else [self setBackgroundColor:[NSColor whiteColor]];
670         } else {
671                 if( transparent ) [self setDrawsBackground:NO]; // allows rgba backgrounds to see through to the Desktop
672                 else [self setDrawsBackground:YES];
673         }
674 }
675
676 - (void) _contentFrameIsReady {
677         WebFrame *contentFrame = [[self mainFrame] findFrameNamed:@"content"];
678
679         id old = _domDocument;
680         _domDocument = (DOMHTMLDocument *)[[contentFrame DOMDocument] retain];
681         [old release];
682
683         old = _body;
684         _body = (DOMHTMLElement *)[[_domDocument getElementById:@"contents"] retain];
685         if( ! _body ) _body = (DOMHTMLElement *)[[_domDocument body] retain];
686         [old release];
687
688         if( ! _body ) {
689                 // try again soon, the DOM is not ready yet
690                 [self performSelector:@selector( _contentFrameIsReady ) withObject:nil afterDelay:0.5];
691                 return;
692         }
693
694         [self _checkForTransparantStyle];
695
696         [self setPreferencesIdentifier:[[self style] identifier]];
697
698         [self clearScrollbarMarks];
699
700         if( [[self window] isFlushWindowDisabled] )
701                 [[self window] enableFlushWindow];
702
703         _contentFrameReady = YES;
704         [[NSNotificationCenter defaultCenter] postNotificationName:JVStyleViewDidClearNotification object:self];
705         if( _switchingStyles )
706                 [NSThread detachNewThreadSelector:@selector( _switchStyle ) toTarget:self withObject:nil];
707 }
708
709 - (void) _reallyAwakeFromNib {
710         _ready = YES;
711         [self _resetDisplay];
712 }
713
714 - (void) _resetDisplay {
715         [[self class] cancelPreviousPerformRequestsWithTarget:self selector:_cmd object:nil];
716
717         [self stopLoading:nil];
718         [self clearScrollbarMarks];
719
720         _contentFrameReady = NO;
721         if( _rememberScrollPosition ) {
722                 _lastScrollPosition = [[_domDocument body] integerForDOMProperty:@"scrollTop"];
723         } else _lastScrollPosition = 0;
724
725         if( ! [[self window] isFlushWindowDisabled] )
726                 [[self window] disableFlushWindow];
727
728         if( _mainFrameReady ) {
729                 WebFrame *contentFrame = [[self mainFrame] findFrameNamed:@"content"];
730                 [contentFrame loadHTMLString:[self _contentHTMLWithBody:@""] baseURL:[self _baseURL]];
731         } else {
732                 NSString *path = [[NSBundle mainBundle] pathForResource:@"base" ofType:@"html"];
733                 NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL fileURLWithPath:path] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:5.];
734                 [[self mainFrame] loadRequest:request];
735         }
736 }
737
738 - (void) _switchStyle {
739         NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
740
741         [NSThread setThreadPriority:0.25];
742
743         [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.00025]]; // wait, WebKit might not be ready.
744
745         JVStyle *style = [[self style] retain];
746         JVChatTranscript *transcript = [[self transcript] retain];
747         NSMutableArray *highlightedMsgs = [[NSMutableArray allocWithZone:nil] initWithCapacity:( [self scrollbackLimit] / 8 )];
748         NSMutableDictionary *parameters = [[NSMutableDictionary allocWithZone:nil] initWithDictionary:_styleParameters copyItems:NO];
749         unsigned long elementCount = [transcript elementCount];
750
751         [parameters setObject:@"'yes'" forKey:@"bulkTransform"];
752
753         for( unsigned long i = elementCount; i > ( elementCount - MIN( [self scrollbackLimit], elementCount ) ); i -= MIN( 25u, i ) ) {
754                 NSArray *elements = [transcript elementsInRange:NSMakeRange( i - MIN( 25u, i ), MIN( 25u, i ) )];
755
756                 id element = nil;
757                 NSEnumerator *enumerator = [elements objectEnumerator];
758                 while( ( element = [enumerator nextObject] ) )
759                         if( [element isKindOfClass:[JVChatMessage class]] && [element isHighlighted] )
760                                 [highlightedMsgs addObject:element];
761
762                 NSString *result = [style transformChatTranscriptElements:elements withParameters:parameters];
763
764                 if( [self style] != style ) goto quickEnd;
765                 if( result ) {
766                         [self performSelectorOnMainThread:@selector( _prependMessages: ) withObject:result waitUntilDone:YES];
767                         [NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.]]; // give time to other threads
768                 }
769         }
770
771         _switchingStyles = NO;
772         [self performSelectorOnMainThread:@selector( markScrollbarForMessages: ) withObject:highlightedMsgs waitUntilDone:YES];
773
774 quickEnd:
775         [self performSelectorOnMainThread:@selector( _switchingStyleFinished: ) withObject:nil waitUntilDone:YES];
776
777         NSNotification *note = [NSNotification notificationWithName:JVStyleViewDidChangeStylesNotification object:self userInfo:nil];
778         [[NSNotificationCenter defaultCenter] postNotificationOnMainThread:note];
779
780         [highlightedMsgs release];
781         [parameters release];
782         [style release];
783         [transcript release];
784         [pool release];
785 }
786
787 - (void) _switchingStyleFinished:(id) sender {
788         _switchingStyles = NO;
789
790         if( _rememberScrollPosition ) {
791                 _rememberScrollPosition = NO;
792                 [[_domDocument body] setInteger:_lastScrollPosition forDOMProperty:@"scrollTop"];
793         }
794 }
795
796 - (void) _appendMessage:(NSString *) message {
797         if( ! _body ) return;
798
799         unsigned messageCount = [self _visibleMessageCount] + 1;
800         unsigned scrollbackLimit = [self scrollbackLimit];
801         JVMarkedScroller *scroller = [self verticalMarkedScroller];
802         BOOL consecutive = ( [message rangeOfString:@"<?message type=\"consecutive\"?>"].location != NSNotFound );
803         if( ! consecutive ) consecutive = ( [message rangeOfString:@"<?message type=\"subsequent\"?>"].location != NSNotFound );
804
805         // check if we are near the bottom of the chat area, and if we should scroll down later
806         BOOL scrollToBottomNeeded = [self scrolledNearBottom];