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 |