chromium/content/shell/browser/shell_platform_delegate_mac.mm

// 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/sys_string_conversions.h"
#include "components/input/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_IN_MIGRATION() << "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 input::NativeWebKeyboardEvent& event) {
  if (event.skip_if_unhandled || 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