Ladybird: Implement a JavaScript console for the AppKit chrome

This adds menu items to open an interactive JavaScript console for a web
page. This more or less mimics the Qt implementation of the console.
Hooks are included to tie the lifetime of the console window with the
tab it belongs to; if the tab is closed, the console window is closed.
This commit is contained in:
Timothy Flynn 2023-08-26 22:43:35 -04:00 committed by Andrew Kaster
parent bf59e06d2a
commit 4d26e4650f
Notes: sideshowbarker 2024-07-17 06:20:50 +09:00
11 changed files with 306 additions and 0 deletions

View file

@ -342,6 +342,9 @@
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"View Source"
action:@selector(viewSource:)
keyEquivalent:@""]];
[submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Console"
action:@selector(openConsole:)
keyEquivalent:@"J"]];
[menu setSubmenu:submenu];
return menu;

View file

@ -0,0 +1,22 @@
/*
* 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 Console : NSWindow
- (instancetype)init:(Tab*)tab;
- (void)reset;
@property (nonatomic, strong) LadybirdWebView* web_view;
@end

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/OwnPtr.h>
#include <LibWebView/ConsoleClient.h>
#import <UI/Console.h>
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#import <Utilities/Conversions.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
static constexpr CGFloat const WINDOW_WIDTH = 520;
static constexpr CGFloat const WINDOW_HEIGHT = 600;
@interface Console () <NSTextFieldDelegate>
{
OwnPtr<WebView::ConsoleClient> m_console_client;
}
@property (nonatomic, strong) Tab* tab;
@property (nonatomic, strong) NSScrollView* scroll_view;
@end
@implementation Console
@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;
self.web_view = [[LadybirdWebView alloc] init:nil];
[self.web_view setPostsBoundsChangedNotifications:YES];
m_console_client = make<WebView::ConsoleClient>([[tab web_view] view], [[self web_view] view]);
self.scroll_view = [[NSScrollView alloc] initWithFrame:[self frame]];
[self.scroll_view setHasVerticalScroller:YES];
[self.scroll_view setHasHorizontalScroller:YES];
[self.scroll_view setLineScroll:24];
[self.scroll_view setContentView:self.web_view];
[self.scroll_view setDocumentView:[[NSView alloc] init]];
auto* font = [NSFont monospacedSystemFontOfSize:12.0
weight:NSFontWeightRegular];
auto* prompt_indicator_attributes = @{
NSForegroundColorAttributeName : [NSColor systemCyanColor],
NSFontAttributeName : font,
};
auto* prompt_indicator_attribute = [[NSAttributedString alloc] initWithString:@">>"
attributes:prompt_indicator_attributes];
auto* prompt_indicator = [NSTextField labelWithAttributedString:prompt_indicator_attribute];
auto* prompt_text = [[NSTextField alloc] init];
[prompt_text setPlaceholderString:@"Enter JavaScript statement"];
[prompt_text setDelegate:self];
[prompt_text setBordered:YES];
[prompt_text setBezeled:YES];
[prompt_text setFont:font];
auto* clear_button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameStopProgressTemplate]
target:self
action:@selector(clearConsole:)];
[clear_button setToolTip:@"Clear the console output"];
auto* controls_stack_view = [NSStackView stackViewWithViews:@[ prompt_indicator, prompt_text, clear_button ]];
[controls_stack_view setOrientation:NSUserInterfaceLayoutOrientationHorizontal];
[controls_stack_view setEdgeInsets:NSEdgeInsetsMake(8, 8, 8, 8)];
auto* content_stack_view = [NSStackView stackViewWithViews:@[ self.scroll_view, controls_stack_view ]];
[content_stack_view setOrientation:NSUserInterfaceLayoutOrientationVertical];
[content_stack_view setSpacing:0];
[self setContentView:content_stack_view];
[self setTitle:@"Console"];
[self setIsVisible:YES];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(onContentScroll:)
name:NSViewBoundsDidChangeNotification
object:[self.scroll_view contentView]];
}
return self;
}
#pragma mark - Public methods
- (void)reset
{
m_console_client->reset();
}
#pragma mark - Private methods
- (void)onContentScroll:(NSNotification*)notification
{
[[self web_view] handleScroll];
}
- (void)clearConsole:(id)sender
{
m_console_client->clear();
}
#pragma mark - NSTextFieldDelegate
- (BOOL)control:(NSControl*)control
textView:(NSTextView*)text_view
doCommandBySelector:(SEL)selector
{
if (selector != @selector(insertNewline:)) {
return NO;
}
auto* ns_script = [[text_view textStorage] string];
auto script = Ladybird::ns_string_to_string(ns_script);
if (!script.bytes_as_string_view().is_whitespace()) {
m_console_client->execute(script);
[text_view setString:@""];
}
return YES;
}
@end

View 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 ConsoleController : NSWindowController
- (instancetype)init:(Tab*)tab;
@end

View file

@ -0,0 +1,63 @@
/*
* Copyright (c) 2023, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import <UI/Console.h>
#import <UI/ConsoleController.h>
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#if !__has_feature(objc_arc)
# error "This project requires ARC"
#endif
@interface ConsoleController () <NSWindowDelegate>
@property (nonatomic, strong) Tab* tab;
@end
@implementation ConsoleController
- (instancetype)init:(Tab*)tab
{
if (self = [super init]) {
self.tab = tab;
}
return self;
}
#pragma mark - Private methods
- (Console*)console
{
return (Console*)[self window];
}
#pragma mark - NSWindowController
- (IBAction)showWindow:(id)sender
{
self.window = [[Console alloc] init:self.tab];
[self.window setDelegate:self];
[self.window makeKeyAndOrderFront:sender];
}
#pragma mark - NSWindowDelegate
- (void)windowWillClose:(NSNotification*)notification
{
[self.tab onConsoleClosed];
}
- (void)windowDidResize:(NSNotification*)notification
{
if (![[self window] inLiveResize]) {
[[[self console] web_view] handleResize];
}
}
@end

View file

@ -10,6 +10,7 @@
#include <LibGfx/Forward.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWebView/Forward.h>
#import <System/Cocoa.h>
@ -41,6 +42,7 @@
- (void)loadURL:(URL const&)url;
- (void)loadHTML:(StringView)html url:(URL const&)url;
- (WebView::ViewImplementation&)view;
- (String const&)handle;
- (void)handleResize;

View file

@ -112,6 +112,11 @@ struct HideCursor {
m_web_view_bridge->load_html(html, url);
}
- (WebView::ViewImplementation&)view
{
return *m_web_view_bridge;
}
- (String const&)handle
{
return m_web_view_bridge->handle();

View file

@ -12,6 +12,11 @@
@interface Tab : NSWindow
- (void)tabWillClose;
- (void)openConsole:(id)sender;
- (void)onConsoleClosed;
@property (nonatomic, strong) LadybirdWebView* web_view;
@end

View file

@ -12,6 +12,8 @@
#include <LibGfx/ShareableBitmap.h>
#import <Application/ApplicationDelegate.h>
#import <UI/Console.h>
#import <UI/ConsoleController.h>
#import <UI/LadybirdWebView.h>
#import <UI/Tab.h>
#import <UI/TabController.h>
@ -29,6 +31,8 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
@property (nonatomic, strong) NSString* title;
@property (nonatomic, strong) NSImage* favicon;
@property (nonatomic, strong) ConsoleController* console_controller;
@end
@implementation Tab
@ -95,6 +99,31 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
return self;
}
#pragma mark - Public methods
- (void)tabWillClose
{
if (self.console_controller != nil) {
[self.console_controller.window close];
}
}
- (void)openConsole:(id)sender
{
if (self.console_controller != nil) {
[self.console_controller.window makeKeyAndOrderFront:sender];
return;
}
self.console_controller = [[ConsoleController alloc] init:self];
[self.console_controller showWindow:nil];
}
- (void)onConsoleClosed
{
self.console_controller = nil;
}
#pragma mark - Private methods
- (TabController*)tabController
@ -185,6 +214,11 @@ static constexpr CGFloat const WINDOW_HEIGHT = 800;
self.title = Ladybird::string_to_ns_string(url.serialize());
self.favicon = [Tab defaultFavicon];
[self updateTabTitleAndFavicon];
if (self.console_controller != nil) {
auto* console = (Console*)[self.console_controller window];
[console reset];
}
}
- (void)onTitleChange:(DeprecatedString const&)title

View file

@ -334,6 +334,8 @@ enum class IsHistoryNavigation {
- (void)windowWillClose:(NSNotification*)notification
{
[[self tab] tabWillClose];
auto* delegate = (ApplicationDelegate*)[NSApp delegate];
[delegate removeTab:self];
}

View file

@ -133,6 +133,8 @@ elseif (APPLE)
AppKit/Application/Application.mm
AppKit/Application/ApplicationDelegate.mm
AppKit/Application/EventLoopImplementation.mm
AppKit/UI/Console.mm
AppKit/UI/ConsoleController.mm
AppKit/UI/Event.mm
AppKit/UI/LadybirdWebView.mm
AppKit/UI/LadybirdWebViewBridge.cpp