blob: b2c5f9b3af814dbfebb1cc2ec28712f551bcc76a [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// 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"
#include <algorithm>
#import "base/mac/foundation_util.h"
#import "base/mac/scoped_nsobject.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"
#import "ui/base/cocoa/underlay_opengl_hosting_window.h"
#include "url/gurl.h"
// Receives notification that the window is closing so that it can start the
// tear-down process. Is responsible for deleting itself when done.
@interface ContentShellWindowDelegate : NSObject <NSWindowDelegate> {
@private
content::Shell* _shell;
}
- (id)initWithShell:(content::Shell*)shell;
@end
@implementation ContentShellWindowDelegate
- (id)initWithShell:(content::Shell*)shell {
if ((self = [super init])) {
_shell = shell;
}
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 {
NSWindow* window = base::mac::ObjCCastStrict<NSWindow>(sender);
[window autorelease];
// Don't leave a dangling pointer if the window lives beyond
// this method. See crbug.com/719830.
[window setDelegate:nil];
delete _shell;
[self release];
return YES;
}
- (void)performAction:(id)sender {
_shell->ActionPerformed([sender tag]);
}
- (void)takeURLStringValueFrom:(id)sender {
_shell->URLEntered(base::SysNSStringToUTF8([sender stringValue]));
}
@end
@interface CrShellWindow : UnderlayOpenGLHostingWindow {
@private
content::Shell* _shell;
}
- (void)setShell:(content::Shell*)shell;
- (void)showDevTools:(id)sender;
@end
@implementation CrShellWindow
- (void)setShell:(content::Shell*)shell {
_shell = shell;
}
- (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,
NSView* target,
NSString* key,
NSUInteger modifier) {
base::scoped_nsobject<NSButton> button(
[[NSButton alloc] initWithFrame:*rect]);
[button setTitle:title];
[button setBezelStyle:NSSmallSquareBezelStyle];
[button setAutoresizingMask:(NSViewMaxXMargin | NSViewMinYMargin)];
[button setTarget:target];
[button setAction:@selector(performAction:)];
[button setTag:control];
[button setKeyEquivalent:key];
[button setKeyEquivalentModifierMask:modifier];
[parent addSubview:button];
rect->origin.x += kButtonWidth;
}
} // namespace
namespace content {
struct ShellPlatformDelegate::ShellData {
gfx::NativeWindow window;
NSTextField* url_edit_view = nullptr;
};
struct ShellPlatformDelegate::PlatformData {};
ShellPlatformDelegate::ShellPlatformDelegate() = default;
ShellPlatformDelegate::~ShellPlatformDelegate() = default;
void ShellPlatformDelegate::Initialize(const gfx::Size& default_window_size) {
// |platform_| is unused on this platform.
}
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 = NSTitledWindowMask | NSClosableWindowMask |
NSMiniaturizableWindowMask | NSResizableWindowMask;
CrShellWindow* window =
[[CrShellWindow alloc] initWithContentRect:content_rect
styleMask:style_mask
backing:NSBackingStoreBuffered
defer:NO];
[window setShell:shell];
[window setTitle: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 setContentMinSize:min_size];
// Set the shell window to participate in Lion Fullscreen mode. Set
// Setting this flag has no effect on Snow Leopard or earlier.
NSUInteger collectionBehavior = [window collectionBehavior];
collectionBehavior |= NSWindowCollectionBehaviorFullScreenPrimary;
[window setCollectionBehavior:collectionBehavior];
// 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).
[window setReleasedWhenClosed:NO];
// Create a window delegate to watch for when it's asked to go away. It will
// clean itself up so we don't need to hold a reference.
ContentShellWindowDelegate* delegate =
[[ContentShellWindowDelegate alloc] initWithShell:shell];
[window setDelegate:delegate];
if (!Shell::ShouldHideToolbar()) {
NSRect button_frame =
NSMakeRect(0, NSMaxY(initial_window_bounds) - kURLBarHeight,
kButtonWidth, kURLBarHeight);
MakeShellButton(&button_frame, @"Back", content, IDC_NAV_BACK,
(NSView*)delegate, @"[", NSCommandKeyMask);
MakeShellButton(&button_frame, @"Forward", content, IDC_NAV_FORWARD,
(NSView*)delegate, @"]", NSCommandKeyMask);
MakeShellButton(&button_frame, @"Reload", content, IDC_NAV_RELOAD,
(NSView*)delegate, @"r", NSCommandKeyMask);
MakeShellButton(&button_frame, @"Stop", content, IDC_NAV_STOP,
(NSView*)delegate, @".", NSCommandKeyMask);
button_frame.size.width =
NSWidth(initial_window_bounds) - NSMinX(button_frame);
base::scoped_nsobject<NSTextField> url_edit_view(
[[NSTextField alloc] initWithFrame:button_frame]);
[content addSubview:url_edit_view];
[url_edit_view setAutoresizingMask:(NSViewWidthSizable | NSViewMinYMargin)];
[url_edit_view setTarget:delegate];
[url_edit_view setAction:@selector(takeURLStringValueFrom:)];
[[url_edit_view cell] setWraps:NO];
[[url_edit_view cell] setScrollable:YES];
shell_data.url_edit_view = url_edit_view.get();
}
// Show the new window.
[window makeKeyAndOrderFront:nil];
shell_data.window = window;
}
gfx::NativeWindow ShellPlatformDelegate::GetNativeWindow(Shell* shell) {
DCHECK(base::Contains(shell_data_map_, shell));
ShellData& shell_data = shell_data_map_[shell];
return shell_data.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 setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)];
NSView* content = [shell_data.window.GetNativeNSWindow() contentView];
[content addSubview:web_view];
NSRect frame = [content bounds];
if (!Shell::ShouldHideToolbar())
frame.size.height -= kURLBarHeight;
[web_view setFrame:frame];
[web_view setNeedsDisplay: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.window.GetNativeNSWindow().contentView setFrame: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.window.GetNativeNSWindow() 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 setStringValue: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];
NSString* title_string = base::SysUTF16ToNSString(title);
[shell_data.window.GetNativeNSWindow() setTitle:title_string];
}
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.window.GetNativeNSWindow() 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.window.GetNativeNSWindow() 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::DidNavigateMainFramePostCommit(
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.
if ([event.os_event type] == NSKeyDown) {
if (([event.os_event modifierFlags] & NSCommandKeyMask) &&
[[event.os_event characters] isEqual:@"l"]) {
[shell_data.window.GetNativeNSWindow()
makeFirstResponder:shell_data.url_edit_view];
return true;
}
[[NSApp mainMenu] performKeyEquivalent:event.os_event];
return true;
}
return false;
}
} // namespace content