// Copyright 2017 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/chrome/browser/location_bar/ui_bundled/location_bar_view_controller.h"
#import "base/containers/contains.h"
#import "base/functional/bind.h"
#import "base/ios/ios_util.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/omnibox/browser/omnibox_field_trial.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "components/prefs/pref_service.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/lens_overlay/coordinator/lens_overlay_availability.h"
#import "ios/chrome/browser/location_bar/ui_bundled/location_bar_constants.h"
#import "ios/chrome/browser/location_bar/ui_bundled/location_bar_steady_view.h"
#import "ios/chrome/browser/orchestrator/ui_bundled/location_bar_offset_provider.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/public/commands/activity_service_commands.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/load_query_commands.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/symbols/symbols.h"
#import "ios/chrome/browser/shared/ui/util/layout_guide_names.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/shared/ui/util/util_swift.h"
#import "ios/chrome/browser/ui/fullscreen/fullscreen_animator.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_constants.h"
#import "ios/chrome/browser/ui/omnibox/text_field_view_containing.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_type.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/pointer_interaction_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/lens/lens_api.h"
#import "ui/base/l10n/l10n_util.h"
using base::UserMetricsAction;
namespace {
typedef NS_ENUM(int, TrailingButtonState) {
kNoButton = 0,
kShareButton,
kVoiceSearchButton,
};
// The size of the symbol image.
const CGFloat kSymbolImagePointSize = 18.;
// Identifier for the omnibox embedded in this location bar as a scribble
// element.
const NSString* kScribbleOmniboxElementId = @"omnibox";
} // namespace
@interface LocationBarViewController () <UIContextMenuInteractionDelegate,
UIIndirectScribbleInteractionDelegate>
// The injected edit view.
@property(nonatomic, strong) UIView<TextFieldViewContaining>* editView;
// The injected text field.
@property(nonatomic, weak) UIView* textField;
// The injected badge view.
@property(nonatomic, strong) UIView* badgeView;
// The injected Contextual Panel entrypoint view;
@property(nonatomic, strong) UIView* contextualPanelEntrypointView;
// The injected placeholder view;
@property(nonatomic, strong) UIView* placeholderView;
// The view that displays current location when the omnibox is not focused.
@property(nonatomic, strong) LocationBarSteadyView* locationBarSteadyView;
@property(nonatomic, assign) TrailingButtonState trailingButtonState;
// When this flag is YES, the share button will not be displayed in situations
// when it normally is shown. Setting it triggers a refresh of the button
// visibility.
@property(nonatomic, assign) BOOL hideShareButtonWhileOnIncognitoNTP;
// Keeps the share button enabled status. This is necessary to preserve the
// state of the share button if it's temporarily replaced by the voice search
// icon (in iPad multitasking).
@property(nonatomic, assign) BOOL shareButtonEnabled;
// Starts voice search, updating the layout guide to be constrained to the
// trailing button.
- (void)startVoiceSearch;
@end
@implementation LocationBarViewController {
BOOL _isNTP;
}
#pragma mark - public
- (instancetype)init {
self = [super init];
if (self) {
_locationBarSteadyView = [[LocationBarSteadyView alloc] init];
}
return self;
}
- (void)setEditView:(UIView<TextFieldViewContaining>*)editView {
DCHECK(!self.editView);
_editView = editView;
_textField = editView.textFieldView;
}
- (void)setBadgeView:(UIView*)badgeView {
DCHECK(!self.badgeView);
_badgeView = badgeView;
}
- (void)setContextualPanelEntrypointView:
(UIView*)contextualPanelEntrypointView {
DCHECK(!self.contextualPanelEntrypointView);
_contextualPanelEntrypointView = contextualPanelEntrypointView;
}
- (void)setPlaceholderView:(UIView*)placeholderView {
CHECK(IsLensOverlayAvailable());
CHECK(!self.placeholderView);
_placeholderView = placeholderView;
}
- (void)switchToEditing:(BOOL)editing {
self.editView.hidden = !editing;
self.locationBarSteadyView.hidden = editing;
}
- (void)setIncognito:(BOOL)incognito {
_incognito = incognito;
self.locationBarSteadyView.colorScheme =
[LocationBarSteadyViewColorScheme standardScheme];
}
- (void)setDispatcher:(id<ActivityServiceCommands,
BrowserCoordinatorCommands,
ApplicationCommands,
LoadQueryCommands,
OmniboxCommands>)dispatcher {
_dispatcher = dispatcher;
}
- (void)setVoiceSearchEnabled:(BOOL)enabled {
if (_voiceSearchEnabled == enabled) {
return;
}
_voiceSearchEnabled = enabled;
[self updateTrailingButtonState];
}
- (void)setHideShareButtonWhileOnIncognitoNTP:(BOOL)hide {
if (_hideShareButtonWhileOnIncognitoNTP == hide) {
return;
}
_hideShareButtonWhileOnIncognitoNTP = hide;
[self updateTrailingButton];
}
- (void)updateTrailingButtonState {
BOOL shouldShowVoiceSearch =
self.traitCollection.horizontalSizeClass ==
UIUserInterfaceSizeClassRegular ||
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact;
if (shouldShowVoiceSearch) {
if (self.voiceSearchEnabled) {
self.trailingButtonState = kVoiceSearchButton;
} else {
self.trailingButtonState = kNoButton;
}
} else {
self.trailingButtonState = kShareButton;
}
}
- (id<ContextualPanelEntrypointVisibilityDelegate>)
contextualEntrypointVisibilityDelegate {
return self.locationBarSteadyView.contextualEntrypointVisibilityDelegate;
}
- (id<BadgeViewVisibilityDelegate>)badgeViewVisibilityDelegate {
return self.locationBarSteadyView.badgeViewVisibilityDelegate;
}
#pragma mark - UIViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.clipsToBounds = YES;
// TODO(crbug.com/328446957): Cleanup when fully launched, at which point
// `contextualPanelEntrypointView` should be CHECK()'ed. Until fully launched,
// the entrypoint view might be nil if the flag is disabled.
if (self.contextualPanelEntrypointView) {
[self.locationBarSteadyView
setContextualPanelEntrypointView:self.contextualPanelEntrypointView];
}
DCHECK(self.badgeView) << "The badge view must be set at this point";
[self.locationBarSteadyView setBadgeView:self.badgeView];
if (IsLensOverlayAvailable()) {
[self.locationBarSteadyView setPlaceholderView:self.placeholderView];
}
[_locationBarSteadyView.locationButton
addTarget:self
action:@selector(locationBarSteadyViewTapped)
forControlEvents:UIControlEventTouchUpInside];
[_locationBarSteadyView
addInteraction:[[UIContextMenuInteraction alloc] initWithDelegate:self]];
UIIndirectScribbleInteraction* scribbleInteraction =
[[UIIndirectScribbleInteraction alloc] initWithDelegate:self];
[_locationBarSteadyView addInteraction:scribbleInteraction];
DCHECK(self.editView) << "The edit view must be set at this point";
[self.view addSubview:self.editView];
self.editView.translatesAutoresizingMaskIntoConstraints = NO;
AddSameConstraints(self.editView, self.view);
[self.view addSubview:self.locationBarSteadyView];
self.locationBarSteadyView.translatesAutoresizingMaskIntoConstraints = NO;
AddSameConstraints(self.locationBarSteadyView, self.view);
[self updateTrailingButtonState];
[self switchToEditing:NO];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[NSNotificationCenter.defaultCenter
removeObserver:self
name:UIPasteboardChangedNotification
object:nil];
[NSNotificationCenter.defaultCenter
removeObserver:self
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[self updateTrailingButtonState];
[super traitCollectionDidChange:previousTraitCollection];
}
#pragma mark - FullscreenUIElement
- (void)updateForFullscreenProgress:(CGFloat)progress {
CGFloat alphaValue = fmax((progress - 0.85) / 0.15, 0);
CGFloat scaleValue = 0.79 + 0.21 * progress;
self.locationBarSteadyView.trailingButton.alpha = alphaValue;
BOOL badgeViewShouldCollapse = progress <= kFullscreenProgressThreshold;
[self.locationBarSteadyView
setFullScreenCollapsedMode:badgeViewShouldCollapse];
self.locationBarSteadyView.transform =
CGAffineTransformMakeScale(scaleValue, scaleValue);
}
- (void)updateForFullscreenEnabled:(BOOL)enabled {
if (!enabled)
[self updateForFullscreenProgress:1.0];
}
- (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator {
CGFloat finalProgress = animator.finalProgress;
[animator addAnimations:^{
[self updateForFullscreenProgress:finalProgress];
}];
}
#pragma mark - LocationBarConsumer
- (void)updateLocationText:(NSString*)string clipTail:(BOOL)clipTail {
[self.locationBarSteadyView setLocationLabelText:string];
self.locationBarSteadyView.locationLabel.lineBreakMode =
clipTail ? NSLineBreakByTruncatingTail : NSLineBreakByTruncatingHead;
}
- (void)updateLocationIcon:(UIImage*)icon
securityStatusText:(NSString*)statusText {
[self.locationBarSteadyView setLocationImage:icon];
self.locationBarSteadyView.securityLevelAccessibilityString = statusText;
}
// Updates display on the NTP. Note that this is only meaningful on iPad, where
// the location bar is visible after scrolling the fakebox off the page. On
// iPhone, the location bar is not shown on the NTP at all.
- (void)updateForNTP:(BOOL)isNTP {
_isNTP = isNTP;
if (isNTP) {
// Display a fake "placeholder".
NSString* placeholderString =
l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT);
[self.locationBarSteadyView
setLocationLabelPlaceholderText:placeholderString];
}
if (base::FeatureList::IsEnabled(kNewNTPOmniboxLayout)) {
[self.locationBarSteadyView setCentered:(!isNTP || self.incognito)];
}
self.hideShareButtonWhileOnIncognitoNTP = isNTP;
}
- (void)setShareButtonEnabled:(BOOL)enabled {
_shareButtonEnabled = enabled;
if (self.trailingButtonState == kShareButton) {
[self.locationBarSteadyView enableTrailingButton:enabled];
if (_shareButtonEnabled) {
[self.layoutGuideCenter
referenceView:self.locationBarSteadyView.trailingButton
underName:kShareButtonGuide];
}
}
}
- (BOOL)canShowLargeContextualPanelEntrypoint {
// TODO(crbug.com/330701617): Add actual checks when implementing badge view
// loud moment blocking (check might need to be in the actual view).
return !self.locationBarSteadyView.hidden;
}
- (void)setLocationBarLabelCenteredBetweenContent:(BOOL)centered {
[self.locationBarSteadyView
setLocationBarLabelCenteredBetweenContent:centered];
}
#pragma mark - LocationBarAnimatee
- (void)offsetTextFieldToMatchSteadyView {
CGAffineTransform offsetTransform =
CGAffineTransformMakeTranslation([self targetOffset], 0);
self.textField.transform = offsetTransform;
}
- (void)resetTextFieldOffsetAndOffsetSteadyViewToMatch {
self.locationBarSteadyView.transform =
CGAffineTransformMakeTranslation(-self.textField.transform.tx, 0);
self.textField.transform = CGAffineTransformIdentity;
}
- (void)offsetSteadyViewToMatchTextField {
CGAffineTransform offsetTransform =
CGAffineTransformMakeTranslation(-[self targetOffset], 0);
self.locationBarSteadyView.transform = offsetTransform;
}
- (void)resetSteadyViewOffsetAndOffsetTextFieldToMatch {
self.textField.transform = CGAffineTransformMakeTranslation(
-self.locationBarSteadyView.transform.tx, 0);
self.locationBarSteadyView.transform = CGAffineTransformIdentity;
}
- (void)setSteadyViewFaded:(BOOL)hidden {
self.locationBarSteadyView.alpha = hidden ? 0 : 1;
}
- (void)hideSteadyViewBadgeAndEntrypointViews {
[self.locationBarSteadyView displayBadgeView:NO animated:NO];
[self.delegate displayContextualPanelEntrypointView:NO];
}
- (void)showSteadyViewBadgeAndEntrypointViews {
[self.locationBarSteadyView displayBadgeView:YES animated:NO];
[self.delegate displayContextualPanelEntrypointView:YES];
}
- (void)setEditViewFaded:(BOOL)hidden {
self.editView.alpha = hidden ? 0 : 1;
}
- (void)setEditViewHidden:(BOOL)hidden {
self.editView.hidden = hidden;
}
- (void)setSteadyViewHidden:(BOOL)hidden {
self.locationBarSteadyView.hidden = hidden;
}
- (void)resetTransforms {
// Focus/defocus animations only affect translations and not scale. So reset
// translation and keep the scale.
self.textField.transform = CGAffineTransformMake(
self.textField.transform.a, self.textField.transform.b,
self.textField.transform.c, self.textField.transform.d, 0, 0);
self.locationBarSteadyView.transform =
CGAffineTransformMake(self.locationBarSteadyView.transform.a,
self.locationBarSteadyView.transform.b,
self.locationBarSteadyView.transform.c,
self.locationBarSteadyView.transform.d, 0, 0);
;
}
#pragma mark animation helpers
// Computes the target offset for the focus/defocus animation that allows to
// visually match the position of edit and steady views.
- (CGFloat)targetOffset {
CGFloat offset =
_isNTP
? kOmniboxEditOffset
: [self.offsetProvider
xOffsetForString:self.locationBarSteadyView.locationLabel.text];
CGRect labelRect = [self.view
convertRect:self.locationBarSteadyView.locationLabel.frame
fromView:self.locationBarSteadyView.locationLabel.superview];
CGRect textFieldRect = self.editView.frame;
CGFloat targetOffset = labelRect.origin.x - textFieldRect.origin.x - offset;
return targetOffset;
}
#pragma mark - UIIndirectScribbleInteractionDelegate
- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
requestElementsInRect:(CGRect)rect
completion:
(void (^)(NSArray<UIScribbleElementIdentifier>*
elements))completion {
completion(@[ kScribbleOmniboxElementId ]);
}
- (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
isElementFocused:
(UIScribbleElementIdentifier)elementIdentifier {
DCHECK(elementIdentifier == kScribbleOmniboxElementId);
return self.delegate.omniboxScribbleForwardingTarget.isFirstResponder;
}
- (CGRect)
indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
frameForElement:(UIScribbleElementIdentifier)elementIdentifier {
DCHECK(elementIdentifier == kScribbleOmniboxElementId);
// Imitate the entire location bar being scribblable.
return self.view.bounds;
}
- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
focusElementIfNeeded:
(UIScribbleElementIdentifier)elementIdentifier
referencePoint:(CGPoint)focusReferencePoint
completion:
(void (^)(UIResponder<UITextInput>* focusedInput))
completion {
if (!self.delegate.omniboxScribbleForwardingTarget.isFirstResponder) {
[self.delegate locationBarRequestScribbleTargetFocus];
}
completion(self.delegate.omniboxScribbleForwardingTarget);
}
#pragma mark - private
- (void)locationBarSteadyViewTapped {
base::RecordAction(base::UserMetricsAction("MobileLocationBarTapped"));
TriggerHapticFeedbackForSelectionChange();
[self.delegate locationBarSteadyViewTapped];
}
- (void)updateTrailingButton {
// Stop constraining the voice guide to the trailing button if transitioning
// from kVoiceSearchButton.
UIView* referencedView =
[self.layoutGuideCenter referencedViewUnderName:kVoiceSearchButtonGuide];
if (referencedView == self.locationBarSteadyView.trailingButton) {
[self.layoutGuideCenter referenceView:nil
underName:kVoiceSearchButtonGuide];
}
// Cancel previous possible state.
[self.locationBarSteadyView.trailingButton
removeTarget:nil
action:nil
forControlEvents:UIControlEventAllEvents];
self.locationBarSteadyView.trailingButton.hidden = NO;
TrailingButtonState state = self.trailingButtonState;
if (state == kShareButton && self.hideShareButtonWhileOnIncognitoNTP) {
state = kNoButton;
}
switch (state) {
case kNoButton: {
self.locationBarSteadyView.trailingButton.hidden = YES;
break;
};
case kShareButton: {
[self.locationBarSteadyView.trailingButton
addTarget:self.dispatcher
action:@selector(sharePage)
forControlEvents:UIControlEventTouchUpInside];
// Add self as a target to collect the metrics.
[self.locationBarSteadyView.trailingButton
addTarget:self
action:@selector(shareButtonPressed)
forControlEvents:UIControlEventTouchUpInside];
UIImage* shareImage =
DefaultSymbolWithPointSize(kShareSymbol, kSymbolImagePointSize);
[self.locationBarSteadyView.trailingButton setImage:shareImage
forState:UIControlStateNormal];
self.locationBarSteadyView.trailingButton.accessibilityLabel =
l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_SHARE);
self.locationBarSteadyView.trailingButton.accessibilityIdentifier =
kOmniboxShareButtonIdentifier;
[self.locationBarSteadyView enableTrailingButton:self.shareButtonEnabled];
if (self.shareButtonEnabled) {
[self.layoutGuideCenter
referenceView:self.locationBarSteadyView.trailingButton
underName:kShareButtonGuide];
}
break;
};
case kVoiceSearchButton: {
[self.locationBarSteadyView.trailingButton
addTarget:self.dispatcher
action:@selector(preloadVoiceSearch)
forControlEvents:UIControlEventTouchDown];
[self.locationBarSteadyView.trailingButton
addTarget:self
action:@selector(startVoiceSearch)
forControlEvents:UIControlEventTouchUpInside];
UIImage* micImage =
DefaultSymbolWithPointSize(kMicrophoneSymbol, kSymbolImagePointSize);
[self.locationBarSteadyView.trailingButton setImage:micImage
forState:UIControlStateNormal];
self.locationBarSteadyView.trailingButton.accessibilityLabel =
l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_VOICE_SEARCH);
self.locationBarSteadyView.trailingButton.accessibilityIdentifier =
kOmniboxVoiceSearchButtonIdentifier;
self.locationBarSteadyView.trailingButton.layer.cornerRadius =
self.locationBarSteadyView.trailingButton.frame.size.width / 2;
self.locationBarSteadyView.trailingButton.clipsToBounds = YES;
[self.locationBarSteadyView enableTrailingButton:YES];
}
}
}
- (void)setTrailingButtonState:(TrailingButtonState)state {
if (_trailingButtonState == state) {
return;
}
_trailingButtonState = state;
[self updateTrailingButton];
}
- (void)startVoiceSearch {
[self.layoutGuideCenter
referenceView:self.locationBarSteadyView.trailingButton
underName:kVoiceSearchButtonGuide];
base::RecordAction(base::UserMetricsAction("MobileOmniboxVoiceSearch"));
[self.dispatcher startVoiceSearch];
}
// Called when the share button is pressed.
// The actual share dialog is opened by the dispatcher, only collect the metrics
// here.
- (void)shareButtonPressed {
RecordAction(UserMetricsAction("MobileToolbarShareMenu"));
[self.delegate recordShareButtonPressed];
}
- (BOOL)shouldUseLensInLongPressMenu {
return ios::provider::IsLensSupported() &&
base::FeatureList::IsEnabled(kEnableLensInOmniboxCopiedImage) &&
self.lensImageEnabled;
}
#pragma mark - UIContextMenuInteractionDelegate
- (UIMenu*)contextMenuUIMenu:(NSArray<UIMenuElement*>*)suggestedActions {
NSMutableArray<UIMenuElement*>* menuElements = [[NSMutableArray alloc] init];
__weak __typeof__(self) weakSelf = self;
UIImage* pasteImage = nil;
if (IsBottomOmniboxAvailable()) {
pasteImage =
DefaultSymbolWithPointSize(kPasteActionSymbol, kSymbolActionPointSize);
// Copy link action.
if (!self.locationBarSteadyView.hidden) {
UIAction* copyAction = [UIAction
actionWithTitle:l10n_util::GetNSString(IDS_IOS_COPY_LINK_ACTION_TITLE)
image:DefaultSymbolWithPointSize(kCopyActionSymbol,
kSymbolActionPointSize)
identifier:nil
handler:^(UIAction* action) {
[weakSelf.delegate locationBarCopyTapped];
}];
[menuElements addObject:copyAction];
}
} else {
// Keep the suggested actions to have the copy action in a separate section.
[menuElements addObjectsFromArray:suggestedActions];
}
std::optional<std::set<ClipboardContentType>> clipboard_content_types =
ClipboardRecentContent::GetInstance()->GetCachedClipboardContentTypes();
if (clipboard_content_types.has_value()) {
std::set<ClipboardContentType> clipboard_content_types_values =
clipboard_content_types.value();
if (base::Contains(clipboard_content_types_values,
ClipboardContentType::Image)) {
// Either add an option to search the copied image with Lens, or via the
// default search engine's reverse image search functionality.
if (self.shouldUseLensInLongPressMenu) {
id lensCopiedImageHandler = ^(UIAction* action) {
[weakSelf lensCopiedImage:nil];
};
UIAction* lensCopiedImageAction = [UIAction
actionWithTitle:l10n_util::GetNSString(
(IDS_IOS_SEARCH_COPIED_IMAGE_WITH_LENS))
image:pasteImage
identifier:nil
handler:lensCopiedImageHandler];
[menuElements addObject:lensCopiedImageAction];
} else {
id searchCopiedImageHandler = ^(UIAction* action) {
[weakSelf searchCopiedImage:nil];
};
UIAction* searchCopiedImageAction =
[UIAction actionWithTitle:l10n_util::GetNSString(
(IDS_IOS_SEARCH_COPIED_IMAGE))
image:pasteImage
identifier:nil
handler:searchCopiedImageHandler];
[menuElements addObject:searchCopiedImageAction];
}
} else if (base::Contains(clipboard_content_types_values,
ClipboardContentType::URL)) {
id visitCopiedLinkHandler = ^(UIAction* action) {
[self visitCopiedLink:nil];
};
UIAction* visitCopiedLinkAction = [UIAction
actionWithTitle:l10n_util::GetNSString((IDS_IOS_VISIT_COPIED_LINK))
image:pasteImage
identifier:nil
handler:visitCopiedLinkHandler];
[menuElements addObject:visitCopiedLinkAction];
} else if (base::Contains(clipboard_content_types_values,
ClipboardContentType::Text)) {
id searchCopiedTextHandler = ^(UIAction* action) {
[self searchCopiedText:nil];
};
UIAction* searchCopiedTextAction = [UIAction
actionWithTitle:l10n_util::GetNSString((IDS_IOS_SEARCH_COPIED_TEXT))
image:pasteImage
identifier:nil
handler:searchCopiedTextHandler];
[menuElements addObject:searchCopiedTextAction];
}
}
// Show Top or Bottom Address Bar action.
if (IsBottomOmniboxAvailable() && IsSplitToolbarMode(self)) {
NSString* title = nil;
UIImage* image = nil;
ToolbarType targetToolbarType;
if (GetApplicationContext()->GetLocalState()->GetBoolean(
prefs::kBottomOmnibox)) {
title = l10n_util::GetNSString(IDS_IOS_TOOLBAR_MENU_TOP_OMNIBOX);
if (@available(iOS 15.1, *)) {
image = DefaultSymbolWithPointSize(kMovePlatterToTopPhoneSymbol,
kSymbolActionPointSize);
} else {
image = CustomSymbolWithPointSize(kCustomMovePlatterToTopPhoneSymbol,
kSymbolActionPointSize);
}
targetToolbarType = ToolbarType::kPrimary;
} else {
title = l10n_util::GetNSString(IDS_IOS_TOOLBAR_MENU_BOTTOM_OMNIBOX);
if (@available(iOS 15.1, *)) {
image = DefaultSymbolWithPointSize(kMovePlatterToBottomPhoneSymbol,
kSymbolActionPointSize);
} else {
image = CustomSymbolWithPointSize(kCustomMovePlatterToBottomPhoneSymbol,
kSymbolActionPointSize);
}
targetToolbarType = ToolbarType::kSecondary;
}
UIAction* moveAddressBarAction = [UIAction
actionWithTitle:title
image:image
identifier:nil
handler:^(UIAction* action) {
[weakSelf moveOmniboxToToolbarType:targetToolbarType];
}];
UIMenu* divider = [UIMenu menuWithTitle:@""
image:nil
identifier:nil
options:UIMenuOptionsDisplayInline
children:@[ moveAddressBarAction ]];
[menuElements addObject:divider];
}
return [UIMenu menuWithTitle:@"" children:menuElements];
}
- (UITargetedPreview*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
previewForHighlightingMenuWithConfiguration:
(UIContextMenuConfiguration*)configuration {
// Use the location bar's container view because that's the view that has the
// background color and corner radius.
return [[UITargetedPreview alloc]
initWithView:self.view.superview
parameters:[[UIPreviewParameters alloc] init]];
}
- (UIContextMenuConfiguration*)contextMenuInteraction:
(UIContextMenuInteraction*)interaction
configurationForMenuAtLocation:(CGPoint)location {
__weak LocationBarViewController* weakSelf = self;
UIContextMenuConfiguration* configuration = [UIContextMenuConfiguration
configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu*(
NSArray<UIMenuElement*>* suggestedActions) {
return [weakSelf contextMenuUIMenu:suggestedActions];
}];
if (IsBottomOmniboxAvailable()) {
configuration.preferredMenuElementOrder =
UIContextMenuConfigurationElementOrderPriority;
}
return configuration;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
// Allow copying if the steady location bar is visible.
if (!self.locationBarSteadyView.hidden && action == @selector(copy:)) {
return YES;
}
return NO;
}
- (void)copy:(id)sender {
[self.delegate locationBarCopyTapped];
}
- (void)searchCopiedImage:(id)sender {
RecordAction(
UserMetricsAction("Mobile.OmniboxContextMenu.SearchCopiedImage"));
[self.delegate searchCopiedImage];
}
- (void)lensCopiedImage:(id)sender {
RecordAction(UserMetricsAction("Mobile.OmniboxContextMenu.LensCopiedImage"));
[self.delegate lensCopiedImage];
}
- (void)visitCopiedLink:(id)sender {
// A search using clipboard link is activity that should indicate a user
// that would be interested in setting Chrome as the default browser.
[self.delegate locationBarVisitCopyLinkTapped];
RecordAction(UserMetricsAction("Mobile.OmniboxContextMenu.VisitCopiedLink"));
ClipboardRecentContent::GetInstance()->GetRecentURLFromClipboard(
base::BindOnce(^(std::optional<GURL> optionalURL) {
if (!optionalURL) {
return;
}
NSString* url = base::SysUTF8ToNSString(optionalURL.value().spec());
dispatch_async(dispatch_get_main_queue(), ^{
[self.dispatcher loadQuery:url immediately:YES];
[self.dispatcher cancelOmniboxEdit];
});
}));
}
- (void)searchCopiedText:(id)sender {
// A search using clipboard text is activity that should indicate a user
// that would be interested in setting Chrome as the default browser.
[self.delegate locationBarSearchCopiedTextTapped];
RecordAction(UserMetricsAction("Mobile.OmniboxContextMenu.SearchCopiedText"));
ClipboardRecentContent::GetInstance()->GetRecentTextFromClipboard(
base::BindOnce(^(std::optional<std::u16string> optionalText) {
if (!optionalText) {
return;
}
NSString* query = base::SysUTF16ToNSString(optionalText.value());
dispatch_async(dispatch_get_main_queue(), ^{
[self.dispatcher loadQuery:query immediately:YES];
[self.dispatcher cancelOmniboxEdit];
});
}));
}
/// Set the preferred omnibox position to `toolbarType`.
- (void)moveOmniboxToToolbarType:(ToolbarType)toolbarType {
GetApplicationContext()->GetLocalState()->SetBoolean(
prefs::kBottomOmnibox, toolbarType == ToolbarType::kSecondary);
if (toolbarType == ToolbarType::kPrimary) {
RecordAction(
UserMetricsAction("Mobile.OmniboxContextMenu.MoveAddressBarToTop"));
} else {
RecordAction(
UserMetricsAction("Mobile.OmniboxContextMenu.MoveAddressBarToBottom"));
}
}
@end