| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/autofill/form_input_accessory_view_controller.h" |
| |
| #include "base/mac/foundation_util.h" |
| #include "components/autofill/core/common/autofill_features.h" |
| #import "ios/chrome/browser/autofill/form_input_accessory_view.h" |
| #import "ios/chrome/browser/autofill/form_suggestion_view.h" |
| #import "ios/chrome/browser/ui/autofill/manual_fill/manual_fill_accessory_view_controller.h" |
| #include "ios/chrome/browser/ui/util/ui_util.h" |
| #import "ios/chrome/browser/ui/util/uikit_ui_util.h" |
| #import "ios/chrome/common/ui_util/constraints_ui_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace autofill { |
| CGFloat const kInputAccessoryHeight = 44.0f; |
| } // namespace autofill |
| |
| @interface FormInputAccessoryViewController () < |
| ManualFillAccessoryViewControllerDelegate> |
| |
| // Grey view used as the background of the keyboard to fix |
| // http://crbug.com/847523 |
| @property(nonatomic, strong) UIView* grayBackgroundView; |
| |
| // The keyboard replacement view, if any. |
| @property(nonatomic, weak) UIView* keyboardReplacementView; |
| |
| // The custom view that should be shown in the input accessory view. |
| @property(nonatomic, strong) FormInputAccessoryView* inputAccessoryView; |
| |
| // The leading view with the suggestions in FormInputAccessoryView. |
| @property(nonatomic, strong) FormSuggestionView* formSuggestionView; |
| |
| // If this view controller is paused it shouldn't add its views to the keyboard. |
| @property(nonatomic, getter=isPaused) BOOL paused; |
| |
| // The manual fill accessory view controller to add at the end of the |
| // suggestions. |
| @property(nonatomic, strong, readonly) |
| ManualFillAccessoryViewController* manualFillAccessoryViewController; |
| |
| // Delegate to handle interactions with the manual fill buttons. |
| @property(nonatomic, readonly, weak) |
| id<ManualFillAccessoryViewControllerDelegate> |
| manualFillAccessoryViewControllerDelegate; |
| |
| // Called when the keyboard will or did change frame. |
| - (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification; |
| |
| @end |
| |
| @implementation FormInputAccessoryViewController { |
| // Last registered keyboard rectangle. |
| CGRect _keyboardFrame; |
| |
| // Whether suggestions have previously been shown. |
| BOOL _suggestionsHaveBeenShown; |
| } |
| |
| @synthesize addressButtonHidden = _addressButtonHidden; |
| @synthesize creditCardButtonHidden = _creditCardButtonHidden; |
| @synthesize formInputNextButtonEnabled = _formInputNextButtonEnabled; |
| @synthesize formInputPreviousButtonEnabled = _formInputPreviousButtonEnabled; |
| @synthesize navigationDelegate = _navigationDelegate; |
| @synthesize passwordButtonHidden = _passwordButtonHidden; |
| |
| #pragma mark - Life Cycle |
| |
| - (instancetype)initWithManualFillAccessoryViewControllerDelegate: |
| (id<ManualFillAccessoryViewControllerDelegate>) |
| manualFillAccessoryViewControllerDelegate { |
| self = [super init]; |
| if (self) { |
| _manualFillAccessoryViewControllerDelegate = |
| manualFillAccessoryViewControllerDelegate; |
| if (autofill::features::IsPasswordManualFallbackEnabled()) { |
| _manualFillAccessoryViewController = |
| [[ManualFillAccessoryViewController alloc] initWithDelegate:self]; |
| } |
| |
| _suggestionsHaveBeenShown = NO; |
| if (IsIPadIdiom()) { |
| _grayBackgroundView = [[UIView alloc] init]; |
| _grayBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; |
| // This color was obtained by try and error. |
| _grayBackgroundView.backgroundColor = |
| [[UIColor alloc] initWithRed:206 / 255.f |
| green:212 / 255.f |
| blue:217 / 255.f |
| alpha:1]; |
| |
| } |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(keyboardWillOrDidChangeFrame:) |
| name:UIKeyboardWillChangeFrameNotification |
| object:nil]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(keyboardWillOrDidChangeFrame:) |
| name:UIKeyboardDidChangeFrameNotification |
| object:nil]; |
| } |
| return self; |
| } |
| |
| #pragma mark - Public |
| |
| - (void)presentView:(UIView*)view { |
| if (self.paused) { |
| return; |
| } |
| DCHECK(view); |
| DCHECK(!view.superview); |
| UIView* keyboardView = [self getKeyboardView]; |
| view.accessibilityViewIsModal = YES; |
| [keyboardView.superview addSubview:view]; |
| UIView* constrainingView = |
| [self recursiveGetKeyboardConstraintView:keyboardView]; |
| DCHECK(constrainingView); |
| view.translatesAutoresizingMaskIntoConstraints = NO; |
| AddSameConstraints(view, constrainingView); |
| self.keyboardReplacementView = view; |
| UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, view); |
| } |
| |
| - (void)unlockManualFallbackView { |
| [self.formSuggestionView unlockTrailingView]; |
| } |
| |
| - (void)lockManualFallbackView { |
| [self.formSuggestionView lockTrailingView]; |
| } |
| |
| - (void)resetManualFallbackIcons { |
| [self.manualFillAccessoryViewController reset]; |
| } |
| |
| #pragma mark - FormInputAccessoryConsumer |
| |
| - (void)showAccessorySuggestions:(NSArray<FormSuggestion*>*)suggestions |
| suggestionClient:(id<FormSuggestionClient>)suggestionClient |
| isHardwareKeyboard:(BOOL)hardwareKeyboard { |
| // On ipad if the keyboard isn't visible don't show the custom view. |
| if (IsIPadIdiom() && |
| (CGRectIntersection([UIScreen mainScreen].bounds, _keyboardFrame) |
| .size.height == 0 || |
| CGRectEqualToRect(_keyboardFrame, CGRectZero))) { |
| [self removeCustomInputAccessoryView]; |
| return; |
| } |
| |
| // Check that |manualFillAccessoryViewController| was not instantiated if flag |
| // is disabled. And return early if there are no suggestions on iPad. |
| if (!autofill::features::IsPasswordManualFallbackEnabled()) { |
| DCHECK(!self.manualFillAccessoryViewController); |
| if (IsIPadIdiom()) { |
| // On iPad, there's no inputAccessoryView available, so we attach the |
| // custom view directly to the keyboard view instead. If this is a form |
| // suggestion view and no suggestions have been triggered yet, don't show |
| // the custom view. |
| if (suggestions && !_suggestionsHaveBeenShown && !suggestions.count) { |
| [self removeCustomInputAccessoryView]; |
| return; |
| } |
| _suggestionsHaveBeenShown = YES; |
| } |
| } |
| |
| // Create the views if they don't exist already. |
| if (!self.formSuggestionView) { |
| self.formSuggestionView = [[FormSuggestionView alloc] init]; |
| } |
| |
| [self.formSuggestionView updateClient:suggestionClient |
| suggestions:suggestions]; |
| |
| if (!self.inputAccessoryView) { |
| self.inputAccessoryView = [[FormInputAccessoryView alloc] init]; |
| if (IsIPadIdiom()) { |
| [self.inputAccessoryView |
| setUpWithLeadingView:self.formSuggestionView |
| customTrailingView:self.manualFillAccessoryViewController.view]; |
| } else { |
| self.formSuggestionView.trailingView = |
| self.manualFillAccessoryViewController.view; |
| [self.inputAccessoryView setUpWithLeadingView:self.formSuggestionView |
| navigationDelegate:self.navigationDelegate]; |
| self.inputAccessoryView.nextButton.enabled = |
| self.formInputNextButtonEnabled; |
| self.inputAccessoryView.previousButton.enabled = |
| self.formInputPreviousButtonEnabled; |
| } |
| } |
| |
| // On iPhones, when using a hardware keyboard, for most models, there's no |
| // space to show suggestions because of the on-screen menu button. |
| self.inputAccessoryView.leadingView.hidden = hardwareKeyboard; |
| |
| [self addInputAccessoryViewIfNeeded]; |
| } |
| |
| - (void)restoreOriginalKeyboardView { |
| [self.manualFillAccessoryViewController reset]; |
| [self restoreOriginalInputAccessoryView]; |
| [self.keyboardReplacementView removeFromSuperview]; |
| self.keyboardReplacementView = nil; |
| self.paused = NO; |
| } |
| |
| - (void)pauseCustomKeyboardView { |
| [self removeCustomInputAccessoryView]; |
| [self.keyboardReplacementView removeFromSuperview]; |
| self.paused = YES; |
| } |
| |
| - (void)continueCustomKeyboardView { |
| self.paused = NO; |
| } |
| |
| - (void)removeAnimationsOnKeyboardView { |
| // Work Around. On focus event, keyboardReplacementView is animated but the |
| // keyboard isn't. Cancel the animation to match the keyboard behavior |
| if (self.keyboardReplacementView.superview) { |
| [self.keyboardReplacementView.layer removeAllAnimations]; |
| } |
| } |
| |
| #pragma mark - Setters |
| |
| - (void)setPasswordButtonHidden:(BOOL)passwordButtonHidden { |
| _passwordButtonHidden = passwordButtonHidden; |
| self.manualFillAccessoryViewController.passwordButtonHidden = |
| passwordButtonHidden; |
| } |
| |
| - (void)setAddressButtonHidden:(BOOL)addressButtonHidden { |
| _addressButtonHidden = addressButtonHidden; |
| self.manualFillAccessoryViewController.addressButtonHidden = |
| addressButtonHidden; |
| } |
| |
| - (void)setCreditCardButtonHidden:(BOOL)creditCardButtonHidden { |
| _creditCardButtonHidden = creditCardButtonHidden; |
| self.manualFillAccessoryViewController.creditCardButtonHidden = |
| creditCardButtonHidden; |
| } |
| |
| - (void)setFormInputNextButtonEnabled:(BOOL)formInputNextButtonEnabled { |
| if (formInputNextButtonEnabled == _formInputNextButtonEnabled) { |
| return; |
| } |
| _formInputNextButtonEnabled = formInputNextButtonEnabled; |
| self.inputAccessoryView.nextButton.enabled = _formInputNextButtonEnabled; |
| } |
| |
| - (void)setFormInputPreviousButtonEnabled:(BOOL)formInputPreviousButtonEnabled { |
| if (formInputPreviousButtonEnabled == _formInputPreviousButtonEnabled) { |
| return; |
| } |
| _formInputPreviousButtonEnabled = formInputPreviousButtonEnabled; |
| self.inputAccessoryView.previousButton.enabled = |
| _formInputPreviousButtonEnabled; |
| } |
| |
| #pragma mark - Private |
| |
| // Removes the custom views related to the input accessory view. |
| - (void)removeCustomInputAccessoryView { |
| [self.inputAccessoryView removeFromSuperview]; |
| [self.grayBackgroundView removeFromSuperview]; |
| } |
| |
| // Removes the custom input accessory views and clears the references. |
| - (void)restoreOriginalInputAccessoryView { |
| [self removeCustomInputAccessoryView]; |
| } |
| |
| // This searches in a keyboard view hierarchy for the best candidate to |
| // constrain a view to the keyboard. |
| - (UIView*)recursiveGetKeyboardConstraintView:(UIView*)view { |
| for (UIView* subview in view.subviews) { |
| // TODO(crbug.com/845472): verify this on iOS 10-12 and all devices. |
| // Currently only tested on X-iOS12, 6+-iOS11 and 7+-iOS10. iPhoneX, iOS 11 |
| // and 12 uses "Dock" and iOS 10 uses "Backdrop". iPhone6+, iOS 11 uses |
| // "Dock". |
| if ([NSStringFromClass([subview class]) containsString:@"Dock"] || |
| [NSStringFromClass([subview class]) containsString:@"Backdrop"]) { |
| return subview; |
| } |
| UIView* found = [self recursiveGetKeyboardConstraintView:subview]; |
| if (found) { |
| return found; |
| } |
| } |
| return nil; |
| } |
| |
| - (UIView*)getKeyboardView { |
| NSArray* windows = [UIApplication sharedApplication].windows; |
| if (windows.count < 2) |
| return nil; |
| |
| UIWindow* window; |
| if (autofill::features::IsPasswordManualFallbackEnabled()) { |
| // TODO(crbug.com/845472): verify this works on iPad with split view before |
| // making this the default. |
| window = windows.lastObject; |
| } else { |
| window = windows[1]; |
| } |
| |
| for (UIView* subview in window.subviews) { |
| if ([NSStringFromClass([subview class]) rangeOfString:@"PeripheralHost"] |
| .location != NSNotFound) { |
| return subview; |
| } |
| if ([NSStringFromClass([subview class]) rangeOfString:@"SetContainer"] |
| .location != NSNotFound) { |
| for (UIView* subsubview in subview.subviews) { |
| if ([NSStringFromClass([subsubview class]) rangeOfString:@"SetHost"] |
| .location != NSNotFound) { |
| return subsubview; |
| } |
| } |
| } |
| } |
| |
| return nil; |
| } |
| |
| - (void)keyboardWillOrDidChangeFrame:(NSNotification*)notification { |
| CGRect keyboardFrame = |
| [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; |
| UIView* keyboardView = [self getKeyboardView]; |
| CGRect windowRect = keyboardView.window.bounds; |
| // On iPad when the keyboard is undocked, on iOS 11 and 12, |
| // `UIKeyboard*HideNotification` or `UIKeyboard*ShowNotification` are not |
| // being sent. So the check for all devices is done here. |
| if (CGRectContainsRect(windowRect, keyboardFrame)) { |
| _keyboardFrame = keyboardFrame; |
| // Make sure the input accessory is there if needed. |
| [self addInputAccessoryViewIfNeeded]; |
| [self addCustomKeyboardViewIfNeeded]; |
| } else { |
| _keyboardFrame = CGRectZero; |
| } |
| // On ipad we hide the views so they don't stick around at the bottom. Only |
| // needed on iPad because we add the view directly to the keyboard view. |
| if (IsIPadIdiom() && self.inputAccessoryView) { |
| if (CGRectEqualToRect(_keyboardFrame, CGRectZero)) { |
| self.inputAccessoryView.hidden = true; |
| self.grayBackgroundView.hidden = true; |
| } else { |
| self.inputAccessoryView.hidden = false; |
| self.grayBackgroundView.hidden = false; |
| } |
| } |
| } |
| |
| - (void)addCustomKeyboardViewIfNeeded { |
| if (self.isPaused) { |
| return; |
| } |
| if (self.keyboardReplacementView && !self.keyboardReplacementView.superview) { |
| [self presentView:self.keyboardReplacementView]; |
| } |
| } |
| |
| // Adds the inputAccessoryView and the backgroundView (on iPads), if those are |
| // not already in the hierarchy. |
| - (void)addInputAccessoryViewIfNeeded { |
| if (self.isPaused) { |
| return; |
| } |
| if (self.inputAccessoryView && !self.inputAccessoryView.superview) { |
| if (IsIPadIdiom()) { |
| UIView* keyboardView = [self getKeyboardView]; |
| self.inputAccessoryView.translatesAutoresizingMaskIntoConstraints = NO; |
| [keyboardView addSubview:self.inputAccessoryView]; |
| [NSLayoutConstraint activateConstraints:@[ |
| [self.inputAccessoryView.leadingAnchor |
| constraintEqualToAnchor:keyboardView.leadingAnchor], |
| [self.inputAccessoryView.trailingAnchor |
| constraintEqualToAnchor:keyboardView.trailingAnchor], |
| [self.inputAccessoryView.bottomAnchor |
| constraintEqualToAnchor:keyboardView.topAnchor], |
| [self.inputAccessoryView.heightAnchor |
| constraintEqualToConstant:autofill::kInputAccessoryHeight] |
| ]]; |
| if (!self.grayBackgroundView.superview) { |
| [keyboardView addSubview:self.grayBackgroundView]; |
| [keyboardView sendSubviewToBack:self.grayBackgroundView]; |
| AddSameConstraints(self.grayBackgroundView, keyboardView); |
| } |
| } else { |
| UIResponder* firstResponder = GetFirstResponder(); |
| if (firstResponder.inputAccessoryView) { |
| [firstResponder.inputAccessoryView addSubview:self.inputAccessoryView]; |
| AddSameConstraints(self.inputAccessoryView, |
| firstResponder.inputAccessoryView); |
| } |
| } |
| } |
| } |
| |
| #pragma mark - ManualFillAccessoryViewControllerDelegate |
| |
| - (void)keyboardButtonPressed { |
| [self.manualFillAccessoryViewControllerDelegate keyboardButtonPressed]; |
| } |
| |
| - (void)accountButtonPressed:(UIButton*)sender { |
| [self.manualFillAccessoryViewControllerDelegate accountButtonPressed:sender]; |
| } |
| |
| - (void)cardButtonPressed:(UIButton*)sender { |
| [self.manualFillAccessoryViewControllerDelegate cardButtonPressed:sender]; |
| } |
| |
| - (void)passwordButtonPressed:(UIButton*)sender { |
| [self.manualFillAccessoryViewControllerDelegate passwordButtonPressed:sender]; |
| } |
| |
| @end |