OLD | NEW |
| (Empty) |
1 // Copyright 2013 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. | |
4 | |
5 #import "ios/chrome/browser/ui/ntp/google_landing_controller.h" | |
6 | |
7 #include <algorithm> | |
8 | |
9 #include "base/mac/foundation_util.h" | |
10 #include "base/metrics/user_metrics.h" | |
11 #include "base/strings/sys_string_conversions.h" | |
12 #include "components/strings/grit/components_strings.h" | |
13 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" | |
14 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h" | |
15 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" | |
16 #import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h" | |
17 #import "ios/chrome/browser/ui/ntp/google_landing_data_source.h" | |
18 #import "ios/chrome/browser/ui/ntp/most_visited_cell.h" | |
19 #import "ios/chrome/browser/ui/ntp/most_visited_layout.h" | |
20 #import "ios/chrome/browser/ui/ntp/new_tab_page_header_constants.h" | |
21 #import "ios/chrome/browser/ui/ntp/new_tab_page_header_view.h" | |
22 #import "ios/chrome/browser/ui/ntp/whats_new_header_view.h" | |
23 #import "ios/chrome/browser/ui/overscroll_actions/overscroll_actions_controller.
h" | |
24 #import "ios/chrome/browser/ui/toolbar/web_toolbar_controller.h" | |
25 #include "ios/chrome/browser/ui/ui_util.h" | |
26 #import "ios/chrome/browser/ui/uikit_ui_util.h" | |
27 #import "ios/chrome/browser/ui/url_loader.h" | |
28 #include "ios/chrome/common/string_util.h" | |
29 #include "ios/chrome/grit/ios_strings.h" | |
30 #import "ios/third_party/material_components_ios/src/components/Snackbar/src/Mat
erialSnackbar.h" | |
31 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" | |
32 #import "ios/web/public/web_state/context_menu_params.h" | |
33 #import "net/base/mac/url_conversions.h" | |
34 #include "ui/base/l10n/l10n_util.h" | |
35 #include "ui/base/page_transition_types.h" | |
36 | |
37 using base::UserMetricsAction; | |
38 | |
39 namespace { | |
40 | |
41 enum { | |
42 SectionWithOmnibox, | |
43 SectionWithMostVisited, | |
44 NumberOfCollectionViewSections, | |
45 }; | |
46 | |
47 enum InterfaceOrientation { | |
48 ALL, | |
49 IPHONE_LANDSCAPE, | |
50 }; | |
51 | |
52 const CGFloat kVoiceSearchButtonWidth = 48; | |
53 const UIEdgeInsets kSearchBoxStretchInsets = {3, 3, 3, 3}; | |
54 | |
55 // Height for the doodle frame when Google is not the default search engine. | |
56 const CGFloat kNonGoogleSearchDoodleHeight = 60; | |
57 // Height for the header view on tablet when Google is not the default search | |
58 // engine. | |
59 const CGFloat kNonGoogleSearchHeaderHeightIPad = 10; | |
60 | |
61 const CGFloat kHintLabelSidePadding = 12; | |
62 const CGFloat kNTPSearchFieldBottomPadding = 16; | |
63 const CGFloat kWhatsNewHeaderHiddenHeight = 8; | |
64 const CGFloat kDoodleTopMarginIPadPortrait = 82; | |
65 const CGFloat kDoodleTopMarginIPadLandscape = 82; | |
66 const NSInteger kMaxNumMostVisitedFaviconRows = 2; | |
67 const CGFloat kMaxSearchFieldFrameMargin = 200; | |
68 const CGFloat kShiftTilesDownAnimationDuration = 0.2; | |
69 | |
70 const CGFloat kMostVisitedPaddingIPhone = 16; | |
71 const CGFloat kMostVisitedPaddingIPadFavicon = 24; | |
72 | |
73 } // namespace | |
74 | |
75 @interface GoogleLandingController (UsedByGoogleLandingView) | |
76 // Update frames for subviews depending on the interface orientation. | |
77 - (void)updateSubviewFrames; | |
78 // Resets the collection view's inset to 0. | |
79 - (void)resetSectionInset; | |
80 - (void)reloadData; | |
81 @end | |
82 | |
83 // Subclassing the main UIScrollView allows calls for setFrame. | |
84 @interface GoogleLandingView : UIView { | |
85 GoogleLandingController* _googleLanding; | |
86 } | |
87 | |
88 - (void)setFrameDelegate:(GoogleLandingController*)delegate; | |
89 | |
90 @end | |
91 | |
92 @implementation GoogleLandingView | |
93 | |
94 - (void)setFrameDelegate:(GoogleLandingController*)delegate { | |
95 _googleLanding = delegate; | |
96 } | |
97 | |
98 - (void)setFrame:(CGRect)frame { | |
99 // On iPad and in fullscreen, the collection view's inset is very large. | |
100 // When Chrome enters slide over mode, the previously set inset is larger than | |
101 // the newly set collection view's width, which makes the collection view | |
102 // throw an exception. | |
103 // To prevent this from happening, we reset the inset to 0 before changing the | |
104 // frame. | |
105 [_googleLanding resetSectionInset]; | |
106 [super setFrame:frame]; | |
107 [_googleLanding updateSubviewFrames]; | |
108 [_googleLanding reloadData]; | |
109 } | |
110 | |
111 @end | |
112 | |
113 @interface GoogleLandingController ()<OverscrollActionsControllerDelegate, | |
114 UICollectionViewDataSource, | |
115 UICollectionViewDelegate, | |
116 UICollectionViewDelegateFlowLayout, | |
117 UIGestureRecognizerDelegate, | |
118 WhatsNewHeaderViewDelegate> { | |
119 // Fake omnibox. | |
120 base::scoped_nsobject<UIButton> _searchTapTarget; | |
121 | |
122 // A collection view for the most visited sites. | |
123 base::scoped_nsobject<UICollectionView> _mostVisitedView; | |
124 | |
125 // The overscroll actions controller managing accelerators over the toolbar. | |
126 base::scoped_nsobject<OverscrollActionsController> | |
127 _overscrollActionsController; | |
128 | |
129 // |YES| when notifications indicate the omnibox is focused. | |
130 BOOL _omniboxFocused; | |
131 | |
132 // Tap and swipe gesture recognizers when the omnibox is focused. | |
133 base::scoped_nsobject<UITapGestureRecognizer> _tapGestureRecognizer; | |
134 base::scoped_nsobject<UISwipeGestureRecognizer> _swipeGestureRecognizer; | |
135 | |
136 // Handles displaying the context menu for all form factors. | |
137 base::scoped_nsobject<ContextMenuCoordinator> _contextMenuCoordinator; | |
138 | |
139 // URL of the last deleted most viewed entry. If present the UI to restore it | |
140 // is shown. | |
141 base::scoped_nsobject<NSURL> _deletedUrl; | |
142 | |
143 // |YES| if the view has finished its first layout. This is useful when | |
144 // determining if the view has sized itself for tablet. | |
145 BOOL _viewLoaded; | |
146 | |
147 // |YES| if the fakebox header should be animated on scroll. | |
148 BOOL _animateHeader; | |
149 | |
150 // |YES| if the collection scrollView is scrolled all the way to the top. Used | |
151 // to lock this position in place on various frame changes. | |
152 BOOL _scrolledToTop; | |
153 | |
154 // |YES| if this NTP panel is visible. When set to |NO| various UI updates | |
155 // are ignored. | |
156 BOOL _isShowing; | |
157 | |
158 CFTimeInterval _shiftTilesDownStartTime; | |
159 CGSize _mostVisitedCellSize; | |
160 base::scoped_nsobject<NSLayoutConstraint> _hintLabelLeadingConstraint; | |
161 base::scoped_nsobject<NSLayoutConstraint> _voiceTapTrailingConstraint; | |
162 base::scoped_nsobject<NSMutableArray> _supplementaryViews; | |
163 base::scoped_nsobject<NewTabPageHeaderView> _headerView; | |
164 base::scoped_nsobject<WhatsNewHeaderView> _promoHeaderView; | |
165 } | |
166 | |
167 // Redeclare the |view| property to be the GoogleLandingView subclass instead of | |
168 // a generic UIView. | |
169 @property(nonatomic, readwrite, strong) GoogleLandingView* view; | |
170 | |
171 // Whether the Google logo or doodle is being shown. | |
172 @property(nonatomic, assign) BOOL logoIsShowing; | |
173 | |
174 // Exposes view and methods to drive the doodle. | |
175 @property(nonatomic, assign) id<LogoVendor> logoVendor; | |
176 | |
177 // |YES| if this consumer is incognito. | |
178 @property(nonatomic, assign) BOOL isOffTheRecord; | |
179 | |
180 // |YES| if this consumer is has voice search enabled. | |
181 @property(nonatomic, assign) BOOL voiceSearchIsEnabled; | |
182 | |
183 // Gets the maximum number of sites shown. | |
184 @property(nonatomic, assign) NSUInteger maximumMostVisitedSitesShown; | |
185 | |
186 // Gets the text of a what's new promo. | |
187 @property(nonatomic, retain) NSString* promoText; | |
188 | |
189 // Gets the icon of a what's new promo. | |
190 // TODO(crbug.com/694750): This should not be WhatsNewIcon. | |
191 @property(nonatomic, assign) WhatsNewIcon promoIcon; | |
192 | |
193 // |YES| if a what's new promo can be displayed. | |
194 @property(nonatomic, assign) BOOL promoCanShow; | |
195 | |
196 // The number of tabs to show in the google landing fake toolbar. | |
197 @property(nonatomic, assign) int tabCount; | |
198 | |
199 // |YES| if the google landing toolbar can show the forward arrow, cached and | |
200 // pushed into the header view. | |
201 @property(nonatomic, assign) BOOL canGoForward; | |
202 | |
203 // |YES| if the google landing toolbar can show the back arrow, cached and | |
204 // pushed into the header view. | |
205 @property(nonatomic, assign) BOOL canGoBack; | |
206 | |
207 // iPhone landscape uses a slightly different layout for the doodle and search | |
208 // field frame. Returns the proper frame from |frames| based on orientation, | |
209 // centered in the view. | |
210 - (CGRect)getOrientationFrame:(const CGRect[])frames; | |
211 // Returns the proper frame for the doodle. | |
212 - (CGRect)doodleFrame; | |
213 // Returns the proper frame for the search field. | |
214 - (CGRect)searchFieldFrame; | |
215 // Returns the height to use for the What's New promo view. | |
216 - (CGFloat)promoHeaderHeight; | |
217 // Add fake search field and voice search microphone. | |
218 - (void)addSearchField; | |
219 // Add most visited collection view. | |
220 - (void)addMostVisited; | |
221 // Update the iPhone fakebox's frame based on the current scroll view offset. | |
222 - (void)updateSearchField; | |
223 // Scrolls most visited to the top of the view when the omnibox is focused. | |
224 - (void)locationBarBecomesFirstResponder; | |
225 // Scroll the view back to 0,0 when the omnibox loses focus. | |
226 - (void)locationBarResignsFirstResponder; | |
227 // When the search field is tapped. | |
228 - (void)searchFieldTapped:(id)sender; | |
229 // Tells WebToolbarController to resign focus to the omnibox. | |
230 - (void)blurOmnibox; | |
231 // Called when a user does a long press on a most visited item. | |
232 - (void)handleMostVisitedLongPress: | |
233 (UILongPressGestureRecognizer*)longPressGesture; | |
234 // When the user removes a most visited a bubble pops up to undo the action. | |
235 - (void)showMostVisitedUndoForURL:(NSURL*)url; | |
236 // If Google is not the default search engine, hide the logo, doodle and | |
237 // fakebox. | |
238 - (void)updateLogoAndFakeboxDisplay; | |
239 // Helper method to set UICollectionViewFlowLayout insets for most visited. | |
240 - (void)setFlowLayoutInset:(UICollectionViewFlowLayout*)layout; | |
241 // Instructs the UICollectionView and UIView to reload it's data and layout. | |
242 - (void)reloadData; | |
243 // Returns the size of |self.mostVisitedData|. | |
244 - (NSUInteger)numberOfItems; | |
245 // Returns the number of non empty tiles (as opposed to the placeholder tiles). | |
246 - (NSInteger)numberOfNonEmptyTilesShown; | |
247 // Returns the URL for the mosted visited item in |self.mostVisitedData|. | |
248 - (GURL)urlForIndex:(NSUInteger)index; | |
249 // Returns the expected height of the NewTabPageHeaderView. | |
250 - (CGFloat)heightForSectionWithOmnibox; | |
251 // Returns the nearest ancestor view that is kind of |aClass|. | |
252 - (UIView*)nearestAncestorOfView:(UIView*)view withClass:(Class)aClass; | |
253 // Updates the collection view's scroll view offset for the next frame of the | |
254 // shiftTilesDown animation. | |
255 - (void)shiftTilesDownAnimationDidFire:(CADisplayLink*)link; | |
256 // Returns the size to use for Most Visited cells in the NTP. | |
257 - (CGSize)mostVisitedCellSize; | |
258 // Returns the padding for use between Most Visited cells. | |
259 - (CGFloat)mostVisitedCellPadding; | |
260 | |
261 @end | |
262 | |
263 @implementation GoogleLandingController | |
264 | |
265 @dynamic view; | |
266 @synthesize logoVendor = _logoVendor; | |
267 @synthesize dataSource = _dataSource; | |
268 // Property declared in NewTabPagePanelProtocol. | |
269 @synthesize delegate = _delegate; | |
270 @synthesize dispatcher = _dispatcher; | |
271 @synthesize isOffTheRecord = _isOffTheRecord; | |
272 @synthesize logoIsShowing = _logoIsShowing; | |
273 @synthesize promoText = _promoText; | |
274 @synthesize promoIcon = _promoIcon; | |
275 @synthesize promoCanShow = _promoCanShow; | |
276 @synthesize maximumMostVisitedSitesShown = _maximumMostVisitedSitesShown; | |
277 @synthesize tabCount = _tabCount; | |
278 @synthesize canGoForward = _canGoForward; | |
279 @synthesize canGoBack = _canGoBack; | |
280 @synthesize voiceSearchIsEnabled = _voiceSearchIsEnabled; | |
281 | |
282 - (void)loadView { | |
283 self.view = [[[GoogleLandingView alloc] | |
284 initWithFrame:[UIScreen mainScreen].bounds] autorelease]; | |
285 } | |
286 | |
287 - (void)viewDidLoad { | |
288 [super viewDidLoad]; | |
289 [self.view setAutoresizingMask:UIViewAutoresizingFlexibleHeight | | |
290 UIViewAutoresizingFlexibleWidth]; | |
291 [self.view setFrameDelegate:self]; | |
292 | |
293 // Initialise |shiftTilesDownStartTime| to a sentinel value to indicate that | |
294 // the animation has not yet started. | |
295 _shiftTilesDownStartTime = -1; | |
296 _mostVisitedCellSize = [self mostVisitedCellSize]; | |
297 _isShowing = YES; | |
298 _scrolledToTop = NO; | |
299 _animateHeader = YES; | |
300 | |
301 _tapGestureRecognizer.reset([[UITapGestureRecognizer alloc] | |
302 initWithTarget:self | |
303 action:@selector(blurOmnibox)]); | |
304 [_tapGestureRecognizer setDelegate:self]; | |
305 _swipeGestureRecognizer.reset([[UISwipeGestureRecognizer alloc] | |
306 initWithTarget:self | |
307 action:@selector(blurOmnibox)]); | |
308 [_swipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionDown]; | |
309 | |
310 [self addSearchField]; | |
311 [self addMostVisited]; | |
312 [self addOverscrollActions]; | |
313 [self reload]; | |
314 } | |
315 | |
316 - (void)viewDidLayoutSubviews { | |
317 [self updateSubviewFrames]; | |
318 } | |
319 | |
320 - (void)viewWillTransitionToSize:(CGSize)size | |
321 withTransitionCoordinator: | |
322 (id<UIViewControllerTransitionCoordinator>)coordinator { | |
323 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; | |
324 | |
325 void (^alongsideBlock)(id<UIViewControllerTransitionCoordinatorContext>) = ^( | |
326 id<UIViewControllerTransitionCoordinatorContext> context) { | |
327 if (IsIPadIdiom() && _scrolledToTop) { | |
328 // Keep the most visited thumbnails scrolled to the top. | |
329 [_mostVisitedView setContentOffset:CGPointMake(0, [self pinnedOffsetY])]; | |
330 return; | |
331 }; | |
332 | |
333 // Invalidate the layout so that the collection view's header size is reset | |
334 // for the new orientation. | |
335 if (!_scrolledToTop) { | |
336 [[_mostVisitedView collectionViewLayout] invalidateLayout]; | |
337 } | |
338 | |
339 // Call -scrollViewDidScroll: so that the omnibox's frame is adjusted for | |
340 // the scroll view's offset. | |
341 [self scrollViewDidScroll:_mostVisitedView]; | |
342 | |
343 }; | |
344 [coordinator animateAlongsideTransition:alongsideBlock completion:nil]; | |
345 } | |
346 | |
347 - (void)dealloc { | |
348 [[NSNotificationCenter defaultCenter] removeObserver:self]; | |
349 [_mostVisitedView setDelegate:nil]; | |
350 [_mostVisitedView setDataSource:nil]; | |
351 [_overscrollActionsController invalidate]; | |
352 [super dealloc]; | |
353 } | |
354 | |
355 #pragma mark - Private | |
356 | |
357 - (CGSize)mostVisitedCellSize { | |
358 if (IsIPadIdiom()) { | |
359 // On iPads, split-screen and slide-over may require showing smaller cells. | |
360 CGSize maximumCellSize = [MostVisitedCell maximumSize]; | |
361 CGSize viewSize = self.view.bounds.size; | |
362 CGFloat smallestDimension = | |
363 viewSize.height > viewSize.width ? viewSize.width : viewSize.height; | |
364 CGFloat cellWidth = AlignValueToPixel( | |
365 (smallestDimension - 3 * [self mostVisitedCellPadding]) / 2); | |
366 if (cellWidth < maximumCellSize.width) { | |
367 return CGSizeMake(cellWidth, cellWidth); | |
368 } else { | |
369 return maximumCellSize; | |
370 } | |
371 } else { | |
372 return [MostVisitedCell maximumSize]; | |
373 } | |
374 } | |
375 | |
376 - (CGFloat)mostVisitedCellPadding { | |
377 return IsIPadIdiom() ? kMostVisitedPaddingIPadFavicon | |
378 : kMostVisitedPaddingIPhone; | |
379 } | |
380 | |
381 - (CGFloat)viewWidth { | |
382 return [self.view frame].size.width; | |
383 } | |
384 | |
385 - (int)numberOfColumns { | |
386 CGFloat width = [self viewWidth]; | |
387 CGFloat padding = [self mostVisitedCellPadding]; | |
388 // Try to fit 4 columns. | |
389 if (width >= 5 * padding + _mostVisitedCellSize.width * 4) | |
390 return 4; | |
391 // Try to fit 3 columns. | |
392 if (width >= 4 * padding + _mostVisitedCellSize.width * 3) | |
393 return 3; | |
394 // Try to fit 2 columns. | |
395 if (width >= 3 * padding + _mostVisitedCellSize.width * 2) | |
396 return 2; | |
397 // We never want to have a layout with only one column, however: At launch, | |
398 // the view's size is initialized to the width of 320, which can only fit | |
399 // one column on iPhone 6 and 6+. TODO(crbug.com/506183): Get rid of the | |
400 // unecessary resize, and add a NOTREACHED() here. | |
401 return 1; | |
402 } | |
403 | |
404 - (CGFloat)leftMargin { | |
405 int columns = [self numberOfColumns]; | |
406 CGFloat whitespace = [self viewWidth] - columns * _mostVisitedCellSize.width - | |
407 (columns - 1) * [self mostVisitedCellPadding]; | |
408 CGFloat margin = AlignValueToPixel(whitespace / 2); | |
409 DCHECK(margin >= [self mostVisitedCellPadding]); | |
410 return margin; | |
411 } | |
412 | |
413 - (CGRect)doodleFrame { | |
414 const CGRect kDoodleFrame[2] = { | |
415 {{0, 66}, {0, 120}}, {{0, 56}, {0, 120}}, | |
416 }; | |
417 CGRect doodleFrame = [self getOrientationFrame:kDoodleFrame]; | |
418 if (!IsIPadIdiom() && !self.logoIsShowing) | |
419 doodleFrame.size.height = kNonGoogleSearchDoodleHeight; | |
420 if (IsIPadIdiom()) { | |
421 doodleFrame.origin.y = IsPortrait() ? kDoodleTopMarginIPadPortrait | |
422 : kDoodleTopMarginIPadLandscape; | |
423 } | |
424 return doodleFrame; | |
425 } | |
426 | |
427 - (CGRect)searchFieldFrame { | |
428 CGFloat y = CGRectGetMaxY([self doodleFrame]); | |
429 CGFloat leftMargin = [self leftMargin]; | |
430 if (leftMargin > kMaxSearchFieldFrameMargin) | |
431 leftMargin = kMaxSearchFieldFrameMargin; | |
432 const CGRect kSearchFieldFrame[2] = { | |
433 {{leftMargin, y + 32}, {0, 50}}, {{leftMargin, y + 16}, {0, 50}}, | |
434 }; | |
435 CGRect searchFieldFrame = [self getOrientationFrame:kSearchFieldFrame]; | |
436 if (IsIPadIdiom()) { | |
437 CGFloat iPadTopMargin = IsPortrait() ? kDoodleTopMarginIPadPortrait | |
438 : kDoodleTopMarginIPadLandscape; | |
439 searchFieldFrame.origin.y += iPadTopMargin - 32; | |
440 } | |
441 return searchFieldFrame; | |
442 } | |
443 | |
444 - (CGRect)getOrientationFrame:(const CGRect[])frames { | |
445 UIInterfaceOrientation orient = | |
446 [[UIApplication sharedApplication] statusBarOrientation]; | |
447 InterfaceOrientation inter_orient = | |
448 (IsIPadIdiom() || UIInterfaceOrientationIsPortrait(orient)) | |
449 ? ALL | |
450 : IPHONE_LANDSCAPE; | |
451 | |
452 // Calculate width based on screen width and origin x. | |
453 CGRect frame = frames[inter_orient]; | |
454 frame.size.width = fmax(self.view.bounds.size.width - 2 * frame.origin.x, 50); | |
455 return frame; | |
456 } | |
457 | |
458 - (CGFloat)promoHeaderHeight { | |
459 CGFloat promoMaxWidth = [self viewWidth] - 2 * [self leftMargin]; | |
460 NSString* text = self.promoText; | |
461 return [WhatsNewHeaderView heightToFitText:text inWidth:promoMaxWidth]; | |
462 } | |
463 | |
464 - (void)updateLogoAndFakeboxDisplay { | |
465 if (self.logoVendor.showingLogo != self.logoIsShowing) { | |
466 self.logoVendor.showingLogo = self.logoIsShowing; | |
467 if (_viewLoaded) { | |
468 [self updateSubviewFrames]; | |
469 | |
470 // Adjust the height of |_headerView| to fit its content which may have | |
471 // been shifted due to the visibility of the doodle. | |
472 CGRect headerFrame = [_headerView frame]; | |
473 headerFrame.size.height = [self heightForSectionWithOmnibox]; | |
474 [_headerView setFrame:headerFrame]; | |
475 | |
476 // Adjust vertical positioning of |_promoHeaderView|. | |
477 CGFloat omniboxHeaderHeight = | |
478 [self collectionView:_mostVisitedView | |
479 layout:[_mostVisitedView | |
480 collectionViewLayout] | |
481 referenceSizeForHeaderInSection:0] | |
482 .height; | |
483 CGRect whatsNewFrame = [_promoHeaderView frame]; | |
484 whatsNewFrame.origin.y = omniboxHeaderHeight; | |
485 [_promoHeaderView setFrame:whatsNewFrame]; | |
486 } | |
487 if (IsIPadIdiom()) | |
488 [_searchTapTarget setHidden:!self.logoIsShowing]; | |
489 } | |
490 } | |
491 | |
492 // Initialize and add a search field tap target and a voice search button. | |
493 - (void)addSearchField { | |
494 CGRect searchFieldFrame = [self searchFieldFrame]; | |
495 _searchTapTarget.reset([[UIButton alloc] initWithFrame:searchFieldFrame]); | |
496 if (IsIPadIdiom()) { | |
497 UIImage* searchBoxImage = [[UIImage imageNamed:@"ntp_google_search_box"] | |
498 resizableImageWithCapInsets:kSearchBoxStretchInsets]; | |
499 [_searchTapTarget setBackgroundImage:searchBoxImage | |
500 forState:UIControlStateNormal]; | |
501 } | |
502 [_searchTapTarget setAdjustsImageWhenHighlighted:NO]; | |
503 [_searchTapTarget addTarget:self | |
504 action:@selector(searchFieldTapped:) | |
505 forControlEvents:UIControlEventTouchUpInside]; | |
506 [_searchTapTarget | |
507 setAccessibilityLabel:l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT)]; | |
508 // Set isAccessibilityElement to NO so that Voice Search button is accessible. | |
509 [_searchTapTarget setIsAccessibilityElement:NO]; | |
510 | |
511 // Set up fakebox hint label. | |
512 CGRect hintFrame = CGRectInset([_searchTapTarget bounds], 12, 3); | |
513 const CGFloat kVoiceSearchOffset = 48; | |
514 hintFrame.size.width = searchFieldFrame.size.width - kVoiceSearchOffset; | |
515 base::scoped_nsobject<UILabel> searchHintLabel( | |
516 [[UILabel alloc] initWithFrame:hintFrame]); | |
517 [_searchTapTarget addSubview:searchHintLabel]; | |
518 [searchHintLabel setTranslatesAutoresizingMaskIntoConstraints:NO]; | |
519 [searchHintLabel | |
520 addConstraint:[NSLayoutConstraint | |
521 constraintWithItem:searchHintLabel | |
522 attribute:NSLayoutAttributeHeight | |
523 relatedBy:NSLayoutRelationEqual | |
524 toItem:nil | |
525 attribute:NSLayoutAttributeNotAnAttribute | |
526 multiplier:1 | |
527 constant:hintFrame.size.height]]; | |
528 [_searchTapTarget | |
529 addConstraint:[NSLayoutConstraint | |
530 constraintWithItem:searchHintLabel | |
531 attribute:NSLayoutAttributeCenterY | |
532 relatedBy:NSLayoutRelationEqual | |
533 toItem:_searchTapTarget | |
534 attribute:NSLayoutAttributeCenterY | |
535 multiplier:1 | |
536 constant:0]]; | |
537 _hintLabelLeadingConstraint.reset( | |
538 [[NSLayoutConstraint constraintWithItem:searchHintLabel | |
539 attribute:NSLayoutAttributeLeading | |
540 relatedBy:NSLayoutRelationEqual | |
541 toItem:_searchTapTarget | |
542 attribute:NSLayoutAttributeLeading | |
543 multiplier:1 | |
544 constant:kHintLabelSidePadding] retain]); | |
545 [_searchTapTarget addConstraint:_hintLabelLeadingConstraint]; | |
546 [searchHintLabel setText:l10n_util::GetNSString(IDS_OMNIBOX_EMPTY_HINT)]; | |
547 if (base::i18n::IsRTL()) { | |
548 [searchHintLabel setTextAlignment:NSTextAlignmentRight]; | |
549 } | |
550 [searchHintLabel | |
551 setTextColor:[UIColor | |
552 colorWithWhite:kiPhoneOmniboxPlaceholderColorBrightness | |
553 alpha:1.0]]; | |
554 [searchHintLabel setFont:[MDCTypography subheadFont]]; | |
555 | |
556 // Add a voice search button. | |
557 UIImage* micImage = [UIImage imageNamed:@"voice_icon"]; | |
558 base::scoped_nsobject<UIButton> voiceTapTarget( | |
559 [[UIButton alloc] initWithFrame:CGRectZero]); | |
560 [_searchTapTarget addSubview:voiceTapTarget]; | |
561 | |
562 [voiceTapTarget setTranslatesAutoresizingMaskIntoConstraints:NO]; | |
563 [_searchTapTarget | |
564 addConstraint:[NSLayoutConstraint | |
565 constraintWithItem:voiceTapTarget | |
566 attribute:NSLayoutAttributeCenterY | |
567 relatedBy:NSLayoutRelationEqual | |
568 toItem:_searchTapTarget | |
569 attribute:NSLayoutAttributeCenterY | |
570 multiplier:1 | |
571 constant:0]]; | |
572 _voiceTapTrailingConstraint.reset( | |
573 [[NSLayoutConstraint constraintWithItem:voiceTapTarget | |
574 attribute:NSLayoutAttributeTrailing | |
575 relatedBy:NSLayoutRelationEqual | |
576 toItem:_searchTapTarget | |
577 attribute:NSLayoutAttributeTrailing | |
578 multiplier:1 | |
579 constant:0] retain]); | |
580 [_searchTapTarget addConstraint:_voiceTapTrailingConstraint]; | |
581 [voiceTapTarget | |
582 addConstraint:[NSLayoutConstraint | |
583 constraintWithItem:voiceTapTarget | |
584 attribute:NSLayoutAttributeHeight | |
585 relatedBy:NSLayoutRelationEqual | |
586 toItem:nil | |
587 attribute:NSLayoutAttributeNotAnAttribute | |
588 multiplier:0 | |
589 constant:kVoiceSearchButtonWidth]]; | |
590 [voiceTapTarget | |
591 addConstraint:[NSLayoutConstraint | |
592 constraintWithItem:voiceTapTarget | |
593 attribute:NSLayoutAttributeWidth | |
594 relatedBy:NSLayoutRelationEqual | |
595 toItem:nil | |
596 attribute:NSLayoutAttributeNotAnAttribute | |
597 multiplier:0 | |
598 constant:kVoiceSearchButtonWidth]]; | |
599 [_searchTapTarget | |
600 addConstraint:[NSLayoutConstraint | |
601 constraintWithItem:searchHintLabel | |
602 attribute:NSLayoutAttributeTrailing | |
603 relatedBy:NSLayoutRelationEqual | |
604 toItem:voiceTapTarget | |
605 attribute:NSLayoutAttributeLeading | |
606 multiplier:1 | |
607 constant:0]]; | |
608 [voiceTapTarget setAdjustsImageWhenHighlighted:NO]; | |
609 [voiceTapTarget setImage:micImage forState:UIControlStateNormal]; | |
610 [voiceTapTarget setTag:IDC_VOICE_SEARCH]; | |
611 [voiceTapTarget setAccessibilityLabel:l10n_util::GetNSString( | |
612 IDS_IOS_ACCNAME_VOICE_SEARCH)]; | |
613 [voiceTapTarget setAccessibilityIdentifier:@"Voice Search"]; | |
614 | |
615 if (self.voiceSearchIsEnabled) { | |
616 [voiceTapTarget addTarget:self | |
617 action:@selector(loadVoiceSearch:) | |
618 forControlEvents:UIControlEventTouchUpInside]; | |
619 [voiceTapTarget addTarget:self | |
620 action:@selector(preloadVoiceSearch:) | |
621 forControlEvents:UIControlEventTouchDown]; | |
622 } else { | |
623 [voiceTapTarget setEnabled:NO]; | |
624 } | |
625 } | |
626 | |
627 - (void)loadVoiceSearch:(id)sender { | |
628 DCHECK(self.voiceSearchIsEnabled); | |
629 base::RecordAction(UserMetricsAction("MobileNTPMostVisitedVoiceSearch")); | |
630 [sender chromeExecuteCommand:sender]; | |
631 } | |
632 | |
633 - (void)preloadVoiceSearch:(id)sender { | |
634 DCHECK(self.voiceSearchIsEnabled); | |
635 [sender removeTarget:self | |
636 action:@selector(preloadVoiceSearch:) | |
637 forControlEvents:UIControlEventTouchDown]; | |
638 | |
639 // Use a GenericChromeCommand because |sender| already has a tag set for a | |
640 // different command. | |
641 base::scoped_nsobject<GenericChromeCommand> command( | |
642 [[GenericChromeCommand alloc] initWithTag:IDC_PRELOAD_VOICE_SEARCH]); | |
643 [sender chromeExecuteCommand:command]; | |
644 } | |
645 | |
646 - (void)setFlowLayoutInset:(UICollectionViewFlowLayout*)layout { | |
647 CGFloat leftMargin = [self leftMargin]; | |
648 [layout setSectionInset:UIEdgeInsetsMake(0, leftMargin, 0, leftMargin)]; | |
649 } | |
650 | |
651 - (void)resetSectionInset { | |
652 UICollectionViewFlowLayout* flowLayout = | |
653 (UICollectionViewFlowLayout*)[_mostVisitedView collectionViewLayout]; | |
654 [flowLayout setSectionInset:UIEdgeInsetsZero]; | |
655 } | |
656 | |
657 - (void)updateSubviewFrames { | |
658 _mostVisitedCellSize = [self mostVisitedCellSize]; | |
659 UICollectionViewFlowLayout* flowLayout = | |
660 base::mac::ObjCCastStrict<UICollectionViewFlowLayout>( | |
661 [_mostVisitedView collectionViewLayout]); | |
662 [flowLayout setItemSize:_mostVisitedCellSize]; | |
663 self.logoVendor.view.frame = [self doodleFrame]; | |
664 | |
665 [self setFlowLayoutInset:flowLayout]; | |
666 [flowLayout invalidateLayout]; | |
667 [_promoHeaderView setSideMargin:[self leftMargin]]; | |
668 | |
669 // On the iPhone 6 Plus, if the app is started in landscape after a fresh | |
670 // install, the UICollectionViewLayout incorrectly sizes the widths of the | |
671 // supplementary views to the portrait width. Correct that here to ensure | |
672 // that the header is property laid out to the UICollectionView's width. | |
673 // crbug.com/491131 | |
674 CGFloat collectionViewWidth = CGRectGetWidth([_mostVisitedView bounds]); | |
675 CGFloat collectionViewMinX = CGRectGetMinX([_mostVisitedView bounds]); | |
676 for (UIView* supplementaryView in _supplementaryViews.get()) { | |
677 CGRect supplementaryViewFrame = supplementaryView.frame; | |
678 supplementaryViewFrame.origin.x = collectionViewMinX; | |
679 supplementaryViewFrame.size.width = collectionViewWidth; | |
680 supplementaryView.frame = supplementaryViewFrame; | |
681 } | |
682 | |
683 BOOL isScrollableNTP = !IsIPadIdiom() || IsCompactTablet(); | |
684 if (isScrollableNTP && _scrolledToTop) { | |
685 // Set the scroll view's offset to the pinned offset to keep the omnibox | |
686 // at the top of the screen if it isn't already. | |
687 CGFloat pinnedOffsetY = [self pinnedOffsetY]; | |
688 if ([_mostVisitedView contentOffset].y < pinnedOffsetY) { | |
689 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)]; | |
690 } else { | |
691 [self updateSearchField]; | |
692 } | |
693 } else { | |
694 [_searchTapTarget setFrame:[self searchFieldFrame]]; | |
695 } | |
696 | |
697 if (!_viewLoaded) { | |
698 _viewLoaded = YES; | |
699 [self.logoVendor fetchDoodle]; | |
700 } | |
701 [self.delegate updateNtpBarShadowForPanelController:self]; | |
702 } | |
703 | |
704 // Initialize and add a panel with most visited sites. | |
705 - (void)addMostVisited { | |
706 CGRect mostVisitedFrame = [self.view bounds]; | |
707 base::scoped_nsobject<UICollectionViewFlowLayout> flowLayout; | |
708 if (IsIPadIdiom()) | |
709 flowLayout.reset([[UICollectionViewFlowLayout alloc] init]); | |
710 else | |
711 flowLayout.reset([[MostVisitedLayout alloc] init]); | |
712 | |
713 [flowLayout setScrollDirection:UICollectionViewScrollDirectionVertical]; | |
714 [flowLayout setItemSize:_mostVisitedCellSize]; | |
715 [flowLayout setMinimumInteritemSpacing:8]; | |
716 [flowLayout setMinimumLineSpacing:[self mostVisitedCellPadding]]; | |
717 DCHECK(!_mostVisitedView); | |
718 _mostVisitedView.reset([[UICollectionView alloc] | |
719 initWithFrame:mostVisitedFrame | |
720 collectionViewLayout:flowLayout]); | |
721 [_mostVisitedView setAutoresizingMask:UIViewAutoresizingFlexibleHeight | | |
722 UIViewAutoresizingFlexibleWidth]; | |
723 [_mostVisitedView setDelegate:self]; | |
724 [_mostVisitedView setDataSource:self]; | |
725 [_mostVisitedView registerClass:[MostVisitedCell class] | |
726 forCellWithReuseIdentifier:@"classCell"]; | |
727 [_mostVisitedView setBackgroundColor:[UIColor clearColor]]; | |
728 [_mostVisitedView setBounces:YES]; | |
729 [_mostVisitedView setShowsHorizontalScrollIndicator:NO]; | |
730 [_mostVisitedView setShowsVerticalScrollIndicator:NO]; | |
731 [_mostVisitedView registerClass:[WhatsNewHeaderView class] | |
732 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader | |
733 withReuseIdentifier:@"whatsNew"]; | |
734 [_mostVisitedView registerClass:[NewTabPageHeaderView class] | |
735 forSupplementaryViewOfKind:UICollectionElementKindSectionHeader | |
736 withReuseIdentifier:@"header"]; | |
737 [_mostVisitedView setAccessibilityIdentifier:@"Google Landing"]; | |
738 | |
739 [self.view addSubview:_mostVisitedView]; | |
740 } | |
741 | |
742 - (void)updateSearchField { | |
743 NSArray* constraints = | |
744 @[ _hintLabelLeadingConstraint, _voiceTapTrailingConstraint ]; | |
745 [_headerView updateSearchField:_searchTapTarget | |
746 withInitialFrame:[self searchFieldFrame] | |
747 subviewConstraints:constraints | |
748 forOffset:[_mostVisitedView contentOffset].y]; | |
749 } | |
750 | |
751 - (void)addOverscrollActions { | |
752 if (!IsIPadIdiom()) { | |
753 _overscrollActionsController.reset([[OverscrollActionsController alloc] | |
754 initWithScrollView:_mostVisitedView]); | |
755 [_overscrollActionsController setStyle:OverscrollStyle::NTP_NON_INCOGNITO]; | |
756 [_overscrollActionsController setDelegate:self]; | |
757 } | |
758 } | |
759 | |
760 // Check to see if the promo label should be hidden. | |
761 - (void)hideWhatsNewIfNecessary { | |
762 if (![_promoHeaderView isHidden] && !self.promoCanShow) { | |
763 [_promoHeaderView setHidden:YES]; | |
764 [self.view setNeedsLayout]; | |
765 } | |
766 } | |
767 | |
768 - (void)locationBarBecomesFirstResponder { | |
769 if (!_isShowing) | |
770 return; | |
771 | |
772 _omniboxFocused = YES; | |
773 [self shiftTilesUp]; | |
774 } | |
775 | |
776 - (void)shiftTilesUp { | |
777 _scrolledToTop = YES; | |
778 // Add gesture recognizer to background |self.view| when omnibox is focused. | |
779 [self.view addGestureRecognizer:_tapGestureRecognizer]; | |
780 [self.view addGestureRecognizer:_swipeGestureRecognizer]; | |
781 | |
782 CGFloat pinnedOffsetY = [self pinnedOffsetY]; | |
783 _animateHeader = !IsIPadIdiom(); | |
784 | |
785 [UIView animateWithDuration:0.25 | |
786 animations:^{ | |
787 if ([_mostVisitedView contentOffset].y < pinnedOffsetY) { | |
788 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY)]; | |
789 [[_mostVisitedView collectionViewLayout] invalidateLayout]; | |
790 } | |
791 } | |
792 completion:^(BOOL finished) { | |
793 // Check to see if we are still scrolled to the top -- it's possible | |
794 // (and difficult) to resign the first responder and initiate a | |
795 // -shiftTilesDown before the animation here completes. | |
796 if (_scrolledToTop) { | |
797 _animateHeader = NO; | |
798 if (!IsIPadIdiom()) { | |
799 [self.dispatcher onFakeboxAnimationComplete]; | |
800 [_headerView fadeOutShadow]; | |
801 [_searchTapTarget setHidden:YES]; | |
802 } | |
803 } | |
804 }]; | |
805 } | |
806 | |
807 - (void)searchFieldTapped:(id)sender { | |
808 [self.dispatcher focusFakebox]; | |
809 } | |
810 | |
811 - (void)blurOmnibox { | |
812 if (_omniboxFocused) { | |
813 [self.dispatcher cancelOmniboxEdit]; | |
814 } else { | |
815 [self locationBarResignsFirstResponder]; | |
816 } | |
817 } | |
818 | |
819 - (void)locationBarResignsFirstResponder { | |
820 if (!_isShowing && !_scrolledToTop) | |
821 return; | |
822 | |
823 _omniboxFocused = NO; | |
824 if ([_contextMenuCoordinator isVisible]) { | |
825 return; | |
826 } | |
827 | |
828 [self shiftTilesDown]; | |
829 } | |
830 | |
831 - (void)shiftTilesDown { | |
832 _animateHeader = YES; | |
833 _scrolledToTop = NO; | |
834 if (!IsIPadIdiom()) { | |
835 [_searchTapTarget setHidden:NO]; | |
836 [self.dispatcher onFakeboxBlur]; | |
837 } | |
838 | |
839 // Reload most visited sites in case the number of placeholder cells needs to | |
840 // be updated after an orientation change. | |
841 [_mostVisitedView reloadData]; | |
842 | |
843 // Reshow views that are within range of the most visited collection view | |
844 // (if necessary). | |
845 [self.view removeGestureRecognizer:_tapGestureRecognizer]; | |
846 [self.view removeGestureRecognizer:_swipeGestureRecognizer]; | |
847 | |
848 // CADisplayLink is used for this animation instead of the standard UIView | |
849 // animation because the standard animation did not properly convert the | |
850 // fakebox from its scrolled up mode to its scrolled down mode. Specifically, | |
851 // calling |UICollectionView reloadData| adjacent to the standard animation | |
852 // caused the fakebox's views to jump incorrectly. CADisplayLink avoids this | |
853 // problem because it allows |shiftTilesDownAnimationDidFire| to directly | |
854 // control each frame. | |
855 CADisplayLink* link = [CADisplayLink | |
856 displayLinkWithTarget:self | |
857 selector:@selector(shiftTilesDownAnimationDidFire:)]; | |
858 [link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; | |
859 | |
860 // Dismisses modal UI elements if displayed. Must be called at the end of | |
861 // -locationBarResignsFirstResponder since it could result in -dealloc being | |
862 // called. | |
863 [self dismissModals]; | |
864 } | |
865 | |
866 - (void)shiftTilesDownAnimationDidFire:(CADisplayLink*)link { | |
867 // If this is the first frame of the animation, store the starting timestamp | |
868 // and do nothing. | |
869 if (_shiftTilesDownStartTime == -1) { | |
870 _shiftTilesDownStartTime = link.timestamp; | |
871 return; | |
872 } | |
873 | |
874 CFTimeInterval timeElapsed = link.timestamp - _shiftTilesDownStartTime; | |
875 double percentComplete = timeElapsed / kShiftTilesDownAnimationDuration; | |
876 // Ensure that the percentage cannot be above 1.0. | |
877 if (percentComplete > 1.0) | |
878 percentComplete = 1.0; | |
879 | |
880 // Find how much the collection view should be scrolled up in the next frame. | |
881 CGFloat yOffset = (1.0 - percentComplete) * [self pinnedOffsetY]; | |
882 [_mostVisitedView setContentOffset:CGPointMake(0, yOffset)]; | |
883 | |
884 if (percentComplete == 1.0) { | |
885 [link invalidate]; | |
886 // Reset |shiftTilesDownStartTime to its sentinal value. | |
887 _shiftTilesDownStartTime = -1; | |
888 [[_mostVisitedView collectionViewLayout] invalidateLayout]; | |
889 } | |
890 } | |
891 | |
892 - (void)reloadData { | |
893 // -reloadData updates from |self.mostVisitedData|. | |
894 // -invalidateLayout is necessary because sometimes the flowLayout has the | |
895 // wrong cached size and will throw an internal exception if the | |
896 // -numberOfItems shrinks. -setNeedsLayout is needed in case | |
897 // -numberOfItems increases enough to add a new row and change the height | |
898 // of _mostVisitedView. | |
899 [_mostVisitedView reloadData]; | |
900 [[_mostVisitedView collectionViewLayout] invalidateLayout]; | |
901 [self.view setNeedsLayout]; | |
902 } | |
903 | |
904 - (CGFloat)heightForSectionWithOmnibox { | |
905 CGFloat headerHeight = | |
906 CGRectGetMaxY([self searchFieldFrame]) + kNTPSearchFieldBottomPadding; | |
907 if (IsIPadIdiom()) { | |
908 if (self.logoIsShowing) { | |
909 if (!self.promoCanShow) { | |
910 UIInterfaceOrientation orient = | |
911 [[UIApplication sharedApplication] statusBarOrientation]; | |
912 const CGFloat kTopSpacingMaterialPortrait = 56; | |
913 const CGFloat kTopSpacingMaterialLandscape = 32; | |
914 headerHeight += UIInterfaceOrientationIsPortrait(orient) | |
915 ? kTopSpacingMaterialPortrait | |
916 : kTopSpacingMaterialLandscape; | |
917 } | |
918 } else { | |
919 headerHeight = kNonGoogleSearchHeaderHeightIPad; | |
920 } | |
921 } | |
922 return headerHeight; | |
923 } | |
924 | |
925 #pragma mark - ToolbarOwner | |
926 | |
927 - (ToolbarController*)relinquishedToolbarController { | |
928 return [_headerView relinquishedToolbarController]; | |
929 } | |
930 | |
931 - (void)reparentToolbarController { | |
932 [_headerView reparentToolbarController]; | |
933 } | |
934 | |
935 #pragma mark - UICollectionView Methods. | |
936 | |
937 - (CGSize)collectionView:(UICollectionView*)collectionView | |
938 layout: | |
939 (UICollectionViewLayout*)collectionViewLayout | |
940 referenceSizeForHeaderInSection:(NSInteger)section { | |
941 CGFloat headerHeight = 0; | |
942 if (section == SectionWithOmnibox) { | |
943 headerHeight = [self heightForSectionWithOmnibox]; | |
944 ((UICollectionViewFlowLayout*)collectionViewLayout).headerReferenceSize = | |
945 CGSizeMake(0, headerHeight); | |
946 } else if (section == SectionWithMostVisited) { | |
947 if (self.promoCanShow) { | |
948 headerHeight = [self promoHeaderHeight]; | |
949 } else { | |
950 headerHeight = kWhatsNewHeaderHiddenHeight; | |
951 } | |
952 } | |
953 return CGSizeMake(0, headerHeight); | |
954 } | |
955 | |
956 #pragma mark - UICollectionViewDelegate | |
957 | |
958 - (BOOL)collectionView:(UICollectionView*)collectionView | |
959 shouldSelectItemAtIndexPath:(NSIndexPath*)indexPath { | |
960 return indexPath.row < static_cast<NSInteger>([self numberOfItems]); | |
961 } | |
962 | |
963 - (void)collectionView:(UICollectionView*)collectionView | |
964 didSelectItemAtIndexPath:(NSIndexPath*)indexPath { | |
965 MostVisitedCell* cell = | |
966 (MostVisitedCell*)[collectionView cellForItemAtIndexPath:indexPath]; | |
967 | |
968 // Keep the UICollectionView alive for one second while the screen | |
969 // reader does its thing. | |
970 // TODO(jif): This needs a radar, since it is almost certainly a | |
971 // UIKit accessibility bug. crbug.com/529271 | |
972 if (UIAccessibilityIsVoiceOverRunning()) { | |
973 UICollectionView* blockView = [_mostVisitedView retain]; | |
974 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, | |
975 static_cast<int64_t>(1 * NSEC_PER_SEC)), | |
976 dispatch_get_main_queue(), ^{ | |
977 [blockView release]; | |
978 }); | |
979 } | |
980 | |
981 const NSUInteger visitedIndex = indexPath.row; | |
982 [self blurOmnibox]; | |
983 DCHECK(visitedIndex < [self numberOfItems]); | |
984 [self.dataSource logMostVisitedClick:visitedIndex tileType:cell.tileType]; | |
985 [self.dispatcher loadURL:[self urlForIndex:visitedIndex] | |
986 referrer:web::Referrer() | |
987 transition:ui::PAGE_TRANSITION_AUTO_BOOKMARK | |
988 rendererInitiated:NO]; | |
989 } | |
990 | |
991 #pragma mark - UICollectionViewDataSource | |
992 | |
993 - (UICollectionReusableView*)collectionView:(UICollectionView*)collectionView | |
994 viewForSupplementaryElementOfKind:(NSString*)kind | |
995 atIndexPath:(NSIndexPath*)indexPath { | |
996 DCHECK(kind == UICollectionElementKindSectionHeader); | |
997 | |
998 if (!_supplementaryViews) | |
999 _supplementaryViews.reset([[NSMutableArray alloc] init]); | |
1000 | |
1001 if (indexPath.section == SectionWithOmnibox) { | |
1002 if (!_headerView) { | |
1003 _headerView.reset([[collectionView | |
1004 dequeueReusableSupplementaryViewOfKind: | |
1005 UICollectionElementKindSectionHeader | |
1006 withReuseIdentifier:@"header" | |
1007 forIndexPath:indexPath] retain]); | |
1008 [_headerView addSubview:[self.logoVendor view]]; | |
1009 [_headerView addSubview:_searchTapTarget]; | |
1010 [_headerView addViewsToSearchField:_searchTapTarget]; | |
1011 | |
1012 if (!IsIPadIdiom()) { | |
1013 // iPhone header also contains a toolbar since the normal toolbar is | |
1014 // hidden. | |
1015 [_headerView addToolbarWithDataSource:self.dataSource | |
1016 dispatcher:self.dispatcher]; | |
1017 [_headerView setToolbarTabCount:self.tabCount]; | |
1018 [_headerView setCanGoForward:self.canGoForward]; | |
1019 [_headerView setCanGoBack:self.canGoBack]; | |
1020 } | |
1021 [_supplementaryViews addObject:_headerView]; | |
1022 } | |
1023 return _headerView; | |
1024 } | |
1025 | |
1026 if (indexPath.section == SectionWithMostVisited) { | |
1027 if (!_promoHeaderView) { | |
1028 _promoHeaderView.reset([[collectionView | |
1029 dequeueReusableSupplementaryViewOfKind: | |
1030 UICollectionElementKindSectionHeader | |
1031 withReuseIdentifier:@"whatsNew" | |
1032 forIndexPath:indexPath] retain]); | |
1033 [_promoHeaderView setSideMargin:[self leftMargin]]; | |
1034 [_promoHeaderView setDelegate:self]; | |
1035 if (self.promoCanShow) { | |
1036 [_promoHeaderView setText:self.promoText]; | |
1037 [_promoHeaderView setIcon:self.promoIcon]; | |
1038 [self.dataSource promoViewed]; | |
1039 } | |
1040 [_supplementaryViews addObject:_promoHeaderView]; | |
1041 } | |
1042 return _promoHeaderView; | |
1043 } | |
1044 | |
1045 NOTREACHED(); | |
1046 return nil; | |
1047 } | |
1048 | |
1049 - (NSInteger)numberOfSectionsInCollectionView: | |
1050 (UICollectionView*)collectionView { | |
1051 return NumberOfCollectionViewSections; | |
1052 } | |
1053 | |
1054 - (NSInteger)collectionView:(UICollectionView*)collectionView | |
1055 numberOfItemsInSection:(NSInteger)section { | |
1056 // The first section only contains a header view and no items. | |
1057 if (section == SectionWithOmnibox) | |
1058 return 0; | |
1059 | |
1060 // Phone always contains the maximum number of cells. Cells in excess of the | |
1061 // number of thumbnails are used solely for layout/sizing. | |
1062 if (!IsIPadIdiom()) | |
1063 return self.maximumMostVisitedSitesShown; | |
1064 | |
1065 return [self numberOfNonEmptyTilesShown]; | |
1066 } | |
1067 | |
1068 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView | |
1069 cellForItemAtIndexPath:(NSIndexPath*)indexPath { | |
1070 MostVisitedCell* cell = (MostVisitedCell*)[collectionView | |
1071 dequeueReusableCellWithReuseIdentifier:@"classCell" | |
1072 forIndexPath:indexPath]; | |
1073 BOOL isPlaceholder = indexPath.row >= (int)[self numberOfItems]; | |
1074 if (isPlaceholder) { | |
1075 [cell showPlaceholder]; | |
1076 for (UIGestureRecognizer* ges in cell.gestureRecognizers) { | |
1077 [cell removeGestureRecognizer:ges]; | |
1078 } | |
1079 | |
1080 // When -numberOfItems is 0, always remove the placeholder. | |
1081 if (indexPath.row >= [self numberOfColumns] || [self numberOfItems] == 0) { | |
1082 // This cell is completely empty and only exists for layout/sizing | |
1083 // purposes. | |
1084 [cell removePlaceholderImage]; | |
1085 } | |
1086 return cell; | |
1087 } | |
1088 | |
1089 const ntp_tiles::NTPTile& ntpTile = | |
1090 [self.dataSource mostVisitedAtIndex:indexPath.row]; | |
1091 NSString* title = base::SysUTF16ToNSString(ntpTile.title); | |
1092 | |
1093 [cell setupWithURL:ntpTile.url title:title dataSource:self.dataSource]; | |
1094 | |
1095 base::scoped_nsobject<UILongPressGestureRecognizer> longPress( | |
1096 [[UILongPressGestureRecognizer alloc] | |
1097 initWithTarget:self | |
1098 action:@selector(handleMostVisitedLongPress:)]); | |
1099 [cell addGestureRecognizer:longPress]; | |
1100 | |
1101 return cell; | |
1102 } | |
1103 | |
1104 #pragma mark - Context Menu | |
1105 | |
1106 // Called when a user does a long press on a most visited item. | |
1107 - (void)handleMostVisitedLongPress:(UILongPressGestureRecognizer*)sender { | |
1108 if (sender.state == UIGestureRecognizerStateBegan) { | |
1109 // Only one long press at a time. | |
1110 if ([_contextMenuCoordinator isVisible]) { | |
1111 return; | |
1112 } | |
1113 | |
1114 NSIndexPath* indexPath = [_mostVisitedView | |
1115 indexPathForCell:static_cast<UICollectionViewCell*>(sender.view)]; | |
1116 const NSUInteger index = indexPath.row; | |
1117 | |
1118 // A long press occured on one of the most visited button. Popup a context | |
1119 // menu. | |
1120 DCHECK(index < [self numberOfItems]); | |
1121 | |
1122 web::ContextMenuParams params; | |
1123 // Get view coordinates in local space. | |
1124 params.location = [sender locationInView:self.view]; | |
1125 params.view.reset([self.view retain]); | |
1126 | |
1127 // Present sheet/popover using controller that is added to view hierarchy. | |
1128 UIViewController* topController = [params.view window].rootViewController; | |
1129 while (topController.presentedViewController) | |
1130 topController = topController.presentedViewController; | |
1131 | |
1132 _contextMenuCoordinator.reset([[ContextMenuCoordinator alloc] | |
1133 initWithBaseViewController:topController | |
1134 params:params]); | |
1135 | |
1136 ProceduralBlock action; | |
1137 | |
1138 // Open In New Tab. | |
1139 GURL url = [self urlForIndex:index]; | |
1140 base::WeakNSObject<GoogleLandingController> weakSelf(self); | |
1141 action = ^{ | |
1142 base::scoped_nsobject<GoogleLandingController> strongSelf( | |
1143 [weakSelf retain]); | |
1144 if (!strongSelf) | |
1145 return; | |
1146 MostVisitedCell* cell = (MostVisitedCell*)sender.view; | |
1147 [[strongSelf dataSource] logMostVisitedClick:index | |
1148 tileType:cell.tileType]; | |
1149 [[strongSelf dispatcher] webPageOrderedOpen:url | |
1150 referrer:web::Referrer() | |
1151 inBackground:YES | |
1152 appendTo:kCurrentTab]; | |
1153 }; | |
1154 [_contextMenuCoordinator | |
1155 addItemWithTitle:l10n_util::GetNSStringWithFixup( | |
1156 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWTAB) | |
1157 action:action]; | |
1158 | |
1159 if (!self.isOffTheRecord) { | |
1160 // Open in Incognito Tab. | |
1161 action = ^{ | |
1162 base::scoped_nsobject<GoogleLandingController> strongSelf( | |
1163 [weakSelf retain]); | |
1164 if (!strongSelf) | |
1165 return; | |
1166 MostVisitedCell* cell = (MostVisitedCell*)sender.view; | |
1167 [[strongSelf dataSource] logMostVisitedClick:index | |
1168 tileType:cell.tileType]; | |
1169 [[strongSelf dispatcher] webPageOrderedOpen:url | |
1170 referrer:web::Referrer() | |
1171 inIncognito:YES | |
1172 inBackground:NO | |
1173 appendTo:kCurrentTab]; | |
1174 }; | |
1175 [_contextMenuCoordinator | |
1176 addItemWithTitle:l10n_util::GetNSStringWithFixup( | |
1177 IDS_IOS_CONTENT_CONTEXT_OPENLINKNEWINCOGNITOTAB) | |
1178 action:action]; | |
1179 } | |
1180 | |
1181 // Remove the most visited url. | |
1182 NSString* title = | |
1183 l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_BUBBLE_REMOVE_BOOKMARK); | |
1184 action = ^{ | |
1185 base::scoped_nsobject<GoogleLandingController> strongSelf( | |
1186 [weakSelf retain]); | |
1187 // Early return if the controller has been deallocated. | |
1188 if (!strongSelf) | |
1189 return; | |
1190 base::RecordAction(UserMetricsAction("MostVisited_UrlBlacklisted")); | |
1191 [[strongSelf dataSource] addBlacklistedURL:url]; | |
1192 [strongSelf showMostVisitedUndoForURL:net::NSURLWithGURL(url)]; | |
1193 }; | |
1194 [_contextMenuCoordinator addItemWithTitle:title action:action]; | |
1195 | |
1196 [_contextMenuCoordinator start]; | |
1197 | |
1198 if (IsIPadIdiom()) | |
1199 [self blurOmnibox]; | |
1200 } | |
1201 } | |
1202 | |
1203 - (void)showMostVisitedUndoForURL:(NSURL*)url { | |
1204 _deletedUrl.reset([url retain]); | |
1205 | |
1206 MDCSnackbarMessageAction* action = | |
1207 [[[MDCSnackbarMessageAction alloc] init] autorelease]; | |
1208 base::WeakNSObject<GoogleLandingController> weakSelf(self); | |
1209 action.handler = ^{ | |
1210 base::scoped_nsobject<GoogleLandingController> strongSelf( | |
1211 [weakSelf retain]); | |
1212 if (!strongSelf) | |
1213 return; | |
1214 [[strongSelf dataSource] | |
1215 removeBlacklistedURL:net::GURLWithNSURL(_deletedUrl)]; | |
1216 }; | |
1217 action.title = l10n_util::GetNSString(IDS_NEW_TAB_UNDO_THUMBNAIL_REMOVE); | |
1218 action.accessibilityIdentifier = @"Undo"; | |
1219 | |
1220 TriggerHapticFeedbackForNotification(UINotificationFeedbackTypeSuccess); | |
1221 MDCSnackbarMessage* message = [MDCSnackbarMessage | |
1222 messageWithText:l10n_util::GetNSString( | |
1223 IDS_IOS_NEW_TAB_MOST_VISITED_ITEM_REMOVED)]; | |
1224 message.action = action; | |
1225 message.category = @"MostVisitedUndo"; | |
1226 [MDCSnackbarManager showMessage:message]; | |
1227 } | |
1228 | |
1229 - (void)onPromoLabelTapped { | |
1230 [self.dispatcher cancelOmniboxEdit]; | |
1231 [_promoHeaderView setHidden:YES]; | |
1232 [self.view setNeedsLayout]; | |
1233 [self.dataSource promoTapped]; | |
1234 } | |
1235 | |
1236 // Returns the Y value to use for the scroll view's contentOffset when scrolling | |
1237 // the omnibox to the top of the screen. | |
1238 - (CGFloat)pinnedOffsetY { | |
1239 CGFloat headerHeight = [_headerView frame].size.height; | |
1240 CGFloat offsetY = | |
1241 headerHeight - ntp_header::kScrolledToTopOmniboxBottomMargin; | |
1242 if (!IsIPadIdiom()) | |
1243 offsetY -= ntp_header::kToolbarHeight; | |
1244 | |
1245 return offsetY; | |
1246 } | |
1247 | |
1248 #pragma mark - NewTabPagePanelProtocol | |
1249 | |
1250 - (void)reload { | |
1251 // Fetch the doodle after the view finishes laying out. Otherwise, tablet | |
1252 // may fetch the wrong sized doodle. | |
1253 if (_viewLoaded) | |
1254 [self.logoVendor fetchDoodle]; | |
1255 [self updateLogoAndFakeboxDisplay]; | |
1256 [self hideWhatsNewIfNecessary]; | |
1257 } | |
1258 | |
1259 - (void)wasShown { | |
1260 _isShowing = YES; | |
1261 [_headerView hideToolbarViewsForNewTabPage]; | |
1262 } | |
1263 | |
1264 - (void)wasHidden { | |
1265 _isShowing = NO; | |
1266 } | |
1267 | |
1268 - (void)dismissModals { | |
1269 [_contextMenuCoordinator stop]; | |
1270 } | |
1271 | |
1272 - (void)dismissKeyboard { | |
1273 } | |
1274 | |
1275 - (void)setScrollsToTop:(BOOL)enable { | |
1276 } | |
1277 | |
1278 - (CGFloat)alphaForBottomShadow { | |
1279 // Get the frame of the bottommost cell in |self.view|'s coordinate system. | |
1280 NSInteger section = SectionWithMostVisited; | |
1281 // Account for the fact that the tableview may not yet contain | |
1282 // |numberOfNonEmptyTilesShown| tiles because it hasn't been updated yet. | |
1283 NSUInteger lastItemIndex = | |
1284 std::min([_mostVisitedView numberOfItemsInSection:SectionWithMostVisited], | |
1285 [self numberOfNonEmptyTilesShown]) - | |
1286 1; | |
1287 DCHECK(lastItemIndex >= 0); | |
1288 NSIndexPath* lastCellIndexPath = | |
1289 [NSIndexPath indexPathForItem:lastItemIndex inSection:section]; | |
1290 UICollectionViewLayoutAttributes* attributes = | |
1291 [_mostVisitedView layoutAttributesForItemAtIndexPath:lastCellIndexPath]; | |
1292 CGRect lastCellFrame = attributes.frame; | |
1293 CGRect cellFrameInSuperview = | |
1294 [_mostVisitedView convertRect:lastCellFrame toView:self.view]; | |
1295 | |
1296 // Calculate when the bottom of the cell passes through the bottom of | |
1297 // |self.view|. | |
1298 CGFloat maxY = CGRectGetMaxY(cellFrameInSuperview); | |
1299 CGFloat viewHeight = CGRectGetHeight(self.view.frame); | |
1300 | |
1301 CGFloat pixelsBelowFrame = maxY - viewHeight; | |
1302 CGFloat alpha = pixelsBelowFrame / kNewTabPageDistanceToFadeShadow; | |
1303 alpha = MIN(MAX(alpha, 0), 1); | |
1304 return alpha; | |
1305 } | |
1306 | |
1307 - (void)willUpdateSnapshot { | |
1308 [_overscrollActionsController clear]; | |
1309 } | |
1310 | |
1311 #pragma mark - LogoAnimationControllerOwnerOwner | |
1312 | |
1313 - (id<LogoAnimationControllerOwner>)logoAnimationControllerOwner { | |
1314 return [self.logoVendor logoAnimationControllerOwner]; | |
1315 } | |
1316 | |
1317 #pragma mark - UIScrollViewDelegate Methods. | |
1318 | |
1319 - (void)scrollViewDidScroll:(UIScrollView*)scrollView { | |
1320 [self.delegate updateNtpBarShadowForPanelController:self]; | |
1321 [_overscrollActionsController scrollViewDidScroll:scrollView]; | |
1322 | |
1323 // Blur the omnibox when the scroll view is scrolled below the pinned offset. | |
1324 CGFloat pinnedOffsetY = [self pinnedOffsetY]; | |
1325 if (_omniboxFocused && scrollView.dragging && | |
1326 scrollView.contentOffset.y < pinnedOffsetY) { | |
1327 [self.dispatcher cancelOmniboxEdit]; | |
1328 } | |
1329 | |
1330 if (IsIPadIdiom()) { | |
1331 return; | |
1332 } | |
1333 | |
1334 if (_animateHeader) { | |
1335 [self updateSearchField]; | |
1336 } | |
1337 } | |
1338 | |
1339 - (void)scrollViewWillBeginDragging:(UIScrollView*)scrollView { | |
1340 [_overscrollActionsController scrollViewWillBeginDragging:scrollView]; | |
1341 } | |
1342 | |
1343 - (void)scrollViewDidEndDragging:(UIScrollView*)scrollView | |
1344 willDecelerate:(BOOL)decelerate { | |
1345 [_overscrollActionsController scrollViewDidEndDragging:scrollView | |
1346 willDecelerate:decelerate]; | |
1347 } | |
1348 | |
1349 - (void)scrollViewWillEndDragging:(UIScrollView*)scrollView | |
1350 withVelocity:(CGPoint)velocity | |
1351 targetContentOffset:(inout CGPoint*)targetContentOffset { | |
1352 [_overscrollActionsController scrollViewWillEndDragging:scrollView | |
1353 withVelocity:velocity | |
1354 targetContentOffset:targetContentOffset]; | |
1355 | |
1356 if (IsIPadIdiom() || _omniboxFocused) | |
1357 return; | |
1358 | |
1359 CGFloat pinnedOffsetY = [self pinnedOffsetY]; | |
1360 CGFloat offsetY = scrollView.contentOffset.y; | |
1361 CGFloat targetY = targetContentOffset->y; | |
1362 if (offsetY > 0 && offsetY < pinnedOffsetY) { | |
1363 // Omnibox is currently between middle and top of screen. | |
1364 if (velocity.y > 0) { // scrolling upwards | |
1365 if (targetY < pinnedOffsetY) { | |
1366 // Scroll the omnibox up to |pinnedOffsetY| if velocity is upwards but | |
1367 // scrolling will stop before reaching |pinnedOffsetY|. | |
1368 targetContentOffset->y = offsetY; | |
1369 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY) | |
1370 animated:YES]; | |
1371 } | |
1372 _scrolledToTop = YES; | |
1373 } else { // scrolling downwards | |
1374 if (targetY > 0) { | |
1375 // Scroll the omnibox down to zero if velocity is downwards or 0 but | |
1376 // scrolling will stop before reaching 0. | |
1377 targetContentOffset->y = offsetY; | |
1378 [_mostVisitedView setContentOffset:CGPointZero animated:YES]; | |
1379 } | |
1380 _scrolledToTop = NO; | |
1381 } | |
1382 } else if (offsetY > pinnedOffsetY && | |
1383 targetContentOffset->y < pinnedOffsetY) { | |
1384 // Most visited cells are currently scrolled up past the omnibox but will | |
1385 // end the scroll below the omnibox. Stop the scroll at just below the | |
1386 // omnibox. | |
1387 targetContentOffset->y = offsetY; | |
1388 [_mostVisitedView setContentOffset:CGPointMake(0, pinnedOffsetY) | |
1389 animated:YES]; | |
1390 _scrolledToTop = YES; | |
1391 } else if (offsetY >= pinnedOffsetY) { | |
1392 _scrolledToTop = YES; | |
1393 } else if (offsetY <= 0) { | |
1394 _scrolledToTop = NO; | |
1395 } | |
1396 } | |
1397 | |
1398 #pragma mark - Most visited / Suggestions service wrapper methods. | |
1399 | |
1400 - (NSUInteger)numberOfItems { | |
1401 NSUInteger numItems = [self.dataSource mostVisitedSize]; | |
1402 NSUInteger maxItems = [self numberOfColumns] * kMaxNumMostVisitedFaviconRows; | |
1403 return MIN(maxItems, numItems); | |
1404 } | |
1405 | |
1406 - (NSInteger)numberOfNonEmptyTilesShown { | |
1407 NSInteger numCells = | |
1408 MIN([self numberOfItems], self.maximumMostVisitedSitesShown); | |
1409 return MAX(numCells, [self numberOfColumns]); | |
1410 } | |
1411 | |
1412 - (GURL)urlForIndex:(NSUInteger)index { | |
1413 return [self.dataSource mostVisitedAtIndex:index].url; | |
1414 } | |
1415 | |
1416 #pragma mark - GoogleLandingController (ExposedForTesting) methods. | |
1417 | |
1418 - (BOOL)scrolledToTop { | |
1419 return _scrolledToTop; | |
1420 } | |
1421 | |
1422 - (BOOL)animateHeader { | |
1423 return _animateHeader; | |
1424 } | |
1425 | |
1426 #pragma mark - OverscrollActionsControllerDelegate | |
1427 | |
1428 - (void)overscrollActionsController:(OverscrollActionsController*)controller | |
1429 didTriggerAction:(OverscrollAction)action { | |
1430 switch (action) { | |
1431 case OverscrollAction::NEW_TAB: { | |
1432 base::scoped_nsobject<GenericChromeCommand> command( | |
1433 [[GenericChromeCommand alloc] initWithTag:IDC_NEW_TAB]); | |
1434 [[self view] chromeExecuteCommand:command]; | |
1435 } break; | |
1436 case OverscrollAction::CLOSE_TAB: { | |
1437 base::scoped_nsobject<GenericChromeCommand> command( | |
1438 [[GenericChromeCommand alloc] initWithTag:IDC_CLOSE_TAB]); | |
1439 [[self view] chromeExecuteCommand:command]; | |
1440 } break; | |
1441 case OverscrollAction::REFRESH: | |
1442 [self reload]; | |
1443 break; | |
1444 case OverscrollAction::NONE: | |
1445 NOTREACHED(); | |
1446 break; | |
1447 } | |
1448 } | |
1449 | |
1450 - (BOOL)shouldAllowOverscrollActions { | |
1451 return YES; | |
1452 } | |
1453 | |
1454 - (UIView*)toolbarSnapshotView { | |
1455 return [[_headerView toolBarView] snapshotViewAfterScreenUpdates:NO]; | |
1456 } | |
1457 | |
1458 - (UIView*)headerView { | |
1459 return self.view; | |
1460 } | |
1461 | |
1462 - (CGFloat)overscrollActionsControllerHeaderInset: | |
1463 (OverscrollActionsController*)controller { | |
1464 return 0; | |
1465 } | |
1466 | |
1467 - (CGFloat)overscrollHeaderHeight { | |
1468 return [_headerView toolBarView].bounds.size.height; | |
1469 } | |
1470 | |
1471 #pragma mark - UIGestureRecognizerDelegate | |
1472 | |
1473 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer | |
1474 shouldReceiveTouch:(UITouch*)touch { | |
1475 return [self nearestAncestorOfView:touch.view | |
1476 withClass:[MostVisitedCell class]] == nil; | |
1477 } | |
1478 | |
1479 - (UIView*)nearestAncestorOfView:(UIView*)view withClass:(Class)aClass { | |
1480 if (!view) { | |
1481 return nil; | |
1482 } | |
1483 if ([view isKindOfClass:aClass]) { | |
1484 return view; | |
1485 } | |
1486 return [self nearestAncestorOfView:[view superview] withClass:aClass]; | |
1487 } | |
1488 | |
1489 #pragma mark - GoogleLandingConsumer | |
1490 | |
1491 - (void)setLogoIsShowing:(BOOL)logoIsShowing { | |
1492 _logoIsShowing = logoIsShowing; | |
1493 [self updateLogoAndFakeboxDisplay]; | |
1494 } | |
1495 | |
1496 - (void)mostVisitedDataUpdated { | |
1497 [self reloadData]; | |
1498 } | |
1499 | |
1500 - (void)mostVisitedIconMadeAvailableAtIndex:(NSUInteger)index { | |
1501 if (index >= [self numberOfItems]) | |
1502 return; | |
1503 | |
1504 NSIndexPath* indexPath = | |
1505 [NSIndexPath indexPathForRow:index inSection:SectionWithMostVisited]; | |
1506 [_mostVisitedView reloadItemsAtIndexPaths:@[ indexPath ]]; | |
1507 } | |
1508 | |
1509 - (void)setTabCount:(int)tabCount { | |
1510 _tabCount = tabCount; | |
1511 [_headerView setToolbarTabCount:self.tabCount]; | |
1512 } | |
1513 | |
1514 - (void)setCanGoForward:(BOOL)canGoForward { | |
1515 _canGoForward = canGoForward; | |
1516 [_headerView setCanGoForward:self.canGoForward]; | |
1517 } | |
1518 | |
1519 - (void)setCanGoBack:(BOOL)canGoBack { | |
1520 _canGoBack = canGoBack; | |
1521 [_headerView setCanGoBack:self.canGoBack]; | |
1522 } | |
1523 @end | |
OLD | NEW |