Drive: Add BatchableRequest subclass.
[chromium-blink-merge.git] / ui / base / ios / cru_context_menu_controller.mm
blob40a08dfcc9ac651e2f9271ec30211d0fa366d32b
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/logging.h"
11 #import "base/mac/scoped_nsobject.h"
12 #include "base/strings/sys_string_conversions.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 #include "ui/gfx/font_list.h"
17 #import "ui/gfx/ios/NSString+CrStringDrawing.h"
18 #include "ui/gfx/text_elider.h"
19 #include "ui/strings/grit/ui_strings.h"
21 @implementation CRUContextMenuController {
22   // Holds all the titles and actions for the menu.
23   base::scoped_nsobject<CRUContextMenuHolder> menuHolder_;
24   // The action sheet controller used to display the UI.
25   base::scoped_nsobject<UIActionSheet> sheet_;
26   // Whether the context menu is visible.
27   BOOL visible_;
31 // Clean up and reset for the next time.
32 - (void)cleanup {
33   // iOS 8 fires multiple callbacks when a button is clicked; one round for the
34   // button that's clicked and a second round with |buttonIndex| equivalent to
35   // tapping outside the context menu. Here the sheet's delegate is reset so
36   // that only the first round of callbacks is processed.
37   // Note that iOS 8 needs the |sheet_| to stay alive so it's not reset until
38   // this CRUContextMenuController is dealloc'd.
39   [sheet_ setDelegate:nil];
40   menuHolder_.reset();
41   visible_ = NO;
44 - (void)dealloc {
45   if (visible_) {
46     // Context menu must be dismissed explicitly if it is still visible at this
47     // stage.
48     NSUInteger cancelButtonIndex = [menuHolder_ itemCount];
49     [sheet_ dismissWithClickedButtonIndex:cancelButtonIndex animated:NO];
50   }
51   sheet_.reset();
52   [super dealloc];
55 - (BOOL)isVisible {
56   return visible_;
59 // 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
61 // that on iPad this method is called with the index of an nonexistent cancel
62 // button when the user taps outside the sheet.
63 - (void)actionSheet:(UIActionSheet*)actionSheet
64     didDismissWithButtonIndex:(NSInteger)buttonIndex {
65   // On iOS 8, if the user taps an item in the context menu, then taps outside
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.
71   if (buttonIndexU < [menuHolder_ itemCount])
72     [menuHolder_ performActionAtIndex:buttonIndexU];
73   [self cleanup];
76 // 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
78 // button when the user taps outside the sheet.
79 - (void)actionSheet:(UIActionSheet*)actionSheet
80     clickedButtonAtIndex:(NSInteger)buttonIndex {
81   // 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.
83   if ([menuHolder_ shouldDismissImmediatelyOnClickedAtIndex:buttonIndex]) {
84     [sheet_ dismissWithClickedButtonIndex:buttonIndex animated:NO];
85   }
88 #pragma mark -
89 #pragma mark WebContextMenuDelegate methods
91 // Displays a menu using a sheet with the given title.
92 - (void)showWithHolder:(CRUContextMenuHolder*)menuHolder
93                atPoint:(CGPoint)localPoint
94                 inView:(UIView*)view {
95   DCHECK([menuHolder itemCount]);
96   menuHolder_.reset([menuHolder retain]);
97   // Check that the view is still visible on screen, otherwise just return and
98   // don't show the context menu.
99   DCHECK([view window] || [view isKindOfClass:[UIWindow class]]);
100   if (![view window] && ![view isKindOfClass:[UIWindow class]])
101     return;
102   CGSize spaceAvailableForTitle =
103       [CRUContextMenuController
104           availableSpaceForTitleInActionSheetWithMenu:menuHolder_
105                                               atPoint:localPoint
106                                                inView:view];
107   NSString* title = menuHolder.menuTitle;
108   if (title) {
109     // Show at least one line of text, even if that means the UIActionSheet's
110     // items will need to scroll.
111     const CGFloat kMinimumVerticalSpace = 21;
112     spaceAvailableForTitle.height =
113         std::max(kMinimumVerticalSpace, spaceAvailableForTitle.height);
114     title = [title cr_stringByElidingToFitSize:spaceAvailableForTitle];
115   }
116   // Create the sheet.
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.
128   [sheet_ addButtonWithTitle:l10n_util::GetNSString(IDS_APP_CANCEL)];
129   [sheet_ setCancelButtonIndex:[menuHolder_ itemCount]];
130   [sheet_ showFromRect:CGRectMake(localPoint.x, localPoint.y, 1.0, 1.0)
131                 inView:view
132               animated:YES];
134   visible_ = YES;
137 // Returns an approximation of the free space available for the title of an
138 // actionSheet filled with |menu| shown in |view| at |point|.
139 + (CGSize)
140     availableSpaceForTitleInActionSheetWithMenu:(CRUContextMenuHolder*)menu
141                                         atPoint:(CGPoint)point
142                                          inView:(UIView*)view {
143   BOOL isIpad = ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET;
144   if (base::ios::IsRunningOnIOS8OrLater()) {
145     // On iOS8 presenting and dismissing a dummy UIActionSheet does not work
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   }
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   }
206 @end
208 @implementation CRUContextMenuController (UsedForTesting)
209 - (UIActionSheet*)sheet {
210   return sheet_.get();
212 @end