1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ui/base/ios/cru_context_menu_controller.h"
9 #include "base/ios/ios_util.h"
10 #include "base/ios/weak_nsobject.h"
11 #include "base/logging.h"
12 #import "base/mac/scoped_nsobject.h"
13 #include "ui/base/device_form_factor.h"
14 #import "ui/base/ios/cru_context_menu_holder.h"
15 #include "ui/base/l10n/l10n_util.h"
16 #import "ui/gfx/ios/NSString+CrStringDrawing.h"
17 #include "ui/strings/grit/ui_strings.h"
21 // Returns the screen's height in points.
22 CGFloat GetScreenHeight() {
23 DCHECK(!base::ios::IsRunningOnIOS8OrLater());
24 switch ([[UIApplication sharedApplication] statusBarOrientation]) {
25 case UIInterfaceOrientationLandscapeLeft:
26 case UIInterfaceOrientationLandscapeRight:
27 return CGRectGetWidth([[UIScreen mainScreen] applicationFrame]);
28 case UIInterfaceOrientationPortraitUpsideDown:
29 case UIInterfaceOrientationPortrait:
30 case UIInterfaceOrientationUnknown:
31 return CGRectGetHeight([[UIScreen mainScreen] applicationFrame]);
37 // Abstracts system implementation of popovers and action sheets.
38 @protocol CRUContextMenuControllerImpl<NSObject>
40 // Whether the context menu is visible.
41 @property(nonatomic, readonly, getter=isVisible) BOOL visible;
43 // Displays a context menu.
44 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
45 atPoint:(CGPoint)localPoint
49 // Backs up CRUContextMenuController on iOS 7 by using UIActionSheet.
50 @interface CRUActionSheetController
51 : NSObject<CRUContextMenuControllerImpl, UIActionSheetDelegate> {
52 // The action sheet used to display the UI.
53 base::scoped_nsobject<UIActionSheet> _sheet;
54 // Holds all the titles and actions for the menu.
55 base::scoped_nsobject<CRUContextMenuHolder> _menuHolder;
59 // Backs up CRUContextMenuController on iOS 8 and higher by using
61 @interface CRUAlertController : NSObject<CRUContextMenuControllerImpl>
62 // Redefined to readwrite.
63 @property(nonatomic, readwrite, getter=isVisible) BOOL visible;
66 // Displays a context menu. Implements Bridge pattern.
67 @implementation CRUContextMenuController {
68 // Implementation specific for iOS version.
69 base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl;
73 return [_impl isVisible];
76 - (instancetype)init {
79 if (base::ios::IsRunningOnIOS8OrLater()) {
80 _impl.reset([[CRUAlertController alloc] init]);
82 _impl.reset([[CRUActionSheetController alloc] init]);
88 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
89 atPoint:(CGPoint)point
90 inView:(UIView*)view {
91 DCHECK(menuHolder.itemCount);
92 // Check that the view is still visible on screen, otherwise just return and
93 // don't show the context menu.
94 if (![view window] && ![view isKindOfClass:[UIWindow class]])
96 [_impl showWithHolder:menuHolder atPoint:point inView:view];
103 @implementation CRUActionSheetController
104 @synthesize visible = _visible;
108 // Context menu must be dismissed explicitly if it is still visible.
109 NSUInteger cancelButtonIndex = [_menuHolder itemCount];
110 [_sheet dismissWithClickedButtonIndex:cancelButtonIndex animated:NO];
115 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
116 atPoint:(CGPoint)point
117 inView:(UIView*)view {
118 // If the content of UIActionSheet does not fit the screen then scrollbars
119 // are added to the menu items area. If that's the case, elide the title to
120 // avoid having scrollbars for menu items.
121 CGSize spaceAvailableForTitle =
122 [self sizeForTitleThatFitsMenuWithHolder:menuHolder
125 NSString* menuTitle = menuHolder.menuTitle;
127 // Show at least one line of text, even if that means the action sheet's
128 // items will need to scroll.
129 const CGFloat kMinimumVerticalSpace = 21;
130 spaceAvailableForTitle.height =
131 std::max(kMinimumVerticalSpace, spaceAvailableForTitle.height);
132 menuTitle = [menuTitle cr_stringByElidingToFitSize:spaceAvailableForTitle];
135 // Present UIActionSheet.
137 [self newActionSheetWithHolder:menuHolder title:menuTitle delegate:self]);
138 [_sheet setCancelButtonIndex:menuHolder.itemCount];
139 [_sheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0)
143 _menuHolder.reset([menuHolder retain]);
147 #pragma mark Implementation
149 // Returns an approximation of the free space available for the title of an
150 // actionSheet filled with |menu| shown in |view| at |point|.
151 - (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder
152 atPoint:(CGPoint)point
153 inView:(UIView*)view {
154 // Create a dummy UIActionSheet.
155 base::scoped_nsobject<UIActionSheet> dummySheet(
156 [self newActionSheetWithHolder:menuHolder title:nil delegate:nil]);
157 // Temporarily add the dummy UIActionSheet to |view|.
158 [dummySheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0)
161 // On iPad the actionsheet is positioned under or over |point| (as opposed
162 // to next to it) when the user clicks within approximately 200 points of
163 // respectively the top or bottom edge. This reduces the amount of vertical
164 // space available for the title, hence the large padding on ipad.
165 const CGFloat kPaddingiPad = 200;
166 const CGFloat kPaddingiPhone = 20;
167 BOOL isIPad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET;
168 const CGFloat padding = isIPad ? kPaddingiPad : kPaddingiPhone;
169 // A title uses the full width of the actionsheet and all the vertical
170 // space on the screen.
171 CGSize result = CGSizeMake(
172 CGRectGetWidth([dummySheet frame]),
173 GetScreenHeight() - CGRectGetHeight([dummySheet frame]) - padding);
174 [dummySheet dismissWithClickedButtonIndex:0 animated:NO];
178 // Returns an UIActionSheet. Callers responsible for releasing returned object.
179 - (UIActionSheet*)newActionSheetWithHolder:(CRUContextMenuHolder*)menuHolder
180 title:(NSString*)title
181 delegate:(id<UIActionSheetDelegate>)delegate {
182 UIActionSheet* sheet = [[UIActionSheet alloc] initWithTitle:title
184 cancelButtonTitle:nil
185 destructiveButtonTitle:nil
186 otherButtonTitles:nil];
188 for (NSString* itemTitle in menuHolder.itemTitles) {
189 [sheet addButtonWithTitle:itemTitle];
191 [sheet addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)];
195 #pragma mark UIActionSheetDelegate
197 // Called when the action sheet is dismissed in the modal context menu sheet.
198 // There is no way to dismiss the sheet without going through this method. Note
199 // that on iPad this method is called with the index of an nonexistent cancel
200 // button when the user taps outside the sheet.
201 - (void)actionSheet:(UIActionSheet*)actionSheet
202 didDismissWithButtonIndex:(NSInteger)buttonIndex {
203 NSUInteger unsignedButtonIndex = buttonIndex;
204 // Assumes "cancel" button is last in order.
205 if (unsignedButtonIndex < [_menuHolder itemCount])
206 [_menuHolder performActionAtIndex:unsignedButtonIndex];
211 // Called when the user chooses a button in the modal context menu sheet. Note
212 // that on iPad this method is called with the index of an nonexistent cancel
213 // button when the user taps outside the sheet.
214 - (void)actionSheet:(UIActionSheet*)actionSheet
215 clickedButtonAtIndex:(NSInteger)buttonIndex {
216 // Some use cases (e.g. opening a new tab on handset) should not wait for the
217 // action sheet to animate away before executing the action.
218 if ([_menuHolder shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) {
219 [_sheet dismissWithClickedButtonIndex:buttonIndex animated:NO];
225 #pragma mark - iOS8 and higher
227 @implementation CRUAlertController
228 @synthesize visible = _visible;
230 - (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder
231 atPoint:(CGPoint)point
232 inView:(UIView*)view {
233 // Presenting and dismissing a dummy UIAlertController flushes a screen.
234 // As a workaround return an estimation of the space available depending
235 // on the device's type.
236 const CGFloat kAvailableWidth = 320;
237 const CGFloat kAvailableHeightTablet = 200;
238 const CGFloat kAvailableHeightPhone = 100;
239 if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
240 return CGSizeMake(kAvailableWidth, kAvailableHeightTablet);
242 return CGSizeMake(kAvailableWidth, kAvailableHeightPhone);
245 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
246 atPoint:(CGPoint)point
247 inView:(UIView*)view {
248 UIAlertController* alert = [UIAlertController
249 alertControllerWithTitle:menuHolder.menuTitle
251 preferredStyle:UIAlertControllerStyleActionSheet];
252 alert.popoverPresentationController.sourceView = view;
253 alert.popoverPresentationController.sourceRect =
254 CGRectMake(point.x, point.y, 1.0, 1.0);
257 base::WeakNSObject<CRUAlertController> weakSelf(self);
258 [menuHolder.itemTitles enumerateObjectsUsingBlock:^(NSString* itemTitle,
259 NSUInteger itemIndex,
261 void (^actionHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
262 [menuHolder performActionAtIndex:itemIndex];
263 [weakSelf setVisible:NO];
265 [alert addAction:[UIAlertAction actionWithTitle:itemTitle
266 style:UIAlertActionStyleDefault
267 handler:actionHandler]];
270 // Cancel button goes last, to match other browsers.
271 void (^cancelHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
272 [weakSelf setVisible:NO];
274 UIAlertAction* cancel_action =
275 [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)
276 style:UIAlertActionStyleCancel
277 handler:cancelHandler];
278 [alert addAction:cancel_action];
280 // Present sheet/popover using controller that is added to view hierarchy.
281 UIViewController* topController = view.window.rootViewController;
282 while (topController.presentedViewController)
283 topController = topController.presentedViewController;
284 [topController presentViewController:alert animated:YES completion:nil];