summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authordhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-04-20 17:48:59 +0000
committerdhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-04-20 17:48:59 +0000
commit4c16642677eb1219c6a448c43a95070811b6af7b (patch)
tree91b6742e868d4495fe6ee51c974c15beeea4ef4f /chrome
parent7716bfdc8f418838559e026f1bdbbf81a30949b6 (diff)
downloadchromium_src-4c16642677eb1219c6a448c43a95070811b6af7b.zip
chromium_src-4c16642677eb1219c6a448c43a95070811b6af7b.tar.gz
chromium_src-4c16642677eb1219c6a448c43a95070811b6af7b.tar.bz2
Mac Bookmarks Folder Bar hover state refactor.
Refactoring to encapsulate hover state in a separate class. BUG=40006 TEST=BookmarkBarFolderHoverStateTest.HoverState Review URL: http://codereview.chromium.org/1676001 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@45053 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_controller.h33
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_controller.mm187
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm67
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_hover_state.h79
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_hover_state.mm177
-rw-r--r--chrome/browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm77
-rw-r--r--chrome/chrome_browser.gypi2
-rw-r--r--chrome/chrome_tests.gypi1
8 files changed, 344 insertions, 279 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.h b/chrome/browser/cocoa/bookmark_bar_folder_controller.h
index b8e9ce7..d2e1238 100644
--- a/chrome/browser/cocoa/bookmark_bar_folder_controller.h
+++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.h
@@ -10,6 +10,7 @@
@class BookmarkBarController;
@class BookmarkBarFolderView;
@class BookmarkFolderTarget;
+@class BookmarkBarFolderHoverState;
// A controller for the pop-up windows from bookmark folder buttons
// which look sort of like menus.
@@ -17,21 +18,6 @@
NSWindowController<BookmarkButtonDelegate,
BookmarkButtonControllerProtocol> {
@private
- // Enumeration of the valid states that the |hoverButton_| member can be in.
- // Because the opening and closing of hover views can be done asyncronously
- // there are periods where the hover state is in transtion between open and
- // closed. During those times of transition the opening or closing operation
- // can be cancelled. We serialize the opening and closing of the
- // |hoverButton_| using this state information. This serialization is to
- // avoid race conditions where one hover button is being opened while another
- // is closing.
- enum HoverState {
- kHoverStateClosed = 0,
- kHoverStateOpening = 1,
- kHoverStateOpen = 2,
- kHoverStateClosing = 3
- };
-
// The button whose click opened us.
scoped_nsobject<BookmarkButton> parentButton_;
@@ -91,15 +77,10 @@
// The context menu for a bookmark button which represents a folder.
IBOutlet NSMenu* folderMenu_;
- // Like normal menus, hovering over a folder button causes it to
- // open. This variable is set when a hover is initiated (but has
- // not necessarily fired yet).
- scoped_nsobject<BookmarkButton> hoverButton_;
-
// We model hover state as a state machine with specific allowable
// transitions. |hoverState_| is the state of this machine at any
// given time.
- HoverState hoverState_;
+ scoped_nsobject<BookmarkBarFolderHoverState> hoverState_;
// Logic for dealing with a click on a bookmark folder button.
scoped_nsobject<BookmarkFolderTarget> folderTarget_;
@@ -147,7 +128,6 @@
- (IBAction)openBookmarkInNewWindow:(id)sender;
@end
-
@interface BookmarkBarFolderController(TestingAPI)
- (NSView*)mainView;
- (NSPoint)windowTopLeft;
@@ -156,14 +136,5 @@
- (id)folderTarget;
- (void)configureWindowLevel;
- (void)performOneScroll:(CGFloat)delta;
-
-// Internal interface for hover button management.
-- (void)scheduleCloseBookmarkFolderOnHoverButton;
-- (void)cancelPendingCloseBookmarkFolderOnHoverButton;
-- (void)scheduleOpenBookmarkFolderOnHoverButton;
-- (void)cancelPendingOpenBookmarkFolderOnHoverButton;
-- (BookmarkButton*)hoverButton;
-- (void)setHoverButton:(BookmarkButton*)button;
-- (HoverState)hoverState;
@end
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm
index 0f16da1..5aa8f12 100644
--- a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm
+++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm
@@ -12,6 +12,7 @@
#import "chrome/browser/cocoa/bookmark_bar_controller.h" // namespace bookmarks
#import "chrome/browser/cocoa/bookmark_bar_folder_view.h"
#import "chrome/browser/cocoa/bookmark_bar_folder_button_cell.h"
+#import "chrome/browser/cocoa/bookmark_bar_folder_hover_state.h"
#import "chrome/browser/cocoa/bookmark_folder_target.h"
#import "chrome/browser/cocoa/browser_window_controller.h"
#import "chrome/browser/cocoa/event_utils.h"
@@ -45,33 +46,6 @@ const CGFloat kScrollWindowVerticalMargin = 0.0;
- (void)addScrollTimerWithDelta:(CGFloat)delta;
@end
-// Hover state machine interface.
-// A strict call order is implied with these calls. It is ONLY valid to make
-// the following state transitions:
-// From: To: Via:
-// closed opening scheduleOpen...:
-// opening closed or open cancelPendingOpen...:
-// or scheduleOpen...: completes.
-// open closing scheduleClose...:
-// closing open or closed cancelPendingClose...:
-// or scheduleClose...: completes.
-//
-// Note: Extra private methods are:
-// setHoverState:
-// closeBookmarkFolderOnHoverButton:
-// openBookmarkFolderOnHoverButton:
-// They should not be called directly from other parts of the
-// BookmarkBarFolderController code.
-@interface BookmarkBarFolderController(PrivateHoverStateMachine)
-- (void)setHoverState:(HoverState)state;
-- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button;
-- (void)scheduleCloseBookmarkFolderOnHoverButton;
-- (void)cancelPendingCloseBookmarkFolderOnHoverButton;
-- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button;
-- (void)scheduleOpenBookmarkFolderOnHoverButton;
-- (void)cancelPendingOpenBookmarkFolderOnHoverButton;
-@end
-
@implementation BookmarkBarFolderController
- (id)initWithParentButton:(BookmarkButton*)button
@@ -87,7 +61,7 @@ const CGFloat kScrollWindowVerticalMargin = 0.0;
buttons_.reset([[NSMutableArray alloc] init]);
folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]);
[self configureWindow];
- hoverState_ = kHoverStateClosed;
+ hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]);
if (scrollable_)
[self addOrUpdateScrollTracking];
}
@@ -593,66 +567,16 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
NSPoint currentLocation = [info draggingLocation];
BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation];
- if ([button isFolder]) {
- if (hoverButton_ == button) {
- // CASE A: hoverButton_ == button implies we've dragged over the same
- // folder so no need to open or close anything new.
- } else if (hoverButton_ && hoverButton_ != button) {
- // CASE B: we have a hoverButton_ but it is different from the new button.
- // This implies we've dragged over a new folder, so we'll close the old
- // and open the new.
- // Note that we only schedule the open or close if we have no other tasks
- // currently pending.
-
- if (hoverState_ == kHoverStateOpen) {
- // Close the old.
- [self scheduleCloseBookmarkFolderOnHoverButton];
- } else if (hoverState_ == kHoverStateClosed) {
- // Open the new.
- hoverButton_.reset([button retain]);
- [self scheduleOpenBookmarkFolderOnHoverButton];
- }
- } else if (!hoverButton_) {
- // CASE C: we don't have a current hoverButton_ but we have dragged onto
- // a new folder so we open the new one.
- hoverButton_.reset([button retain]);
- [self scheduleOpenBookmarkFolderOnHoverButton];
- }
- } else if (!button) {
- if (hoverButton_) {
- // CASE D: We have a hoverButton_ but we've moved onto an area that
- // requires no hover. We close the hoverButton_ in this case. This
- // means cancelling if the open is pending (i.e. |kHoverStateOpening|)
- // or closing if we don't alrealy have once in progress.
-
- // Intiate close only if we have not already done so.
- if (hoverState_ == kHoverStateOpening) {
- // Cancel the pending open.
- [self cancelPendingOpenBookmarkFolderOnHoverButton];
- } else if (hoverState_ != kHoverStateClosing) {
- // Schedule the close.
- [self scheduleCloseBookmarkFolderOnHoverButton];
- }
- } else {
- // CASE E: We have neither a hoverButton_ nor a new button that requires
- // a hover. In this case we do nothing.
- }
- }
-
- return NSDragOperationMove;
+ // Delegate handling of dragging over a button to the |hoverState_| member.
+ return [hoverState_ draggingEnteredButton:button];
}
// Unlike bookmark_bar_controller, we need to keep track of dragging state.
// We also need to make sure we cancel the delayed hover close.
- (void)draggingExited:(id<NSDraggingInfo>)info {
// NOT the same as a cancel --> we may have moved the mouse into the submenu.
- if (hoverButton_) {
- if (hoverState_ == kHoverStateOpening) {
- [self cancelPendingOpenBookmarkFolderOnHoverButton];
- } else if (hoverState_ == kHoverStateClosing) {
- [self cancelPendingCloseBookmarkFolderOnHoverButton];
- }
- }
+ // Delegate handling of the hover button to the |hoverState_| member.
+ [hoverState_ draggingExited];
}
- (BOOL)dragShouldLockBarVisibility {
@@ -827,9 +751,6 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
buttonThatMouseIsIn_ = sender;
// Cancel a previous hover if needed.
- // TODO(dhollowa): we're scheduling hoverButton_ operations by using 'self'
- // too. This has the potential to cause trouble. We're avoiding that trouble
- // currently because mouse events are non-overlapping with drag events.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
// If already opened, then we exited but re-entered the button
@@ -855,9 +776,6 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
// this object (or others it may own) is in the event chain. Thus
// we have a retain/autorelease.
[self retain];
- // TODO(dhollowa): we're scheduling hoverButton_ operations by using 'self'
- // too. This has the potential to cause trouble. We're avoiding that trouble
- // currently because mouse events are non-overlapping with drag events.
[NSObject cancelPreviousPerformRequestsWithTarget:self];
[self autorelease];
}
@@ -889,99 +807,6 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) {
[super close];
}
-#pragma mark Hover State Methods
-
-// This method encodes the rules of our |hoverButton_| state machine. Only
-// specific state transitions are allowable (encoded in the DCHECK).
-// Note that there is no state for simultaneously opening and closing. A
-// pending open must complete before scheduling a close, and vice versa. And
-// it is not possible to make a transition directly from open to closed, and
-// vice versa.
-- (void)setHoverState:(HoverState)state {
- DCHECK(
- (hoverState_ == kHoverStateClosed && state == kHoverStateOpening) ||
- (hoverState_ == kHoverStateOpening && state == kHoverStateClosed) ||
- (hoverState_ == kHoverStateOpening && state == kHoverStateOpen) ||
- (hoverState_ == kHoverStateOpen && state == kHoverStateClosing) ||
- (hoverState_ == kHoverStateClosing && state == kHoverStateOpen) ||
- (hoverState_ == kHoverStateClosing && state == kHoverStateClosed)
- ) << "bad transition: old = " << hoverState_ << " new = " << state;
-
- hoverState_ = state;
-}
-
-// Called after a delay to close a previously hover-opened folder.
-// Note: this method is not meant to be invoked directly, only through
-// a delayed call to |scheduleCloseBookmarkFolderOnHoverButton:|.
-- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button {
- [NSObject
- cancelPreviousPerformRequestsWithTarget:self
- selector:@selector(closeBookmarkFolderOnHoverButton:)
- object:hoverButton_];
- [self setHoverState:kHoverStateClosed];
- [[button target] closeBookmarkFolder:button];
- hoverButton_.reset();
-}
-
-// Schedule close of hover button. Transition to kHoverStateClosing state.
-- (void)scheduleCloseBookmarkFolderOnHoverButton {
- [self setHoverState:kHoverStateClosing];
- [self performSelector:@selector(closeBookmarkFolderOnHoverButton:)
- withObject:hoverButton_
- afterDelay:bookmarks::kDragHoverCloseDelay];
-}
-
-// Cancel pending hover close. Transition to kHoverStateOpen state.
-- (void)cancelPendingCloseBookmarkFolderOnHoverButton {
- [self setHoverState:kHoverStateOpen];
- [NSObject
- cancelPreviousPerformRequestsWithTarget:self
- selector:@selector(closeBookmarkFolderOnHoverButton:)
- object:hoverButton_];
-}
-
-// Called after a delay to open a new hover folder.
-// Note: this method is not meant to be invoked directly, only through
-// a delayed call to |scheduleOpenBookmarkFolderOnHoverButton:|.
-- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button {
- [self setHoverState:kHoverStateOpen];
- [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
- withObject:button];
-}
-
-// Schedule open of hover button. Transition to kHoverStateOpening state.
-- (void)scheduleOpenBookmarkFolderOnHoverButton {
- [self setHoverState:kHoverStateOpening];
- [self performSelector:@selector(openBookmarkFolderOnHoverButton:)
- withObject:hoverButton_
- afterDelay:bookmarks::kDragHoverOpenDelay];
-}
-
-// Cancel pending hover open. Transition to kHoverStateClosed state.
-- (void)cancelPendingOpenBookmarkFolderOnHoverButton {
- [self setHoverState:kHoverStateClosed];
- [NSObject
- cancelPreviousPerformRequestsWithTarget:self
- selector:@selector(openBookmarkFolderOnHoverButton:)
- object:hoverButton_];
- hoverButton_.reset();
-}
-
-// Testing only accessor.
-- (BookmarkButton*)hoverButton {
- return hoverButton_;
-}
-
-// Testing only setter.
-- (void)setHoverButton:(BookmarkButton*)button {
- hoverButton_.reset(button);
-}
-
-// Testing only accessor.
-- (HoverState)hoverState {
- return hoverState_;
-}
-
#pragma mark Methods Forwarded to BookmarkBarController
- (IBAction)cutBookmark:(id)sender {
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm
index c7fe92a..a0f8d8e 100644
--- a/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm
+++ b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm
@@ -319,73 +319,6 @@ TEST_F(BookmarkBarFolderControllerTest, ChildFolderWidth) {
EXPECT_GT(wideWidth, thinWidth);
}
-// Hover state machine interface.
-// A strict call order is implied with these calls. It is ONLY valid to make
-// these specific state transitions.
-TEST_F(BookmarkBarFolderControllerTest, HoverState) {
- scoped_nsobject<BookmarkBarFolderController> bbfc;
- bbfc.reset(SimpleBookmarkBarFolderController());
-
- // Initial state.
- EXPECT_FALSE([bbfc hoverButton]);
- ASSERT_EQ(kHoverStateClosed, [bbfc hoverState]);
-
- scoped_nsobject<BookmarkButton> button;
- button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0, 0, 20, 20)]);
-
- // Set initial button.
- [bbfc setHoverButton:[button retain]];
- ASSERT_EQ(button, [bbfc hoverButton]);
-
- // Test transition from closed to opening.
- ASSERT_EQ(kHoverStateClosed, [bbfc hoverState]);
- [bbfc scheduleOpenBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateOpening, [bbfc hoverState]);
-
- // Test transition from opening to closed (aka cancel open).
- [bbfc cancelPendingOpenBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateClosed, [bbfc hoverState]);
- ASSERT_EQ(nil, [bbfc hoverButton]);
-
- // Reset the button.
- [bbfc setHoverButton:[button retain]];
- ASSERT_EQ(button, [bbfc hoverButton]);
-
- // Test transition from closed to opening.
- ASSERT_EQ(kHoverStateClosed, [bbfc hoverState]);
- [bbfc scheduleOpenBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateOpening, [bbfc hoverState]);
-
- // Test transition from opening to opened.
- MessageLoop::current()->PostDelayedTask(
- FROM_HERE,
- new MessageLoop::QuitTask,
- bookmarks::kDragHoverOpenDelay * 1000.0 * 1.5);
- MessageLoop::current()->Run();
- ASSERT_EQ(kHoverStateOpen, [bbfc hoverState]);
- ASSERT_EQ(button, [bbfc hoverButton]);
-
- // Test transition from opening to opened.
- [bbfc scheduleCloseBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateClosing, [bbfc hoverState]);
-
- // Test transition from closing to open (aka cancel close).
- [bbfc cancelPendingCloseBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateOpen, [bbfc hoverState]);
- ASSERT_EQ(button, [bbfc hoverButton]);
-
- // Test transition from closing to closed.
- [bbfc scheduleCloseBookmarkFolderOnHoverButton];
- ASSERT_EQ(kHoverStateClosing, [bbfc hoverState]);
- MessageLoop::current()->PostDelayedTask(
- FROM_HERE,
- new MessageLoop::QuitTask,
- bookmarks::kDragHoverCloseDelay * 1000.0 * 1.5);
- MessageLoop::current()->Run();
- ASSERT_EQ(kHoverStateClosed, [bbfc hoverState]);
- ASSERT_EQ(nil, [bbfc hoverButton]);
-}
-
// Simple scrolling tests.
TEST_F(BookmarkBarFolderControllerTest, SimpleScroll) {
scoped_nsobject<BookmarkBarFolderController> bbfc;
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_hover_state.h b/chrome/browser/cocoa/bookmark_bar_folder_hover_state.h
new file mode 100644
index 0000000..8ed03f7
--- /dev/null
+++ b/chrome/browser/cocoa/bookmark_bar_folder_hover_state.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#include "base/scoped_nsobject.h"
+#import "chrome/browser/cocoa/bookmark_button.h"
+
+// Hover state machine. Encapsulates the hover state for
+// BookmarkBarFolderController.
+// A strict call order is implied with these calls. It is ONLY valid to make
+// the following state transitions:
+// From: To: Via:
+// closed opening scheduleOpen...:
+// opening closed cancelPendingOpen...: or
+// open scheduleOpen...: completes.
+// open closing scheduleClose...:
+// closing open cancelPendingClose...: or
+// closed scheduleClose...: completes.
+//
+@interface BookmarkBarFolderHoverState : NSObject {
+ @private
+ // Enumeration of the valid states that the |hoverButton_| member can be in.
+ // Because the opening and closing of hover views can be done asyncronously
+ // there are periods where the hover state is in transtion between open and
+ // closed. During those times of transition the opening or closing operation
+ // can be cancelled. We serialize the opening and closing of the
+ // |hoverButton_| using this state information. This serialization is to
+ // avoid race conditions where one hover button is being opened while another
+ // is closing.
+ enum HoverState {
+ kHoverStateClosed = 0,
+ kHoverStateOpening = 1,
+ kHoverStateOpen = 2,
+ kHoverStateClosing = 3
+ };
+
+ // Like normal menus, hovering over a folder button causes it to
+ // open. This variable is set when a hover is initiated (but has
+ // not necessarily fired yet).
+ scoped_nsobject<BookmarkButton> hoverButton_;
+
+ // We model hover state as a state machine with specific allowable
+ // transitions. |hoverState_| is the state of this machine at any
+ // given time.
+ HoverState hoverState_;
+}
+
+// Designated initializer.
+- (id)init;
+- (void)dealloc;
+
+// The BookmarkBarFolderHoverState decides when it is appropriate to hide
+// and show the button that the BookmarkBarFolderController drags over.
+- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button;
+
+// The BookmarkBarFolderHoverState decides the fate of the hover button
+// when the BookmarkBarFolderController's view is exited.
+- (void)draggingExited;
+
+@end
+
+// Exposing these for unit testing purposes. They are used privately in the
+// implementation as well.
+@interface BookmarkBarFolderHoverState(PrivateAPI)
+// State change APIs.
+- (void)scheduleCloseBookmarkFolderOnHoverButton;
+- (void)cancelPendingCloseBookmarkFolderOnHoverButton;
+- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)hoverButton;
+- (void)cancelPendingOpenBookmarkFolderOnHoverButton;
+@end
+
+// Exposing these for unit testing purposes. They are used only in tests.
+@interface BookmarkBarFolderHoverState(TestingAPI)
+// Accessors and setters for button and hover state.
+- (BookmarkButton*)hoverButton;
+- (HoverState)hoverState;
+@end
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_hover_state.mm b/chrome/browser/cocoa/bookmark_bar_folder_hover_state.mm
new file mode 100644
index 0000000..27fb156
--- /dev/null
+++ b/chrome/browser/cocoa/bookmark_bar_folder_hover_state.mm
@@ -0,0 +1,177 @@
+// Copyright (c) 2010 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 "chrome/browser/cocoa/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/cocoa/bookmark_bar_controller.h" // namespace bookmarks
+
+@interface BookmarkBarFolderHoverState(Private)
+- (void)setHoverState:(HoverState)state;
+- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button;
+- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button;
+@end
+
+@implementation BookmarkBarFolderHoverState
+
+- (id)init {
+ if ((self = [super init])) {
+ hoverState_ = kHoverStateClosed;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ // Finalize using |draggingExited:| to cancel any pending hover actions.
+ [self draggingExited];
+ [super dealloc];
+}
+
+- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button {
+ if ([button isFolder]) {
+ if (hoverButton_ == button) {
+ // CASE A: hoverButton_ == button implies we've dragged over
+ // the same folder so no need to open or close anything new.
+ } else if (hoverButton_ &&
+ hoverButton_ != button) {
+ // CASE B: we have a hoverButton_ but it is different from the new button.
+ // This implies we've dragged over a new folder, so we'll close the old
+ // and open the new.
+ // Note that we only schedule the open or close if we have no other tasks
+ // currently pending.
+
+ if (hoverState_ == kHoverStateOpen) {
+ // Close the old.
+ [self scheduleCloseBookmarkFolderOnHoverButton];
+ } else if (hoverState_ == kHoverStateClosed) {
+ // Open the new.
+ [self scheduleOpenBookmarkFolderOnHoverButton:[button retain]];
+ }
+ } else if (!hoverButton_) {
+ // CASE C: we don't have a current hoverButton_ but we have dragged onto
+ // a new folder so we open the new one.
+ [self scheduleOpenBookmarkFolderOnHoverButton:[button retain]];
+ }
+ } else if (!button) {
+ if (hoverButton_) {
+ // CASE D: We have a hoverButton_ but we've moved onto an area that
+ // requires no hover. We close the hoverButton_ in this case. This
+ // means cancelling if the open is pending (i.e. |kHoverStateOpening|)
+ // or closing if we don't alrealy have once in progress.
+
+ // Intiate close only if we have not already done so.
+ if (hoverState_ == kHoverStateOpening) {
+ // Cancel the pending open.
+ [self cancelPendingOpenBookmarkFolderOnHoverButton];
+ } else if (hoverState_ != kHoverStateClosing) {
+ // Schedule the close.
+ [self scheduleCloseBookmarkFolderOnHoverButton];
+ }
+ } else {
+ // CASE E: We have neither a hoverButton_ nor a new button that requires
+ // a hover. In this case we do nothing.
+ }
+ }
+
+ return NSDragOperationMove;
+}
+
+- (void)draggingExited {
+ if (hoverButton_) {
+ if (hoverState_ == kHoverStateOpening) {
+ [self cancelPendingOpenBookmarkFolderOnHoverButton];
+ } else if (hoverState_ == kHoverStateClosing) {
+ [self cancelPendingCloseBookmarkFolderOnHoverButton];
+ }
+ }
+}
+
+// Schedule close of hover button. Transition to kHoverStateClosing state.
+- (void)scheduleCloseBookmarkFolderOnHoverButton {
+ DCHECK(hoverButton_);
+ [self setHoverState:kHoverStateClosing];
+ [self performSelector:@selector(closeBookmarkFolderOnHoverButton:)
+ withObject:hoverButton_
+ afterDelay:bookmarks::kDragHoverCloseDelay];
+}
+
+// Cancel pending hover close. Transition to kHoverStateOpen state.
+- (void)cancelPendingCloseBookmarkFolderOnHoverButton {
+ [self setHoverState:kHoverStateOpen];
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(closeBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+}
+
+// Schedule open of hover button. Transition to kHoverStateOpening state.
+- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ DCHECK(button);
+ hoverButton_.reset(button);
+ [self setHoverState:kHoverStateOpening];
+ [self performSelector:@selector(openBookmarkFolderOnHoverButton:)
+ withObject:hoverButton_
+ afterDelay:bookmarks::kDragHoverOpenDelay];
+}
+
+// Cancel pending hover open. Transition to kHoverStateClosed state.
+- (void)cancelPendingOpenBookmarkFolderOnHoverButton {
+ [self setHoverState:kHoverStateClosed];
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(openBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+ hoverButton_.reset();
+}
+
+// Hover button accessor. For testing only.
+- (BookmarkButton*)hoverButton {
+ return hoverButton_;
+}
+
+// Hover state accessor. For testing only.
+- (HoverState)hoverState {
+ return hoverState_;
+}
+
+// This method encodes the rules of our |hoverButton_| state machine. Only
+// specific state transitions are allowable (encoded in the DCHECK).
+// Note that there is no state for simultaneously opening and closing. A
+// pending open must complete before scheduling a close, and vice versa. And
+// it is not possible to make a transition directly from open to closed, and
+// vice versa.
+- (void)setHoverState:(HoverState)state {
+ DCHECK(
+ (hoverState_ == kHoverStateClosed && state == kHoverStateOpening) ||
+ (hoverState_ == kHoverStateOpening && state == kHoverStateClosed) ||
+ (hoverState_ == kHoverStateOpening && state == kHoverStateOpen) ||
+ (hoverState_ == kHoverStateOpen && state == kHoverStateClosing) ||
+ (hoverState_ == kHoverStateClosing && state == kHoverStateOpen) ||
+ (hoverState_ == kHoverStateClosing && state == kHoverStateClosed)
+ ) << "bad transition: old = " << hoverState_ << " new = " << state;
+
+ hoverState_ = state;
+}
+
+// Called after a delay to close a previously hover-opened folder.
+// Note: this method is not meant to be invoked directly, only through
+// a delayed call to |scheduleCloseBookmarkFolderOnHoverButton:|.
+- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ [NSObject
+ cancelPreviousPerformRequestsWithTarget:self
+ selector:@selector(closeBookmarkFolderOnHoverButton:)
+ object:hoverButton_];
+ [self setHoverState:kHoverStateClosed];
+ [[button target] closeBookmarkFolder:button];
+ hoverButton_.reset();
+}
+
+// Called after a delay to open a new hover folder.
+// Note: this method is not meant to be invoked directly, only through
+// a delayed call to |scheduleOpenBookmarkFolderOnHoverButton:|.
+- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button {
+ [self setHoverState:kHoverStateOpen];
+ [[button target] performSelector:@selector(openBookmarkFolderFromButton:)
+ withObject:button];
+}
+
+@end
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm
new file mode 100644
index 0000000..ee10a45
--- /dev/null
+++ b/chrome/browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm
@@ -0,0 +1,77 @@
+// Copyright (c) 2010 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 <Cocoa/Cocoa.h>
+
+#include "base/message_loop.h"
+#import "chrome/browser/cocoa/bookmark_bar_controller.h"
+#import "chrome/browser/cocoa/bookmark_bar_folder_hover_state.h"
+#import "chrome/browser/cocoa/browser_test_helper.h"
+#import "chrome/browser/cocoa/cocoa_test_helper.h"
+
+namespace {
+
+typedef CocoaTest BookmarkBarFolderHoverStateTest;
+
+// Hover state machine interface.
+// A strict call order is implied with these calls. It is ONLY valid to make
+// these specific state transitions.
+TEST(BookmarkBarFolderHoverStateTest, HoverState) {
+ BrowserTestHelper helper;
+ scoped_nsobject<BookmarkBarFolderHoverState> bbfhs;
+ bbfhs.reset([[BookmarkBarFolderHoverState alloc] init]);
+
+ // Initial state.
+ EXPECT_FALSE([bbfhs hoverButton]);
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+
+ scoped_nsobject<BookmarkButton> button;
+ button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0, 0, 20, 20)]);
+
+ // Test transition from closed to opening.
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ [bbfhs scheduleOpenBookmarkFolderOnHoverButton:[button retain]];
+ ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]);
+
+ // Test transition from opening to closed (aka cancel open).
+ [bbfhs cancelPendingOpenBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ ASSERT_EQ(nil, [bbfhs hoverButton]);
+
+ // Test transition from closed to opening.
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ [bbfhs scheduleOpenBookmarkFolderOnHoverButton:[button retain]];
+ ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]);
+
+ // Test transition from opening to opened.
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ new MessageLoop::QuitTask,
+ bookmarks::kDragHoverOpenDelay * 1000.0 * 1.5);
+ MessageLoop::current()->Run();
+ ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]);
+ ASSERT_EQ(button, [bbfhs hoverButton]);
+
+ // Test transition from opening to opened.
+ [bbfhs scheduleCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]);
+
+ // Test transition from closing to open (aka cancel close).
+ [bbfhs cancelPendingCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]);
+ ASSERT_EQ(button, [bbfhs hoverButton]);
+
+ // Test transition from closing to closed.
+ [bbfhs scheduleCloseBookmarkFolderOnHoverButton];
+ ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]);
+ MessageLoop::current()->PostDelayedTask(
+ FROM_HERE,
+ new MessageLoop::QuitTask,
+ bookmarks::kDragHoverCloseDelay * 1000.0 * 1.5);
+ MessageLoop::current()->Run();
+ ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]);
+ ASSERT_EQ(nil, [bbfhs hoverButton]);
+}
+
+} // namespace
diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi
index a897859..ad49954 100644
--- a/chrome/chrome_browser.gypi
+++ b/chrome/chrome_browser.gypi
@@ -516,6 +516,8 @@
'browser/cocoa/bookmark_bar_folder_button_cell.mm',
'browser/cocoa/bookmark_bar_folder_controller.h',
'browser/cocoa/bookmark_bar_folder_controller.mm',
+ 'browser/cocoa/bookmark_bar_folder_hover_state.h',
+ 'browser/cocoa/bookmark_bar_folder_hover_state.mm',
'browser/cocoa/bookmark_bar_folder_view.h',
'browser/cocoa/bookmark_bar_folder_view.mm',
'browser/cocoa/bookmark_bar_folder_window.h',
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index be38e5d..7652182 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -627,6 +627,7 @@
'browser/cocoa/bookmark_bar_controller_unittest.mm',
'browser/cocoa/bookmark_bar_folder_button_cell_unittest.mm',
'browser/cocoa/bookmark_bar_folder_controller_unittest.mm',
+ 'browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm',
'browser/cocoa/bookmark_bar_folder_view_unittest.mm',
'browser/cocoa/bookmark_bar_folder_window_unittest.mm',
'browser/cocoa/bookmark_bar_toolbar_view_unittest.mm',