blob: 645a9651e7c63e7355585bf13c83638e123f1d15 [file] [log] [blame]
// Copyright 2012 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/ui/toolbar/web_toolbar_controller.h"
#import <CoreLocation/CoreLocation.h>
#include <QuartzCore/QuartzCore.h>
#include <stdint.h>
#include <algorithm>
#include <memory>
#include "base/command_line.h"
#include "base/ios/weak_nsobject.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/foundation_util.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/strings/sys_string_conversions.h"
#include "components/google/core/browser/google_util.h"
#include "components/omnibox/browser/omnibox_edit_model.h"
#include "components/search_engines/util.h"
#include "components/strings/grit/components_strings.h"
#include "components/toolbar/toolbar_model.h"
#include "ios/chrome/browser/autocomplete/autocomplete_scheme_classifier_impl.h"
#include "ios/chrome/browser/browser_state/chrome_browser_state.h"
#include "ios/chrome/browser/chrome_url_constants.h"
#include "ios/chrome/browser/reading_list/reading_list_model_factory.h"
#include "ios/chrome/browser/search_engines/template_url_service_factory.h"
#import "ios/chrome/browser/tabs/tab.h"
#import "ios/chrome/browser/tabs/tab_model.h"
#import "ios/chrome/browser/ui/animation_util.h"
#import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h"
#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#import "ios/chrome/browser/ui/history/tab_history_popup_controller.h"
#import "ios/chrome/browser/ui/image_util.h"
#import "ios/chrome/browser/ui/keyboard/hardware_keyboard_watcher.h"
#include "ios/chrome/browser/ui/omnibox/location_bar_controller_impl.h"
#include "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h"
#import "ios/chrome/browser/ui/reversed_animation.h"
#include "ios/chrome/browser/ui/rtl_geometry.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_controller+protected.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_controller.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_model_ios.h"
#include "ios/chrome/browser/ui/toolbar/toolbar_resource_macros.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#import "ios/chrome/browser/ui/url_loader.h"
#import "ios/chrome/browser/ui/voice/text_to_speech_player.h"
#import "ios/chrome/browser/ui/voice/voice_search_notification_names.h"
#import "ios/chrome/common/material_timing.h"
#include "ios/chrome/grit/ios_strings.h"
#import "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/images/branded_image_provider.h"
#import "ios/public/provider/chrome/browser/voice/voice_search_provider.h"
#include "ios/shared/chrome/browser/ui/omnibox/location_bar_controller.h"
#include "ios/shared/chrome/browser/ui/omnibox/location_bar_delegate.h"
#import "ios/third_party/material_components_ios/src/components/Palettes/src/MaterialPalettes.h"
#import "ios/third_party/material_components_ios/src/components/ProgressView/src/MaterialProgressView.h"
#import "ios/third_party/material_components_ios/src/components/Typography/src/MaterialTypography.h"
#include "ios/web/public/referrer.h"
#import "ios/web/public/web_state/web_state.h"
#import "net/base/mac/url_conversions.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/page_transition_types.h"
#import "ui/gfx/ios/NSString+CrStringDrawing.h"
using base::UserMetricsAction;
using ios::material::TimingFunction;
NSString* const kTabHistoryPopupWillShowNotification =
@"kTabHistoryPopupWillShowNotification";
NSString* const kTabHistoryPopupWillHideNotification =
@"kTabHistoryPopupWillHideNotification";
const CGFloat kiPhoneOmniboxPlaceholderColorBrightness = 150 / 255.0;
// The histogram recording CLAuthorizationStatus for omnibox queries.
const char* const kOmniboxQueryLocationAuthorizationStatusHistogram =
"Omnibox.QueryIosLocationAuthorizationStatus";
// The number of possible CLAuthorizationStatus values to report.
const int kLocationAuthorizationStatusCount = 4;
namespace {
// The brightness of the toolbar's background color (visible on NTPs when the
// background view is hidden).
const CGFloat kNTPBackgroundColorBrightness = 1.0;
const CGFloat kNTPBackgroundColorBrightnessIncognito = 34.0 / 255.0;
// How far below the omnibox to position the popup.
const CGFloat kiPadOmniboxPopupVerticalOffset = 3;
// Padding to place on the sides of the omnibox when expanded.
const CGFloat kExpandedOmniboxPadding = 6;
// Padding between the back button and the omnibox when the forward button isn't
// displayed.
const CGFloat kBackButtonTrailingPadding = 7.0;
const CGFloat kForwardButtonTrailingPadding = -1.0;
const CGFloat kReloadButtonTrailingPadding = 4.0;
// Cancel button sizing.
const CGFloat kCancelButtonBottomMargin = 4.0;
const CGFloat kCancelButtonTopMargin = 4.0;
const CGFloat kCancelButtonLeadingMargin = 7.0;
const CGFloat kCancelButtonWidth = 40.0;
const CGFloat kIpadButtonTitleFontSize = 20.0;
const CGFloat kIphoneButtonTitleFontSize = 15.0;
// Additional offset to adjust the y coordinate of the determinate progress bar
// up by.
const CGFloat kDeterminateProgressBarYOffset = 1.0;
const CGFloat kMaterialProgressBarHeight = 2.0;
const CGFloat kLoadCompleteHideProgressBarDelay = 0.5;
// The default position animation is 10 pixels toward the trailing side, so
// that's a negative leading offset.
const LayoutOffset kPositionAnimationLeadingOffset = -10.0;
const CGFloat kIPadToolbarY = 53;
const CGFloat kScrollFadeDistance = 30;
// Offset from the image edge to the beginning of the visible omnibox rectangle.
// The image is symmetrical, so the offset is equal on each side.
const CGFloat kBackgroundImageVisibleRectOffset = 6;
enum {
WebToolbarButtonNameBack = NumberOfToolbarButtonNames,
WebToolbarButtonNameCallingApp,
WebToolbarButtonNameForward,
WebToolbarButtonNameReload,
WebToolbarButtonNameStar,
WebToolbarButtonNameStop,
WebToolbarButtonNameTTS,
WebToolbarButtonNameVoice,
NumberOfWebToolbarButtonNames,
};
const CGFloat kWebToolbarWidths[INTERFACE_IDIOM_COUNT] = {224, 720};
// UI layouts. iPhone values followed by iPad values.
// Frame for the WebToolbar-specific container, which is a subview of the
// toolbar view itself.
// clang-format off
const LayoutRect kWebToolbarFrame[INTERFACE_IDIOM_COUNT] = {
{kPortraitWidth[IPHONE_IDIOM], LayoutRectPositionZero,
{kWebToolbarWidths[IPHONE_IDIOM], 56}},
{kPortraitWidth[IPAD_IDIOM], LayoutRectPositionZero,
{kWebToolbarWidths[IPAD_IDIOM], 56}},
};
#define IPHONE_LAYOUT(L, Y, H, W) \
{ kWebToolbarWidths[IPHONE_IDIOM], {L, Y}, {H, W} }
#define IPAD_LAYOUT(L, Y, H, W) \
{ kWebToolbarWidths[IPAD_IDIOM], {L, Y}, {H, W} }
// Layouts inside the WebToolbar frame
const LayoutRect kOmniboxFrame[INTERFACE_IDIOM_COUNT] = {
IPHONE_LAYOUT(55, 7, 169, 43), IPAD_LAYOUT(152, 7, 568, 43),
};
const LayoutRect kBackButtonFrame[INTERFACE_IDIOM_COUNT] = {
IPHONE_LAYOUT(0, 4, 48, 48), IPAD_LAYOUT(4, 4, 48, 48),
};
const LayoutRect kForwardButtonFrame[INTERFACE_IDIOM_COUNT] = {
IPHONE_LAYOUT(48, 4, 48, 48), IPAD_LAYOUT(52, 4, 48, 48),
};
// clang-format on
// iPad-only layouts
// Layout for both the stop and reload buttons, which are in the same location.
const LayoutRect kStopReloadButtonFrame = IPAD_LAYOUT(100, 4, 48, 48);
const LayoutRect kStarButtonFrame = IPAD_LAYOUT(644, 4, 36, 48);
const LayoutRect kVoiceSearchButtonFrame = IPAD_LAYOUT(680, 4, 36, 48);
// Vertical distance between the y-coordinate of the omnibox's center and the
// y-coordinate of the web toolbar's center.
const CGFloat kOmniboxCenterOffsetY = 0.5;
// Last button in accessory view for keyboard, commonly used TLD.
NSString* const kDotComTLD = @".com";
void RunBlockAfterDelay(NSTimeInterval delay, void (^block)(void)) {
DCHECK([NSThread isMainThread]);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * delay),
dispatch_get_main_queue(), block);
}
CGRect RectShiftedDownForStatusBar(CGRect rect) {
if (IsIPadIdiom())
return rect;
rect.origin.y += StatusBarHeight();
return rect;
}
CGRect RectShiftedUpForStatusBar(CGRect rect) {
if (IsIPadIdiom())
return rect;
rect.origin.y -= StatusBarHeight();
return rect;
}
CGRect RectShiftedUpAndResizedForStatusBar(CGRect rect) {
if (IsIPadIdiom())
return rect;
rect.size.height += StatusBarHeight();
return RectShiftedUpForStatusBar(rect);
}
CGRect RectShiftedDownAndResizedForStatusBar(CGRect rect) {
if (IsIPadIdiom())
return rect;
rect.size.height -= StatusBarHeight();
return RectShiftedDownForStatusBar(rect);
}
} // namespace
// View for the accessory view above the keyboard. Subclassed to allow playing
// input clicks when pressed.
@interface KeyboardAccessoryView : UIInputView<UIInputViewAudioFeedback>
@end
@implementation KeyboardAccessoryView
- (BOOL)enableInputClicksWhenVisible {
return YES;
}
@end
// TODO(crbug.com/619982) Remove this block and add CAAnimationDelegate when we
// switch the main bots to Xcode 8.
#if defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0)
@interface WebToolbarController ()<CAAnimationDelegate>
@end
#endif
@interface WebToolbarController ()<LocationBarDelegate,
OmniboxPopupPositioner,
ToolbarFrameDelegate> {
// Top-level view for web content.
base::scoped_nsobject<UIView> _webToolbar;
base::scoped_nsobject<UIButton> _backButton;
base::scoped_nsobject<UIButton> _forwardButton;
base::scoped_nsobject<UIButton> _reloadButton;
base::scoped_nsobject<UIButton> _stopButton;
base::scoped_nsobject<UIButton> _starButton;
base::scoped_nsobject<UIButton> _voiceSearchButton;
base::scoped_nsobject<OmniboxTextFieldIOS> _omniBox;
base::scoped_nsobject<UIButton> _cancelButton;
base::scoped_nsobject<UIView> _keyBoardAccessoryView;
base::scoped_nsobject<UIButton> _keyboardVoiceSearchButton;
// Progress bar used to show what fraction of the page has loaded.
base::scoped_nsobject<MDCProgressView> _determinateProgressView;
base::scoped_nsobject<UIImageView> _omniboxBackground;
BOOL _prerenderAnimating;
base::scoped_nsobject<UIImageView> _incognitoIcon;
base::scoped_nsobject<UIView> _clippingView;
std::unique_ptr<LocationBarController> _locationBar;
BOOL _initialLayoutComplete;
// If |YES|, toolbar is incognito.
BOOL _incognito;
// If set to |YES|, disables animations that tests would otherwise trigger.
BOOL _unitTesting;
// If set to |YES|, text to speech is currently playing and the toolbar voice
// icon should indicate so.
BOOL _isTTSPlaying;
// Keeps track of whether or not the back button's images have been reversed.
ToolbarButtonMode _backButtonMode;
// Keeps track of whether or not the forward button's images have been
// reversed.
ToolbarButtonMode _forwardButtonMode;
// Keeps track of last known trait collection used by the subviews.
base::scoped_nsobject<UITraitCollection> _lastKnownTraitCollection;
// A snapshot of the current toolbar view. Only valid for phone, will be nil
// if on tablet.
base::scoped_nsobject<UIImage> _snapshot;
// A hash of the state of the toolbar when the snapshot was taken.
uint32_t _snapshotHash;
// View controller for displaying tab history when the user long presses the
// back or forward button. nil if not visible.
base::scoped_nsobject<TabHistoryPopupController> _tabHistoryPopupController;
// Hardware keyboard watcher, to detect the type of keyboard currently
// attached.
base::scoped_nsobject<HardwareKeyboardWatcher> _hardwareKeyboardWatcher;
// The current browser state.
ios::ChromeBrowserState* _browserState; // weak
}
// Accessor for cancel button. Handles lazy initialization.
- (UIButton*)cancelButton;
// Handler called after user pressed the cancel button.
- (void)cancelButtonPressed:(id)sender;
- (void)layoutCancelButton;
// Change the location bar dimensions according to the focus status.
// Also show/hide relevant buttons.
- (void)layoutOmnibox;
- (void)setBackButtonEnabled:(BOOL)enabled;
// Show or hide the forward button, animating the frame of the location bar to
// be in the right position. This can be called multiple times.
- (void)setForwardButtonEnabled:(BOOL)enabled;
- (void)startProgressBar;
- (void)stopProgressBar;
- (void)hideProgressBarAndTakeSnapshot;
- (void)showReloadButton;
- (void)showStopButton;
// Creates a hash of the state of the toolbar to know whether or not the cached
// snapshot is out of date.
// The hash takes into account any UI that may change the appearance of the
// toolbar. The one UI state it ignores is the stack view button's press
// state. That is because we want the snapshot to be valid when the stack is
// pressed, rather than creating a new snapshot exactly during the user
// interaction this snapshot is aimed to optimize.
- (uint32_t)snapshotHashWithWidth:(CGFloat)width;
// Called by long press gesture recognizer, used to display back/forward
// history.
- (void)handleLongPress:(UILongPressGestureRecognizer*)gesture;
- (void)setImagesForNavButton:(UIButton*)button
withTabHistoryVisible:(BOOL)tabHistoryVisible;
// Received when a TTS player has received audio data.
- (void)audioReadyForPlayback:(NSNotification*)notification;
// Updates the TTS button depending on whether or not TTS is currently playing.
- (void)updateIsTTSPlaying:(NSNotification*)notify;
// Moves VoiceOver to the button used to perform a voice search.
- (void)moveVoiceOverToVoiceSearchButton;
// Fade in and out toolbar items as the frame moves off screen.
- (void)updateToolbarAlphaForFrame:(CGRect)frame;
// Navigate to |query| from omnibox.
- (void)loadURLForQuery:(NSString*)query;
- (UIView*)keyboardButtonWithTitle:(NSString*)title frame:(CGRect)frame;
// Lazily instantiate the keyboard accessory view.
- (UIView*)keyboardAccessoryView;
- (void)preloadVoiceSearch:(id)sender;
// Calculates the CGRect to use for the omnibox's frame. Also sets the frames
// of some buttons and |_webToolbar|.
- (CGRect)newOmniboxFrame;
- (void)animateMaterialOmnibox;
- (void)fadeInOmniboxTrailingView;
- (void)fadeInOmniboxLeadingView;
- (void)fadeOutOmniboxTrailingView;
- (void)fadeOutOmniboxLeadingView;
- (void)fadeInIncognitoIcon;
- (void)fadeOutIncognitoIcon;
// Fade in the visible navigation buttons.
- (void)fadeInNavigationControls;
// Fade out the visible navigation buttons.
- (void)fadeOutNavigationControls;
// When the collapse animation is complete, hide the Material background and
// restore the omnibox's background image.
- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag;
- (void)updateSnapshotWithWidth:(CGFloat)width forced:(BOOL)force;
// Insert 'com' without the period if cursor is directly after a period.
- (NSString*)updateTextForDotCom:(NSString*)text;
// Handle the user pressing a key in the keyboard accessory view.
- (void)pressKey:(id)sender;
@end
@implementation WebToolbarController
@synthesize delegate = _delegate;
@synthesize urlLoader = _urlLoader;
- (instancetype)initWithDelegate:(id<WebToolbarDelegate>)delegate
urlLoader:(id<UrlLoader>)urlLoader
browserState:(ios::ChromeBrowserState*)browserState
preloadProvider:(id<PreloadProvider>)preloader {
DCHECK(delegate);
DCHECK(urlLoader);
DCHECK(browserState);
_delegate = delegate;
_urlLoader = urlLoader;
_browserState = browserState;
_incognito = browserState->IsOffTheRecord();
self = [super initWithStyle:(_incognito ? ToolbarControllerStyleIncognitoMode
: ToolbarControllerStyleLightMode)];
if (!self)
return nil;
self.readingListModel =
ReadingListModelFactory::GetForBrowserState(browserState);
InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM;
// Note that |_webToolbar| gets its frame set to -specificControlArea later in
// this method.
_webToolbar.reset([[UIView alloc]
initWithFrame:LayoutRectGetRect(kWebToolbarFrame[idiom])]);
UIColor* textColor =
_incognito
? [UIColor whiteColor]
: [UIColor colorWithWhite:0 alpha:[MDCTypography body1FontOpacity]];
UIColor* tintColor = _incognito ? textColor : nil;
CGRect omniboxRect = LayoutRectGetRect(kOmniboxFrame[idiom]);
_omniBox.reset([[OmniboxTextFieldIOS alloc]
initWithFrame:omniboxRect
font:[MDCTypography subheadFont]
textColor:textColor
tintColor:tintColor]);
if (_incognito) {
[_omniBox setIncognito:YES];
[_omniBox
setSelectedTextBackgroundColor:[UIColor colorWithWhite:1 alpha:0.1]];
[_omniBox setPlaceholderTextColor:[UIColor colorWithWhite:1 alpha:0.5]];
} else if (!IsIPadIdiom()) {
// Set placeholder text color to match fakebox placeholder text color when
// on iPhone and in regular mode.
UIColor* placeholderTextColor =
[UIColor colorWithWhite:kiPhoneOmniboxPlaceholderColorBrightness
alpha:1.0];
[_omniBox setPlaceholderTextColor:placeholderTextColor];
}
_backButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(kBackButtonFrame[idiom])]);
[_backButton setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleBottomMargin];
// Note that the forward button gets repositioned when -layoutOmnibox is
// called.
_forwardButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(kForwardButtonFrame[idiom])]);
[_forwardButton
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
[_webToolbar addSubview:_backButton];
[_webToolbar addSubview:_forwardButton];
// _omniboxBackground needs to be added under _omniBox so as not to cover up
// _omniBox.
_omniboxBackground.reset([[UIImageView alloc] initWithFrame:omniboxRect]);
[_omniboxBackground
setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin];
if (idiom == IPAD_IDIOM) {
[_webToolbar addSubview:_omniboxBackground];
} else {
[_backButton setImageEdgeInsets:UIEdgeInsetsMakeDirected(0, 0, 0, -9)];
[_forwardButton setImageEdgeInsets:UIEdgeInsetsMakeDirected(0, -7, 0, 0)];
CGRect clippingFrame =
RectShiftedUpAndResizedForStatusBar(kToolbarFrame[idiom]);
_clippingView.reset([[UIView alloc] initWithFrame:clippingFrame]);
[_clippingView setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin];
[_clippingView setClipsToBounds:YES];
[_clippingView setUserInteractionEnabled:NO];
[_webToolbar addSubview:_clippingView];
CGRect omniboxBackgroundFrame =
RectShiftedDownForStatusBar([_omniboxBackground frame]);
[_omniboxBackground setFrame:omniboxBackgroundFrame];
[_clippingView addSubview:_omniboxBackground];
[self.view
setBackgroundColor:[UIColor colorWithWhite:kNTPBackgroundColorBrightness
alpha:1.0]];
if (_incognito) {
[self.view
setBackgroundColor:
[UIColor colorWithWhite:kNTPBackgroundColorBrightnessIncognito
alpha:1.0]];
_incognitoIcon.reset([[UIImageView alloc]
initWithImage:[UIImage imageNamed:@"incognito_marker_typing"]]);
[_incognitoIcon setAlpha:0];
[_incognitoIcon
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin()];
[self layoutIncognitoIcon];
[_webToolbar addSubview:_incognitoIcon];
}
}
[_webToolbar addSubview:_omniBox];
[_backButton setEnabled:NO];
[_forwardButton setEnabled:NO];
if (idiom == IPAD_IDIOM) {
// Note that the reload button gets repositioned when -layoutOmnibox is
// called.
_reloadButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(kStopReloadButtonFrame)]);
[_reloadButton
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
_stopButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(kStopReloadButtonFrame)]);
[_stopButton
setAutoresizingMask:UIViewAutoresizingFlexibleTrailingMargin() |
UIViewAutoresizingFlexibleBottomMargin];
_starButton.reset(
[[UIButton alloc] initWithFrame:LayoutRectGetRect(kStarButtonFrame)]);
[_starButton setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleLeadingMargin()];
_voiceSearchButton.reset([[UIButton alloc]
initWithFrame:LayoutRectGetRect(kVoiceSearchButtonFrame)]);
[_voiceSearchButton
setAutoresizingMask:UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleLeadingMargin()];
[_webToolbar addSubview:_voiceSearchButton];
[_webToolbar addSubview:_starButton];
[_webToolbar addSubview:_stopButton];
[_webToolbar addSubview:_reloadButton];
[self setUpButton:_voiceSearchButton
withImageEnum:WebToolbarButtonNameVoice
forInitialState:UIControlStateNormal
hasDisabledImage:NO
synchronously:NO];
[self setUpButton:_starButton
withImageEnum:WebToolbarButtonNameStar
forInitialState:UIControlStateNormal
hasDisabledImage:NO
synchronously:YES];
[self setUpButton:_stopButton
withImageEnum:WebToolbarButtonNameStop
forInitialState:UIControlStateDisabled
hasDisabledImage:YES
synchronously:NO];
[self setUpButton:_reloadButton
withImageEnum:WebToolbarButtonNameReload
forInitialState:UIControlStateNormal
hasDisabledImage:YES
synchronously:NO];
[_stopButton setHidden:YES];
} else {
[_forwardButton setAlpha:0.0];
}
// Set up the button images and omnibox background.
[self setUpButton:_backButton
withImageEnum:WebToolbarButtonNameBack
forInitialState:UIControlStateDisabled
hasDisabledImage:YES
synchronously:NO];
[self setUpButton:_forwardButton
withImageEnum:WebToolbarButtonNameForward
forInitialState:UIControlStateDisabled
hasDisabledImage:YES
synchronously:NO];
_backButtonMode = ToolbarButtonModeNormal;
_forwardButtonMode = ToolbarButtonModeNormal;
base::scoped_nsobject<UILongPressGestureRecognizer> backLongPress(
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)]);
[_backButton addGestureRecognizer:backLongPress];
base::scoped_nsobject<UILongPressGestureRecognizer> forwardLongPress(
[[UILongPressGestureRecognizer alloc]
initWithTarget:self
action:@selector(handleLongPress:)]);
[_forwardButton addGestureRecognizer:forwardLongPress];
// TODO(leng): Consider moving this to a pak file as well. For now,
// because it is also used by find_bar_controller_ios, leave it as is.
NSString* imageName =
_incognito ? @"omnibox_transparent_background" : @"omnibox_background";
[_omniboxBackground setImage:StretchableImageNamed(imageName, 12, 12)];
[_omniBox setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin];
[_reloadButton addTarget:self
action:@selector(cancelOmniboxEdit)
forControlEvents:UIControlEventTouchUpInside];
[_stopButton addTarget:self
action:@selector(cancelOmniboxEdit)
forControlEvents:UIControlEventTouchUpInside];
[_backButton setTag:IDC_BACK];
[_forwardButton setTag:IDC_FORWARD];
[_reloadButton setTag:IDC_RELOAD];
[_stopButton setTag:IDC_STOP];
[_starButton setTag:IDC_BOOKMARK_PAGE];
[_voiceSearchButton setTag:IDC_VOICE_SEARCH];
SetA11yLabelAndUiAutomationName(_backButton, IDS_ACCNAME_BACK, @"Back");
SetA11yLabelAndUiAutomationName(_forwardButton, IDS_ACCNAME_FORWARD,
@"Forward");
SetA11yLabelAndUiAutomationName(_reloadButton, IDS_IOS_ACCNAME_RELOAD,
@"Reload");
SetA11yLabelAndUiAutomationName(_stopButton, IDS_IOS_ACCNAME_STOP, @"Stop");
SetA11yLabelAndUiAutomationName(_starButton, IDS_TOOLTIP_STAR, @"Bookmark");
SetA11yLabelAndUiAutomationName(
_voiceSearchButton, IDS_IOS_ACCNAME_VOICE_SEARCH, @"Voice Search");
SetA11yLabelAndUiAutomationName(_omniBox, IDS_ACCNAME_LOCATION, @"Address");
// Resize the container to match the available area.
[self.view addSubview:_webToolbar];
[_webToolbar setAutoresizingMask:UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin];
[_webToolbar setFrame:[self specificControlsArea]];
_locationBar = base::MakeUnique<LocationBarControllerImpl>(
_omniBox, _browserState, preloader, self, self);
// Create the determinate progress bar (phone only).
if (idiom == IPHONE_IDIOM) {
CGFloat progressWidth = self.view.frame.size.width;
CGFloat progressHeight = 0;
progressHeight = kMaterialProgressBarHeight;
_determinateProgressView.reset([[MDCProgressView alloc] init]);
_determinateProgressView.get().hidden = YES;
[_determinateProgressView
setProgressTintColor:[MDCPalette cr_bluePalette].tint500];
[_determinateProgressView
setTrackTintColor:[MDCPalette cr_bluePalette].tint100];
int progressBarYOffset =
floor(progressHeight / 2) + kDeterminateProgressBarYOffset;
int progressBarY = self.view.bounds.size.height - progressBarYOffset;
CGRect progressBarFrame =
CGRectMake(0, progressBarY, progressWidth, progressHeight);
[_determinateProgressView setFrame:progressBarFrame];
[self.view addSubview:_determinateProgressView];
}
// Attach the spacebar view to the omnibox.
[_omniBox setInputAccessoryView:[self keyboardAccessoryView]];
// Add the handler to preload voice search when the voice search button is
// tapped, but only if voice search is enabled.
if (ios::GetChromeBrowserProvider()
->GetVoiceSearchProvider()
->IsVoiceSearchEnabled()) {
[_voiceSearchButton addTarget:self
action:@selector(preloadVoiceSearch:)
forControlEvents:UIControlEventTouchDown];
} else {
[_voiceSearchButton setEnabled:NO];
}
// Register for text-to-speech (TTS) events on tablet.
if (idiom == IPAD_IDIOM) {
NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
[defaultCenter addObserver:self
selector:@selector(audioReadyForPlayback:)
name:kTTSAudioReadyForPlaybackNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(updateIsTTSPlaying:)
name:kTTSWillStartPlayingNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(updateIsTTSPlaying:)
name:kTTSDidStopPlayingNotification
object:nil];
[defaultCenter addObserver:self
selector:@selector(moveVoiceOverToVoiceSearchButton)
name:kVoiceSearchWillHideNotification
object:nil];
}
[self.view setDelegate:self];
return self;
}
- (void)browserStateDestroyed {
// The location bar has a browser state reference, so must be destroyed at
// this point.
_locationBar.reset();
_browserState = nullptr;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_tabHistoryPopupController setDelegate:nil];
[super dealloc];
}
#pragma mark -
#pragma mark Public methods.
- (void)updateToolbarState {
ToolbarModelIOS* toolbarModelIOS = [self.delegate toolbarModelIOS];
if (toolbarModelIOS->IsLoading()) {
[self showStopButton];
[self startProgressBar];
[_determinateProgressView
setProgress:toolbarModelIOS->GetLoadProgressFraction()
animated:YES
completion:nil];
} else {
[self stopProgressBar];
[self showReloadButton];
}
_locationBar->SetShouldShowHintText(toolbarModelIOS->ShouldDisplayHintText());
_locationBar->OnToolbarUpdated();
BOOL forwardButtonEnabled = toolbarModelIOS->CanGoForward();
[self setBackButtonEnabled:toolbarModelIOS->CanGoBack()];
[self setForwardButtonEnabled:forwardButtonEnabled];
// Update the bookmarked "starred" state of current tab.
[_starButton setSelected:toolbarModelIOS->IsCurrentTabBookmarked()];
if (!_initialLayoutComplete)
_initialLayoutComplete = YES;
if (!toolbarModelIOS->IsLoading() && !IsIPadIdiom())
[self updateSnapshotWithWidth:0 forced:NO];
}
- (void)updateToolbarForSideSwipeSnapshot:(Tab*)tab {
web::WebState* webState = tab.webState;
BOOL isCurrentTab = webState == [self.delegate currentWebState];
BOOL isNTP = webState->GetVisibleURL() == GURL(kChromeUINewTabURL);
// Don't do anything for a live non-ntp tab.
if (isCurrentTab && !isNTP) {
// This has the effect of making any alpha-ed out items visible.
[self updateToolbarAlphaForFrame:CGRectZero];
[_omniBox setHidden:NO];
return;
}
[[self view] setHidden:NO];
[_determinateProgressView setHidden:YES];
BOOL forwardEnabled = tab.canGoForward;
[_backButton setHidden:isNTP ? !forwardEnabled : NO];
[_backButton setEnabled:tab.canGoBack];
[_forwardButton setHidden:!forwardEnabled];
[_forwardButton setEnabled:forwardEnabled];
[_omniBox setHidden:YES];
[self.backgroundView setAlpha:isNTP ? 0 : 1];
[_omniboxBackground setHidden:isNTP ? YES : NO];
[self hideViewsForNewTabPage:isNTP ? YES : NO];
[self layoutOmnibox];
}
- (void)resetToolbarAfterSideSwipeSnapshot {
[_omniBox setHidden:NO];
[_backButton setHidden:NO];
[_forwardButton setHidden:NO];
[_omniboxBackground setHidden:NO];
[self.backgroundView setAlpha:1];
[self hideViewsForNewTabPage:NO];
[self updateToolbarState];
}
- (void)showPrerenderingAnimation {
_prerenderAnimating = YES;
[_determinateProgressView setProgress:0];
[_determinateProgressView setHidden:NO
animated:YES
completion:^(BOOL finished) {
[_determinateProgressView
setProgress:1
animated:YES
completion:^(BOOL finished) {
[_determinateProgressView setHidden:YES
animated:YES
completion:nil];
}];
}];
}
- (void)setControlsHidden:(BOOL)hidden {
[self setStandardControlsVisible:!hidden];
[_webToolbar setHidden:hidden];
}
- (void)setControlsAlpha:(CGFloat)alpha {
[self setStandardControlsAlpha:alpha];
[_webToolbar setAlpha:alpha];
}
- (void)currentPageLoadStarted {
[self startProgressBar];
}
- (void)selectedTabChanged {
[self cancelOmniboxEdit];
}
- (CGRect)bookmarkButtonAnchorRect {
// Shrink the padding around the bookmark button so the popovers are anchored
// correctly.
return CGRectInset([_starButton bounds], 6, 11);
}
- (UIView*)bookmarkButtonView {
return _starButton.get();
}
- (CGRect)visibleOmniboxFrame {
CGRect frame = _omniboxBackground.get().frame;
frame = [self.view.superview convertRect:frame
fromView:[_omniboxBackground superview]];
// Account for the omnibox background image transparent sides.
return CGRectInset(frame, -kBackgroundImageVisibleRectOffset, 0);
}
- (UIImage*)snapshotWithWidth:(CGFloat)width {
if (IsIPadIdiom())
return nil;
// Below call will be no-op if cached snapshot is valid.
[self updateSnapshotWithWidth:width forced:YES];
return [[_snapshot retain] autorelease];
}
- (void)showTabHistoryPopupInView:(UIView*)view
withItems:(const web::NavigationItemList&)items
forBackHistory:(BOOL)isBackHistory {
if (_tabHistoryPopupController)
return;
base::RecordAction(UserMetricsAction("MobileToolbarShowTabHistoryMenu"));
UIButton* historyButton = isBackHistory ? _backButton : _forwardButton;
// Keep the button pressed by swapping the normal and highlighted images.
[self setImagesForNavButton:historyButton withTabHistoryVisible:YES];
// Set the origin for the tools popup to the leading side of the bottom of the
// pressed buttons.
CGRect buttonBounds = [historyButton.imageView bounds];
CGPoint origin = CGPointMake(CGRectGetLeadingEdge(buttonBounds),
CGRectGetMaxY(buttonBounds));
CGPoint convertedOrigin =
[view convertPoint:origin fromView:historyButton.imageView];
_tabHistoryPopupController.reset([[TabHistoryPopupController alloc]
initWithOrigin:convertedOrigin
parentView:view
items:items]);
[_tabHistoryPopupController setDelegate:self];
// Fade in the popup and notify observers.
CGRect containerFrame = [[_tabHistoryPopupController popupContainer] frame];
CGPoint destination = CGPointMake(CGRectGetLeadingEdge(containerFrame),
CGRectGetMinY(containerFrame));
[_tabHistoryPopupController fadeInPopupFromSource:convertedOrigin
toDestination:destination];
[[NSNotificationCenter defaultCenter]
postNotificationName:kTabHistoryPopupWillShowNotification
object:nil];
}
- (void)dismissTabHistoryPopup {
if (!_tabHistoryPopupController)
return;
TabHistoryPopupController* tempTHPC = _tabHistoryPopupController.get();
[tempTHPC containerView].userInteractionEnabled = NO;
[tempTHPC dismissAnimatedWithCompletion:^{
// Unpress the back/forward button by restoring the normal and
// highlighted images to their usual state.
[self setImagesForNavButton:_backButton withTabHistoryVisible:NO];
[self setImagesForNavButton:_forwardButton withTabHistoryVisible:NO];
// Reference tempTHPC so the block retains it.
[tempTHPC self];
}];
// reset _tabHistoryPopupController to prevent -applicationDidEnterBackground
// from posting another kTabHistoryPopupWillHideNotification.
_tabHistoryPopupController.reset();
[[NSNotificationCenter defaultCenter]
postNotificationName:kTabHistoryPopupWillHideNotification
object:nil];
}
- (BOOL)isOmniboxFirstResponder {
return [_omniBox isFirstResponder];
}
- (BOOL)showingOmniboxPopup {
OmniboxViewIOS* omniboxViewIOS =
static_cast<OmniboxViewIOS*>(_locationBar.get()->GetLocationEntry());
return omniboxViewIOS->IsPopupOpen();
}
- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
_lastKnownTraitCollection.reset([[UITraitCollection
traitCollectionWithTraitsFromCollections:@[ self.view.traitCollection ]]
retain]);
if (IsIPadIdiom()) {
// Update toolbar accessory views.
BOOL isCompactTabletView = IsCompactTablet(self.view);
[_voiceSearchButton setHidden:isCompactTabletView];
[_starButton setHidden:isCompactTabletView];
[_reloadButton setHidden:isCompactTabletView];
[_stopButton setHidden:isCompactTabletView];
[self updateToolbarState];
// Update keyboard accessory views.
BOOL hidden = [_keyboardVoiceSearchButton isHidden];
_keyBoardAccessoryView.reset();
[_omniBox setInputAccessoryView:[self keyboardAccessoryView]];
[_keyboardVoiceSearchButton setHidden:hidden];
if ([_omniBox isFirstResponder]) {
[_omniBox reloadInputViews];
}
// Re-layout toolbar and omnibox.
[_webToolbar setFrame:[self specificControlsArea]];
[self layoutOmnibox];
}
}
#pragma mark -
#pragma mark Overridden superclass methods.
- (void)applicationDidEnterBackground:(NSNotification*)notify {
if (_tabHistoryPopupController) {
// Dismiss the tab history popup without animation.
[self setImagesForNavButton:_backButton withTabHistoryVisible:NO];
[self setImagesForNavButton:_forwardButton withTabHistoryVisible:NO];
_tabHistoryPopupController.reset(nil);
[[NSNotificationCenter defaultCenter]
postNotificationName:kTabHistoryPopupWillHideNotification
object:nil];
}
[super applicationDidEnterBackground:notify];
}
- (void)setUpButton:(UIButton*)button
withImageEnum:(int)imageEnum
forInitialState:(UIControlState)initialState
hasDisabledImage:(BOOL)hasDisabledImage
synchronously:(BOOL)synchronously {
[super setUpButton:button
withImageEnum:imageEnum
forInitialState:initialState
hasDisabledImage:hasDisabledImage
synchronously:synchronously];
if (button != _starButton.get())
return;
// The star button behaves slightly differently. It uses the pressed
// image for its selected state as well as its pressed state.
void (^starBlock)(void) = ^{
UIImage* starImage = [self imageForImageEnum:WebToolbarButtonNameStar
forState:ToolbarButtonUIStatePressed];
[_starButton setAdjustsImageWhenHighlighted:NO];
[_starButton setImage:starImage forState:UIControlStateSelected];
};
if (synchronously) {
starBlock();
} else {
dispatch_time_t addImageDelay =
dispatch_time(DISPATCH_TIME_NOW, kNonInitialImageAdditionDelayNanosec);
dispatch_after(addImageDelay, dispatch_get_main_queue(), starBlock);
}
}
- (void)standardButtonPressed:(UIButton*)sender {
[super standardButtonPressed:sender];
[self cancelOmniboxEdit];
}
- (IBAction)recordUserMetrics:(id)sender {
if (sender == _backButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarBack"));
} else if (sender == _forwardButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarForward"));
} else if (sender == _reloadButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarReload"));
} else if (sender == _stopButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarStop"));
} else if (sender == _voiceSearchButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarVoiceSearch"));
} else if (sender == _keyboardVoiceSearchButton.get()) {
base::RecordAction(UserMetricsAction("MobileCustomRowVoiceSearch"));
} else if (sender == _starButton.get()) {
base::RecordAction(UserMetricsAction("MobileToolbarToggleBookmark"));
} else {
[super recordUserMetrics:sender];
}
}
- (IBAction)stackButtonTouchDown:(id)sender {
[self.delegate prepareToEnterTabSwitcher:self];
}
- (BOOL)imageShouldFlipForRightToLeftLayoutDirection:(int)imageEnum {
DCHECK(imageEnum < NumberOfWebToolbarButtonNames);
if (imageEnum < NumberOfToolbarButtonNames)
return [super imageShouldFlipForRightToLeftLayoutDirection:imageEnum];
if (imageEnum == WebToolbarButtonNameBack ||
imageEnum == WebToolbarButtonNameForward ||
imageEnum == WebToolbarButtonNameReload ||
imageEnum == WebToolbarButtonNameCallingApp) {
return YES;
}
return NO;
}
- (int)imageEnumForButton:(UIButton*)button {
if (button == _voiceSearchButton.get())
return _isTTSPlaying ? WebToolbarButtonNameTTS : WebToolbarButtonNameVoice;
if (button == _starButton.get())
return WebToolbarButtonNameStar;
if (button == _stopButton.get())
return WebToolbarButtonNameStop;
if (button == _reloadButton.get())
return WebToolbarButtonNameReload;
if (button == _backButton.get())
return WebToolbarButtonNameBack;
if (button == _forwardButton.get())
return WebToolbarButtonNameForward;
return [super imageEnumForButton:button];
}
- (int)imageIdForImageEnum:(int)index
style:(ToolbarControllerStyle)style
forState:(ToolbarButtonUIState)state {
DCHECK(style < ToolbarControllerStyleMaxStyles);
DCHECK(state < NumberOfToolbarButtonUIStates);
// Additional checking. These three buttons should only ever be used on iPad.
DCHECK(IsIPadIdiom() || index != WebToolbarButtonNameStar);
DCHECK(IsIPadIdiom() || index != WebToolbarButtonNameTTS);
DCHECK(IsIPadIdiom() || index != WebToolbarButtonNameVoice);
if (index >= NumberOfWebToolbarButtonNames)
NOTREACHED();
if (index < NumberOfToolbarButtonNames)
return [super imageIdForImageEnum:index style:style forState:state];
// Incognito mode gets dark buttons.
if (style == ToolbarControllerStyleIncognitoMode)
style = ToolbarControllerStyleDarkMode;
// Some images can be overridden by the branded image provider.
if (index == WebToolbarButtonNameVoice) {
int image_id;
if (ios::GetChromeBrowserProvider()
->GetBrandedImageProvider()
->GetToolbarVoiceSearchButtonImageId(&image_id)) {
return image_id;
}
// Otherwise fall through to the default voice search button images below.
}
// Rebase |index| so that it properly indexes into the array below.
index -= NumberOfToolbarButtonNames;
const int numberOfAddedNames =
NumberOfWebToolbarButtonNames - NumberOfToolbarButtonNames;
// Name, style [light, dark], UIControlState [normal, pressed, disabled]
static int buttonImageIds[numberOfAddedNames][2]
[NumberOfToolbarButtonUIStates] = {
TOOLBAR_IDR_THREE_STATE(BACK),
TOOLBAR_IDR_LIGHT_ONLY_TWO_STATE(CALLINGAPP),
TOOLBAR_IDR_THREE_STATE(FORWARD),
TOOLBAR_IDR_THREE_STATE(RELOAD),
TOOLBAR_IDR_TWO_STATE(STAR),
TOOLBAR_IDR_THREE_STATE(STOP),
TOOLBAR_IDR_TWO_STATE(TTS),
TOOLBAR_IDR_TWO_STATE(VOICE),
};
return buttonImageIds[index][style][state];
}
- (void)animateTransitionWithBeginFrame:(CGRect)beginFrame
endFrame:(CGRect)endFrame
transitionStyle:(ToolbarTransitionStyle)style {
// Convert background to clear for animations. This is necessary because we
// need to see the animating buttons on the card's tab, which are occurring
// behind the toolbar. The desired color is restored upon completion.
self.view.backgroundColor = [UIColor clearColor];
// Add base toolbar animations
[super animateTransitionWithBeginFrame:beginFrame
endFrame:endFrame
transitionStyle:style];
// Animation values
CAAnimation* frameAnimation = nil;
CFTimeInterval frameDuration = ios::material::kDuration1;
CAMediaTimingFunction* frameTiming =
TimingFunction(ios::material::CurveEaseInOut);
CGRect beginBounds = {CGPointZero, beginFrame.size};
CGRect endBounds = {CGPointZero, endFrame.size};
// Animate web toolbar: Maintain the trailing padding so that toolbar buttons
// on the trailing side have enough room, and ensure that the height is at
// most the specific control area's height.
CGRect specificControlsArea = [self specificControlsArea];
CGFloat webToolbarTrailingPadding =
CGRectGetTrailingLayoutOffsetInBoundingRect(specificControlsArea,
self.view.bounds);
CGFloat webToolbarMaxHeight = specificControlsArea.size.height;
UIEdgeInsets webToolbarInsets =
UIEdgeInsetsMakeDirected(0, 0, 0, webToolbarTrailingPadding);
webToolbarInsets.top =
std::max<CGFloat>(0.f, beginBounds.size.height - webToolbarMaxHeight);
CGRect webToolbarBeginFrame =
UIEdgeInsetsInsetRect(beginBounds, webToolbarInsets);
webToolbarInsets.top =
std::max<CGFloat>(0.f, endBounds.size.height - webToolbarMaxHeight);
CGRect webToolbarEndFrame =
UIEdgeInsetsInsetRect(endBounds, webToolbarInsets);
frameAnimation = FrameAnimationMake([_webToolbar layer], webToolbarBeginFrame,
webToolbarEndFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
[self.transitionLayers addObject:[_webToolbar layer]];
[[_webToolbar layer] addAnimation:frameAnimation
forKey:kToolbarTransitionAnimationKey];
// Animate omnibox: center the omnibox vertically within the card web toolbar,
// maintain its leading offset, and adjusting its width to match the available
// space on the card.
CGFloat omniboxHeight = [_omniBox frame].size.height;
LayoutRect toolbarOmniboxLayout = LayoutRectForRectInBoundingRect(
[_omniBox frame], [_omniBox superview].bounds);
CGFloat omniboxLeading = toolbarOmniboxLayout.position.leading;
LayoutRect omniboxBeginLayout = toolbarOmniboxLayout;
omniboxBeginLayout.boundingWidth = CGRectGetWidth(webToolbarBeginFrame);
omniboxBeginLayout.position.originY =
kOmniboxCenterOffsetY +
0.5 * (CGRectGetHeight(webToolbarBeginFrame) - omniboxHeight);
omniboxBeginLayout.size.width =
CGRectGetWidth(webToolbarBeginFrame) - omniboxLeading;
omniboxBeginLayout.size.height = omniboxHeight;
LayoutRect omniboxEndLayout = toolbarOmniboxLayout;
omniboxEndLayout.boundingWidth = CGRectGetWidth(webToolbarEndFrame);
omniboxEndLayout.position.originY =
kOmniboxCenterOffsetY +
0.5 * (CGRectGetHeight(webToolbarEndFrame) - omniboxHeight);
omniboxEndLayout.size.width =
CGRectGetWidth(webToolbarEndFrame) - omniboxLeading;
omniboxEndLayout.size.height = omniboxHeight;
CGRect omniboxBeginFrame =
AlignRectOriginAndSizeToPixels(LayoutRectGetRect(omniboxBeginLayout));
CGRect omniboxEndFrame =
AlignRectOriginAndSizeToPixels(LayoutRectGetRect(omniboxEndLayout));
frameAnimation =
FrameAnimationMake([_omniBox layer], omniboxBeginFrame, omniboxEndFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
[self.transitionLayers addObject:[_omniBox layer]];
[[_omniBox layer] addAnimation:frameAnimation
forKey:kToolbarTransitionAnimationKey];
[_omniBox
animateFadeWithStyle:((style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW)
? OMNIBOX_TEXT_FIELD_FADE_STYLE_OUT
: OMNIBOX_TEXT_FIELD_FADE_STYLE_IN)];
// Animate clipping view: convert the begin and end bounds since
// |_clippingView| is a subview of |_webToolbar|.
CGRect clippingViewBeginFrame =
CGRectOffset(beginBounds, -webToolbarBeginFrame.origin.x,
-webToolbarBeginFrame.origin.y);
CGRect clippingViewEndFrame = CGRectOffset(
endBounds, -webToolbarEndFrame.origin.x, -webToolbarEndFrame.origin.y);
frameAnimation = FrameAnimationMake(
[_clippingView layer], clippingViewBeginFrame, clippingViewEndFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
[self.transitionLayers addObject:[_clippingView layer]];
[[_clippingView layer] addAnimation:frameAnimation
forKey:kToolbarTransitionAnimationKey];
// Animate omnibox background: the frames should match the omnibox, but in
// |_clippingView|'s coordinate system.
CGRect omniboxBackgroundBeginFrame =
CGRectOffset(omniboxBeginFrame, -clippingViewBeginFrame.origin.x,
-clippingViewBeginFrame.origin.y);
CGRect omniboxBackgroundEndFrame =
CGRectOffset(omniboxEndFrame, -clippingViewEndFrame.origin.x,
-clippingViewEndFrame.origin.y);
frameAnimation = FrameAnimationMake([_omniboxBackground layer],
omniboxBackgroundBeginFrame,
omniboxBackgroundEndFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
BOOL shouldFadeOutOmnibox = (style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW);
CAAnimation* opacityAnimation = OpacityAnimationMake(
shouldFadeOutOmnibox ? 1.0 : 0.0, shouldFadeOutOmnibox ? 0.0 : 1.0);
opacityAnimation.duration = shouldFadeOutOmnibox ? ios::material::kDuration8
: ios::material::kDuration6;
opacityAnimation.beginTime =
shouldFadeOutOmnibox ? 0.0 : ios::material::kDuration8;
opacityAnimation.timingFunction =
TimingFunction(shouldFadeOutOmnibox ? ios::material::CurveEaseIn
: ios::material::CurveEaseOut);
CAAnimation* animationGroup =
AnimationGroupMake(@[ frameAnimation, opacityAnimation ]);
[self.transitionLayers addObject:[_omniboxBackground layer]];
[[_omniboxBackground layer] addAnimation:animationGroup
forKey:kToolbarTransitionAnimationKey];
// Animate progress bar: Match the width and bottom edge of the content
// bounds while maintaining the height.
int progressBarYOffset =
floor(kMaterialProgressBarHeight / 2) + kDeterminateProgressBarYOffset;
CGRect progressBarBeginFrame = AlignRectToPixel(CGRectMake(
beginBounds.origin.x, beginBounds.size.height - progressBarYOffset,
beginBounds.size.width, kMaterialProgressBarHeight));
CGRect progressBarEndFrame = AlignRectToPixel(
CGRectMake(endBounds.origin.x, endBounds.size.height - progressBarYOffset,
endBounds.size.width, kMaterialProgressBarHeight));
frameAnimation =
FrameAnimationMake([_determinateProgressView layer],
progressBarBeginFrame, progressBarEndFrame);
frameAnimation.duration = frameDuration;
frameAnimation.timingFunction = frameTiming;
[self.transitionLayers addObject:[_determinateProgressView layer]];
[[_determinateProgressView layer]
addAnimation:frameAnimation
forKey:kToolbarTransitionAnimationKey];
// Animate buttons
[self animateTransitionForButtonsInView:_webToolbar
containerBeginBounds:beginBounds
containerEndBounds:endBounds
transitionStyle:style];
}
- (void)reverseTransitionAnimations {
[super reverseTransitionAnimations];
[_omniBox reverseFadeAnimations];
}
- (void)cleanUpTransitionAnimations {
CGFloat backgroundColorBrightness =
_incognito ? kNTPBackgroundColorBrightnessIncognito
: kNTPBackgroundColorBrightness;
self.view.backgroundColor =
[UIColor colorWithWhite:backgroundColorBrightness alpha:1.0];
[super cleanUpTransitionAnimations];
[_omniBox cleanUpFadeAnimations];
}
- (void)hideViewsForNewTabPage:(BOOL)hide {
[super hideViewsForNewTabPage:hide];
if (_incognito) {
CGFloat alpha = hide ? 0 : 1;
[self.backgroundView setAlpha:alpha];
}
}
#pragma mark -
#pragma mark LocationBarDelegate methods.
- (void)loadGURLFromLocationBar:(const GURL&)url
transition:(ui::PageTransition)transition {
if (url.SchemeIs(url::kJavaScriptScheme)) {
// Evaluate the URL as JavaScript if its scheme is JavaScript.
NSString* jsToEval = [base::SysUTF8ToNSString(url.GetContent())
stringByRemovingPercentEncoding];
[self.delegate loadJavaScriptFromLocationBar:jsToEval];
} else {
// When opening a URL, force the omnibox to resign first responder. This
// will also close the popup.
// TODO(rohitrao): Is it ok to call |cancelOmniboxEdit| after |loadURL|? It
// doesn't seem to be causing major problems. If we call cancel before
// load, then any prerendered pages get destroyed before the call to load.
[self.urlLoader loadURL:url
referrer:web::Referrer()
transition:transition
rendererInitiated:NO];
if (google_util::IsGoogleSearchUrl(url)) {
UMA_HISTOGRAM_ENUMERATION(
kOmniboxQueryLocationAuthorizationStatusHistogram,
[CLLocationManager authorizationStatus],
kLocationAuthorizationStatusCount);
}
}
[self cancelOmniboxEdit];
}
- (void)locationBarHasBecomeFirstResponder {
[self.delegate locationBarDidBecomeFirstResponder:self];
[self animateMaterialOmnibox];
[_keyboardVoiceSearchButton setHidden:NO];
// Record the appropriate user action for focusing the omnibox.
web::WebState* webState = [self.delegate currentWebState];
if (webState) {
if (webState->GetVisibleURL() == GURL(kChromeUINewTabURL)) {
OmniboxEditModel* model = _locationBar->GetLocationEntry()->model();
if (model->is_caret_visible()) {
base::RecordAction(
base::UserMetricsAction("MobileFocusedOmniboxOnNtp"));
} else {
base::RecordAction(
base::UserMetricsAction("MobileFocusedFakeboxOnNtp"));
}
} else {
base::RecordAction(
base::UserMetricsAction("MobileFocusedOmniboxNotOnNtp"));
}
}
}
- (void)locationBarHasResignedFirstResponder {
[self.delegate locationBarDidResignFirstResponder:self];
[self animateMaterialOmnibox];
}
- (void)locationBarBeganEdit {
[self.delegate locationBarBeganEdit:self];
}
- (void)locationBarChanged {
// Hide the voice search button once the user starts editing the omnibox but
// show it if the omnibox is empty.
bool isEditingOrEmpty = _locationBar->GetLocationEntry()->IsEditingOrEmpty();
BOOL editingAndNotEmpty = isEditingOrEmpty && _omniBox.get().text.length != 0;
// If the voice search button is visible but about to be hidden (i.e.
// the omnibox is no longer empty) then this is the first omnibox text so
// record a user action.
if (![_keyboardVoiceSearchButton isHidden] && editingAndNotEmpty) {
base::RecordAction(UserMetricsAction("MobileFirstTextInOmnibox"));
}
[_keyboardVoiceSearchButton setHidden:editingAndNotEmpty];
}
- (web::WebState*)getWebState {
return [self.delegate currentWebState];
}
- (ToolbarModel*)toolbarModel {
ToolbarModelIOS* toolbarModelIOS = [self.delegate toolbarModelIOS];
return toolbarModelIOS ? toolbarModelIOS->GetToolbarModel() : nullptr;
}
#pragma mark -
#pragma mark OmniboxFocuser methods.
- (void)focusOmnibox {
if (![_webToolbar isHidden])
[_omniBox becomeFirstResponder];
}
- (void)cancelOmniboxEdit {
_locationBar->HideKeyboardAndEndEditing();
[self updateToolbarState];
}
- (void)focusFakebox {
if (IsIPadIdiom()) {
OmniboxEditModel* model = _locationBar->GetLocationEntry()->model();
// Setting the caret visibility to false causes OmniboxEditModel to indicate
// that omnibox interaction was initiated from the fakebox. Note that
// SetCaretVisibility is a no-op unless OnSetFocus is called first. Only
// set fakebox on iPad, where there is a distinction between the omnibox
// and the fakebox on the NTP. On iPhone there is no visible omnibox, so
// there's no need to indicate interaction was initiated from the fakebox.
model->OnSetFocus(false);
model->SetCaretVisibility(false);
} else {
// Set the omnibox background's frame to full bleed.
CGRect mobFrame = CGRectInset([_clippingView bounds], -2, -2);
[_omniboxBackground setFrame:mobFrame];
}
[self focusOmnibox];
}
- (void)onFakeboxBlur {
DCHECK(!IsIPadIdiom());
// Hide the toolbar if the NTP is currently displayed.
web::WebState* webState = [self.delegate currentWebState];
if (webState && (webState->GetVisibleURL() == GURL(kChromeUINewTabURL))) {
[self.view setHidden:YES];
}
}
- (void)onFakeboxAnimationComplete {
DCHECK(!IsIPadIdiom());
[self.view setHidden:NO];
}
#pragma mark -
#pragma mark OmniboxPopupPositioner methods.
- (UIView*)popupAnchorView {
return self.view;
}
- (CGRect)popupFrame:(CGFloat)height {
UIView* parent = [[self popupAnchorView] superview];
CGRect frame = [parent bounds];
// Set tablet popup width to the same width and origin of omnibox.
if (IsIPadIdiom()) {
// For iPad, the omnibox visually extends to include the voice search button
// on the right. Start with the field's frame in |parent|'s coordinate
// system.
CGRect fieldFrame =
[parent convertRect:[_omniBox bounds] fromView:_omniBox];
// Now create a new frame that's below the field, stretching the full width
// of |parent|, minus an inset on each side.
CGFloat maxY = CGRectGetMaxY(fieldFrame);
// The popup extends to the full width of the screen.
frame.origin.x = 0;
frame.size.width = self.view.frame.size.width;
frame.origin.y = maxY + kiPadOmniboxPopupVerticalOffset;
frame.size.height = height;
} else {
// For iPhone place the popup just below the toolbar.
CGRect fieldFrame =
[parent convertRect:[_webToolbar bounds] fromView:_webToolbar];
frame.origin.y =
CGRectGetMaxY(fieldFrame) - [ToolbarController toolbarDropShadowHeight];
frame.size.height = CGRectGetMaxY([parent bounds]) - frame.origin.y;
}
return frame;
}
#pragma mark -
#pragma mark PopupMenuDelegate methods.
- (void)dismissPopupMenu:(PopupMenuController*)controller {
if ([controller isKindOfClass:[TabHistoryPopupController class]] &&
(TabHistoryPopupController*)controller == _tabHistoryPopupController)
[self dismissTabHistoryPopup];
else
[super dismissPopupMenu:controller];
}
#pragma mark -
#pragma mark ToolbarFrameDelegate methods.
- (void)frameDidChangeFrame:(CGRect)newFrame fromFrame:(CGRect)oldFrame {
if (oldFrame.origin.y == newFrame.origin.y)
return;
[self updateToolbarAlphaForFrame:newFrame];
}
- (void)windowDidChange {
if (![_lastKnownTraitCollection
containsTraitsInCollection:self.view.traitCollection]) {
[self traitCollectionDidChange:_lastKnownTraitCollection];
}
}
#pragma mark -
#pragma mark VoiceSearchControllerDelegate methods.
- (void)receiveVoiceSearchResult:(NSString*)result {
DCHECK(result);
[self loadURLForQuery:result];
}
#pragma mark -
#pragma mark QRScannerViewControllerDelegate methods.
- (void)receiveQRScannerResult:(NSString*)result loadImmediately:(BOOL)load {
DCHECK(result);
if (load) {
[self loadURLForQuery:result];
} else {
[self focusOmnibox];
[_omniBox insertTextWhileEditing:result];
// Notify the accessibility system to start reading the new contents of the
// Omnibox.
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
_omniBox);
}
}
#pragma mark -
#pragma mark Private methods.
- (UIButton*)cancelButton {
if (_cancelButton)
return _cancelButton;
_cancelButton.reset([[UIButton buttonWithType:UIButtonTypeCustom] retain]);
NSString* collapseName = _incognito ? @"collapse_incognito" : @"collapse";
[_cancelButton setImage:[UIImage imageNamed:collapseName]
forState:UIControlStateNormal];
NSString* collapsePressedName =
_incognito ? @"collapse_pressed_incognito" : @"collapse_pressed";
[_cancelButton setImage:[UIImage imageNamed:collapsePressedName]
forState:UIControlStateHighlighted];
[_cancelButton setAccessibilityLabel:l10n_util::GetNSString(IDS_CANCEL)];
[_cancelButton setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() |
UIViewAutoresizingFlexibleHeight];
[_cancelButton addTarget:self
action:@selector(cancelButtonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[_webToolbar addSubview:_cancelButton];
return _cancelButton;
}
- (void)cancelButtonPressed:(id)sender {
[self cancelOmniboxEdit];
}
- (void)layoutCancelButton {
CGFloat height = CGRectGetHeight([self specificControlsArea]) -
kCancelButtonTopMargin - kCancelButtonBottomMargin;
LayoutRect cancelButtonLayout;
cancelButtonLayout.position.leading =
CGRectGetWidth([_webToolbar bounds]) - height;
cancelButtonLayout.boundingWidth = CGRectGetWidth([_webToolbar bounds]);
cancelButtonLayout.position.originY = kCancelButtonTopMargin;
cancelButtonLayout.size = CGSizeMake(kCancelButtonWidth, height);
CGRect frame = LayoutRectGetRect(cancelButtonLayout);
// Use the property to force creation.
[self.cancelButton setFrame:frame];
}
- (void)layoutIncognitoIcon {
LayoutRect iconLayout = LayoutRectForRectInBoundingRect(
[_incognitoIcon frame], [_webToolbar bounds]);
iconLayout.position.leading = 16;
iconLayout.position.originY = floor(
(kToolbarFrame[IPHONE_IDIOM].size.height - iconLayout.size.height) / 2);
[_incognitoIcon setFrame:LayoutRectGetRect(iconLayout)];
}
- (void)layoutOmnibox {
// Position the forward and reload buttons trailing the back button in that
// order.
LayoutRect leadingControlLayout = LayoutRectForRectInBoundingRect(
[_backButton frame], [_webToolbar bounds]);
LayoutRect forwardButtonLayout =
LayoutRectGetTrailingLayout(leadingControlLayout);
forwardButtonLayout.position.originY = [_forwardButton frame].origin.y;
forwardButtonLayout.size = [_forwardButton frame].size;
LayoutRect reloadButtonLayout =
LayoutRectGetTrailingLayout(forwardButtonLayout);
reloadButtonLayout.position.originY = [_reloadButton frame].origin.y;
reloadButtonLayout.size = [_reloadButton frame].size;
CGRect newForwardButtonFrame = LayoutRectGetRect(forwardButtonLayout);
CGRect newReloadButtonFrame = LayoutRectGetRect(reloadButtonLayout);
CGRect newOmniboxFrame = [self newOmniboxFrame];
BOOL isPad = IsIPadIdiom();
BOOL growOmnibox = [_omniBox isFirstResponder];
// Animate buttons. Hide most of the buttons (standard set, back, forward)
// for extended omnibox layout. Also show an extra cancel button so the
// extended mode can be cancelled.
CGFloat alphaForNormalOmniboxButton = growOmnibox ? 0.0 : 1.0;
CGFloat alphaForGrownOmniboxButton = growOmnibox ? 1.0 : 0.0;
CGFloat forwardButtonAlpha =
[_forwardButton isEnabled] || (isPad && !IsCompactTablet(self.view))
? alphaForNormalOmniboxButton
: 0.0;
CGFloat backButtonAlpha = alphaForNormalOmniboxButton;
// For the initial layout/testing we just set the length of animation to 0.0
// which means the changes will be done without any animation.
[UIView animateWithDuration:((_initialLayoutComplete && !_unitTesting) ? 0.2
: 0.0)
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction |
UIViewAnimationOptionBeginFromCurrentState
animations:^{
if (!isPad)
[self setStandardControlsVisible:!growOmnibox];
if (!(isPad && growOmnibox)) {
[_backButton setAlpha:backButtonAlpha];
[_forwardButton setAlpha:forwardButtonAlpha];
[_forwardButton setFrame:newForwardButtonFrame];
[_cancelButton setAlpha:alphaForGrownOmniboxButton];
[_reloadButton setFrame:newReloadButtonFrame];
[_stopButton setFrame:newReloadButtonFrame];
}
}
completion:nil];
if (CGRectEqualToRect([_omniBox frame], newOmniboxFrame))
return;
// Hide the clear and voice search buttons during omniBox frame animations.
[_omniBox setRightViewMode:UITextFieldViewModeNever];
// Make sure the accessory images are in the correct positions so they do not
// move during the animation.
[_omniBox rightView].frame =
[_omniBox rightViewRectForBounds:newOmniboxFrame];
[_omniBox leftView].frame = [_omniBox leftViewRectForBounds:newOmniboxFrame];
CGRect materialBackgroundFrame = RectShiftedDownForStatusBar(newOmniboxFrame);
// Extreme jank happens during initial layout if an animation is invoked. Not
// certain why. o_O
// Duration set to 0.0 prevents the animation during initial layout.
[UIView animateWithDuration:((_initialLayoutComplete && !_unitTesting) ? 0.2
: 0.0)
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[_omniBox setFrame:newOmniboxFrame];
[_omniboxBackground setFrame:materialBackgroundFrame];
}
completion:^(BOOL finished) {
[_omniBox setRightViewMode:UITextFieldViewModeAlways];
}];
}
- (void)setBackButtonEnabled:(BOOL)enabled {
[_backButton setEnabled:enabled];
}
- (void)setForwardButtonEnabled:(BOOL)enabled {
if (enabled == [_forwardButton isEnabled])
return;
[_forwardButton setEnabled:enabled];
if (!enabled && [_forwardButton isHidden])
return;
[self layoutOmnibox];
}
- (void)startProgressBar {
if ([_determinateProgressView isHidden]) {
[_determinateProgressView setProgress:0];
[_determinateProgressView setHidden:NO animated:YES completion:nil];
}
}
- (void)stopProgressBar {
if (_determinateProgressView && ![_determinateProgressView isHidden]) {
// Update the toolbar snapshot, but only after the progress bar has
// disappeared.
if (!_prerenderAnimating) {
// Calling -completeAndHide while a prerender animation is in progress
// will result in hiding the progress bar before the animation is
// complete.
[_determinateProgressView setProgress:1
animated:YES
completion:^(BOOL finished) {
[_determinateProgressView setHidden:YES
animated:YES
completion:nil];
}];
}
CGFloat delay = _unitTesting ? 0 : kLoadCompleteHideProgressBarDelay;
[self performSelector:@selector(hideProgressBarAndTakeSnapshot)
withObject:nil
afterDelay:delay];
}
}
- (void)hideProgressBarAndTakeSnapshot {
// The UI may have been torn down while this selector was queued. If
// |self.delegate| is nil, it is not safe to continue.
if (!self.delegate)
return;
[_determinateProgressView setHidden:YES];
[self updateSnapshotWithWidth:0 forced:NO];
_prerenderAnimating = NO;
}
- (void)showReloadButton {
if (!IsCompactTablet(self.view)) {
[_reloadButton setHidden:NO];
[_stopButton setHidden:YES];
}
}
- (void)showStopButton {
if (!IsCompactTablet(self.view)) {
[_reloadButton setHidden:YES];
[_stopButton setHidden:NO];
}
}
- (uint32_t)snapshotHashWithWidth:(CGFloat)width {
uint32_t hash = [super snapshotHash];
// Take only the lower 3 bits of the UIButton state, as they are the only
// ones that change per enabled, highlighted, or nomal.
const uint32_t kButtonStateMask = 0x07;
hash ^= ([_backButton state] & kButtonStateMask) |
(([_forwardButton state] & kButtonStateMask) << 3) |
(([_cancelButton state] & kButtonStateMask) << 6);
// Omnibox size & text it contains.
hash ^= [[_omniBox text] hash];
hash ^= static_cast<uint32_t>([_omniBox frame].size.width) << 16;
hash ^= static_cast<uint32_t>([_omniBox frame].size.height) << 24;
// Also note progress bar state.
float progress = 0;
if (_determinateProgressView && ![_determinateProgressView isHidden])
progress = [_determinateProgressView progress];
// The progress is in the range 0 to 1, so static_cast<uint32_t> won't work.
// Normally, static_cast does the right thing: truncates the float to an int.
// Here, that would not provide the necessary granularity.
hash ^= *(reinterpret_cast<uint32_t*>(&progress));
// Size changes matter.
hash ^= static_cast<uint32_t>(width) << 15;
hash ^= static_cast<uint32_t>([self view].frame.size.height) << 23;
return hash;
}
- (void)handleLongPress:(UILongPressGestureRecognizer*)gesture {
if (gesture.state != UIGestureRecognizerStateBegan)
return;
if (gesture.view == _backButton.get()) {
base::scoped_nsobject<GenericChromeCommand> command(
[[GenericChromeCommand alloc] initWithTag:IDC_SHOW_BACK_HISTORY]);
[_backButton chromeExecuteCommand:command];
} else if (gesture.view == _forwardButton.get()) {
base::scoped_nsobject<GenericChromeCommand> command(
[[GenericChromeCommand alloc] initWithTag:IDC_SHOW_FORWARD_HISTORY]);
[_forwardButton chromeExecuteCommand:command];
}
}
- (void)setImagesForNavButton:(UIButton*)button
withTabHistoryVisible:(BOOL)tabHistoryVisible {
BOOL isBackButton = button == _backButton;
ToolbarButtonMode newMode =
tabHistoryVisible ? ToolbarButtonModeReversed : ToolbarButtonModeNormal;
if (isBackButton && newMode == _backButtonMode)
return;
if (!isBackButton && newMode == _forwardButtonMode)
return;
base::scoped_nsobject<UIImage> normalImage(
[[button imageForState:UIControlStateNormal] retain]);
base::scoped_nsobject<UIImage> highlightedImage(
[[button imageForState:UIControlStateHighlighted] retain]);
[button setImage:highlightedImage forState:UIControlStateNormal];
[button setImage:normalImage forState:UIControlStateHighlighted];
if (isBackButton)
_backButtonMode = newMode;
else
_forwardButtonMode = newMode;
}
- (void)audioReadyForPlayback:(NSNotification*)notification {
if (![_voiceSearchButton isHidden]) {
// Only trigger TTS playback when the voice search button is visible.
TextToSpeechPlayer* TTSPlayer =
base::mac::ObjCCastStrict<TextToSpeechPlayer>(notification.object);
[TTSPlayer beginPlayback];
}
}
- (void)updateIsTTSPlaying:(NSNotification*)notify {
BOOL wasTTSPlaying = _isTTSPlaying;
_isTTSPlaying =
[notify.name isEqualToString:kTTSWillStartPlayingNotification];
if (wasTTSPlaying != _isTTSPlaying && IsIPadIdiom()) {
[self setUpButton:_voiceSearchButton
withImageEnum:(_isTTSPlaying ? WebToolbarButtonNameTTS
: WebToolbarButtonNameVoice)
forInitialState:UIControlStateNormal
hasDisabledImage:NO
synchronously:NO];
}
[self updateToolbarState];
if (_isTTSPlaying && UIAccessibilityIsVoiceOverRunning()) {
// Moving VoiceOver without RunBlockAfterDelay results in VoiceOver not
// staying on |_voiceSearchButton| and instead moving to views inside the
// UIWebView.
// Use |voiceSearchButton| in the block to prevent |self| from being
// retained.
UIButton* voiceSearchButton = _voiceSearchButton;
RunBlockAfterDelay(0.0, ^{
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
voiceSearchButton);
});
}
}
- (void)moveVoiceOverToVoiceSearchButton {
UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification,
_voiceSearchButton);
}
- (void)updateToolbarAlphaForFrame:(CGRect)frame {
// Don't update the toolbar buttons if we are animating for a transition.
if (self.view.animatingTransition)
return;
CGFloat distanceOffscreen =
IsIPadIdiom()
? fmax((kIPadToolbarY - frame.origin.y) - kScrollFadeDistance, 0)
: -1 * frame.origin.y;
CGFloat fraction = 1 - fmin(distanceOffscreen / kScrollFadeDistance, 1);
if (![_omniBox isFirstResponder])
[self setStandardControlsAlpha:fraction];
[_backButton setAlpha:fraction];
if ([_forwardButton isEnabled])
[_forwardButton setAlpha:fraction];
[_reloadButton setAlpha:fraction];
[_omniboxBackground setAlpha:fraction];
[_omniBox setAlpha:fraction];
[_starButton setAlpha:fraction];
[_voiceSearchButton setAlpha:fraction];
}
- (void)loadURLForQuery:(NSString*)query {
GURL searchURL;
metrics::OmniboxInputType::Type type = AutocompleteInput::Parse(
base::SysNSStringToUTF16(query), std::string(),
AutocompleteSchemeClassifierImpl(), nullptr, nullptr, &searchURL);
if (type != metrics::OmniboxInputType::URL || !searchURL.is_valid()) {
searchURL = GetDefaultSearchURLForSearchTerms(
ios::TemplateURLServiceFactory::GetForBrowserState(_browserState),
base::SysNSStringToUTF16(query));
}
if (searchURL.is_valid()) {
// It is necessary to include PAGE_TRANSITION_FROM_ADDRESS_BAR in the
// transition type is so that query-in-the-omnibox is triggered for the
// URL.
ui::PageTransition transition = ui::PageTransitionFromInt(
ui::PAGE_TRANSITION_LINK | ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);
[self.urlLoader loadURL:GURL(searchURL)
referrer:web::Referrer()
transition:transition
rendererInitiated:NO];
}
}
- (UIView*)keyboardButtonWithTitle:(NSString*)title frame:(CGRect)frame {
UIButton* button = [UIButton buttonWithType:UIButtonTypeCustom];
UIFont* font = nil;
UIImage* backgroundImage = nil;
if (IsIPadIdiom()) {
font = GetUIFont(FONT_HELVETICA, false, kIpadButtonTitleFontSize);
} else {
font = GetUIFont(FONT_HELVETICA, true, kIphoneButtonTitleFontSize);
}
// TODO(leng): Consider moving these images to pak files as well.
backgroundImage = [UIImage imageNamed:@"keyboard_button"];
button.frame = frame;
[button setTitle:title forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setFont:font];
[button setBackgroundImage:backgroundImage forState:UIControlStateNormal];
[button addTarget:self
action:@selector(pressKey:)
forControlEvents:UIControlEventTouchUpInside];
button.isAccessibilityElement = YES;
[button setAccessibilityLabel:title];
return button;
}
- (UIView*)keyboardAccessoryView {
const CGFloat kViewHeightTablet = 70.0;
const CGFloat kViewHeightPhone = 43.0;
const CGFloat kButtonInset = 5.0;
const CGFloat kButtonSizeXTablet = 61.0;
const CGFloat kButtonSizeXPhone = 46.0;
const CGFloat kButtonSizeYTablet = 62.0;
const CGFloat kButtonSizeYPhone = 35.0;
const CGFloat kBetweenButtonSpacing = 15.0;
const CGFloat kBetweenButtonSpacingPhone = 7.0;
if (_keyBoardAccessoryView)
return _keyBoardAccessoryView;
const BOOL isTablet = IsIPadIdiom() && !IsCompactTablet(self.view);
// TODO(pinkerton): purge this view when low memory.
CGFloat width = [[UIScreen mainScreen] bounds].size.width;
CGFloat height = isTablet ? kViewHeightTablet : kViewHeightPhone;
CGRect frame = CGRectMake(0.0, 0.0, width, height);
_keyBoardAccessoryView.reset([[KeyboardAccessoryView alloc]
initWithFrame:frame
inputViewStyle:UIInputViewStyleKeyboard]);
[_keyBoardAccessoryView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
NSArray* buttonTitles =
[NSArray arrayWithObjects:@":", @".", @"-", @"/", kDotComTLD, nil];
// Center buttons in available space by placing them within a parent view
// that auto-centers.
CGFloat betweenButtonSpacing =
isTablet ? kBetweenButtonSpacing : kBetweenButtonSpacingPhone;
const CGFloat buttonWidth = isTablet ? kButtonSizeXTablet : kButtonSizeXPhone;
CGFloat totalWidth = (buttonTitles.count * buttonWidth) +
((buttonTitles.count - 1) * betweenButtonSpacing);
CGFloat indent = floor((width - totalWidth) / 2.0);
if (indent < kButtonInset)
indent = kButtonInset;
CGRect parentViewRect = CGRectMake(indent, 0.0, totalWidth, height);
base::scoped_nsobject<UIView> parentView(
[[UIView alloc] initWithFrame:parentViewRect]);
[parentView setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin |
UIViewAutoresizingFlexibleRightMargin];
[_keyBoardAccessoryView addSubview:parentView];
// Create the buttons, starting at the left edge of |parentView|.
CGRect currentFrame =
CGRectMake(0.0, kButtonInset, buttonWidth,
isTablet ? kButtonSizeYTablet : kButtonSizeYPhone);
for (NSString* title in buttonTitles) {
UIView* button = [self keyboardButtonWithTitle:title frame:currentFrame];
[parentView addSubview:button];
currentFrame.origin.x = CGRectGetMaxX(currentFrame) + betweenButtonSpacing;
}
// Create the voice search button and add it to _keyBoardAccessoryView over
// the text buttons.
_keyboardVoiceSearchButton.reset(
[[UIButton buttonWithType:UIButtonTypeCustom] retain]);
[_keyboardVoiceSearchButton
setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[_keyboardVoiceSearchButton setTag:IDC_VOICE_SEARCH];
SetA11yLabelAndUiAutomationName(_keyboardVoiceSearchButton,
IDS_IOS_ACCNAME_VOICE_SEARCH,
@"Voice Search");
// TODO(leng): Consider moving these icons into a pak file.
UIImage* voiceRow = [UIImage imageNamed:@"custom_row_voice"];
UIImage* voiceRowPressed = [UIImage imageNamed:@"custom_row_voice_pressed"];
[_keyboardVoiceSearchButton setBackgroundImage:voiceRow
forState:UIControlStateNormal];
[_keyboardVoiceSearchButton setBackgroundImage:voiceRowPressed
forState:UIControlStateHighlighted];
UIImage* voiceIcon = [UIImage imageNamed:@"voice_icon_keyboard_accessory"];
[_keyboardVoiceSearchButton setAdjustsImageWhenHighlighted:NO];
[_keyboardVoiceSearchButton setImage:voiceIcon forState:UIControlStateNormal];
[_keyboardVoiceSearchButton setFrame:[_keyBoardAccessoryView bounds]];
// Only add the voice search actions if voice search is enabled.
if (ios::GetChromeBrowserProvider()
->GetVoiceSearchProvider()
->IsVoiceSearchEnabled()) {
[_keyboardVoiceSearchButton addTarget:self
action:@selector(recordUserMetrics:)
forControlEvents:UIControlEventTouchUpInside];
[_keyboardVoiceSearchButton addTarget:_keyboardVoiceSearchButton
action:@selector(chromeExecuteCommand:)
forControlEvents:UIControlEventTouchUpInside];
[_keyboardVoiceSearchButton addTarget:self
action:@selector(preloadVoiceSearch:)
forControlEvents:UIControlEventTouchDown];
} else {
[_keyboardVoiceSearchButton addTarget:self
action:@selector(ignoreVoiceSearch:)
forControlEvents:UIControlEventTouchUpInside];
}
[_keyBoardAccessoryView addSubview:_keyboardVoiceSearchButton];
// Reset the external keyboard watcher.
_hardwareKeyboardWatcher.reset([[HardwareKeyboardWatcher alloc]
initWithAccessoryView:_keyBoardAccessoryView]);
return _keyBoardAccessoryView;
}
- (void)preloadVoiceSearch:(id)sender {
DCHECK(ios::GetChromeBrowserProvider()
->GetVoiceSearchProvider()
->IsVoiceSearchEnabled());
[sender removeTarget:self
action:@selector(preloadVoiceSearch:)
forControlEvents:UIControlEventTouchDown];
// Use a GenericChromeCommand because |sender| already has a tag set for a
// different command.
base::scoped_nsobject<GenericChromeCommand> command(
[[GenericChromeCommand alloc] initWithTag:IDC_PRELOAD_VOICE_SEARCH]);
[sender chromeExecuteCommand:command];
}
// Called when the keyboard voice search button is tapped with voice search
// disabled. Hides the voice search button but takes no other action.
- (void)ignoreVoiceSearch:(id)sender {
[_keyboardVoiceSearchButton setHidden:YES];
}
- (CGFloat)omniboxLeading {
// Compute what the leading (x-origin) position for the omniboox should be
// based on what other controls are active.
InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM;
CGFloat trailingPadding = 0.0;
LayoutRect leadingControlLayout = LayoutRectForRectInBoundingRect(
[_backButton frame], [_webToolbar bounds]);
LayoutRect forwardButtonLayout =
LayoutRectGetTrailingLayout(leadingControlLayout);
forwardButtonLayout.position.leading += trailingPadding;
forwardButtonLayout.position.originY = [_forwardButton frame].origin.y;
forwardButtonLayout.size = [_forwardButton frame].size;
LayoutRect reloadButtonLayout =
LayoutRectGetTrailingLayout(forwardButtonLayout);
reloadButtonLayout.position.originY = [_reloadButton frame].origin.y;
reloadButtonLayout.size = [_reloadButton frame].size;
LayoutRect omniboxReferenceLayout;
if (idiom == IPAD_IDIOM && !IsCompactTablet(self.view)) {
omniboxReferenceLayout = reloadButtonLayout;
omniboxReferenceLayout.size.width += kReloadButtonTrailingPadding;
} else if ([_forwardButton isEnabled]) {
omniboxReferenceLayout = forwardButtonLayout;
omniboxReferenceLayout.size.width += kForwardButtonTrailingPadding;
} else {
omniboxReferenceLayout = kBackButtonFrame[idiom];
omniboxReferenceLayout.size.width += kBackButtonTrailingPadding;
}
DCHECK(!(omniboxReferenceLayout.position.leading == 0 &&
CGSizeEqualToSize(omniboxReferenceLayout.size, CGSizeZero)));
return LayoutRectGetTrailingEdge(omniboxReferenceLayout);
}
// TODO(crbug.com/525943): refactor this method and related code to not resize
// |_webToolbar| and buttons as a side effect.
- (CGRect)newOmniboxFrame {
InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM;
LayoutRect newOmniboxLayout;
// Grow the omnibox if focused.
BOOL growOmnibox = [_omniBox isFirstResponder];
if (idiom == IPAD_IDIOM) {
// When the omnibox is focused, the star button is hidden.
[_starButton setAlpha:(growOmnibox ? 0 : 1)];
newOmniboxLayout =
LayoutRectForRectInBoundingRect([_omniBox frame], [_webToolbar bounds]);
CGFloat omniboxLeading = [self omniboxLeading];
CGFloat omniboxLeadingDiff =
omniboxLeading - newOmniboxLayout.position.leading;
newOmniboxLayout.position.leading = omniboxLeading;
newOmniboxLayout.size.width -= omniboxLeadingDiff;
} else {
// If the omnibox is growing, take over the whole toolbar area (standard
// toolbar controls will be hidden below). Temporarily suppress autoresizing
// to avoid interfering with the omnibox animation.
[_webToolbar setAutoresizesSubviews:NO];
CGRect expandedFrame =
RectShiftedDownAndResizedForStatusBar(self.view.bounds);
[_webToolbar
setFrame:growOmnibox ? expandedFrame : [self specificControlsArea]];
[_webToolbar setAutoresizesSubviews:YES];
// Compute new omnibox layout after the web toolbar is resized.
newOmniboxLayout =
LayoutRectForRectInBoundingRect([_omniBox frame], [_webToolbar bounds]);
if (growOmnibox) {
// If the omnibox is expanded, there is padding on both the left and right
// sides.
newOmniboxLayout.position.leading = kExpandedOmniboxPadding;
// When the |_webToolbar| is resized without autoresizes subviews enabled
// the cancel button does not pin to the trailing side and is out of
// place. Lazily allocate and force a layout here.
[self layoutCancelButton];
LayoutRect cancelButtonLayout = LayoutRectForRectInBoundingRect(
[self cancelButton].frame, [_webToolbar bounds]);
newOmniboxLayout.size.width = cancelButtonLayout.position.leading -
kExpandedOmniboxPadding -
newOmniboxLayout.position.leading;
// Likewise the incognito icon will not be pinned to the leading side, so
// force a layout here.
if (_incognitoIcon)
[self layoutIncognitoIcon];
} else {
newOmniboxLayout.position.leading = [self omniboxLeading];
newOmniboxLayout.size.width = CGRectGetWidth([_webToolbar frame]) -
newOmniboxLayout.position.leading;
}
}
CGRect newOmniboxFrame = LayoutRectGetRect(newOmniboxLayout);
DCHECK(!CGRectEqualToRect(newOmniboxFrame, CGRectZero));
return newOmniboxFrame;
}
- (void)animateMaterialOmnibox {
// The iPad omnibox does not animate.
if (IsIPadIdiom())
return [self layoutOmnibox];
CGRect newOmniboxFrame = [self newOmniboxFrame];
BOOL growOmnibox = [_omniBox isFirstResponder];
// Determine the starting and ending bounds and position for |_omniBox|.
// Increasing the height of _omniBox results in the text inside it jumping
// vertically during the animation, so the height change will not be animated.
CGRect fromBounds = [_omniBox layer].bounds;
LayoutRect toLayout =
LayoutRectForRectInBoundingRect(newOmniboxFrame, [_webToolbar bounds]);
CGRect toBounds = CGRectZero;
CGPoint fromPosition = [_omniBox layer].position;
CGPoint toPosition = fromPosition;
CGRect omniboxRect = LayoutRectGetRect(kOmniboxFrame[IPHONE_IDIOM]);
if (growOmnibox) {
toLayout.size = CGSizeMake([_webToolbar bounds].size.width -
self.cancelButton.frame.size.width -
kCancelButtonLeadingMargin,
omniboxRect.size.height);
toLayout.position.leading = 0;
if (_incognito) {
// Adjust the width and leading of the omnibox to account for the
// incognito icon.
// TODO(crbug/525943): Refactor so this value isn't calculated here, and
// instead is calculated in -newOmniboxFrame?
// (include in (crbug/525943) refactor).
LayoutRect incognitioIconLayout = LayoutRectForRectInBoundingRect(
[_incognitoIcon frame], [_webToolbar frame]);
CGFloat trailingEdge = LayoutRectGetTrailingEdge(incognitioIconLayout);
toLayout.size.width -= trailingEdge;
toLayout.position.leading = trailingEdge;
}
} else {
// Shrink the omnibox to its original width.
toLayout.size =
CGSizeMake(newOmniboxFrame.size.width, omniboxRect.size.height);
}
toBounds = LayoutRectGetBoundsRect(toLayout);
toPosition.x =
LayoutRectGetPositionForAnchor(toLayout, [_omniBox layer].anchorPoint).x;
// Determine starting and ending bounds for |_omniboxBackground|.
// _omniboxBackground is needed to simulate the omnibox growing vertically and
// covering the entire height of the toolbar.
CGRect backgroundFromBounds = [_omniboxBackground layer].bounds;
CGRect backgroundToBounds = CGRectZero;
CGPoint backgroundFromPosition = [_omniboxBackground layer].position;
CGPoint backgroundToPosition = CGPointZero;
if (growOmnibox) {
// Grow the background to cover the whole toolbar.
backgroundToBounds = [_clippingView bounds];
backgroundToPosition.x = backgroundToBounds.size.width / 2;
backgroundToPosition.y = backgroundToBounds.size.height / 2;
// Increase the bounds of the background so that the border extends past the
// toolbar and is clipped.
backgroundToBounds = CGRectInset(backgroundToBounds, -2, -2);
} else {
// Shrink the background to the same bounds as the omnibox.
backgroundToBounds = toBounds;
backgroundToPosition = toPosition;
backgroundToPosition.y += StatusBarHeight();
}
// Is the omnibox already at the new size? Then there's nothing to animate.
if (CGRectEqualToRect([_omniBox layer].bounds, toBounds))
return;
[self animateStandardControlsForOmniboxExpansion:growOmnibox];
if (growOmnibox) {
[self fadeOutNavigationControls];
[self fadeInOmniboxTrailingView];
if (![_omniBox isShowingQueryRefinementChip]) {
// Don't animate the query refinement chip.
if (_locationBar.get()->IsShowingPlaceholderWhileCollapsed())
[self fadeOutOmniboxLeadingView];
else
[_omniBox leftView].alpha = 0;
}
if (_incognito)
[self fadeInIncognitoIcon];
} else {
[self fadeInNavigationControls];
[self fadeOutOmniboxTrailingView];
if (![_omniBox isShowingQueryRefinementChip]) {
// Don't animate the query refinement chip.
if (_locationBar.get()->IsShowingPlaceholderWhileCollapsed())
[self fadeInOmniboxLeadingView];
else
[_omniBox leftView].alpha = 1;
}
if (_incognito)
[self fadeOutIncognitoIcon];
}
// Animate the new bounds and position for the omnibox and background.
[CATransaction begin];
[CATransaction setCompletionBlock:^{
// Re-layout the omnibox's subviews after the animation to allow VoiceOver
// to select the clear text button.
[_omniBox setNeedsLayout];
}];
CGFloat duration = ios::material::kDuration1;
// If app is on the regular New Tab Page, make this animation occur instantly
// since this page has a fakebox to omnibox transition.
web::WebState* webState = [self.delegate currentWebState];
if (webState && webState->GetVisibleURL() == GURL(kChromeUINewTabURL) &&
!_incognito) {
duration = 0.0;
}
[CATransaction setAnimationDuration:duration];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseInOut)];
// TODO(crbug.com/525943): As part of animation cleanup, refactor
// these animations into groups produced by FrameAnimationMake() from
// animation_util, and do all of the bounds/position calculations above in
// terms of frames.
CABasicAnimation* resizeAnimation =
[CABasicAnimation animationWithKeyPath:@"bounds"];
resizeAnimation.delegate = self;
[resizeAnimation setValue:@"resizeOmnibox" forKey:@"id"];
resizeAnimation.fromValue = [NSValue valueWithCGRect:fromBounds];
resizeAnimation.toValue = [NSValue valueWithCGRect:toBounds];
[_omniBox layer].bounds = toBounds;
[[_omniBox layer] addAnimation:resizeAnimation forKey:@"resizeBounds"];
CABasicAnimation* positionAnimation =
[CABasicAnimation animationWithKeyPath:@"position"];
positionAnimation.fromValue = [NSValue valueWithCGPoint:fromPosition];
positionAnimation.toValue = [NSValue valueWithCGPoint:toPosition];
[_omniBox layer].position = toPosition;
[[_omniBox layer] addAnimation:positionAnimation forKey:@"movePosition"];
resizeAnimation = [CABasicAnimation animationWithKeyPath:@"bounds"];
resizeAnimation.fromValue = [NSValue valueWithCGRect:backgroundFromBounds];
resizeAnimation.toValue = [NSValue valueWithCGRect:backgroundToBounds];
[_omniboxBackground layer].bounds = backgroundToBounds;
[[_omniboxBackground layer] addAnimation:resizeAnimation
forKey:@"resizeBounds"];
CABasicAnimation* backgroundPositionAnimation =
[CABasicAnimation animationWithKeyPath:@"position"];
backgroundPositionAnimation.fromValue =
[NSValue valueWithCGPoint:backgroundFromPosition];
backgroundPositionAnimation.toValue =
[NSValue valueWithCGPoint:backgroundToPosition];
[_omniboxBackground layer].position = backgroundToPosition;
[[_omniboxBackground layer] addAnimation:backgroundPositionAnimation
forKey:@"movePosition"];
[CATransaction commit];
}
- (void)fadeInOmniboxTrailingView {
UIView* trailingView = [_omniBox rightView];
trailingView.alpha = 0;
[_cancelButton setAlpha:0];
[_cancelButton setHidden:NO];
// Instead of passing a delay into -fadeInView:, wait to call -fadeInView:.
// The CABasicAnimation's start and end positions are calculated immediately
// instead of after the animation's delay, but the omnibox's layer isn't set
// yet to its final state and as a result the start and end positions will not
// be correct.
dispatch_time_t delay = dispatch_time(
DISPATCH_TIME_NOW, ios::material::kDuration6 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^(void) {
[self fadeInView:trailingView
fromLeadingOffset:kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration1
afterDelay:0.2];
[self fadeInView:_cancelButton
fromLeadingOffset:kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration1
afterDelay:0.1];
});
}
- (void)fadeInOmniboxLeadingView {
UIView* leadingView = [_omniBox leftView];
leadingView.alpha = 0;
// Instead of passing a delay into -fadeInView:, wait to call -fadeInView:.
// The CABasicAnimation's start and end positions are calculated immediately
// instead of after the animation's delay, but the omnibox's layer isn't set
// yet to its final state and as a result the start and end positions will not
// be correct.
dispatch_time_t delay = dispatch_time(
DISPATCH_TIME_NOW, ios::material::kDuration2 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^(void) {
[self fadeInView:leadingView
fromLeadingOffset:kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration1
afterDelay:0];
});
}
- (void)fadeOutOmniboxTrailingView {
UIView* trailingView = [_omniBox rightView];
// Animate the opacity of the trailingView to 0.
[CATransaction begin];
[CATransaction setAnimationDuration:ios::material::kDuration4];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)];
[CATransaction setCompletionBlock:^{
[_cancelButton setHidden:YES];
}];
CABasicAnimation* fadeOut =
[CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @1;
fadeOut.toValue = @0;
trailingView.layer.opacity = 0;
[trailingView.layer addAnimation:fadeOut forKey:@"fade"];
// Animate the trailingView |kPositionAnimationLeadingOffset| pixels towards
// the
// leading edge.
CABasicAnimation* shift = [CABasicAnimation animationWithKeyPath:@"position"];
CGPoint startPosition = [trailingView layer].position;
CGPoint endPosition =
CGPointLayoutOffset(startPosition, kPositionAnimationLeadingOffset);
shift.fromValue = [NSValue valueWithCGPoint:startPosition];
shift.toValue = [NSValue valueWithCGPoint:endPosition];
[[trailingView layer] addAnimation:shift forKey:@"shift"];
[_cancelButton layer].opacity = 0;
[[_cancelButton layer] addAnimation:fadeOut forKey:@"fade"];
CABasicAnimation* shiftCancelButton =
[CABasicAnimation animationWithKeyPath:@"position"];
startPosition = [_cancelButton layer].position;
endPosition =
CGPointLayoutOffset(startPosition, kPositionAnimationLeadingOffset);
shiftCancelButton.fromValue = [NSValue valueWithCGPoint:startPosition];
shiftCancelButton.toValue = [NSValue valueWithCGPoint:endPosition];
[[_cancelButton layer] addAnimation:shiftCancelButton forKey:@"shift"];
[CATransaction commit];
}
- (void)fadeOutOmniboxLeadingView {
UIView* leadingView = [_omniBox leftView];
// Animate the opacity of leadingView to 0.
[CATransaction begin];
[CATransaction setAnimationDuration:ios::material::kDuration2];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseInOut)];
CABasicAnimation* fadeOut =
[CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @1;
fadeOut.toValue = @0;
leadingView.layer.opacity = 0;
[leadingView.layer addAnimation:fadeOut forKey:@"fade"];
// Animate leadingView |kPositionAnimationLeadingOffset| pixels trailing.
CABasicAnimation* shift = [CABasicAnimation animationWithKeyPath:@"position"];
CGPoint startPosition = [leadingView layer].position;
CGPoint endPosition =
CGPointLayoutOffset(startPosition, kPositionAnimationLeadingOffset);
shift.fromValue = [NSValue valueWithCGPoint:startPosition];
shift.toValue = [NSValue valueWithCGPoint:endPosition];
[[leadingView layer] addAnimation:shift forKey:@"shift"];
[CATransaction commit];
}
- (void)fadeInIncognitoIcon {
DCHECK(_incognito);
DCHECK(!IsIPadIdiom());
[self fadeInView:_incognitoIcon
fromLeadingOffset:-kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration2
afterDelay:ios::material::kDuration2];
}
- (void)fadeOutIncognitoIcon {
DCHECK(_incognito);
DCHECK(!IsIPadIdiom());
// Animate the opacity of the incognito icon to 0.
CALayer* layer = [_incognitoIcon layer];
[CATransaction begin];
[CATransaction setAnimationDuration:ios::material::kDuration4];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)];
CABasicAnimation* fadeOut =
[CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @1;
fadeOut.toValue = @0;
layer.opacity = 0;
[layer addAnimation:fadeOut forKey:@"fade"];
// Animate the incognito icon |kPositionAnimationLeadingOffset| pixels
// trailing.
CABasicAnimation* shift = [CABasicAnimation animationWithKeyPath:@"position"];
CGPoint startPosition = layer.position;
// Constant is a leading value, so invert it to move in the trailing
// direction.
CGPoint endPosition =
CGPointLayoutOffset(startPosition, -kPositionAnimationLeadingOffset);
shift.fromValue = [NSValue valueWithCGPoint:startPosition];
shift.toValue = [NSValue valueWithCGPoint:endPosition];
[layer addAnimation:shift forKey:@"shift"];
[CATransaction commit];
}
- (void)fadeInNavigationControls {
// Determine which navigation buttons are visible and need to be animated.
UIView* firstView = nil;
UIView* secondView = nil;
if ([_forwardButton isEnabled]) {
firstView = _forwardButton;
secondView = _backButton;
} else {
firstView = _backButton;
}
[self fadeInView:firstView
fromLeadingOffset:kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration2
afterDelay:ios::material::kDuration1];
if (secondView) {
[self fadeInView:secondView
fromLeadingOffset:kPositionAnimationLeadingOffset
withDuration:ios::material::kDuration2
afterDelay:ios::material::kDuration3];
}
}
- (void)fadeOutNavigationControls {
[CATransaction begin];
[CATransaction setAnimationDuration:ios::material::kDuration2];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)];
CABasicAnimation* fadeButtons =
[CABasicAnimation animationWithKeyPath:@"opacity"];
fadeButtons.fromValue = @1;
fadeButtons.toValue = @0;
if ([_forwardButton isEnabled]) {
[_forwardButton layer].opacity = 0;
[[_forwardButton layer] addAnimation:fadeButtons forKey:@"fadeOut"];
}
[_backButton layer].opacity = 0;
[[_backButton layer] addAnimation:fadeButtons forKey:@"fadeOut"];
[CATransaction commit];
// Animate the navigation buttons 10 pixels to the left.
[CATransaction begin];
[CATransaction setAnimationDuration:ios::material::kDuration1];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)];
CABasicAnimation* shiftBackButton =
[CABasicAnimation animationWithKeyPath:@"position"];
CGPoint startPosition = [_backButton layer].position;
CGPoint endPosition =
CGPointLayoutOffset(startPosition, kPositionAnimationLeadingOffset);
shiftBackButton.fromValue = [NSValue valueWithCGPoint:startPosition];
shiftBackButton.toValue = [NSValue valueWithCGPoint:endPosition];
[[_backButton layer] addAnimation:shiftBackButton forKey:@"shiftButton"];
CABasicAnimation* shiftForwardButton =
[CABasicAnimation animationWithKeyPath:@"position"];
startPosition = [_forwardButton layer].position;
endPosition =
CGPointLayoutOffset(startPosition, kPositionAnimationLeadingOffset);
shiftForwardButton.fromValue = [NSValue valueWithCGPoint:startPosition];
shiftForwardButton.toValue = [NSValue valueWithCGPoint:endPosition];
[[_forwardButton layer] addAnimation:shiftForwardButton
forKey:@"shiftButton"];
[CATransaction commit];
}
- (void)animationDidStop:(CAAnimation*)anim finished:(BOOL)flag {
if ([[anim valueForKey:@"id"] isEqual:@"resizeOmnibox"] &&
![_omniBox isFirstResponder]) {
[_omniBox setRightView:nil];
}
}
- (void)updateSnapshotWithWidth:(CGFloat)width forced:(BOOL)force {
// If |width| is 0, the current view's width is acceptable.
if (width < 1)
width = [self view].frame.size.width;
// Snapshot is not used on the iPad.
if (IsIPadIdiom()) {
NOTREACHED();
return;
}
// If the snapshot is valid, don't redraw.
if (_snapshot.get() && _snapshotHash == [self snapshotHashWithWidth:width])
return;
// Don't update the snapshot while the progress bar is moving, or while the
// tools menu is open, unless |force| is true.
BOOL shouldRedraw = force || ([self toolsPopupController] == nil &&
[_determinateProgressView isHidden]);
if (!shouldRedraw)
return;
if ([[self delegate]
respondsToSelector:@selector(willUpdateToolbarSnapshot)]) {
[[self delegate] willUpdateToolbarSnapshot];
}
// Temporarily resize the toolbar if necessary in order to match the desired
// width. (Such a mismatch can occur if the device has been rotated while this
// view was not visible, for example.)
CGRect frame = [self view].frame;
CGFloat oldWidth = frame.size.width;
frame.size.width = width;
[self view].frame = frame;
UIGraphicsBeginImageContextWithOptions(frame.size, NO, 0.0);
[[self view].layer renderInContext:UIGraphicsGetCurrentContext()];
_snapshot.reset([UIGraphicsGetImageFromCurrentImageContext() retain]);
UIGraphicsEndImageContext();
// In the past, when the current tab was prerendered, taking a snapshot
// sometimes lead to layout of its UIWebView. As this may be the fist time
// the UIWebViews was layed out, its scroll view was scrolled. This lead
// to scroll events that changed the frame of the toolbar when fullscreen
// was enabled.
// DCHECK that the toolbar frame does not change while taking a snapshot.
DCHECK_EQ(frame.origin.x, [self view].frame.origin.x);
DCHECK_EQ(frame.origin.y, [self view].frame.origin.y);
DCHECK_EQ(frame.size.width, [self view].frame.size.width);
DCHECK_EQ(frame.size.height, [self view].frame.size.height);
frame.size.width = oldWidth;
[self view].frame = frame;
_snapshotHash = [self snapshotHashWithWidth:width];
}
- (NSString*)updateTextForDotCom:(NSString*)text {
if ([text isEqualToString:kDotComTLD]) {
UITextRange* textRange = [_omniBox selectedTextRange];
NSInteger pos = [_omniBox offsetFromPosition:[_omniBox beginningOfDocument]
toPosition:textRange.start];
if (pos > 0 && [[_omniBox text] characterAtIndex:pos - 1] == '.')
return [kDotComTLD substringFromIndex:1];
}
return text;
}
- (void)pressKey:(id)sender {
DCHECK([sender isKindOfClass:[UIButton class]]);
[[UIDevice currentDevice] playInputClick];
NSString* text = [self updateTextForDotCom:[sender currentTitle]];
[_omniBox insertTextWhileEditing:text];
}
@end
@implementation WebToolbarController (Testing)
- (BOOL)isForwardButtonEnabled {
return [_forwardButton isEnabled];
}
- (BOOL)isBackButtonEnabled {
return [_backButton isEnabled];
}
- (BOOL)isStarButtonSelected {
return [_starButton isSelected];
}
- (void)setUnitTesting:(BOOL)unitTesting {
_unitTesting = unitTesting;
}
- (CGFloat)loadProgressFraction {
return [_determinateProgressView progress];
}
- (std::string)getLocationText {
return base::UTF16ToUTF8([_omniBox displayedText]);
}
- (BOOL)isLoading {
return ![_determinateProgressView isHidden];
}
- (BOOL)isPrerenderAnimationRunning {
return _prerenderAnimating;
}
- (OmniboxTextFieldIOS*)omnibox {
return _omniBox.get();
}
@end