Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(11)

Unified Diff: chrome/browser/cocoa/bookmark_bar_controller.mm

Issue 384105: Mac: Animate the bookmark bar showing/hiding. (Closed)
Patch Set: Comments added per rohitrao's review. Created 11 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/cocoa/bookmark_bar_controller.mm
diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm
index 7a616dea29c3512823c4d8553c76ed6f0b72991d..3db0e8baf647bfc568277eae1e380468541d0312 100644
--- a/chrome/browser/cocoa/bookmark_bar_controller.mm
+++ b/chrome/browser/cocoa/bookmark_bar_controller.mm
@@ -10,6 +10,7 @@
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/browser.h"
#include "chrome/browser/browser_list.h"
+#import "chrome/browser/cocoa/background_gradient_view.h"
#import "chrome/browser/cocoa/bookmark_bar_bridge.h"
#import "chrome/browser/cocoa/bookmark_bar_constants.h"
#import "chrome/browser/cocoa/bookmark_bar_controller.h"
@@ -35,6 +36,77 @@
#include "skia/ext/skia_utils_mac.h"
#import "third_party/mozilla/include/NSPasteboard+Utils.h"
+// Bookmark bar state changing and animations
+//
+// The bookmark bar has three real states: "showing" (a normal bar attached to
+// the toolbar), "hidden", and "detached" (pretending to be part of the web
+// content on the NTP). It can, or at least should be able to, animate between
+// these states. There are several complications even without animation:
+// - The placement of the bookmark bar is done by the BWC, and it needs to know
+// the state in order to place the bookmark bar correctly (immediately below
+// the toolbar when showing, below the infobar when detached).
+// - The "divider" (a black line) needs to be drawn by either the toolbar (when
+// the bookmark bar is hidden or detached) or by the bookmark bar (when it is
+// showing). It should not be drawn by both.
+// - The toolbar needs to vertically "compress" when the bookmark bar is
+// showing. This ensures the proper display of both the bookmark bar and the
+// toolbar, and gives a padded area around the bookmark bar items for right
+// clicks, etc.
+//
+// Our model is that the BWC controls us and also the toolbar. We try not to
+// talk to the browser nor the toolbar directly, instead centralizing control in
+// the BWC. The key method by which the BWC controls us is
+// |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. This invokes state
+// changes, and at appropriate times we request that the BWC do things for us
+// via either the resize delegate or our general delegate. If the BWC needs any
+// information about what it should do, or tell the toolbar to do, it can then
+// query us back (e.g., |-isShownAs...|, |-getDesiredToolbarHeightCompression|,
+// |-shouldToolbarShowDivider|, etc.).
+//
+// Animation-related complications:
+// - Compression of the toolbar is touchy during animation. It must not be
+// compressed while the bookmark bar is animating to/from showing (from/to
+// hidden), otherwise it would look like the bookmark bar's contents are
+// sliding out of the controls inside the toolbar. As such, we have to make
+// sure that the bookmark bar is shown at the right location and at the
+// right height (at various points in time).
+// - Showing the divider is also complicated during animation between hidden
+// and showing. We have to make sure that the toolbar does not show the
+// divider despite the fact that it's not compressed. The exception to this
+// is at the beginning/end of the animation when the toolbar is still
+// uncompressed but the bookmark bar has height 0. If we're not careful, we
+// get a flicker at this point.
+// - We have to ensure that we do the right thing if we're told to change state
+// while we're running an animation. The generic/easy thing to do is to jump
+// to the end state of our current animation, and (if the new state change
+// again involves an animation) begin the new animation. We can do better
+// than that, however, and sometimes just change the current animation to go
+// to the new end state (e.g., by "reversing" the animation in the showing ->
+// hidden -> showing case). We also have to ensure that demands to
+// immediately change state are always honoured.
+//
+// Pointers to animation logic:
+// - |-moveToVisualState:withAnimation:| starts animations, deciding which ones
+// we know how to handle.
+// - |showBookmarkBarWithAnimation:| has most of the actual logic.
+// - |-getDesiredToolbarHeightCompression| and |-shouldToolbarShowDivider|
+// contain related logic.
+// - The BWC's |-layoutSubviews| needs to know how to position things.
+// - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and
+// |-bookmarkBar:willAnimateFromState:toState:| in order to inform the
+// toolbar of required changes.
+
+namespace {
+
+// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in
+// normal mode).
+const CGFloat kBookmarkBarOverlap = 6.0;
+
+// Duration of the bookmark bar animations.
+const NSTimeInterval kBookmarkBarAnimationDuration = 0.12;
+
+} // namespace
+
// Specialization of NSButton that responds to middle-clicks. By default,
// NSButton ignores them.
@interface BookmarkButton : NSButton
@@ -47,7 +119,34 @@
@end
@interface BookmarkBarController(Private)
-- (void)showBookmarkBar:(BOOL)enable immediately:(BOOL)immediately;
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar;
+
+// Moves to the given next state (from the current state), possibly animating.
+// If |animate| is NO, it will stop any running animation and jump to the given
+// state. If YES, it may either (depending on implementation) jump to the end of
+// the current animation and begin the next one, or stop the current animation
+// mid-flight and animate to the next state.
+- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ withAnimation:(BOOL)animate;
+
+// Return the backdrop to the bookmark bar as various types.
+- (BackgroundGradientView*)backgroundGradientView;
+- (AnimatableView*)animatableView;
+
+// Puts stuff into the final visual state without animating, stopping a running
+// animation if necessary.
+- (void)finalizeVisualState;
+
+// Stops any current animation in its tracks (midway).
+- (void)stopCurrentAnimation;
+
+// Show/hide the bookmark bar. Handles animating the resize of the content view.
+// if |animate| is YES, the changes are made using the animator; otherwise they
+// are made immediately.
+- (void)showBookmarkBarWithAnimation:(BOOL)animate;
+
- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu;
- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu;
- (void)tagEmptyMenu:(NSMenu*)menu;
@@ -62,20 +161,27 @@
@implementation BookmarkBarController
+@synthesize visualState = visualState_;
+@synthesize lastVisualState = lastVisualState_;
+@synthesize delegate = delegate_;
+
- (id)initWithBrowser:(Browser*)browser
initialWidth:(float)initialWidth
- compressDelegate:(id<ToolbarCompressable>)compressDelegate
+ delegate:(id<BookmarkBarControllerDelegate>)delegate
resizeDelegate:(id<ViewResizer>)resizeDelegate {
if ((self = [super initWithNibName:@"BookmarkBar"
bundle:mac_util::MainAppBundle()])) {
+ // Initialize to an invalid state.
+ visualState_ = bookmarks::kInvalidState;
+ lastVisualState_ = bookmarks::kInvalidState;
+
browser_ = browser;
initialWidth_ = initialWidth;
bookmarkModel_ = browser_->profile()->GetBookmarkModel();
buttons_.reset([[NSMutableArray alloc] init]);
- compressDelegate_ = compressDelegate;
+ delegate_ = delegate;
resizeDelegate_ = resizeDelegate;
- tabObserver_.reset(
- new TabStripModelObserverBridge(browser_->tabstrip_model(), self));
+ [[self animatableView] setResizeDelegate:resizeDelegate];
ResourceBundle& rb = ResourceBundle::GetSharedInstance();
folderImage_.reset([rb.GetNSImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]);
@@ -85,6 +191,9 @@
}
- (void)dealloc {
+ // We better stop any in-flight animation if we're being killed.
+ [[self animatableView] stopAnimation];
+
// Remove our view from its superview so it doesn't attempt to reference
// it when the controller is gone.
//TODO(dmaclach): Remove -- http://crbug.com/25845
@@ -112,8 +221,8 @@
DCHECK([offTheSideButton_ attachedMenu]);
- // To make life happier when the bookmark bar is floating, the
- // chevron is a child of the button view.
+ // To make life happier when the bookmark bar is floating, the chevron is a
+ // child of the button view.
[offTheSideButton_ removeFromSuperview];
[buttonView_ addSubview:offTheSideButton_];
@@ -127,14 +236,16 @@
object:[self view]];
}
-// Method is the same as [self view], but is provided to be explicit.
+// (Private) Method is the same as [self view], but is provided to be explicit.
- (BackgroundGradientView*)backgroundGradientView {
+ DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]);
return (BackgroundGradientView*)[self view];
}
-- (void)showIfNeeded {
- if (browser_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar))
- [self showBookmarkBar:YES immediately:YES];
+// (Private) Method is the same as [self view], but is provided to be explicit.
+- (AnimatableView*)animatableView {
+ DCHECK([[self view] isKindOfClass:[AnimatableView class]]);
+ return (AnimatableView*)[self view];
}
// Position the off-the-side chevron to the left of the otherBookmarks button.
@@ -185,16 +296,15 @@
NSPoint newOrigin;
newOrigin.x = parent.origin.x + bookmarks::kNoBookmarksHorizontalOffset;
newOrigin.y = parent.origin.y + parent.size.height -
- ([self isAlwaysVisible] ? bookmarks::kNoBookmarksVerticalOffset :
- bookmarks::kNoBookmarksNTPVerticalOffset);
+ ([self isShownAsToolbar] ? bookmarks::kNoBookmarksVerticalOffset :
+ bookmarks::kNoBookmarksNTPVerticalOffset);
[noItemsView setFrameOrigin:newOrigin];
}
-// Change the layout of the bookmark bar's subviews in response to a
-// visibility change (e.g. show or hide the bar) or style change
-// (attached or floating).
+// Change the layout of the bookmark bar's subviews in response to a visibility
+// change (e.g., show or hide the bar) or style change (attached or floating).
- (void)layoutSubviews {
- if ([self drawAsFloatingBar]) {
+ if (visualState_ == bookmarks::kDetachedState) {
// The internal bookmark bar should have padding to center it.
NSRect frame = [[self view] frame];
[buttonView_ setFrame:
@@ -212,49 +322,125 @@
}
}
-// Show or hide the bar based on the value of |show|. Handles animating the
-// resize of the content view. if |immediately| is YES, make changes
-// immediately instead of using an animator. The routine which enables the bar
-// will show it if relevant using other mechanisms (the pref) to determine
-// desired state.
-- (void)showBookmarkBar:(BOOL)show immediately:(BOOL)immediately {
- BOOL compressed = [self isAlwaysVisible];
- [compressDelegate_ setShouldBeCompressed:compressed];
+// (Private)
+- (void)showBookmarkBarWithAnimation:(BOOL)animate {
+ if (animate) {
+ // Animating from hidden to normal bar.
+ if (lastVisualState_ == bookmarks::kHiddenState &&
+ visualState_ == bookmarks::kShowingState) {
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ // Height takes into account the extra height we have since the toolbar
+ // only compresses when we're done.
+ [view animateToNewHeight:([self preferredHeight] - kBookmarkBarOverlap)
+ duration:kBookmarkBarAnimationDuration];
+ return;
+ }
+
+ // Animating from normal bar to hidden.
+ if (lastVisualState_ == bookmarks::kShowingState &&
+ visualState_ == bookmarks::kHiddenState) {
+ // The toolbar uncompresses immediately at the beginning (otherwise the
+ // slide looks wrong, since we slide into the bottom of stuff in the
+ // toolbar). Do this only if we're at the beginning height since we may
+ // enter this mid-animation.
+ if (NSHeight([[self view] frame]) == bookmarks::kBookmarkBarHeight) {
+ [resizeDelegate_ resizeView:[self view]
+ newHeight:(bookmarks::kBookmarkBarHeight -
+ kBookmarkBarOverlap)];
+ }
+ [[self backgroundGradientView] setShowsDivider:YES];
+ [[self view] setHidden:NO];
+ AnimatableView* view = [self animatableView];
+ [view animateToNewHeight:0
+ duration:kBookmarkBarAnimationDuration];
+ return;
+ }
+
+ // TODO(viettrungluu): other animation cases. Note: will need to fade in/out
+ // divider when animating from/to detached bar (to/from normal bar).
+
+ // Fall through for any cases we don't know about.
+ }
+ BOOL show = [self isVisible];
CGFloat height = show ? [self preferredHeight] : 0;
[resizeDelegate_ resizeView:[self view] newHeight:height];
- [[self view] setHidden:show ? NO : YES];
- DCHECK([[self view] isKindOfClass:[BookmarkBarToolbarView class]]);
+ // Only show the divider if showing the normal bookmark bar.
+ BOOL showDivider = (visualState_ == bookmarks::kShowingState ||
+ lastVisualState_ == bookmarks::kShowingState) ? YES : NO;
+ [[self backgroundGradientView] setShowsDivider:showDivider];
+
+ // Make sure we're shown.
+ [[self view] setHidden:(show ? NO : YES)];
[self layoutSubviews];
[self frameDidChange];
}
-// We don't change a preference; we only change visibility.
-// Preference changing (global state) is handled in
-// BrowserWindowCocoa::ToggleBookmarkBar(). We simply update the visibility of
-// the bar based on the current value of the pref.
+// We don't change a preference; we only change visibility. Preference changing
+// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We
+// simply update based on what we're told.
- (void)updateVisibility {
- [self showBookmarkBar:[self isVisible] immediately:YES];
+ [self showBookmarkBarWithAnimation:NO];
}
- (void)setBookmarkBarEnabled:(BOOL)enabled {
- barIsEnabled_ = enabled ? YES : NO;
+ barIsEnabled_ = enabled;
[self updateVisibility];
}
- (BOOL)isVisible {
- return ([self isAlwaysVisible] && barIsEnabled_) || [self isNewTabPage];
+ return (barIsEnabled_ && (visualState_ == bookmarks::kShowingState ||
+ visualState_ == bookmarks::kDetachedState)) ?
+ YES : NO;
}
-- (BOOL)isNewTabPage {
- return browser_ && browser_->GetSelectedTabContents() &&
- browser_->GetSelectedTabContents()->ShouldShowBookmarkBar();
+- (BOOL)isAnimationRunning {
+ return (lastVisualState_ == bookmarks::kInvalidState) ? NO : YES;
}
-- (BOOL)isAlwaysVisible {
- return browser_ &&
- browser_->profile()->GetPrefs()->GetBoolean(prefs::kShowBookmarkBar);
+- (BOOL)isShownAsToolbar {
+ return (visualState_ == bookmarks::kShowingState) ? YES : NO;
+}
+
+- (BOOL)isShownAsDetachedBar {
+ return (visualState_ == bookmarks::kDetachedState) ? YES : NO;
+}
+
+- (CGFloat)getDesiredToolbarHeightCompression {
+ // Some special cases....
+ if ([self isAnimationRunning]) {
+ // No toolbar compression when animating between showing and hidden.
+ if ((lastVisualState_ == bookmarks::kHiddenState &&
+ visualState_ == bookmarks::kShowingState) ||
+ (lastVisualState_ == bookmarks::kShowingState &&
+ visualState_ == bookmarks::kHiddenState))
+ return 0;
+
+ // TODO(viettrungluu): other animation cases.
+ }
+
+ return (visualState_ == bookmarks::kShowingState) ? kBookmarkBarOverlap : 0;
+}
+
+- (BOOL)shouldToolbarShowDivider {
+ // Some special cases....
+ if ([self isAnimationRunning]) {
+ // In general, the toolbar shouldn't show a divider while we're animating
+ // between showing and hidden. The exception is when our height is < 1, in
+ // which case we can't draw it.
+ if ((lastVisualState_ == bookmarks::kHiddenState &&
+ visualState_ == bookmarks::kShowingState) ||
+ (lastVisualState_ == bookmarks::kShowingState &&
+ visualState_ == bookmarks::kHiddenState))
+ return (NSHeight([[self view] frame]) < 1) ? YES : NO;
+
+ // TODO(viettrungluu): other animation cases.
+ }
+
+ return (visualState_ == bookmarks::kShowingState) ? NO : YES;
}
- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point {
@@ -274,18 +460,15 @@
}
- (int)currentTabContentsHeight {
- return browser_->GetSelectedTabContents()->view()->GetContainerSize().
- height();
+ return browser_->GetSelectedTabContents() ?
+ browser_->GetSelectedTabContents()->view()->GetContainerSize().height() :
+ 0;
}
- (ThemeProvider*)themeProvider {
return browser_->profile()->GetThemeProvider();
}
-- (BOOL)drawAsFloatingBar {
- return ![self isAlwaysVisible] && [self isNewTabPage];
-}
-
// Return nil if menuItem has no delegate.
- (BookmarkNode*)nodeFromMenuItem:(id)menuItem {
NSCell* cell = reinterpret_cast<NSCell*>([[menuItem menu] delegate]);
@@ -349,27 +532,10 @@
}
- (int)preferredHeight {
- return [self isAlwaysVisible] ? bookmarks::kBookmarkBarHeight :
+ return [self isShownAsToolbar] ? bookmarks::kBookmarkBarHeight :
bookmarks::kNTPBookmarkBarHeight;
}
-- (void)selectTabWithContents:(TabContents*)newContents
- previousContents:(TabContents*)oldContents
- atIndex:(NSInteger)index
- userGesture:(bool)wasUserGesture {
- // We need selectTabWithContents: for when we change from a tab that is the
- // new tab page to a tab that isn't.
- [self updateVisibility];
-}
-
-- (void)tabChangedWithContents:(TabContents*)contents
- atIndex:(NSInteger)index
- loadingOnly:(BOOL)loading {
- // We need tabChangedWithContents: for when the user clicks a bookmark from
- // the bookmark bar on the new tab page.
- [self updateVisibility];
-}
-
// Recursively add the given bookmark node and all its children to
// menu, one menu item per node.
- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu {
@@ -934,4 +1100,106 @@
return defaultImage_;
}
+// Determines the appropriate state for the given situation.
++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar {
+ if (showNormalBar)
+ return bookmarks::kShowingState;
+ if (showDetachedBar)
+ return bookmarks::kDetachedState;
+ return bookmarks::kHiddenState;
+}
+
+- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState
+ withAnimation:(BOOL)animate {
+ BOOL isAnimationRunning = [self isAnimationRunning];
+
+ // No-op if the next state is the same as the "current" one, subject to the
+ // following conditions:
+ // - no animation is running; or
+ // - an animation is running and |animate| is YES ([*] if it's NO, we'd want
+ // to cancel the animation and jump to the final state).
+ if ((nextVisualState == visualState_) && (!isAnimationRunning || animate))
+ return;
+
+ // If an animation is running, we want to finalize it. Otherwise we'd have to
+ // be able to animate starting from the middle of one type of animation. We
+ // assume that animations that we know about can be "reversed".
+ if (isAnimationRunning) {
+ // Don't cancel if we're going to reverse the animation.
+ if (nextVisualState != lastVisualState_) {
+ [self stopCurrentAnimation];
+ [self finalizeVisualState];
+ }
+
+ // If we're in case [*] above, we can stop here.
+ if (nextVisualState == visualState_)
+ return;
+ }
+
+ // Now update with the new state change.
+ lastVisualState_ = visualState_;
+ visualState_ = nextVisualState;
+
+ if (animate) {
+ // Take care of any animation cases we know how to handle.
+
+ // We know how to handle hidden <-> normal....
+ if ((lastVisualState_ == bookmarks::kHiddenState &&
+ visualState_ == bookmarks::kShowingState) ||
+ (lastVisualState_ == bookmarks::kShowingState &&
+ visualState_ == bookmarks::kHiddenState)) {
+ [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_
+ toState:visualState_];
+ [self showBookmarkBarWithAnimation:YES];
+ return;
+ }
+
+ // TODO(viettrungluu): we don't know about any yet....
+
+ // Let any animation cases which we don't know how to handle fall through to
+ // the unanimated case.
+ }
+
+ // Just jump to the state.
+ [self finalizeVisualState];
+}
+
+// N.B.: |-moveToVisualState:...| will check if this should be a no-op or not.
+- (void)updateAndShowNormalBar:(BOOL)showNormalBar
+ showDetachedBar:(BOOL)showDetachedBar
+ withAnimation:(BOOL)animate {
+ bookmarks::VisualState newVisualState =
+ [BookmarkBarController visualStateToShowNormalBar:showNormalBar
+ showDetachedBar:showDetachedBar];
+ [self moveToVisualState:newVisualState
+ withAnimation:animate];
+}
+
+// (Private)
+- (void)finalizeVisualState {
+ // We promise that our delegate that the variables will be finalized before
+ // the call to |-bookmarkBar:didChangeFromState:toState:|.
+ bookmarks::VisualState oldVisualState = lastVisualState_;
+ lastVisualState_ = bookmarks::kInvalidState;
+
+ // Notify our delegate.
+ [delegate_ bookmarkBar:self didChangeFromState:oldVisualState
+ toState:visualState_];
+
+ // Update ourselves visually.
+ [self updateVisibility];
+}
+
+// (Private)
+- (void)stopCurrentAnimation {
+ [[self animatableView] stopAnimation];
+}
+
+// Delegate method for |AnimatableView| (a superclass of
+// |BookmarkBarToolbarView|).
+- (void)animationDidEnd:(NSAnimation*)animation {
+ [self finalizeVisualState];
+}
+
@end
« no previous file with comments | « chrome/browser/cocoa/bookmark_bar_controller.h ('k') | chrome/browser/cocoa/bookmark_bar_controller_unittest.mm » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698