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

Side by Side 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: Resolved Sylvain's review comments 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 unified diff | Download patch
OLDNEW
1 // Copyright 2012 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #import "ui/base/ios/cru_context_menu_controller.h" 5 #import "ui/base/ios/cru_context_menu_controller.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 8
9 #include "base/ios/ios_util.h" 9 #include "base/ios/ios_util.h"
10 #include "base/ios/weak_nsobject.h"
10 #include "base/logging.h" 11 #include "base/logging.h"
11 #import "base/mac/scoped_nsobject.h" 12 #import "base/mac/scoped_nsobject.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "ui/base/device_form_factor.h" 13 #include "ui/base/device_form_factor.h"
14 #import "ui/base/ios/cru_context_menu_holder.h" 14 #import "ui/base/ios/cru_context_menu_holder.h"
15 #include "ui/base/l10n/l10n_util.h" 15 #include "ui/base/l10n/l10n_util.h"
16 #include "ui/gfx/font_list.h"
17 #import "ui/gfx/ios/NSString+CrStringDrawing.h" 16 #import "ui/gfx/ios/NSString+CrStringDrawing.h"
18 #include "ui/gfx/text_elider.h"
19 #include "ui/strings/grit/ui_strings.h" 17 #include "ui/strings/grit/ui_strings.h"
20 18
19 namespace {
20
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 }
33 }
34
35 } // namespace
36
37 // Abstracts system implementation of popovers and action sheets.
38 @protocol CRUContextMenuControllerImpl<NSObject>
39
40 // Whether the context menu is visible.
41 @property(nonatomic, readonly, getter=isVisible) BOOL visible;
42
43 // Displays a context menu.
44 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
45 atPoint:(CGPoint)localPoint
46 inView:(UIView*)view;
47 @end
48
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;
56 }
57 @end
58
59 // Backs up CRUContextMenuController on iOS 8 and higher by using
60 // UIAlertController.
61 @interface CRUAlertController : NSObject<CRUContextMenuControllerImpl>
62 // Redefinied to readwrite.
sdefresne 2015/04/25 11:08:17 nit: s/Redefinied/Redefined/
Eugene But (OOO till 7-30) 2015/04/27 16:42:58 Done.
63 @property(nonatomic, readwrite, getter=isVisible) BOOL visible;
64 @end
65
66 // Displays a context menu. Implements Bridge pattern.
21 @implementation CRUContextMenuController { 67 @implementation CRUContextMenuController {
22 // Holds all the titles and actions for the menu. 68 // Implementation specific for iOS version.
23 base::scoped_nsobject<CRUContextMenuHolder> menuHolder_; 69 base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl;
24 // The action sheet controller used to display the UI. 70 }
25 base::scoped_nsobject<UIActionSheet> sheet_; 71
26 // Whether the context menu is visible. 72 - (BOOL)isVisible {
27 BOOL visible_; 73 return [_impl isVisible];
28 } 74 }
29 75
30 76 - (instancetype)init {
31 // Clean up and reset for the next time. 77 self = [super init];
32 - (void)cleanup { 78 if (self) {
33 // iOS 8 fires multiple callbacks when a button is clicked; one round for the 79 if (base::ios::IsRunningOnIOS8OrLater()) {
34 // button that's clicked and a second round with |buttonIndex| equivalent to 80 _impl.reset([[CRUAlertController alloc] init]);
35 // tapping outside the context menu. Here the sheet's delegate is reset so 81 } else {
36 // that only the first round of callbacks is processed. 82 _impl.reset([[CRUActionSheetController alloc] init]);
37 // Note that iOS 8 needs the |sheet_| to stay alive so it's not reset until 83 }
38 // this CRUContextMenuController is dealloc'd. 84 }
39 [sheet_ setDelegate:nil]; 85 return self;
40 menuHolder_.reset(); 86 }
41 visible_ = NO; 87
42 } 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];
97 }
98
99 @end
100
101 #pragma mark - iOS 7
102
103 @implementation CRUActionSheetController
104 @synthesize visible = _visible;
43 105
44 - (void)dealloc { 106 - (void)dealloc {
45 if (visible_) { 107 if (_visible) {
46 // Context menu must be dismissed explicitly if it is still visible at this 108 // Context menu must be dismissed explicitly if it is still visible.
47 // stage. 109 NSUInteger cancelButtonIndex = [_menuHolder itemCount];
48 NSUInteger cancelButtonIndex = [menuHolder_ itemCount]; 110 [_sheet dismissWithClickedButtonIndex:cancelButtonIndex animated:NO];
49 [sheet_ dismissWithClickedButtonIndex:cancelButtonIndex animated:NO]; 111 }
50 }
51 sheet_.reset();
52 [super dealloc]; 112 [super dealloc];
53 } 113 }
54 114
55 - (BOOL)isVisible { 115 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
56 return visible_; 116 atPoint:(CGPoint)point
57 } 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 }
134
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];
142
143 _menuHolder.reset([menuHolder retain]);
144 _visible = YES;
145 }
146
147 #pragma mark Implementation
148
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;
176 }
177
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];
187
188 for (NSString* itemTitle in menuHolder.itemTitles) {
189 [sheet addButtonWithTitle:itemTitle];
190 }
191 [sheet addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)];
192 return sheet;
193 }
194
195 #pragma mark UIActionSheetDelegate
58 196
59 // Called when the action sheet is dismissed in the modal context menu sheet. 197 // Called when the action sheet is dismissed in the modal context menu sheet.
60 // There is no way to dismiss the sheet without going through this method. Note 198 // There is no way to dismiss the sheet without going through this method. Note
61 // that on iPad this method is called with the index of an nonexistent cancel 199 // that on iPad this method is called with the index of an nonexistent cancel
62 // button when the user taps outside the sheet. 200 // button when the user taps outside the sheet.
63 - (void)actionSheet:(UIActionSheet*)actionSheet 201 - (void)actionSheet:(UIActionSheet*)actionSheet
64 didDismissWithButtonIndex:(NSInteger)buttonIndex { 202 didDismissWithButtonIndex:(NSInteger)buttonIndex {
65 // On iOS 8, if the user taps an item in the context menu, then taps outside 203 NSUInteger unsignedButtonIndex = buttonIndex;
66 // the context menu, the |buttonIndex| passed into this method may be
67 // different from the |buttonIndex| passed into
68 // |actionsheet:willDismissWithButtonIndex:|. See crbug.com/411894.
69 NSUInteger buttonIndexU = buttonIndex;
70 // Assumes "cancel" button is last in order. 204 // Assumes "cancel" button is last in order.
71 if (buttonIndexU < [menuHolder_ itemCount]) 205 if (unsignedButtonIndex < [_menuHolder itemCount])
72 [menuHolder_ performActionAtIndex:buttonIndexU]; 206 [_menuHolder performActionAtIndex:unsignedButtonIndex];
73 [self cleanup]; 207 _menuHolder.reset();
208 _visible = NO;
74 } 209 }
75 210
76 // Called when the user chooses a button in the modal context menu sheet. Note 211 // Called when the user chooses a button in the modal context menu sheet. Note
77 // that on iPad this method is called with the index of an nonexistent cancel 212 // that on iPad this method is called with the index of an nonexistent cancel
78 // button when the user taps outside the sheet. 213 // button when the user taps outside the sheet.
79 - (void)actionSheet:(UIActionSheet*)actionSheet 214 - (void)actionSheet:(UIActionSheet*)actionSheet
80 clickedButtonAtIndex:(NSInteger)buttonIndex { 215 clickedButtonAtIndex:(NSInteger)buttonIndex {
81 // Some use cases (e.g. opening a new tab on handset) should not wait for the 216 // Some use cases (e.g. opening a new tab on handset) should not wait for the
82 // action sheet to animate away before executing the action. 217 // action sheet to animate away before executing the action.
83 if ([menuHolder_ shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) { 218 if ([_menuHolder shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) {
84 [sheet_ dismissWithClickedButtonIndex:buttonIndex animated:NO]; 219 [_sheet dismissWithClickedButtonIndex:buttonIndex animated:NO];
85 } 220 }
86 } 221 }
87 222
88 #pragma mark - 223 @end
89 #pragma mark WebContextMenuDelegate methods 224
90 225 #pragma mark - iOS8 and higher
91 // Displays a menu using a sheet with the given title. 226
92 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder 227 @implementation CRUAlertController
93 atPoint:(CGPoint)localPoint 228 @synthesize visible = _visible;
229
230 - (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder
231 atPoint:(CGPoint)point
232 inView:(UIView*)view {
233 // On presenting and dismissing a dummy UIAlertController flushes a screen.
sdefresne 2015/04/25 11:08:16 nit: // Presenting and dismissing a dummy ...
Eugene But (OOO till 7-30) 2015/04/27 16:42:58 Done.
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);
243 }
244
245 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
246 atPoint:(CGPoint)point
94 inView:(UIView*)view { 247 inView:(UIView*)view {
95 DCHECK([menuHolder itemCount]); 248 UIAlertController* alert = [UIAlertController
96 menuHolder_.reset([menuHolder retain]); 249 alertControllerWithTitle:menuHolder.menuTitle
97 // Check that the view is still visible on screen, otherwise just return and 250 message:nil
98 // don't show the context menu. 251 preferredStyle:UIAlertControllerStyleActionSheet];
99 DCHECK([view window] || [view isKindOfClass:[UIWindow class]]); 252 alert.popoverPresentationController.sourceView = view;
100 if (![view window] && ![view isKindOfClass:[UIWindow class]]) 253 alert.popoverPresentationController.sourceRect =
101 return; 254 CGRectMake(point.x, point.y, 1.0, 1.0);
102 CGSize spaceAvailableForTitle = 255
103 [CRUContextMenuController 256 // Add the actions.
104 availableSpaceForTitleInActionSheetWithMenu:menuHolder_ 257 base::WeakNSObject<CRUAlertController> weakSelf(self);
105 atPoint:localPoint 258 [menuHolder.itemTitles enumerateObjectsUsingBlock:^(NSString* itemTitle,
106 inView:view]; 259 NSUInteger itemIndex,
107 NSString* title = menuHolder.menuTitle; 260 BOOL*) {
108 if (title) { 261 void (^actionHandler)(UIAlertAction*) = ^(UIAlertAction* action) {
109 // Show at least one line of text, even if that means the UIActionSheet's 262 [menuHolder performActionAtIndex:itemIndex];
110 // items will need to scroll. 263 [weakSelf setVisible:NO];
111 const CGFloat kMinimumVerticalSpace = 21; 264 };
112 spaceAvailableForTitle.height = 265 [alert addAction:[UIAlertAction actionWithTitle:itemTitle
113 std::max(kMinimumVerticalSpace, spaceAvailableForTitle.height); 266 style:UIAlertActionStyleDefault
114 title = [title cr_stringByElidingToFitSize:spaceAvailableForTitle]; 267 handler:actionHandler]];
115 } 268 }];
116 // Create the sheet. 269
117 sheet_.reset(
118 [[UIActionSheet alloc] initWithTitle:title
119 delegate:self
120 cancelButtonTitle:nil
121 destructiveButtonTitle:nil
122 otherButtonTitles:nil]);
123 // Add the labels, in order, to the sheet.
124 for (NSString* label in [menuHolder_ itemTitles]) {
125 [sheet_ addButtonWithTitle:label];
126 }
127 // Cancel button goes last, to match other browsers. 270 // Cancel button goes last, to match other browsers.
128 [sheet_ addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)]; 271 UIAlertAction* cancel_action =
129 [sheet_ setCancelButtonIndex:[menuHolder_ itemCount]]; 272 [UIAlertAction actionWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)
130 [sheet_ showFromRect:CGRectMake(localPoint.x, localPoint.y, 1.0, 1.0) 273 style:UIAlertActionStyleCancel
131 inView:view 274 handler:nil];
132 animated:YES]; 275 [alert addAction:cancel_action];
133 276
134 visible_ = YES; 277 // Present sheet/popover using controller that is added to view hierarchy.
135 } 278 UIViewController* topController = view.window.rootViewController;
136 279 while (topController.presentedViewController)
137 // Returns an approximation of the free space available for the title of an 280 topController = topController.presentedViewController;
138 // actionSheet filled with |menu| shown in |view| at |point|. 281 [topController presentViewController:alert
139 + (CGSize) 282 animated:YES
140 availableSpaceForTitleInActionSheetWithMenu:(CRUContextMenuHolder*)menu 283 completion:^{
141 atPoint:(CGPoint)point 284 [weakSelf setVisible:YES];
142 inView:(UIView*)view { 285 }];
143 BOOL isIpad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET; 286 }
144 if (base::ios::IsRunningOnIOS8OrLater()) { 287
145 // On iOS8 presenting and dismissing a dummy UIActionSheet does not work 288 @end
146 // (http://crbug.com/392245 and rdar://17677745).
147 // As a workaround we return an estimation of the space available depending
148 // on the device's type.
149 const CGFloat kAvailableWidth = 320;
150 const CGFloat kAvailableHeightTablet = 200;
151 const CGFloat kAvailableHeightPhone = 100;
152 if (isIpad) {
153 return CGSizeMake(kAvailableWidth, kAvailableHeightTablet);
154 }
155 return CGSizeMake(kAvailableWidth, kAvailableHeightPhone);
156 } else {
157 // Creates a dummy UIActionSheet.
158 base::scoped_nsobject<UIActionSheet> dummyActionSheet(
159 [[UIActionSheet alloc] initWithTitle:nil
160 delegate:nil
161 cancelButtonTitle:nil
162 destructiveButtonTitle:nil
163 otherButtonTitles:nil]);
164 for (NSString* label in [menu itemTitles]) {
165 [dummyActionSheet addButtonWithTitle:label];
166 }
167 [dummyActionSheet addButtonWithTitle:
168 l10n_util::GetNSString(IDS_APP_CANCEL)];
169 // Temporarily adds the dummy UIActionSheet to |view|.
170 [dummyActionSheet showFromRect:CGRectMake(point.x, point.y, 1.0, 1.0)
171 inView:view
172 animated:NO];
173 // On iPad the actionsheet is positioned under or over |point| (as opposed
174 // to next to it) when the user clicks within approximately 200 points of
175 // respectively the top or bottom edge. This reduces the amount of vertical
176 // space available for the title, hence the large padding on ipad.
177 const int kPaddingiPad = 200;
178 const int kPaddingiPhone = 20;
179 CGFloat padding = isIpad ? kPaddingiPad : kPaddingiPhone;
180 // A title uses the full width of the actionsheet and all the vertical
181 // space on the screen.
182 CGSize availableSpaceForTitle =
183 CGSizeMake([dummyActionSheet frame].size.width,
184 [CRUContextMenuController screenHeight] -
185 [dummyActionSheet frame].size.height -
186 padding);
187 [dummyActionSheet dismissWithClickedButtonIndex:0 animated:NO];
188 return availableSpaceForTitle;
189 }
190 }
191
192 // Returns the screen's height in pixels.
193 + (int)screenHeight {
194 DCHECK(!base::ios::IsRunningOnIOS8OrLater());
195 switch ([[UIApplication sharedApplication] statusBarOrientation]) {
196 case UIInterfaceOrientationLandscapeLeft:
197 case UIInterfaceOrientationLandscapeRight:
198 return [[UIScreen mainScreen] applicationFrame].size.width;
199 case UIInterfaceOrientationPortraitUpsideDown:
200 case UIInterfaceOrientationPortrait:
201 case UIInterfaceOrientationUnknown:
202 return [[UIScreen mainScreen] applicationFrame].size.height;
203 }
204 }
205
206 @end
207
208 @implementation CRUContextMenuController (UsedForTesting)
209 - (UIActionSheet*)sheet {
210 return sheet_.get();
211 }
212 @end
OLDNEW
« 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