blob: d3d4c687accf5af452d7988e7b8cc4776cf2ebaa [file] [log] [blame]
// Copyright 2023 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_web_contents_view_delegate.h"
#import <UIKit/UIKit.h>
#include <memory>
#include "base/apple/foundation_util.h"
#include "base/command_line.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "build/build_config.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/shell/browser/shell_web_contents_view_delegate_creator.h"
#include "content/shell/common/shell_switches.h"
#include "third_party/blink/public/common/context_menu_data/edit_flags.h"
enum {
ShellContextMenuItemCutTag = 0,
ShellContextMenuItemCopyTag,
ShellContextMenuItemCopyLinkTag,
ShellContextMenuItemPasteTag,
ShellContextMenuItemDeleteTag,
ShellContextMenuItemOpenLinkTag
};
// 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;
@end
@implementation ContextMenuHiddenButton {
content::ContextMenuParams _params;
base::WeakPtr<content::WebContents> _webContents;
}
+ (instancetype)buttonWithFrame:(CGRect)frame
contextMenuParams:(content::ContextMenuParams)params
forWebContents:(content::WebContents*)webContents {
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();
return button;
}
- (UIContextMenuConfiguration*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
configurationForMenuAtLocation:(CGPoint)location {
UIContextMenuConfiguration* config = [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu* _Nullable(
NSArray<UIMenuElement*>* _Nonnull suggestedActions) {
return [self buildContextMenuItems];
}];
[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,
_params.impression);
}
}
- (UIAction*)makeMenuItem:(NSString*)title menuTag:(NSInteger)tag {
auto menuActionHandler = ^(UIAction* action) {
switch (tag) {
case ShellContextMenuItemCutTag:
self->_webContents->Cut();
break;
case ShellContextMenuItemCopyTag:
self->_webContents->Copy();
break;
#if BUILDFLAG(IS_IOS_TVOS)
TVOS_NOT_YET_IMPLEMENTED();
#else
case ShellContextMenuItemCopyLinkTag: {
UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
pasteboard.string = [NSString
stringWithUTF8String:self->_params.link_url.spec().c_str()];
break;
}
#endif
case ShellContextMenuItemPasteTag:
self->_webContents->Paste();
break;
case ShellContextMenuItemDeleteTag:
self->_webContents->Delete();
break;
case ShellContextMenuItemOpenLinkTag: {
content::NavigationController::LoadURLParams params(
self->_params.link_url);
self->_webContents->GetController().LoadURLWithParams(params);
break;
}
}
};
UIAction* menu = [UIAction actionWithTitle:title
image:nil
identifier:nil
handler:menuActionHandler];
return menu;
}
- (UIMenu*)buildContextMenuItems {
bool hasLink = !_params.unfiltered_link_url.is_empty();
bool hasSelection = !_params.selection_text.empty();
bool isEditable = _params.is_editable;
NSMutableArray* menuItems = [[NSMutableArray alloc] init];
if (hasLink) {
[menuItems addObject:[self makeMenuItem:@"Go to the Link"
menuTag:ShellContextMenuItemOpenLinkTag]];
#if BUILDFLAG(IS_IOS_TVOS)
TVOS_NOT_YET_IMPLEMENTED();
#else
[menuItems addObject:[self makeMenuItem:@"Copy Link"
menuTag:ShellContextMenuItemCopyLinkTag]];
#endif
}
if (isEditable) {
if (_params.edit_flags & blink::ContextMenuDataEditFlags::kCanCut) {
[menuItems addObject:[self makeMenuItem:@"Cut"
menuTag:ShellContextMenuItemCutTag]];
}
if (_params.edit_flags & blink::ContextMenuDataEditFlags::kCanCopy) {
[menuItems addObject:[self makeMenuItem:@"Copy"
menuTag:ShellContextMenuItemCopyTag]];
}
if (_params.edit_flags & blink::ContextMenuDataEditFlags::kCanPaste) {
[menuItems addObject:[self makeMenuItem:@"Paste"
menuTag:ShellContextMenuItemPasteTag]];
}
if (_params.edit_flags & blink::ContextMenuDataEditFlags::kCanDelete) {
[menuItems addObject:[self makeMenuItem:@"Delete"
menuTag:ShellContextMenuItemDeleteTag]];
}
} else if (hasSelection) {
[menuItems addObject:[self makeMenuItem:@"Copy"
menuTag:ShellContextMenuItemCopyTag]];
}
NSString* title =
hasLink ? [NSString
stringWithUTF8String:self->_params.link_url.spec().c_str()]
: @"";
return [UIMenu menuWithTitle:title children:menuItems];
}
@end
namespace content {
namespace {
gfx::NativeView GetContentNativeView(WebContents* web_contents) {
RenderWidgetHostView* rwhv = web_contents->GetRenderWidgetHostView();
if (!rwhv) {
return gfx::NativeView();
}
return rwhv->GetNativeView();
}
} // namespace
class ShellWebContentsUIButtonHolder {
public:
UIButton* __strong button_;
};
std::unique_ptr<WebContentsViewDelegate> CreateShellWebContentsViewDelegate(
WebContents* web_contents) {
return std::make_unique<ShellWebContentsViewDelegate>(web_contents);
}
ShellWebContentsViewDelegate::ShellWebContentsViewDelegate(
WebContents* web_contents)
: web_contents_(web_contents) {
DCHECK(web_contents_); // Avoids 'unused private field' build error.
hidden_button_ = std::make_unique<ShellWebContentsUIButtonHolder>();
}
ShellWebContentsViewDelegate::~ShellWebContentsViewDelegate() {}
void ShellWebContentsViewDelegate::ShowContextMenu(
RenderFrameHost& render_frame_host,
const ContextMenuParams& params) {
if (switches::IsRunWebTestsSwitchPresent()) {
return;
}
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_];
[view addSubview:hidden_button_->button_];
[hidden_button_->button_ performPrimaryAction];
}
} // namespace content