From 4483204c9c8073eb7848ea958a65bb6e5139f271 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 13 Sep 2023 14:55:34 -0400 Subject: [PATCH] Ladybird: Implement a basic Inspector window for the AppKit chrome This commit includes only fetching the DOM tree from the WebContent process and displaying it in an NSOutlineView. The displayed tree includes some basic styling (e.g. colors). --- .../AppKit/Application/ApplicationDelegate.mm | 3 + Ladybird/AppKit/UI/Inspector.h | 21 ++ Ladybird/AppKit/UI/Inspector.mm | 257 ++++++++++++++++++ Ladybird/AppKit/UI/InspectorController.h | 17 ++ Ladybird/AppKit/UI/InspectorController.mm | 58 ++++ Ladybird/AppKit/UI/LadybirdWebView.h | 1 + Ladybird/AppKit/UI/LadybirdWebView.mm | 4 + Ladybird/AppKit/UI/Tab.h | 3 + Ladybird/AppKit/UI/Tab.mm | 34 +++ Ladybird/CMakeLists.txt | 2 + 10 files changed, 400 insertions(+) create mode 100644 Ladybird/AppKit/UI/Inspector.h create mode 100644 Ladybird/AppKit/UI/Inspector.mm create mode 100644 Ladybird/AppKit/UI/InspectorController.h create mode 100644 Ladybird/AppKit/UI/InspectorController.mm diff --git a/Ladybird/AppKit/Application/ApplicationDelegate.mm b/Ladybird/AppKit/Application/ApplicationDelegate.mm index d025e15e7c..ccc5786b0e 100644 --- a/Ladybird/AppKit/Application/ApplicationDelegate.mm +++ b/Ladybird/AppKit/Application/ApplicationDelegate.mm @@ -347,6 +347,9 @@ [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Console" action:@selector(openConsole:) keyEquivalent:@"J"]]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Inspector" + action:@selector(openInspector:) + keyEquivalent:@"I"]]; [menu setSubmenu:submenu]; return menu; diff --git a/Ladybird/AppKit/UI/Inspector.h b/Ladybird/AppKit/UI/Inspector.h new file mode 100644 index 0000000000..5cca8b5862 --- /dev/null +++ b/Ladybird/AppKit/UI/Inspector.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@class LadybirdWebView; +@class Tab; + +@interface Inspector : NSWindow + +- (instancetype)init:(Tab*)tab; + +- (void)inspect; +- (void)reset; + +@end diff --git a/Ladybird/AppKit/UI/Inspector.mm b/Ladybird/AppKit/UI/Inspector.mm new file mode 100644 index 0000000000..caae1119c9 --- /dev/null +++ b/Ladybird/AppKit/UI/Inspector.mm @@ -0,0 +1,257 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include + +#import +#import +#import +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +static constexpr CGFloat const WINDOW_WIDTH = 600; +static constexpr CGFloat const WINDOW_HEIGHT = 800; + +@interface Inspector () + +@property (nonatomic, strong) Tab* tab; + +@property (nonatomic, strong) NSOutlineView* dom_tree_outline_view; +@property (nonatomic, strong) NSDictionary* dom_tree; + +@end + +@implementation Inspector + +@synthesize tab = _tab; + +- (instancetype)init:(Tab*)tab +{ + auto tab_rect = [tab frame]; + auto position_x = tab_rect.origin.x + (tab_rect.size.width - WINDOW_WIDTH) / 2; + auto position_y = tab_rect.origin.y + (tab_rect.size.height - WINDOW_HEIGHT) / 2; + + auto window_rect = NSMakeRect(position_x, position_y, WINDOW_WIDTH, WINDOW_HEIGHT); + auto style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + + self = [super initWithContentRect:window_rect + styleMask:style_mask + backing:NSBackingStoreBuffered + defer:NO]; + + if (self) { + self.tab = tab; + + auto* split_view = [[NSSplitView alloc] initWithFrame:[self frame]]; + [split_view setDividerStyle:NSSplitViewDividerStylePaneSplitter]; + + auto* top_tab_view = [[NSTabView alloc] init]; + [split_view addSubview:top_tab_view]; + + [self initializeDOMTreeTab:top_tab_view]; + [self reset]; + + auto& web_view = [[self.tab web_view] view]; + __weak Inspector* weak_self = self; + + web_view.on_received_dom_tree = [weak_self](auto const& dom_tree) { + Inspector* strong_self = weak_self; + if (strong_self == nil) { + return; + } + + strong_self.dom_tree = Ladybird::deserialize_json_to_dictionary(dom_tree); + + if (strong_self.dom_tree) { + [strong_self.dom_tree_outline_view reloadItem:nil reloadChildren:YES]; + [strong_self.dom_tree_outline_view sizeToFit]; + } else { + strong_self.dom_tree = @{}; + } + }; + + [self setContentView:split_view]; + [self setTitle:@"Inspector"]; + [self setIsVisible:YES]; + + auto split_view_height = [split_view frame].size.height; + [split_view setPosition:(split_view_height * 0.6f) ofDividerAtIndex:0]; + } + + return self; +} + +- (void)dealloc +{ + auto& web_view = [[self.tab web_view] view]; + web_view.on_received_dom_tree = nullptr; +} + +#pragma mark - Public methods + +- (void)inspect +{ + auto& web_view = [[self.tab web_view] view]; + web_view.inspect_dom_tree(); +} + +- (void)reset +{ + self.dom_tree = @{}; + [self.dom_tree_outline_view reloadItem:nil reloadChildren:YES]; + [self.dom_tree_outline_view sizeToFit]; +} + +#pragma mark - Private methods + +- (void)initializeDOMTreeTab:(NSTabView*)tab_view +{ + auto* tab = [[NSTabViewItem alloc] initWithIdentifier:@"DOM Tree"]; + [tab setLabel:@"DOM"]; + + auto* scroll_view = [[NSScrollView alloc] init]; + [scroll_view setHasVerticalScroller:YES]; + [scroll_view setHasHorizontalScroller:YES]; + [scroll_view setLineScroll:24]; + [tab setView:scroll_view]; + + self.dom_tree_outline_view = [[NSOutlineView alloc] initWithFrame:[tab_view frame]]; + [self.dom_tree_outline_view setDoubleAction:@selector(onTreeDoubleClick:)]; + [self.dom_tree_outline_view setDataSource:self]; + [self.dom_tree_outline_view setDelegate:self]; + [self.dom_tree_outline_view setHeaderView:nil]; + [scroll_view setDocumentView:self.dom_tree_outline_view]; + + auto* column = [[NSTableColumn alloc] initWithIdentifier:@"DOM Tree"]; + [self.dom_tree_outline_view addTableColumn:column]; + + [tab_view addTabViewItem:tab]; +} + +- (void)onTreeDoubleClick:(id)sender +{ + NSOutlineView* outline_view = sender; + id item = [outline_view itemAtRow:[outline_view clickedRow]]; + + if ([outline_view isItemExpanded:item]) { + [outline_view collapseItem:item]; + } else { + [outline_view expandItem:item]; + } +} + +#pragma mark - NSOutlineViewDataSource + +- (id)outlineView:(NSOutlineView*)view + child:(NSInteger)index + ofItem:(id)item +{ + if (item == nil) { + item = self.dom_tree; + } + + NSArray* children = [item objectForKey:@"children"]; + return [children objectAtIndex:index]; +} + +- (NSInteger)outlineView:(NSOutlineView*)view + numberOfChildrenOfItem:(id)item +{ + if (item == nil) { + item = self.dom_tree; + } + + NSArray* children = [item objectForKey:@"children"]; + return static_cast(children.count); +} + +- (BOOL)outlineView:(NSOutlineView*)view + isItemExpandable:(id)item +{ + NSArray* children = [item objectForKey:@"children"]; + return children.count != 0; +} + +- (NSView*)outlineView:(NSOutlineView*)outline_view + viewForTableColumn:(NSTableColumn*)table_column + item:(id)item +{ + auto* font = [NSFont monospacedSystemFontOfSize:12.0 weight:NSFontWeightRegular]; + auto* bold_font = [NSFont monospacedSystemFontOfSize:12.0 weight:NSFontWeightBold]; + + auto attributed_text = [&](NSString* text, NSColor* color = nil, BOOL bold = false) { + auto* attributes = [[NSMutableDictionary alloc] initWithDictionary:@{ + NSFontAttributeName : bold ? bold_font : font, + }]; + + if (color != nil) { + [attributes setObject:color forKey:NSForegroundColorAttributeName]; + } + + return [[NSMutableAttributedString alloc] initWithString:text attributes:attributes]; + }; + + NSString* type = [item objectForKey:@"type"]; + NSMutableAttributedString* text = nil; + + if ([type isEqualToString:@"text"]) { + text = attributed_text([[item objectForKey:@"text"] stringByCollapsingConsecutiveWhitespace]); + } else if ([type isEqualToString:@"comment"]) { + auto* comment = [NSString stringWithFormat:@"", [item objectForKey:@"data"]]; + text = attributed_text(comment, [NSColor systemGreenColor]); + } else if ([type isEqualToString:@"shadow-root"]) { + auto* shadow = [NSString stringWithFormat:@"%@ (%@)", [item objectForKey:@"name"], [item objectForKey:@"mode"]]; + text = attributed_text(shadow, [NSColor systemGrayColor]); + } else if ([type isEqualToString:@"element"]) { + text = attributed_text(@"<"); + + auto* element = attributed_text( + [[item objectForKey:@"name"] lowercaseString], + [NSColor systemPinkColor], + YES); + [text appendAttributedString:element]; + + NSDictionary* attributes = [item objectForKey:@"attributes"]; + + [attributes enumerateKeysAndObjectsUsingBlock:^(id name, id value, BOOL*) { + [text appendAttributedString:attributed_text(@" ")]; + + name = attributed_text(name, [NSColor systemOrangeColor]); + [text appendAttributedString:name]; + + [text appendAttributedString:attributed_text(@"=")]; + + value = [NSString stringWithFormat:@"\"%@\"", [value stringByCollapsingConsecutiveWhitespace]]; + value = attributed_text(value, [NSColor systemCyanColor]); + [text appendAttributedString:value]; + }]; + + [text appendAttributedString:attributed_text(@">")]; + } else { + text = attributed_text([item objectForKey:@"name"], [NSColor systemGrayColor]); + } + + auto* view = [NSTextField labelWithAttributedString:text]; + view.identifier = [NSString stringWithFormat:@"%@", [item objectForKey:@"id"]]; + + return view; +} + +#pragma mark - NSOutlineViewDelegate + +- (BOOL)outlineView:(NSOutlineView*)outline_view + shouldEditTableColumn:(NSTableColumn*)table_column + item:(id)item +{ + return NO; +} + +@end diff --git a/Ladybird/AppKit/UI/InspectorController.h b/Ladybird/AppKit/UI/InspectorController.h new file mode 100644 index 0000000000..099e05974d --- /dev/null +++ b/Ladybird/AppKit/UI/InspectorController.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@class Tab; + +@interface InspectorController : NSWindowController + +- (instancetype)init:(Tab*)tab; + +@end diff --git a/Ladybird/AppKit/UI/InspectorController.mm b/Ladybird/AppKit/UI/InspectorController.mm new file mode 100644 index 0000000000..9520c7ab63 --- /dev/null +++ b/Ladybird/AppKit/UI/InspectorController.mm @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#import +#import +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +@interface InspectorController () + +@property (nonatomic, strong) Tab* tab; + +@end + +@implementation InspectorController + +- (instancetype)init:(Tab*)tab +{ + if (self = [super init]) { + self.tab = tab; + } + + return self; +} + +#pragma mark - Private methods + +- (Inspector*)inspector +{ + return (Inspector*)[self window]; +} + +#pragma mark - NSWindowController + +- (IBAction)showWindow:(id)sender +{ + self.window = [[Inspector alloc] init:self.tab]; + [self.window setDelegate:self]; + [self.window makeKeyAndOrderFront:sender]; + + [[self inspector] inspect]; +} + +#pragma mark - NSWindowDelegate + +- (void)windowWillClose:(NSNotification*)notification +{ + [self.tab onInspectorClosed]; +} + +@end diff --git a/Ladybird/AppKit/UI/LadybirdWebView.h b/Ladybird/AppKit/UI/LadybirdWebView.h index da7661be44..f839ee7012 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.h +++ b/Ladybird/AppKit/UI/LadybirdWebView.h @@ -25,6 +25,7 @@ - (void)loadURL:(URL const&)url; - (void)onLoadStart:(URL const&)url isRedirect:(BOOL)is_redirect; +- (void)onLoadFinish:(URL const&)url; - (void)onTitleChange:(DeprecatedString const&)title; - (void)onFaviconChange:(Gfx::Bitmap const&)bitmap; diff --git a/Ladybird/AppKit/UI/LadybirdWebView.mm b/Ladybird/AppKit/UI/LadybirdWebView.mm index 303490c531..bba79d03e4 100644 --- a/Ladybird/AppKit/UI/LadybirdWebView.mm +++ b/Ladybird/AppKit/UI/LadybirdWebView.mm @@ -227,6 +227,10 @@ struct HideCursor { } }; + m_web_view_bridge->on_load_finish = [self](auto const& url) { + [self.observer onLoadFinish:url]; + }; + m_web_view_bridge->on_title_change = [self](auto const& title) { [self.observer onTitleChange:title]; }; diff --git a/Ladybird/AppKit/UI/Tab.h b/Ladybird/AppKit/UI/Tab.h index d60e85892b..559a5b84cd 100644 --- a/Ladybird/AppKit/UI/Tab.h +++ b/Ladybird/AppKit/UI/Tab.h @@ -17,6 +17,9 @@ - (void)openConsole:(id)sender; - (void)onConsoleClosed; +- (void)openInspector:(id)sender; +- (void)onInspectorClosed; + @property (nonatomic, strong) LadybirdWebView* web_view; @end diff --git a/Ladybird/AppKit/UI/Tab.mm b/Ladybird/AppKit/UI/Tab.mm index 53b9abb4b0..61fabf617c 100644 --- a/Ladybird/AppKit/UI/Tab.mm +++ b/Ladybird/AppKit/UI/Tab.mm @@ -14,6 +14,8 @@ #import #import #import +#import +#import #import #import #import @@ -32,6 +34,7 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; @property (nonatomic, strong) NSImage* favicon; @property (nonatomic, strong) ConsoleController* console_controller; +@property (nonatomic, strong) InspectorController* inspector_controller; @end @@ -109,6 +112,9 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; if (self.console_controller != nil) { [self.console_controller.window close]; } + if (self.inspector_controller != nil) { + [self.inspector_controller.window close]; + } } - (void)openConsole:(id)sender @@ -127,6 +133,22 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; self.console_controller = nil; } +- (void)openInspector:(id)sender +{ + if (self.inspector_controller != nil) { + [self.inspector_controller.window makeKeyAndOrderFront:sender]; + return; + } + + self.inspector_controller = [[InspectorController alloc] init:self]; + [self.inspector_controller showWindow:nil]; +} + +- (void)onInspectorClosed +{ + self.inspector_controller = nil; +} + #pragma mark - Private methods - (TabController*)tabController @@ -222,6 +244,18 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800; auto* console = (Console*)[self.console_controller window]; [console reset]; } + if (self.inspector_controller != nil) { + auto* inspector = (Inspector*)[self.inspector_controller window]; + [inspector reset]; + } +} + +- (void)onLoadFinish:(URL const&)url +{ + if (self.inspector_controller != nil) { + auto* inspector = (Inspector*)[self.inspector_controller window]; + [inspector inspect]; + } } - (void)onTitleChange:(DeprecatedString const&)title diff --git a/Ladybird/CMakeLists.txt b/Ladybird/CMakeLists.txt index f2b781eb42..71b4a26e4e 100644 --- a/Ladybird/CMakeLists.txt +++ b/Ladybird/CMakeLists.txt @@ -136,6 +136,8 @@ elseif (APPLE) AppKit/UI/Console.mm AppKit/UI/ConsoleController.mm AppKit/UI/Event.mm + AppKit/UI/Inspector.mm + AppKit/UI/InspectorController.mm AppKit/UI/LadybirdWebView.mm AppKit/UI/LadybirdWebViewBridge.cpp AppKit/UI/Palette.mm