Index: chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm |
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm |
index 3569a619425dd2ea41a34a923a1aa3a9d7f0e605..6740237ddea2b3f698c674dd33a8c8ac494a2a1a 100644 |
--- a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm |
+++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm |
@@ -21,6 +21,7 @@ |
#import "chrome/browser/ui/cocoa/background_gradient_view.h" |
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" |
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" |
+#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" |
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" |
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" |
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" |
@@ -210,6 +211,7 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
- (void)addButtonsToView; |
- (void)centerNoItemsLabel; |
- (void)setNodeForBarMenu; |
+- (void)watchForExitEvent:(BOOL)watch; |
- (void)resetAllButtonPositionsWithAnimation:(BOOL)animate; |
- (BOOL)animationEnabled; |
@@ -325,6 +327,7 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
bridge_.reset(NULL); |
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
+ [self watchForExitEvent:NO]; |
[super dealloc]; |
} |
@@ -366,6 +369,18 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
name:NSViewFrameDidChangeNotification |
object:[self view]]; |
+ // Watch for things going to or from fullscreen. |
+ [[NSNotificationCenter defaultCenter] |
+ addObserver:self |
+ selector:@selector(willEnterOrLeaveFullscreen:) |
+ name:kWillEnterFullscreenNotification |
+ object:nil]; |
+ [[NSNotificationCenter defaultCenter] |
+ addObserver:self |
+ selector:@selector(willEnterOrLeaveFullscreen:) |
+ name:kWillLeaveFullscreenNotification |
+ object:nil]; |
+ |
// Don't pass ourself along (as 'self') until our init is completely |
// done. Thus, this call is (almost) last. |
bridge_.reset(new BookmarkBarBridge(self, bookmarkModel_)); |
@@ -396,6 +411,23 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
object:[[self view] window]]; |
} |
+// When going fullscreen we can run into trouble. Our view is removed |
+// from the non-fullscreen window before the non-fullscreen window |
+// loses key, so our parentDidResignKey: callback never gets called. |
+// In addition, a bookmark folder controller needs to be autoreleased |
+// (in case it's in the event chain when closed), but the release |
+// implicitly needs to happen while it's connected to the original |
+// (non-fullscreen) window to "unlock bar visibility". Such a |
+// contract isn't honored when going fullscreen with the menu option |
+// (not with the keyboard shortcut). We fake it as best we can here. |
+// We have a similar problem leaving fullscreen. |
+- (void)willEnterOrLeaveFullscreen:(NSNotification*)notification { |
+ if (folderController_) { |
+ [self childFolderWillClose:folderController_]; |
+ [self closeFolderAndStopTrackingMenus]; |
+ } |
+} |
+ |
// NSNotificationCenter callback. |
- (void)parentWindowWillClose:(NSNotification*)notification { |
[self closeFolderAndStopTrackingMenus]; |
@@ -516,9 +548,79 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
#pragma mark Actions |
+// Helper methods called on the main thread by runMenuFlashThread. |
+ |
+- (void)setButtonFlashStateOn:(id)sender { |
+ [sender highlight:YES]; |
+} |
+ |
+- (void)setButtonFlashStateOff:(id)sender { |
+ [sender highlight:NO]; |
+} |
+ |
+-(void)cleanupAfterMenuFlashThread:(id)sender { |
+ [self closeFolderAndStopTrackingMenus]; |
+ |
+ // Items retained by doMenuFlashOnSeparateThread below. |
+ [sender release]; |
+ [self release]; |
+} |
+ |
+// End runMenuFlashThread helper methods. |
+ |
+// This call is invoked only by doMenuFlashOnSeparateThread below. |
+// It makes the selected BookmarkButton (which is masquerading as a menu item) |
+// flash a few times to give confirmation feedback, then it closes the menu. |
+// It spends all its time sleeping or scheduling UI work on the main thread. |
+- (void)runMenuFlashThread:(id)sender { |
+ |
+ // Check this is not running on the main thread, as it sleeps. |
+ DCHECK(![NSThread isMainThread]); |
+ |
+ // Duration of flash phases and number of flashes designed to evoke a |
+ // slightly retro "more mac-like than the Mac" feel. |
+ // Current Cocoa UI has a barely perceptible flash,probably because Apple |
+ // doesn't fire the action til after the animation and so there's a hurry. |
+ // As this code is fully asynchronous, it can take its time. |
+ const float kBBOnFlashTime = 0.08; |
+ const float kBBOffFlashTime = 0.08; |
+ const int kBookmarkButtonMenuFlashes = 3; |
+ |
+ for (int count = 0 ; count < kBookmarkButtonMenuFlashes ; count++) { |
+ [self performSelectorOnMainThread:@selector(setButtonFlashStateOn:) |
+ withObject:sender |
+ waitUntilDone:NO]; |
+ [NSThread sleepForTimeInterval:kBBOnFlashTime]; |
+ [self performSelectorOnMainThread:@selector(setButtonFlashStateOff:) |
+ withObject:sender |
+ waitUntilDone:NO]; |
+ [NSThread sleepForTimeInterval:kBBOffFlashTime]; |
+ } |
+ [self performSelectorOnMainThread:@selector(cleanupAfterMenuFlashThread:) |
+ withObject:sender |
+ waitUntilDone:NO]; |
+} |
+ |
+// Non-blocking call which starts the process to make the selected menu item |
+// flash a few times to give confirmation feedback, after which it closes the |
+// menu. The item is of course actually a BookmarkButton masquerading as a menu |
+// item). |
+- (void)doMenuFlashOnSeparateThread:(id)sender { |
+ |
+ // Ensure that self and sender don't go away before the animation completes. |
+ // These retains are balanced in cleanupAfterMenuFlashThread above. |
+ [self retain]; |
+ [sender retain]; |
+ [NSThread detachNewThreadSelector:@selector(runMenuFlashThread:) |
+ toTarget:self |
+ withObject:sender]; |
+} |
+ |
- (IBAction)openBookmark:(id)sender { |
- BOOL isMenuItem = [sender isFolder]; |
+ BOOL isMenuItem = [[sender cell] isFolderButtonCell]; |
BOOL animate = isMenuItem && [self animationEnabled]; |
+ if (animate) |
+ [self doMenuFlashOnSeparateThread:sender]; |
DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); |
const BookmarkNode* node = [sender bookmarkNode]; |
WindowOpenDisposition disposition = |
@@ -537,6 +639,9 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
showFolderMenus_ = !showFolderMenus_; |
+ if (sender == offTheSideButton_) |
+ [[sender cell] setStartingChildIndex:displayedButtonCount_]; |
+ |
// Toggle presentation of bar folder menus. |
[folderTarget_ openBookmarkFolderFromButton:sender]; |
} |
@@ -754,8 +859,16 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
// Configure the off-the-side button (e.g. specify the node range, |
// check if we should enable or disable it, etc). |
- (void)configureOffTheSideButtonContentsAndVisibility { |
+ // If deleting a button while off-the-side is open, buttons may be |
+ // promoted from off-the-side to the bar. Accomodate. |
+ if (folderController_ && |
+ ([folderController_ parentButton] == offTheSideButton_)) { |
+ [folderController_ reconfigureMenu]; |
+ } |
+ |
+ [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; |
[[offTheSideButton_ cell] |
- setBookmarkNode:bookmarkModel_->bookmark_bar_node()]; |
+ setBookmarkNode:bookmarkModel_->bookmark_bar_node()]; |
int bookmarkChildren = bookmarkModel_->bookmark_bar_node()->child_count(); |
if (bookmarkChildren > displayedButtonCount_) { |
[offTheSideButton_ setHidden:NO]; |
@@ -770,6 +883,59 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
} |
} |
+// Main menubar observation code, so we can know to close our fake menus if the |
+// user clicks on the actual menubar, as multiple unconnected menus sharing |
+// the screen looks weird. |
+// Needed because the hookForEvent method doesn't see the click on the menubar. |
+ |
+// Gets called when the menubar is clicked. |
+- (void)begunTracking:(NSNotification *)notification { |
+ [self closeFolderAndStopTrackingMenus]; |
+} |
+ |
+// Install the callback. |
+- (void)startObservingMenubar { |
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
+ [nc addObserver:self |
+ selector:@selector(begunTracking:) |
+ name:NSMenuDidBeginTrackingNotification |
+ object:[NSApp mainMenu]]; |
+} |
+ |
+// Remove the callback. |
+- (void)stopObservingMenubar { |
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; |
+ [nc removeObserver:self |
+ name:NSMenuDidBeginTrackingNotification |
+ object:[NSApp mainMenu]]; |
+} |
+ |
+// End of menubar observation code. |
+ |
+// Begin (or end) watching for a click outside this window. Unlike |
+// normal NSWindows, bookmark folder "fake menu" windows do not become |
+// key or main. Thus, traditional notification (e.g. WillResignKey) |
+// won't work. Our strategy is to watch (at the app level) for a |
+// "click outside" these windows to detect when they logically lose |
+// focus. |
+- (void)watchForExitEvent:(BOOL)watch { |
+ CrApplication* app = static_cast<CrApplication*>([NSApplication |
+ sharedApplication]); |
+ DCHECK([app isKindOfClass:[CrApplication class]]); |
+ if (watch) { |
+ if (!watchingForExitEvent_) { |
+ [app addEventHook:self]; |
+ [self startObservingMenubar]; |
+ } |
+ } else { |
+ if (watchingForExitEvent_) { |
+ [app removeEventHook:self]; |
+ [self stopObservingMenubar]; |
+ } |
+ } |
+ watchingForExitEvent_ = watch; |
+} |
+ |
// Keep the "no items" label centered in response to a frame size change. |
- (void)centerNoItemsLabel { |
// Note that this computation is done in the parent's coordinate system, |
@@ -1641,6 +1807,66 @@ void RecordAppLaunch(Profile* profile, GURL url) { |
[[otherBookmarksButton_ cell] setTextColor:color]; |
} |
+// Return YES if the event indicates an exit from the bookmark bar |
+// folder menus. E.g. "click outside" of the area we are watching. |
+// At this time we are watching the area that includes all popup |
+// bookmark folder windows. |
+- (BOOL)isEventAnExitEvent:(NSEvent*)event { |
+ NSWindow* eventWindow = [event window]; |
+ NSWindow* myWindow = [[self view] window]; |
+ switch ([event type]) { |
+ case NSLeftMouseDown: |
+ case NSRightMouseDown: |
+ // If the click is in my window but NOT in the bookmark bar, consider |
+ // it a click 'outside'. Clicks directly on an active button (i.e. one |
+ // that is a folder and for which its folder menu is showing) are 'in'. |
+ // All other clicks on the bookmarks bar are counted as 'outside' |
+ // because they should close any open bookmark folder menu. |
+ if (eventWindow == myWindow) { |
+ NSView* hitView = |
+ [[eventWindow contentView] hitTest:[event locationInWindow]]; |
+ if (hitView == [folderController_ parentButton]) |
+ return NO; |
+ if (![hitView isDescendantOf:[self view]] || hitView == buttonView_) |
+ return YES; |
+ } |
+ // If a click in a bookmark bar folder window and that isn't |
+ // one of my bookmark bar folders, YES is click outside. |
+ if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow |
+ class]]) { |
+ return YES; |
+ } |
+ break; |
+ case NSKeyDown: { |
+ // Event hooks often see the same keydown event twice due to the way key |
+ // events get dispatched and redispatched, so ignore if this keydown |
+ // event has the EXACT same timestamp as the previous keydown. |
+ static NSTimeInterval lastKeyDownEventTime; |
+ NSTimeInterval thisTime = [event timestamp]; |
+ if (lastKeyDownEventTime != thisTime) { |
+ lastKeyDownEventTime = thisTime; |
+ if ([event modifierFlags] & NSCommandKeyMask) |
+ return YES; |
+ else if (folderController_) |
+ return [folderController_ handleInputText:[event characters]]; |
+ } |
+ return NO; |
+ } |
+ case NSKeyUp: |
+ return NO; |
+ case NSLeftMouseDragged: |
+ // We can get here with the following sequence: |
+ // - open a bookmark folder |
+ // - right-click (and unclick) on it to open context menu |
+ // - move mouse to window titlebar then click-drag it by the titlebar |
+ // http://crbug.com/49333 |
+ return NO; |
+ default: |
+ break; |
+ } |
+ return NO; |
+} |
+ |
#pragma mark Drag & Drop |
// Find something like std::is_between<T>? I can't believe one doesn't exist. |
@@ -1979,6 +2205,9 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
return; |
} |
} |
+ |
+ if (folderController_) |
+ [folderController_ faviconLoadedForNode:node]; |
} |
// TODO(jrg): for now this is brute force. |
@@ -2087,6 +2316,16 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// If already opened, then we exited but re-entered the button, so do nothing. |
if ([folderController_ parentButton] == sender) |
return; |
+ // Else open a new one if it makes sense to do so. |
+ if ([sender bookmarkNode]->is_folder()) { |
+ // Update |hoverButton_| so that it corresponds to the open folder. |
+ hoverButton_.reset([sender retain]); |
+ [folderTarget_ openBookmarkFolderFromButton:sender]; |
+ } else { |
+ // We're over a non-folder bookmark so close any old folders. |
+ [folderController_ close]; |
+ folderController_ = nil; |
+ } |
} |
// BookmarkButtonDelegate protocol implementation. |
@@ -2127,7 +2366,8 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// Close all bookmark folders. "Folder" here is the fake menu for |
// bookmark folders, not a button context menu. |
- (void)closeAllBookmarkFolders { |
- [folderController_ closeMenu]; |
+ [self watchForExitEvent:NO]; |
+ [folderController_ close]; |
folderController_ = nil; |
} |
@@ -2182,9 +2422,17 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
hoverButton_.reset(); |
} |
hoverButton_.reset([button retain]); |
+ DCHECK([[hoverButton_ target] |
+ respondsToSelector:@selector(openBookmarkFolderFromButton:)]); |
+ [[hoverButton_ target] |
+ performSelector:@selector(openBookmarkFolderFromButton:) |
+ withObject:hoverButton_ |
+ afterDelay:bookmarks::kDragHoverOpenDelay |
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]]; |
} |
if (!button) { |
if (hoverButton_) { |
+ [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; |
[[hoverButton_ target] closeBookmarkFolder:hoverButton_]; |
hoverButton_.reset(); |
} |
@@ -2289,6 +2537,29 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
return x; |
} |
+- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { |
+ // If the bookmarkbar is not in detached mode, lock bar visibility, forcing |
+ // the overlay to stay open when in fullscreen mode. |
+ if (![self isInState:bookmarks::kDetachedState] && |
+ ![self isAnimatingToState:bookmarks::kDetachedState]) { |
+ BrowserWindowController* browserController = |
+ [BrowserWindowController browserWindowControllerForView:[self view]]; |
+ [browserController lockBarVisibilityForOwner:child |
+ withAnimation:NO |
+ delay:NO]; |
+ } |
+} |
+ |
+- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { |
+ // Release bar visibility, allowing the overlay to close if in fullscreen |
+ // mode. |
+ BrowserWindowController* browserController = |
+ [BrowserWindowController browserWindowControllerForView:[self view]]; |
+ [browserController releaseBarVisibilityForOwner:child |
+ withAnimation:NO |
+ delay:NO]; |
+} |
+ |
// Add a new folder controller as triggered by the given folder button. |
- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { |
@@ -2308,15 +2579,13 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// Folder controller, like many window controllers, owns itself. |
folderController_ = |
[[BookmarkBarFolderController alloc] initWithParentButton:parentButton |
- bookmarkModel:bookmarkModel_ |
+ parentController:nil |
barController:self]; |
- [folderController_ autorelease]; |
+ [folderController_ showWindow:self]; |
- // If this is for the off-the-side menu, set the display count. |
- if (parentButton == offTheSideButton_) |
- [folderController_ setOffTheSideNodeStartIndex:displayedButtonCount_]; |
- |
- [folderController_ openMenu]; |
+ // Only BookmarkBarController has this; the |
+ // BookmarkBarFolderController does not. |
+ [self watchForExitEvent:YES]; |
// No longer need to hold the lock; the folderController_ now owns it. |
[browserController releaseBarVisibilityForOwner:self |
@@ -2357,6 +2626,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// A button from somewhere else (not the bar) is being moved to the |
// off-the-side so insure it gets redrawn if its showing. |
[self reconfigureBookmarkBar]; |
+ [folderController_ reconfigureMenu]; |
} |
} |
@@ -2424,6 +2694,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// while possibly re-laying out the bookmark bar. |
[self removeButton:fromIndex animate:NO]; |
[self reconfigureBookmarkBar]; |
+ [folderController_ reconfigureMenu]; |
} else if (toIndex < buttonCount) { |
// A button is being added to the bar and removed from off-the-side. |
// By now the node has already been inserted into the model so the |
@@ -2433,6 +2704,11 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
DCHECK(movedNode); |
[self addButtonForNode:movedNode atIndex:toIndex]; |
[self reconfigureBookmarkBar]; |
+ } else { |
+ // A button is being moved within the off-the-side. |
+ fromIndex -= buttonCount; |
+ toIndex -= buttonCount; |
+ [folderController_ moveButtonFromIndex:fromIndex toIndex:toIndex]; |
} |
} |
} |
@@ -2457,6 +2733,12 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
--displayedButtonCount_; |
[self resetAllButtonPositionsWithAnimation:YES]; |
[self reconfigureBookmarkBar]; |
+ } else if (folderController_ && |
+ [folderController_ parentButton] == offTheSideButton_) { |
+ // The button being removed is in the OTS (off-the-side) and the OTS |
+ // menu is showing so we need to remove the button. |
+ NSInteger index = buttonIndex - displayedButtonCount_; |
+ [folderController_ removeButton:index animate:YES]; |
} |
} |
@@ -2466,7 +2748,15 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { |
// folder menus. |
if (bookmarkModel_->bookmark_bar_node() == node) |
return self; |
- return nil; |
+ return [folderController_ controllerForNode:node]; |
+} |
+ |
+#pragma mark BookmarkButtonControllerProtocol |
+ |
+// NOT an override of a standard Cocoa call made to NSViewControllers. |
+- (void)hookForEvent:(NSEvent*)theEvent { |
+ if ([self isEventAnExitEvent:theEvent]) |
+ [self closeFolderAndStopTrackingMenus]; |
} |
#pragma mark TestingAPI Only |