Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(21)

Unified Diff: ui/base/ios/cru_context_menu_controller.mm

Issue 1103743002: Use UIAlertController on iOS8 for Context Menu. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Set visible to YES right after presenting alert controller. Created 5 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « ui/base/ios/cru_context_menu_controller.h ('k') | ui/base/ios/cru_context_menu_controller_unittest.mm » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
« no previous file with comments | « ui/base/ios/cru_context_menu_controller.h ('k') | ui/base/ios/cru_context_menu_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698