OLD | NEW |
---|---|
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" | |
10 #include "base/ios/weak_nsobject.h" | 9 #include "base/ios/weak_nsobject.h" |
11 #include "base/logging.h" | 10 #include "base/logging.h" |
12 #import "base/mac/scoped_nsobject.h" | 11 #import "base/mac/scoped_nsobject.h" |
13 #include "ui/base/device_form_factor.h" | 12 #include "ui/base/device_form_factor.h" |
14 #import "ui/base/ios/cru_context_menu_holder.h" | 13 #import "ui/base/ios/cru_context_menu_holder.h" |
15 #include "ui/base/l10n/l10n_util.h" | 14 #include "ui/base/l10n/l10n_util.h" |
16 #import "ui/gfx/ios/NSString+CrStringDrawing.h" | |
17 #include "ui/strings/grit/ui_strings.h" | 15 #include "ui/strings/grit/ui_strings.h" |
18 | 16 |
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] bounds]); | |
28 case UIInterfaceOrientationPortraitUpsideDown: | |
29 case UIInterfaceOrientationPortrait: | |
30 case UIInterfaceOrientationUnknown: | |
31 return CGRectGetHeight([[UIScreen mainScreen] bounds]); | |
32 } | |
33 } | |
34 | |
35 } // namespace | |
36 | |
37 // Abstracts system implementation of popovers and action sheets. | 17 // Abstracts system implementation of popovers and action sheets. |
38 @protocol CRUContextMenuControllerImpl<NSObject> | 18 @protocol CRUContextMenuControllerImpl<NSObject> |
39 | 19 |
40 // Whether the context menu is visible. | 20 // Whether the context menu is visible. |
41 @property(nonatomic, readonly, getter=isVisible) BOOL visible; | 21 @property(nonatomic, readonly, getter=isVisible) BOOL visible; |
42 | 22 |
43 // Displays a context menu. | 23 // Displays a context menu. |
44 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder | 24 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
45 atPoint:(CGPoint)localPoint | 25 atPoint:(CGPoint)localPoint |
46 inView:(UIView*)view; | 26 inView:(UIView*)view; |
47 @end | 27 @end |
48 | 28 |
49 // Backs up CRUContextMenuController on iOS 7 by using UIActionSheet. | 29 // Backs up CRUContextMenuController by using UIAlertController. |
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> | 30 @interface CRUAlertController : NSObject<CRUContextMenuControllerImpl> |
Eugene But (OOO till 7-30)
2016/02/17 15:06:24
There is no need for using a bridge pattern now an
| |
62 // Redefined to readwrite. | 31 // Redefined to readwrite. |
63 @property(nonatomic, readwrite, getter=isVisible) BOOL visible; | 32 @property(nonatomic, readwrite, getter=isVisible) BOOL visible; |
64 @end | 33 @end |
65 | 34 |
66 // Displays a context menu. Implements Bridge pattern. | 35 // Displays a context menu. Implements Bridge pattern. |
67 @implementation CRUContextMenuController { | 36 @implementation CRUContextMenuController { |
68 // Implementation specific for iOS version. | 37 // Implementation specific for iOS version. |
69 base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl; | 38 base::scoped_nsprotocol<id<CRUContextMenuControllerImpl>> _impl; |
70 } | 39 } |
71 | 40 |
72 - (BOOL)isVisible { | 41 - (BOOL)isVisible { |
73 return [_impl isVisible]; | 42 return [_impl isVisible]; |
74 } | 43 } |
75 | 44 |
76 - (instancetype)init { | 45 - (instancetype)init { |
77 self = [super init]; | 46 self = [super init]; |
78 if (self) { | 47 if (self) { |
79 if (base::ios::IsRunningOnIOS8OrLater()) { | 48 _impl.reset([[CRUAlertController alloc] init]); |
80 _impl.reset([[CRUAlertController alloc] init]); | |
81 } else { | |
82 _impl.reset([[CRUActionSheetController alloc] init]); | |
83 } | |
84 } | 49 } |
85 return self; | 50 return self; |
86 } | 51 } |
87 | 52 |
88 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder | 53 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder |
89 atPoint:(CGPoint)point | 54 atPoint:(CGPoint)point |
90 inView:(UIView*)view { | 55 inView:(UIView*)view { |
91 DCHECK(menuHolder.itemCount); | 56 DCHECK(menuHolder.itemCount); |
92 // Check that the view is still visible on screen, otherwise just return and | 57 // Check that the view is still visible on screen, otherwise just return and |
93 // don't show the context menu. | 58 // don't show the context menu. |
94 if (![view window] && ![view isKindOfClass:[UIWindow class]]) | 59 if (![view window] && ![view isKindOfClass:[UIWindow class]]) |
95 return; | 60 return; |
96 [_impl showWithHolder:menuHolder atPoint:point inView:view]; | 61 [_impl showWithHolder:menuHolder atPoint:point inView:view]; |
97 } | 62 } |
98 | 63 |
99 @end | 64 @end |
100 | 65 |
101 #pragma mark - iOS 7 | |
102 | |
103 @implementation CRUActionSheetController | |
104 @synthesize visible = _visible; | |
105 | |
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]; | |
113 } | |
114 | |
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 } | |
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 | |
196 | |
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; | |
209 } | |
210 | |
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 } | |
221 } | |
222 | |
223 @end | |
224 | |
225 #pragma mark - iOS8 and higher | |
226 | |
227 @implementation CRUAlertController | 66 @implementation CRUAlertController |
228 @synthesize visible = _visible; | 67 @synthesize visible = _visible; |
229 | 68 |
230 - (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder | 69 - (CGSize)sizeForTitleThatFitsMenuWithHolder:(CRUContextMenuHolder*)menuHolder |
231 atPoint:(CGPoint)point | 70 atPoint:(CGPoint)point |
232 inView:(UIView*)view { | 71 inView:(UIView*)view { |
233 // Presenting and dismissing a dummy UIAlertController flushes a screen. | 72 // Presenting and dismissing a dummy UIAlertController flushes a screen. |
234 // As a workaround return an estimation of the space available depending | 73 // As a workaround return an estimation of the space available depending |
235 // on the device's type. | 74 // on the device's type. |
236 const CGFloat kAvailableWidth = 320; | 75 const CGFloat kAvailableWidth = 320; |
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
279 | 118 |
280 // Present sheet/popover using controller that is added to view hierarchy. | 119 // Present sheet/popover using controller that is added to view hierarchy. |
281 UIViewController* topController = view.window.rootViewController; | 120 UIViewController* topController = view.window.rootViewController; |
282 while (topController.presentedViewController) | 121 while (topController.presentedViewController) |
283 topController = topController.presentedViewController; | 122 topController = topController.presentedViewController; |
284 [topController presentViewController:alert animated:YES completion:nil]; | 123 [topController presentViewController:alert animated:YES completion:nil]; |
285 self.visible = YES; | 124 self.visible = YES; |
286 } | 125 } |
287 | 126 |
288 @end | 127 @end |
OLD | NEW |