chromium/ios/web/content/ui/content_context_menu_controller.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/web/content/ui/content_context_menu_controller.h"

#import <UIKit/UIKit.h>

#import "base/apple/foundation_util.h"
#import "content/public/browser/context_menu_params.h"
#import "content/public/browser/render_widget_host_view.h"
#import "content/public/browser/web_contents.h"
#import "ios/web/content/web_state/content_web_state.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/web_state_delegate.h"
#import "services/network/public/mojom/referrer_policy.mojom.h"

// A hidden button used only for creating context menus. The only way to
// programmatically trigger a context menu on iOS is to trigger the primary
// action of a button that shows a context menu as its primary action.
@interface ContextMenuHiddenButton : UIButton

// The frame determines the position at which the context menu is shown.
+ (instancetype)buttonWithFrame:(CGRect)frame
              contextMenuParams:(content::ContextMenuParams)params
                 forWebContents:(content::WebContents*)webContents
                        forView:(UIView*)view;
@end

@implementation ContextMenuHiddenButton {
  content::ContextMenuParams _params;
  base::WeakPtr<content::WebContents> _webContents;
  UIView* _view;
}

+ (instancetype)buttonWithFrame:(CGRect)frame
              contextMenuParams:(content::ContextMenuParams)params
                 forWebContents:(content::WebContents*)webContents
                        forView:(UIView*)view {
  ContextMenuHiddenButton* button =
      [ContextMenuHiddenButton buttonWithType:UIButtonTypeSystem];
  button.hidden = YES;
  button.userInteractionEnabled = NO;
  button.contextMenuInteractionEnabled = YES;
  button.showsMenuAsPrimaryAction = YES;
  button.frame = frame;
  button.layer.zPosition = CGFLOAT_MIN;
  button->_params = params;
  button->_webContents = webContents->GetWeakPtr();
  button->_view = view;
  return button;
}

#pragma mark - Private

- (NSString*)convertToNSString:(const std::u16string&)string {
  return [[NSString alloc] initWithBytes:string.data()
                                  length:string.size() * sizeof(char16_t)
                                encoding:NSUTF16LittleEndianStringEncoding];
}

- (web::ReferrerPolicy)convertToWebReferrerPolicy:
    (network::mojom::ReferrerPolicy)policy {
  switch (policy) {
    case network::mojom::ReferrerPolicy::kAlways:
      return web::ReferrerPolicy::ReferrerPolicyAlways;
    case network::mojom::ReferrerPolicy::kDefault:
      return web::ReferrerPolicy::ReferrerPolicyDefault;
    case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade:
      return web::ReferrerPolicy::ReferrerPolicyNoReferrerWhenDowngrade;
    case network::mojom::ReferrerPolicy::kNever:
      return web::ReferrerPolicy::ReferrerPolicyNever;
    case network::mojom::ReferrerPolicy::kOrigin:
      return web::ReferrerPolicy::ReferrerPolicyOrigin;
    case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin:
      return web::ReferrerPolicy::ReferrerPolicyOriginWhenCrossOrigin;
    case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin:
      return web::ReferrerPolicy::ReferrerPolicyStrictOriginWhenCrossOrigin;
    case network::mojom::ReferrerPolicy::kSameOrigin:
      return web::ReferrerPolicy::ReferrerPolicySameOrigin;
    case network::mojom::ReferrerPolicy::kStrictOrigin:
      return web::ReferrerPolicy::ReferrerPolicyStrictOrigin;
  }
  NOTREACHED_IN_MIGRATION();
  return web::ReferrerPolicy::ReferrerPolicyDefault;
}

- (web::ContextMenuParams)webContextMenuParams {
  // TODO(crbug.com/333767962): The 'title_attribute' is intentionally not set
  // to match the Chrome on WebKit behavior that the link URL is displayed at
  // the top of the context menu.
  web::ContextMenuParams web_params;
  web_params.is_main_frame = !_params.is_subframe;
  web_params.link_url = _params.link_url;
  web_params.referrer_policy =
      [self convertToWebReferrerPolicy:_params.referrer_policy];
  web_params.src_url = _params.src_url;
  web_params.view = _view;
  web_params.location.x = _params.x;
  web_params.location.y = _params.y;
  web_params.text = [self convertToNSString:_params.link_text];
  web_params.alt_text = [self convertToNSString:_params.alt_text];
  return web_params;
}

- (UIContextMenuConfiguration*)contextMenuInteraction:
                                   (UIContextMenuInteraction*)interaction
                       configurationForMenuAtLocation:(CGPoint)location {
  web::ContentWebState* web_state =
      static_cast<web::ContentWebState*>(_webContents->GetDelegate());
  DCHECK(web_state);

  __block UIContextMenuConfiguration* config = nil;
  if (web_state && web_state->GetDelegate()) {
    web_state->GetDelegate()->ContextMenuConfiguration(
        web_state, [self webContextMenuParams],
        ^(UIContextMenuConfiguration* conf) {
          config = conf;
        });
  }

  [super contextMenuInteraction:interaction
      configurationForMenuAtLocation:location];
  return config;
}

- (void)contextMenuInteraction:(UIContextMenuInteraction*)interaction
       willEndForConfiguration:(UIContextMenuConfiguration*)configuration
                      animator:(id<UIContextMenuInteractionAnimating>)animator {
  [super contextMenuInteraction:interaction
        willEndForConfiguration:configuration
                       animator:animator];
  if (_webContents) {
    _webContents->NotifyContextMenuClosed(_params.link_followed);
  }
}

@end

namespace {

gfx::NativeView GetContentNativeView(content::WebContents* web_contents) {
  content::RenderWidgetHostView* rwhv = web_contents->GetRenderWidgetHostView();
  if (!rwhv) {
    return gfx::NativeView();
  }
  return rwhv->GetNativeView();
}

}  // namespace

class IOSWebContentsUIButtonHolder {
 public:
  UIButton* __strong button_;
};

ContentContextMenuController::ContentContextMenuController() {
  hidden_button_ = std::make_unique<IOSWebContentsUIButtonHolder>();
}

ContentContextMenuController::~ContentContextMenuController() = default;

void ContentContextMenuController::ShowContextMenu(
    content::RenderFrameHost& render_frame_host,
    const content::ContextMenuParams& params) {
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(&render_frame_host);

  UIView* view = base::apple::ObjCCastStrict<UIView>(
      GetContentNativeView(web_contents).Get());
  CGRect frame = CGRectMake(params.x, params.y, 0, 0);

  [hidden_button_->button_ removeFromSuperview];
  hidden_button_->button_ =
      [ContextMenuHiddenButton buttonWithFrame:frame
                             contextMenuParams:params
                                forWebContents:web_contents
                                       forView:view];
  [view addSubview:hidden_button_->button_];
  [hidden_button_->button_ performPrimaryAction];
}