Index: ios/chrome/browser/ui/toolbar/toolbar_controller.mm |
diff --git a/ios/chrome/browser/ui/toolbar/toolbar_controller.mm b/ios/chrome/browser/ui/toolbar/toolbar_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..759f031bed97206333c7051014f60450ea87513e |
--- /dev/null |
+++ b/ios/chrome/browser/ui/toolbar/toolbar_controller.mm |
@@ -0,0 +1,1065 @@ |
+// Copyright 2012 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/toolbar/toolbar_controller.h" |
+ |
+#include <QuartzCore/QuartzCore.h> |
+ |
+#include "base/format_macros.h" |
+#include "base/i18n/rtl.h" |
+#include "base/ios/ios_util.h" |
+#include "base/mac/bundle_locations.h" |
+#include "base/mac/foundation_util.h" |
+#include "base/memory/ptr_util.h" |
+#include "base/metrics/user_metrics.h" |
+#include "base/metrics/user_metrics_action.h" |
+#import "ios/chrome/browser/ui/animation_util.h" |
+#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
+#import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
+#include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
+#import "ios/chrome/browser/ui/fullscreen_controller.h" |
+#import "ios/chrome/browser/ui/image_util.h" |
+#import "ios/chrome/browser/ui/reversed_animation.h" |
+#include "ios/chrome/browser/ui/rtl_geometry.h" |
+#import "ios/chrome/browser/ui/toolbar/toolbar_controller+protected.h" |
+#import "ios/chrome/browser/ui/toolbar/toolbar_controller_private.h" |
+#include "ios/chrome/browser/ui/toolbar/toolbar_resource_macros.h" |
+#import "ios/chrome/browser/ui/toolbar/toolbar_tools_menu_button.h" |
+#import "ios/chrome/browser/ui/toolbar/tools_menu_button_observer_bridge.h" |
+#import "ios/chrome/browser/ui/tools_menu/tools_menu_context.h" |
+#import "ios/chrome/browser/ui/tools_menu/tools_popup_controller.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#import "ios/chrome/common/material_timing.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#include "ios/chrome/grit/ios_theme_resources.h" |
+#import "ios/third_party/material_roboto_font_loader_ios/src/src/MaterialRobotoFontLoader.h" |
+#include "ui/base/resource/resource_bundle.h" |
+ |
+using base::UserMetricsAction; |
+using ios::material::TimingFunction; |
+ |
+// Animation key used for stack view transition animations |
+NSString* const kToolbarTransitionAnimationKey = @"ToolbarTransitionAnimation"; |
+ |
+// Externed max tab count. |
+const NSInteger kStackButtonMaxTabCount = 99; |
+// Font sizes for the button containing the tab count |
+const NSInteger kFontSizeFewerThanTenTabs = 11; |
+const NSInteger kFontSizeTenTabsOrMore = 9; |
+ |
+// The initial capacity used to construct |self.transitionLayers|. The value |
+// is chosen because WebToolbarController animates 11 separate layers during |
+// transitions; this value should be updated if new subviews are animated in |
+// the future. |
+const NSUInteger kTransitionLayerCapacity = 11; |
+ |
+// Externed delay before non-initial button images are loaded. |
+const int64_t kNonInitialImageAdditionDelayNanosec = 500000LL; |
+NSString* const kMenuWillShowNotification = @"kMenuWillShowNotification"; |
+NSString* const kMenuWillHideNotification = @"kMenuWillHideNotification"; |
+ |
+NSString* const kToolbarIdentifier = @"kToolbarIdentifier"; |
+NSString* const kIncognitoToolbarIdentifier = @"kIncognitoToolbarIdentifier"; |
+NSString* const kToolbarToolsMenuButtonIdentifier = |
+ @"kToolbarToolsMenuButtonIdentifier"; |
+NSString* const kToolbarStackButtonIdentifier = |
+ @"kToolbarStackButtonIdentifier"; |
+NSString* const kToolbarShareButtonIdentifier = |
+ @"kToolbarShareButtonIdentifier"; |
+ |
+// Macros for creating CGRects of height H, origin (0,0), with the portrait |
+// width of phone/pad devices. |
+// clang-format off |
+#define IPHONE_FRAME(H) { { 0, 0 }, { kPortraitWidth[IPHONE_IDIOM], H } } |
+#define IPAD_FRAME(H) { { 0, 0 }, { kPortraitWidth[IPAD_IDIOM], H } } |
+ |
+// Makes a two-element C array of CGRects as described above, one for each |
+// device idiom. |
+#define FRAME_PAIR(H) { IPHONE_FRAME(H), IPAD_FRAME(H) } |
+// clang-format on |
+ |
+const CGRect kToolbarFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(56); |
+ |
+namespace { |
+ |
+// Color constants for the stack button text, normal and pressed states. These |
+// arrays are indexed by ToolbarControllerStyle enum values. |
+const CGFloat kStackButtonNormalColors[] = { |
+ 85.0 / 255.0, // ToolbarControllerStyleLightMode |
+ 238.0 / 255.0, // ToolbarControllerStyleDarkMode |
+ 238.0 / 255.0, // ToolbarControllerStyleIncognitoMode |
+}; |
+ |
+const int kStackButtonHighlightedColors[] = { |
+ 0x4285F4, // ToolbarControllerStyleLightMode |
+ 0x888a8c, // ToolbarControllerStyleDarkMode |
+ 0x888a8c, // ToolbarControllerStyleIncognitoMode |
+}; |
+ |
+// UI frames. iPhone values followed by iPad values. |
+// Full-width frames that don't change for RTL languages. |
+const CGRect kBackgroundViewFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(56); |
+const CGRect kShadowViewFrame[INTERFACE_IDIOM_COUNT] = FRAME_PAIR(2); |
+// Full bleed shadow frame is iPhone-only |
+const CGRect kFullBleedShadowViewFrame = IPHONE_FRAME(10); |
+ |
+// Frames that change for RTL. |
+// clang-format off |
+const LayoutRect kStackButtonFrame = |
+ {kPortraitWidth[IPHONE_IDIOM], {230, 4}, {48, 48}}; |
+const LayoutRect kShareMenuButtonFrame = |
+ {kPortraitWidth[IPAD_IDIOM], {680, 4}, {46, 48}}; |
+const LayoutRect kToolsMenuButtonFrame[INTERFACE_IDIOM_COUNT] = { |
+ {kPortraitWidth[IPHONE_IDIOM], {276, 4}, {44, 48}}, |
+ {kPortraitWidth[IPAD_IDIOM], {723, 4}, {46, 48}} |
+}; |
+// clang-format on |
+ |
+// Distance to shift buttons when fading out. |
+const LayoutOffset kButtonFadeOutXOffset = 10; |
+ |
+} // namespace |
+ |
+// Helper class to display a UIButton with the image and text centered |
+// vertically and horizontally. |
+@interface ToolbarCenteredButton : UIButton { |
+} |
+@end |
+ |
+@implementation ToolbarCenteredButton |
+ |
+- (instancetype)initWithFrame:(CGRect)frame { |
+ self = [super initWithFrame:frame]; |
+ if (self) { |
+ self.titleLabel.textAlignment = NSTextAlignmentCenter; |
+ } |
+ return self; |
+} |
+ |
+- (void)layoutSubviews { |
+ [super layoutSubviews]; |
+ CGSize size = self.bounds.size; |
+ CGPoint center = CGPointMake(size.width / 2, size.height / 2); |
+ self.imageView.center = center; |
+ self.imageView.frame = AlignRectToPixel(self.imageView.frame); |
+ self.titleLabel.frame = self.bounds; |
+} |
+ |
+@end |
+ |
+@implementation ToolbarView |
+ |
+@synthesize animatingTransition = animatingTransition_; |
+@synthesize hitTestBoundsContraintRelaxed = hitTestBoundsContraintRelaxed_; |
+ |
+// Some views added to the toolbar have bounds larger than the toolbar bounds |
+// and still needs to receive touches. The overscroll actions view is one of |
+// those. That method is overridden in order to still perform hit testing on |
+// subviews that resides outside the toolbar bounds. |
+- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event { |
+ UIView* hitView = [super hitTest:point withEvent:event]; |
+ if (hitView || !self.hitTestBoundsContraintRelaxed) |
+ return hitView; |
+ |
+ for (UIView* view in [[self subviews] reverseObjectEnumerator]) { |
+ if (!view.userInteractionEnabled || [view isHidden] || [view alpha] < 0.01) |
+ continue; |
+ const CGPoint convertedPoint = [view convertPoint:point fromView:self]; |
+ if ([view pointInside:convertedPoint withEvent:event]) { |
+ hitView = [view hitTest:convertedPoint withEvent:event]; |
+ if (hitView) |
+ break; |
+ } |
+ } |
+ return hitView; |
+} |
+ |
+- (void)setDelegate:(id<ToolbarFrameDelegate>)delegate { |
+ delegate_.reset(delegate); |
+} |
+ |
+- (void)setFrame:(CGRect)frame { |
+ CGRect oldFrame = self.frame; |
+ [super setFrame:frame]; |
+ [delegate_ frameDidChangeFrame:frame fromFrame:oldFrame]; |
+} |
+ |
+- (void)didMoveToWindow { |
+ [super didMoveToWindow]; |
+ [delegate_ windowDidChange]; |
+} |
+ |
+- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)event { |
+ // Don't allow UIView block-based animations if we're already performing |
+ // explicit transition animations. |
+ if (self.animatingTransition) |
+ return (id<CAAction>)[NSNull null]; |
+ return [super actionForLayer:layer forKey:event]; |
+} |
+ |
+@end |
+ |
+@interface ToolbarController () { |
+ // The top-level toolbar view. |
+ base::scoped_nsobject<ToolbarView> view_; |
+ // The view for the toolbar background image. |
+ base::scoped_nsobject<UIImageView> backgroundView_; |
+ base::scoped_nsobject<UIImageView> shadowView_; |
+ base::scoped_nsobject<UIImageView> fullBleedShadowView_; |
+ |
+ // The backing object for |self.transitionLayers|. |
+ base::scoped_nsobject<NSMutableArray> transitionLayers_; |
+ |
+ base::scoped_nsobject<ToolbarToolsMenuButton> toolsMenuButton_; |
+ base::scoped_nsobject<UIButton> stackButton_; |
+ base::scoped_nsobject<UIButton> shareButton_; |
+ base::scoped_nsobject<NSArray> standardButtons_; |
+ std::unique_ptr<ToolsMenuButtonObserverBridge> toolsMenuButtonObserverBridge_; |
+ ToolbarControllerStyle style_; |
+ |
+ // The following is nil if not visible. |
+ base::scoped_nsobject<ToolsPopupController> toolsPopupController_; |
+} |
+ |
+// Returns the background image that should be used for |style|. |
+- (const gfx::Image&)getBackgroundImageForStyle:(ToolbarControllerStyle)style; |
+ |
+// Whether the share button should be visible in the toolbar. |
+- (BOOL)shareButtonShouldBeVisible; |
+ |
+// Update share button visibility and |standardButtons_| array. |
+- (void)updateStandardButtons; |
+ |
+// Returns an animation for |button| for a toolbar transition animation with |
+// |style|. |button|'s frame will be interpolated between its layout in the |
+// screen toolbar to the card's tab frame, and will be faded in for |
+// ToolbarTransitionStyleToStackView and faded out for |
+// ToolbarTransitionStyleToBVC. |
+- (CAAnimation*)transitionAnimationForButton:(UIButton*)button |
+ containerBeginBounds:(CGRect)containerBeginBounds |
+ containerEndBounds:(CGRect)containerEndBounds |
+ withStyle:(ToolbarTransitionStyle)style; |
+@end |
+ |
+@implementation ToolbarController |
+ |
+@synthesize readingListModel = readingListModel_; |
+ |
+@synthesize style = style_; |
+ |
+- (void)setReadingListModel:(ReadingListModel*)readingListModel { |
+ readingListModel_ = readingListModel; |
+ if (readingListModel_) |
+ toolsMenuButtonObserverBridge_ = |
+ base::MakeUnique<ToolsMenuButtonObserverBridge>(readingListModel_, |
+ toolsMenuButton_); |
+} |
+ |
+- (instancetype)initWithStyle:(ToolbarControllerStyle)style { |
+ self = [super init]; |
+ if (self) { |
+ style_ = style; |
+ DCHECK_LT(style_, ToolbarControllerStyleMaxStyles); |
+ |
+ InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM; |
+ CGRect viewFrame = kToolbarFrame[idiom]; |
+ CGRect backgroundFrame = kBackgroundViewFrame[idiom]; |
+ CGRect stackButtonFrame = LayoutRectGetRect(kStackButtonFrame); |
+ CGRect toolsMenuButtonFrame = |
+ LayoutRectGetRect(kToolsMenuButtonFrame[idiom]); |
+ |
+ if (idiom == IPHONE_IDIOM) { |
+ CGFloat statusBarOffset = [self statusBarOffset]; |
+ viewFrame.size.height += statusBarOffset; |
+ backgroundFrame.size.height += statusBarOffset; |
+ stackButtonFrame.origin.y += statusBarOffset; |
+ toolsMenuButtonFrame.origin.y += statusBarOffset; |
+ } |
+ |
+ view_.reset([[ToolbarView alloc] initWithFrame:viewFrame]); |
+ backgroundView_.reset([[UIImageView alloc] initWithFrame:backgroundFrame]); |
+ toolsMenuButton_.reset([[ToolbarToolsMenuButton alloc] |
+ initWithFrame:toolsMenuButtonFrame |
+ style:style_]); |
+ [toolsMenuButton_ setTag:IDC_SHOW_TOOLS_MENU]; |
+ [toolsMenuButton_ |
+ setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() | |
+ UIViewAutoresizingFlexibleBottomMargin]; |
+ |
+ [view_ addSubview:backgroundView_]; |
+ [view_ addSubview:toolsMenuButton_]; |
+ [view_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
+ [backgroundView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth | |
+ UIViewAutoresizingFlexibleHeight]; |
+ |
+ if (idiom == IPAD_IDIOM) { |
+ CGRect shareButtonFrame = LayoutRectGetRect(kShareMenuButtonFrame); |
+ shareButton_.reset([[UIButton alloc] initWithFrame:shareButtonFrame]); |
+ [shareButton_ setTag:IDC_SHARE_PAGE]; |
+ [shareButton_ |
+ setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() | |
+ UIViewAutoresizingFlexibleBottomMargin]; |
+ [self setUpButton:shareButton_ |
+ withImageEnum:ToolbarButtonNameShare |
+ forInitialState:UIControlStateNormal |
+ hasDisabledImage:YES |
+ synchronously:NO]; |
+ SetA11yLabelAndUiAutomationName(shareButton_, IDS_IOS_TOOLS_MENU_SHARE, |
+ kToolbarShareButtonIdentifier); |
+ [view_ addSubview:shareButton_]; |
+ } |
+ |
+ CGRect shadowFrame = kShadowViewFrame[idiom]; |
+ shadowFrame.origin.y = CGRectGetMaxY(backgroundFrame); |
+ shadowView_.reset([[UIImageView alloc] initWithFrame:shadowFrame]); |
+ [shadowView_ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
+ [shadowView_ setUserInteractionEnabled:NO]; |
+ [view_ addSubview:shadowView_]; |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ gfx::Image shadow = rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW); |
+ [shadowView_ setImage:shadow.ToUIImage()]; |
+ |
+ if (idiom == IPHONE_IDIOM) { |
+ // iPad omnibox does not expand to full bleed. |
+ CGRect fullBleedShadowFrame = kFullBleedShadowViewFrame; |
+ fullBleedShadowFrame.origin.y = shadowFrame.origin.y; |
+ fullBleedShadowView_.reset( |
+ [[UIImageView alloc] initWithFrame:fullBleedShadowFrame]); |
+ [fullBleedShadowView_ |
+ setAutoresizingMask:UIViewAutoresizingFlexibleWidth]; |
+ [fullBleedShadowView_ setUserInteractionEnabled:NO]; |
+ [fullBleedShadowView_ setAlpha:0]; |
+ [view_ addSubview:fullBleedShadowView_]; |
+ gfx::Image fullBleedShadow = |
+ rb.GetNativeImageNamed(IDR_IOS_TOOLBAR_SHADOW_FULL_BLEED); |
+ [fullBleedShadowView_ setImage:fullBleedShadow.ToUIImage()]; |
+ } |
+ |
+ transitionLayers_.reset( |
+ [[NSMutableArray alloc] initWithCapacity:kTransitionLayerCapacity]); |
+ |
+ // UIImageViews do not default to userInteractionEnabled:YES. |
+ [view_ setUserInteractionEnabled:YES]; |
+ [backgroundView_ setUserInteractionEnabled:YES]; |
+ |
+ gfx::Image tile = [self getBackgroundImageForStyle:style]; |
+ [[self backgroundView] |
+ setImage:StretchableImageFromUIImage(tile.ToUIImage(), 0.0, 3.0)]; |
+ |
+ if (idiom == IPHONE_IDIOM) { |
+ stackButton_.reset( |
+ [[ToolbarCenteredButton alloc] initWithFrame:stackButtonFrame]); |
+ [stackButton_ setTag:IDC_TOGGLE_TAB_SWITCHER]; |
+ [[stackButton_ titleLabel] |
+ setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
+ [stackButton_ |
+ setTitleColor:[UIColor colorWithWhite:kStackButtonNormalColors[style_] |
+ alpha:1.0] |
+ forState:UIControlStateNormal]; |
+ UIColor* highlightColor = |
+ UIColorFromRGB(kStackButtonHighlightedColors[style_], 1.0); |
+ [stackButton_ setTitleColor:highlightColor |
+ forState:UIControlStateHighlighted]; |
+ |
+ [stackButton_ |
+ setAutoresizingMask:UIViewAutoresizingFlexibleLeadingMargin() | |
+ UIViewAutoresizingFlexibleBottomMargin]; |
+ [stackButton_ addTarget:self |
+ action:@selector(stackButtonTouchDown:) |
+ forControlEvents:UIControlEventTouchDown]; |
+ |
+ [self setUpButton:stackButton_ |
+ withImageEnum:ToolbarButtonNameStack |
+ forInitialState:UIControlStateNormal |
+ hasDisabledImage:NO |
+ synchronously:NO]; |
+ [view_ addSubview:stackButton_]; |
+ } |
+ [self registerEventsForButton:toolsMenuButton_]; |
+ |
+ self.view.accessibilityIdentifier = |
+ style == ToolbarControllerStyleIncognitoMode |
+ ? kIncognitoToolbarIdentifier |
+ : kToolbarIdentifier; |
+ SetA11yLabelAndUiAutomationName(stackButton_, IDS_IOS_TOOLBAR_SHOW_TABS, |
+ kToolbarStackButtonIdentifier); |
+ SetA11yLabelAndUiAutomationName(toolsMenuButton_, IDS_IOS_TOOLBAR_SETTINGS, |
+ kToolbarToolsMenuButtonIdentifier); |
+ [self updateStandardButtons]; |
+ |
+ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; |
+ [defaultCenter addObserver:self |
+ selector:@selector(applicationDidEnterBackground:) |
+ name:UIApplicationDidEnterBackgroundNotification |
+ object:nil]; |
+ } |
+ return self; |
+} |
+ |
+- (instancetype)init { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (UIFont*)fontForSize:(NSInteger)size { |
+ return [[MDFRobotoFontLoader sharedInstance] boldFontOfSize:size]; |
+} |
+ |
+- (void)dealloc { |
+ [[NSNotificationCenter defaultCenter] removeObserver:self]; |
+ [toolsPopupController_ setDelegate:nil]; |
+ [super dealloc]; |
+} |
+ |
+- (UIImageView*)view { |
+ return view_.get(); |
+} |
+ |
+- (UIImageView*)backgroundView { |
+ return backgroundView_.get(); |
+} |
+ |
+- (CGFloat)statusBarOffset { |
+ return StatusBarHeight(); |
+} |
+ |
+- (UIImageView*)shadowView { |
+ return shadowView_.get(); |
+} |
+ |
+- (NSMutableArray*)transitionLayers { |
+ return transitionLayers_.get(); |
+} |
+ |
+- (BOOL)imageShouldFlipForRightToLeftLayoutDirection:(int)imageEnum { |
+ // None of the images this class knows about should flip. |
+ return NO; |
+} |
+ |
+- (void)updateStandardButtons { |
+ BOOL shareButtonShouldBeVisible = [self shareButtonShouldBeVisible]; |
+ [shareButton_ setHidden:!shareButtonShouldBeVisible]; |
+ NSMutableArray* standardButtons = [NSMutableArray array]; |
+ [standardButtons addObject:toolsMenuButton_]; |
+ if (stackButton_) |
+ [standardButtons addObject:stackButton_]; |
+ if (shareButtonShouldBeVisible) |
+ [standardButtons addObject:shareButton_]; |
+ standardButtons_.reset([standardButtons retain]); |
+} |
+ |
+- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { |
+ [self updateStandardButtons]; |
+} |
+ |
+- (ToolsPopupController*)toolsPopupController { |
+ return toolsPopupController_.get(); |
+} |
+ |
+- (void)applicationDidEnterBackground:(NSNotification*)notify { |
+ if (toolsPopupController_.get()) { |
+ // Dismiss the tools popup menu without animation. |
+ [toolsMenuButton_ setToolsMenuIsVisible:NO]; |
+ toolsPopupController_.reset(nil); |
+ [[NSNotificationCenter defaultCenter] |
+ postNotificationName:kMenuWillHideNotification |
+ object:nil]; |
+ } |
+} |
+ |
+- (BOOL)shareButtonShouldBeVisible { |
+ // The share button only exists on iPad, and when some tabs are visible |
+ // (i.e. when not in DarkMode), and when the width is greater than |
+ // the tablet mini view. |
+ if (!IsIPadIdiom() || style_ == ToolbarControllerStyleDarkMode || |
+ IsCompactTablet(self.view)) |
+ return NO; |
+ |
+ return YES; |
+} |
+ |
+- (void)setShareButtonEnabled:(BOOL)enabled { |
+ [shareButton_ setEnabled:enabled]; |
+} |
+ |
+- (UIImage*)imageForImageEnum:(int)imageEnum |
+ forState:(ToolbarButtonUIState)state { |
+ int imageId = |
+ [self imageIdForImageEnum:imageEnum style:[self style] forState:state]; |
+ DCHECK(imageId); |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ gfx::Image tile = rb.GetNativeImageNamed(imageId); |
+ UIImage* image = tile.ToUIImage(); |
+ return (UseRTLLayout() && |
+ [self imageShouldFlipForRightToLeftLayoutDirection:imageEnum]) |
+ ? [image imageFlippedForRightToLeftLayoutDirection] |
+ : image; |
+} |
+ |
+- (int)imageEnumForButton:(UIButton*)button { |
+ if (button == stackButton_.get()) |
+ return ToolbarButtonNameStack; |
+ return NumberOfToolbarButtonNames; |
+} |
+ |
+- (int)imageIdForImageEnum:(int)index |
+ style:(ToolbarControllerStyle)style |
+ forState:(ToolbarButtonUIState)state { |
+ DCHECK(index < NumberOfToolbarButtonNames); |
+ DCHECK(style < ToolbarControllerStyleMaxStyles); |
+ DCHECK(state < NumberOfToolbarButtonUIStates); |
+ // Incognito mode gets dark buttons. |
+ if (style == ToolbarControllerStyleIncognitoMode) |
+ style = ToolbarControllerStyleDarkMode; |
+ |
+ // Name, style [light, dark], UIControlState [normal, pressed, disabled] |
+ static int buttonImageIds[NumberOfToolbarButtonNames][2] |
+ [NumberOfToolbarButtonUIStates] = { |
+ TOOLBAR_IDR_THREE_STATE(OVERVIEW), |
+ TOOLBAR_IDR_THREE_STATE(SHARE), |
+ }; |
+ |
+ DCHECK(buttonImageIds[index][style][state]); |
+ return buttonImageIds[index][style][state]; |
+} |
+ |
+- (void)setUpButton:(UIButton*)button |
+ withImageEnum:(int)imageEnum |
+ forInitialState:(UIControlState)initialState |
+ hasDisabledImage:(BOOL)hasDisabledImage |
+ synchronously:(BOOL)synchronously { |
+ [self registerEventsForButton:button]; |
+ // Add the non-initial images after a slight delay, to help performance |
+ // and responsiveness on startup. |
+ dispatch_time_t addImageDelay = |
+ dispatch_time(DISPATCH_TIME_NOW, kNonInitialImageAdditionDelayNanosec); |
+ |
+ void (^normalImageBlock)(void) = ^{ |
+ UIImage* image = |
+ [self imageForImageEnum:imageEnum forState:ToolbarButtonUIStateNormal]; |
+ [button setImage:image forState:UIControlStateNormal]; |
+ }; |
+ if (synchronously || initialState == UIControlStateNormal) |
+ normalImageBlock(); |
+ else |
+ dispatch_after(addImageDelay, dispatch_get_main_queue(), normalImageBlock); |
+ |
+ void (^pressedImageBlock)(void) = ^{ |
+ UIImage* image = |
+ [self imageForImageEnum:imageEnum forState:ToolbarButtonUIStatePressed]; |
+ [button setImage:image forState:UIControlStateHighlighted]; |
+ }; |
+ if (synchronously || initialState == UIControlStateHighlighted) |
+ pressedImageBlock(); |
+ else |
+ dispatch_after(addImageDelay, dispatch_get_main_queue(), pressedImageBlock); |
+ |
+ if (hasDisabledImage) { |
+ void (^disabledImageBlock)(void) = ^{ |
+ UIImage* image = [self imageForImageEnum:imageEnum |
+ forState:ToolbarButtonUIStateDisabled]; |
+ [button setImage:image forState:UIControlStateDisabled]; |
+ }; |
+ if (synchronously || initialState == UIControlStateDisabled) { |
+ disabledImageBlock(); |
+ } else { |
+ dispatch_after(addImageDelay, dispatch_get_main_queue(), |
+ disabledImageBlock); |
+ } |
+ } |
+} |
+ |
+- (void)registerEventsForButton:(UIButton*)button { |
+ if (button != toolsMenuButton_.get()) { |
+ // |target| must be |self| (as opposed to |nil|) because |self| isn't in the |
+ // responder chain. |
+ [button addTarget:self |
+ action:@selector(standardButtonPressed:) |
+ forControlEvents:UIControlEventTouchUpInside]; |
+ } |
+ [button addTarget:self |
+ action:@selector(recordUserMetrics:) |
+ forControlEvents:UIControlEventTouchUpInside]; |
+ [button addTarget:button |
+ action:@selector(chromeExecuteCommand:) |
+ forControlEvents:UIControlEventTouchUpInside]; |
+} |
+ |
+- (CGRect)shareButtonAnchorRect { |
+ // Shrink the padding around the shareButton so the popovers are anchored |
+ // correctly. |
+ return CGRectInset([shareButton_ bounds], 10, 0); |
+} |
+ |
+- (UIView*)shareButtonView { |
+ return shareButton_.get(); |
+} |
+ |
+- (void)showToolsMenuPopupWithContext:(ToolsMenuContext*)context { |
+ // Because an animation hides and shows the tools popup menu it is possible to |
+ // tap the tools button multiple times before the tools menu is shown. Ignore |
+ // repeated taps between animations. |
+ if (toolsPopupController_) |
+ return; |
+ |
+ base::RecordAction(UserMetricsAction("ShowAppMenu")); |
+ |
+ // Keep the button pressed. |
+ [toolsMenuButton_ setToolsMenuIsVisible:YES]; |
+ |
+ [context setToolsMenuButton:toolsMenuButton_]; |
+ toolsPopupController_.reset( |
+ [[ToolsPopupController alloc] initWithContext:context]); |
+ |
+ [toolsPopupController_ setDelegate:self]; |
+ |
+ [[NSNotificationCenter defaultCenter] |
+ postNotificationName:kMenuWillShowNotification |
+ object:nil]; |
+} |
+ |
+- (void)dismissToolsMenuPopup { |
+ if (!toolsPopupController_.get()) |
+ return; |
+ ToolsPopupController* tempTPC = toolsPopupController_.get(); |
+ [tempTPC containerView].userInteractionEnabled = NO; |
+ [tempTPC dismissAnimatedWithCompletion:^{ |
+ // Unpress the tools menu button by restoring the normal and |
+ // highlighted images to their usual state. |
+ [toolsMenuButton_ setToolsMenuIsVisible:NO]; |
+ // Reference tempTPC so the block retains it. |
+ [tempTPC self]; |
+ }]; |
+ // reset tabHistoryPopupController_ to prevent -applicationDidEnterBackground |
+ // from posting another kMenuWillHideNotification. |
+ toolsPopupController_.reset(); |
+ |
+ [[NSNotificationCenter defaultCenter] |
+ postNotificationName:kMenuWillHideNotification |
+ object:nil]; |
+} |
+ |
+- (const gfx::Image&)getBackgroundImageForStyle:(ToolbarControllerStyle)style { |
+ int backgroundImageId; |
+ if (style == ToolbarControllerStyleLightMode) |
+ backgroundImageId = IDR_IOS_TOOLBAR_LIGHT_BACKGROUND; |
+ else |
+ backgroundImageId = IDR_IOS_TOOLBAR_DARK_BACKGROUND; |
+ |
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
+ return rb.GetNativeImageNamed(backgroundImageId); |
+} |
+ |
+- (CGRect)specificControlsArea { |
+ // Return the rect to the leading side of the leading-most trailing control. |
+ UIView* trailingControl = toolsMenuButton_; |
+ if (!IsIPadIdiom()) |
+ trailingControl = stackButton_; |
+ if ([self shareButtonShouldBeVisible]) |
+ trailingControl = shareButton_; |
+ LayoutRect trailing = |
+ LayoutRectForRectInBoundingRect(trailingControl.frame, self.view.bounds); |
+ LayoutRect controlsArea = LayoutRectGetLeadingLayout(trailing); |
+ controlsArea.size.height = self.view.bounds.size.height; |
+ controlsArea.position.originY = self.view.bounds.origin.y; |
+ CGRect controlsFrame = LayoutRectGetRect(controlsArea); |
+ |
+ if (!IsIPadIdiom()) { |
+ controlsFrame.origin.y += StatusBarHeight(); |
+ controlsFrame.size.height -= StatusBarHeight(); |
+ } |
+ return controlsFrame; |
+} |
+ |
+- (void)animateStandardControlsForOmniboxExpansion:(BOOL)growOmnibox { |
+ if (growOmnibox) |
+ [self fadeOutStandardControls]; |
+ else |
+ [self fadeInStandardControls]; |
+} |
+ |
+- (void)fadeOutStandardControls { |
+ // The opacity animation has a different duration from the position animation. |
+ // Thus they require separate CATransations. |
+ |
+ // Animate the opacity of the buttons to 0. |
+ [CATransaction begin]; |
+ [CATransaction setAnimationDuration:ios::material::kDuration2]; |
+ [CATransaction |
+ setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)]; |
+ CABasicAnimation* fadeButtons = |
+ [CABasicAnimation animationWithKeyPath:@"opacity"]; |
+ fadeButtons.fromValue = @1; |
+ fadeButtons.toValue = @0; |
+ |
+ for (UIButton* button in standardButtons_.get()) { |
+ if (![button isHidden]) { |
+ [button layer].opacity = 0; |
+ [[button layer] addAnimation:fadeButtons forKey:@"fade"]; |
+ } |
+ } |
+ [CATransaction commit]; |
+ |
+ // Animate the buttons 10 pixels in the leading-to-trailing direction |
+ [CATransaction begin]; |
+ [CATransaction setAnimationDuration:ios::material::kDuration1]; |
+ [CATransaction |
+ setAnimationTimingFunction:TimingFunction(ios::material::CurveEaseIn)]; |
+ |
+ for (UIButton* button in standardButtons_.get()) { |
+ CABasicAnimation* shiftButton = |
+ [CABasicAnimation animationWithKeyPath:@"position"]; |
+ CGPoint startPosition = [button layer].position; |
+ CGPoint endPosition = |
+ CGPointLayoutOffset(startPosition, kButtonFadeOutXOffset); |
+ shiftButton.fromValue = [NSValue valueWithCGPoint:startPosition]; |
+ shiftButton.toValue = [NSValue valueWithCGPoint:endPosition]; |
+ [[button layer] addAnimation:shiftButton forKey:@"shiftButton"]; |
+ } |
+ |
+ [CATransaction commit]; |
+ |
+ // Fade to the full bleed shadow. |
+ [UIView animateWithDuration:ios::material::kDuration1 |
+ animations:^{ |
+ [shadowView_ setAlpha:0]; |
+ [fullBleedShadowView_ setAlpha:1]; |
+ }]; |
+} |
+ |
+- (void)fadeInStandardControls { |
+ for (UIButton* button in standardButtons_.get()) { |
+ [self fadeInView:button |
+ fromLeadingOffset:10 |
+ withDuration:ios::material::kDuration2 |
+ afterDelay:ios::material::kDuration1]; |
+ } |
+ |
+ // Fade to the normal shadow. |
+ [UIView animateWithDuration:ios::material::kDuration1 |
+ animations:^{ |
+ [shadowView_ setAlpha:self.backgroundView.alpha]; |
+ [fullBleedShadowView_ setAlpha:0]; |
+ }]; |
+} |
+ |
+- (void)animationDidStart:(CAAnimation*)anim { |
+ // Once the buttons start fading in, set their opacity to 1 so there's no |
+ // flicker at the end of the animation. |
+ for (UIButton* button in standardButtons_.get()) { |
+ if (anim == [[button layer] animationForKey:@"fadeIn"]) { |
+ [button layer].opacity = 1; |
+ return; |
+ } |
+ } |
+} |
+ |
+- (void)fadeInView:(UIView*)view |
+ fromLeadingOffset:(LayoutOffset)leadingOffset |
+ withDuration:(NSTimeInterval)duration |
+ afterDelay:(NSTimeInterval)delay { |
+ [CATransaction begin]; |
+ [CATransaction setDisableActions:YES]; |
+ [CATransaction setCompletionBlock:^{ |
+ [view.layer removeAnimationForKey:@"fadeIn"]; |
+ }]; |
+ view.alpha = 1.0; |
+ |
+ // Animate the position of |view| |leadingOffset| pixels after |delay|. |
+ CGRect shiftedFrame = CGRectLayoutOffset(view.frame, leadingOffset); |
+ CAAnimation* shiftAnimation = |
+ FrameAnimationMake(view.layer, shiftedFrame, view.frame); |
+ shiftAnimation.duration = duration; |
+ shiftAnimation.beginTime = delay; |
+ shiftAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut); |
+ |
+ // Animate the opacity of |view| to 1 after |delay|. |
+ CAAnimation* fadeAnimation = OpacityAnimationMake(0.0, 1.0); |
+ fadeAnimation.duration = duration; |
+ fadeAnimation.beginTime = delay; |
+ shiftAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut); |
+ |
+ // Add group animation to layer. |
+ CAAnimation* group = AnimationGroupMake(@[ shiftAnimation, fadeAnimation ]); |
+ [view.layer addAnimation:group forKey:@"fadeIn"]; |
+ |
+ [CATransaction commit]; |
+} |
+ |
+- (CAAnimation*)transitionAnimationForButton:(UIButton*)button |
+ containerBeginBounds:(CGRect)containerBeginBounds |
+ containerEndBounds:(CGRect)containerEndBounds |
+ withStyle:(ToolbarTransitionStyle)style { |
+ BOOL toStackView = style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW; |
+ CGRect cardBounds = toStackView ? containerEndBounds : containerBeginBounds; |
+ CGRect toolbarBounds = |
+ toStackView ? containerBeginBounds : containerEndBounds; |
+ |
+ // |button|'s model layer frame is the button's frame within |toolbarBounds|. |
+ CGRect toolbarButtonFrame = button.layer.frame; |
+ LayoutRect toolbarButtonLayout = |
+ LayoutRectForRectInBoundingRect(toolbarButtonFrame, toolbarBounds); |
+ |
+ // |button|'s leading or trailing padding is maintained depending on its |
+ // resizing mask. Its vertical positioning should be centered within the |
+ // container view's card bounds. |
+ LayoutRect cardButtonLayout = toolbarButtonLayout; |
+ cardButtonLayout.boundingWidth = CGRectGetWidth(cardBounds); |
+ BOOL flexibleLeading = |
+ button.autoresizingMask & UIViewAutoresizingFlexibleLeadingMargin(); |
+ if (flexibleLeading) { |
+ CGFloat trailingPadding = |
+ LayoutRectGetTrailingLayout(toolbarButtonLayout).size.width; |
+ cardButtonLayout.position.leading = cardButtonLayout.boundingWidth - |
+ trailingPadding - |
+ cardButtonLayout.size.width; |
+ } |
+ cardButtonLayout.position.originY = |
+ CGRectGetMidY(cardBounds) - 0.5 * cardButtonLayout.size.height; |
+ cardButtonLayout.position = |
+ AlignLayoutRectPositionToPixel(cardButtonLayout.position); |
+ CGRect cardButtonFrame = LayoutRectGetRect(cardButtonLayout); |
+ |
+ CGRect beginFrame = toStackView ? toolbarButtonFrame : cardButtonFrame; |
+ CGRect endFrame = toStackView ? cardButtonFrame : toolbarButtonFrame; |
+ |
+ // Create animations. |
+ CAAnimation* frameAnimation = |
+ FrameAnimationMake(button.layer, beginFrame, endFrame); |
+ frameAnimation.duration = ios::material::kDuration1; |
+ frameAnimation.timingFunction = TimingFunction(ios::material::CurveEaseInOut); |
+ CAAnimation* fadeAnimation = |
+ OpacityAnimationMake(toStackView ? 1.0 : 0.0, toStackView ? 0.0 : 1.0); |
+ fadeAnimation.duration = ios::material::kDuration8; |
+ fadeAnimation.timingFunction = TimingFunction(ios::material::CurveEaseIn); |
+ return AnimationGroupMake(@[ frameAnimation, fadeAnimation ]); |
+} |
+ |
+- (void)animateTransitionForButtonsInView:(UIView*)containerView |
+ containerBeginBounds:(CGRect)containerBeginBounds |
+ containerEndBounds:(CGRect)containerEndBounds |
+ transitionStyle:(ToolbarTransitionStyle)style { |
+ [containerView.subviews enumerateObjectsUsingBlock:^( |
+ UIButton* button, NSUInteger idx, BOOL* stop) { |
+ if ([button isKindOfClass:[UIButton class]] && button.alpha > 0.0) { |
+ CAAnimation* buttonAnimation = |
+ [self transitionAnimationForButton:button |
+ containerBeginBounds:containerBeginBounds |
+ containerEndBounds:containerEndBounds |
+ withStyle:style]; |
+ [self.transitionLayers addObject:button.layer]; |
+ [button.layer addAnimation:buttonAnimation |
+ forKey:kToolbarTransitionAnimationKey]; |
+ } |
+ }]; |
+} |
+ |
+- (void)reverseTransitionAnimations { |
+ ReverseAnimationsForKeyForLayers(kToolbarTransitionAnimationKey, |
+ [self transitionLayers]); |
+} |
+ |
+- (UIButton*)stackButton { |
+ return stackButton_; |
+} |
+ |
+- (void)cleanUpTransitionAnimations { |
+ RemoveAnimationForKeyFromLayers(kToolbarTransitionAnimationKey, |
+ self.transitionLayers); |
+ [self.transitionLayers removeAllObjects]; |
+} |
+ |
+- (void)animateTransitionWithBeginFrame:(CGRect)beginFrame |
+ endFrame:(CGRect)endFrame |
+ transitionStyle:(ToolbarTransitionStyle)style { |
+ // Animation values. |
+ DCHECK(!self.transitionLayers.count); |
+ BOOL transitioningToStackView = |
+ (style == TOOLBAR_TRANSITION_STYLE_TO_STACK_VIEW); |
+ CAAnimation* frameAnimation = nil; |
+ CAMediaTimingFunction* frameTiming = |
+ TimingFunction(ios::material::CurveEaseInOut); |
+ CFTimeInterval frameDuration = ios::material::kDuration1; |
+ CGRect beginBounds = {CGPointZero, beginFrame.size}; |
+ CGRect endBounds = {CGPointZero, endFrame.size}; |
+ |
+ // Update layer geometry. |
+ frameAnimation = FrameAnimationMake(self.view.layer, beginFrame, endFrame); |
+ frameAnimation.duration = frameDuration; |
+ frameAnimation.timingFunction = frameTiming; |
+ [self.transitionLayers addObject:self.view.layer]; |
+ [self.view.layer addAnimation:frameAnimation |
+ forKey:kToolbarTransitionAnimationKey]; |
+ |
+ // Hide background view using CAAnimation so it can be unhidden when the |
+ // animations are removed in |-cleanUpTransitionAnimations|. |
+ CAAnimation* hideAnimation = OpacityAnimationMake(0.0, 0.0); |
+ [self.transitionLayers addObject:self.backgroundView.layer]; |
+ [self.backgroundView.layer addAnimation:hideAnimation |
+ forKey:kToolbarTransitionAnimationKey]; |
+ |
+ // Update shadow. When transitioning to the stack view, hide the shadow. |
+ // When transitioning to the BVC, animate its frame while fading in. |
+ CAAnimation* shadowAnimation = nil; |
+ if (transitioningToStackView) { |
+ shadowAnimation = hideAnimation; |
+ } else { |
+ InterfaceIdiom idiom = IsIPadIdiom() ? IPAD_IDIOM : IPHONE_IDIOM; |
+ CGFloat shadowHeight = kShadowViewFrame[idiom].size.height; |
+ CGFloat shadowVerticalOffset = [[self class] toolbarDropShadowHeight]; |
+ beginFrame = CGRectOffset(beginBounds, 0.0, |
+ beginBounds.size.height - shadowVerticalOffset); |
+ beginFrame.size.height = shadowHeight; |
+ endFrame = CGRectOffset(endBounds, 0.0, |
+ endBounds.size.height - shadowVerticalOffset); |
+ endFrame.size.height = shadowHeight; |
+ frameAnimation = |
+ FrameAnimationMake([shadowView_ layer], beginFrame, endFrame); |
+ frameAnimation.duration = frameDuration; |
+ frameAnimation.timingFunction = frameTiming; |
+ CAAnimation* fadeAnimation = OpacityAnimationMake(0.0, 1.0); |
+ fadeAnimation.timingFunction = TimingFunction(ios::material::CurveEaseOut); |
+ fadeAnimation.duration = ios::material::kDuration3; |
+ shadowAnimation = AnimationGroupMake(@[ frameAnimation, fadeAnimation ]); |
+ } |
+ [self.transitionLayers addObject:[shadowView_ layer]]; |
+ [[shadowView_ layer] addAnimation:shadowAnimation |
+ forKey:kToolbarTransitionAnimationKey]; |
+ |
+ // Animate toolbar buttons |
+ [self animateTransitionForButtonsInView:self.view |
+ containerBeginBounds:beginBounds |
+ containerEndBounds:endBounds |
+ transitionStyle:style]; |
+} |
+ |
+- (void)hideViewsForNewTabPage:(BOOL)hide { |
+ DCHECK(!IsIPadIdiom()); |
+ [shadowView_ setHidden:hide]; |
+} |
+ |
+- (void)setStandardControlsVisible:(BOOL)visible { |
+ if (visible) { |
+ for (UIButton* button in standardButtons_.get()) { |
+ [button setAlpha:1.0]; |
+ } |
+ } else { |
+ for (UIButton* button in standardButtons_.get()) { |
+ [button setAlpha:0.0]; |
+ } |
+ } |
+} |
+ |
+- (void)setStandardControlsAlpha:(CGFloat)alpha { |
+ for (UIButton* button in standardButtons_.get()) { |
+ if (![button isHidden]) |
+ [button setAlpha:alpha]; |
+ } |
+} |
+ |
+- (void)setBackgroundAlpha:(CGFloat)alpha { |
+ [backgroundView_ setAlpha:alpha]; |
+ [shadowView_ setAlpha:alpha]; |
+} |
+ |
+- (void)setStandardControlsTransform:(CGAffineTransform)transform { |
+ for (UIButton* button in standardButtons_.get()) { |
+ [button setTransform:transform]; |
+ } |
+} |
+ |
+- (void)standardButtonPressed:(UIButton*)sender { |
+ // This check for valid button images assumes that the buttons all have a |
+ // different image for the highlighted state as for the normal state. |
+ // Currently, that assumption is true. |
+ if ([sender imageForState:UIControlStateHighlighted] == |
+ [sender imageForState:UIControlStateNormal]) { |
+ // Update the button images synchronously - somehow the button was pressed |
+ // before the dispatched task completed. |
+ [self setUpButton:sender |
+ withImageEnum:[self imageEnumForButton:sender] |
+ forInitialState:UIControlStateNormal |
+ hasDisabledImage:NO |
+ synchronously:YES]; |
+ } |
+} |
+ |
+- (void)setTabCount:(NSInteger)tabCount { |
+ if (!stackButton_) |
+ return; |
+ // Enable or disable the stack view icon based on the number of tabs. This |
+ // locks the user in the stack view when there are no tabs. |
+ [stackButton_ setEnabled:tabCount > 0 ? YES : NO]; |
+ |
+ // Update the text shown in the |stackButton_|. Note that the button's title |
+ // may be empty or contain an easter egg, but the accessibility value will |
+ // always be equal to |tabCount|. Also, the text of |stackButton_| is shifted |
+ // up, via |kEasterEggTitleInsets|, to avoid overlapping with the button's |
+ // outline. |
+ NSString* stackButtonValue = |
+ [NSString stringWithFormat:@"%" PRIdNS, tabCount]; |
+ NSString* stackButtonTitle; |
+ if (tabCount <= 0) { |
+ stackButtonTitle = @""; |
+ } else if (tabCount > kStackButtonMaxTabCount) { |
+ stackButtonTitle = @":)"; |
+ [[stackButton_ titleLabel] |
+ setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
+ } else { |
+ stackButtonTitle = stackButtonValue; |
+ if (tabCount < 10) { |
+ [[stackButton_ titleLabel] |
+ setFont:[self fontForSize:kFontSizeFewerThanTenTabs]]; |
+ } else { |
+ [[stackButton_ titleLabel] |
+ setFont:[self fontForSize:kFontSizeTenTabsOrMore]]; |
+ } |
+ } |
+ |
+ [stackButton_ setTitle:stackButtonTitle forState:UIControlStateNormal]; |
+ [stackButton_ setAccessibilityValue:stackButtonValue]; |
+} |
+ |
+- (IBAction)recordUserMetrics:(id)sender { |
+ if (sender == toolsMenuButton_.get()) |
+ base::RecordAction(UserMetricsAction("MobileToolbarShowMenu")); |
+ else if (sender == stackButton_.get()) |
+ base::RecordAction(UserMetricsAction("MobileToolbarShowStackView")); |
+ else if (sender == shareButton_.get()) |
+ base::RecordAction(UserMetricsAction("MobileToolbarShareMenu")); |
+ else |
+ NOTREACHED(); |
+} |
+ |
+- (IBAction)stackButtonTouchDown:(id)sender { |
+ // Exists only for override by subclasses. |
+} |
+ |
++ (CGFloat)toolbarDropShadowHeight { |
+ return 0.0; |
+} |
+ |
+- (uint32_t)snapshotHash { |
+ // Only the 3 lowest bits are used by UIControlState. |
+ uint32_t hash = [toolsMenuButton_ state] & 0x07; |
+ // When the tools popup controller is valid, it means that the images |
+ // representing the tools menu button have been swapped. Factor that in by |
+ // adding in whether or not the tools popup menu is a valid object, rather |
+ // than trying to figure out which image is currently visible. |
+ hash |= toolsPopupController_ ? (1 << 4) : 0; |
+ // The label of the stack button changes with the number of tabs open. |
+ hash ^= [[stackButton_ titleForState:UIControlStateNormal] hash]; |
+ return hash; |
+} |
+ |
+#pragma mark - |
+#pragma mark PopupMenuDelegate methods. |
+ |
+- (void)dismissPopupMenu:(PopupMenuController*)controller { |
+ if ([controller isKindOfClass:[ToolsPopupController class]] && |
+ (ToolsPopupController*)controller == toolsPopupController_) |
+ [self dismissToolsMenuPopup]; |
+} |
+ |
+@end |