Linux: Depend on liberation-fonts package for RPMs.
[chromium-blink-merge.git] / ui / base / ios / cru_context_menu_controller.mm
blob27ce9ee25808a5151507b0c5b16bc81cc109bc3a
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"
7 #include <algorithm>
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"
19 namespace {
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]);
32   }
35 }  // namespace
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
46                 inView:(UIView*)view;
47 @end
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;
57 @end
59 // Backs up CRUContextMenuController on iOS 8 and higher by using
60 // UIAlertController.
61 @interface CRUAlertController : NSObject<CRUContextMenuControllerImpl>
62 // Redefined to readwrite.
63 @property(nonatomic, readwrite, getter=isVisible) BOOL visible;
64 @end
66 // Displays a context menu. Implements Bridge pattern.
67 @implementation CRUContextMenuController {
68   // Implementation specific for iOS version.
69   base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl;
72 - (BOOL)isVisible {
73   return [_impl isVisible];
76 - (instancetype)init {
77   self = [super init];
78   if (self) {
79     if (base::ios::IsRunningOnIOS8OrLater()) {
80       _impl.reset([[CRUAlertController alloc] init]);
81     } else {
82       _impl.reset([[CRUActionSheetController alloc] init]);
83     }
84   }
85   return self;
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]])
95     return;
96   [_impl showWithHolder:menuHolder atPoint:point inView:view];
99 @end
101 #pragma mark - iOS 7
103 @implementation CRUActionSheetController
104 @synthesize visible = _visible;
106 - (void)dealloc {
107   if (_visible) {
108     // Context menu must be dismissed explicitly if it is still visible.
109     NSUInteger cancelButtonIndex = [_menuHolder itemCount];
110     [_sheet dismissWithClickedButtonIndex:cancelButtonIndex animated:NO];
111   }
112   [super dealloc];
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
123                                        atPoint:point
124                                         inView:view];
125   NSString* menuTitle = menuHolder.menuTitle;
126   if (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];
133   }
135   // Present UIActionSheet.
136   _sheet.reset(
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)
140                 inView:view
141               animated:YES];
143   _menuHolder.reset([menuHolder retain]);
144   _visible = YES;
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)
159                     inView:view
160                   animated:NO];
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];
175   return result;
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
183                                                      delegate:delegate
184                                             cancelButtonTitle:nil
185                                        destructiveButtonTitle:nil
186                                             otherButtonTitles:nil];
188   for (NSString* itemTitle in menuHolder.itemTitles) {
189     [sheet addButtonWithTitle:itemTitle];
190   }
191   [sheet addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)];
192   return sheet;
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];
207   _menuHolder.reset();
208   _visible = NO;
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];
220   }
223 @end
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);
241   }
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
250                        message:nil
251                 preferredStyle:UIAlertControllerStyleActionSheet];
252   alert.popoverPresentationController.sourceView = view;
253   alert.popoverPresentationController.sourceRect =
254       CGRectMake(point.x, point.y, 1.0, 1.0);
256   // Add the actions.
257   base::WeakNSObject<CRUAlertController> weakSelf(self);
258   [menuHolder.itemTitles enumerateObjectsUsingBlock:^(NSString* itemTitle,
259                                                       NSUInteger itemIndex,
260                                                       BOOL*) {
261     void (^actionHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
262       [menuHolder performActionAtIndex:itemIndex];
263       [weakSelf setVisible:NO];
264     };
265     [alert addAction:[UIAlertAction actionWithTitle:itemTitle
266                                               style:UIAlertActionStyleDefault
267                                             handler:actionHandler]];
268   }];
270   // Cancel button goes last, to match other browsers.
271   void (^cancelHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
272     [weakSelf setVisible:NO];
273   };
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];
285   self.visible = YES;
288 @end