2007-06-18 Mitz Pettel <mitz@webkit.org>
[WebKit-https.git] / WebKit / WebInspector / WebInspector.m
1 /*
2  * Copyright (C) 2006, 2007 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  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "WebInspector.h"
30 #import "WebInspectorInternal.h"
31
32 #import "WebFrame.h"
33 #import "WebFrameInternal.h"
34 #import "WebInspectorPanel.h"
35 #import "WebNodeHighlight.h"
36 #import "WebPreferences.h"
37 #import "WebScriptDebugDelegate.h"
38 #import "WebTypesInternal.h"
39 #import "WebView.h"
40 #import "WebViewPrivate.h"
41
42 #import <WebKit/DOMCore.h>
43 #import <WebKit/DOMExtensions.h>
44
45 @interface NSWindow (NSWindowPrivate)
46 - (void)_setContentHasShadow:(BOOL)hasShadow;
47 @end
48
49 #pragma mark -
50
51 static WebInspector *sharedWebInspector = nil;
52
53 @implementation WebInspector
54 + (WebInspector *)sharedWebInspector
55 {
56     if (!sharedWebInspector)
57         sharedWebInspector = [[self alloc] init];
58     return sharedWebInspector;
59 }
60
61 #pragma mark -
62
63 - (id)init
64 {
65     if (![super initWithWindow:nil])
66         return nil;
67
68     [self setWindowFrameAutosaveName:@"Web Inspector"];
69
70     _private = [[WebInspectorPrivate alloc] init];
71
72     // Keep preferences separate from the rest of the client, making sure we are using expected preference values.
73     // One reason this is good is that it keeps the inspector out of history via "private browsing".
74
75     WebPreferences *preferences = [[WebPreferences alloc] init];
76     [preferences setAutosaves:NO];
77     [preferences setPrivateBrowsingEnabled:YES];
78     [preferences setLoadsImagesAutomatically:YES];
79     [preferences setJavaScriptEnabled:YES];
80     [preferences setAllowsAnimatedImages:YES];
81     [preferences setLoadsImagesAutomatically:YES];
82     [preferences setPlugInsEnabled:NO];
83     [preferences setJavaEnabled:NO];
84     [preferences setUserStyleSheetEnabled:NO];
85     [preferences setTabsToLinks:NO];
86     [preferences setMinimumFontSize:0];
87     [preferences setMinimumLogicalFontSize:9];
88
89     _private->webView = [[WebView alloc] init];
90     [_private->webView setPreferences:preferences];
91     [_private->webView setFrameLoadDelegate:self];
92     [_private->webView setUIDelegate:self];
93 #ifndef NDEBUG
94     [_private->webView setScriptDebugDelegate:self];
95 #endif
96     [_private->webView setDrawsBackground:NO];
97     [_private->webView setProhibitsMainFrameScrolling:YES];
98     [_private->webView _setDashboardBehavior:WebDashboardBehaviorAlwaysSendMouseEventsToAllWindows to:YES];
99     [_private->webView _setDashboardBehavior:WebDashboardBehaviorAlwaysAcceptsFirstMouse to:YES];
100
101     [preferences release];
102
103     NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"inspector" ofType:@"html" inDirectory:@"webInspector"];
104     NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL fileURLWithPath:path]];
105     [[_private->webView mainFrame] loadRequest:request];
106     [request release];
107
108     while (!_private->webViewLoaded)
109         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
110
111     return self;
112 }
113
114 - (id)initWithWebFrame:(WebFrame *)webFrame
115 {
116     if (![self init])
117         return nil;
118     [self setWebFrame:webFrame];
119     return self;
120 }
121
122 - (void)dealloc
123 {
124     [_private release];
125     [super dealloc];
126 }
127
128 #pragma mark -
129
130 - (NSWindow *)window
131 {
132     NSWindow *window = [super window];
133     if (!window) {
134         NSPanel *window = [[WebInspectorPanel alloc] initWithContentRect:NSMakeRect(60.0f, 200.0f, 350.0f, 550.0f)
135             styleMask:(NSBorderlessWindowMask | NSUtilityWindowMask) backing:NSBackingStoreBuffered defer:YES];
136         [window setBackgroundColor:[NSColor clearColor]];
137         [window setOpaque:NO];
138         [window setHasShadow:YES];
139         [window _setContentHasShadow:NO];
140         [window setWorksWhenModal:YES];
141         [window setAcceptsMouseMovedEvents:YES];
142         [window setIgnoresMouseEvents:NO];
143         [window setFloatingPanel:YES];
144         [window setReleasedWhenClosed:YES];
145         [window setMovableByWindowBackground:YES];
146         [window setHidesOnDeactivate:NO];
147         [window setDelegate:self];
148         [window setMinSize:NSMakeSize(280.0f, 450.0f)];
149
150         [_private->webView setFrame:[[window contentView] frame]];
151         [window setContentView:_private->webView];
152
153         [self setWindow:window];
154         [window release];
155
156         return window;
157     }
158
159     return window;
160 }
161
162 - (void)windowWillClose:(NSNotification *)notification
163 {
164     [_private->currentHighlight expire];
165
166     [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillResignActiveNotification object:nil];
167     [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil];
168     [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationWillTerminateNotification object:nil];
169     [[NSNotificationCenter defaultCenter] removeObserver:self name:WebNodeHighlightExpiredNotification object:nil];
170     [[NSNotificationCenter defaultCenter] removeObserver:self name:NSViewFrameDidChangeNotification object:nil];
171     [[NSNotificationCenter defaultCenter] removeObserver:self name:NSSystemColorsDidChangeNotification object:nil];
172
173     [self setRootDOMNode:nil];
174     [self setFocusedDOMNode:nil];
175     [self setWebFrame:nil];
176
177     [[_private->webView windowScriptObject] setValue:[NSNull null] forKey:@"Inspector"];
178
179     if (self == sharedWebInspector) {
180         [self release];
181         sharedWebInspector = nil;
182     }
183 }
184
185 - (IBAction)showWindow:(id)sender
186 {
187     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_nodeHighlightExpired:) name:WebNodeHighlightExpiredNotification object:nil];
188     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_updateSystemColors) name:NSSystemColorsDidChangeNotification object:nil];
189     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillResignActive) name:NSApplicationWillResignActiveNotification object:nil];
190     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationDidBecomeActive) name:NSApplicationDidBecomeActiveNotification object:nil];
191     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_applicationWillTerminate) name:NSApplicationWillTerminateNotification object:nil];
192
193     [[_private->webView windowScriptObject] setValue:self forKey:@"Inspector"];
194
195     [super showWindow:sender];
196 }
197
198 #pragma mark -
199
200 - (void)setWebFrame:(WebFrame *)webFrame
201 {
202     if ([webFrame isEqual:_private->inspectedWebFrame])
203         return;
204
205     if (_private->inspectedWebFrame) {
206         [[NSNotificationCenter defaultCenter] removeObserver:self name:WebViewProgressFinishedNotification object:[_private->inspectedWebFrame webView]];
207         [_private->inspectedWebFrame _removeInspector:self];
208     }
209
210     id oldFrame = _private->inspectedWebFrame;
211     _private->inspectedWebFrame = [webFrame retain];
212     [oldFrame release];
213
214     if (_private->inspectedWebFrame) {
215         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inspectedWebViewProgressFinished:) name:WebViewProgressFinishedNotification object:[_private->inspectedWebFrame webView]];
216         [_private->inspectedWebFrame _addInspector:self];
217     }
218
219     _private->preventHighlight = YES;
220     [self setFocusedDOMNode:[webFrame DOMDocument]];
221     _private->preventHighlight = NO;
222 }
223
224 - (WebFrame *)webFrame
225 {
226     return _private->inspectedWebFrame;
227 }
228
229 #pragma mark -
230
231 - (void)setRootDOMNode:(DOMNode *)node
232 {
233     NSArray *args = [[NSArray alloc] initWithObjects:(node ? (id)node : (id)[NSNull null]), nil];
234     [[_private->webView windowScriptObject] callWebScriptMethod:@"updateRootNode" withArguments:args];
235     [args release];
236 }
237
238 - (DOMNode *)rootDOMNode
239 {
240     return [[_private->webView windowScriptObject] valueForKey:@"rootDOMNode"];
241 }
242
243 #pragma mark -
244
245 - (void)setFocusedDOMNode:(DOMNode *)node
246 {
247     NSArray *args = [[NSArray alloc] initWithObjects:(node ? (id)node : (id)[NSNull null]), nil];
248     [[_private->webView windowScriptObject] callWebScriptMethod:@"updateFocusedNode" withArguments:args];
249     [args release];
250 }
251
252 - (DOMNode *)focusedDOMNode
253 {
254     return [[_private->webView windowScriptObject] valueForKey:@"focusedDOMNode"];
255 }
256 @end
257
258 #pragma mark -
259
260 @implementation WebInspector (WebInspectorScripting)
261 - (void)showOptionsMenu
262 {
263     NSMenu *menu = [[NSMenu alloc] init];
264     [menu setAutoenablesItems:NO];
265
266     NSMenuItem *item = [[NSMenuItem alloc] init];
267     [item setTitle:@"Ignore Whitespace"];
268     [item setTarget:self];
269     [item setAction:@selector(_toggleIgnoreWhitespace:)];
270
271     id value = [[_private->webView windowScriptObject] valueForKey:@"ignoreWhitespace"];
272     [item setState:(value && [value isKindOfClass:[NSNumber class]] && [value boolValue] ? NSOnState : NSOffState)];
273
274     [menu addItem:item];
275     [item release];
276
277     item = [[NSMenuItem alloc] init];
278     [item setTitle:@"Show User Agent Styles"];
279     [item setTarget:self];
280     [item setAction:@selector(_toggleShowUserAgentStyles:)];
281
282     value = [[_private->webView windowScriptObject] valueForKey:@"showUserAgentStyles"];
283     [item setState:(value && [value isKindOfClass:[NSNumber class]] && [value boolValue] ? NSOnState : NSOffState)];
284
285     [menu addItem:item];
286     [item release];
287
288     [NSMenu popUpContextMenu:menu withEvent:[[self window] currentEvent] forView:_private->webView];
289     [menu release];
290
291     // hack to force a layout and balance mouse events
292     NSEvent *currentEvent = [[self window] currentEvent];
293     NSEvent *event = [NSEvent mouseEventWithType:NSLeftMouseUp location:[currentEvent locationInWindow] modifierFlags:[currentEvent modifierFlags] timestamp:[currentEvent timestamp] windowNumber:[[currentEvent window] windowNumber] context:[currentEvent context] eventNumber:[currentEvent eventNumber] clickCount:[currentEvent clickCount] pressure:[currentEvent pressure]];
294     [[[[_private->webView mainFrame] frameView] documentView] mouseUp:event];
295 }
296
297 - (void)highlightDOMNode:(DOMNode *)node
298 {
299     if (!_private->preventHighlight) {
300         NSRect bounds = [node boundingBox];
301         if (!NSIsEmptyRect(bounds)) {
302             NSRect visible = [[[_private->inspectedWebFrame frameView] documentView] visibleRect];
303             BOOL needsScroll = !NSContainsRect(visible, bounds) && !NSContainsRect(bounds, visible);
304
305             // only scroll if the bounds isn't in the visible rect and dosen't contain the visible rect
306             if (needsScroll) {
307                 // scroll to the parent element if we aren't focused on an element
308                 DOMElement *element;
309                 if ([node isKindOfClass:[DOMElement class]])
310                     element = (DOMElement *)node;
311                 else
312                     element = (DOMElement *)[node parentNode];
313                 [element scrollIntoViewIfNeeded:YES];
314
315                 // give time for the scroll to happen
316                 [self performSelector:@selector(_highlightNode:) withObject:node afterDelay:0.25];
317             } else
318                 [self _highlightNode:node];
319         } else
320             [_private->currentHighlight expire];
321     }
322 }
323 @end
324
325 #pragma mark -
326
327 @implementation WebInspector (WebInspectorPrivate)
328 - (IBAction)_toggleIgnoreWhitespace:(id)sender
329 {
330     [[_private->webView windowScriptObject] callWebScriptMethod:@"toggleIgnoreWhitespace" withArguments:nil];
331 }
332
333 - (IBAction)_toggleShowUserAgentStyles:(id)sender
334 {
335     [[_private->webView windowScriptObject] callWebScriptMethod:@"toggleShowUserAgentStyles" withArguments:nil];
336 }
337
338 - (void)_highlightNode:(DOMNode *)node
339 {
340     if (_private->currentHighlight) {
341         [_private->currentHighlight expire];
342         [_private->currentHighlight release];
343         _private->currentHighlight = nil;
344     }
345
346     NSView *view = [[_private->inspectedWebFrame frameView] documentView];
347     if (![view window])
348         return; // skip the highlight if we have no window (e.g. hidden tab)
349
350     NSRect bounds = NSIntersectionRect([node boundingBox], [view visibleRect]);
351     if (!NSIsEmptyRect(bounds)) {
352         NSArray *rects = nil;
353         if ([node isKindOfClass:[DOMElement class]]) {
354             DOMCSSStyleDeclaration *style = [[node ownerDocument] getComputedStyle:(DOMElement *)node pseudoElement:@""];
355             if ([[style getPropertyValue:@"display"] isEqualToString:@"inline"])
356                 rects = [[node lineBoxRects] retain];
357         } else if ([node isKindOfClass:[DOMText class]]
358 #if ENABLE(SVG)
359                    && ![[node parentNode] isKindOfClass:NSClassFromString(@"DOMSVGElement")]
360 #endif
361                   )
362             rects = [[node lineBoxRects] retain];
363
364         if (![rects count])
365             rects = [[NSArray alloc] initWithObjects:[NSValue valueWithRect:bounds], nil];
366
367         _private->currentHighlight = [[WebNodeHighlight alloc] initWithBounds:bounds andRects:rects forView:view];
368
369         [rects release];
370     }
371 }
372
373 - (void)_nodeHighlightExpired:(NSNotification *)notification
374 {
375     if (_private->currentHighlight == [notification object]) {
376         [_private->currentHighlight release];
377         _private->currentHighlight = nil;
378     }
379 }
380
381 - (void)_updateSystemColors
382 {
383     CGFloat red = 0.0f, green = 0.0f, blue = 0.0f;
384     NSColor *color = [[NSColor alternateSelectedControlColor] colorUsingColorSpaceName:NSDeviceRGBColorSpace];
385     [color getRed:&red green:&green blue:&blue alpha:NULL];
386
387     NSString *colorText = [NSString stringWithFormat:@"rgba(%.0f, %.0f, %.0f, 0.4) !important", (red * 255), (green * 255), (blue * 255)];
388     NSString *styleText = [NSString stringWithFormat:@".focused .selected { background-color: %@ } .blured .selected { border-color: %@ }", colorText, colorText];
389     DOMDocument *document = [[_private->webView mainFrame] DOMDocument];
390     DOMElement *style = [document getElementById:@"systemColors"];
391     if (!style) {
392         style = [document createElement:@"style"];
393         [style setAttribute:@"id" value:@"systemColors"];
394         [[[document getElementsByTagName:@"head"] item:0] appendChild:style];
395     }
396
397     [style setTextContent:styleText];
398 }
399
400 - (void)_applicationWillResignActive
401 {
402     [(NSPanel *)[self window] setFloatingPanel:NO];
403 }
404
405 - (void)_applicationDidBecomeActive
406 {
407     [(NSPanel *)[self window] setFloatingPanel:YES];
408 }
409
410 - (void)_applicationWillTerminate
411 {
412     [_private->webView close];
413 }
414
415 - (void)_webFrameDetached:(WebFrame *)frame
416 {
417     [self setRootDOMNode:nil];
418     [self setFocusedDOMNode:nil];
419     [self setWebFrame:nil];
420 }
421
422 #pragma mark -
423
424 + (BOOL)isSelectorExcludedFromWebScript:(SEL)aSelector
425 {
426     return NO;
427 }
428
429 + (NSString *)webScriptNameForSelector:(SEL)aSelector {
430     NSMutableString *name = [[NSStringFromSelector(aSelector) mutableCopy] autorelease];
431     [name replaceOccurrencesOfString:@":" withString:@"_" options:NSLiteralSearch range:NSMakeRange(0, [name length])];
432     if ([name hasSuffix:@"_"])
433         return [name substringToIndex:[name length] - 1];
434     return name;
435 }
436
437 + (BOOL)isKeyExcludedFromWebScript:(const char *)name
438 {
439     return NO;
440 }
441
442 #pragma mark -
443
444 - (void)inspectedWebViewProgressFinished:(NSNotification *)notification
445 {
446     if ([notification object] == [[self webFrame] webView])
447         [self setFocusedDOMNode:[[self webFrame] DOMDocument]];
448 }
449
450 #pragma mark -
451
452 - (void)webView:(WebView *)sender windowScriptObjectAvailable:(WebScriptObject *)windowScriptObject
453 {
454     // note: this is the Inspector's own WebView, not the one being inspected
455     [[sender windowScriptObject] setValue:self forKey:@"Inspector"];
456 }
457
458 - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
459 {
460     // note: this is the Inspector's own WebView, not the one being inspected
461     _private->webViewLoaded = YES;
462
463     [[self window] invalidateShadow];
464
465     [self _updateSystemColors];
466 }
467
468 - (void)webView:(WebView *)sender runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WebFrame *)frame
469 {
470     // note: this is the Inspector's own WebView, not the one being inspected
471 #ifndef NDEBUG
472     NSLog(@"%@", message);
473 #endif
474 }
475
476 #ifndef NDEBUG
477 - (void)webView:(WebView *)view didParseSource:(NSString *)source baseLineNumber:(unsigned)baseLine fromURL:(NSURL *)url sourceId:(int)sid forWebFrame:(WebFrame *)webFrame
478 {
479     // note: this is the Inspector's own WebView, not the one being inspected
480     if (!_private->debugFileMap)
481         _private->debugFileMap = [[NSMutableDictionary alloc] init];
482     if (url)
483         [_private->debugFileMap setObject:url forKey:[NSNumber numberWithInt:sid]];
484 }
485
486 - (void)webView:(WebView *)view exceptionWasRaised:(WebScriptCallFrame *)frame sourceId:(int)sid line:(int)lineno forWebFrame:(WebFrame *)webFrame
487 {
488     // note: this is the Inspector's own WebView, not the one being inspected
489     NSURL *url = [_private->debugFileMap objectForKey:[NSNumber numberWithInt:sid]];
490     NSLog(@"JS exception: %@ on %@ line %d", [[frame exception] valueForKey:@"message"], url, lineno);
491
492     NSMutableArray *stack = [[NSMutableArray alloc] init];
493     WebScriptCallFrame *currentFrame = frame;
494     while (currentFrame) {
495         if ([currentFrame functionName])
496             [stack addObject:[currentFrame functionName]];
497         else if ([frame caller])
498             [stack addObject:@"(anonymous function)"];
499         else
500             [stack addObject:@"(global scope)"];
501         currentFrame = [currentFrame caller];
502     }
503
504     NSLog(@"Stack trace:\n%@", [stack componentsJoinedByString:@"\n"]);
505     [stack release];
506 }
507 #endif
508 @end
509
510 #pragma mark -
511
512 @implementation WebInspectorPrivate
513 - (void)dealloc
514 {
515     // Releasing our WebView will trigger a garbage collection, but this dealloc is
516     // likely going to happen inside GC and we don't want to re-enter the collector.
517     // Just autorelease is not good enough, because the ObjC bindings code drains the
518     // autorelease pool.
519     [webView performSelector:@selector(release) withObject:nil afterDelay:0];
520     [inspectedWebFrame release];
521     [currentHighlight release];
522 #ifndef NDEBUG
523     [debugFileMap release];
524 #endif
525     [super dealloc];
526 }
527 @end