mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-17 16:28:06 +03:00
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).
This commit is contained in:
parent
33b006f157
commit
4483204c9c
Notes:
sideshowbarker
2024-07-18 02:47:59 +09:00
Author: https://github.com/trflynn89 Commit: https://github.com/SerenityOS/serenity/commit/4483204c9c Pull-request: https://github.com/SerenityOS/serenity/pull/21063 Reviewed-by: https://github.com/ADKaster Reviewed-by: https://github.com/bugaevc
@ -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;
|
||||
|
21
Ladybird/AppKit/UI/Inspector.h
Normal file
21
Ladybird/AppKit/UI/Inspector.h
Normal file
@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <System/Cocoa.h>
|
||||
|
||||
@class LadybirdWebView;
|
||||
@class Tab;
|
||||
|
||||
@interface Inspector : NSWindow
|
||||
|
||||
- (instancetype)init:(Tab*)tab;
|
||||
|
||||
- (void)inspect;
|
||||
- (void)reset;
|
||||
|
||||
@end
|
257
Ladybird/AppKit/UI/Inspector.mm
Normal file
257
Ladybird/AppKit/UI/Inspector.mm
Normal file
@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/DeprecatedString.h>
|
||||
#include <LibWebView/ViewImplementation.h>
|
||||
|
||||
#import <UI/Inspector.h>
|
||||
#import <UI/LadybirdWebView.h>
|
||||
#import <UI/Tab.h>
|
||||
#import <Utilities/Conversions.h>
|
||||
#import <Utilities/NSString+Ladybird.h>
|
||||
|
||||
#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 () <NSOutlineViewDataSource, NSOutlineViewDelegate>
|
||||
|
||||
@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<NSInteger>(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
|
17
Ladybird/AppKit/UI/InspectorController.h
Normal file
17
Ladybird/AppKit/UI/InspectorController.h
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#import <System/Cocoa.h>
|
||||
|
||||
@class Tab;
|
||||
|
||||
@interface InspectorController : NSWindowController
|
||||
|
||||
- (instancetype)init:(Tab*)tab;
|
||||
|
||||
@end
|
58
Ladybird/AppKit/UI/InspectorController.mm
Normal file
58
Ladybird/AppKit/UI/InspectorController.mm
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#import <UI/Inspector.h>
|
||||
#import <UI/InspectorController.h>
|
||||
#import <UI/LadybirdWebView.h>
|
||||
#import <UI/Tab.h>
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
# error "This project requires ARC"
|
||||
#endif
|
||||
|
||||
@interface InspectorController () <NSWindowDelegate>
|
||||
|
||||
@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
|
@ -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;
|
||||
|
@ -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];
|
||||
};
|
||||
|
@ -17,6 +17,9 @@
|
||||
- (void)openConsole:(id)sender;
|
||||
- (void)onConsoleClosed;
|
||||
|
||||
- (void)openInspector:(id)sender;
|
||||
- (void)onInspectorClosed;
|
||||
|
||||
@property (nonatomic, strong) LadybirdWebView* web_view;
|
||||
|
||||
@end
|
||||
|
@ -14,6 +14,8 @@
|
||||
#import <Application/ApplicationDelegate.h>
|
||||
#import <UI/Console.h>
|
||||
#import <UI/ConsoleController.h>
|
||||
#import <UI/Inspector.h>
|
||||
#import <UI/InspectorController.h>
|
||||
#import <UI/LadybirdWebView.h>
|
||||
#import <UI/Tab.h>
|
||||
#import <UI/TabController.h>
|
||||
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user