blob: bdf372204fd57e2f28fe673c6b75fad900bb37e6 [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.
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_view_controller.h"
#import "base/check.h"
#import "base/ios/ios_util.h"
#import "base/mac/foundation_util.h"
#import "base/metrics/histogram_macros.h"
#import "base/metrics/user_metrics.h"
#import "base/strings/sys_string_conversions.h"
#import "components/signin/public/base/signin_switches.h"
#import "components/strings/grit/components_strings.h"
#import "ios/chrome/browser/ntp/new_tab_page_tab_helper.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/lens_commands.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_commands.h"
#import "ios/chrome/browser/shared/public/commands/open_lens_input_selection_command.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/named_guide.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/content_suggestions/content_suggestions_collection_utils.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_commands.h"
#import "ios/chrome/browser/ui/content_suggestions/content_suggestions_feature.h"
#import "ios/chrome/browser/ui/content_suggestions/ntp_home_constant.h"
#import "ios/chrome/browser/ui/lens/lens_entrypoint.h"
#import "ios/chrome/browser/ui/ntp/logo_vendor.h"
#import "ios/chrome/browser/ui/ntp/metrics/new_tab_page_metrics_recorder.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_controller_delegate.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_commands.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_view.h"
#import "ios/chrome/browser/ui/ntp/new_tab_page_header_view_controller_delegate.h"
#import "ios/chrome/browser/ui/start_surface/start_surface_features.h"
#import "ios/chrome/browser/ui/toolbar/public/fakebox_focuser.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
#import "ios/chrome/common/button_configuration_util.h"
#import "ios/chrome/common/ui/util/constraints_ui_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using base::UserMetricsAction;
namespace {
NSString* const kScribbleFakeboxElementId = @"fakebox";
} // namespace
@interface NewTabPageHeaderViewController () <
DoodleObserver,
UIIndirectScribbleInteractionDelegate,
UIPointerInteractionDelegate>
// `YES` if this consumer is has voice search enabled.
@property(nonatomic, assign) BOOL voiceSearchIsEnabled;
// Exposes view and methods to drive the doodle.
@property(nonatomic, weak, readonly) id<LogoVendor> logoVendor;
@property(nonatomic, strong) NewTabPageHeaderView* headerView;
@property(nonatomic, strong) UIButton* fakeOmnibox;
@property(nonatomic, strong) UIButton* accessibilityButton;
@property(nonatomic, strong) NSString* identityDiscAccessibilityLabel;
@property(nonatomic, strong, readwrite) UIButton* identityDiscButton;
@property(nonatomic, strong) UIImage* identityDiscImage;
@property(nonatomic, strong) UIButton* fakeTapButton;
@property(nonatomic, strong) NSLayoutConstraint* doodleHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint* doodleTopMarginConstraint;
@property(nonatomic, strong) NSLayoutConstraint* fakeOmniboxWidthConstraint;
@property(nonatomic, strong) NSLayoutConstraint* fakeOmniboxHeightConstraint;
@property(nonatomic, strong) NSLayoutConstraint* fakeOmniboxTopMarginConstraint;
@property(nonatomic, strong) NSLayoutConstraint* headerViewHeightConstraint;
@property(nonatomic, assign) BOOL logoFetched;
// Whether the Google logo or doodle is being shown.
@property(nonatomic, assign) BOOL logoIsShowing;
@end
@implementation NewTabPageHeaderViewController
- (instancetype)init {
return [super initWithNibName:nil bundle:nil];
}
#pragma mark - Public
- (UIView*)toolBarView {
return self.headerView.toolBarView;
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollection.horizontalSizeClass !=
previousTraitCollection.horizontalSizeClass) {
[self updateFakeboxDisplay];
}
}
- (void)willTransitionToTraitCollection:(UITraitCollection*)newCollection
withTransitionCoordinator:
(id<UIViewControllerTransitionCoordinator>)coordinator {
[super willTransitionToTraitCollection:newCollection
withTransitionCoordinator:coordinator];
void (^transition)(id<UIViewControllerTransitionCoordinatorContext>) =
^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Ensure omnibox is reset when not a regular tablet.
if (IsSplitToolbarMode(newCollection)) {
[self.toolbarDelegate setScrollProgressForTabletOmnibox:1];
}
// Fake Tap button only needs to work in portrait. Disable the button
// in landscape because in landscape the button covers logoView (which
// need to handle taps).
self.fakeTapButton.userInteractionEnabled = IsSplitToolbarMode(self);
};
[coordinator animateAlongsideTransition:transition completion:nil];
}
- (void)dealloc {
[self.accessibilityButton removeObserver:self forKeyPath:@"highlighted"];
}
- (void)expandHeaderForFocus {
// Make sure that the offset is after the pinned offset to have the fake
// omnibox taking the full width.
CGFloat offset = 9000;
[self.headerView updateSearchFieldWidth:self.fakeOmniboxWidthConstraint
height:self.fakeOmniboxHeightConstraint
topMargin:self.fakeOmniboxTopMarginConstraint
forOffset:offset
screenWidth:self.headerView.bounds.size.width
safeAreaInsets:self.view.safeAreaInsets];
self.fakeOmniboxWidthConstraint.constant = self.headerView.bounds.size.width;
[self.headerView layoutIfNeeded];
NamedGuide* omniboxGuide = [NamedGuide guideWithName:kOmniboxGuide
view:self.headerView];
CGRect omniboxFrameInFakebox =
[[omniboxGuide owningView] convertRect:[omniboxGuide layoutFrame]
toView:self.fakeOmnibox];
self.headerView.fakeLocationBarLeadingConstraint.constant =
omniboxFrameInFakebox.origin.x;
self.headerView.fakeLocationBarTrailingConstraint.constant =
-(self.fakeOmnibox.bounds.size.width -
(omniboxFrameInFakebox.origin.x + omniboxFrameInFakebox.size.width));
self.headerView.voiceSearchButton.alpha = 0;
self.headerView.cancelButton.alpha = 0.7;
self.headerView.omnibox.alpha = 1;
self.headerView.searchHintLabel.alpha = 0;
[self.headerView layoutIfNeeded];
}
- (void)completeHeaderFakeOmniboxFocusAnimationWithFinalPosition:
(UIViewAnimatingPosition)finalPosition {
self.headerView.omnibox.hidden = YES;
self.headerView.cancelButton.hidden = YES;
self.headerView.searchHintLabel.alpha = 1;
self.headerView.voiceSearchButton.alpha = 1;
if (finalPosition == UIViewAnimatingPositionEnd &&
self.delegate.scrolledToMinimumHeight) {
// Check to see if the collection are still scrolled to the top --
// it's possible (and difficult) to unfocus the omnibox and initiate a
// -shiftTilesDownForOmniboxDefocus before the animation here completes.
if (IsSplitToolbarMode(self)) {
[self.dispatcher onFakeboxAnimationComplete];
}
}
}
// TODO(crbug.com/1403613): Name animateScrollAnimation something more aligned
// to its true state indication. Why update the constraints only sometimes?
- (void)updateFakeOmniboxForOffset:(CGFloat)offset
screenWidth:(CGFloat)screenWidth
safeAreaInsets:(UIEdgeInsets)safeAreaInsets
animateScrollAnimation:(BOOL)animateScrollAnimation {
if (self.isShowing) {
CGFloat progress =
self.logoIsShowing || !IsRegularXRegularSizeClass(self)
? [self.headerView searchFieldProgressForOffset:offset
safeAreaInsets:safeAreaInsets]
// RxR with no logo hides the fakebox, so always show the omnibox.
: 1;
if (!IsSplitToolbarMode(self)) {
[self.toolbarDelegate setScrollProgressForTabletOmnibox:progress];
} else {
// Ensure omnibox is reset when not a regular tablet.
[self.toolbarDelegate setScrollProgressForTabletOmnibox:1];
}
}
if (animateScrollAnimation) {
[self.headerView updateSearchFieldWidth:self.fakeOmniboxWidthConstraint
height:self.fakeOmniboxHeightConstraint
topMargin:self.fakeOmniboxTopMarginConstraint
forOffset:offset
screenWidth:screenWidth
safeAreaInsets:safeAreaInsets];
}
}
- (void)updateFakeOmniboxForWidth:(CGFloat)width {
self.fakeOmniboxWidthConstraint.constant =
content_suggestions::SearchFieldWidth(width, self.traitCollection);
}
- (void)layoutHeader {
[self.headerView layoutIfNeeded];
}
// Update the doodle top margin to the new value.
- (void)updateConstraints {
self.doodleTopMarginConstraint.constant =
content_suggestions::DoodleTopMargin([self topInset],
self.traitCollection);
[self.headerView updateForTopSafeAreaInset:[self topInset]];
}
- (CGFloat)pinnedOffsetY {
CGFloat offsetY =
[self headerHeight] - ntp_header::kScrolledToTopOmniboxBottomMargin;
if (IsSplitToolbarMode(self)) {
offsetY -= ToolbarExpandedHeight(
self.traitCollection.preferredContentSizeCategory) +
[self topInset];
}
return AlignValueToPixel(offsetY);
}
- (CGFloat)headerHeight {
return content_suggestions::HeightForLogoHeader(
self.logoIsShowing, self.logoVendor.isShowingDoodle, [self topInset],
self.traitCollection);
}
- (void)viewDidLoad {
[super viewDidLoad];
if (!self.headerView) {
self.view.translatesAutoresizingMaskIntoConstraints = NO;
CGFloat width = self.view.frame.size.width;
self.headerView = [[NewTabPageHeaderView alloc] init];
self.headerView.isGoogleDefaultSearchEngine =
self.isGoogleDefaultSearchEngine;
self.headerView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:self.headerView];
AddSameConstraints(self.headerView, self.view);
[self addFakeOmnibox];
[self.headerView addSubview:self.logoVendor.view];
// Fake Tap View has identity disc, which should render above the doodle.
[self addFakeTapView];
[self.headerView addSubview:self.fakeOmnibox];
self.logoVendor.view.translatesAutoresizingMaskIntoConstraints = NO;
self.logoVendor.view.accessibilityIdentifier =
ntp_home::NTPLogoAccessibilityID();
self.fakeOmnibox.translatesAutoresizingMaskIntoConstraints = NO;
[self.headerView addSeparatorToSearchField:self.fakeOmnibox];
// Identity disc needs to be added after the Google logo/doodle since it
// needs to respond to user taps first.
[self addIdentityDisc];
UIEdgeInsets safeAreaInsets = self.baseViewController.view.safeAreaInsets;
width = std::max<CGFloat>(
0, width - safeAreaInsets.left - safeAreaInsets.right);
self.fakeOmniboxWidthConstraint = [self.fakeOmnibox.widthAnchor
constraintEqualToConstant:content_suggestions::SearchFieldWidth(
width, self.traitCollection)];
[self addConstraintsForLogoView:self.logoVendor.view
fakeOmnibox:self.fakeOmnibox
andHeaderView:self.headerView];
[self.logoVendor fetchDoodle];
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Check if the identity disc button was properly set before the view appears.
DCHECK(self.identityDiscButton);
if (base::FeatureList::IsEnabled(switches::kIdentityStatusConsistency)) {
DCHECK(self.identityDiscImage);
DCHECK(self.identityDiscButton.accessibilityLabel);
DCHECK([self.identityDiscButton imageForState:UIControlStateNormal]);
}
}
#pragma mark - Private
// Initialize and add a search field tap target and a voice search button.
- (void)addFakeOmnibox {
self.fakeOmnibox = [[UIButton alloc] init];
// TODO(crbug.com/1418068): Remove after minimum version required is >=
// iOS 15 and refactor with UIButtonConfiguration.
SetAdjustsImageWhenHighlighted(self.fakeOmnibox, NO);
// Set isAccessibilityElement to NO so that Voice Search button is accessible.
[self.fakeOmnibox setIsAccessibilityElement:NO];
self.fakeOmnibox.accessibilityIdentifier =
ntp_home::FakeOmniboxAccessibilityID();
// Set a button the same size as the fake omnibox as the accessibility
// element. If the hint is the only accessible element, when the fake omnibox
// is taking the full width, there are few points that are not accessible and
// allow to select the content below it.
self.accessibilityButton = [[UIButton alloc] init];
[self.accessibilityButton addTarget:self
action:@selector(fakeboxTapped)
forControlEvents:UIControlEventTouchUpInside];
// Because the visual fakebox background is implemented within
// NewTabPageHeaderView, KVO the highlight events of
// `accessibilityButton` and pass them along.
[self.accessibilityButton addObserver:self
forKeyPath:@"highlighted"
options:NSKeyValueObservingOptionNew
context:NULL];
self.accessibilityButton.isAccessibilityElement = YES;
self.accessibilityButton.accessibilityLabel =
l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT);
[self.fakeOmnibox addSubview:self.accessibilityButton];
self.accessibilityButton.translatesAutoresizingMaskIntoConstraints = NO;
AddSameConstraints(self.fakeOmnibox, self.accessibilityButton);
[self.fakeOmnibox
addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
[self.headerView addViewsToSearchField:self.fakeOmnibox];
UIIndirectScribbleInteraction* scribbleInteraction =
[[UIIndirectScribbleInteraction alloc] initWithDelegate:self];
[self.fakeOmnibox addInteraction:scribbleInteraction];
[self.headerView.voiceSearchButton addTarget:self
action:@selector(loadVoiceSearch:)
forControlEvents:UIControlEventTouchUpInside];
[self.headerView.voiceSearchButton addTarget:self
action:@selector(preloadVoiceSearch:)
forControlEvents:UIControlEventTouchDown];
if (self.headerView.lensButton) {
[self.headerView.lensButton addTarget:self
action:@selector(openLens)
forControlEvents:UIControlEventTouchUpInside];
}
[self updateVoiceSearchDisplay];
}
// On NTP in split toolbar mode the omnibox has different location (in the
// middle of the screen), but the users have muscle memory and still tap on area
// where omnibox is normally placed (the top area of NTP). Fake Tap Button is
// located in the same position where omnibox is normally placed and focuses the
// omnibox when tapped. Fake Tap Button user interactions are only enabled in
// split toolbar mode.
- (void)addFakeTapView {
UIView* toolbar = [[UIView alloc] init];
toolbar.translatesAutoresizingMaskIntoConstraints = NO;
self.fakeTapButton = [[UIButton alloc] init];
self.fakeTapButton.userInteractionEnabled = IsSplitToolbarMode(self);
self.fakeTapButton.isAccessibilityElement = NO;
self.fakeTapButton.translatesAutoresizingMaskIntoConstraints = NO;
[toolbar addSubview:self.fakeTapButton];
[self.headerView addToolbarView:toolbar];
[self.fakeTapButton addTarget:self
action:@selector(fakeTapViewTapped)
forControlEvents:UIControlEventTouchUpInside];
AddSameConstraints(self.fakeTapButton, toolbar);
}
- (void)addIdentityDisc {
// Set up a button. Details for the button will be set through delegate
// implementation of UserAccountImageUpdateDelegate.
self.identityDiscButton = [UIButton buttonWithType:UIButtonTypeCustom];
[self.identityDiscButton addTarget:self.commandHandler
action:@selector(identityDiscWasTapped)
forControlEvents:UIControlEventTouchUpInside];
self.identityDiscButton.pointerInteractionEnabled = YES;
self.identityDiscButton.pointerStyleProvider =
^UIPointerStyle*(UIButton* button, UIPointerEffect* proposedEffect,
UIPointerShape* proposedShape) {
// The identity disc button is oversized to the avatar image to meet the
// minimum touch target dimensions. The hover pointer effect should
// match the avatar image dimensions, not the button dimensions.
CGFloat singleInset =
(button.frame.size.width - ntp_home::kIdentityAvatarDimension) / 2;
CGRect rect = CGRectInset(button.frame, singleInset, singleInset);
UIPointerShape* shape =
[UIPointerShape shapeWithRoundedRect:rect
cornerRadius:rect.size.width / 2];
return [UIPointerStyle styleWithEffect:proposedEffect shape:shape];
};
if (base::FeatureList::IsEnabled(switches::kIdentityStatusConsistency)) {
// `self.identityDiscButton` should not be updated if
// `self.identityDiscImage` is not available yet.
if (self.identityDiscImage) {
[self updateIdentityDiscState];
}
} else {
[self updateIdentityDiscState];
}
[self.headerView setIdentityDiscView:self.identityDiscButton];
}
// Configures `identityDiscButton` with the current state of
// `identityDiscImage`.
- (void)updateIdentityDiscState {
if (base::FeatureList::IsEnabled(switches::kIdentityStatusConsistency)) {
DCHECK(self.identityDiscImage);
DCHECK(self.identityDiscAccessibilityLabel);
} else {
self.identityDiscButton.hidden = !self.identityDiscImage;
}
self.identityDiscButton.accessibilityLabel =
self.identityDiscAccessibilityLabel;
[self.identityDiscButton setImage:self.identityDiscImage
forState:UIControlStateNormal];
self.identityDiscButton.imageView.layer.cornerRadius =
self.identityDiscImage.size.width / 2;
self.identityDiscButton.imageView.layer.masksToBounds = YES;
}
- (void)openLens {
[self.NTPMetricsRecorder recordLensTapped];
OpenLensInputSelectionCommand* command = [[OpenLensInputSelectionCommand
alloc]
initWithEntryPoint:LensEntrypoint::NewTabPage
presentationStyle:LensInputSelectionPresentationStyle::SlideFromRight
presentationCompletion:nil];
[self.dispatcher openLensInputSelection:command];
}
- (void)loadVoiceSearch:(id)sender {
DCHECK(self.voiceSearchIsEnabled);
[self.NTPMetricsRecorder recordVoiceSearchTapped];
UIView* voiceSearchButton = base::mac::ObjCCastStrict<UIView>(sender);
[self.layoutGuideCenter referenceView:voiceSearchButton
underName:kVoiceSearchButtonGuide];
[self.dispatcher startVoiceSearch];
}
- (void)preloadVoiceSearch:(id)sender {
DCHECK(self.voiceSearchIsEnabled);
[sender removeTarget:self
action:@selector(preloadVoiceSearch:)
forControlEvents:UIControlEventTouchDown];
[self.dispatcher preloadVoiceSearch];
}
- (void)fakeTapViewTapped {
[self.NTPMetricsRecorder recordFakeTapViewTapped];
[self.commandHandler fakeboxTapped];
}
- (void)fakeboxTapped {
[self.NTPMetricsRecorder recordFakeOmniboxTapped];
[self.commandHandler fakeboxTapped];
}
- (void)focusAccessibilityOnOmnibox {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
self.fakeOmnibox);
}
// TODO(crbug.com/807330) The fakebox is currently a collection of views spread
// between NewTabPageHeaderViewController and inside
// NewTabPageHeaderView. Post refresh this can be coalesced into one
// control, and the KVO highlight logic below can be removed.
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary*)change
context:(void*)context {
if ([@"highlighted" isEqualToString:keyPath]) {
[self.headerView setFakeboxHighlighted:[object isHighlighted]];
}
}
// If display is compact size, shows fakebox. If display is regular size,
// shows fakebox if the logo is visible and hides otherwise
- (void)updateFakeboxDisplay {
[self.doodleHeightConstraint
setConstant:content_suggestions::DoodleHeight(
self.logoVendor.showingLogo,
self.logoVendor.isShowingDoodle, self.traitCollection)];
self.fakeOmnibox.hidden =
IsRegularXRegularSizeClass(self) && !self.logoIsShowing;
[self.headerView layoutIfNeeded];
self.headerViewHeightConstraint.constant =
content_suggestions::HeightForLogoHeader(
self.logoIsShowing, self.logoVendor.isShowingDoodle, [self topInset],
self.traitCollection);
}
// If Google is not the default search engine, hides the logo, doodle and
// fakebox. Makes them appear if Google is set as default.
- (void)updateLogoAndFakeboxDisplay {
if (self.logoVendor.showingLogo != self.logoIsShowing) {
self.logoVendor.showingLogo = self.logoIsShowing;
[self updateFakeboxDisplay];
}
}
// Ensures the state of the Voice Search button matches whether or not it's
// enabled. If it's not, disables the button and removes it from the a11y loop
// for VoiceOver.
- (void)updateVoiceSearchDisplay {
self.headerView.voiceSearchButton.enabled = self.voiceSearchIsEnabled;
self.headerView.voiceSearchButton.isAccessibilityElement =
self.voiceSearchIsEnabled;
}
// Adds the constraints for the `logoView`, the `fakeomnibox` related to the
// `headerView`. It also sets the properties constraints related to those views.
- (void)addConstraintsForLogoView:(UIView*)logoView
fakeOmnibox:(UIView*)fakeOmnibox
andHeaderView:(UIView*)headerView {
self.doodleTopMarginConstraint = [logoView.topAnchor
constraintEqualToAnchor:headerView.topAnchor
constant:content_suggestions::DoodleTopMargin(
[self topInset], self.traitCollection)];
self.doodleHeightConstraint = [logoView.heightAnchor
constraintEqualToConstant:content_suggestions::DoodleHeight(
self.logoVendor.showingLogo,
self.logoVendor.isShowingDoodle,
self.traitCollection)];
self.fakeOmniboxHeightConstraint = [fakeOmnibox.heightAnchor
constraintEqualToConstant:ToolbarExpandedHeight(
self.traitCollection
.preferredContentSizeCategory)];
self.fakeOmniboxTopMarginConstraint = [logoView.bottomAnchor
constraintEqualToAnchor:fakeOmnibox.topAnchor
constant:-content_suggestions::SearchFieldTopMargin()];
self.headerViewHeightConstraint =
[headerView.heightAnchor constraintEqualToConstant:[self headerHeight]];
self.headerViewHeightConstraint.active = YES;
self.doodleTopMarginConstraint.active = YES;
self.doodleHeightConstraint.active = YES;
self.fakeOmniboxWidthConstraint.active = YES;
self.fakeOmniboxHeightConstraint.active = YES;
self.fakeOmniboxTopMarginConstraint.active = YES;
[logoView.widthAnchor constraintEqualToAnchor:headerView.widthAnchor].active =
YES;
[logoView.leadingAnchor constraintEqualToAnchor:headerView.leadingAnchor]
.active = YES;
[fakeOmnibox.centerXAnchor constraintEqualToAnchor:headerView.centerXAnchor]
.active = YES;
}
- (CGFloat)topInset {
return 0;
}
#pragma mark - UIIndirectScribbleInteractionDelegate
- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
requestElementsInRect:(CGRect)rect
completion:
(void (^)(NSArray<UIScribbleElementIdentifier>*
elements))completion
API_AVAILABLE(ios(14.0)) {
completion(@[ kScribbleFakeboxElementId ]);
}
- (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
isElementFocused:
(UIScribbleElementIdentifier)elementIdentifier
API_AVAILABLE(ios(14.0)) {
DCHECK(elementIdentifier == kScribbleFakeboxElementId);
return self.toolbarDelegate.fakeboxScribbleForwardingTarget.isFirstResponder;
}
- (CGRect)
indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
frameForElement:(UIScribbleElementIdentifier)elementIdentifier
API_AVAILABLE(ios(14.0)) {
DCHECK(elementIdentifier == kScribbleFakeboxElementId);
// Imitate the entire location bar being scribblable.
return interaction.view.bounds;
}
- (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
focusElementIfNeeded:
(UIScribbleElementIdentifier)elementIdentifier
referencePoint:(CGPoint)focusReferencePoint
completion:
(void (^)(UIResponder<UITextInput>* focusedInput))
completion API_AVAILABLE(ios(14.0)) {
if (!self.toolbarDelegate.fakeboxScribbleForwardingTarget.isFirstResponder) {
[self.toolbarDelegate.fakeboxScribbleForwardingTarget becomeFirstResponder];
}
completion(self.toolbarDelegate.fakeboxScribbleForwardingTarget);
}
- (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction*)interaction
shouldDelayFocusForElement:
(UIScribbleElementIdentifier)elementIdentifier
API_AVAILABLE(ios(14.0)) {
DCHECK(elementIdentifier == kScribbleFakeboxElementId);
return YES;
}
#pragma mark - LogoAnimationControllerOwnerOwner
- (id<LogoAnimationControllerOwner>)logoAnimationControllerOwner {
// Only return the logo vendor's animation controller owner if the logo view
// is fully visible. This prevents the logo from being used in transition
// animations if the logo has been scrolled off screen.
UIView* logoView = self.logoVendor.view;
UIView* parentView = self.parentViewController.view;
CGRect logoFrame = [parentView convertRect:logoView.bounds fromView:logoView];
BOOL isLogoFullyVisible = CGRectEqualToRect(
CGRectIntersection(logoFrame, parentView.bounds), logoFrame);
return isLogoFullyVisible ? [self.logoVendor logoAnimationControllerOwner]
: nil;
}
#pragma mark - DoodleObserver
- (void)doodleDisplayStateChanged:(BOOL)doodleShowing {
[self.doodleHeightConstraint
setConstant:content_suggestions::DoodleHeight(self.logoVendor.showingLogo,
doodleShowing,
self.traitCollection)];
self.headerViewHeightConstraint.constant =
content_suggestions::HeightForLogoHeader(
self.logoIsShowing, self.logoVendor.isShowingDoodle, [self topInset],
self.traitCollection);
[self.commandHandler updateForHeaderSizeChange];
}
#pragma mark - NewTabPageHeaderConsumer
- (void)setLogoIsShowing:(BOOL)logoIsShowing {
_logoIsShowing = logoIsShowing;
[self updateLogoAndFakeboxDisplay];
}
- (void)setLogoVendor:(id<LogoVendor>)logoVendor {
_logoVendor = logoVendor;
_logoVendor.doodleObserver = self;
}
- (void)locationBarBecomesFirstResponder {
if (!self.isShowing) {
return;
}
[self.commandHandler fakeboxTapped];
}
- (void)setVoiceSearchIsEnabled:(BOOL)voiceSearchIsEnabled {
if (_voiceSearchIsEnabled == voiceSearchIsEnabled) {
return;
}
_voiceSearchIsEnabled = voiceSearchIsEnabled;
[self updateVoiceSearchDisplay];
}
#pragma mark - UserAccountImageUpdateDelegate
- (void)setSignedOutAccountImage {
if (base::FeatureList::IsEnabled(switches::kIdentityStatusConsistency)) {
self.identityDiscImage = DefaultSymbolTemplateWithPointSize(
kPersonCropCircleSymbol, ntp_home::kSignedOutIdentityIconDimension);
self.identityDiscAccessibilityLabel =
l10n_util::GetNSString(IDS_IOS_IDENTITY_DISC_SIGNED_OUT);
} else {
self.identityDiscImage = nil;
}
// `self.identityDiscButton` should not be updated if the view has not been
// created yet.
if (self.identityDiscButton) {
[self updateIdentityDiscState];
}
}
- (void)updateAccountImage:(UIImage*)image
name:(NSString*)name
email:(NSString*)email {
DCHECK(image && image.size.width == ntp_home::kIdentityAvatarDimension &&
image.size.height == ntp_home::kIdentityAvatarDimension)
<< base::SysNSStringToUTF8([image description]);
DCHECK(email);
self.identityDiscImage = image;
if (name) {
self.identityDiscAccessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_IDENTITY_DISC_WITH_NAME_AND_EMAIL,
base::SysNSStringToUTF16(name), base::SysNSStringToUTF16(email));
} else {
self.identityDiscAccessibilityLabel = l10n_util::GetNSStringF(
IDS_IOS_IDENTITY_DISC_WITH_EMAIL, base::SysNSStringToUTF16(email));
}
// `self.identityDiscButton` should not be updated if the view has not been
// created yet.
if (self.identityDiscButton) {
[self updateIdentityDiscState];
}
}
#pragma mark UIPointerInteractionDelegate
- (UIPointerRegion*)pointerInteraction:(UIPointerInteraction*)interaction
regionForRequest:(UIPointerRegionRequest*)request
defaultRegion:(UIPointerRegion*)defaultRegion {
return defaultRegion;
}
- (UIPointerStyle*)pointerInteraction:(UIPointerInteraction*)interaction
styleForRegion:(UIPointerRegion*)region {
// Without this, the hover effect looks slightly oversized.
CGRect rect = CGRectInset(interaction.view.bounds, 1, 1);
UIBezierPath* path =
[UIBezierPath bezierPathWithRoundedRect:rect
cornerRadius:rect.size.height];
UIPreviewParameters* parameters = [[UIPreviewParameters alloc] init];
parameters.visiblePath = path;
UITargetedPreview* preview =
[[UITargetedPreview alloc] initWithView:interaction.view
parameters:parameters];
UIPointerHoverEffect* effect =
[UIPointerHoverEffect effectWithPreview:preview];
effect.prefersScaledContent = NO;
effect.prefersShadow = NO;
UIPointerShape* shape = [UIPointerShape
beamWithPreferredLength:interaction.view.bounds.size.height / 2
axis:UIAxisVertical];
return [UIPointerStyle styleWithEffect:effect shape:shape];
}
@end