chromium/ios/chrome/common/ui/elements/form_input_accessory_view.mm

// Copyright 2014 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/common/ui/elements/form_input_accessory_view.h"

#import <QuartzCore/QuartzCore.h>

#import "base/check.h"
#import "base/i18n/rtl.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/elements/form_input_accessory_view_text_data.h"
#import "ios/chrome/common/ui/elements/gradient_view.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"

namespace {

// Default Height for the accessory.
constexpr CGFloat kDefaultAccessoryHeight = 44;

// Large Height for the accessory.
constexpr CGFloat kLargeAccessoryHeight = 59;

// Button target area for the large keyboard accessory.
constexpr CGFloat kLargeButtonTargetArea = 44;

// The width for the background-colored gradient UIView.
constexpr CGFloat ManualFillGradientWidth = 44;

// The width for the background-colored gradient UIView for the large keyboard
// accessory.
constexpr CGFloat ManualFillLargeAccessoryGradientWidth = 6;

// The margin for the background-colored gradient UIView.
constexpr CGFloat ManualFillGradientMargin = 14;

// The spacing between the items in the navigation view.
constexpr CGFloat ManualFillNavigationItemSpacing = 4;

// The left content inset for the close button.
constexpr CGFloat ManualFillCloseButtonLeftInset = 7;

// The right content inset for the close button.
constexpr CGFloat ManualFillCloseButtonRightInset = 15;

// The bottom content inset for the close button.
constexpr CGFloat ManualFillCloseButtonBottomInset = 4;

// The height for the top and bottom sepparator lines.
constexpr CGFloat ManualFillSeparatorHeight = 0.5;

}  // namespace

NSString* const kFormInputAccessoryViewAccessibilityID =
    @"kFormInputAccessoryViewAccessibilityID";

NSString* const kFormInputAccessoryViewOmniboxTypingShieldAccessibilityID =
    @"kFormInputAccessoryViewOmniboxTypingShieldAccessibilityID";

@interface FormInputAccessoryView ()

// The navigation delegate if any.
@property(nonatomic, weak) id<FormInputAccessoryViewDelegate> delegate;

@property(nonatomic, weak) UIButton* previousButton;

@property(nonatomic, weak) UIButton* nextButton;

@property(nonatomic, weak) UIButton* manualFillButton;

@property(nonatomic, weak) UIButton* passwordManualFillButton;

@property(nonatomic, weak) UIButton* creditCardManualFillButton;

@property(nonatomic, weak) UIButton* addressManualFillButton;

@property(nonatomic, weak) UIView* leadingView;

@property(nonatomic, weak) UIView* trailingView;

@property(nonatomic, strong) UIImage* manualFillSymbol;

@property(nonatomic, strong) UIImage* passwordManualFillSymbol;

@property(nonatomic, strong) UIImage* creditCardManualFillSymbol;

@property(nonatomic, strong) UIImage* addressManualFillSymbol;

@property(nonatomic, strong) UIImage* closeButtonSymbol;

@end

@implementation FormInputAccessoryView {
  // Transparent view on the top edge to let the keyboard know about the
  // omnibox.
  UIButton* _omniboxTypingShield;
  // Height constraint used to show/hide the `omniboxTypingShield`.
  NSLayoutConstraint* _omniboxTypingShieldHeightConstraint;
  // Bottom constraint used to show/hide the `omniboxTypingShield`.
  NSLayoutConstraint* _omniboxTypingShieldBottomConstraint;
  // Bottom constraint used to show/hide the `omniboxTypingShield` when the view
  // is hidden.
  NSLayoutConstraint* _omniboxTypingShieldHiddenBottomConstraint;
  // View containing the leading and trailing buttons.
  UIView* _contentView;
  // Whether we are using the large accessory view.
  BOOL _largeAccessoryViewEnabled;
}

#pragma mark - Public

// Override `intrinsicContentSize` so Auto Layout hugs the content of this view.
- (CGSize)intrinsicContentSize {
  return CGSizeZero;
}

- (void)setUpWithLeadingView:(UIView*)leadingView
          customTrailingView:(UIView*)customTrailingView {
  [self setUpWithLeadingView:leadingView
              customTrailingView:customTrailingView
              navigationDelegate:nil
                manualFillSymbol:nil
        passwordManualFillSymbol:nil
      creditCardManualFillSymbol:nil
         addressManualFillSymbol:nil
               closeButtonSymbol:nil];
}

- (void)setUpWithLeadingView:(UIView*)leadingView
          navigationDelegate:(id<FormInputAccessoryViewDelegate>)delegate {
  [self setUpWithLeadingView:leadingView
              customTrailingView:nil
              navigationDelegate:delegate
                manualFillSymbol:nil
        passwordManualFillSymbol:nil
      creditCardManualFillSymbol:nil
         addressManualFillSymbol:nil
               closeButtonSymbol:nil];
}

- (void)setUpWithLeadingView:(UIView*)leadingView
            navigationDelegate:(id<FormInputAccessoryViewDelegate>)delegate
              manualFillSymbol:(UIImage*)manualFillSymbol
      passwordManualFillSymbol:(UIImage*)passwordManualFillSymbol
    creditCardManualFillSymbol:(UIImage*)creditCardManualFillSymbol
       addressManualFillSymbol:(UIImage*)addressManualFillSymbol
             closeButtonSymbol:(UIImage*)closeButtonSymbol {
  DCHECK(manualFillSymbol);
  _largeAccessoryViewEnabled = YES;
  [self setUpWithLeadingView:leadingView
              customTrailingView:nil
              navigationDelegate:delegate
                manualFillSymbol:manualFillSymbol
        passwordManualFillSymbol:passwordManualFillSymbol
      creditCardManualFillSymbol:creditCardManualFillSymbol
         addressManualFillSymbol:addressManualFillSymbol
               closeButtonSymbol:closeButtonSymbol];
}

- (void)setOmniboxTypingShieldHeight:(CGFloat)typingShieldHeight {
  _omniboxTypingShieldHeightConstraint.constant = typingShieldHeight;
  if (self.window) {
    [self layoutIfNeeded];
  }
}

#pragma mark - UIInputViewAudioFeedback

- (BOOL)enableInputClicksWhenVisible {
  return YES;
}

#pragma mark - Private Methods

- (void)closeButtonTapped {
  [self.delegate formInputAccessoryViewDidTapCloseButton:self];
}

- (void)nextButtonTapped {
  [self.delegate formInputAccessoryViewDidTapNextButton:self];
}

- (void)previousButtonTapped {
  [self.delegate formInputAccessoryViewDidTapPreviousButton:self];
}

- (void)manualFillButtonTapped {
  [self.delegate formInputAccessoryViewDidTapManualFillButton:self];
}

- (void)passwordManualFillButtonTapped {
  [self.delegate formInputAccessoryViewDidTapPasswordManualFillButton:self];
}

- (void)creditCardManualFillButtonTapped {
  [self.delegate formInputAccessoryViewDidTapCreditCardManualFillButton:self];
}

- (void)addressManualFillButtonTapped {
  [self.delegate formInputAccessoryViewDidTapAddressManualFillButton:self];
}

- (void)omniboxTypingShieldTapped {
  [self.delegate fromInputAccessoryViewDidTapOmniboxTypingShield:self];
}

// Sets up the view with the given `leadingView`. If `delegate` is not nil,
// navigation controls are shown on the right and use `delegate` for actions.
// Else navigation controls are replaced with `customTrailingView`. If none of
// `delegate` and `customTrailingView` is set, leadingView will take all the
// space.
- (void)setUpWithLeadingView:(UIView*)leadingView
            customTrailingView:(UIView*)customTrailingView
            navigationDelegate:(id<FormInputAccessoryViewDelegate>)delegate
              manualFillSymbol:(UIImage*)manualFillSymbol
      passwordManualFillSymbol:(UIImage*)passwordManualFillSymbol
    creditCardManualFillSymbol:(UIImage*)creditCardManualFillSymbol
       addressManualFillSymbol:(UIImage*)addressManualFillSymbol
             closeButtonSymbol:(UIImage*)closeButtonSymbol {
  DCHECK(!self.subviews.count);  // This should only be called once.

  self.accessibilityIdentifier = kFormInputAccessoryViewAccessibilityID;
  self.translatesAutoresizingMaskIntoConstraints = NO;
  self.backgroundColor = UIColor.clearColor;
  self.opaque = NO;
  self.manualFillSymbol = manualFillSymbol;
  self.passwordManualFillSymbol = passwordManualFillSymbol;
  self.creditCardManualFillSymbol = creditCardManualFillSymbol;
  self.addressManualFillSymbol = addressManualFillSymbol;
  self.closeButtonSymbol = closeButtonSymbol;

  _contentView = [[UIView alloc] init];
  _contentView.translatesAutoresizingMaskIntoConstraints = NO;
  _contentView.backgroundColor = [self contentBackgroundColor];
  [self addSubview:_contentView];
  AddSameConstraintsToSides(
      self, _contentView,
      LayoutSides::kLeading | LayoutSides::kTrailing | LayoutSides::kBottom);
  // Lower the top constraint as the omniboxTypingShield can be above it.
  NSLayoutConstraint* topConstraint =
      [self.topAnchor constraintEqualToAnchor:_contentView.topAnchor];
  topConstraint.priority = UILayoutPriorityRequired - 1;
  topConstraint.active = YES;

  leadingView = leadingView ?: [[UIView alloc] init];
  self.leadingView = leadingView;
  leadingView.translatesAutoresizingMaskIntoConstraints = NO;

  UIView* trailingView;
  if (delegate) {
    self.delegate = delegate;
    trailingView = [self viewForNavigationButtons];
  } else {
    trailingView = customTrailingView;
  }
  self.trailingView = trailingView;

  // If there is no trailing view, set the leading view as the only view and
  // return early.
  if (!trailingView) {
    [_contentView addSubview:leadingView];
    AddSameConstraints(_contentView, leadingView);
    return;
  }

  UIView* leadingViewContainer = [[UIView alloc] init];
  leadingViewContainer.translatesAutoresizingMaskIntoConstraints = NO;
  [_contentView addSubview:leadingViewContainer];
  [leadingViewContainer addSubview:leadingView];
  AddSameConstraints(leadingViewContainer, leadingView);

  trailingView.translatesAutoresizingMaskIntoConstraints = NO;
  [self addSubview:trailingView];

  NSLayoutConstraint* defaultHeightConstraint = [_contentView.heightAnchor
      constraintEqualToConstant:[self accessoryHeight]];
  defaultHeightConstraint.priority = UILayoutPriorityDefaultHigh;

  id<LayoutGuideProvider> layoutGuide = self.safeAreaLayoutGuide;
  [NSLayoutConstraint activateConstraints:@[
    defaultHeightConstraint,
    [leadingViewContainer.topAnchor
        constraintEqualToAnchor:_contentView.topAnchor],
    [leadingViewContainer.bottomAnchor
        constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor],
    [leadingViewContainer.leadingAnchor
        constraintEqualToAnchor:layoutGuide.leadingAnchor],
    [trailingView.trailingAnchor
        constraintEqualToAnchor:layoutGuide.trailingAnchor],
    [trailingView.topAnchor constraintEqualToAnchor:_contentView.topAnchor],
    [trailingView.bottomAnchor
        constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor],
  ]];

  // Gradient view to disolve the leading view's end.
  UIView* gradientView =
      [[GradientView alloc] initWithStartColor:[[self contentBackgroundColor]
                                                   colorWithAlphaComponent:0]
                                      endColor:[self contentBackgroundColor]
                                    startPoint:CGPointMake(0, 0.5)
                                      endPoint:CGPointMake(0.6, 0.5)];

  gradientView.translatesAutoresizingMaskIntoConstraints = NO;
  if (base::i18n::IsRTL()) {
    gradientView.transform = CGAffineTransformMakeRotation(M_PI);
  }
  [_contentView insertSubview:gradientView belowSubview:trailingView];

  UIView* topGrayLine = [[UIView alloc] init];
  topGrayLine.backgroundColor = [UIColor colorNamed:kGrey50Color];
  topGrayLine.translatesAutoresizingMaskIntoConstraints = NO;
  [_contentView addSubview:topGrayLine];

  UIView* bottomGrayLine = [[UIView alloc] init];
  bottomGrayLine.backgroundColor = [UIColor colorNamed:kGrey50Color];
  bottomGrayLine.translatesAutoresizingMaskIntoConstraints = NO;
  [_contentView addSubview:bottomGrayLine];

  [NSLayoutConstraint activateConstraints:@[
    [topGrayLine.topAnchor constraintEqualToAnchor:_contentView.topAnchor],
    [topGrayLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
    [topGrayLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
    [topGrayLine.heightAnchor
        constraintEqualToConstant:ManualFillSeparatorHeight],

    [bottomGrayLine.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
    [bottomGrayLine.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
    [bottomGrayLine.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
    [bottomGrayLine.heightAnchor
        constraintEqualToConstant:ManualFillSeparatorHeight],

    [gradientView.topAnchor constraintEqualToAnchor:trailingView.topAnchor],
    [gradientView.bottomAnchor
        constraintEqualToAnchor:trailingView.bottomAnchor],
    [gradientView.widthAnchor
        constraintEqualToConstant:_largeAccessoryViewEnabled
                                      ? ManualFillLargeAccessoryGradientWidth
                                      : ManualFillGradientWidth],
    [gradientView.trailingAnchor
        constraintEqualToAnchor:trailingView.leadingAnchor
                       constant:_largeAccessoryViewEnabled
                                    ? 0
                                    : ManualFillGradientMargin],

    [leadingViewContainer.trailingAnchor
        constraintEqualToAnchor:trailingView.leadingAnchor],
  ]];

  [self createOmniboxTypingShield];
}

// Returns a view that shows navigation buttons.
- (UIView*)viewForNavigationButtons {
  FormInputAccessoryViewTextData* textData =
      [self.delegate textDataforFormInputAccessoryView:self];

  UIButton* closeButton = [self createCloseButtonWithText:textData];

  UIStackView* navigationView = nil;
  if (_largeAccessoryViewEnabled) {
    UIButton* manualFillButton = [self createManualFillButtonWithText:textData];
    manualFillButton.hidden = YES;
    self.manualFillButton = manualFillButton;

    UIButton* passwordManualFillButton =
        [self createPasswordManualFillButtonWithText:textData];
    passwordManualFillButton.hidden = YES;
    self.passwordManualFillButton = passwordManualFillButton;

    UIButton* creditCardManualFillButton =
        [self createCreditCardManualFillButtonWithText:textData];
    creditCardManualFillButton.hidden = YES;
    self.creditCardManualFillButton = creditCardManualFillButton;

    UIButton* addressManualFillButton =
        [self createAddressManualFillButtonWithText:textData];
    addressManualFillButton.hidden = YES;
    self.addressManualFillButton = addressManualFillButton;

    navigationView = [[UIStackView alloc] initWithArrangedSubviews:@[
      passwordManualFillButton, creditCardManualFillButton,
      addressManualFillButton, manualFillButton, closeButton
    ]];
  } else {
    UIButton* previousButton = [self createPreviousButtonWithText:textData];
    self.previousButton = previousButton;

    UIButton* nextButton = [self createNextButtonWithText:textData];
    self.nextButton = nextButton;

    navigationView = [[UIStackView alloc]
        initWithArrangedSubviews:@[ previousButton, nextButton, closeButton ]];
  }
  navigationView.spacing = ManualFillNavigationItemSpacing;
  return navigationView;
}

// Create a button with the desired image, action and accessibility label.
- (UIButton*)createImageButton:(UIImage*)image
                        action:(SEL)action
            accessibilityLabel:(NSString*)accessibilityLabel {
  UIButton* imageButton = [UIButton buttonWithType:UIButtonTypeSystem];
  [imageButton setImage:image forState:UIControlStateNormal];
  if (_largeAccessoryViewEnabled) {
    [imageButton.widthAnchor
        constraintGreaterThanOrEqualToConstant:kLargeButtonTargetArea]
        .active = YES;
    [imageButton.heightAnchor
        constraintGreaterThanOrEqualToConstant:kLargeButtonTargetArea]
        .active = YES;
  }
  [imageButton addTarget:self
                  action:action
        forControlEvents:UIControlEventTouchUpInside];
  [imageButton setAccessibilityLabel:accessibilityLabel];
  return imageButton;
}

// Create the manual fill button.
- (UIButton*)createManualFillButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self createImageButton:self.manualFillSymbol
                          action:@selector(manualFillButtonTapped)
              accessibilityLabel:textData.manualFillButtonAccessibilityLabel];
}

// Create the password manual fill button.
- (UIButton*)createPasswordManualFillButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self
       createImageButton:self.passwordManualFillSymbol
                  action:@selector(passwordManualFillButtonTapped)
      accessibilityLabel:textData.passwordManualFillButtonAccessibilityLabel];
}

// Create the credit card manual fill button.
- (UIButton*)createCreditCardManualFillButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self
       createImageButton:self.creditCardManualFillSymbol
                  action:@selector(creditCardManualFillButtonTapped)
      accessibilityLabel:textData.creditCardManualFillButtonAccessibilityLabel];
}

// Create the address manual fill button.
- (UIButton*)createAddressManualFillButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self
       createImageButton:self.addressManualFillSymbol
                  action:@selector(addressManualFillButtonTapped)
      accessibilityLabel:textData.addressManualFillButtonAccessibilityLabel];
}

// Create the previous button.
- (UIButton*)createPreviousButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self createImageButton:[UIImage imageNamed:@"mf_arrow_up"]
                          action:@selector(previousButtonTapped)
              accessibilityLabel:textData.previousButtonAccessibilityLabel];
}

// Create the next button.
- (UIButton*)createNextButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  return [self createImageButton:[UIImage imageNamed:@"mf_arrow_down"]
                          action:@selector(nextButtonTapped)
              accessibilityLabel:textData.nextButtonAccessibilityLabel];
}

// Create the close button.
- (UIButton*)createCloseButtonWithText:
    (FormInputAccessoryViewTextData*)textData {
  UIButton* closeButton = [UIButton buttonWithType:UIButtonTypeSystem];
  [closeButton addTarget:self
                  action:@selector(closeButtonTapped)
        forControlEvents:UIControlEventTouchUpInside];

  UIButtonConfiguration* buttonConfiguration =
      [UIButtonConfiguration plainButtonConfiguration];
  if (self.closeButtonSymbol) {
    buttonConfiguration.image = self.closeButtonSymbol;
  } else {
    buttonConfiguration.title = textData.closeButtonTitle;
  }
  buttonConfiguration.contentInsets = NSDirectionalEdgeInsetsMake(
      0, ManualFillCloseButtonLeftInset,
      self.closeButtonSymbol ? ManualFillCloseButtonBottomInset : 0,
      ManualFillCloseButtonRightInset);
  closeButton.configuration = buttonConfiguration;

  [closeButton setAccessibilityLabel:textData.closeButtonAccessibilityLabel];

  return closeButton;
}

- (void)createOmniboxTypingShield {
  if (!_omniboxTypingShield) {
    CHECK(_contentView);
    _omniboxTypingShield = [[UIButton alloc] init];
    _omniboxTypingShield.translatesAutoresizingMaskIntoConstraints = NO;
    _omniboxTypingShield.backgroundColor = UIColor.clearColor;
    _omniboxTypingShield.isAccessibilityElement = NO;
    _omniboxTypingShield.opaque = NO;
    _omniboxTypingShield.accessibilityIdentifier =
        kFormInputAccessoryViewOmniboxTypingShieldAccessibilityID;
    [self addSubview:_omniboxTypingShield];

    AddSameConstraintsToSides(
        self, _omniboxTypingShield,
        LayoutSides::kTop | LayoutSides::kLeading | LayoutSides::kTrailing);
    _omniboxTypingShieldHeightConstraint =
        [_omniboxTypingShield.heightAnchor constraintEqualToConstant:0];
    _omniboxTypingShieldBottomConstraint = [_omniboxTypingShield.bottomAnchor
        constraintEqualToAnchor:_contentView.topAnchor];
    _omniboxTypingShieldHiddenBottomConstraint =
        [_omniboxTypingShield.bottomAnchor
            constraintEqualToAnchor:self.bottomAnchor];
    [NSLayoutConstraint activateConstraints:@[
      _omniboxTypingShieldHeightConstraint, _omniboxTypingShieldBottomConstraint
    ]];
    [_omniboxTypingShield addTarget:self
                             action:@selector(omniboxTypingShieldTapped)
                   forControlEvents:UIControlEventTouchUpInside];
  }
}

// Returns the height of the accessory. Returns a larger height when using the
// large accessory view.
- (CGFloat)accessoryHeight {
  return _largeAccessoryViewEnabled ? kLargeAccessoryHeight
                                    : kDefaultAccessoryHeight;
}

// Returns the content view's background color. Returns grey when using the
// large accessory view.
- (UIColor*)contentBackgroundColor {
  return _largeAccessoryViewEnabled
             ? [UIColor colorNamed:kGroupedPrimaryBackgroundColor]
             : [UIColor colorNamed:kBackgroundColor];
}

#pragma mark - UIView

- (void)setHidden:(BOOL)hidden {
  [super setHidden:hidden];

  // The bottom omnibox is anchored to the typing shield. If we don't change the
  // shield's anchor, when hiding the accessory view, there is a blank space the
  // size of the keyboard accessory view between the top of the view below the
  // accessory view and the omnibox. By changing the anchor here, the omnibox
  // appears directly above the view below the accessory view, without any gaps.
  _omniboxTypingShieldBottomConstraint.active = !hidden;
  _omniboxTypingShieldHiddenBottomConstraint.active = hidden;

  [self layoutIfNeeded];
}

@end