Chromium Code Reviews| Index: ui/base/ios/cru_context_menu_controller.mm |
| diff --git a/ui/base/ios/cru_context_menu_controller.mm b/ui/base/ios/cru_context_menu_controller.mm |
| index 40a08dfcc9ac651e2f9271ec30211d0fa366d32b..06b5988d61f9fa16f90f03ffdb5d2b23d5f003e7 100644 |
| --- a/ui/base/ios/cru_context_menu_controller.mm |
| +++ b/ui/base/ios/cru_context_menu_controller.mm |
| @@ -7,206 +7,282 @@ |
| #include <algorithm> |
| #include "base/ios/ios_util.h" |
| +#include "base/ios/weak_nsobject.h" |
| #include "base/logging.h" |
| #import "base/mac/scoped_nsobject.h" |
| -#include "base/strings/sys_string_conversions.h" |
| #include "ui/base/device_form_factor.h" |
| #import "ui/base/ios/cru_context_menu_holder.h" |
| #include "ui/base/l10n/l10n_util.h" |
| -#include "ui/gfx/font_list.h" |
| #import "ui/gfx/ios/NSString+CrStringDrawing.h" |
| -#include "ui/gfx/text_elider.h" |
| #include "ui/strings/grit/ui_strings.h" |
| -@implementation CRUContextMenuController { |
| - // Holds all the titles and actions for the menu. |
| - base::scoped_nsobject<CRUContextMenuHolder> menuHolder_; |
| - // The action sheet controller used to display the UI. |
| - base::scoped_nsobject<UIActionSheet> sheet_; |
| - // Whether the context menu is visible. |
| - BOOL visible_; |
| +namespace { |
| + |
| +// Returns the screen's height in points. |
| +CGFloat GetScreenHeight() { |
| + DCHECK(!base::ios::IsRunningOnIOS8OrLater()); |
| + switch ([[UIApplication sharedApplication] statusBarOrientation]) { |
| + case UIInterfaceOrientationLandscapeLeft: |
| + case UIInterfaceOrientationLandscapeRight: |
| + return CGRectGetWidth([[UIScreen mainScreen] applicationFrame]); |
| + case UIInterfaceOrientationPortraitUpsideDown: |
| + case UIInterfaceOrientationPortrait: |
| + case UIInterfaceOrientationUnknown: |
| + return CGRectGetHeight([[UIScreen mainScreen] applicationFrame]); |
| + } |
| } |
| +} // namespace |
| -// Clean up and reset for the next time. |
| -- (void)cleanup { |
| - // iOS 8 fires multiple callbacks when a button is clicked; one round for the |
| - // button that's clicked and a second round with |buttonIndex| equivalent to |
| - // tapping outside the context menu. Here the sheet's delegate is reset so |
| - // that only the first round of callbacks is processed. |
| - // Note that iOS 8 needs the |sheet_| to stay alive so it's not reset until |
| - // this CRUContextMenuController is dealloc'd. |
| - [sheet_ setDelegate:nil]; |
| - menuHolder_.reset(); |
| - visible_ = NO; |
| -} |
| +// Abstracts system implementation of popovers and action sheets. |
| +@protocol CRUContextMenuControllerImpl<NSObject> |
| -- (void)dealloc { |
| - if (visible_) { |
| - // Context menu must be dismissed explicitly if it is still visible at this |
| - // stage. |
| - NSUInteger cancelButtonIndex = [menuHolder_ itemCount]; |
| - [sheet_ dismissWithClickedButtonIndex:cancelButtonIndex animated:NO]; |
| - } |
| - sheet_.reset(); |
| - [super dealloc]; |
| +// Whether the context menu is visible. |
| +@property(nonatomic, readonly, getter=isVisible) BOOL visible; |
| + |
| +// Displays a context menu. |
| +- (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
| + atPoint:(CGPoint)localPoint |
| + inView:(UIView*)view; |
| +@end |
| + |
| +// Backs up CRUContextMenuController on iOS 7 by using UIActionSheet. |
| +@interface CRUActionSheetController |
| + : NSObject<CRUContextMenuControllerImpl, UIActionSheetDelegate> { |
| + // The action sheet used to display the UI. |
| + base::scoped_nsobject<UIActionSheet> _sheet; |
| + // Holds all the titles and actions for the menu. |
| + base::scoped_nsobject<CRUContextMenuHolder> _menuHolder; |
| } |
| +@end |
| -- (BOOL)isVisible { |
| - return visible_; |
| +// Backs up CRUContextMenuController on iOS 8 and higher by using |
| +// UIAlertController. |
| +@interface CRUAlertController : NSObject<CRUContextMenuControllerImpl> |
| +// Redefinied to readwrite. |
|
sdefresne
2015/04/25 11:08:17
nit: s/Redefinied/Redefined/
Eugene But (OOO till 7-30)
2015/04/27 16:42:58
Done.
|
| +@property(nonatomic, readwrite, getter=isVisible) BOOL visible; |
| +@end |
| + |
| +// Displays a context menu. Implements Bridge pattern. |
| +@implementation CRUContextMenuController { |
| + // Implementation specific for iOS version. |
| + base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl; |
| } |
| -// Called when the action sheet is dismissed in the modal context menu sheet. |
| -// There is no way to dismiss the sheet without going through this method. Note |
| -// that on iPad this method is called with the index of an nonexistent cancel |
| -// button when the user taps outside the sheet. |
| -- (void)actionSheet:(UIActionSheet*)actionSheet |
| - didDismissWithButtonIndex:(NSInteger)buttonIndex { |
| - // On iOS 8, if the user taps an item in the context menu, then taps outside |
| - // the context menu, the |buttonIndex| passed into this method may be |
| - // different from the |buttonIndex| passed into |
| - // |actionsheet:willDismissWithButtonIndex:|. See crbug.com/411894. |
| - NSUInteger buttonIndexU = buttonIndex; |
| - // Assumes "cancel" button is last in order. |
| - if (buttonIndexU < [menuHolder_ itemCount]) |
| - [menuHolder_ performActionAtIndex:buttonIndexU]; |
| - [self cleanup]; |
| +- (BOOL)isVisible { |
| + return [_impl isVisible]; |
| } |
| -// Called when the user chooses a button in the modal context menu sheet. Note |
| -// that on iPad this method is called with the index of an nonexistent cancel |
| -// button when the user taps outside the sheet. |
| -- (void)actionSheet:(UIActionSheet*)actionSheet |
| - clickedButtonAtIndex:(NSInteger)buttonIndex { |
| - // Some use cases (e.g. opening a new tab on handset) should not wait for the |
| - // action sheet to animate away before executing the action. |
| - if ([menuHolder_ shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) { |
| - [sheet_ dismissWithClickedButtonIndex:buttonIndex animated:NO]; |
| +- (instancetype)init { |
| + self = [super init]; |
| + if (self) { |
| + if (base::ios::IsRunningOnIOS8OrLater()) { |
| + _impl.reset([[CRUAlertController alloc] init]); |
| + } else { |
| + _impl.reset([[CRUActionSheetController alloc] init]); |
| + } |
| } |
| + return self; |
| } |
| -#pragma mark - |
| -#pragma mark WebContextMenuDelegate methods |
| - |
| -// Displays a menu using a sheet with the given title. |
| - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
| - atPoint:(CGPoint)localPoint |
| + atPoint:(CGPoint)point |
| inView:(UIView*)view { |
| - DCHECK([menuHolder itemCount]); |
| - menuHolder_.reset([menuHolder retain]); |
| + DCHECK(menuHolder.itemCount); |
| // Check that the view is still visible on screen, otherwise just return and |
| // don't show the context menu. |
| - DCHECK([view window] || [view isKindOfClass:[UIWindow class]]); |
| if (![view window] && ![view isKindOfClass:[UIWindow class]]) |
| return; |
| + [_impl showWithHolder:menuHolder atPoint:point inView:view]; |
| +} |
| + |
| +@end |
| + |
| +#pragma mark - iOS 7 |
| + |
| +@implementation CRUActionSheetController |
| +@synthesize visible = _visible; |
| + |
| +- (void)dealloc { |
| + if (_visible) { |
| + // Context menu must be dismissed explicitly if it is still visible. |
| + NSUInteger cancelButtonIndex = [_menuHolder itemCount]; |
| + [_sheet dismissWithClickedButtonIndex:cancelButtonIndex animated:NO]; |
| + } |
| + [super dealloc]; |
| +} |
| + |
| +- (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
| + atPoint:(CGPoint)point |
| + inView:(UIView*)view { |
| + // If the content of UIActionSheet does not fit the screen then scrollbars |
| + // are added to the menu items area. If that's the case, elide the title to |
| + // avoid having scrollbars for menu items. |
| CGSize spaceAvailableForTitle = |
| - [CRUContextMenuController |
| - availableSpaceForTitleInActionSheetWithMenu:menuHolder_ |
| - atPoint:localPoint |
| - inView:view]; |
| - NSString* title = menuHolder.menuTitle; |
| - if (title) { |
| - // Show at least one line of text, even if that means the UIActionSheet's |
| + [self sizeForTitleThatFitsMenuWithHolder:menuHolder |
| + atPoint:point |
| + inView:view]; |
| + NSString* menuTitle = menuHolder.menuTitle; |
| + if (menuTitle) { |
| + // Show at least one line of text, even if that means the action sheet's |
| // items will need to scroll. |
| const CGFloat kMinimumVerticalSpace = 21; |
| spaceAvailableForTitle.height = |
| std::max(kMinimumVerticalSpace, spaceAvailableForTitle.height); |
| - title = [title cr_stringByElidingToFitSize:spaceAvailableForTitle]; |
| - } |
| - // Create the sheet. |
| - sheet_.reset( |
| - [[UIActionSheet alloc] initWithTitle:title |
| - delegate:self |
| - cancelButtonTitle:nil |
| - destructiveButtonTitle:nil |
| - otherButtonTitles:nil]); |
| - // Add the labels, in order, to the sheet. |
| - for (NSString* label in [menuHolder_ itemTitles]) { |
| - [sheet_ addButtonWithTitle:label]; |
| + menuTitle = [menuTitle cr_stringByElidingToFitSize:spaceAvailableForTitle]; |
| } |
| - // Cancel button goes last, to match other browsers. |
| - [sheet_ addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)]; |
| - [sheet_ setCancelButtonIndex:[menuHolder_ itemCount]]; |
| - [sheet_ showFromRect:CGRectMake(localPoint.x, localPoint.y, 1.0, 1.0) |
| + |
| + // Present UIActionSheet. |
| + _sheet.reset( |
| + [self newActionSheetWithHolder:menuHolder title:menuTitle delegate:self]); |
| + [_sheet setCancelButtonIndex:menuHolder.itemCount]; |
| + [_sheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0) |
| inView:view |
| animated:YES]; |
| - visible_ = YES; |
| + _menuHolder.reset([menuHolder retain]); |
| + _visible = YES; |
| } |
| +#pragma mark Implementation |
| + |
| // Returns an approximation of the free space available for the title of an |
| // actionSheet filled with |menu| shown in |view| at |point|. |
| -+ (CGSize) |
| - availableSpaceForTitleInActionSheetWithMenu:(CRUContextMenuHolder*)menu |
| - atPoint:(CGPoint)point |
| - inView:(UIView*)view { |
| - BOOL isIpad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET; |
| - if (base::ios::IsRunningOnIOS8OrLater()) { |
| - // On iOS8 presenting and dismissing a dummy UIActionSheet does not work |
| - // (http://crbug.com/392245 and rdar://17677745). |
| - // As a workaround we return an estimation of the space available depending |
| - // on the device's type. |
| - const CGFloat kAvailableWidth = 320; |
| - const CGFloat kAvailableHeightTablet = 200; |
| - const CGFloat kAvailableHeightPhone = 100; |
| - if (isIpad) { |
| - return CGSizeMake(kAvailableWidth, kAvailableHeightTablet); |
| - } |
| - return CGSizeMake(kAvailableWidth, kAvailableHeightPhone); |
| - } else { |
| - // Creates a dummy UIActionSheet. |
| - base::scoped_nsobject<UIActionSheet> dummyActionSheet( |
| - [[UIActionSheet alloc] initWithTitle:nil |
| - delegate:nil |
| - cancelButtonTitle:nil |
| - destructiveButtonTitle:nil |
| - otherButtonTitles:nil]); |
| - for (NSString* label in [menu itemTitles]) { |
| - [dummyActionSheet addButtonWithTitle:label]; |
| - } |
| - [dummyActionSheet addButtonWithTitle: |
| - l10n_util::GetNSString(IDS_APP_CANCEL)]; |
| - // Temporarily adds the dummy UIActionSheet to |view|. |
| - [dummyActionSheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0) |
| - inView:view |
| - animated:NO]; |
| - // On iPad the actionsheet is positioned under or over |point| (as opposed |
| - // to next to it) when the user clicks within approximately 200 points of |
| - // respectively the top or bottom edge. This reduces the amount of vertical |
| - // space available for the title, hence the large padding on ipad. |
| - const int kPaddingiPad = 200; |
| - const int kPaddingiPhone = 20; |
| - CGFloat padding = isIpad ? kPaddingiPad : kPaddingiPhone; |
| - // A title uses the full width of the actionsheet and all the vertical |
| - // space on the screen. |
| - CGSize availableSpaceForTitle = |
| - CGSizeMake([dummyActionSheet frame].size.width, |
| - [CRUContextMenuController screenHeight] - |
| - [dummyActionSheet frame].size.height - |
| - padding); |
| - [dummyActionSheet dismissWithClickedButtonIndex:0 animated:NO]; |
| - return availableSpaceForTitle; |
| +- (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder |
| + atPoint:(CGPoint)point |
| + inView:(UIView*)view { |
| + // Create a dummy UIActionSheet. |
| + base::scoped_nsobject<UIActionSheet> dummySheet( |
| + [self newActionSheetWithHolder:menuHolder title:nil delegate:nil]); |
| + // Temporarily add the dummy UIActionSheet to |view|. |
| + [dummySheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0) |
| + inView:view |
| + animated:NO]; |
| + // On iPad the actionsheet is positioned under or over |point| (as opposed |
| + // to next to it) when the user clicks within approximately 200 points of |
| + // respectively the top or bottom edge. This reduces the amount of vertical |
| + // space available for the title, hence the large padding on ipad. |
| + const CGFloat kPaddingiPad = 200; |
| + const CGFloat kPaddingiPhone = 20; |
| + BOOL isIPad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET; |
| + const CGFloat padding = isIPad ? kPaddingiPad : kPaddingiPhone; |
| + // A title uses the full width of the actionsheet and all the vertical |
| + // space on the screen. |
| + CGSize result = CGSizeMake( |
| + CGRectGetWidth([dummySheet frame]), |
| + GetScreenHeight() - CGRectGetHeight([dummySheet frame]) - padding); |
| + [dummySheet dismissWithClickedButtonIndex:0 animated:NO]; |
| + return result; |
| +} |
| + |
| +// Returns an UIActionSheet. Callers responsible for releasing returned object. |
| +- (UIActionSheet*)newActionSheetWithHolder:(CRUContextMenuHolder*)menuHolder |
| + title:(NSString*)title |
| + delegate:(id<UIActionSheetDelegate>)delegate { |
| + UIActionSheet* sheet = [[UIActionSheet alloc] initWithTitle:title |
| + delegate:delegate |
| + cancelButtonTitle:nil |
| + destructiveButtonTitle:nil |
| + otherButtonTitles:nil]; |
| + |
| + for (NSString* itemTitle in menuHolder.itemTitles) { |
| + [sheet addButtonWithTitle:itemTitle]; |
| } |
| + [sheet addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)]; |
| + return sheet; |
| } |
| -// Returns the screen's height in pixels. |
| -+ (int)screenHeight { |
| - DCHECK(!base::ios::IsRunningOnIOS8OrLater()); |
| - switch ([[UIApplication sharedApplication] statusBarOrientation]) { |
| - case UIInterfaceOrientationLandscapeLeft: |
| - case UIInterfaceOrientationLandscapeRight: |
| - return [[UIScreen mainScreen] applicationFrame].size.width; |
| - case UIInterfaceOrientationPortraitUpsideDown: |
| - case UIInterfaceOrientationPortrait: |
| - case UIInterfaceOrientationUnknown: |
| - return [[UIScreen mainScreen] applicationFrame].size.height; |
| +#pragma mark UIActionSheetDelegate |
| + |
| +// Called when the action sheet is dismissed in the modal context menu sheet. |
| +// There is no way to dismiss the sheet without going through this method. Note |
| +// that on iPad this method is called with the index of an nonexistent cancel |
| +// button when the user taps outside the sheet. |
| +- (void)actionSheet:(UIActionSheet*)actionSheet |
| + didDismissWithButtonIndex:(NSInteger)buttonIndex { |
| + NSUInteger unsignedButtonIndex = buttonIndex; |
| + // Assumes "cancel" button is last in order. |
| + if (unsignedButtonIndex < [_menuHolder itemCount]) |
| + [_menuHolder performActionAtIndex:unsignedButtonIndex]; |
| + _menuHolder.reset(); |
| + _visible = NO; |
| +} |
| + |
| +// Called when the user chooses a button in the modal context menu sheet. Note |
| +// that on iPad this method is called with the index of an nonexistent cancel |
| +// button when the user taps outside the sheet. |
| +- (void)actionSheet:(UIActionSheet*)actionSheet |
| + clickedButtonAtIndex:(NSInteger)buttonIndex { |
| + // Some use cases (e.g. opening a new tab on handset) should not wait for the |
| + // action sheet to animate away before executing the action. |
| + if ([_menuHolder shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) { |
| + [_sheet dismissWithClickedButtonIndex:buttonIndex animated:NO]; |
| } |
| } |
| @end |
| -@implementation CRUContextMenuController (UsedForTesting) |
| -- (UIActionSheet*)sheet { |
| - return sheet_.get(); |
| +#pragma mark - iOS8 and higher |
| + |
| +@implementation CRUAlertController |
| +@synthesize visible = _visible; |
| + |
| +- (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder |
| + atPoint:(CGPoint)point |
| + inView:(UIView*)view { |
| + // On presenting and dismissing a dummy UIAlertController flushes a screen. |
|
sdefresne
2015/04/25 11:08:16
nit: // Presenting and dismissing a dummy ...
Eugene But (OOO till 7-30)
2015/04/27 16:42:58
Done.
|
| + // As a workaround return an estimation of the space available depending |
| + // on the device's type. |
| + const CGFloat kAvailableWidth = 320; |
| + const CGFloat kAvailableHeightTablet = 200; |
| + const CGFloat kAvailableHeightPhone = 100; |
| + if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) { |
| + return CGSizeMake(kAvailableWidth, kAvailableHeightTablet); |
| + } |
| + return CGSizeMake(kAvailableWidth, kAvailableHeightPhone); |
| +} |
| + |
| +- (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
| + atPoint:(CGPoint)point |
| + inView:(UIView*)view { |
| + UIAlertController* alert = [UIAlertController |
| + alertControllerWithTitle:menuHolder.menuTitle |
| + message:nil |
| + preferredStyle:UIAlertControllerStyleActionSheet]; |
| + alert.popoverPresentationController.sourceView = view; |
| + alert.popoverPresentationController.sourceRect = |
| + CGRectMake(point.x, point.y, 1.0, 1.0); |
| + |
| + // Add the actions. |
| + base::WeakNSObject<CRUAlertController> weakSelf(self); |
| + [menuHolder.itemTitles enumerateObjectsUsingBlock:^(NSString* itemTitle, |
| + NSUInteger itemIndex, |
| + BOOL*) { |
| + void (^actionHandler)(UIAlertAction*) = ^(UIAlertAction* action) { |
| + [menuHolder performActionAtIndex:itemIndex]; |
| + [weakSelf setVisible:NO]; |
| + }; |
| + [alert addAction:[UIAlertAction actionWithTitle:itemTitle |
| + style:UIAlertActionStyleDefault |
| + handler:actionHandler]]; |
| + }]; |
| + |
| + // Cancel button goes last, to match other browsers. |
| + UIAlertAction* cancel_action = |
| + [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL) |
| + style:UIAlertActionStyleCancel |
| + handler:nil]; |
| + [alert addAction:cancel_action]; |
| + |
| + // Present sheet/popover using controller that is added to view hierarchy. |
| + UIViewController* topController = view.window.rootViewController; |
| + while (topController.presentedViewController) |
| + topController = topController.presentedViewController; |
| + [topController presentViewController:alert |
| + animated:YES |
| + completion:^{ |
| + [weakSelf setVisible:YES]; |
| + }]; |
| } |
| + |
| @end |