OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 #import "ios/chrome/browser/ui/tabs/tab_strip_controller.h" |
| 6 #import "ios/chrome/browser/ui/tabs/tab_strip_controller_private.h" |
| 7 |
| 8 #include <cmath> |
| 9 #include <vector> |
| 10 |
| 11 #include "base/i18n/rtl.h" |
| 12 #import "base/ios/weak_nsobject.h" |
| 13 #include "base/mac/bundle_locations.h" |
| 14 #include "base/mac/foundation_util.h" |
| 15 #include "base/mac/objc_property_releaser.h" |
| 16 #include "base/mac/scoped_nsobject.h" |
| 17 #include "base/metrics/user_metrics.h" |
| 18 #include "base/metrics/user_metrics_action.h" |
| 19 #include "base/strings/sys_string_conversions.h" |
| 20 #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| 21 #include "ios/chrome/browser/experimental_flags.h" |
| 22 #import "ios/chrome/browser/tabs/tab.h" |
| 23 #import "ios/chrome/browser/tabs/tab_model.h" |
| 24 #import "ios/chrome/browser/tabs/tab_model_observer.h" |
| 25 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
| 26 #include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
| 27 #import "ios/chrome/browser/ui/fullscreen_controller.h" |
| 28 #include "ios/chrome/browser/ui/rtl_geometry.h" |
| 29 #include "ios/chrome/browser/ui/tab_switcher/tab_switcher_tab_strip_placeholder_
view.h" |
| 30 #import "ios/chrome/browser/ui/tabs/tab_strip_controller+tab_switcher_animation.
h" |
| 31 #import "ios/chrome/browser/ui/tabs/tab_strip_view.h" |
| 32 #import "ios/chrome/browser/ui/tabs/tab_view.h" |
| 33 #include "ios/chrome/browser/ui/tabs/target_frame_cache.h" |
| 34 #include "ios/chrome/browser/ui/ui_util.h" |
| 35 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 36 #import "ios/chrome/browser/ui/util/snapshot_util.h" |
| 37 #include "ios/chrome/grit/ios_strings.h" |
| 38 #import "ios/web/public/web_state/web_state.h" |
| 39 #include "third_party/google_toolbox_for_mac/src/iPhone/GTMFadeTruncatingLabel.h
" |
| 40 #include "ui/gfx/image/image.h" |
| 41 |
| 42 using base::UserMetricsAction; |
| 43 |
| 44 NSString* const kWillStartTabStripTabAnimation = |
| 45 @"kWillStartTabStripTabAnimation"; |
| 46 NSString* const kTabStripDragStarted = @"kTabStripDragStarted"; |
| 47 NSString* const kTabStripDragEnded = @"kTabStripDragEnded"; |
| 48 |
| 49 namespace TabStrip { |
| 50 UIColor* BackgroundColor() { |
| 51 DCHECK(IsIPadIdiom()); |
| 52 return [UIColor colorWithRed:0.149 green:0.149 blue:0.164 alpha:1]; |
| 53 } |
| 54 } |
| 55 |
| 56 namespace { |
| 57 |
| 58 // Animation duration for tab animations. |
| 59 const NSTimeInterval kTabAnimationDuration = 0.25; |
| 60 |
| 61 // Animation duration for tab strip fade. |
| 62 const NSTimeInterval kTabStripFadeAnimationDuration = 0.15; |
| 63 |
| 64 // Amount of time needed to trigger drag and drop mode when long pressing. |
| 65 const NSTimeInterval kDragAndDropLongPressDuration = 0.4; |
| 66 |
| 67 // Tab dimensions. |
| 68 const CGFloat kTabOverlap = 26.0; |
| 69 const CGFloat kTabOverlapForCompactLayout = 30.0; |
| 70 |
| 71 const CGFloat kNewTabOverlap = 8.0; |
| 72 const CGFloat kMaxTabWidth = 265.0; |
| 73 |
| 74 // Toggle button dimensions. |
| 75 const CGFloat kModeToggleButtonWidth = 36.0; |
| 76 const CGFloat kTabSwitcherToggleButtonWidth = 46.0; |
| 77 const CGFloat kModeToggleButtonBackgroundWidth = 62.0; |
| 78 |
| 79 const CGFloat kNewTabRightPadding = 4.0; |
| 80 const CGFloat kMinTabWidth = 200.0; |
| 81 const CGFloat kMinTabWidthForCompactLayout = 160.0; |
| 82 |
| 83 const CGFloat kCollapsedTabOverlap = 5.0; |
| 84 const NSUInteger kMaxNumCollapsedTabs = 3; |
| 85 const NSUInteger kMaxNumCollapsedTabsForCompactLayout = 0; |
| 86 |
| 87 // Tabs with a visible width smaller than this draw as collapsed tabs.. |
| 88 const CGFloat kCollapsedTabWidthThreshold = 40.0; |
| 89 |
| 90 // Autoscroll constants. The autoscroll distance is set to |
| 91 // |kMaxAutoscrollDistance| at the edges of the scroll view. As the tab moves |
| 92 // away from the edges of the scroll view, the autoscroll distance decreases by |
| 93 // one for each |kAutoscrollDecrementWidth| points. |
| 94 const CGFloat kMaxAutoscrollDistance = 10.0; |
| 95 const CGFloat kAutoscrollDecrementWidth = 10.0; |
| 96 |
| 97 // Dimming view constants, in points. |
| 98 const CGFloat kDimmingViewBottomInsetHighRes = 0.0; |
| 99 const CGFloat kDimmingViewBottomInset = 0.0; |
| 100 |
| 101 // The size of the tab strip view. |
| 102 const CGFloat kTabStripHeight = 39.0; |
| 103 |
| 104 // The size of the new tab button. |
| 105 const CGFloat kNewTabButtonWidth = 59.9; |
| 106 |
| 107 // Default image insets for the new tab button. |
| 108 const CGFloat kNewTabButtonHorizontalImageInset = 2.0; |
| 109 const CGFloat kNewTabButtonTopImageInset = 6.0; |
| 110 const CGFloat kNewTabButtonBottomImageInset = 7.0; |
| 111 |
| 112 // Offsets needed to keep the UI properly centered on high-res screens, in |
| 113 // points. |
| 114 const CGFloat kNewTabButtonBottomOffsetHighRes = 2.0; |
| 115 } |
| 116 |
| 117 @interface TabStripController ()<TabModelObserver, |
| 118 TabStripViewLayoutDelegate, |
| 119 UIGestureRecognizerDelegate, |
| 120 UIScrollViewDelegate> { |
| 121 base::scoped_nsobject<TabModel> _tabModel; |
| 122 UIView* _view; |
| 123 TabStripView* _tabStripView; |
| 124 UIButton* _buttonNewTab; |
| 125 UIButton* _modeToggleButton; // weak, nil if not visible. |
| 126 UIButton* _tabSwitcherToggleButton; // weak, nil if not visible. |
| 127 |
| 128 // Background view of the toggle button. Only visible while in compact layout. |
| 129 base::scoped_nsobject<UIImageView> _toggleButtonBackgroundView; |
| 130 |
| 131 TabStrip::Style _style; |
| 132 base::WeakNSProtocol<id<FullScreenControllerDelegate>> _fullscreenDelegate; |
| 133 |
| 134 // Array of TabViews. There is a one-to-one correspondence between this array |
| 135 // and the set of Tabs in the TabModel. |
| 136 base::scoped_nsobject<NSMutableArray> _tabArray; |
| 137 |
| 138 // Set of TabViews that are currently closing. These TabViews are also in |
| 139 // |_tabArray|. Used to translate between |_tabArray| indexes and TabModel |
| 140 // indexes. |
| 141 base::scoped_nsobject<NSMutableSet> _closingTabs; |
| 142 |
| 143 // Tracks target frames for TabViews. |
| 144 // TODO(rohitrao): This is unnecessary, as UIKit updates view frames |
| 145 // immediately, so [view frame] will always return the end state of the |
| 146 // current animation. We can remove this cache entirely. b/5516053 |
| 147 TargetFrameCache _targetFrames; |
| 148 |
| 149 // Animate when doing layout. This flag is set by setNeedsLayoutWithAnimation |
| 150 // and cleared in layoutSubviews. |
| 151 BOOL _animateLayout; |
| 152 |
| 153 // The current tab width. Recomputed whenever a tab is added or removed. |
| 154 CGFloat _currentTabWidth; |
| 155 |
| 156 // View used to dim unselected tabs when in reordering mode. Nil when not |
| 157 // reordering tabs. |
| 158 base::scoped_nsobject<UIView> _dimmingView; |
| 159 |
| 160 // Is the selected tab highlighted, used when dragging or swiping tabs. |
| 161 BOOL _highlightsSelectedTab; |
| 162 |
| 163 // YES when in reordering mode. |
| 164 // TODO(rohitrao): This is redundant with |_draggedTab|. Remove it. |
| 165 BOOL _isReordering; |
| 166 |
| 167 // The tab that is currently being dragged. nil when not in reordering mode. |
| 168 base::scoped_nsobject<TabView> _draggedTab; |
| 169 |
| 170 // The last known location of the touch that is dragging the tab. This |
| 171 // location is in the coordinate system of |[_tabStripView superview]| because |
| 172 // that coordinate system does not change as the scroll view scrolls. |
| 173 CGPoint _lastDragLocation; |
| 174 |
| 175 // Timer used to autoscroll when in reordering mode. Is nil when not active. |
| 176 // Owned by its runloop. |
| 177 NSTimer* _autoscrollTimer; // weak |
| 178 |
| 179 // The distance to scroll for each autoscroll timer tick. If negative, the |
| 180 // tabstrip will scroll to the left; if positive, to the right. |
| 181 CGFloat _autoscrollDistance; |
| 182 |
| 183 // The model index of the placeholder gap, if one exists. This value is used |
| 184 // as the new model index of the dragged tab when it is dropped. |
| 185 NSUInteger _placeholderGapModelIndex; |
| 186 |
| 187 // If YES, display the mode toggle switch at the left side of the strip. Can |
| 188 // be set after creation. |
| 189 BOOL _hasModeToggleSwitch; |
| 190 |
| 191 // If YES, display the tab switcher toggle switch at the left side of the |
| 192 // strip. Can be set after creation. |
| 193 BOOL _hasTabSwitcherToggleSwitch; |
| 194 |
| 195 base::mac::ObjCPropertyReleaser _propertyReleaser_TabStripController; |
| 196 } |
| 197 |
| 198 @property(nonatomic, readonly, retain) TabStripView* tabStripView; |
| 199 @property(nonatomic, readonly, retain) UIButton* buttonNewTab; |
| 200 @property(nonatomic, readonly, assign) UIButton* tabSwitcherToggleButton; |
| 201 |
| 202 // Initializes the tab array based on the the entries in the TabModel. Creates |
| 203 // one TabView per Tab and adds it to the tabstrip. A later call to |
| 204 // |-layoutTabs| is needed to properly place the tabs in the correct positions. |
| 205 - (void)initializeTabArrayFromTabModel; |
| 206 |
| 207 // Initializes the tab array to have only one empty tab, for the case (used |
| 208 // during startup) when there is not a tab model available. |
| 209 - (void)initializeTabArrayWithNoModel; |
| 210 |
| 211 // Add and remove the mode toggle icon and adjusts the size of the scroll view |
| 212 // accordingly. Assumes incognito style has already been checked. |
| 213 - (void)installModeToggleButton; |
| 214 - (void)removeModeToggleButton; |
| 215 |
| 216 // Add and remove the tab switcher toggle icon and adjusts the size of the |
| 217 // scroll view accordingly. The tab switcher toggle button is replacing the |
| 218 // incognito mode toggle button. |
| 219 // TODO:(jbbegue) crbug/477676 Remove reference to the incognito toggle button |
| 220 // once we know for sure that it will be replaced by the tab switcher toggle |
| 221 // button. |
| 222 - (void)installTabSwitcherToggleButton; |
| 223 - (void)removeTabSwitcherToggleButton; |
| 224 |
| 225 // Returns an autoreleased TabView object with no content. |
| 226 - (TabView*)emptyTabView; |
| 227 |
| 228 // Returns an autoreleased TabView object based on the given Tab. |
| 229 // |isSelected| is passed in here as an optimization, so that the TabView is |
| 230 // drawn correctly the first time, without requiring the model to send a |
| 231 // -setSelected message to the TabView. |
| 232 - (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected; |
| 233 |
| 234 // Creates and installs the view used to dim unselected tabs. Does nothing if |
| 235 // the view already exists. |
| 236 - (void)installDimmingViewWithAnimation:(BOOL)animate; |
| 237 |
| 238 // Remove the dimming view, |
| 239 - (void)removeDimmingViewWithAnimation:(BOOL)animate; |
| 240 |
| 241 // Converts between model indexes and |_tabArray| indexes. The conversion is |
| 242 // necessary because |_tabArray| contains closing tabs whereas the TabModel does |
| 243 // not. |
| 244 - (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex; |
| 245 - (NSUInteger)modelIndexForIndex:(NSUInteger)index; |
| 246 - (NSUInteger)modelIndexForTabView:(TabView*)view; |
| 247 |
| 248 // Helper methods to handle each stage of a drag. |
| 249 - (void)beginDrag:(UILongPressGestureRecognizer*)gesture; |
| 250 - (void)continueDrag:(UILongPressGestureRecognizer*)gesture; |
| 251 - (void)endDrag:(UILongPressGestureRecognizer*)gesture; |
| 252 - (void)cancelDrag:(UILongPressGestureRecognizer*)gesture; |
| 253 |
| 254 // Resets any internal variables used to track drag state. |
| 255 - (void)resetDragState; |
| 256 |
| 257 // Returns whether or not the tabstrip is currently in reordering mode. |
| 258 - (BOOL)isReorderingTabs; |
| 259 |
| 260 // Installs or removes the autoscroll timer. |
| 261 - (void)installAutoscrollTimerIfNeeded; |
| 262 - (void)removeAutoscrollTimer; |
| 263 |
| 264 // Called once per autoscroll timer tick. Adjusts the scroll view's content |
| 265 // offset as needed. |
| 266 - (void)autoscrollTimerFired:(NSTimer*)timer; |
| 267 |
| 268 // Calculates and stores the autoscroll distance for the given tab view. The |
| 269 // autoscroll distance is a function of the distance between the edge of the |
| 270 // scroll view and the tab's frame. |
| 271 - (void)computeAutoscrollDistanceForTabView:(TabView*)view; |
| 272 |
| 273 // Constrains the stored autoscroll distance to prevent the scroll view from |
| 274 // overscrolling. |
| 275 - (void)constrainAutoscrollDistance; |
| 276 |
| 277 #if 0 |
| 278 // Returns the appropriate model index for the currently dragged tab, given its |
| 279 // current position. (If dropped, the tab would be at this index in the model.) |
| 280 // TODO(rohitrao): Implement this method. |
| 281 - (NSUInteger)modelIndexForDraggedTab; |
| 282 #endif |
| 283 |
| 284 // Returns the horizontal visible tab strip width used to compute the tab width |
| 285 // and the tabs and new tab button in regular layout mode. |
| 286 // Takes into account whether or not the mode toggle button is showing. |
| 287 - (CGFloat)tabStripVisibleSpace; |
| 288 |
| 289 // Updates the scroll view's content size based on the current set of tabs and |
| 290 // closing tabs. After updating the content size, repositions views so they |
| 291 // they will appear stationary on screen. |
| 292 - (void)updateContentSizeAndRepositionViews; |
| 293 |
| 294 // Returns the frame, in the scroll view content's coordinate system, of the |
| 295 // given tab view. |
| 296 - (CGRect)scrollViewFrameForTab:(TabView*)view; |
| 297 |
| 298 // Returns the portion of |frame| which is not covered by |frameOnTop|. |
| 299 - (CGRect)calculateVisibleFrameForFrame:(CGRect)frame |
| 300 whenUnderFrame:(CGRect)frameOnTop; |
| 301 |
| 302 // Schedules a layout of the scroll view and sets the internal |_animateLayout| |
| 303 // flag so that the layout will be animated. |
| 304 - (void)setNeedsLayoutWithAnimation; |
| 305 |
| 306 // Returns the maximum number of collapsed tabs depending on the current layout |
| 307 // mode. |
| 308 - (NSUInteger)maxNumCollapsedTabs; |
| 309 |
| 310 // Returns the tab overlap width depending on the current layout mode. |
| 311 - (CGFloat)tabOverlap; |
| 312 |
| 313 // Returns the minimum tab view width depending on the current layout mode. |
| 314 - (CGFloat)minTabWidth; |
| 315 |
| 316 // Updates the content offset of the tab strip view in order to keep the |
| 317 // selected tab view visible. |
| 318 // Content offset adjustement is only needed/performed in compact mode. |
| 319 // This method must be called with a valid |tabIndex|. |
| 320 - (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex; |
| 321 |
| 322 // Update the frame of the tab strip view (scrollview) frame, content inset and |
| 323 // toggle buttons states depending on the current layout mode. |
| 324 - (void)updateScrollViewFrameForToggleButton; |
| 325 |
| 326 @end |
| 327 |
| 328 @implementation TabStripController |
| 329 |
| 330 @synthesize buttonNewTab = _buttonNewTab; |
| 331 @synthesize hasModeToggleSwitch = _hasModeToggleSwitch; |
| 332 @synthesize hasTabSwitcherToggleSwitch = _hasTabSwitcherToggleSwitch; |
| 333 @synthesize highlightsSelectedTab = _highlightsSelectedTab; |
| 334 @synthesize modeToggleButton = _modeToggleButton; |
| 335 @synthesize tabStripView = _tabStripView; |
| 336 @synthesize tabSwitcherToggleButton = _tabSwitcherToggleButton; |
| 337 @synthesize view = _view; |
| 338 |
| 339 - (instancetype)initWithTabModel:(TabModel*)tabModel |
| 340 style:(TabStrip::Style)style { |
| 341 if ((self = [super init])) { |
| 342 _propertyReleaser_TabStripController.Init(self, [TabStripController class]); |
| 343 _tabArray.reset([[NSMutableArray alloc] initWithCapacity:10]); |
| 344 _closingTabs.reset([[NSMutableSet alloc] initWithCapacity:5]); |
| 345 |
| 346 _tabModel.reset([tabModel retain]); |
| 347 [_tabModel addObserver:self]; |
| 348 _style = style; |
| 349 _hasModeToggleSwitch = NO; |
| 350 _hasTabSwitcherToggleSwitch = NO; |
| 351 |
| 352 // |self.view| setup. |
| 353 CGRect tabStripFrame = [UIApplication sharedApplication].keyWindow.bounds; |
| 354 tabStripFrame.size.height = kTabStripHeight; |
| 355 _view = [[UIView alloc] initWithFrame:tabStripFrame]; |
| 356 _view.autoresizingMask = (UIViewAutoresizingFlexibleWidth | |
| 357 UIViewAutoresizingFlexibleBottomMargin); |
| 358 _view.backgroundColor = TabStrip::BackgroundColor(); |
| 359 if (UseRTLLayout()) |
| 360 _view.transform = CGAffineTransformMakeScale(-1, 1); |
| 361 |
| 362 // |self.tabStripView| setup. |
| 363 _tabStripView = [[TabStripView alloc] initWithFrame:_view.bounds]; |
| 364 _tabStripView.autoresizingMask = |
| 365 (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); |
| 366 _tabStripView.backgroundColor = _view.backgroundColor; |
| 367 _tabStripView.layoutDelegate = self; |
| 368 _tabStripView.accessibilityIdentifier = style == TabStrip::kStyleIncognito |
| 369 ? @"Incognito Tab Strip" |
| 370 : @"Tab Strip"; |
| 371 [_view addSubview:_tabStripView]; |
| 372 |
| 373 // |self.buttonNewTab| setup. |
| 374 CGRect buttonNewTabFrame = tabStripFrame; |
| 375 buttonNewTabFrame.size.width = kNewTabButtonWidth; |
| 376 _buttonNewTab = [[UIButton alloc] initWithFrame:buttonNewTabFrame]; |
| 377 BOOL isBrowserStateIncognito = |
| 378 tabModel && tabModel.browserState->IsOffTheRecord(); |
| 379 _buttonNewTab.tag = |
| 380 isBrowserStateIncognito ? IDC_NEW_INCOGNITO_TAB : IDC_NEW_TAB; |
| 381 // TODO(crbug.com/600829): Rewrite layout code and convert these masks to |
| 382 // to trailing and leading margins rather than right and bottom. |
| 383 _buttonNewTab.autoresizingMask = (UIViewAutoresizingFlexibleRightMargin | |
| 384 UIViewAutoresizingFlexibleBottomMargin); |
| 385 _buttonNewTab.imageView.contentMode = UIViewContentModeCenter; |
| 386 UIImage* buttonNewTabImage = nil; |
| 387 UIImage* buttonNewTabPressedImage = nil; |
| 388 if (_style == TabStrip::kStyleIncognito) { |
| 389 buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab_incognito"]; |
| 390 buttonNewTabPressedImage = |
| 391 [UIImage imageNamed:@"tabstrip_new_tab_incognito_pressed"]; |
| 392 } else { |
| 393 buttonNewTabImage = [UIImage imageNamed:@"tabstrip_new_tab"]; |
| 394 buttonNewTabPressedImage = |
| 395 [UIImage imageNamed:@"tabstrip_new_tab_pressed"]; |
| 396 } |
| 397 [_buttonNewTab setImage:buttonNewTabImage forState:UIControlStateNormal]; |
| 398 [_buttonNewTab setImage:buttonNewTabPressedImage |
| 399 forState:UIControlStateHighlighted]; |
| 400 UIEdgeInsets imageInsets = UIEdgeInsetsMake( |
| 401 kNewTabButtonTopImageInset, kNewTabButtonHorizontalImageInset, |
| 402 kNewTabButtonBottomImageInset, kNewTabButtonHorizontalImageInset); |
| 403 if (IsHighResScreen()) { |
| 404 imageInsets.top += kNewTabButtonBottomOffsetHighRes; |
| 405 imageInsets.bottom -= kNewTabButtonBottomOffsetHighRes; |
| 406 } |
| 407 _buttonNewTab.imageEdgeInsets = imageInsets; |
| 408 SetA11yLabelAndUiAutomationName( |
| 409 _buttonNewTab, |
| 410 isBrowserStateIncognito ? IDS_IOS_TOOLS_MENU_NEW_INCOGNITO_TAB |
| 411 : IDS_IOS_TOOLS_MENU_NEW_TAB, |
| 412 isBrowserStateIncognito ? @"New Incognito Tab" : @"New Tab"); |
| 413 // Use a nil target to send |-chromeExecuteCommand:| down the responder |
| 414 // chain. |
| 415 [_buttonNewTab addTarget:nil |
| 416 action:@selector(chromeExecuteCommand:) |
| 417 forControlEvents:UIControlEventTouchUpInside]; |
| 418 [_buttonNewTab addTarget:self |
| 419 action:@selector(recordUserMetrics:) |
| 420 forControlEvents:UIControlEventTouchUpInside]; |
| 421 [_tabStripView addSubview:_buttonNewTab]; |
| 422 |
| 423 // Add tab buttons to tab strip. |
| 424 if (_tabModel) |
| 425 [self initializeTabArrayFromTabModel]; |
| 426 else |
| 427 [self initializeTabArrayWithNoModel]; |
| 428 |
| 429 // Update the layout of the tab buttons. |
| 430 [self updateContentSizeAndRepositionViews]; |
| 431 [self layoutTabStripSubviews]; |
| 432 |
| 433 // Don't highlight the selected tab by default. |
| 434 self.highlightsSelectedTab = NO; |
| 435 } |
| 436 return self; |
| 437 } |
| 438 |
| 439 - (instancetype)init { |
| 440 NOTREACHED(); |
| 441 return nil; |
| 442 } |
| 443 |
| 444 - (void)dealloc { |
| 445 [_tabStripView setDelegate:nil]; |
| 446 [_tabStripView setLayoutDelegate:nil]; |
| 447 [_tabModel removeObserver:self]; |
| 448 [super dealloc]; |
| 449 } |
| 450 |
| 451 - (id<FullScreenControllerDelegate>)fullscreenDelegate { |
| 452 return _fullscreenDelegate; |
| 453 } |
| 454 |
| 455 - (void)setFullscreenDelegate: |
| 456 (id<FullScreenControllerDelegate>)fullscreenDelegate { |
| 457 _fullscreenDelegate.reset(fullscreenDelegate); |
| 458 } |
| 459 |
| 460 - (void)initializeTabArrayFromTabModel { |
| 461 DCHECK(_tabModel); |
| 462 for (Tab* tab in _tabModel.get()) { |
| 463 BOOL isSelectedTab = [_tabModel currentTab] == tab; |
| 464 TabView* view = [self tabViewForTab:tab isSelected:isSelectedTab]; |
| 465 [_tabArray addObject:view]; |
| 466 [_tabStripView addSubview:view]; |
| 467 } |
| 468 } |
| 469 |
| 470 - (void)initializeTabArrayWithNoModel { |
| 471 DCHECK(!_tabModel); |
| 472 TabView* view = [self emptyTabView]; |
| 473 [_tabArray addObject:view]; |
| 474 [_tabStripView addSubview:view]; |
| 475 [view setSelected:YES]; |
| 476 return; |
| 477 } |
| 478 |
| 479 - (void)setHasModeToggleSwitch:(BOOL)hasModeToggleSwitch { |
| 480 if (_hasModeToggleSwitch && !hasModeToggleSwitch) |
| 481 [self removeModeToggleButton]; |
| 482 if (!_hasModeToggleSwitch && hasModeToggleSwitch) |
| 483 [self installModeToggleButton]; |
| 484 if (_hasModeToggleSwitch != hasModeToggleSwitch) { |
| 485 _hasModeToggleSwitch = hasModeToggleSwitch; |
| 486 [self updateContentSizeAndRepositionViews]; |
| 487 [self setNeedsLayoutWithAnimation]; |
| 488 } |
| 489 } |
| 490 |
| 491 - (void)setHasTabSwitcherToggleSwitch:(BOOL)hasTabSwitcherToggleSwitch { |
| 492 if (_hasTabSwitcherToggleSwitch && !hasTabSwitcherToggleSwitch) |
| 493 [self removeTabSwitcherToggleButton]; |
| 494 if (!_hasTabSwitcherToggleSwitch && hasTabSwitcherToggleSwitch) |
| 495 [self installTabSwitcherToggleButton]; |
| 496 if (_hasTabSwitcherToggleSwitch != hasTabSwitcherToggleSwitch) { |
| 497 _hasTabSwitcherToggleSwitch = hasTabSwitcherToggleSwitch; |
| 498 [self updateContentSizeAndRepositionViews]; |
| 499 [self setNeedsLayoutWithAnimation]; |
| 500 } |
| 501 } |
| 502 |
| 503 - (TabView*)emptyTabView { |
| 504 TabView* view = |
| 505 [[[TabView alloc] initWithEmptyView:YES selected:YES] autorelease]; |
| 506 [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)]; |
| 507 [view setContentMode:UIViewContentModeRedraw]; |
| 508 |
| 509 // Setting the tab to be hidden marks it as a new tab. The layout code will |
| 510 // make the tab visible and set up the appropriate animations. |
| 511 [view setHidden:YES]; |
| 512 |
| 513 return view; |
| 514 } |
| 515 |
| 516 - (TabView*)tabViewForTab:(Tab*)tab isSelected:(BOOL)isSelected { |
| 517 TabView* view = |
| 518 [[[TabView alloc] initWithEmptyView:NO selected:isSelected] autorelease]; |
| 519 if (UseRTLLayout()) |
| 520 [view setTransform:CGAffineTransformMakeScale(-1, 1)]; |
| 521 [view setIncognitoStyle:(_style == TabStrip::kStyleIncognito)]; |
| 522 [view setContentMode:UIViewContentModeRedraw]; |
| 523 [[view titleLabel] setText:[tab title]]; |
| 524 [view setFavicon:[tab favicon]]; |
| 525 |
| 526 // Set the tab buttons' action messages. |
| 527 [view addTarget:self |
| 528 action:@selector(tabTapped:) |
| 529 forControlEvents:UIControlEventTouchUpInside]; |
| 530 [[view closeButton] addTarget:self |
| 531 action:@selector(closeTab:) |
| 532 forControlEvents:UIControlEventTouchUpInside]; |
| 533 |
| 534 // Install a long press gesture recognizer to handle drag and drop. |
| 535 base::scoped_nsobject<UILongPressGestureRecognizer> longPress( |
| 536 [[UILongPressGestureRecognizer alloc] |
| 537 initWithTarget:self |
| 538 action:@selector(handleLongPress:)]); |
| 539 [longPress setMinimumPressDuration:kDragAndDropLongPressDuration]; |
| 540 [longPress setDelegate:self]; |
| 541 [view addGestureRecognizer:longPress]; |
| 542 |
| 543 // Giving the tab view exclusive touch prevents other views from receiving |
| 544 // touches while a TabView is handling a touch. |
| 545 [view setExclusiveTouch:YES]; |
| 546 |
| 547 // Setting the tab to be hidden marks it as a new tab. The layout code will |
| 548 // make the tab visible and set up the appropriate animations. |
| 549 [view setHidden:YES]; |
| 550 |
| 551 return view; |
| 552 } |
| 553 |
| 554 - (void)setHighlightsSelectedTab:(BOOL)highlightsSelectedTab { |
| 555 if (highlightsSelectedTab) |
| 556 [self installDimmingViewWithAnimation:YES]; |
| 557 else |
| 558 [self removeDimmingViewWithAnimation:YES]; |
| 559 |
| 560 _highlightsSelectedTab = highlightsSelectedTab; |
| 561 } |
| 562 |
| 563 - (void)installDimmingViewWithAnimation:(BOOL)animate { |
| 564 // The dimming view should not cover the bottom 2px of the tab strip, as those |
| 565 // pixels are visually part of the top border of the toolbar. The bottom |
| 566 // inset constants take into account the conversion from pixels to points. |
| 567 CGRect frame = [_tabStripView bounds]; |
| 568 frame.size.height -= (IsHighResScreen() ? kDimmingViewBottomInsetHighRes |
| 569 : kDimmingViewBottomInset); |
| 570 |
| 571 // Create the dimming view if it doesn't exist. In all cases, make sure it's |
| 572 // set up correctly. |
| 573 if (_dimmingView.get()) |
| 574 [_dimmingView setFrame:frame]; |
| 575 else |
| 576 _dimmingView.reset([[UIView alloc] initWithFrame:frame]); |
| 577 |
| 578 // Enable user interaction in order to eat touches from views behind it. |
| 579 [_dimmingView setUserInteractionEnabled:YES]; |
| 580 [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor() |
| 581 colorWithAlphaComponent:0]]; |
| 582 [_dimmingView setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | |
| 583 UIViewAutoresizingFlexibleHeight)]; |
| 584 [_tabStripView addSubview:_dimmingView]; |
| 585 |
| 586 CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0; |
| 587 [UIView animateWithDuration:duration |
| 588 animations:^{ |
| 589 [_dimmingView |
| 590 setBackgroundColor:[TabStrip::BackgroundColor() |
| 591 colorWithAlphaComponent:0.6]]; |
| 592 }]; |
| 593 } |
| 594 |
| 595 - (void)removeDimmingViewWithAnimation:(BOOL)animate { |
| 596 if (_dimmingView) { |
| 597 CGFloat duration = animate ? kTabStripFadeAnimationDuration : 0; |
| 598 [UIView animateWithDuration:duration |
| 599 animations:^{ |
| 600 [_dimmingView setBackgroundColor:[TabStrip::BackgroundColor() |
| 601 colorWithAlphaComponent:0]]; |
| 602 } |
| 603 completion:^(BOOL finished) { |
| 604 // Do not remove the dimming view if the animation was aborted. |
| 605 if (finished) { |
| 606 [_dimmingView removeFromSuperview]; |
| 607 _dimmingView.reset(); |
| 608 } |
| 609 }]; |
| 610 } |
| 611 } |
| 612 |
| 613 - (void)recordUserMetrics:(id)sender { |
| 614 if (sender == _buttonNewTab) |
| 615 base::RecordAction(UserMetricsAction("MobileTabStripNewTab")); |
| 616 else if (sender == _modeToggleButton) |
| 617 base::RecordAction(UserMetricsAction("MobileTabStripSwitchMode")); |
| 618 else if (sender == _tabSwitcherToggleButton) |
| 619 base::RecordAction(UserMetricsAction("MobileTabSwitcherOpen")); |
| 620 else |
| 621 LOG(WARNING) << "Trying to record metrics for unknown sender " |
| 622 << base::SysNSStringToUTF8([sender description]); |
| 623 } |
| 624 |
| 625 - (void)tabTapped:(id)sender { |
| 626 DCHECK([sender isKindOfClass:[TabView class]]); |
| 627 |
| 628 // Ignore taps while in reordering mode. |
| 629 if ([self isReorderingTabs]) |
| 630 return; |
| 631 |
| 632 NSUInteger index = [self modelIndexForTabView:(TabView*)sender]; |
| 633 DCHECK_NE(NSNotFound, static_cast<NSInteger>(index)); |
| 634 if (index == NSNotFound) |
| 635 return; |
| 636 Tab* tappedTab = [_tabModel tabAtIndex:index]; |
| 637 Tab* currentTab = [_tabModel currentTab]; |
| 638 if (IsIPadIdiom() && (currentTab != tappedTab)) { |
| 639 [currentTab updateSnapshotWithOverlay:YES visibleFrameOnly:YES]; |
| 640 } |
| 641 [_tabModel setCurrentTab:tappedTab]; |
| 642 [self updateContentOffsetForTabIndex:index]; |
| 643 } |
| 644 |
| 645 - (void)closeTab:(id)sender { |
| 646 // Ignore taps while in reordering mode. |
| 647 // TODO(rohitrao): We should just hide the close buttons instead. |
| 648 if ([self isReorderingTabs]) |
| 649 return; |
| 650 |
| 651 base::RecordAction(UserMetricsAction("MobileTabStripCloseTab")); |
| 652 DCHECK([sender isKindOfClass:[UIButton class]]); |
| 653 UIView* superview = [sender superview]; |
| 654 DCHECK([superview isKindOfClass:[TabView class]]); |
| 655 TabView* tab = (TabView*)superview; |
| 656 NSUInteger modelIndex = [self modelIndexForTabView:tab]; |
| 657 if (modelIndex != NSNotFound) |
| 658 [_tabModel closeTabAtIndex:modelIndex]; |
| 659 } |
| 660 |
| 661 - (void)handleLongPress:(UILongPressGestureRecognizer*)gesture { |
| 662 switch ([gesture state]) { |
| 663 case UIGestureRecognizerStateBegan: |
| 664 [[NSNotificationCenter defaultCenter] |
| 665 postNotificationName:kTabStripDragStarted |
| 666 object:nil]; |
| 667 [self beginDrag:gesture]; |
| 668 break; |
| 669 case UIGestureRecognizerStateChanged: |
| 670 [self continueDrag:gesture]; |
| 671 break; |
| 672 case UIGestureRecognizerStateEnded: |
| 673 [self endDrag:gesture]; |
| 674 [[NSNotificationCenter defaultCenter] |
| 675 postNotificationName:kTabStripDragEnded |
| 676 object:nil]; |
| 677 break; |
| 678 case UIGestureRecognizerStateCancelled: |
| 679 [self cancelDrag:gesture]; |
| 680 [[NSNotificationCenter defaultCenter] |
| 681 postNotificationName:kTabStripDragEnded |
| 682 object:nil]; |
| 683 break; |
| 684 default: |
| 685 NOTREACHED(); |
| 686 } |
| 687 } |
| 688 |
| 689 - (NSUInteger)indexForModelIndex:(NSUInteger)modelIndex { |
| 690 NSUInteger index = modelIndex; |
| 691 NSUInteger i = 0; |
| 692 for (TabView* tab in _tabArray.get()) { |
| 693 if ([_closingTabs containsObject:tab]) |
| 694 ++index; |
| 695 |
| 696 if (i == index) |
| 697 break; |
| 698 |
| 699 ++i; |
| 700 } |
| 701 |
| 702 DCHECK_GE(index, modelIndex); |
| 703 return index; |
| 704 } |
| 705 |
| 706 - (NSUInteger)modelIndexForIndex:(NSUInteger)index { |
| 707 NSUInteger modelIndex = 0; |
| 708 NSUInteger arrayIndex = 0; |
| 709 for (TabView* tab in _tabArray.get()) { |
| 710 if (arrayIndex == index) { |
| 711 if ([_closingTabs containsObject:tab]) |
| 712 return NSNotFound; |
| 713 return modelIndex; |
| 714 } |
| 715 |
| 716 if (![_closingTabs containsObject:tab]) |
| 717 ++modelIndex; |
| 718 |
| 719 ++arrayIndex; |
| 720 } |
| 721 |
| 722 return NSNotFound; |
| 723 } |
| 724 |
| 725 - (NSUInteger)modelIndexForTabView:(TabView*)view { |
| 726 return [self modelIndexForIndex:[_tabArray indexOfObject:view]]; |
| 727 } |
| 728 |
| 729 #pragma mark - |
| 730 #pragma mark Tab Drag and Drop methods |
| 731 |
| 732 - (void)beginDrag:(UILongPressGestureRecognizer*)gesture { |
| 733 DCHECK([[gesture view] isKindOfClass:[TabView class]]); |
| 734 TabView* view = (TabView*)[gesture view]; |
| 735 |
| 736 // Sanity checks. |
| 737 NSUInteger index = [self modelIndexForTabView:view]; |
| 738 DCHECK_NE(NSNotFound, static_cast<NSInteger>(index)); |
| 739 if (index == NSNotFound) |
| 740 return; |
| 741 |
| 742 // Install the dimming view, hide the new tab button, and select the tab so it |
| 743 // appears highlighted. |
| 744 Tab* tab = [_tabModel tabAtIndex:index]; |
| 745 self.highlightsSelectedTab = YES; |
| 746 _buttonNewTab.hidden = YES; |
| 747 [_tabModel setCurrentTab:tab]; |
| 748 |
| 749 // Set up initial drag state. |
| 750 _lastDragLocation = [gesture locationInView:[_tabStripView superview]]; |
| 751 _isReordering = YES; |
| 752 _draggedTab.reset([view retain]); |
| 753 _placeholderGapModelIndex = [self modelIndexForTabView:_draggedTab]; |
| 754 |
| 755 // Update the autoscroll distance and timer. |
| 756 [self computeAutoscrollDistanceForTabView:_draggedTab]; |
| 757 if (_autoscrollDistance != 0) |
| 758 [self installAutoscrollTimerIfNeeded]; |
| 759 else |
| 760 [self removeAutoscrollTimer]; |
| 761 } |
| 762 |
| 763 - (void)continueDrag:(UILongPressGestureRecognizer*)gesture { |
| 764 DCHECK([[gesture view] isKindOfClass:[TabView class]]); |
| 765 TabView* view = (TabView*)[gesture view]; |
| 766 |
| 767 // Update the position of the dragged tab. |
| 768 CGPoint location = [gesture locationInView:[_tabStripView superview]]; |
| 769 CGFloat dx = location.x - _lastDragLocation.x; |
| 770 CGRect frame = [view frame]; |
| 771 frame.origin.x += dx; |
| 772 [view setFrame:frame]; |
| 773 _lastDragLocation = location; |
| 774 |
| 775 // Update the autoscroll distance and timer. |
| 776 [self computeAutoscrollDistanceForTabView:_draggedTab]; |
| 777 if (_autoscrollDistance != 0) |
| 778 [self installAutoscrollTimerIfNeeded]; |
| 779 else |
| 780 [self removeAutoscrollTimer]; |
| 781 |
| 782 [self setNeedsLayoutWithAnimation]; |
| 783 } |
| 784 |
| 785 - (void)endDrag:(UILongPressGestureRecognizer*)gesture { |
| 786 DCHECK([[gesture view] isKindOfClass:[TabView class]]); |
| 787 |
| 788 NSUInteger fromIndex = [self modelIndexForTabView:_draggedTab]; |
| 789 // TODO(rohitrao): We're seeing crashes where fromIndex is NSNotFound, |
| 790 // indicating that the dragged tab is no longer in the TabModel. This could |
| 791 // happen if a tab closed itself during a drag. Investigate this further, but |
| 792 // for now, simply test |fromIndex| before proceeding. |
| 793 if (fromIndex == NSNotFound) { |
| 794 [self resetDragState]; |
| 795 [self setNeedsLayoutWithAnimation]; |
| 796 return; |
| 797 } |
| 798 |
| 799 Tab* tab = [_tabModel tabAtIndex:fromIndex]; |
| 800 NSUInteger toIndex = _placeholderGapModelIndex; |
| 801 DCHECK_NE(NSNotFound, static_cast<NSInteger>(toIndex)); |
| 802 DCHECK_LT(toIndex, [_tabModel count]); |
| 803 |
| 804 // Reset drag state variables before notifying the model that the tab moved. |
| 805 [self resetDragState]; |
| 806 |
| 807 [_tabModel moveTab:tab toIndex:toIndex]; |
| 808 [self setNeedsLayoutWithAnimation]; |
| 809 } |
| 810 |
| 811 - (void)cancelDrag:(UILongPressGestureRecognizer*)gesture { |
| 812 DCHECK([[gesture view] isKindOfClass:[TabView class]]); |
| 813 |
| 814 // Reset drag state and trigger a relayout to moved tabs back into their |
| 815 // correct positions. |
| 816 [self resetDragState]; |
| 817 [self setNeedsLayoutWithAnimation]; |
| 818 } |
| 819 |
| 820 - (void)resetDragState { |
| 821 self.highlightsSelectedTab = NO; |
| 822 _buttonNewTab.hidden = NO; |
| 823 [self removeAutoscrollTimer]; |
| 824 |
| 825 _isReordering = NO; |
| 826 _placeholderGapModelIndex = NSNotFound; |
| 827 _draggedTab.reset(); |
| 828 } |
| 829 |
| 830 - (BOOL)isReorderingTabs { |
| 831 return _isReordering; |
| 832 } |
| 833 |
| 834 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)recognizer { |
| 835 DCHECK([recognizer isKindOfClass:[UILongPressGestureRecognizer class]]); |
| 836 |
| 837 // If a drag is already in progress, do not allow another to start. |
| 838 return ![self isReorderingTabs]; |
| 839 } |
| 840 |
| 841 #pragma mark - |
| 842 #pragma mark Autoscroll methods |
| 843 |
| 844 - (void)installAutoscrollTimerIfNeeded { |
| 845 if (_autoscrollTimer) |
| 846 return; |
| 847 |
| 848 _autoscrollTimer = |
| 849 [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) |
| 850 target:self |
| 851 selector:@selector(autoscrollTimerFired:) |
| 852 userInfo:nil |
| 853 repeats:YES]; |
| 854 } |
| 855 |
| 856 - (void)removeAutoscrollTimer { |
| 857 [_autoscrollTimer invalidate]; |
| 858 _autoscrollTimer = nil; |
| 859 } |
| 860 |
| 861 - (void)autoscrollTimerFired:(NSTimer*)timer { |
| 862 [self constrainAutoscrollDistance]; |
| 863 |
| 864 CGPoint offset = [_tabStripView contentOffset]; |
| 865 offset.x += _autoscrollDistance; |
| 866 [_tabStripView setContentOffset:offset]; |
| 867 |
| 868 // Fixed-position views need to have their frames adusted to compensate for |
| 869 // the content offset shift. These include the dragged tab, the dimming |
| 870 // view, and the new tab button. |
| 871 CGRect tabFrame = [_draggedTab frame]; |
| 872 tabFrame.origin.x += _autoscrollDistance; |
| 873 [_draggedTab setFrame:tabFrame]; |
| 874 |
| 875 CGRect dimFrame = [_dimmingView frame]; |
| 876 dimFrame.origin.x += _autoscrollDistance; |
| 877 [_dimmingView setFrame:dimFrame]; |
| 878 |
| 879 // Even though the new tab button is hidden during drag and drop, keep its |
| 880 // frame updated to prevent it from animating back into place when the drag |
| 881 // finishes. |
| 882 CGRect newTabFrame = [_buttonNewTab frame]; |
| 883 newTabFrame.origin.x += _autoscrollDistance; |
| 884 [_buttonNewTab setFrame:newTabFrame]; |
| 885 |
| 886 // TODO(rohitrao): Find a good way to re-enable the sliding over animation |
| 887 // when autoscrolling. Right now any running animations are immediately |
| 888 // stopped by the next call to autoscrollTimerFired. |
| 889 [_tabStripView setNeedsLayout]; |
| 890 } |
| 891 |
| 892 - (void)computeAutoscrollDistanceForTabView:(TabView*)view { |
| 893 CGRect scrollBounds = [_tabStripView bounds]; |
| 894 CGRect viewFrame = [view frame]; |
| 895 |
| 896 // The distance between this tab and the edge of the scroll view. |
| 897 CGFloat distanceFromEdge = |
| 898 MIN(CGRectGetMinX(viewFrame) - CGRectGetMinX(scrollBounds), |
| 899 CGRectGetMaxX(scrollBounds) - CGRectGetMaxX(viewFrame)); |
| 900 if (distanceFromEdge < 0) |
| 901 distanceFromEdge = 0; |
| 902 |
| 903 // Negative if the tab is closer to the left edge of the scroll view, positive |
| 904 // if it is closer to the right edge. |
| 905 CGFloat leftRightMultiplier = |
| 906 (CGRectGetMidX(viewFrame) < CGRectGetMidX(scrollBounds)) ? -1.0 : 1.0; |
| 907 |
| 908 // The autoscroll distance decreases linearly as the tab view gets further |
| 909 // from the edge of the scroll view. |
| 910 _autoscrollDistance = |
| 911 leftRightMultiplier * |
| 912 MAX(0.0, ceilf(kMaxAutoscrollDistance - |
| 913 distanceFromEdge / kAutoscrollDecrementWidth)); |
| 914 } |
| 915 |
| 916 - (void)constrainAutoscrollDistance { |
| 917 // Make sure autoscroll distance is not so large as to cause overscroll. |
| 918 CGPoint offset = [_tabStripView contentOffset]; |
| 919 |
| 920 // Check to make sure there is no overscroll off the right edge. |
| 921 CGFloat maxOffset = [_tabStripView contentSize].width - |
| 922 CGRectGetWidth([_tabStripView bounds]); |
| 923 if (offset.x + _autoscrollDistance > maxOffset) |
| 924 _autoscrollDistance = (maxOffset - offset.x); |
| 925 |
| 926 // Perform the left edge check after the right edge check, to prevent |
| 927 // right-justifying the tabs when there is no overflow. |
| 928 if (offset.x + _autoscrollDistance < 0) |
| 929 _autoscrollDistance = -offset.x; |
| 930 } |
| 931 |
| 932 #pragma mark - |
| 933 #pragma mark TabStripModelObserver methods |
| 934 |
| 935 // Observer method. |
| 936 - (void)tabModel:(TabModel*)model |
| 937 didInsertTab:(Tab*)tab |
| 938 atIndex:(NSUInteger)modelIndex |
| 939 inForeground:(BOOL)fg { |
| 940 TabView* view = [self tabViewForTab:tab isSelected:fg]; |
| 941 [_tabArray insertObject:view atIndex:[self indexForModelIndex:modelIndex]]; |
| 942 [[self tabStripView] addSubview:view]; |
| 943 |
| 944 [self updateContentSizeAndRepositionViews]; |
| 945 [self setNeedsLayoutWithAnimation]; |
| 946 [self updateContentOffsetForTabIndex:modelIndex]; |
| 947 } |
| 948 |
| 949 // Observer method. |
| 950 - (void)tabModel:(TabModel*)model |
| 951 didRemoveTab:(Tab*)tab |
| 952 atIndex:(NSUInteger)modelIndex { |
| 953 // Keep the actual view around while it is animating out. Once the animation |
| 954 // is done, remove the view. |
| 955 NSUInteger index = [self indexForModelIndex:modelIndex]; |
| 956 TabView* view = [_tabArray objectAtIndex:index]; |
| 957 [_closingTabs addObject:view]; |
| 958 _targetFrames.RemoveFrame(view); |
| 959 |
| 960 // Adjust the content size now that the tab has been removed from the model. |
| 961 [self updateContentSizeAndRepositionViews]; |
| 962 |
| 963 // Signal the FullscreenController that the toolbar needs to stay on |
| 964 // screen for a bit, so the animation is visible. |
| 965 [[NSNotificationCenter defaultCenter] |
| 966 postNotificationName:kWillStartTabStripTabAnimation |
| 967 object:nil]; |
| 968 |
| 969 // Leave the view where it is horizontally and animate it downwards out of |
| 970 // sight. |
| 971 CGRect frame = [view frame]; |
| 972 frame = CGRectOffset(frame, 0, CGRectGetHeight(frame)); |
| 973 [UIView animateWithDuration:kTabAnimationDuration |
| 974 animations:^{ |
| 975 [view setFrame:frame]; |
| 976 } |
| 977 completion:^(BOOL finished) { |
| 978 [view removeFromSuperview]; |
| 979 [_tabArray removeObject:view]; |
| 980 [_closingTabs removeObject:view]; |
| 981 }]; |
| 982 |
| 983 [self setNeedsLayoutWithAnimation]; |
| 984 } |
| 985 |
| 986 // Observer method. |
| 987 - (void)tabModel:(TabModel*)model |
| 988 didMoveTab:(Tab*)tab |
| 989 fromIndex:(NSUInteger)fromIndex |
| 990 toIndex:(NSUInteger)toIndex { |
| 991 DCHECK(!_isReordering); |
| 992 |
| 993 // Reorder the objects in _tabArray to keep in sync with the model ordering. |
| 994 NSUInteger arrayIndex = [self indexForModelIndex:fromIndex]; |
| 995 base::scoped_nsobject<TabView> view( |
| 996 [[_tabArray objectAtIndex:arrayIndex] retain]); |
| 997 [_tabArray removeObject:view]; |
| 998 [_tabArray insertObject:view atIndex:toIndex]; |
| 999 [self setNeedsLayoutWithAnimation]; |
| 1000 } |
| 1001 |
| 1002 // Observer method. |
| 1003 - (void)tabModel:(TabModel*)model |
| 1004 didChangeActiveTab:(Tab*)newTab |
| 1005 previousTab:(Tab*)previousTab |
| 1006 atIndex:(NSUInteger)modelIndex { |
| 1007 for (TabView* view in _tabArray.get()) { |
| 1008 [view setSelected:NO]; |
| 1009 } |
| 1010 |
| 1011 NSUInteger index = [self indexForModelIndex:modelIndex]; |
| 1012 TabView* activeView = [_tabArray objectAtIndex:index]; |
| 1013 [activeView setSelected:YES]; |
| 1014 |
| 1015 // No need to animate this change, as selecting a new tab simply changes the |
| 1016 // z-ordering of the TabViews. If a new tab was selected as a result of a tab |
| 1017 // closure, then the animated layout has already been scheduled. |
| 1018 [_tabStripView setNeedsLayout]; |
| 1019 } |
| 1020 |
| 1021 // Observer method. |
| 1022 - (void)tabModel:(TabModel*)model didChangeTab:(Tab*)tab { |
| 1023 NSUInteger modelIndex = [_tabModel indexOfTab:tab]; |
| 1024 if (modelIndex == NSNotFound) { |
| 1025 DCHECK(false) << "Received notification for a Tab that is not contained in " |
| 1026 << "the TabModel"; |
| 1027 return; |
| 1028 } |
| 1029 NSUInteger index = [self indexForModelIndex:modelIndex]; |
| 1030 TabView* view = [_tabArray objectAtIndex:index]; |
| 1031 [view setTitle:tab.title]; |
| 1032 [view setFavicon:[tab favicon]]; |
| 1033 if (tab.webState->IsLoading()) |
| 1034 [view startProgressSpinner]; |
| 1035 else |
| 1036 [view stopProgressSpinner]; |
| 1037 [view setNeedsDisplay]; |
| 1038 } |
| 1039 |
| 1040 // Observer method. |
| 1041 - (void)tabModel:(TabModel*)model |
| 1042 didReplaceTab:(Tab*)oldTab |
| 1043 withTab:(Tab*)newTab |
| 1044 atIndex:(NSUInteger)index { |
| 1045 // TabViews do not hold references to their parent Tabs, so it's safe to treat |
| 1046 // this as a tab change rather than a tab replace. |
| 1047 [self tabModel:model didChangeTab:newTab]; |
| 1048 } |
| 1049 |
| 1050 #pragma mark - |
| 1051 #pragma mark Views and Layout |
| 1052 |
| 1053 - (void)installModeToggleButton { |
| 1054 // Add the mode toggle button view. |
| 1055 DCHECK(!_modeToggleButton); |
| 1056 UIImage* toggleIcon = nil; |
| 1057 int toggleIdsAccessibilityLabel; |
| 1058 NSString* toggleEnglishUiAutomationName; |
| 1059 if (_style == TabStrip::kStyleDark) { |
| 1060 toggleIcon = [UIImage imageNamed:@"tabstrip_switch"]; |
| 1061 toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_ENTER_INCOGNITO; |
| 1062 toggleEnglishUiAutomationName = @"Enter Incognito* Mode"; |
| 1063 } else { |
| 1064 toggleIcon = [UIImage imageNamed:@"tabstrip_incognito_switch"]; |
| 1065 toggleIdsAccessibilityLabel = IDS_IOS_SWITCH_BROWSER_MODE_LEAVE_INCOGNITO; |
| 1066 toggleEnglishUiAutomationName = @"Leave Incognito* Mode"; |
| 1067 } |
| 1068 const CGFloat tabStripHeight = _view.frame.size.height; |
| 1069 CGRect buttonFrame = |
| 1070 CGRectMake(CGRectGetMaxX(_view.frame) - kModeToggleButtonWidth, 0.0, |
| 1071 kModeToggleButtonWidth, tabStripHeight); |
| 1072 _modeToggleButton = [UIButton buttonWithType:UIButtonTypeCustom]; |
| 1073 _modeToggleButton.frame = buttonFrame; |
| 1074 [_modeToggleButton setImageEdgeInsets:UIEdgeInsetsMake(7, 5, 7, 5)]; |
| 1075 _modeToggleButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin; |
| 1076 _modeToggleButton.backgroundColor = [UIColor clearColor]; |
| 1077 [_modeToggleButton setImage:toggleIcon forState:UIControlStateNormal]; |
| 1078 // Set target/action to bubble up with command id as tag. |
| 1079 [_modeToggleButton addTarget:nil |
| 1080 action:@selector(chromeExecuteCommand:) |
| 1081 forControlEvents:UIControlEventTouchUpInside]; |
| 1082 [_modeToggleButton setTag:IDC_SWITCH_BROWSER_MODES]; |
| 1083 [_modeToggleButton addTarget:self |
| 1084 action:@selector(recordUserMetrics:) |
| 1085 forControlEvents:UIControlEventTouchUpInside]; |
| 1086 |
| 1087 SetA11yLabelAndUiAutomationName(_modeToggleButton, |
| 1088 toggleIdsAccessibilityLabel, |
| 1089 toggleEnglishUiAutomationName); |
| 1090 [_view addSubview:_modeToggleButton]; |
| 1091 // Shrink the scroll view. |
| 1092 [self updateScrollViewFrameForToggleButton]; |
| 1093 } |
| 1094 |
| 1095 - (void)removeModeToggleButton { |
| 1096 // Remove the button view. |
| 1097 DCHECK(_modeToggleButton); |
| 1098 [_modeToggleButton removeFromSuperview]; |
| 1099 _modeToggleButton = nil; |
| 1100 // Extend the scroll view. |
| 1101 [self updateScrollViewFrameForToggleButton]; |
| 1102 } |
| 1103 |
| 1104 - (CGFloat)tabStripVisibleSpace { |
| 1105 CGFloat availableSpace = CGRectGetWidth([_tabStripView bounds]) - |
| 1106 CGRectGetWidth([_buttonNewTab frame]) + |
| 1107 kNewTabOverlap; |
| 1108 if (IsCompactTablet()) { |
| 1109 if ([self hasModeToggleSwitch]) |
| 1110 availableSpace -= kNewTabRightPadding + kModeToggleButtonWidth; |
| 1111 else |
| 1112 availableSpace -= kNewTabRightPadding; |
| 1113 } else { |
| 1114 if (![self hasModeToggleSwitch]) |
| 1115 availableSpace -= kNewTabRightPadding; |
| 1116 } |
| 1117 return availableSpace; |
| 1118 } |
| 1119 |
| 1120 - (void)installTabSwitcherToggleButton { |
| 1121 // Add the mode toggle button view. |
| 1122 DCHECK(!_tabSwitcherToggleButton); |
| 1123 UIImage* tabSwitcherToggleIcon = |
| 1124 [UIImage imageNamed:@"tabswitcher_tab_switcher_button"]; |
| 1125 tabSwitcherToggleIcon = [tabSwitcherToggleIcon |
| 1126 imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; |
| 1127 int tabSwitcherToggleIdsAccessibilityLabel = |
| 1128 IDS_IOS_TAB_STRIP_ENTER_TAB_SWITCHER; |
| 1129 NSString* tabSwitcherToggleEnglishUiAutomationName = @"Enter Tab Switcher"; |
| 1130 const CGFloat tabStripHeight = _view.frame.size.height; |
| 1131 CGRect buttonFrame = |
| 1132 CGRectMake(CGRectGetMaxX(_view.frame) - kTabSwitcherToggleButtonWidth, |
| 1133 0.0, kTabSwitcherToggleButtonWidth, tabStripHeight); |
| 1134 _tabSwitcherToggleButton = [UIButton buttonWithType:UIButtonTypeCustom]; |
| 1135 [_tabSwitcherToggleButton setTintColor:[UIColor whiteColor]]; |
| 1136 _tabSwitcherToggleButton.frame = buttonFrame; |
| 1137 [_tabSwitcherToggleButton setContentMode:UIViewContentModeCenter]; |
| 1138 _tabSwitcherToggleButton.autoresizingMask = |
| 1139 UIViewAutoresizingFlexibleLeftMargin; |
| 1140 _tabSwitcherToggleButton.backgroundColor = [UIColor clearColor]; |
| 1141 _tabSwitcherToggleButton.exclusiveTouch = YES; |
| 1142 [_tabSwitcherToggleButton setImage:tabSwitcherToggleIcon |
| 1143 forState:UIControlStateNormal]; |
| 1144 // Set target/action to bubble up with command id as tag. |
| 1145 [_tabSwitcherToggleButton addTarget:nil |
| 1146 action:@selector(chromeExecuteCommand:) |
| 1147 forControlEvents:UIControlEventTouchUpInside]; |
| 1148 [_tabSwitcherToggleButton setTag:IDC_TOGGLE_TAB_SWITCHER]; |
| 1149 [_tabSwitcherToggleButton addTarget:self |
| 1150 action:@selector(recordUserMetrics:) |
| 1151 forControlEvents:UIControlEventTouchUpInside]; |
| 1152 |
| 1153 SetA11yLabelAndUiAutomationName(_tabSwitcherToggleButton, |
| 1154 tabSwitcherToggleIdsAccessibilityLabel, |
| 1155 tabSwitcherToggleEnglishUiAutomationName); |
| 1156 [_view addSubview:_tabSwitcherToggleButton]; |
| 1157 // Shrink the scroll view. |
| 1158 [self updateScrollViewFrameForToggleButton]; |
| 1159 } |
| 1160 |
| 1161 - (void)removeTabSwitcherToggleButton { |
| 1162 // Remove the button view. |
| 1163 DCHECK(_tabSwitcherToggleButton); |
| 1164 [_tabSwitcherToggleButton removeFromSuperview]; |
| 1165 _tabSwitcherToggleButton = nil; |
| 1166 // Extend the scroll view. |
| 1167 [self updateScrollViewFrameForToggleButton]; |
| 1168 } |
| 1169 |
| 1170 - (void)updateContentSizeAndRepositionViews { |
| 1171 // TODO(rohitrao): The following lines are duplicated in |
| 1172 // layoutTabStripSubviews. Find a way to consolidate this logic. |
| 1173 const NSUInteger tabCount = [_tabArray count] - [_closingTabs count]; |
| 1174 if (!tabCount) |
| 1175 return; |
| 1176 const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]); |
| 1177 CGFloat visibleSpace = [self tabStripVisibleSpace]; |
| 1178 _currentTabWidth = |
| 1179 (visibleSpace + ([self tabOverlap] * (tabCount - 1))) / tabCount; |
| 1180 _currentTabWidth = MIN(_currentTabWidth, kMaxTabWidth); |
| 1181 _currentTabWidth = MAX(_currentTabWidth, [self minTabWidth]); |
| 1182 |
| 1183 // Set the content size to be large enough to contain all the tabs at the |
| 1184 // desired width, with the standard overlap, plus the new tab button. |
| 1185 CGSize contentSize = CGSizeMake( |
| 1186 _currentTabWidth * tabCount - ([self tabOverlap] * (tabCount - 1)) + |
| 1187 CGRectGetWidth([_buttonNewTab frame]) - kNewTabOverlap, |
| 1188 tabHeight); |
| 1189 if (CGSizeEqualToSize([_tabStripView contentSize], contentSize)) |
| 1190 return; |
| 1191 |
| 1192 // Background: The scroll view might change the content offset when updating |
| 1193 // the content size. This can happen when the old content offset would result |
| 1194 // in an overscroll at the new content size. (Note that the content offset |
| 1195 // will never change if the content size is growing.) |
| 1196 // |
| 1197 // To handle this without making views appear to jump, shift all of the |
| 1198 // subviews by an amount equal to the size change. This effectively places |
| 1199 // the subviews back where they were before the change, in terms of screen |
| 1200 // coordinates. |
| 1201 CGPoint oldOffset = [_tabStripView contentOffset]; |
| 1202 [_tabStripView setContentSize:contentSize]; |
| 1203 |
| 1204 CGFloat dx = [_tabStripView contentOffset].x - oldOffset.x; |
| 1205 for (UIView* view in [_tabStripView subviews]) { |
| 1206 CGRect frame = [view frame]; |
| 1207 frame.origin.x += dx; |
| 1208 [view setFrame:frame]; |
| 1209 _targetFrames.AddFrame(view, frame); |
| 1210 } |
| 1211 } |
| 1212 |
| 1213 - (CGRect)scrollViewFrameForTab:(TabView*)view { |
| 1214 NSUInteger index = [self modelIndexForTabView:view]; |
| 1215 |
| 1216 CGRect frame = [view frame]; |
| 1217 frame.origin.x = |
| 1218 (_currentTabWidth * index) - ([self tabOverlap] * (index - 1)); |
| 1219 return frame; |
| 1220 } |
| 1221 |
| 1222 - (CGRect)calculateVisibleFrameForFrame:(CGRect)frame |
| 1223 whenUnderFrame:(CGRect)frameOnTop { |
| 1224 CGFloat minX = CGRectGetMinX(frame); |
| 1225 CGFloat maxX = CGRectGetMaxX(frame); |
| 1226 |
| 1227 if (CGRectGetMinX(frame) < CGRectGetMinX(frameOnTop)) |
| 1228 maxX = CGRectGetMinX(frameOnTop); |
| 1229 else |
| 1230 minX = CGRectGetMaxX(frameOnTop); |
| 1231 |
| 1232 frame.origin.x = minX; |
| 1233 frame.size.width = maxX - minX; |
| 1234 return frame; |
| 1235 } |
| 1236 |
| 1237 #pragma mark - |
| 1238 #pragma mark - compact layout |
| 1239 |
| 1240 - (NSUInteger)maxNumCollapsedTabs { |
| 1241 return IsCompactTablet() ? kMaxNumCollapsedTabsForCompactLayout |
| 1242 : kMaxNumCollapsedTabs; |
| 1243 } |
| 1244 |
| 1245 - (CGFloat)tabOverlap { |
| 1246 return IsCompactTablet() ? kTabOverlapForCompactLayout : kTabOverlap; |
| 1247 } |
| 1248 |
| 1249 - (CGFloat)minTabWidth { |
| 1250 return IsCompactTablet() ? kMinTabWidthForCompactLayout : kMinTabWidth; |
| 1251 } |
| 1252 |
| 1253 - (void)updateContentOffsetForTabIndex:(NSUInteger)tabIndex { |
| 1254 DCHECK_NE(NSNotFound, static_cast<NSInteger>(tabIndex)); |
| 1255 |
| 1256 if (IsCompactTablet()) { |
| 1257 if (tabIndex == [_tabArray count] - 1) { |
| 1258 const CGFloat tabStripAvailableSpace = |
| 1259 _tabStripView.frame.size.width - _tabStripView.contentInset.right; |
| 1260 if (_tabStripView.contentSize.width > tabStripAvailableSpace) { |
| 1261 CGFloat scrollToPoint = |
| 1262 _tabStripView.contentSize.width - tabStripAvailableSpace; |
| 1263 [_tabStripView setContentOffset:CGPointMake(scrollToPoint, 0) |
| 1264 animated:YES]; |
| 1265 } |
| 1266 } else { |
| 1267 TabView* tabView = [_tabArray objectAtIndex:tabIndex]; |
| 1268 CGRect scrollRect = |
| 1269 CGRectInset(tabView.frame, -_tabStripView.contentInset.right, 0); |
| 1270 if (tabView) |
| 1271 [_tabStripView scrollRectToVisible:scrollRect animated:YES]; |
| 1272 } |
| 1273 } |
| 1274 } |
| 1275 |
| 1276 - (void)updateScrollViewFrameForToggleButton { |
| 1277 CGRect tabFrame = _tabStripView.frame; |
| 1278 tabFrame.size.width = _view.bounds.size.width; |
| 1279 if (!IsCompactTablet()) { |
| 1280 if (_modeToggleButton) |
| 1281 tabFrame.size.width -= kModeToggleButtonWidth; |
| 1282 if (_tabSwitcherToggleButton) |
| 1283 tabFrame.size.width -= kTabSwitcherToggleButtonWidth; |
| 1284 _tabStripView.contentInset = UIEdgeInsetsZero; |
| 1285 [_toggleButtonBackgroundView setHidden:YES]; |
| 1286 } else { |
| 1287 if (!_toggleButtonBackgroundView) { |
| 1288 _toggleButtonBackgroundView.reset([[UIImageView alloc] init]); |
| 1289 const CGFloat tabStripHeight = _view.frame.size.height; |
| 1290 const CGRect backgroundViewFrame = CGRectMake( |
| 1291 CGRectGetMaxX(_view.frame) - kModeToggleButtonBackgroundWidth, 0.0, |
| 1292 kModeToggleButtonBackgroundWidth, tabStripHeight); |
| 1293 [_toggleButtonBackgroundView setFrame:backgroundViewFrame]; |
| 1294 [_toggleButtonBackgroundView |
| 1295 setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin]; |
| 1296 UIImage* backgroundToggleImage = |
| 1297 [UIImage imageNamed:@"tabstrip_toggle_button_gradient"]; |
| 1298 [_toggleButtonBackgroundView setImage:backgroundToggleImage]; |
| 1299 [_view addSubview:_toggleButtonBackgroundView]; |
| 1300 } |
| 1301 const BOOL hasModeToggleButton = |
| 1302 _modeToggleButton || _tabSwitcherToggleButton; |
| 1303 [_toggleButtonBackgroundView setHidden:!hasModeToggleButton]; |
| 1304 if (!hasModeToggleButton) |
| 1305 _tabStripView.contentInset = UIEdgeInsetsZero; |
| 1306 if (_modeToggleButton) { |
| 1307 _tabStripView.contentInset = |
| 1308 UIEdgeInsetsMake(0, 0, 0, kModeToggleButtonWidth); |
| 1309 [_view bringSubviewToFront:_modeToggleButton]; |
| 1310 } |
| 1311 if (_tabSwitcherToggleButton) { |
| 1312 _tabStripView.contentInset = |
| 1313 UIEdgeInsetsMake(0, 0, 0, kTabSwitcherToggleButtonWidth); |
| 1314 [_view bringSubviewToFront:_tabSwitcherToggleButton]; |
| 1315 } |
| 1316 } |
| 1317 [_tabStripView setFrame:tabFrame]; |
| 1318 } |
| 1319 |
| 1320 #pragma mark - TabStripViewLayoutDelegate |
| 1321 |
| 1322 // Creates TabViews for each Tab in the TabModel and positions them in the |
| 1323 // correct location onscreen. |
| 1324 - (void)layoutTabStripSubviews { |
| 1325 const NSUInteger tabCount = [_tabArray count] - [_closingTabs count]; |
| 1326 if (!tabCount) |
| 1327 return; |
| 1328 BOOL animate = _animateLayout; |
| 1329 _animateLayout = NO; |
| 1330 // Disable the animation if the tab count is changing from 0 to 1. |
| 1331 if (tabCount == 1 && [_closingTabs count] == 0) |
| 1332 animate = NO; |
| 1333 |
| 1334 const CGFloat tabHeight = CGRectGetHeight([_tabStripView bounds]); |
| 1335 |
| 1336 // In compact layout mode the space used to layout the tabs is not |
| 1337 // constrained and uses the whole scroll view content size width. In regular |
| 1338 // layout mode the available space is constrained to the visible space. |
| 1339 CGFloat availableSpace = IsCompactTablet() ? _tabStripView.contentSize.width |
| 1340 : [self tabStripVisibleSpace]; |
| 1341 |
| 1342 // The array and model indexes of the selected tab. |
| 1343 NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]]; |
| 1344 NSUInteger selectedArrayIndex = [self indexForModelIndex:selectedModelIndex]; |
| 1345 |
| 1346 // This method lays out tabs in two coordinate systems. The first, the |
| 1347 // "virtual" coordinate system, is a system rooted at x=0 that contains all |
| 1348 // the tabs laid out as if the tabstrip was infinitely long. In this system, |
| 1349 // |virtualMinX| contains the starting X coordinate of the next tab to be |
| 1350 // placed and |virtualMaxX| contains the maximum X coordinate of the last tab |
| 1351 // to be placed. |
| 1352 // |
| 1353 // The scroll view's content area is sized to be large enough to hold all the |
| 1354 // tabs with proper overlap, but the viewport is set to only show a part of |
| 1355 // the content area. The specific part that is shown is given by the scroll |
| 1356 // view's contentOffset. |
| 1357 // |
| 1358 // To layout tabs, first calculate where the tab should be in the "virtual" |
| 1359 // coordinate system. This gives the frame of the tab assuming the tabstrip |
| 1360 // was large enough to hold all tabs without needing to overflow. Then, |
| 1361 // adjust the tab's virtual frame to move it onscreen. This gives the tab's |
| 1362 // real frame. |
| 1363 CGFloat virtualMinX = 0; |
| 1364 CGFloat virtualMaxX = 0; |
| 1365 CGFloat offset = IsCompactTablet() ? 0 : [_tabStripView contentOffset].x; |
| 1366 |
| 1367 // Keeps track of which tabs need to be animated. Using an autoreleased array |
| 1368 // instead of scoped_nsobject because scoped_nsobject doesn't seem to work |
| 1369 // well with blocks. |
| 1370 NSMutableArray* tabsNeedingAnimation = |
| 1371 [NSMutableArray arrayWithCapacity:tabCount]; |
| 1372 |
| 1373 CGRect dragFrame = [_draggedTab frame]; |
| 1374 |
| 1375 TabView* previousTabView = nil; |
| 1376 CGRect previousTabFrame = CGRectZero; |
| 1377 BOOL hasPlaceholderGap = NO; |
| 1378 for (NSUInteger arrayIndex = 0; arrayIndex < [_tabArray count]; |
| 1379 ++arrayIndex) { |
| 1380 TabView* view = (TabView*)[_tabArray objectAtIndex:arrayIndex]; |
| 1381 |
| 1382 // Arrange the tabs in a V going backwards from the selected tab. This |
| 1383 // differs from desktop in order to make the tab overflow behavior work (on |
| 1384 // desktop, the tabs are arranged going backwards from left to right, with |
| 1385 // the selected tab above all others). |
| 1386 // |
| 1387 // When reordering, use slightly different logic. Instead of a V based on |
| 1388 // the model indexes of the tabs, the V fans out from the placeholder gap, |
| 1389 // which is visually where the dragged tab is. In reordering mode, the tabs |
| 1390 // are not necessarily z-ordered according to their model indexes, because |
| 1391 // they are not necessarily drawn in the spot dictated by their current |
| 1392 // model index. |
| 1393 BOOL isSelectedTab = (arrayIndex == selectedArrayIndex); |
| 1394 BOOL zOrderedAbove = |
| 1395 _isReordering ? !hasPlaceholderGap : (arrayIndex <= selectedArrayIndex); |
| 1396 |
| 1397 if (isSelectedTab) { |
| 1398 // Order matters. The dimming view needs to end up behind the selected |
| 1399 // tab, so it's brought to the front first, followed by the tab. |
| 1400 [_tabStripView bringSubviewToFront:_dimmingView]; |
| 1401 [_tabStripView bringSubviewToFront:view]; |
| 1402 } else if (zOrderedAbove) { |
| 1403 // If the current tab comes after the selected tab in the model but still |
| 1404 // needs to be z-ordered above, place it relative to the dimming view, |
| 1405 // rather than blindly bringing it to the front. This can only happen in |
| 1406 // reordering mode. |
| 1407 if (arrayIndex > selectedArrayIndex) { |
| 1408 DCHECK(_isReordering); |
| 1409 [_tabStripView insertSubview:view belowSubview:_dimmingView]; |
| 1410 } else { |
| 1411 [_tabStripView bringSubviewToFront:view]; |
| 1412 } |
| 1413 } else { |
| 1414 [_tabStripView sendSubviewToBack:view]; |
| 1415 } |
| 1416 |
| 1417 // Ignore closing tabs when repositioning. |
| 1418 NSUInteger currentModelIndex = [self modelIndexForIndex:arrayIndex]; |
| 1419 if (currentModelIndex == NSNotFound) |
| 1420 continue; |
| 1421 |
| 1422 // Ignore the tab that is currently being dragged. |
| 1423 if (_isReordering && view == _draggedTab) |
| 1424 continue; |
| 1425 |
| 1426 // |realMinX| is the furthest left the tab can be, in real coordinates. |
| 1427 // This is computed by counting the number of possible collapsed tabs that |
| 1428 // can be to the left of this tab, then multiplying that count by the size |
| 1429 // of a collapsed tab. |
| 1430 // |
| 1431 // There can be up to |[self maxNumCollapsedTabs]| to the left of the |
| 1432 // selected |
| 1433 // tab, and the same number to the right of the selected tab. |
| 1434 NSUInteger numPossibleCollapsedTabsToLeft = |
| 1435 std::min(currentModelIndex, [self maxNumCollapsedTabs]); |
| 1436 if (currentModelIndex > selectedModelIndex) { |
| 1437 // If this tab is to the right of the selected tab, also include the |
| 1438 // number of collapsed tabs on the right of the selected tab. |
| 1439 numPossibleCollapsedTabsToLeft = |
| 1440 std::min(selectedModelIndex, [self maxNumCollapsedTabs]) + |
| 1441 std::min(currentModelIndex - selectedModelIndex, |
| 1442 [self maxNumCollapsedTabs]); |
| 1443 } |
| 1444 CGFloat realMinX = |
| 1445 offset + (numPossibleCollapsedTabsToLeft * kCollapsedTabOverlap); |
| 1446 |
| 1447 // |realMaxX| is the furthest right the tab can be, in real coordinates. |
| 1448 NSUInteger numPossibleCollapsedTabsToRight = |
| 1449 std::min(tabCount - currentModelIndex - 1, [self maxNumCollapsedTabs]); |
| 1450 if (currentModelIndex < selectedModelIndex) { |
| 1451 // If this tab is to the left of the selected tab, also include the |
| 1452 // number of collapsed tabs on the left of the selected tab. |
| 1453 numPossibleCollapsedTabsToRight = |
| 1454 std::min(tabCount - selectedModelIndex - 1, |
| 1455 [self maxNumCollapsedTabs]) + |
| 1456 std::min(selectedModelIndex - currentModelIndex, |
| 1457 [self maxNumCollapsedTabs]); |
| 1458 } |
| 1459 CGFloat realMaxX = offset + availableSpace - |
| 1460 (numPossibleCollapsedTabsToRight * kCollapsedTabOverlap); |
| 1461 |
| 1462 // If this tab is to the right of the currently dragged tab, add a |
| 1463 // placeholder gap. |
| 1464 if (_isReordering && !hasPlaceholderGap && |
| 1465 CGRectGetMinX(dragFrame) < virtualMinX + (_currentTabWidth / 2.0)) { |
| 1466 virtualMinX += _currentTabWidth - [self tabOverlap]; |
| 1467 hasPlaceholderGap = YES; |
| 1468 |
| 1469 // Fix up the z-ordering of the current view. It was placed assuming that |
| 1470 // the placeholder gap hasn't been hit yet. |
| 1471 [_tabStripView sendSubviewToBack:view]; |
| 1472 |
| 1473 // The model index of the placeholder gap is equal to the model index of |
| 1474 // the shifted tab, adjusted for the presence of the dragged tab. This |
| 1475 // value will be used as the new model index for the dragged tab when it |
| 1476 // is dropped. |
| 1477 _placeholderGapModelIndex = currentModelIndex; |
| 1478 if ([self modelIndexForTabView:_draggedTab] < currentModelIndex) |
| 1479 _placeholderGapModelIndex--; |
| 1480 } |
| 1481 |
| 1482 // |tabX| stores where we are placing the tab, in real coordinates. Start |
| 1483 // by trying to place the tab at the computed |virtualMinX|, then constrain |
| 1484 // that by |realMinX| and |realMaxX|. |
| 1485 CGFloat tabX = MAX(virtualMinX, realMinX); |
| 1486 if (tabX + _currentTabWidth > realMaxX) |
| 1487 tabX = realMaxX - _currentTabWidth; |
| 1488 |
| 1489 CGRect frame = CGRectMake(AlignValueToPixel(tabX), 0, |
| 1490 AlignValueToPixel(_currentTabWidth), tabHeight); |
| 1491 virtualMinX += (_currentTabWidth - [self tabOverlap]); |
| 1492 virtualMaxX = CGRectGetMaxX(frame); |
| 1493 |
| 1494 // TODO(rohitrao): Temporarily disabled this logic as it does not play well with |
| 1495 // tab scrolling. |
| 1496 #if 0 |
| 1497 // If this tab is completely hidden by the previous tab, remove it from the |
| 1498 // scroll view. Otherwise, add it back in if needed. |
| 1499 if (selectedArrayIndex != arrayIndex && |
| 1500 CGRectEqualToRect(frame, previousTabFrame)) { |
| 1501 [view removeFromSuperview]; |
| 1502 } else if (![view superview]) { |
| 1503 // TODO(rohitrao): Find a way to move the z-ordering code from the top of |
| 1504 // the function to down here, so we can consolidate the logic. |
| 1505 [_tabStripView insertSubview:view atIndex: |
| 1506 (zOrderedAbove ? [[_tabStripView subviews] count] : 0)]; |
| 1507 } |
| 1508 #endif |
| 1509 |
| 1510 // Update the tab's collapsed state based on overlap with the previous tab. |
| 1511 if (zOrderedAbove) { |
| 1512 CGRect visibleRect = [self calculateVisibleFrameForFrame:previousTabFrame |
| 1513 whenUnderFrame:frame]; |
| 1514 BOOL collapsed = |
| 1515 CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold; |
| 1516 [previousTabView setCollapsed:collapsed]; |
| 1517 |
| 1518 // The selected tab can never be collapsed, since no tab will ever be |
| 1519 // z-ordered above it to obscure it. |
| 1520 if (isSelectedTab) |
| 1521 [view setCollapsed:NO]; |
| 1522 } else { |
| 1523 CGRect visibleRect = |
| 1524 [self calculateVisibleFrameForFrame:frame |
| 1525 whenUnderFrame:previousTabFrame]; |
| 1526 BOOL collapsed = |
| 1527 CGRectGetWidth(visibleRect) < kCollapsedTabWidthThreshold; |
| 1528 [view setCollapsed:collapsed]; |
| 1529 } |
| 1530 |
| 1531 if (animate) { |
| 1532 if (!CGRectEqualToRect(frame, [view frame])) |
| 1533 [tabsNeedingAnimation addObject:view]; |
| 1534 } else { |
| 1535 if (!CGRectEqualToRect(frame, [view frame])) |
| 1536 [view setFrame:frame]; |
| 1537 } |
| 1538 |
| 1539 // Throw the target frame into the dictionary so we can animate it later. |
| 1540 _targetFrames.AddFrame(view, frame); |
| 1541 |
| 1542 // Ensure the tab is visible. |
| 1543 if ([view isHidden]) { |
| 1544 if (animate) { |
| 1545 // If it is a new tab, and animation is enabled, make it a submarine tab |
| 1546 // by immediately positioning it under the tabstrip. |
| 1547 CGRect submarineFrame = CGRectOffset(frame, 0, CGRectGetHeight(frame)); |
| 1548 [view setFrame:submarineFrame]; |
| 1549 } |
| 1550 [view setHidden:NO]; |
| 1551 } |
| 1552 |
| 1553 previousTabView = view; |
| 1554 previousTabFrame = frame; |
| 1555 } |
| 1556 |
| 1557 // If in reordering mode and there was no placeholder gap, then the dragged |
| 1558 // tab must be all the way to the right of the other tabs. Set the |
| 1559 // _placeholderGapModelIndex accordingly. |
| 1560 if (!hasPlaceholderGap && _isReordering) |
| 1561 _placeholderGapModelIndex = [_tabModel count] - 1; |
| 1562 |
| 1563 // Do not move the new tab button if it is hidden. This will lead to better |
| 1564 // animations when exiting drag and drop mode, as the new tab button will not |
| 1565 // have moved during the drag. |
| 1566 CGRect newTabFrame = [_buttonNewTab frame]; |
| 1567 BOOL moveNewTab = |
| 1568 (newTabFrame.origin.x != virtualMaxX) && !_buttonNewTab.hidden; |
| 1569 newTabFrame.origin = CGPointMake(virtualMaxX - kNewTabOverlap, 0); |
| 1570 if (!animate && moveNewTab) |
| 1571 [_buttonNewTab setFrame:newTabFrame]; |
| 1572 |
| 1573 if (animate) { |
| 1574 float delay = 0.0; |
| 1575 if ([self.fullscreenDelegate currentHeaderOffset] != 0) { |
| 1576 // Move the toolbar to visible and wait for the end of that animation to |
| 1577 // animate the appearance of the new tab. |
| 1578 delay = ios_internal::kToolbarAnimationDuration; |
| 1579 // Signal the FullscreenController that the toolbar needs to stay on |
| 1580 // screen for a bit, so the animation is visible. |
| 1581 [[NSNotificationCenter defaultCenter] |
| 1582 postNotificationName:kWillStartTabStripTabAnimation |
| 1583 object:nil]; |
| 1584 } |
| 1585 |
| 1586 [UIView animateWithDuration:kTabAnimationDuration |
| 1587 delay:delay |
| 1588 options:UIViewAnimationOptionAllowUserInteraction |
| 1589 animations:^{ |
| 1590 for (TabView* view in tabsNeedingAnimation) { |
| 1591 DCHECK(_targetFrames.HasFrame(view)); |
| 1592 [view setFrame:_targetFrames.GetFrame(view)]; |
| 1593 } |
| 1594 if (moveNewTab) |
| 1595 [_buttonNewTab setFrame:newTabFrame]; |
| 1596 } |
| 1597 completion:nil]; |
| 1598 } |
| 1599 } |
| 1600 |
| 1601 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
| 1602 [self updateScrollViewFrameForToggleButton]; |
| 1603 [self updateContentSizeAndRepositionViews]; |
| 1604 NSUInteger selectedModelIndex = [_tabModel indexOfTab:[_tabModel currentTab]]; |
| 1605 if (selectedModelIndex != NSNotFound) { |
| 1606 [self updateContentOffsetForTabIndex:selectedModelIndex]; |
| 1607 } |
| 1608 } |
| 1609 |
| 1610 - (void)setNeedsLayoutWithAnimation { |
| 1611 _animateLayout = YES; |
| 1612 [_tabStripView setNeedsLayout]; |
| 1613 } |
| 1614 |
| 1615 @end |
| 1616 |
| 1617 #pragma mark - TabSwitcherAnimation |
| 1618 |
| 1619 @implementation TabStripController (TabSwitcherAnimation) |
| 1620 |
| 1621 - (TabSwitcherTabStripPlaceholderView*)placeholderView { |
| 1622 TabSwitcherTabStripPlaceholderView* placeholderView = |
| 1623 [[[TabSwitcherTabStripPlaceholderView alloc] |
| 1624 initWithFrame:self.view.bounds] autorelease]; |
| 1625 CGFloat xOffset = [_tabStripView contentOffset].x; |
| 1626 UIView* previousView = nil; |
| 1627 const NSUInteger selectedModelIndex = |
| 1628 [_tabModel indexOfTab:[_tabModel currentTab]]; |
| 1629 const NSUInteger selectedArrayIndex = |
| 1630 [self indexForModelIndex:selectedModelIndex]; |
| 1631 [self updateContentSizeAndRepositionViews]; |
| 1632 [self layoutTabStripSubviews]; |
| 1633 for (NSUInteger tabArrayIndex = 0; tabArrayIndex < [_tabArray count]; |
| 1634 ++tabArrayIndex) { |
| 1635 UIView* tabView = _tabArray[tabArrayIndex]; |
| 1636 UIView* tabSnapshotView = snapshot_util::GenerateSnapshot(tabView); |
| 1637 tabSnapshotView.frame = CGRectOffset(tabView.frame, -xOffset, 0); |
| 1638 tabSnapshotView.transform = tabView.transform; |
| 1639 // Order views of the tabs in a pyramid fashion, culminating with |
| 1640 // the selected tab. |
| 1641 // For example, if _tabArray has views [0..6], and 3 is the selected index, |
| 1642 // they will be arranged in the order [0, 1, 2, 6, 5, 4, 3]. |
| 1643 if (previousView && tabArrayIndex > selectedArrayIndex) { |
| 1644 [placeholderView insertSubview:tabSnapshotView belowSubview:previousView]; |
| 1645 } else { |
| 1646 [placeholderView addSubview:tabSnapshotView]; |
| 1647 } |
| 1648 previousView = tabSnapshotView; |
| 1649 } |
| 1650 UIView* buttonSnapshot = snapshot_util::GenerateSnapshot(_buttonNewTab); |
| 1651 buttonSnapshot.frame = CGRectOffset(_buttonNewTab.frame, -xOffset, 0); |
| 1652 [placeholderView addSubview:buttonSnapshot]; |
| 1653 return placeholderView; |
| 1654 } |
| 1655 |
| 1656 @end |
| 1657 |
| 1658 @implementation TabStripController (Testing) |
| 1659 |
| 1660 - (TabView*)existingTabViewForTab:(Tab*)tab { |
| 1661 NSUInteger tabIndex = [_tabModel indexOfTab:tab]; |
| 1662 NSUInteger tabViewIndex = [self indexForModelIndex:tabIndex]; |
| 1663 return [_tabArray objectAtIndex:tabViewIndex]; |
| 1664 } |
| 1665 |
| 1666 @end |
OLD | NEW |