| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/shell/browser/shell_platform_delegate.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include <algorithm> |
| |
| #import "base/apple/foundation_util.h" |
| #include "base/check_op.h" |
| #include "base/containers/contains.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "content/public/browser/native_web_keyboard_event.h" |
| #include "content/public/browser/render_frame_host.h" |
| #include "content/public/browser/render_widget_host.h" |
| #include "content/public/browser/render_widget_host_view.h" |
| #include "content/public/browser/web_contents.h" |
| #include "content/shell/app/resource.h" |
| #include "content/shell/browser/shell.h" |
| #include "url/gurl.h" |
| |
| // Receives notification that the window is closing so that it can start the |
| // tear-down process. |
| @interface ContentShellWindowDelegate : NSObject <NSWindowDelegate> |
| |
| @property(readonly) NSWindow* window; |
| |
| - (id)initWithShell:(content::Shell*)shell window:(NSWindow*)window; |
| - (void)showDevTools:(id)sender; |
| |
| @end |
| |
| @implementation ContentShellWindowDelegate { |
| raw_ptr<content::Shell> _shell; |
| NSWindow* __strong _window; |
| } |
| |
| @synthesize window = _window; |
| |
| - (id)initWithShell:(content::Shell*)shell window:(NSWindow*)window { |
| if ((self = [super init])) { |
| _shell = shell; |
| _window = window; |
| window.delegate = self; |
| } |
| return self; |
| } |
| |
| // Called when the window is about to close. Perform the self-destruction |
| // sequence by getting rid of the shell and removing it and the window from |
| // the various global lists. By returning YES, we allow the window to be |
| // removed from the screen. |
| - (BOOL)windowShouldClose:(id)sender { |
| CHECK_EQ(base::apple::ObjCCastStrict<NSWindow>(sender), _window); |
| // Don't leave a dangling pointer if the window lives beyond |
| // this method. See crbug.com/719830. |
| _window.delegate = nil; |
| _window = nil; |
| _shell.ClearAndDelete(); |
| |
| return YES; |
| } |
| |
| - (void)performAction:(id)sender { |
| _shell->ActionPerformed([sender tag]); |
| } |
| |
| - (void)takeURLStringValueFrom:(id)sender { |
| _shell->URLEntered(base::SysNSStringToUTF8([sender stringValue])); |
| } |
| |
| - (void)showDevTools:(id)sender { |
| _shell->ShowDevTools(); |
| } |
| |
| @end |
| |
| namespace { |
| |
| NSString* kWindowTitle = @"Content Shell"; |
| |
| // Layout constants (in view coordinates) |
| const CGFloat kButtonWidth = 72; |
| const CGFloat kURLBarHeight = 24; |
| |
| // The minimum size of the window's content (in view coordinates) |
| const CGFloat kMinimumWindowWidth = 400; |
| const CGFloat kMinimumWindowHeight = 300; |
| |
| void MakeShellButton(NSRect* rect, |
| NSString* title, |
| NSView* parent, |
| int control, |
| id target, |
| NSString* key, |
| NSUInteger modifier) { |
| NSButton* button = [[NSButton alloc] initWithFrame:*rect]; |
| button.title = title; |
| button.bezelStyle = NSBezelStyleSmallSquare; |
| button.autoresizingMask = (NSViewMaxXMargin | NSViewMinYMargin); |
| button.target = target; |
| button.action = @selector(performAction:); |
| button.tag = control; |
| button.keyEquivalent = key; |
| button.keyEquivalentModifierMask = modifier; |
| [parent addSubview:button]; |
| rect->origin.x += kButtonWidth; |
| } |
| |
| } // namespace |
| |
| namespace content { |
| |
| struct ShellPlatformDelegate::ShellData { |
| ContentShellWindowDelegate* __strong delegate; |
| NSTextField* __weak url_edit_view; |
| }; |
| |
| struct ShellPlatformDelegate::PlatformData {}; |
| |
| ShellPlatformDelegate::ShellPlatformDelegate() = default; |
| ShellPlatformDelegate::~ShellPlatformDelegate() = default; |
| |
| void ShellPlatformDelegate::Initialize(const gfx::Size& default_window_size) { |
| screen_ = std::make_unique<display::ScopedNativeScreen>(); |
| } |
| |
| void ShellPlatformDelegate::CreatePlatformWindow( |
| Shell* shell, |
| const gfx::Size& initial_size) { |
| DCHECK(!base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| int width = initial_size.width(); |
| int height = initial_size.height(); |
| |
| if (!Shell::ShouldHideToolbar()) { |
| height += kURLBarHeight; |
| } |
| NSRect initial_window_bounds = NSMakeRect(0, 0, width, height); |
| NSRect content_rect = initial_window_bounds; |
| NSUInteger style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | |
| NSWindowStyleMaskMiniaturizable | |
| NSWindowStyleMaskResizable; |
| NSWindow* window = |
| [[NSWindow alloc] initWithContentRect:content_rect |
| styleMask:style_mask |
| backing:NSBackingStoreBuffered |
| defer:NO]; |
| window.title = kWindowTitle; |
| NSView* content = window.contentView; |
| |
| // If the window is allowed to get too small, it will wreck the view bindings. |
| NSSize min_size = NSMakeSize(kMinimumWindowWidth, kMinimumWindowHeight); |
| min_size = [content convertSize:min_size toView:nil]; |
| // Note that this takes window coordinates. |
| window.contentMinSize = min_size; |
| |
| // Set the shell window to participate in fullscreen mode. |
| window.collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary; |
| |
| // Rely on the window delegate to clean us up rather than immediately |
| // releasing when the window gets closed. We use the delegate to do everything |
| // from the autorelease pool so the shell isn't on the stack during cleanup |
| // (ie, a window close from javascript). Also, releasedWhenClosed == YES is |
| // incompatible with ARC. |
| window.releasedWhenClosed = NO; |
| |
| // Create a window delegate to watch for when it's asked to go away. |
| ContentShellWindowDelegate* delegate = |
| [[ContentShellWindowDelegate alloc] initWithShell:shell window:window]; |
| |
| if (!Shell::ShouldHideToolbar()) { |
| NSRect button_frame = |
| NSMakeRect(0, NSMaxY(initial_window_bounds) - kURLBarHeight, |
| kButtonWidth, kURLBarHeight); |
| |
| MakeShellButton(&button_frame, @"Back", content, IDC_NAV_BACK, delegate, |
| @"[", NSEventModifierFlagCommand); |
| MakeShellButton(&button_frame, @"Forward", content, IDC_NAV_FORWARD, |
| delegate, @"]", NSEventModifierFlagCommand); |
| MakeShellButton(&button_frame, @"Reload", content, IDC_NAV_RELOAD, delegate, |
| @"r", NSEventModifierFlagCommand); |
| MakeShellButton(&button_frame, @"Stop", content, IDC_NAV_STOP, delegate, |
| @".", NSEventModifierFlagCommand); |
| |
| button_frame.size.width = |
| NSWidth(initial_window_bounds) - NSMinX(button_frame); |
| NSTextField* url_edit_view = |
| [[NSTextField alloc] initWithFrame:button_frame]; |
| [content addSubview:url_edit_view]; |
| url_edit_view.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin; |
| url_edit_view.target = delegate; |
| url_edit_view.action = @selector(takeURLStringValueFrom:); |
| url_edit_view.cell.wraps = NO; |
| url_edit_view.cell.scrollable = YES; |
| shell_data.url_edit_view = url_edit_view; |
| } |
| |
| // Show the new window. |
| [window makeKeyAndOrderFront:nil]; |
| |
| shell_data.delegate = delegate; |
| } |
| |
| gfx::NativeWindow ShellPlatformDelegate::GetNativeWindow(Shell* shell) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| return shell_data.delegate.window; |
| } |
| |
| void ShellPlatformDelegate::CleanUp(Shell* shell) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| shell_data_map_.erase(shell); |
| } |
| |
| void ShellPlatformDelegate::SetContents(Shell* shell) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| NSView* web_view = shell->web_contents()->GetNativeView().GetNativeNSView(); |
| web_view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; |
| |
| NSWindow* window = shell_data.delegate.window; |
| [window.contentView addSubview:web_view]; |
| |
| NSRect frame = window.contentView.bounds; |
| if (!Shell::ShouldHideToolbar()) { |
| frame.size.height -= kURLBarHeight; |
| } |
| web_view.frame = frame; |
| web_view.needsDisplay = YES; |
| } |
| |
| void ShellPlatformDelegate::ResizeWebContent(Shell* shell, |
| const gfx::Size& content_size) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| int toolbar_height = Shell::ShouldHideToolbar() ? 0 : kURLBarHeight; |
| NSRect frame = NSMakeRect(0, 0, content_size.width(), |
| content_size.height() + toolbar_height); |
| shell_data.delegate.window.contentView.frame = frame; |
| } |
| |
| void ShellPlatformDelegate::EnableUIControl(Shell* shell, |
| UIControl control, |
| bool is_enabled) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| int id; |
| switch (control) { |
| case BACK_BUTTON: |
| id = IDC_NAV_BACK; |
| break; |
| case FORWARD_BUTTON: |
| id = IDC_NAV_FORWARD; |
| break; |
| case STOP_BUTTON: |
| id = IDC_NAV_STOP; |
| break; |
| default: |
| NOTREACHED() << "Unknown UI control"; |
| return; |
| } |
| [[shell_data.delegate.window.contentView viewWithTag:id] |
| setEnabled:is_enabled]; |
| } |
| |
| void ShellPlatformDelegate::SetAddressBarURL(Shell* shell, const GURL& url) { |
| if (Shell::ShouldHideToolbar()) { |
| return; |
| } |
| |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| NSString* url_string = base::SysUTF8ToNSString(url.spec()); |
| shell_data.url_edit_view.stringValue = url_string; |
| } |
| |
| void ShellPlatformDelegate::SetIsLoading(Shell* shell, bool loading) {} |
| |
| void ShellPlatformDelegate::SetTitle(Shell* shell, |
| const std::u16string& title) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| shell_data.delegate.window.title = base::SysUTF16ToNSString(title); |
| } |
| |
| void ShellPlatformDelegate::MainFrameCreated(Shell* shell) {} |
| |
| bool ShellPlatformDelegate::DestroyShell(Shell* shell) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| [shell_data.delegate.window performClose:nil]; |
| return true; // The performClose() will do the destruction of Shell. |
| } |
| |
| void ShellPlatformDelegate::ActivateContents(Shell* shell, |
| WebContents* top_contents) { |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| // This focuses the main frame RenderWidgetHost in the window, but does not |
| // make the window itself active. The WebContentsDelegate (this class) is |
| // responsible for doing both. |
| top_contents->Focus(); |
| // This makes the window the active window for the application, and when the |
| // app is active, the window will be also. That makes all RenderWidgetHosts |
| // for the window active (which is separate from focused on mac). |
| [shell_data.delegate.window makeKeyAndOrderFront:nil]; |
| // This makes the application active so that we can actually move focus |
| // between windows and the renderer can receive focus/blur events. |
| [NSApp activateIgnoringOtherApps:YES]; |
| } |
| |
| void ShellPlatformDelegate::DidNavigatePrimaryMainFramePostCommit( |
| Shell* shell, |
| WebContents* contents) {} |
| |
| bool ShellPlatformDelegate::HandleKeyboardEvent( |
| Shell* shell, |
| WebContents* source, |
| const NativeWebKeyboardEvent& event) { |
| if (event.skip_in_browser || Shell::ShouldHideToolbar()) { |
| return false; |
| } |
| |
| DCHECK(base::Contains(shell_data_map_, shell)); |
| ShellData& shell_data = shell_data_map_[shell]; |
| |
| // The event handling to get this strictly right is a tangle; cheat here a bit |
| // by just letting the menus have a chance at it. |
| NSEvent* ns_event = event.os_event.Get(); |
| if (ns_event.type == NSEventTypeKeyDown) { |
| if ((ns_event.modifierFlags & NSEventModifierFlagCommand) && |
| [ns_event.characters isEqual:@"l"]) { |
| [shell_data.delegate.window makeFirstResponder:shell_data.url_edit_view]; |
| return true; |
| } |
| |
| [NSApp.mainMenu performKeyEquivalent:ns_event]; |
| return true; |
| } |
| return false; |
| } |
| |
| } // namespace content |