// Copyright 2018 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/toolbar/adaptive_toolbar_view_controller.h"
#import <MaterialComponents/MaterialProgressView.h>
#import "base/metrics/user_metrics.h"
#import "base/notreached.h"
#import "base/time/time.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_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/animation_util.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/ui/fullscreen/fullscreen_animator.h"
#import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_menus_provider.h"
#import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_view.h"
#import "ios/chrome/browser/ui/toolbar/adaptive_toolbar_view_controller_delegate.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_button.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_button_factory.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_configuration.h"
#import "ios/chrome/browser/ui/toolbar/buttons/toolbar_tab_grid_button.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_constants.h"
#import "ios/chrome/browser/ui/toolbar/public/toolbar_utils.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ui/base/device_form_factor.h"
namespace {
const CGFloat kRotationInRadians = 5.0 / 180 * M_PI;
// Scale factor for the animation, must be < 1.
const CGFloat kScaleFactorDiff = 0.50;
const CGFloat kTabGridAnimationsTotalDuration = 0.5;
// The identifier for the context menu action trigger.
NSString* const kContextMenuActionIdentifier = @"kContextMenuActionIdentifier";
// The duration of the slide in animation.
const base::TimeDelta kToobarSlideInAnimationDuration = base::Milliseconds(500);
// Progress of fullscreen when the toolbars are fully visible.
const CGFloat kFullscreenProgressFullyExpanded = 1.0;
} // namespace
@interface AdaptiveToolbarViewController ()
// Redefined to be an AdaptiveToolbarView.
@property(nonatomic, strong) UIView<AdaptiveToolbarView>* view;
// Whether a page is loading.
@property(nonatomic, assign, getter=isLoading) BOOL loading;
@property(nonatomic, assign) BOOL isNTP;
// The last progress of fullscreen registered. The progress range is between 0
// and 1.
@property(nonatomic, assign) CGFloat previousFullscreenProgress;
// The page's theme color.
@property(nonatomic, strong) UIColor* pageThemeColor;
// The under page background color.
@property(nonatomic, strong) UIColor* underPageBackgroundColor;
@end
@implementation AdaptiveToolbarViewController
@dynamic view;
@synthesize buttonFactory = _buttonFactory;
@synthesize loading = _loading;
@synthesize isNTP = _isNTP;
#pragma mark - Public
- (ToolbarButton*)toolsMenuButton {
return self.view.toolsMenuButton;
}
- (void)updateForSideSwipeSnapshot:(BOOL)onNonIncognitoNTP {
self.view.progressBar.hidden = YES;
self.view.progressBar.alpha = 0;
}
- (void)resetAfterSideSwipeSnapshot {
self.view.progressBar.alpha = 1;
}
- (void)triggerToolbarSlideInAnimationFromBelow:(BOOL)fromBelow {
// Toolbar slide-in animations are disabled on iPads.
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
return;
}
const UIView* view = self.view;
CGFloat toolbarHeight = view.frame.size.height;
view.transform = CGAffineTransformMakeTranslation(
0, fromBelow ? toolbarHeight : -toolbarHeight);
auto animations = ^{
[UIView addKeyframeWithRelativeStartTime:0
relativeDuration:1
animations:^{
view.transform = CGAffineTransformIdentity;
}];
};
[UIView
animateKeyframesWithDuration:kToobarSlideInAnimationDuration.InSecondsF()
delay:0
options:UIViewAnimationCurveEaseOut
animations:animations
completion:nil];
}
- (void)setTabGridButtonIPHHighlighted:(BOOL)iphHighlighted {
self.view.tabGridButton.iphHighlighted = iphHighlighted;
}
- (void)setNewTabButtonIPHHighlighted:(BOOL)iphHighlighted {
self.view.openNewTabButton.iphHighlighted = iphHighlighted;
}
- (void)showPrerenderingAnimation {
__weak __typeof__(self) weakSelf = self;
[self.view.progressBar setProgress:0];
if (self.hasOmnibox) {
[self.view.progressBar setHidden:NO
animated:YES
completion:^(BOOL finished) {
[weakSelf stopProgressBar];
}];
}
}
- (BOOL)hasOmnibox {
return self.locationBarViewController != nil;
}
#pragma mark - UIViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self updateAllButtonsVisibility];
}
- (void)viewDidLoad {
[super viewDidLoad];
// The first time, the toolbar is fully displayed.
self.previousFullscreenProgress = kFullscreenProgressFullyExpanded;
[self addStandardActionsForAllButtons];
// Add the layout guide names to the buttons.
self.view.toolsMenuButton.guideName = kToolsMenuGuide;
self.view.tabGridButton.guideName = kTabSwitcherGuide;
self.view.openNewTabButton.guideName = kNewTabButtonGuide;
self.view.forwardButton.guideName = kForwardButtonGuide;
self.view.backButton.guideName = kBackButtonGuide;
self.view.shareButton.guideName = kShareButtonGuide;
[self addLayoutGuideCenterToButtons];
// Add navigation popup menu triggers.
[self configureMenuProviderForButton:self.view.backButton
buttonType:AdaptiveToolbarButtonTypeBack];
[self configureMenuProviderForButton:self.view.forwardButton
buttonType:AdaptiveToolbarButtonTypeForward];
[self configureMenuProviderForButton:self.view.openNewTabButton
buttonType:AdaptiveToolbarButtonTypeNewTab];
[self configureMenuProviderForButton:self.view.tabGridButton
buttonType:AdaptiveToolbarButtonTypeTabGrid];
// LocationBarContainer initial fullscreen progress.
[self updateLocationBarHeightForFullscreenProgress:
kFullscreenProgressFullyExpanded];
// CollapsedToolbarButton exit fullscreen.
[self.view.collapsedToolbarButton
addTarget:self
action:@selector(collapsedToolbarButtonTapped)
forControlEvents:UIControlEventTouchUpInside];
UIHoverGestureRecognizer* hoverGestureRecognizer =
[[UIHoverGestureRecognizer alloc]
initWithTarget:self
action:@selector(exitFullscreen)];
[self.view.collapsedToolbarButton
addGestureRecognizer:hoverGestureRecognizer];
[self traitCollectionDidChange:nil];
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
// Progress bar and buttons visibility.
[self updateAllButtonsVisibility];
if (IsRegularXRegularSizeClass(self)) {
[self.view.progressBar setHidden:YES animated:NO completion:nil];
} else if (self.loading && self.hasOmnibox) {
[self.view.progressBar setHidden:NO animated:NO completion:nil];
}
// Restore locationBarContainer height with previous fullscreen progress.
if (previousTraitCollection.preferredContentSizeCategory !=
self.traitCollection.preferredContentSizeCategory) {
[self updateLocationBarHeightForFullscreenProgress:
self.previousFullscreenProgress];
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// TODO(crbug.com/41413004): Remove this call once iPad trait collection
// override issue is fixed.
[self updateAllButtonsVisibility];
}
- (void)didMoveToParentViewController:(UIViewController*)parent {
[super didMoveToParentViewController:parent];
[self updateAllButtonsVisibility];
}
#pragma mark - Public Properties
- (void)setLayoutGuideCenter:(LayoutGuideCenter*)layoutGuideCenter {
_layoutGuideCenter = layoutGuideCenter;
if (self.isViewLoaded) {
[self addLayoutGuideCenterToButtons];
}
}
- (void)setLocationBarViewController:
(UIViewController*)locationBarViewController {
_locationBarViewController = locationBarViewController;
if (locationBarViewController) {
[self addChildViewController:locationBarViewController];
[locationBarViewController didMoveToParentViewController:self];
[self.view setLocationBarView:locationBarViewController.view];
self.view.locationBarContainer.hidden = NO;
// Update the constraint of the location bar view to make sure the text is
// centered.
[locationBarViewController.view updateConstraintsIfNeeded];
} else {
[self.view setLocationBarView:nil];
self.view.locationBarContainer.hidden = YES;
}
[self updateProgressBarVisibility];
}
#pragma mark - ToolbarConsumer
- (void)setCanGoForward:(BOOL)canGoForward {
self.view.forwardButton.enabled = canGoForward;
}
- (void)setCanGoBack:(BOOL)canGoBack {
self.view.backButton.enabled = canGoBack;
}
- (void)setLoadingState:(BOOL)loading {
if (self.loading == loading) {
return;
}
self.loading = loading;
self.view.reloadButton.hiddenInCurrentState = loading;
self.view.stopButton.hiddenInCurrentState = !loading;
[self.view layoutIfNeeded];
if (!loading) {
[self stopProgressBar];
} else if (self.view.progressBar.hidden &&
!IsRegularXRegularSizeClass(self) && !self.isNTP) {
[self.view.progressBar setProgress:0];
[self updateProgressBarVisibility];
// Layout if needed the progress bar to avoid having the progress bar
// going backward when opening a page from the NTP.
[self.view.progressBar layoutIfNeeded];
}
}
- (void)setLoadingProgressFraction:(double)progress {
[self.view.progressBar setProgress:progress animated:YES completion:nil];
}
- (void)setTabCount:(int)tabCount addedInBackground:(BOOL)inBackground {
if (self.view.tabGridButton.tabCount == tabCount) {
return;
}
CGFloat scaleSign = tabCount > self.view.tabGridButton.tabCount ? 1 : -1;
self.view.tabGridButton.tabCount = tabCount;
if (IsRegularXRegularSizeClass(self)) {
// No animation on Regular x Regular.
return;
}
CGFloat scaleFactor = 1 + scaleSign * kScaleFactorDiff;
CGAffineTransform baseTransform =
inBackground ? CGAffineTransformMakeRotation(kRotationInRadians)
: CGAffineTransformIdentity;
auto animations = ^{
[UIView addKeyframeWithRelativeStartTime:0
relativeDuration:0.5
animations:^{
self.view.tabGridButton.transform =
CGAffineTransformScale(baseTransform,
scaleFactor,
scaleFactor);
}];
[UIView addKeyframeWithRelativeStartTime:0.5
relativeDuration:0.5
animations:^{
self.view.tabGridButton.transform =
CGAffineTransformIdentity;
}];
};
[UIView animateKeyframesWithDuration:kTabGridAnimationsTotalDuration
delay:0
options:UIViewAnimationCurveEaseInOut
animations:animations
completion:nil];
}
- (void)setVoiceSearchEnabled:(BOOL)enabled {
// No-op, should be handled by the location bar.
}
- (void)setShareMenuEnabled:(BOOL)enabled {
self.view.shareButton.enabled = enabled;
}
- (void)setIsNTP:(BOOL)isNTP {
_isNTP = isNTP;
}
- (void)setPageThemeColor:(UIColor*)pageThemeColor {
if ([_pageThemeColor isEqual:pageThemeColor]) {
return;
}
_pageThemeColor = pageThemeColor;
[self updateBackgroundColor];
}
- (void)setUnderPageBackgroundColor:(UIColor*)underPageBackgroundColor {
if ([_underPageBackgroundColor isEqual:underPageBackgroundColor]) {
return;
}
_underPageBackgroundColor = underPageBackgroundColor;
[self updateBackgroundColor];
}
#pragma mark - NewTabPageControllerDelegate
- (void)setScrollProgressForTabletOmnibox:(CGFloat)progress {
// No-op, should be handled by the primary toolbar.
}
#pragma mark - FullscreenUIElement
- (void)updateForFullscreenProgress:(CGFloat)progress {
self.previousFullscreenProgress = progress;
const CGFloat alphaValue = fmax(progress * 2 - 1, 0);
[self updateLocationBarHeightForFullscreenProgress:progress];
self.view.locationBarContainer.backgroundColor =
[self.buttonFactory.toolbarConfiguration
locationBarBackgroundColorWithVisibility:alphaValue];
self.view.collapsedToolbarButton.hidden = progress > 0.05;
}
- (void)updateForFullscreenEnabled:(BOOL)enabled {
if (!enabled) {
[self updateForFullscreenProgress:kFullscreenProgressFullyExpanded];
}
}
- (void)animateFullscreenWithAnimator:(FullscreenAnimator*)animator {
CGFloat finalProgress = animator.finalProgress;
// Using the animator doesn't work as the animation doesn't trigger a relayout
// of the constraints (see crbug.com/978462, crbug.com/950994).
[UIView animateWithDuration:animator.duration
animations:^{
[self updateForFullscreenProgress:finalProgress];
[self.view layoutIfNeeded];
}];
}
#pragma mark - Protected
- (void)stopProgressBar {
__weak AdaptiveToolbarViewController* weakSelf = self;
[self.view.progressBar setProgress:kFullscreenProgressFullyExpanded
animated:YES
completion:^(BOOL finished) {
[weakSelf updateProgressBarVisibility];
}];
}
- (void)collapsedToolbarButtonTapped {
base::RecordAction(base::UserMetricsAction("MobileFullscreenExitedManually"));
[self exitFullscreen];
}
- (void)updateBackgroundColor {
// Implemented in subclass.
}
#pragma mark - PopupMenuUIUpdating
- (void)updateUIForOverflowMenuIPHDisplayed {
self.view.toolsMenuButton.iphHighlighted = YES;
}
- (void)updateUIForIPHDismissed {
self.view.backButton.iphHighlighted = NO;
self.view.forwardButton.iphHighlighted = NO;
self.view.openNewTabButton.iphHighlighted = NO;
self.view.tabGridButton.iphHighlighted = NO;
self.view.toolsMenuButton.iphHighlighted = NO;
}
- (void)setOverflowMenuBlueDot:(BOOL)hasBlueDot {
self.view.toolsMenuButton.hasBlueDot = hasBlueDot;
}
#pragma mark - Private
// Updates `locationBarContainer` height and adjusts its corner radius for the
// fullscreen `progress`
- (void)updateLocationBarHeightForFullscreenProgress:(CGFloat)progress {
const CGFloat expandedHeight =
LocationBarHeight(self.traitCollection.preferredContentSizeCategory);
const CGFloat collapsedHeight =
ToolbarCollapsedHeight(self.traitCollection.preferredContentSizeCategory);
const CGFloat expandedCollapsedDelta = expandedHeight - collapsedHeight;
const CGFloat height =
AlignValueToPixel(collapsedHeight + expandedCollapsedDelta * progress);
self.view.locationBarContainerHeight.constant = height;
self.view.locationBarContainer.layer.cornerRadius = height / 2;
}
// Makes sure that the visibility of the progress bar is matching the one which
// is expected.
- (void)updateProgressBarVisibility {
__weak __typeof(self) weakSelf = self;
BOOL hasOmnibox = self.locationBarViewController != nil;
if (!hasOmnibox) {
self.view.progressBar.hidden = YES;
return;
}
if (self.loading && self.view.progressBar.hidden) {
[self.view.progressBar setHidden:NO
animated:YES
completion:^(BOOL finished) {
[weakSelf updateProgressBarVisibility];
}];
} else if (!self.loading && !self.view.progressBar.hidden) {
[self.view.progressBar setHidden:YES
animated:YES
completion:^(BOOL finished) {
[weakSelf updateProgressBarVisibility];
}];
}
}
// Updates all buttons visibility to match any recent WebState or SizeClass
// change.
- (void)updateAllButtonsVisibility {
for (ToolbarButton* button in self.view.allButtons) {
[button updateHiddenInCurrentSizeClass];
}
}
// Registers the actions which will be triggered when tapping a button.
- (void)addStandardActionsForAllButtons {
for (ToolbarButton* button in self.view.allButtons) {
if (button != self.view.toolsMenuButton &&
button != self.view.openNewTabButton) {
[button addTarget:self.omniboxCommandsHandler
action:@selector(cancelOmniboxEdit)
forControlEvents:UIControlEventTouchUpInside];
}
[button addTarget:self
action:@selector(recordUserMetrics:)
forControlEvents:UIControlEventTouchUpInside];
}
}
// Records the use of a button.
- (IBAction)recordUserMetrics:(id)sender {
if (!sender)
return;
if (sender == self.view.backButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarBack"));
} else if (sender == self.view.forwardButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarForward"));
} else if (sender == self.view.reloadButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarReload"));
} else if (sender == self.view.stopButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarStop"));
} else if (sender == self.view.toolsMenuButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarShowMenu"));
} else if (sender == self.view.tabGridButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarShowStackView"));
} else if (sender == self.view.shareButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarShareMenu"));
} else if (sender == self.view.openNewTabButton) {
base::RecordAction(base::UserMetricsAction("MobileToolbarNewTabShortcut"));
base::RecordAction(base::UserMetricsAction("MobileTabNewTab"));
} else {
NOTREACHED_IN_MIGRATION();
}
}
// Configures `button` with the menu provider, making sure that the items are
// updated when the menu is presented. The `buttonType` is passed to the menu
// provider.
- (void)configureMenuProviderForButton:(UIButton*)button
buttonType:(AdaptiveToolbarButtonType)buttonType {
// Adds an empty menu so the event triggers the first time.
UIMenu* emptyMenu = [UIMenu menuWithChildren:@[]];
button.menu = emptyMenu;
[button removeActionForIdentifier:kContextMenuActionIdentifier
forControlEvents:UIControlEventMenuActionTriggered];
__weak UIButton* weakButton = button;
__weak __typeof(self) weakSelf = self;
UIAction* action = [UIAction
actionWithTitle:@""
image:nil
identifier:kContextMenuActionIdentifier
handler:^(UIAction* uiAction) {
base::RecordAction(
base::UserMetricsAction("MobileMenuToolbarMenuTriggered"));
TriggerHapticFeedbackForImpact(UIImpactFeedbackStyleHeavy);
weakButton.menu =
[weakSelf.menuProvider menuForButtonOfType:buttonType];
}];
[button addAction:action forControlEvents:UIControlEventMenuActionTriggered];
}
- (void)addLayoutGuideCenterToButtons {
self.view.toolsMenuButton.layoutGuideCenter = self.layoutGuideCenter;
self.view.tabGridButton.layoutGuideCenter = self.layoutGuideCenter;
self.view.openNewTabButton.layoutGuideCenter = self.layoutGuideCenter;
self.view.forwardButton.layoutGuideCenter = self.layoutGuideCenter;
self.view.backButton.layoutGuideCenter = self.layoutGuideCenter;
self.view.shareButton.layoutGuideCenter = self.layoutGuideCenter;
}
// Exits fullscreen.
- (void)exitFullscreen {
[self.adaptiveDelegate exitFullscreen];
}
@end