| 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..c81615c0d644fcd8af83e2e73247ea1d20d6789d 100644
|
| --- a/ui/base/ios/cru_context_menu_controller.mm
|
| +++ b/ui/base/ios/cru_context_menu_controller.mm
|
| @@ -7,206 +7,279 @@
|
| #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>
|
| +// Redefined to readwrite.
|
| +@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 {
|
| + // Presenting and dismissing a dummy UIAlertController flushes a screen.
|
| + // 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:nil];
|
| + self.visible = YES;
|
| }
|
| +
|
| @end
|
|
|