diff --git a/UI/AppKit/Application/ApplicationDelegate.mm b/UI/AppKit/Application/ApplicationDelegate.mm index f4ddc5e7566..cc1a21518b0 100644 --- a/UI/AppKit/Application/ApplicationDelegate.mm +++ b/UI/AppKit/Application/ApplicationDelegate.mm @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024, Tim Flynn + * Copyright (c) 2023-2025, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,6 +9,7 @@ #include #import +#import #import #import #import @@ -42,8 +43,12 @@ @property (nonatomic, strong) NSMutableArray* managed_tabs; @property (nonatomic, weak) Tab* active_tab; +@property (nonatomic, strong) InfoBar* info_bar; + @property (nonatomic, strong) TaskManagerController* task_manager_controller; +@property (nonatomic, strong) NSMenuItem* toggle_devtools_menu_item; + - (NSMenuItem*)createApplicationMenu; - (NSMenuItem*)createFileMenu; - (NSMenuItem*)createEditMenu; @@ -133,6 +138,10 @@ - (void)setActiveTab:(Tab*)tab { self.active_tab = tab; + + if (self.info_bar) { + [self.info_bar tabBecameActive:self.active_tab]; + } } - (Tab*)activeTab @@ -237,6 +246,57 @@ [current_window close]; } +- (void)toggleDevToolsEnabled:(id)sender +{ + if (auto result = WebView::Application::the().toggle_devtools_enabled(); result.is_error()) { + auto error_message = MUST(String::formatted("Unable to start DevTools: {}", result.error())); + + auto* dialog = [[NSAlert alloc] init]; + [dialog setMessageText:Ladybird::string_to_ns_string(error_message)]; + + [dialog beginSheetModalForWindow:self.active_tab + completionHandler:nil]; + } else { + switch (result.value()) { + case WebView::Application::DevtoolsState::Disabled: + [self devtoolsDisabled]; + break; + case WebView::Application::DevtoolsState::Enabled: + [self devtoolsEnabled]; + break; + } + } +} + +- (void)devtoolsDisabled +{ + [self.toggle_devtools_menu_item setTitle:@"Enable DevTools"]; + + if (self.info_bar) { + [self.info_bar hide]; + self.info_bar = nil; + } +} + +- (void)devtoolsEnabled +{ + [self.toggle_devtools_menu_item setTitle:@"Disable DevTools"]; + + if (!self.info_bar) { + self.info_bar = [[InfoBar alloc] init]; + } + + auto message = MUST(String::formatted("DevTools is enabled on port {}", WebView::Application::chrome_options().devtools_port)); + + [self.info_bar showWithMessage:Ladybird::string_to_ns_string(message) + dismissButtonTooltip:@"Disable DevTools" + dismissButtonClicked:^{ + MUST(WebView::Application::the().toggle_devtools_enabled()); + [self devtoolsDisabled]; + } + activeTab:self.active_tab]; +} + - (void)openTaskManager:(id)sender { if (self.task_manager_controller != nil) { @@ -608,6 +668,12 @@ [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"View Source" action:@selector(viewSource:) keyEquivalent:@""]]; + + self.toggle_devtools_menu_item = [[NSMenuItem alloc] initWithTitle:@"Enable DevTools" + action:@selector(toggleDevToolsEnabled:) + keyEquivalent:@"I"]; + [submenu addItem:self.toggle_devtools_menu_item]; + [submenu addItem:[[NSMenuItem alloc] initWithTitle:@"Open Task Manager" action:@selector(openTaskManager:) keyEquivalent:@"M"]]; diff --git a/UI/AppKit/CMakeLists.txt b/UI/AppKit/CMakeLists.txt index 5e6fc6ee94b..b3d07cc40b1 100644 --- a/UI/AppKit/CMakeLists.txt +++ b/UI/AppKit/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(ladybird_impl STATIC Application/Application.mm Application/ApplicationDelegate.mm Interface/Event.mm + Interface/InfoBar.mm Interface/LadybirdWebView.mm Interface/LadybirdWebViewBridge.cpp Interface/LadybirdWebViewWindow.mm diff --git a/UI/AppKit/Interface/InfoBar.h b/UI/AppKit/Interface/InfoBar.h new file mode 100644 index 00000000000..12ef1619339 --- /dev/null +++ b/UI/AppKit/Interface/InfoBar.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#import + +@class Tab; + +using InfoBarDismissed = void (^)(void); + +@interface InfoBar : NSStackView + +- (void)showWithMessage:(NSString*)message + dismissButtonTooltip:(NSString*)tooltip + dismissButtonClicked:(InfoBarDismissed)on_dimissed + activeTab:(Tab*)tab; +- (void)hide; + +- (void)tabBecameActive:(Tab*)tab; + +@end diff --git a/UI/AppKit/Interface/InfoBar.mm b/UI/AppKit/Interface/InfoBar.mm new file mode 100644 index 00000000000..194e2b2fb3c --- /dev/null +++ b/UI/AppKit/Interface/InfoBar.mm @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025, Tim Flynn + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#import +#import + +#if !__has_feature(objc_arc) +# error "This project requires ARC" +#endif + +static constexpr CGFloat const INFO_BAR_HEIGHT = 40; + +@interface InfoBar () + +@property (nonatomic, strong) NSTextField* text_label; +@property (nonatomic, strong) NSButton* dismiss_button; +@property (nonatomic, copy) InfoBarDismissed on_dimissed; + +@end + +@implementation InfoBar + +- (instancetype)init +{ + if (self = [super init]) { + self.text_label = [NSTextField labelWithString:@""]; + + self.dismiss_button = [NSButton buttonWithImage:[NSImage imageNamed:NSImageNameStopProgressTemplate] + target:self + action:@selector(dismiss:)]; + [self.dismiss_button setBezelStyle:NSBezelStyleAccessoryBarAction]; + + [self addView:self.text_label inGravity:NSStackViewGravityLeading]; + [self addView:self.dismiss_button inGravity:NSStackViewGravityTrailing]; + + [self setOrientation:NSUserInterfaceLayoutOrientationHorizontal]; + [self setEdgeInsets:NSEdgeInsets { 0, 8, 0, 8 }]; + + [[self heightAnchor] constraintEqualToConstant:INFO_BAR_HEIGHT].active = YES; + } + + return self; +} + +- (void)showWithMessage:(NSString*)message + dismissButtonTooltip:(NSString*)tooltip + dismissButtonClicked:(InfoBarDismissed)on_dimissed + activeTab:(Tab*)tab +{ + [self.text_label setStringValue:message]; + + [self.dismiss_button setToolTip:tooltip]; + self.on_dimissed = on_dimissed; + + if (tab) { + [self attachToTab:tab]; + } + + [self setHidden:NO]; +} + +- (void)dismiss:(id)sender +{ + if (self.on_dimissed) { + self.on_dimissed(); + } + + [self hide]; +} + +- (void)hide +{ + [self removeFromSuperview]; + [self setHidden:YES]; +} + +- (void)tabBecameActive:(Tab*)tab +{ + if (![self isHidden]) { + [self attachToTab:tab]; + } +} + +- (void)attachToTab:(Tab*)tab +{ + [self removeFromSuperview]; + + [tab.contentView addView:self inGravity:NSStackViewGravityTrailing]; + [[self leadingAnchor] constraintEqualToAnchor:[tab.contentView leadingAnchor]].active = YES; +} + +@end