[iOS] Upstream WAK
[WebKit.git] / Source / WebCore / platform / ios / wak / WAKView.mm
1 /*
2  * Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WAKViewPrivate.h"
28
29 #if PLATFORM(IOS)
30
31 #import "GraphicsContext.h"
32 #import "WAKClipView.h"
33 #import "WAKScrollView.h"
34 #import "WAKViewPrivate.h"
35 #import "WAKWindow.h"
36 #import "WKGraphics.h"
37 #import "WKUtilities.h"
38 #import "WKViewPrivate.h"
39 #import "WebCoreThreadMessage.h"
40 #import "WebEvent.h"
41 #import <wtf/Assertions.h>
42
43 NSString *WAKViewFrameSizeDidChangeNotification =   @"WAKViewFrameSizeDidChangeNotification";
44 NSString *WAKViewDidScrollNotification =            @"WAKViewDidScrollNotification";
45
46 static WAKView *globalFocusView = nil;
47 static CGInterpolationQuality sInterpolationQuality;
48
49 static void setGlobalFocusView(WAKView *view)
50 {
51     if (view == globalFocusView)
52         return;
53
54     [view retain];
55     [globalFocusView release];
56     globalFocusView = view;
57 }
58
59 static WAKScrollView *enclosingScrollView(WAKView *view)
60 {
61     view = [view superview];
62     while (view && ![view isKindOfClass:[WAKScrollView class]])
63         view = [view superview];
64     return (WAKScrollView *)view;
65 }
66
67 @interface WAKScrollView()
68 - (void)_adjustScrollers;
69 @end
70
71 @interface WAKView (WAKInternal)
72 - (id)_initWithViewRef:(WKViewRef)viewRef;
73 @end
74
75 @implementation WAKView
76
77 static NSInvocation* invocationForPostNotification(NSString *name, id object, id userInfo)
78 {
79     NSNotificationCenter *target = [NSNotificationCenter defaultCenter];
80     NSInvocation *invocation = WebThreadMakeNSInvocation(target, @selector(postNotificationName:object:userInfo:));
81     [invocation setArgument:&name atIndex:2];
82     [invocation setArgument:&object atIndex:3];
83     [invocation setArgument:&userInfo atIndex:4];
84     return invocation;
85 }
86
87 static void notificationCallback (WKViewRef v, WKViewNotificationType type, void *userInfo)
88 {
89     UNUSED_PARAM(v);
90     WAKView *view = (WAKView *)userInfo;
91     switch (type){
92         case WKViewNotificationViewDidMoveToWindow: {
93             [view viewDidMoveToWindow];
94             break;
95         }
96         case WKViewNotificationViewFrameSizeChanged: {
97             [view frameSizeChanged];
98             if (WAKScrollView *scrollView = enclosingScrollView(view))
99                 [scrollView _adjustScrollers];
100
101             // Posting a notification to the main thread can cause the WebThreadLock to be
102             // relinquished, which gives the main thread a change to run (and possible try
103             // to paint). We don't want this to happen if we've updating view sizes in the middle
104             // of layout, so use an async notification. <rdar://problem/6745974>
105             NSInvocation *invocation = invocationForPostNotification(WAKViewFrameSizeDidChangeNotification, view, nil);
106             WebThreadCallDelegateAsync(invocation);
107             break;
108         }
109         case WKViewNotificationViewDidScroll: {
110             WebThreadRunOnMainThread(^ {
111                  [[NSNotificationCenter defaultCenter] postNotificationName:WAKViewDidScrollNotification object:view userInfo:nil];
112             });
113             break;
114         }            
115         default: {
116             break;
117         }
118     }
119 }
120
121 - (void)handleEvent:(WebEvent *)event
122 {
123     ASSERT(event);
124     WAKView *view = self;
125     while (view) {
126         if ([view _selfHandleEvent:event])
127             break;
128         view = [view superview];
129     }
130 }
131
132 - (BOOL)_selfHandleEvent:(WebEvent *)event
133 {
134     WebEventType type = event.type;
135     
136     switch (type) {
137         case WebEventMouseDown: {
138             [self mouseDown:event];
139             break;
140         }
141         case WebEventMouseUp: {
142             [self mouseUp:event];
143             break;
144         }
145         case WebEventMouseMoved: {
146             [self mouseMoved:event];
147             break;
148         }
149         case WebEventKeyDown: {
150             [self keyDown:event];
151             break;
152         }
153         case WebEventKeyUp: {
154             [self keyUp:event];
155             break;
156         }
157         case WebEventScrollWheel: {
158             [self scrollWheel:event];
159             break;
160         }
161 #if ENABLE(TOUCH_EVENTS)
162         case WebEventTouchBegin:
163         case WebEventTouchChange:
164         case WebEventTouchEnd:
165         case WebEventTouchCancel: {
166             [self touch:event];
167             break;
168         }
169 #endif
170         default:
171             ASSERT_NOT_REACHED();
172             break;
173     }
174     return YES;
175 }
176
177 - (NSResponder *)nextResponder
178 {
179     return [self superview];
180 }
181
182 static bool responderCallback(WKViewRef, WKViewResponderCallbackType type, void *userInfo)
183 {
184     return [(WAKView *)userInfo _handleResponderCall:type];
185 }
186
187 - (BOOL)_handleResponderCall:(WKViewResponderCallbackType)type
188 {
189     switch (type) {
190         case WKViewResponderAcceptsFirstResponder:
191             return [self acceptsFirstResponder];
192         case WKViewResponderBecomeFirstResponder:
193             return [self becomeFirstResponder];
194         case WKViewResponderResignFirstResponder:
195             return [self resignFirstResponder];
196     }
197     return NO;
198 }
199
200 static void willRemoveSubviewCallback(WKViewRef view, WKViewRef subview)
201 {
202     [WAKViewForWKViewRef(view) willRemoveSubview:WAKViewForWKViewRef(subview)];
203 }
204
205 static void invalidateGStateCallback(WKViewRef view)
206 {
207     [WAKViewForWKViewRef(view) invalidateGState];
208 }
209
210 + (WAKView *)_wrapperForViewRef:(WKViewRef)_viewRef
211 {
212     ASSERT(_viewRef);
213     if (_viewRef->isa.classInfo == &WKViewClassInfo)
214         return [[[WAKView alloc] _initWithViewRef:_viewRef] autorelease];
215     WKError ("unable to create wrapper for %s\n", _viewRef->isa.classInfo->name);
216     return nil;
217 }
218
219 - (id)_initWithViewRef:(WKViewRef)viewR
220 {
221     self = [super init];
222     if (!self)
223         return nil;
224
225     viewRef = (WKViewRef)WKRetain (viewR);
226     viewRef->wrapper = (void *)self;
227
228     return self;
229 }
230
231 - (id)init
232 {
233     return [self initWithFrame:CGRectZero];
234 }
235
236 - (id)initWithFrame:(CGRect)rect
237 {
238     WKViewRef view = WKViewCreateWithFrame(rect, &viewContext);
239     self = [self _initWithViewRef:view];
240     if (self) {
241         viewContext.notificationCallback = notificationCallback;
242         viewContext.notificationUserInfo = self;
243         viewContext.responderCallback = responderCallback;
244         viewContext.responderUserInfo = self;
245         viewContext.willRemoveSubviewCallback = willRemoveSubviewCallback;
246         viewContext.invalidateGStateCallback = invalidateGStateCallback;
247     }
248     WKRelease(view);
249     return self;
250 }
251
252 - (void)dealloc
253 {
254     [[[subviewReferences copy] autorelease] makeObjectsPerformSelector:@selector(removeFromSuperview)];
255
256     if (viewRef) {
257         _WKViewSetViewContext (viewRef, 0);
258         viewRef->wrapper = NULL;
259         WKRelease (viewRef);
260     }
261     
262     [subviewReferences release];
263     
264     [super dealloc];
265 }
266
267 - (WAKWindow *)window
268 {
269     return WKViewGetWindow(viewRef);
270 }
271
272 - (WKViewRef)_viewRef
273 {
274     return viewRef;
275 }
276
277 - (NSMutableSet *)_subviewReferences
278 {
279     if (!subviewReferences)
280         subviewReferences = [[NSMutableSet alloc] init];
281     return subviewReferences;
282 }
283
284 static void _WAKCopyWrapper(const void *value, void *context)
285 {
286     if (!value)
287         return;
288     if (!context)
289         return;
290     
291     NSMutableArray *array = (NSMutableArray *)context;
292     WAKView *view = WAKViewForWKViewRef((WKViewRef)value);
293     if (view)
294         [array addObject:view];
295 }
296
297 - (NSArray *)subviews
298 {
299     CFArrayRef subviews = WKViewGetSubviews([self _viewRef]);
300     if (!subviews)
301         return [NSArray array];
302     
303     CFIndex count = CFArrayGetCount(subviews);
304     if (count == 0)
305         return [NSArray array];
306     
307     NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
308     if (!result)
309         return [NSArray array];
310     
311     CFArrayApplyFunction(subviews, CFRangeMake(0, count), _WAKCopyWrapper, (void*)result);
312     
313     return result;
314 }
315
316 - (WAKView *)superview
317 {
318     return WAKViewForWKViewRef(viewRef->superview);
319 }
320
321 - (WAKView *)lastScrollableAncestor
322 {
323     WAKView *view = nil;
324     WAKScrollView *scrollView = enclosingScrollView(self);
325     
326     while (scrollView) {
327         
328         CGSize scrollViewSize = WKViewGetFrame((WKViewRef)scrollView).size;
329
330         WAKView *documentView = [scrollView documentView];
331         scrollView = enclosingScrollView(scrollView);
332                                          
333         // If this document view can be scrolled, and this is NOT the last scroll view.
334         // Our last scroll view is non-existent as far as we're concerned.
335
336         if (scrollView && !CGSizeEqualToSize(scrollViewSize, [documentView frame].size))
337             view = documentView;
338     }
339     
340     return view;
341 }
342
343 - (void)addSubview:(WAKView *)subview
344 {
345     [subview retain];
346     [subview removeFromSuperview];
347     WKViewAddSubview (viewRef, [subview _viewRef]);
348     
349     // Keep a reference to subview so it sticks around.
350     [[self _subviewReferences] addObject:subview];
351     [subview release];
352 }
353
354 - (void)willRemoveSubview:(WAKView *)subview
355 {
356     UNUSED_PARAM(subview);
357 }
358
359 - (void)removeFromSuperview
360 {
361     WAKView *oldSuperview = [[self superview] retain];
362     WKViewRemoveFromSuperview (viewRef);
363     [[oldSuperview _subviewReferences] removeObject:self];
364     [oldSuperview release];
365 }
366
367 - (void)viewDidMoveToWindow
368 {
369 }
370
371 - (void)frameSizeChanged
372 {
373 }
374
375 - (void)setNeedsDisplay:(BOOL)flag
376 {
377     if (!flag)
378         return;
379     [self setNeedsDisplayInRect:WKViewGetBounds(viewRef)];
380 }
381
382 - (void)setNeedsDisplayInRect:(CGRect)invalidRect
383 {
384     if (_isHidden)
385         return;
386
387     WAKWindow *window = [self window];
388     if (!window || CGRectIsEmpty(invalidRect))
389         return;
390
391     CGRect rect = CGRectIntersection(invalidRect, [self bounds]);
392     if (CGRectIsEmpty(rect))
393         return;
394
395     CGRect baseRect = WKViewConvertRectToBase(viewRef, rect);
396     [window setNeedsDisplayInRect:baseRect];
397 }
398
399 - (BOOL)needsDisplay 
400 {
401     return NO;
402 }
403
404 - (void)display
405 {
406     [self displayRect:WKViewGetBounds(viewRef)];
407 }
408
409 - (void)displayIfNeeded
410 {
411 }
412
413 - (void)drawRect:(CGRect)rect
414 {
415     // Do nothing.  Override in subclass.
416     UNUSED_PARAM(rect);
417 }
418
419 - (void)viewWillDraw
420 {
421     [[self subviews] makeObjectsPerformSelector:@selector(viewWillDraw)];
422 }
423
424 + (WAKView *)focusView
425 {
426     return globalFocusView;
427 }
428
429 - (NSRect)bounds
430
431     return WKViewGetBounds (viewRef);
432 }
433
434 - (NSRect)frame 
435
436     return WKViewGetFrame (viewRef);
437 }
438
439 - (void)setFrame:(NSRect)frameRect
440 {
441     WKViewSetFrameOrigin (viewRef, frameRect.origin);
442     WKViewSetFrameSize (viewRef, frameRect.size);
443 }
444
445 - (void)setFrameOrigin:(NSPoint)newOrigin
446 {
447     WKViewSetFrameOrigin (viewRef, newOrigin);
448 }
449
450 - (void)setFrameSize:(NSSize)newSize
451 {
452     WKViewSetFrameSize (viewRef, newSize);
453 }
454
455 - (void)setBoundsSize:(NSSize)size
456 {
457     WKViewSetBoundsSize (viewRef, size);
458 }
459
460 - (void)setBoundsOrigin:(NSPoint)newOrigin
461 {
462     WKViewSetBoundsOrigin(viewRef, newOrigin);
463 }
464
465 - (void)_lockFocusViewInContext:(CGContextRef)context
466 {
467     ASSERT(context);
468     setGlobalFocusView(self);
469     CGContextSaveGState(context);
470
471     WAKView *superview = [self superview];
472     if (superview)
473         CGContextClipToRect(context, [superview bounds]);
474
475     CGContextConcatCTM(context, _WKViewGetTransform(viewRef));
476 }
477
478 - (void)_unlockFocusViewInContext:(CGContextRef)context
479 {
480     ASSERT(context);
481     CGContextRestoreGState(context);
482     setGlobalFocusView(nil);
483 }
484
485 static CGInterpolationQuality toCGInterpolationQuality(WebCore::InterpolationQuality quality)
486 {
487     switch (quality) {
488     case WebCore::InterpolationDefault:
489         return kCGInterpolationDefault;
490     case WebCore::InterpolationNone:
491         return kCGInterpolationNone;
492     case WebCore::InterpolationLow:
493         return kCGInterpolationLow;
494     case WebCore::InterpolationMedium:
495         return kCGInterpolationMedium;
496     case WebCore::InterpolationHigh:
497         return kCGInterpolationHigh;
498     default:
499         ASSERT_NOT_REACHED();
500         return kCGInterpolationLow;
501     }
502 }
503
504 + (void)_setInterpolationQuality:(int)quality
505 {
506     sInterpolationQuality = toCGInterpolationQuality((WebCore::InterpolationQuality)quality);
507 }
508
509 - (void)_drawRect:(NSRect)dirtyRect context:(CGContextRef)context lockFocus:(bool)lockFocus
510 {
511     if (_isHidden)
512         return;
513
514     ASSERT(viewRef);
515     ASSERT(context);
516     CGRect localRect = WKViewGetBounds(viewRef);
517     dirtyRect = CGRectIntersection(localRect, dirtyRect);
518     if (CGRectIsEmpty(dirtyRect))
519         return;
520
521     if (lockFocus)
522         [self _lockFocusViewInContext:context];
523
524     if (viewRef->scale != 1)
525         CGContextSetInterpolationQuality(context, sInterpolationQuality);
526
527     CGContextClipToRect(context, dirtyRect);
528     [self drawRect:dirtyRect];
529
530     if (!_drawsOwnDescendants) {
531         NSArray *subViews = [self subviews];
532         for (WAKView *subView in subViews) {
533             NSRect childDirtyRect = [self convertRect:dirtyRect toView:subView];
534             [subView _drawRect:CGRectIntegral(childDirtyRect) context:context lockFocus:YES];
535         }
536     }
537
538     if (lockFocus)
539         [self _unlockFocusViewInContext:context];
540 }
541
542 - (void)displayRect:(NSRect)rect
543 {
544     CGContextRef context = WKGetCurrentGraphicsContext();
545     if (!context) {
546         WKError ("unable to get context for view");
547         return;
548     }
549
550     [self _drawRect:rect context:context lockFocus:YES];
551 }
552
553 - (void)displayRectIgnoringOpacity:(NSRect)rect
554 {
555     [self displayRect:rect];
556 }
557
558 - (void)displayRectIgnoringOpacity:(NSRect)rect inContext:(CGContextRef)context
559 {
560     if (!context) {
561         WKError ("invalid parameter: context must not be NULL");
562         return;
563     }
564
565     CGContextRef previousContext = WKGetCurrentGraphicsContext();
566     if (context != previousContext)
567         WKSetCurrentGraphicsContext(context);
568
569     [self _drawRect:rect context:context lockFocus:NO];
570
571     if (context != previousContext)
572         WKSetCurrentGraphicsContext (previousContext);
573 }
574
575 - (NSRect)visibleRect
576 {
577     return WKViewGetVisibleRect (viewRef);
578 }
579
580 - (NSPoint)convertPoint:(NSPoint)aPoint toView:(WAKView *)aView 
581 {
582     CGPoint p = WKViewConvertPointToBase (viewRef, aPoint);
583     if (aView)
584         return WKViewConvertPointFromBase ([aView _viewRef], p);
585     return p;
586 }
587
588 - (NSPoint)convertPoint:(NSPoint)aPoint fromView:(WAKView *)aView
589 {
590     if (aView)
591         aPoint = WKViewConvertPointToBase ([aView _viewRef], aPoint);
592     return WKViewConvertPointFromBase (viewRef, aPoint);
593 }
594
595 - (NSSize)convertSize:(NSSize)size toView:(WAKView *)aView
596 {
597     return [self convertRect:NSMakeRect(0.0f, 0.0f, size.width, size.height) toView:aView].size;
598 }
599
600 - (NSRect)convertRect:(NSRect)aRect fromView:(WAKView *)aView
601 {
602     if (aView)
603         aRect = WKViewConvertRectToBase ([aView _viewRef], aRect);
604     return WKViewConvertRectFromBase (viewRef, aRect);
605 }
606
607 - (NSRect)convertRect:(NSRect)aRect toView:(WAKView *)aView
608 {
609     CGRect r = WKViewConvertRectToBase (viewRef, aRect);
610     if (aView)
611         return WKViewConvertRectFromBase ([aView _viewRef], r);
612     return r;
613 }
614
615 - (void)lockFocus
616 {
617     [self _lockFocusViewInContext:WKGetCurrentGraphicsContext()];
618 }
619
620 - (void)unlockFocus
621 {
622     [self _unlockFocusViewInContext:WKGetCurrentGraphicsContext()];
623 }
624
625 - (WAKView *)hitTest:(NSPoint)point
626 {
627     if (!CGRectContainsPoint([self frame], point))
628         return nil;
629
630     CGPoint subviewPoint = WKViewConvertPointFromSuperview(viewRef, point);
631     for (WAKView *subview in [self subviews]) {
632         if (WAKView *hitView = [subview hitTest: subviewPoint])
633             return hitView;
634     }
635
636     return self;
637 }
638
639 - (void)setHidden:(BOOL)flag 
640 {
641     ASSERT(viewRef);
642     if (_isHidden != flag) {
643         // setNeedsDisplay does nothing if the view is hidden so call it at the right time.
644         if (flag) {
645             [self setNeedsDisplay:YES];
646             _isHidden = flag;
647         } else {
648             _isHidden = flag;
649             [self setNeedsDisplay:YES];
650         }
651     }
652 }
653
654 - (BOOL)isDescendantOf:(NSView *)aView
655 {
656     if (aView == self)
657         return YES;
658     else if (![self superview])
659         return NO;
660     else
661         return [[self superview] isDescendantOf:aView];
662 }
663
664 - (BOOL)isHiddenOrHasHiddenAncestor
665 {
666     if (_isHidden)
667         return YES;
668
669     if (![self superview])
670         return NO;
671
672     return [[self superview] isHiddenOrHasHiddenAncestor];
673 }
674
675 - (BOOL)mouse:(NSPoint)aPoint inRect:(NSRect)aRect 
676
677     return WKMouseInRect (aPoint, aRect);
678 }
679
680 - (BOOL)needsPanelToBecomeKey
681 {
682     return true;
683 }
684
685 - (void)setNextKeyView:(NSView *)aView
686 {
687     UNUSED_PARAM(aView);
688 }
689
690 - (WAKView *)previousValidKeyView { return nil; }
691 - (WAKView *)nextKeyView { return nil; }
692 - (WAKView *)nextValidKeyView { return nil; }
693 - (WAKView *)previousKeyView { return nil; }
694
695 - (void)invalidateGState { }
696 - (void)releaseGState { }
697 - (BOOL)inLiveResize { return NO; }
698
699 - (void)setAutoresizingMask:(unsigned int)mask
700 {
701     WKViewSetAutoresizingMask(viewRef, mask);
702 }
703
704 - (unsigned int)autoresizingMask
705 {
706     return WKViewGetAutoresizingMask(viewRef);
707 }
708
709 - (void)scrollPoint:(NSPoint)point 
710 {
711     if (WAKScrollView *scrollView = enclosingScrollView(self)) {
712         CGPoint scrollViewPoint = [self convertPoint:point toView:scrollView];
713         [scrollView scrollPoint:scrollViewPoint];
714     }
715 }
716
717 - (BOOL)scrollRectToVisible:(NSRect)rect
718 {
719     // Scroll if the rect is not already visible.
720     CGRect visibleRect = [self visibleRect];
721     if (!CGRectContainsRect(visibleRect, rect) && !CGRectContainsRect(rect, visibleRect))
722         [self scrollPoint:rect.origin];
723
724     // Return value ignored by WebCore.
725
726     return NO;
727 }
728
729 // Overridden by subclasses (only WebHTMLView for now).
730 - (void)setNeedsLayout:(BOOL)flag
731 {
732     UNUSED_PARAM(flag);
733 }
734
735 - (void)layout { }
736 - (void)layoutIfNeeded { }
737
738 - (void)setScale:(float)scale
739 {
740     WKViewSetScale(viewRef, scale);
741 }
742
743 - (float)scale
744 {
745     return WKViewGetScale(viewRef);
746 }
747
748 - (void)_setDrawsOwnDescendants:(BOOL)draw
749 {
750     _drawsOwnDescendants = draw;
751 }
752
753 - (NSString *)description
754 {
755     NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: WAK: %p (WK: %p); ", [self class], self, viewRef];
756
757     CGRect frame = [self frame];
758     [description appendFormat:@"frame = (%g %g; %g %g)>", frame.origin.x, frame.origin.y, frame.size.width, frame.size.height];
759
760     return description;
761 }
762
763 - (void)_appendDescriptionToString:(NSMutableString *)info atLevel:(int)level
764 {
765     NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
766
767     if ([info length] != 0)
768         [info appendString:@"\n"];
769
770     for (int i = 1; i <= level; i++)
771         [info appendString:@"   | "];
772
773     [info appendString:[self description]];
774
775     for (WAKView *subview in [self subviews])
776         [subview _appendDescriptionToString:info atLevel:level + 1];
777
778     [pool release];
779 }
780
781 @end
782
783 #endif // PLATFORM(IOS)