blob: 8f0e860a9cfaf242d9741c79d40bc6c56d561366 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/ui/tools_menu/tools_menu_view_controller.h"
#include <stdint.h>
#include "base/ios/ios_util.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram_macros.h"
#include "components/strings/grit/components_strings.h"
#include "ios/chrome/browser/experimental_flags.h"
#import "ios/chrome/browser/ui/animation_util.h"
#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_menu_notification_delegate.h"
#import "ios/chrome/browser/ui/reading_list/reading_list_menu_notifier.h"
#import "ios/chrome/browser/ui/tools_menu/reading_list_menu_view_item.h"
#import "ios/chrome/browser/ui/tools_menu/tools_menu_constants.h"
#import "ios/chrome/browser/ui/tools_menu/tools_menu_model.h"
#import "ios/chrome/browser/ui/tools_menu/tools_menu_view_item.h"
#import "ios/chrome/browser/ui/tools_menu/tools_menu_view_tools_cell.h"
#import "ios/chrome/browser/ui/tools_menu/tools_popup_controller.h"
#include "ios/chrome/browser/ui/ui_util.h"
#import "ios/chrome/browser/ui/uikit_ui_util.h"
#import "ios/chrome/common/material_timing.h"
#include "ios/chrome/grit/ios_strings.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_provider.h"
#import "ios/shared/chrome/browser/ui/tools_menu/tools_menu_configuration.h"
#import "ios/third_party/material_components_ios/src/components/Ink/src/MaterialInk.h"
#include "ios/web/public/user_agent.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using ios::material::TimingFunction;
namespace {
// Time for ink to fade into view.
static const NSTimeInterval kMDCInkTouchDelayInterval = 0.15;
static const CGFloat kMenuItemHeight = 48;
static NSString* const kToolsItemCellID = @"ToolsItemCellID";
NS_INLINE void AnimateInViews(NSArray* views,
CGFloat initialX,
CGFloat initialY) {
[views enumerateObjectsUsingBlock:^(UIView* view, NSUInteger index,
BOOL* stop) {
CGFloat beginTime = index * .035;
CABasicAnimation* transformAnimation =
[CABasicAnimation animationWithKeyPath:@"transform"];
[transformAnimation
setFromValue:[NSValue
valueWithCATransform3D:CATransform3DMakeTranslation(
initialX, initialY, 0)]];
[transformAnimation
setToValue:[NSValue valueWithCATransform3D:CATransform3DIdentity]];
[transformAnimation setDuration:ios::material::kDuration1];
[transformAnimation setBeginTime:beginTime];
CAAnimation* fadeAnimation = OpacityAnimationMake(0, 1);
[fadeAnimation setDuration:ios::material::kDuration1];
[fadeAnimation
setTimingFunction:TimingFunction(ios::material::CurveEaseOut)];
[fadeAnimation setBeginTime:beginTime];
[[view layer]
addAnimation:AnimationGroupMake(@[ transformAnimation, fadeAnimation ])
forKey:@"animateIn"];
}];
}
} // anonymous namespace
@interface ToolsMenuCollectionView : UICollectionView
@property(nonatomic, assign) CGPoint touchBeginPoint;
@property(nonatomic, assign) CGPoint touchEndPoint;
@end
@implementation ToolsMenuCollectionView
@synthesize touchBeginPoint = _touchBeginPoint;
@synthesize touchEndPoint = _touchEndPoint;
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
_touchBeginPoint = [[touches anyObject] locationInView:self];
[super touchesBegan:touches withEvent:event];
}
- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
_touchEndPoint = [[touches anyObject] locationInView:self];
[super touchesEnded:touches withEvent:event];
}
@end
// Class Extension for private methods.
@interface ToolsMenuViewController ()<UICollectionViewDelegateFlowLayout,
UICollectionViewDataSource,
ReadingListMenuNotificationDelegate> {
BOOL _waitForInk;
// Weak pointer to ReadingListMenuNotifier, used to set the starting values
// for the reading list badge.
__weak ReadingListMenuNotifier* _readingListMenuNotifier;
}
@property(nonatomic, strong) ToolsMenuCollectionView* menuView;
@property(nonatomic, strong) MDCInkView* touchFeedbackView;
@property(nonatomic, assign) ToolbarType toolbarType;
// Populated by the configuration object in |initializeMenuWithConfiguration:|
// stores the time this view controller was requested by the user for the
// reporting of metrics.
@property(nonatomic, assign) NSTimeInterval requestStartTime;
// Returns the reading list cell.
- (ReadingListMenuViewCell*)readingListCell;
@end
@implementation ToolsMenuViewController
@synthesize menuView = _menuView;
@synthesize isCurrentPageBookmarked = _isCurrentPageBookmarked;
@synthesize touchFeedbackView = _touchFeedbackView;
@synthesize isTabLoading = _isTabLoading;
@synthesize toolbarType = _toolbarType;
@synthesize menuItems = _menuItems;
@synthesize delegate = _delegate;
@synthesize requestStartTime = _requestStartTime;
#pragma mark Public methods
- (CGFloat)optimalHeight:(CGFloat)suggestedHeight {
NSInteger numberOfItems = [self.menuItems count];
if (_toolbarType == ToolbarTypeWebiPhone) {
// Account for the height of the first row, not included in |menuItems|.
numberOfItems++;
}
CGFloat maxItems = suggestedHeight / kMenuItemHeight;
if (maxItems >= numberOfItems) {
return numberOfItems * kMenuItemHeight;
} else {
const CGFloat halfHeight = kMenuItemHeight / 2;
return round(maxItems) * kMenuItemHeight - halfHeight;
}
}
- (void)setItemEnabled:(BOOL)enabled withTag:(NSInteger)tag {
for (ToolsMenuViewItem* item in _menuItems) {
if ([item tag] == tag) {
[item setActive:enabled];
break;
}
}
for (ToolsMenuViewCell* cell in [_menuView visibleCells]) {
if (![cell isKindOfClass:[ToolsMenuViewCell class]])
continue;
if ([cell tag] != tag)
continue;
NSIndexPath* path = [_menuView indexPathForCell:cell];
NSInteger itemIndex = [self dataIndexForIndexPath:path];
[cell configureForMenuItem:[_menuItems objectAtIndex:itemIndex]];
}
}
- (void)setIsCurrentPageBookmarked:(BOOL)value {
_isCurrentPageBookmarked = value;
ToolsMenuViewToolsCell* toolsCell = [self toolsCell];
[[toolsCell starButton] setHidden:_isCurrentPageBookmarked];
[[toolsCell starredButton] setHidden:!_isCurrentPageBookmarked];
}
- (void)setCanUseReaderMode:(BOOL)enabled {
[self setItemEnabled:enabled withTag:IDC_READER_MODE];
}
- (void)setCanShowFindBar:(BOOL)enabled {
[self setItemEnabled:enabled withTag:IDC_FIND];
}
- (void)setCanShowShareMenu:(BOOL)enabled {
ToolsMenuViewToolsCell* toolsCell = [self toolsCell];
[[toolsCell shareButton] setEnabled:enabled];
[self setItemEnabled:enabled withTag:IDC_SHARE_PAGE];
}
- (UIButton*)toolsButton {
UIButton* toolsButton = [[self toolsCell] toolsButton];
[toolsButton addTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
[toolsButton setTranslatesAutoresizingMaskIntoConstraints:YES];
[toolsButton setOpaque:NO];
[toolsButton setBackgroundColor:[UIColor clearColor]];
return toolsButton;
}
- (void)setIsTabLoading:(BOOL)isTabLoading {
_isTabLoading = isTabLoading;
ToolsMenuViewToolsCell* toolsCell = [self toolsCell];
[[toolsCell stopButton] setHidden:!isTabLoading];
[[toolsCell reloadButton] setHidden:isTabLoading];
}
- (void)initializeMenuWithConfiguration:(ToolsMenuConfiguration*)configuration {
self.requestStartTime = configuration.requestStartTime;
if (configuration.readingListMenuNotifier) {
_readingListMenuNotifier = configuration.readingListMenuNotifier;
[configuration.readingListMenuNotifier setDelegate:self];
}
if (IsIPadIdiom()) {
_toolbarType =
configuration.hasNoOpenedTabs
? ToolbarTypeNoTabsiPad
: (!IsCompactTablet() ? ToolbarTypeWebiPad : ToolbarTypeWebiPhone);
} else {
// kOptionInTabSwitcher option must be enabled on iPhone with
// no opened tabs.
DCHECK(!configuration.hasNoOpenedTabs || configuration.isInTabSwitcher);
_toolbarType = configuration.isInTabSwitcher ? ToolbarTypeSwitcheriPhone
: ToolbarTypeWebiPhone;
}
// Build the menu, adding all relevant items.
NSMutableArray* menu = [NSMutableArray array];
for (size_t i = 0; i < arraysize(itemInfoList); ++i) {
const MenuItemInfo& item = itemInfoList[i];
if (!ToolsMenuItemShouldBeVisible(item, _toolbarType, configuration))
continue;
NSString* title = l10n_util::GetNSStringWithFixup(item.title_id);
Class itemClass =
item.item_class ? item.item_class : [ToolsMenuViewItem class];
// Sanity check that the class is a useful one.
DCHECK([itemClass respondsToSelector:@selector(menuItemWithTitle:
accessibilityIdentifier:
command:)]);
[menu addObject:[itemClass menuItemWithTitle:title
accessibilityIdentifier:item.accessibility_id
command:item.command_id]];
}
#if !defined(NDEBUG)
NSUserDefaults* standardDefaults = [NSUserDefaults standardUserDefaults];
if ((_toolbarType & ToolbarTypeWebAll) &&
[standardDefaults boolForKey:@"DevViewSource"]) {
// Debug menu, not localized, only visible if turned on by a default.
[menu addObject:[self createViewSourceItem]];
}
#endif // !defined(NDEBUG)
[self setMenuItems:menu];
// Decide the enabled state of the currently visible item between
// "Request Desktop Site" and "Request Mobile Site".
switch (configuration.userAgentType) {
case web::UserAgentType::NONE:
[self setItemEnabled:NO withTag:IDC_REQUEST_DESKTOP_SITE];
break;
case web::UserAgentType::MOBILE:
[self setItemEnabled:YES withTag:IDC_REQUEST_DESKTOP_SITE];
break;
case web::UserAgentType::DESKTOP:
[self setItemEnabled:YES withTag:IDC_REQUEST_MOBILE_SITE];
if (!experimental_flags::IsRequestMobileSiteEnabled()) {
// When Request Mobile Site is disabled, the enabled state of Request
// Desktop Site button needs to be set to NO because it is visible even
// though the current UserAgentType is DESKTOP.
[self setItemEnabled:NO withTag:IDC_REQUEST_DESKTOP_SITE];
}
break;
}
// Disable IDC_CLOSE_ALL_TABS menu item if on phone with no tabs.
if (!IsIPadIdiom()) {
[self setItemEnabled:!configuration.hasNoOpenedTabs
withTag:IDC_CLOSE_ALL_TABS];
}
}
#if !defined(NDEBUG)
- (ToolsMenuViewItem*)createViewSourceItem {
return [ToolsMenuViewItem menuItemWithTitle:@"View Source"
accessibilityIdentifier:@"View Source"
command:IDC_VIEW_SOURCE];
}
#endif // !defined(NDEBUG)
#pragma mark - Data handling utilities
- (ToolsMenuViewToolsCell*)toolsCell {
for (ToolsMenuViewToolsCell* visibleCell in [_menuView visibleCells]) {
if ([visibleCell isKindOfClass:[ToolsMenuViewToolsCell class]])
return visibleCell;
}
return nil;
}
- (ReadingListMenuViewCell*)readingListCell {
for (ReadingListMenuViewCell* visibleCell in [_menuView visibleCells]) {
if ([visibleCell isKindOfClass:[ReadingListMenuViewCell class]])
return visibleCell;
}
return nil;
}
- (NSInteger)dataIndexForIndexPath:(NSIndexPath*)path {
NSInteger item = [path item];
if (_toolbarType == ToolbarTypeWebiPhone)
--item;
return item;
}
#pragma mark - UIViewController Overrides
- (void)loadView {
[super loadView];
UIView* rootView = [self view];
[rootView setAutoresizingMask:UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth];
[rootView setBackgroundColor:[UIColor whiteColor]];
_touchFeedbackView = [[MDCInkView alloc] initWithFrame:CGRectZero];
UICollectionViewFlowLayout* menuItemsLayout =
[[UICollectionViewFlowLayout alloc] init];
_menuView = [[ToolsMenuCollectionView alloc] initWithFrame:[rootView bounds]
collectionViewLayout:menuItemsLayout];
[_menuView setAccessibilityLabel:l10n_util::GetNSString(IDS_IOS_TOOLS_MENU)];
[_menuView setAccessibilityIdentifier:kToolsMenuTableViewId];
[_menuView setTranslatesAutoresizingMaskIntoConstraints:NO];
[_menuView setBackgroundColor:[UIColor whiteColor]];
[_menuView setDataSource:self];
[_menuView setDelegate:self];
[_menuView setOpaque:YES];
[rootView addSubview:_menuView];
[self updateViewConstraints];
}
- (void)viewDidLoad {
[super viewDidLoad];
[_menuView registerClass:[ToolsMenuViewToolsCell class]
forCellWithReuseIdentifier:kToolsItemCellID];
[_menuView registerClass:[ToolsMenuViewItem cellClass]
forCellWithReuseIdentifier:[ToolsMenuViewItem cellID]];
// Register each custom class.
NSMutableSet* registeredClasses = [NSMutableSet set];
for (size_t i = 0; i < arraysize(itemInfoList); ++i) {
const MenuItemInfo& item = itemInfoList[i];
if (!item.item_class || [registeredClasses containsObject:item.item_class])
continue;
[_menuView registerClass:[item.item_class cellClass]
forCellWithReuseIdentifier:[item.item_class cellID]];
[registeredClasses addObject:item.item_class];
}
}
- (void)updateViewConstraints {
[super updateViewConstraints];
UIView* rootView = [self view];
NSDictionary* view = @{ @"menu" : _menuView };
NSArray* constraints = @[ @"V:|-(0)-[menu]-(0)-|", @"H:|-(0)-[menu]-(0)-|" ];
ApplyVisualConstraints(constraints, view, rootView);
}
#pragma mark - Content Animation Stuff
- (void)animateContentIn {
// Make sure that the collection view has laid out before trying to animate
// the contents.
[_menuView layoutIfNeeded];
NSArray* visibleCells =
[[_menuView visibleCells] sortedArrayUsingComparator:^NSComparisonResult(
UIView* view1, UIView* view2) {
CGPoint center1 = [view1 center];
CGPoint center2 = [view2 center];
if (center1.y < center2.y)
return NSOrderedAscending;
if (center1.y > center2.y)
return NSOrderedDescending;
return NSOrderedSame;
}];
ToolsMenuViewToolsCell* toolsCell = nil;
if (_toolbarType == ToolbarTypeWebiPhone) {
toolsCell = [visibleCells firstObject];
if ([toolsCell isKindOfClass:[ToolsMenuViewToolsCell class]]) {
visibleCells = [visibleCells
subarrayWithRange:NSMakeRange(1, [visibleCells count] - 1)];
} else {
toolsCell = nil;
}
}
[CATransaction begin];
[CATransaction
setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseInOut)];
[CATransaction setAnimationDuration:ios::material::kDuration5];
[CATransaction setCompletionBlock:^{
if (self.requestStartTime != 0) {
UMA_HISTOGRAM_TIMES(
"Toolbar.ShowToolsMenuResponsiveness",
base::TimeDelta::FromSecondsD(
[NSDate timeIntervalSinceReferenceDate] - self.requestStartTime));
// Reset the start time to ensure that whatever happens, we only record
// this once.
self.requestStartTime = 0;
}
}];
AnimateInViews([toolsCell allButtons], 10, 0);
AnimateInViews(visibleCells, 0, -10);
[CATransaction commit];
[[self readingListCell]
updateBadgeCount:_readingListMenuNotifier.readingListUnreadCount
animated:YES];
[[self readingListCell]
updateSeenState:_readingListMenuNotifier.readingListUnseenItemsExist
animated:YES];
}
- (void)hideContent {
_menuView.alpha = 0;
// Remove the target/action for touching the buttons. VoiceOver may hold on
// to these buttons after |self| has been released, causing a crash if a
// button is activated (see http://crbug.com/480974 ).
UIButton* toolsButton = [[self toolsCell] toolsButton];
[toolsButton removeTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
for (UIButton* button in [[self toolsCell] allButtons]) {
[button removeTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
}
}
#pragma mark - Button event handling
- (IBAction)buttonPressed:(id)sender {
int commandId = [sender tag];
DCHECK(commandId);
// The bookmark command workaround is only needed for metrics; remap it
// to the real command for the dispatch. This is very hacky, but it will go
// away soon. See crbug/228521
DCHECK([sender respondsToSelector:@selector(setTag:)]);
if (commandId == IDC_TEMP_EDIT_BOOKMARK)
[sender setTag:IDC_BOOKMARK_PAGE];
// Do nothing when tapping the tools menu a second time.
if (commandId != IDC_SHOW_TOOLS_MENU) {
[self chromeExecuteCommand:sender];
}
if (commandId == IDC_TEMP_EDIT_BOOKMARK)
[sender setTag:IDC_TEMP_EDIT_BOOKMARK];
[_delegate commandWasSelected:commandId];
}
#pragma mark - UICollectionViewDelegate Implementation
- (BOOL)collectionView:(ToolsMenuCollectionView*)view
shouldHighlightItemAtIndexPath:(NSIndexPath*)path {
if (view.tracking)
return NO;
NSInteger item = [self dataIndexForIndexPath:path];
return (item >= 0);
}
- (void)collectionView:(ToolsMenuCollectionView*)view
didHighlightItemAtIndexPath:(NSIndexPath*)path {
ToolsMenuViewCell* cell =
(ToolsMenuViewCell*)[view cellForItemAtIndexPath:path];
NSInteger item = [self dataIndexForIndexPath:path];
DCHECK_GE(item, 0);
ToolsMenuViewItem* menuItem = [_menuItems objectAtIndex:item];
DCHECK(menuItem);
if ([menuItem active]) {
[_touchFeedbackView setFrame:cell.bounds];
[cell addSubview:_touchFeedbackView];
CGPoint touchPoint = [view touchBeginPoint];
touchPoint = [view convertPoint:touchPoint toView:_touchFeedbackView];
[_touchFeedbackView startTouchBeganAnimationAtPoint:touchPoint
completion:nil];
}
}
- (void)collectionView:(ToolsMenuCollectionView*)view
didUnhighlightItemAtIndexPath:(NSIndexPath*)path {
CGPoint touchPoint = [view touchEndPoint];
touchPoint = [view convertPoint:touchPoint toView:_touchFeedbackView];
__weak MDCInkView* inkView = _touchFeedbackView;
_waitForInk = YES;
[_touchFeedbackView startTouchEndedAnimationAtPoint:touchPoint
completion:^{
_waitForInk = NO;
[inkView removeFromSuperview];
}];
}
- (BOOL)collectionView:(UICollectionView*)view
shouldSelectItemAtIndexPath:(NSIndexPath*)path {
NSInteger item = [self dataIndexForIndexPath:path];
if (item < 0)
return NO;
return [[_menuItems objectAtIndex:item] active];
}
- (void)collectionView:(UICollectionView*)view
didSelectItemAtIndexPath:(NSIndexPath*)path {
[view deselectItemAtIndexPath:path animated:YES];
NSInteger item = [self dataIndexForIndexPath:path];
if (item < 0)
return;
dispatch_time_t delayTime = dispatch_time(
DISPATCH_TIME_NOW, (int64_t)(NSEC_PER_SEC * kMDCInkTouchDelayInterval));
dispatch_after(
_waitForInk ? delayTime : 0, dispatch_get_main_queue(), ^(void) {
ToolsMenuViewItem* menuItem = [_menuItems objectAtIndex:item];
DCHECK([menuItem tag]);
[_delegate commandWasSelected:[menuItem tag]];
[self chromeExecuteCommand:menuItem];
});
}
#pragma mark - UICollectionViewDataSource Implementation
- (NSInteger)collectionView:(UICollectionView*)view
numberOfItemsInSection:(NSInteger)section {
NSInteger numberOfItems = [_menuItems count];
if (_toolbarType == ToolbarTypeWebiPhone)
++numberOfItems;
return numberOfItems;
}
- (UICollectionViewCell*)collectionView:(UICollectionView*)view
cellForItemAtIndexPath:(NSIndexPath*)path {
NSInteger item = [self dataIndexForIndexPath:path];
if (item < 0) {
ToolsMenuViewToolsCell* cell =
[view dequeueReusableCellWithReuseIdentifier:kToolsItemCellID
forIndexPath:path];
for (UIButton* button in [cell allButtons]) {
[button addTarget:self
action:@selector(buttonPressed:)
forControlEvents:UIControlEventTouchUpInside];
}
return cell;
}
ToolsMenuViewItem* menuItem = [_menuItems objectAtIndex:item];
ToolsMenuViewCell* menuItemCell =
[view dequeueReusableCellWithReuseIdentifier:[[menuItem class] cellID]
forIndexPath:path];
[menuItemCell configureForMenuItem:menuItem];
return menuItemCell;
}
#pragma mark - UICollectionViewDelegateFlowLayout Implementation
- (CGSize)collectionView:(UICollectionView*)view
layout:(UICollectionViewLayout*)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath*)path {
return CGSizeMake(CGRectGetWidth([_menuView bounds]), kMenuItemHeight);
}
- (CGFloat)collectionView:(UICollectionView*)collectionView
layout:(UICollectionViewLayout*)
collectionViewLayout
minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return 0;
}
#pragma mark - ReadingListMenuNotificationDelegate Implementation
- (void)unreadCountChanged:(NSInteger)unreadCount {
[[self readingListCell] updateBadgeCount:unreadCount animated:YES];
}
- (void)unseenStateChanged:(BOOL)unseenItemsExist {
[[self readingListCell] updateSeenState:unseenItemsExist animated:YES];
}
@end