diff options
Diffstat (limited to 'chrome/browser/cocoa/bookmark_bar_folder_controller.mm')
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller.mm | 227 |
1 files changed, 173 insertions, 54 deletions
diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm index 9ae9a5b..0f16da1 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm @@ -34,7 +34,8 @@ const CGFloat kBookmarkBarFolderScrollAmount = // window, but should probably be 8.0 or something. // TODO(jrg): http://crbug.com/36225 const CGFloat kScrollWindowVerticalMargin = 0.0; -} + +} // namespace @interface BookmarkBarFolderController(Private) - (void)configureWindow; @@ -44,6 +45,32 @@ 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 @@ -60,6 +87,7 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; buttons_.reset([[NSMutableArray alloc] init]); folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]); [self configureWindow]; + hoverState_ = kHoverStateClosed; if (scrollable_) [self addOrUpdateScrollTracking]; } @@ -494,22 +522,6 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; folderController_ = nil; } -// Called after a delay to close a previously hover-opened folder. -- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button { - if (hoverButton_ || !draggingExited_) { - // If there is a new one which hover-opened, we're done. If we - // are still inside while dragging but have no hoverButton_ we - // must be hovering over something else (e.g. normal bookmark - // button), so we're still done. - [[button target] closeBookmarkFolder:button]; - hoverButton_.reset(); - } else { - // If there is no hoverOpen but we've exited our window, we may be - // in a subfolder. Restore our state so it's cleaned up later. - hoverButton_.reset([button retain]); - } -} - // Delegate callback. - (void)windowWillClose:(NSNotification*)notification { [parentController_ childFolderWillClose:self]; @@ -578,45 +590,52 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // Caution: there are subtle differences between this one and // bookmark_bar_controller.mm's version. - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { - draggingExited_ = NO; NSPoint currentLocation = [info draggingLocation]; BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation]; + if ([button isFolder]) { if (hoverButton_ == button) { - return NSDragOperationMove; // already open or timed to open. - } - if (hoverButton_) { - // Oops, another one triggered or open. - DCHECK( - [[hoverButton_ target] - respondsToSelector:@selector(closeBookmarkFolderOnHoverButton:)]); - [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ - target]]; - [self performSelector:@selector(closeBookmarkFolderOnHoverButton:) - withObject:[hoverButton_ target] - afterDelay:bookmarks::kDragHoverCloseDelay]; + // 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]; } - hoverButton_.reset([button retain]); - DCHECK([[hoverButton_ target] - respondsToSelector:@selector(openBookmarkFolderFromButton:)]); - [[hoverButton_ target] - performSelector:@selector(openBookmarkFolderFromButton:) - withObject:hoverButton_ - afterDelay:bookmarks::kDragHoverOpenDelay]; - } - - // If we get here we may no longer have a hover button. - if (!button) { + } else if (!button) { if (hoverButton_) { - [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ - target]]; - DCHECK( - [[hoverButton_ target] - respondsToSelector:@selector(closeBookmarkFolderOnHoverButton:)]); - [self performSelector:@selector(closeBookmarkFolderOnHoverButton:) - withObject:hoverButton_ - afterDelay:bookmarks::kDragHoverCloseDelay]; - hoverButton_.reset(); + // 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. } } @@ -626,12 +645,13 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // 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 { - draggingExited_ = YES; // NOT the same as a cancel --> we may have moved the mouse into the submenu. if (hoverButton_) { - [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; - [NSObject cancelPreviousPerformRequestsWithTarget:hoverButton_]; - hoverButton_.reset(); + if (hoverState_ == kHoverStateOpening) { + [self cancelPendingOpenBookmarkFolderOnHoverButton]; + } else if (hoverState_ == kHoverStateClosing) { + [self cancelPendingCloseBookmarkFolderOnHoverButton]; + } } } @@ -807,6 +827,9 @@ 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 @@ -832,6 +855,9 @@ 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]; } @@ -863,6 +889,99 @@ 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 { |