diff options
author | dhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-20 17:48:59 +0000 |
---|---|---|
committer | dhollowa@chromium.org <dhollowa@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-20 17:48:59 +0000 |
commit | 4c16642677eb1219c6a448c43a95070811b6af7b (patch) | |
tree | 91b6742e868d4495fe6ee51c974c15beeea4ef4f /chrome | |
parent | 7716bfdc8f418838559e026f1bdbbf81a30949b6 (diff) | |
download | chromium_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.h | 33 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller.mm | 187 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm | 67 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_hover_state.h | 79 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_hover_state.mm | 177 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_hover_state_unittest.mm | 77 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 2 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 1 |
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', |