diff options
author | mrossetti@chromium.org <mrossetti@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-29 18:24:32 +0000 |
---|---|---|
committer | mrossetti@chromium.org <mrossetti@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-04-29 18:24:32 +0000 |
commit | 94d2135c70fc622226a72fd970ed1d3dd0428b6d (patch) | |
tree | 5189dc8b8ff562288411b831a8eae399fcc02f2f | |
parent | 54cfd69da88df660a056ababe8f9ed8aff8d8e9b (diff) | |
download | chromium_src-94d2135c70fc622226a72fd970ed1d3dd0428b6d.zip chromium_src-94d2135c70fc622226a72fd970ed1d3dd0428b6d.tar.gz chromium_src-94d2135c70fc622226a72fd970ed1d3dd0428b6d.tar.bz2 |
Add and implement cut/copy/paste to the bookmark bar and folder context menus.Prevent dropping bookmark folders onto their children to avoid creating cycles and crashing. Keep folder menu windows open after a cut/copy/paste/delete and drop operation. Add test helpers for easing comparison of bookmark model changes.
BUG=41217,36614,32064, 41404
Add and implement cut/copy/paste to the bookmark bar and folder context menus.Prevent dropping bookmark folders onto their children to avoid creating cycles and crashing. Keep folder menu windows open after a cut/copy/paste/delete and drop operation.
TEST=1) Present the context menu for the bookmark bar in various places and insure that the cut/copy/paste menu items are shown and properly enabled or disabled.
2) Present the context menu for a folder menu in various places and insure that the cut/copy/paste menu items are shown and properly enabled or disabled.
3) Perform cut/copy/paste/delete operations in a folder menu and subfolder menu and verify that the menu remains presented after the operation completes.
4) Drag a bookmark item from the bar to another location on the bar and verify that the button no longer shows up in the old location but does show up in the new location.
5) Drag a bookmark item from the bar to be within a folder located _after_ the item being dragged. Verify the bar no longer shows the bookmark item. Verify the folder does show the bookmark item. Verify that the folder window has shifted to the left so that it remains aligned with it corresponding bar folder indicator.
6) Drag a bookmark item from the bar to be within a folder located _before_ the item being dragged. Verify the bar no longer shows the bookmark item. Verify the folder does show the bookmark item. Verify that the folder window has _not_ shifted to the left and that it remains aligned with it corresponding bar folder indicator.
7) Drag a bookmark around within the same folder and verify that the folder window does not move.
8) Drag a bookmark item from a folder to the bar in a position _after_ the folder form which the item comes. The folder menu window should close.
9) Drag a bookmark item from a folder to a different folder and verify that the original folder closes and the new folder remains open.
10) Drag an item from a parent folder to a child folder and verify that both folders remain open. Verify that the subfolder remains aligned to its associated folder icon in the parent folder.
11) Drag an item from a child folder to a parent folder. Verify that the child folder closes and the parent folder remains open.
12) Try dragging a parent folder onto a child folder and verify that the drag is not allowed to complete.
13) Hover over a folder item in a folder menu window. Verify that the folder opens up a subview menu window. Now drag the original folder (the one being hovered over) and verify that its subfolder menu automatically closes.
14) Slowly shrink window width until "off the side" menu appears; make sure it appears as soon as the last button gets removed. Now slowly grow window and make sure it goes away at the right time.
15) Add enough bookmarks to the bookmark bar to nearly fill the width of the window. Verify that the off-the-side chevron is not showing. Slowly shrink the width of the window until the off-the-side-chevron appears. Verify that the right-most bookmark button has disappeared and does not draw over the top of the chevron. Continue to shrink the window and verify that bookmark buttons disappear. View the contents of the chevron and verify that the bookmark buttons which previously appeared on the bar now appear in the chevron. Slowly grow the window and verify that bookmark buttons reappear without drawing over the top of the chevron. Verify that those bookmark buttons no longer appear in the chevron when it is popped up. Continue growing the window until all bookmark buttons appear in the bar and the chevron disappears.
16) Shrink the window so that the off-the-side chevron appears and contains three or more bookmarks. Pop open the chevron. Drag bookmarks up and down in the chevron menu and verify that moves and copies are performed. Drag a bar bookmark into the chevron and verify that the chevron pops up and allows the drag. Verify that the bar and chevron contents rearrange such that buttons on the bar fill but do not overflow the bar. Drag a chevron bookmark out onto the bar. Once again verify that the bar and chevron buttons rearrange such that the bar is full but not overflowing.
Review URL: http://codereview.chromium.org/1742003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@45965 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/browser/bookmarks/bookmark_model_unittest.cc | 47 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_controller.h | 19 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_controller.mm | 410 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_controller_unittest.mm | 235 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller.h | 10 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller.mm | 271 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm | 574 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_view.mm | 5 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm | 15 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_unittest_helper.h | 48 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_bar_unittest_helper.mm | 67 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_button.h | 30 | ||||
-rw-r--r-- | chrome/browser/cocoa/bookmark_button.mm | 1 | ||||
-rw-r--r-- | chrome/chrome_tests.gypi | 4 | ||||
-rw-r--r-- | chrome/test/model_test_utils.cc | 82 | ||||
-rw-r--r-- | chrome/test/model_test_utils.h | 40 |
16 files changed, 1638 insertions, 220 deletions
diff --git a/chrome/browser/bookmarks/bookmark_model_unittest.cc b/chrome/browser/bookmarks/bookmark_model_unittest.cc index f1c9c93..9d85f1f 100644 --- a/chrome/browser/bookmarks/bookmark_model_unittest.cc +++ b/chrome/browser/bookmarks/bookmark_model_unittest.cc @@ -17,6 +17,7 @@ #include "chrome/common/chrome_paths.h" #include "chrome/common/notification_registrar.h" #include "chrome/common/notification_service.h" +#include "chrome/test/model_test_utils.h" #include "chrome/test/testing_profile.h" #include "testing/gtest/include/gtest/gtest.h" @@ -36,24 +37,6 @@ void SwapDateAdded(BookmarkNode* n1, BookmarkNode* n2) { n2->set_date_added(tmp); } -// Helper function used to verify node arrangements. -std::wstring ModelStringFromNode(const BookmarkNode* node) { - // Since the children of the node are not available as a vector, - // we'll just have to do it the hard way. - int childCount = node->GetChildCount(); - std::wstring childString; - for (int i = 0; i < childCount; ++i) { - const BookmarkNode* child = node->GetChild(i); - if (child->is_folder()) { - childString += child->GetTitle() + L":[ " + ModelStringFromNode(child) - + L"] "; - } else { - childString += child->GetTitle() + L" "; - } - } - return childString; -} - } // anonymous namespace class BookmarkModelTest : public testing::Test, public BookmarkModelObserver { @@ -326,61 +309,53 @@ TEST_F(BookmarkModelTest, Move) { TEST_F(BookmarkModelTest, Copy) { const BookmarkNode* root = model.GetBookmarkBarNode(); - model.AddURL(root, 0, L"a", GURL("http://a.com")); - const BookmarkNode* node1 = model.AddGroup(root, 1, L"1"); - model.AddURL(node1, 0, L"b", GURL("http://b.com")); - model.AddURL(node1, 1, L"c", GURL("http://c.com")); - model.AddURL(root, 2, L"d", GURL("http://d.com")); - const BookmarkNode* node2 = model.AddGroup(root, 3, L"2"); - model.AddURL(node2, 0, L"e", GURL("http://e.com")); - model.AddURL(node2, 1, L"f", GURL("http://f.com")); - model.AddURL(node2, 2, L"g", GURL("http://g.com")); - model.AddURL(root, 4, L"h", GURL("http://d.com")); + static const std::wstring model_string(L"a 1:[ b c ] d 2:[ e f g ] h "); + model_test_utils::AddNodesFromModelString(model, root, model_string); // Validate initial model. - std::wstring actualModelString = ModelStringFromNode(root); - EXPECT_EQ(L"a 1:[ b c ] d 2:[ e f g ] h ", ModelStringFromNode(root)); + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); // Copy 'd' to be after '1:b': URL item from bar to folder. const BookmarkNode* nodeToCopy = root->GetChild(2); const BookmarkNode* destination = root->GetChild(1); model.Copy(nodeToCopy, destination, 1); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a 1:[ b d c ] d 2:[ e f g ] h ", actualModelString); // Copy '1:d' to be after 'a': URL item from folder to bar. const BookmarkNode* group = root->GetChild(1); nodeToCopy = group->GetChild(1); model.Copy(nodeToCopy, root, 1); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a d 1:[ b d c ] d 2:[ e f g ] h ", actualModelString); // Copy '1' to be after '2:e': Folder from bar to folder. nodeToCopy = root->GetChild(2); destination = root->GetChild(4); model.Copy(nodeToCopy, destination, 1); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f g ] h ", actualModelString); // Copy '2:1' to be after '2:f': Folder within same folder. group = root->GetChild(4); nodeToCopy = group->GetChild(1); model.Copy(nodeToCopy, group, 3); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h ", actualModelString); // Copy first 'd' to be after 'h': URL item within the bar. nodeToCopy = root->GetChild(1); model.Copy(nodeToCopy, root, 6); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a d 1:[ b d c ] d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ", actualModelString); // Copy '2' to be after 'a': Folder within the bar. nodeToCopy = root->GetChild(4); model.Copy(nodeToCopy, root, 1); - actualModelString = ModelStringFromNode(root); + actualModelString = model_test_utils::ModelStringFromNode(root); EXPECT_EQ(L"a 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] d 1:[ b d c ] " L"d 2:[ e 1:[ b d c ] f 1:[ b d c ] g ] h d ", actualModelString); diff --git a/chrome/browser/cocoa/bookmark_bar_controller.h b/chrome/browser/cocoa/bookmark_bar_controller.h index d9183b4..29952b8 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.h +++ b/chrome/browser/cocoa/bookmark_bar_controller.h @@ -44,13 +44,17 @@ const CGFloat kDefaultBookmarkWidth = 150.0; // TODO(jrg): http://crbug.com/36276 to get final sizes. // Used as a min/max width for buttons on menus (not on the bar). -const CGFloat kBookmarkMenuButtonMinimumWidth = kDefaultBookmarkWidth; +const CGFloat kBookmarkMenuButtonMinimumWidth = 100.0; const CGFloat kBookmarkMenuButtonMaximumWidth = 485.0; const CGFloat kBookmarkVerticalPadding = 2.0; const CGFloat kBookmarkHorizontalPadding = 1.0; // (end magic numbers from Cole) +// Make subfolder menus overlap their parent menu a bit to give a better +// perception of a menuing system. +const CGFloat kBookmarkMenuOverlap = 1.0; + // Delay before opening a subfolder (and closing the previous one) // when hovering over a folder button. const NSTimeInterval kHoverOpenDelay = 0.3; @@ -208,11 +212,7 @@ willAnimateFromState:(bookmarks::VisualState)oldState // Since we create everything before doing layout we can't be sure // that all bookmark buttons we create will be visible. Thus, // [buttons_ count] isn't a definitive check. - int bookmarkBarDisplayedButtons_; - - // We only build the off-the-side menu on demand. - // We flag a need to rebuild with this variable. - BOOL needToRebuildOffTheSideMenu_; + int displayedButtonCount_; } @property(readonly, nonatomic) bookmarks::VisualState visualState; @@ -264,9 +264,6 @@ willAnimateFromState:(bookmarks::VisualState)oldState // Provide a favIcon for a bookmark node. May return nil. - (NSImage*)favIconForNode:(const BookmarkNode*)node; -// Provide a contextual menu for a bookmark node. May return nil. -- (NSMenu*)contextMenuForNode:(const BookmarkNode*)node; - // Actions for manipulating bookmarks. // Open a normal bookmark or folder from a button, ... - (IBAction)openBookmark:(id)sender; @@ -318,7 +315,7 @@ willAnimateFromState:(bookmarks::VisualState)oldState - (NSCell*)cellForBookmarkNode:(const BookmarkNode*)node; - (void)clearBookmarkBar; - (BookmarkBarView*)buttonView; -- (NSArray*)buttons; +- (NSMutableArray*)buttons; - (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell xOffset:(int*)xOffset; - (void)checkForBookmarkButtonGrowth:(NSButton*)button; - (void)frameDidChange; @@ -336,6 +333,8 @@ willAnimateFromState:(bookmarks::VisualState)oldState - (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point; - (BOOL)isEventAnExitEvent:(NSEvent*)event; - (id)folderTarget; +- (int)displayedButtonCount; + @end // The (internal) |NSPasteboard| type string for bookmark button drags, used for diff --git a/chrome/browser/cocoa/bookmark_bar_controller.mm b/chrome/browser/cocoa/bookmark_bar_controller.mm index 1ebe272..809cba4 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller.mm @@ -138,6 +138,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // Modifies self->buttons_. Do not add more buttons than will fit on the view. - (void)addNodesToButtonList:(const BookmarkNode*)node; +// Create an autoreleased button appropriate for insertion into the bookmark +// bar. Update |xOffset| with the offset appropriate for the subsequent button. +- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node + xOffset:(int*)xOffset; + // Puts stuff into the final visual state without animating, stopping a running // animation if necessary. - (void)finalizeVisualState; @@ -161,6 +166,14 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; - (int)indexForDragOfButton:(BookmarkButton*)sourceButton toPoint:(NSPoint)point; +// Add or remove buttons to/from the bar until it is filled but not overflowed. +- (void)redistributeButtonsOnBarAsNeeded; + +// Determine the nature of the bookmark bar contents based on the number of +// buttons showing. If too many then show the off-the-side list, if none +// then show the no items label. +- (void)reconfigureBookmarkBar; + - (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu; - (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu; - (void)tagEmptyMenu:(NSMenu*)menu; @@ -368,11 +381,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // Configure the off-the-side button (e.g. specify the node range, // check if we should enable or disable it, etc). - (void)configureOffTheSideButtonContentsAndVisibility { - [[offTheSideButton_ cell] setStartingChildIndex:bookmarkBarDisplayedButtons_]; + [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; [[offTheSideButton_ cell] setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()]; int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->GetChildCount(); - if (bookmarkChildren > bookmarkBarDisplayedButtons_) { + if (bookmarkChildren > displayedButtonCount_) { [offTheSideButton_ setHidden:NO]; } else { [offTheSideButton_ setHidden:YES]; @@ -387,18 +400,8 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; - (void)frameDidChange { if (!bookmarkModel_->IsLoaded()) return; - CGFloat width = NSWidth([[self view] frame]); - if (width > savedFrameWidth_) { - savedFrameWidth_ = width; - [self clearBookmarkBar]; - [self addNodesToButtonList:bookmarkModel_->GetBookmarkBarNode()]; - [self updateTheme:[[[self view] window] themeProvider]]; - [self addButtonsToView]; - } - [self positionOffTheSideButton]; - [self addButtonsToView]; - [self configureOffTheSideButtonContentsAndVisibility]; - [self centerNoItemsLabel]; + [self updateTheme:[[[self view] window] themeProvider]]; + [self reconfigureBookmarkBar]; } // Close all bookmark folders. "Folder" here is the fake menu for @@ -449,6 +452,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // Else open a new one if it makes sense to do so. if ([sender bookmarkNode]->is_folder()) [folderTarget_ openBookmarkFolderFromButton:sender]; +#if 1 + else + // We're over a non-folder bookmark so close any old folders. + [self closeAllBookmarkFolders]; +#endif } // BookmarkButtonDelegate protocol implementation. @@ -528,6 +536,9 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; return folderTarget_.get(); } +- (int)displayedButtonCount { + return displayedButtonCount_; +} // Keep the "no items" label centered in response to a frame size change. - (void)centerNoItemsLabel { @@ -742,11 +753,11 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; DCHECK(afterNode); int index = afterNode->GetParent()->IndexOfChild(afterNode); // Make sure we don't get confused by buttons which aren't visible. - return std::min(index, bookmarkBarDisplayedButtons_); + return std::min(index, displayedButtonCount_); } // If nothing is to my right I am at the end! - return bookmarkBarDisplayedButtons_; + return displayedButtonCount_; } - (BOOL)dragButton:(BookmarkButton*)sourceButton @@ -776,11 +787,10 @@ const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; // Be sure we don't try and drop a folder into itself. if (sourceNode != destParent) { - if (copy) { + if (copy) bookmarkModel_->Copy(sourceNode, destParent, destIndex); - } else { + else bookmarkModel_->Move(sourceNode, destParent, destIndex); - } } [self closeAllBookmarkFolders]; // For a hover open, if needed. @@ -862,6 +872,21 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { NSPoint point = [info draggingLocation]; BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + + // Don't allow drops that would result in cycles. + if (button) { + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + if (data && [info draggingSource]) { + BookmarkButton* sourceButton = nil; + [data getBytes:&sourceButton length:sizeof(sourceButton)]; + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + const BookmarkNode* destNode = [button bookmarkNode]; + if (destNode->HasAncestor(sourceNode)) + button = nil; + } + } + if ([button isFolder]) { if (hoverButton_ == button) { return NSDragOperationMove; // already open or timed to open @@ -916,7 +941,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { toPoint:(NSPoint)point { CGFloat x = 0; int destIndex = [self indexForDragOfButton:sourceButton toPoint:point]; - int numButtons = bookmarkBarDisplayedButtons_; + int numButtons = displayedButtonCount_; // If it's a drop strictly between existing buttons ... if (destIndex >= 0 && destIndex < numButtons) { @@ -1121,7 +1146,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { - (IBAction)openOffTheSideFolderFromButton:(id)sender { DCHECK([sender isKindOfClass:[BookmarkButton class]]); DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]); - [[sender cell] setStartingChildIndex:bookmarkBarDisplayedButtons_]; + [[sender cell] setStartingChildIndex:displayedButtonCount_]; [folderTarget_ openBookmarkFolderFromButton:sender]; } @@ -1407,8 +1432,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { [buttons_ makeObjectsPerformSelector:@selector(removeFromSuperview)]; [buttons_ removeAllObjects]; [self clearMenuTagMap]; - needToRebuildOffTheSideMenu_ = YES; - bookmarkBarDisplayedButtons_ = 0; + displayedButtonCount_ = 0; // Make sure there are no stale pointers in the pasteboard. This // can be important if a bookmark is deleted (via bookmark sync) @@ -1428,7 +1452,8 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // TODO(jrg): move much of the cell config into the BookmarkButtonCell class. - (NSCell*)cellForBookmarkNode:(const BookmarkNode*)node { NSImage* image = node ? [self favIconForNode:node] : nil; - NSMenu* menu = [self contextMenuForNode:node]; + NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ : + buttonContextMenu_; BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node contextMenu:menu cellText:nil @@ -1440,11 +1465,6 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { return cell; } -- (NSMenu*)contextMenuForNode:(const BookmarkNode*)node { - return node && node->is_folder() ? buttonFolderContextMenu_ : - buttonContextMenu_; -} - // Return an appropriate width for the given bookmark button cell. // The "+2" is needed because, sometimes, Cocoa is off by a tad. // Example: for a bookmark named "Moma" or "SFGate", it is one pixel @@ -1461,12 +1481,8 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // for the next button. - (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell xOffset:(int*)xOffset { + DCHECK(xOffset); NSRect bounds = [buttonView_ bounds]; - // TODO(erg,jrg): There used to be an if statement here, comparing the height - // to 0. This essentially broke sizing because we're dealing with floats and - // the height wasn't precisely zero. The previous author wrote that they were - // doing this because of an animator, but we are not doing animations for beta - // so we do not care. bounds.size.height = bookmarks::kBookmarkButtonHeight; NSRect frame = NSInsetRect(bounds, @@ -1525,42 +1541,45 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { int xOffset = 0; for (int i = 0; i < node->GetChildCount(); i++) { const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; + if (NSMinX([button frame]) >= maxViewX) + break; + [buttons_ addObject:button]; + } +} - NSCell* cell = [self cellForBookmarkNode:child]; - NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&xOffset]; +- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node + xOffset:(int*)xOffset { + NSCell* cell = [self cellForBookmarkNode:node]; + NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset]; - // Break early if the button is off the end of the parent view. - if (NSMinX(frame) >= maxViewX) - break; + scoped_nsobject<BookmarkButton> + button([[BookmarkButton alloc] initWithFrame:frame]); + DCHECK(button.get()); - scoped_nsobject<BookmarkButton> - button([[BookmarkButton alloc] initWithFrame:frame]); - DCHECK(button.get()); - [buttons_ addObject:button]; + // [NSButton setCell:] warns to NOT use setCell: other than in the + // initializer of a control. However, we are using a basic + // NSButton whose initializer does not take an NSCell as an + // object. To honor the assumed semantics, we do nothing with + // NSButton between alloc/init and setCell:. + [button setCell:cell]; + [button setDelegate:self]; - // [NSButton setCell:] warns to NOT use setCell: other than in the - // initializer of a control. However, we are using a basic - // NSButton whose initializer does not take an NSCell as an - // object. To honor the assumed semantics, we do nothing with - // NSButton between alloc/init and setCell:. - [button setCell:cell]; - [button setDelegate:self]; - - if (child->is_folder()) { - [button setTarget:self]; - [button setAction:@selector(openBookmarkFolderFromButton:)]; - } else { - // Make the button do something - [button setTarget:self]; - [button setAction:@selector(openBookmark:)]; - // Add a tooltip. - NSString* title = base::SysWideToNSString(child->GetTitle()); - std::string url_string = child->GetURL().possibly_invalid_spec(); - NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, - url_string.c_str()]; - [button setToolTip:tooltip]; - } + if (node->is_folder()) { + [button setTarget:self]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; + } else { + // Make the button do something + [button setTarget:self]; + [button setAction:@selector(openBookmark:)]; + // Add a tooltip. + NSString* title = base::SysWideToNSString(node->GetTitle()); + std::string url_string = node->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, + url_string.c_str()]; + [button setToolTip:tooltip]; } + return [[button.get() retain] autorelease]; } // Add non-bookmark buttons to the view. This includes the chevron @@ -1577,21 +1596,23 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // Add bookmark buttons to the view only if they are completely // visible and don't overlap the "other bookmarks". Remove buttons -// which are clipped. Called when building the bookmark bar and when -// the window resizes. +// which are clipped. Called when building the bookmark bar the first time. - (void)addButtonsToView { - NSView* superview = nil; - bookmarkBarDisplayedButtons_ = 0; - for (NSButton* button in buttons_.get()) { - superview = [button superview]; - if (NSMaxX([button frame]) <= NSMinX([offTheSideButton_ frame])) { - if (!superview) - [buttonView_ addSubview:button]; - bookmarkBarDisplayedButtons_++; - } else { - if (superview) - [button removeFromSuperview]; - } + displayedButtonCount_ = 0; + NSMutableArray* buttons = [self buttons]; + for (NSButton* button in buttons) { + if (NSMaxX([button frame]) > (NSMinX([offTheSideButton_ frame]) - + bookmarks::kBookmarkHorizontalPadding)) + break; + [buttonView_ addSubview:button]; + ++displayedButtonCount_; + } + NSUInteger removalCount = + [buttons count] - (NSUInteger)displayedButtonCount_; + if (removalCount > 0) { + NSRange removalRange = + NSMakeRange(displayedButtonCount_, removalCount); + [buttons removeObjectsInRange:removalRange]; } } @@ -1668,23 +1689,37 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { // http://crbug.com/38665 } -// TODO(jrg): for now this is brute force. - (void)nodeMoved:(BookmarkModel*)model oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { - [self loaded:model]; + const BookmarkNode* movedNode = newParent->GetChild(newIndex); + id<BookmarkButtonControllerProtocol> oldController = + [self controllerForNode:oldParent]; + id<BookmarkButtonControllerProtocol> newController = + [self controllerForNode:newParent]; + if (newController == oldController) { + [oldController moveButtonFromIndex:oldIndex toIndex:newIndex]; + } else { + [oldController removeButton:oldIndex animate:NO]; + [newController addButtonForNode:movedNode atIndex:newIndex]; + } } -// TODO(jrg): for now this is brute force. - (void)nodeAdded:(BookmarkModel*)model - parent:(const BookmarkNode*)oldParent index:(int)index { - [self loaded:model]; + parent:(const BookmarkNode*)newParent index:(int)newIndex { + const BookmarkNode* newNode = newParent->GetChild(newIndex); + id<BookmarkButtonControllerProtocol> newController = + [self controllerForNode:newParent]; + [newController addButtonForNode:newNode atIndex:newIndex]; } -// TODO(jrg): for now this is brute force. - (void)nodeRemoved:(BookmarkModel*)model parent:(const BookmarkNode*)oldParent index:(int)index { - [self loaded:model]; + // Locate the parent node. The parent may not be showing, in which case + // we do nothing. + id<BookmarkButtonControllerProtocol> parentController = + [self controllerForNode:oldParent]; + [parentController removeButton:index animate:YES]; } // TODO(jrg): for now this is brute force. @@ -1722,7 +1757,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { [self loaded:model]; } -- (NSArray*)buttons { +- (NSMutableArray*)buttons { return buttons_.get(); } @@ -1916,4 +1951,205 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; } +- (id<BookmarkButtonControllerProtocol>)controllerForNode: + (const BookmarkNode*)node { + // See if it's in the bar, then if it is in the hierarchy of visible + // folder menus. + if (bookmarkModel_->GetBookmarkBarNode() == node) + return self; + return [folderController_ controllerForNode:node]; +} + +- (void)reconfigureBookmarkBar { + [self redistributeButtonsOnBarAsNeeded]; + [self positionOffTheSideButton]; + [self configureOffTheSideButtonContentsAndVisibility]; + [self centerNoItemsLabel]; +} + +- (void)redistributeButtonsOnBarAsNeeded { + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + NSInteger barCount = node->GetChildCount(); + + // Determine the current maximum extent of the visible buttons. + CGFloat maxViewX = NSMaxX([[self view] bounds]); + NSButton* otherBookmarksButton = otherBookmarksButton_.get(); + // If necessary, pull in the width to account for the Other Bookmarks button. + if (otherBookmarksButton_) + maxViewX = [otherBookmarksButton frame].origin.x - + bookmarks::kBookmarkHorizontalPadding; + // If we're already overflowing, then we need to account for the chevron. + if (barCount > displayedButtonCount_) + maxViewX = [offTheSideButton_ frame].origin.x - + bookmarks::kBookmarkHorizontalPadding; + + // As a result of pasting or dragging, the bar may now have more buttons + // than will fit so remove any which overflow. They will be shown in + // the off-the-side folder. + while (displayedButtonCount_ > 0) { + BookmarkButton* button = [buttons_ lastObject]; + if (NSMaxX([button frame]) < maxViewX) + break; + [buttons_ removeLastObject]; + [button removeFromSuperview]; + --displayedButtonCount_; + } + + // As a result of cutting, deleting and dragging, the bar may now have room + // for more buttons. + int xOffset = displayedButtonCount_ > 0 ? + NSMaxX([[buttons_ lastObject] frame]) + + bookmarks::kBookmarkHorizontalPadding : 0; + for (int i = displayedButtonCount_; i < barCount; ++i) { + const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self buttonForNode:child xOffset:&xOffset]; + // If we're testing against the last possible button then account + // for the chevron no longer needing to be shown. + if (i == barCount + 1) + maxViewX += NSWidth([offTheSideButton_ frame]) + + bookmarks::kBookmarkHorizontalPadding; + if (NSMaxX([button frame]) >= maxViewX) + break; + ++displayedButtonCount_; + [buttons_ addObject:button]; + [buttonView_ addSubview:button]; + } +} + +#pragma mark BookmarkButtonControllerProtocol + +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex { + int newOffset = 0; + if (buttonIndex == -1) + buttonIndex = [buttons_ count]; // New button goes at the end. + if (buttonIndex <= (NSInteger)[buttons_ count]) { + if (buttonIndex) { + BookmarkButton* targetButton = [buttons_ objectAtIndex:buttonIndex - 1]; + NSRect targetFrame = [targetButton frame]; + newOffset = targetFrame.origin.x + NSWidth(targetFrame) + + bookmarks::kBookmarkHorizontalPadding; + } + BookmarkButton* newButton = [self buttonForNode:node xOffset:&newOffset]; + CGFloat xOffset = + NSWidth([newButton frame]) + bookmarks::kBookmarkHorizontalPadding; + NSUInteger buttonCount = [buttons_ count]; + for (NSUInteger i = buttonIndex; i < buttonCount; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSPoint buttonOrigin = [button frame].origin; + buttonOrigin.x += xOffset; + [button setFrameOrigin:buttonOrigin]; + } + ++displayedButtonCount_; + [buttons_ insertObject:newButton atIndex:buttonIndex]; + [buttonView_ addSubview:newButton]; + + // See if any buttons need to be pushed off to or brought in from the side. + [self reconfigureBookmarkBar]; + } else { + // 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]; + } +} + +// TODO(mrossetti): jrg wants this broken up into smaller functions. +- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { + if (fromIndex != toIndex) { + NSInteger buttonCount = (NSInteger)[buttons_ count]; + if (toIndex == -1) + toIndex = buttonCount; + // See if we have a simple move within the bar, which will be the case if + // both button indexes are in the visible space. + if (fromIndex < buttonCount && toIndex < buttonCount) { + BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; + NSRect movedFrame = [movedButton frame]; + NSPoint toOrigin = movedFrame.origin; + CGFloat xOffset = + NSWidth(movedFrame) + bookmarks::kBookmarkHorizontalPadding; + // Hide the button to reduce flickering while drawing the window. + [movedButton setHidden:YES]; + [buttons_ removeObjectAtIndex:fromIndex]; + if (fromIndex < toIndex) { + // Move the button from left to right within the bar. + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1]; + NSRect toFrame = [targetButton frame]; + toOrigin.x = toFrame.origin.x - NSWidth(movedFrame) + NSWidth(toFrame); + for (NSInteger i = fromIndex; i < toIndex; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect frame = [button frame]; + frame.origin.x -= xOffset; + [button setFrameOrigin:frame.origin]; + } + } else { + // Move the button from right to left within the bar. + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex]; + toOrigin = [targetButton frame].origin; + for (NSInteger i = fromIndex - 1; i >= toIndex; --i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.x += xOffset; + [button setFrameOrigin:buttonFrame.origin]; + } + } + [buttons_ insertObject:movedButton atIndex:toIndex]; + [movedButton setFrameOrigin:toOrigin]; + [movedButton setHidden:NO]; + } else if (fromIndex < buttonCount) { + // A button is being removed from the bar and added to off-the-side. + // By now the node has already been inserted into the model so the + // button to be added is represented by |toIndex|. Things get + // complicated because the off-the-side is showing and must be redrawn + // 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 + // button to be added is represented by |toIndex|. + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + const BookmarkNode* movedNode = node->GetChild(toIndex); + 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]; + } + } +} + +- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { + if (buttonIndex < (NSInteger)[buttons_ count]) { + BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; + NSRect poofFrame = [oldButton bounds]; + NSPoint poofPoint = NSMakePoint(NSMidX(poofFrame), NSMidY(poofFrame)); + poofPoint = [oldButton convertPoint:poofPoint toView:nil]; + poofPoint = [[oldButton window] convertBaseToScreen:poofPoint]; + NSRect oldFrame = [oldButton frame]; + [oldButton removeFromSuperview]; + if (animate) + NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, + NSZeroSize, nil, nil, nil); + CGFloat xOffset = NSWidth(oldFrame) + bookmarks::kBookmarkHorizontalPadding; + [buttons_ removeObjectAtIndex:buttonIndex]; + NSUInteger buttonCount = [buttons_ count]; + for (NSUInteger i = buttonIndex; i < buttonCount; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.x -= xOffset; + [button setFrame:buttonFrame]; + // If this button is showing its menu then we need to move the menu, too. + if (button == [folderController_ parentButton]) + [folderController_ offsetFolderMenuWindow:NSMakeSize(xOffset, 0.0)]; + } + --displayedButtonCount_; + [self reconfigureBookmarkBar]; + } +} + @end diff --git a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm index 8bf9a27..996e677 100644 --- a/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_controller_unittest.mm @@ -11,6 +11,7 @@ #import "chrome/browser/cocoa/bookmark_bar_constants.h" #import "chrome/browser/cocoa/bookmark_bar_controller.h" #import "chrome/browser/cocoa/bookmark_bar_folder_window.h" +#import "chrome/browser/cocoa/bookmark_bar_unittest_helper.h" #import "chrome/browser/cocoa/bookmark_bar_view.h" #import "chrome/browser/cocoa/bookmark_button.h" #import "chrome/browser/cocoa/bookmark_button_cell.h" @@ -20,6 +21,7 @@ #include "chrome/browser/cocoa/test_event_utils.h" #import "chrome/browser/cocoa/view_resizer_pong.h" #include "chrome/common/pref_names.h" +#include "chrome/test/model_test_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" #import "third_party/ocmock/OCMock/OCMock.h" @@ -171,53 +173,75 @@ class FakeTheme : public ThemeProvider { namespace { -static const int kContentAreaHeight = 500; -static const int kInfoBarViewHeight = 30; - -class BookmarkBarControllerTest : public CocoaTest { +class BookmarkBarControllerTestBase : public CocoaTest { public: - BookmarkBarControllerTest() { + BrowserTestHelper helper_; + scoped_nsobject<NSView> parent_view_; + scoped_nsobject<ViewResizerPong> resizeDelegate_; + scoped_nsobject<BookmarkBarController> bar_; + + BookmarkBarControllerTestBase() { resizeDelegate_.reset([[ViewResizerPong alloc] init]); NSRect parent_frame = NSMakeRect(0, 0, 800, 50); parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]); [parent_view_ setHidden:YES]; - bar_.reset( - [[BookmarkBarControllerNoOpen alloc] - initWithBrowser:helper_.browser() - initialWidth:NSWidth(parent_frame) - delegate:nil - resizeDelegate:resizeDelegate_.get()]); - + bar_.reset([[BookmarkBarController alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth(parent_frame) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); InstallAndToggleBar(bar_.get()); - - // Create a menu/item to act like a sender - menu_.reset([[BookmarkMenu alloc] initWithTitle:@"I_dont_care"]); - menu_item_.reset([[NSMenuItem alloc] - initWithTitle:@"still_dont_care" - action:NULL - keyEquivalent:@""]); - cell_.reset([[NSButtonCell alloc] init]); - [menu_item_ setMenu:menu_.get()]; - [menu_ setDelegate:cell_.get()]; } void InstallAndToggleBar(BookmarkBarController* bar) { // Force loading of the nib. [bar view]; // Awkwardness to look like we've been installed. + for (NSView* subView in [parent_view_ subviews]) + [subView removeFromSuperview]; [parent_view_ addSubview:[bar view]]; NSRect frame = [[[bar view] superview] frame]; frame.origin.y = 100; [[[bar view] superview] setFrame:frame]; // Make sure it's on in a window so viewDidMoveToWindow is called - [[test_window() contentView] addSubview:parent_view_]; + NSView* contentView = [test_window() contentView]; + if (![parent_view_ isDescendantOf:contentView]) + [contentView addSubview:parent_view_]; // Make sure it's open so certain things aren't no-ops. [bar updateAndShowNormalBar:YES showDetachedBar:NO withAnimation:NO]; } +}; + +class BookmarkBarControllerTest : public BookmarkBarControllerTestBase { + public: + scoped_nsobject<BookmarkMenu> menu_; + scoped_nsobject<NSMenuItem> menu_item_; + scoped_nsobject<NSButtonCell> cell_; + + BookmarkBarControllerTest() { + bar_.reset( + [[BookmarkBarControllerNoOpen alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth([parent_view_ frame]) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + + InstallAndToggleBar(bar_.get()); + + // Create a menu/item to act like a sender + menu_.reset([[BookmarkMenu alloc] initWithTitle:@"I_dont_care"]); + menu_item_.reset([[NSMenuItem alloc] + initWithTitle:@"still_dont_care" + action:NULL + keyEquivalent:@""]); + cell_.reset([[NSButtonCell alloc] init]); + [menu_item_ setMenu:menu_.get()]; + [menu_ setDelegate:cell_.get()]; + } // Return a menu item that points to the given URL. NSMenuItem* ItemForBookmarkBarMenu(GURL& gurl) { @@ -235,13 +259,9 @@ class BookmarkBarControllerTest : public CocoaTest { return menu_item_; } - BrowserTestHelper helper_; - scoped_nsobject<NSView> parent_view_; - scoped_nsobject<ViewResizerPong> resizeDelegate_; - scoped_nsobject<BookmarkBarControllerNoOpen> bar_; - scoped_nsobject<BookmarkMenu> menu_; - scoped_nsobject<NSMenuItem> menu_item_; - scoped_nsobject<NSButtonCell> cell_; + BookmarkBarControllerNoOpen* noOpenBar() { + return (BookmarkBarControllerNoOpen*)bar_.get(); + } }; TEST_F(BookmarkBarControllerTest, ShowWhenShowBookmarkBarTrue) { @@ -422,8 +442,8 @@ TEST_F(BookmarkBarControllerTest, OffTheSideButtonHidden) { EXPECT_TRUE([bar_ offTheSideButtonIsHidden]); } + const BookmarkNode* parent = model->GetBookmarkBarNode(); for (int i = 0; i < 20; i++) { - const BookmarkNode* parent = model->GetBookmarkBarNode(); model->AddURL(parent, parent->GetChildCount(), L"super duper wide title", GURL("http://superfriends.hall-of-justice.edu")); @@ -534,8 +554,8 @@ TEST_F(BookmarkBarControllerTest, OpenBookmark) { [cell setRepresentedObject:[NSValue valueWithPointer:node.get()]]; [bar_ openBookmark:button]; - EXPECT_EQ(bar_.get()->urls_[0], node->GetURL()); - EXPECT_EQ(bar_.get()->dispositions_[0], CURRENT_TAB); + EXPECT_EQ(noOpenBar()->urls_[0], node->GetURL()); + EXPECT_EQ(noOpenBar()->dispositions_[0], CURRENT_TAB); } // Confirm opening of bookmarks works from the menus (different @@ -556,8 +576,8 @@ TEST_F(BookmarkBarControllerTest, OpenBookmarkFromMenus) { GURL gurl(urls[i]); [bar_ performSelector:selectors[i] withObject:ItemForBookmarkBarMenu(gurl)]; - EXPECT_EQ(bar_.get()->urls_[0], gurl); - EXPECT_EQ(bar_.get()->dispositions_[0], dispositions[i]); + EXPECT_EQ(noOpenBar()->urls_[0], gurl); + EXPECT_EQ(noOpenBar()->dispositions_[0], dispositions[i]); [bar_ clear]; } } @@ -616,7 +636,6 @@ TEST_F(BookmarkBarControllerTest, TestAddRemoveAndClear) { TEST_F(BookmarkBarControllerTest, TestButtonLimits) { BookmarkModel* model = helper_.profile()->GetBookmarkModel(); EXPECT_EQ(0U, [[bar_ buttons] count]); - // Add one; make sure we see it. const BookmarkNode* parent = model->GetBookmarkBarNode(); model->AddURL(parent, parent->GetChildCount(), @@ -625,7 +644,7 @@ TEST_F(BookmarkBarControllerTest, TestButtonLimits) { // Add 30 which we expect to be 'too many'. Make sure we don't see // 30 buttons. - [bar_ clearBookmarkBar]; + model->Remove(parent, 0); EXPECT_EQ(0U, [[bar_ buttons] count]); for (int i=0; i<30; i++) { model->AddURL(parent, parent->GetChildCount(), @@ -767,7 +786,7 @@ TEST_F(BookmarkBarControllerTest, MiddleClick) { EXPECT_TRUE(first); [first otherMouseUp:test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0)]; - EXPECT_EQ(bar_.get()->urls_.size(), 1U); + EXPECT_EQ(noOpenBar()->urls_.size(), 1U); } TEST_F(BookmarkBarControllerTest, DisplaysHelpMessageOnEmpty) { @@ -1239,12 +1258,12 @@ TEST_F(BookmarkBarControllerTest, NodeDeletedWhileMenuIsOpen) { GURL("http://www.google.com")); NSMenuItem* item = ItemForBookmarkBarMenu(initialNode); - EXPECT_EQ(0U, bar_.get()->urls_.size()); + EXPECT_EQ(0U, noOpenBar()->urls_.size()); // Basic check of the menu item and an IBOutlet it can call. EXPECT_EQ(initialNode, [bar_ nodeFromMenuItem:item]); [bar_ openBookmarkInNewWindow:item]; - EXPECT_EQ(1U, bar_.get()->urls_.size()); + EXPECT_EQ(1U, noOpenBar()->urls_.size()); [bar_ clear]; // Now delete the node and make sure things are happy (no crash, @@ -1254,7 +1273,7 @@ TEST_F(BookmarkBarControllerTest, NodeDeletedWhileMenuIsOpen) { // Should not crash by referencing a deleted node. [bar_ openBookmarkInNewWindow:item]; // Confirm the above did nothing in case it somehow didn't crash. - EXPECT_EQ(0U, bar_.get()->urls_.size()); + EXPECT_EQ(0U, noOpenBar()->urls_.size()); // Confirm some more non-crashes. [bar_ openBookmarkInNewForegroundTab:item]; @@ -1300,6 +1319,60 @@ TEST_F(BookmarkBarControllerTest, CloseFolderOnAnimate) { EXPECT_FALSE([bar_ folderController]); } +TEST_F(BookmarkBarControllerTest, MoveRemoveAddButtons) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Remember how many buttons are showing. + int oldDisplayedButtons = [bar_ displayedButtonCount]; + NSArray* buttons = [bar_ buttons]; + + // Move a button around a bit. + [bar_ moveButtonFromIndex:0 toIndex:2]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"1b"]); + EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]); + [bar_ moveButtonFromIndex:2 toIndex:0]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"3b"]); + EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]); + + // Add a couple of buttons. + const BookmarkNode* parent = root->GetChild(1); // Purloin an existing node. + const BookmarkNode* node = parent->GetChild(0); + [bar_ addButtonForNode:node atIndex:0]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f"]); + EXPECT_TRUE([[[buttons objectAtIndex:3] title] isEqualToString:@"3b"]); + EXPECT_EQ(oldDisplayedButtons + 1, [bar_ displayedButtonCount]); + node = parent->GetChild(1); + [bar_ addButtonForNode:node atIndex:-1]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f"]); + EXPECT_TRUE([[[buttons objectAtIndex:3] title] isEqualToString:@"3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:4] title] isEqualToString:@"2f2b"]); + EXPECT_EQ(oldDisplayedButtons + 2, [bar_ displayedButtonCount]); + + // Remove a couple of buttons. + [bar_ removeButton:4 animate:NO]; + [bar_ removeButton:1 animate:NO]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"3b"]); + EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]); +} + + class BookmarkBarControllerOpenAllTest : public BookmarkBarControllerTest { public: BookmarkBarControllerOpenAllTest() { @@ -1394,14 +1467,14 @@ TEST_F(BookmarkBarControllerOpenAllTest, CommandClickOnFolder) { [[[fakeApp stub] andReturn:commandClick] currentEvent]; id oldApp = NSApp; NSApp = fakeApp; - size_t originalDispositionCount = bar_.get()->dispositions_.size(); + size_t originalDispositionCount = noOpenBar()->dispositions_.size(); // Click! [first performClick:first]; - size_t dispositionCount = bar_.get()->dispositions_.size(); + size_t dispositionCount = noOpenBar()->dispositions_.size(); EXPECT_EQ(originalDispositionCount+1, dispositionCount); - EXPECT_EQ(bar_.get()->dispositions_[dispositionCount-1], NEW_BACKGROUND_TAB); + EXPECT_EQ(noOpenBar()->dispositions_[dispositionCount-1], NEW_BACKGROUND_TAB); // Replace NSApp NSApp = oldApp; @@ -1469,4 +1542,78 @@ TEST_F(BookmarkBarControllerNotificationTest, DeregistersForNotifications) { // TODO(viettrungluu): figure out how to test animations. +class BookmarkBarControllerDragDropTest : public BookmarkBarControllerTestBase { + public: + BookmarkBarControllerDragDropTest() { + InstallAndToggleBar(bar_.get()); + } +}; + +TEST_F(BookmarkBarControllerDragDropTest, DragMoveBarBookmarkToOffTheSide) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1bWithLongName 2fWithLongName:[ " + "2f1bWithLongName 2f2fWithLongName:[ 2f2f1bWithLongName " + "2f2f2bWithLongName 2f2f3bWithLongName 2f4b ] 2f3bWithLongName ] " + "3bWithLongName 4bWithLongName 5bWithLongName 6bWithLongName " + "7bWithLongName 8bWithLongName 9bWithLongName 10bWithLongName " + "11bWithLongName 12bWithLongName 13b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Insure that the off-the-side is showing. + ASSERT_FALSE([bar_ offTheSideButtonIsHidden]); + + // Remember how many buttons are showing and are available. + int oldDisplayedButtons = [bar_ displayedButtonCount]; + int oldChildCount = root->GetChildCount(); + + // Pop up the off-the-side menu. + BookmarkButton* otsButton = (BookmarkButton*)[bar_ offTheSideButton]; + ASSERT_TRUE(otsButton); + [[otsButton target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:otsButton]; + BookmarkBarFolderController* otsController = [bar_ folderController]; + EXPECT_TRUE(otsController); + NSWindow* toWindow = [otsController window]; + EXPECT_TRUE(toWindow); + BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"3bWithLongName"]; + ASSERT_TRUE(draggedButton); + int oldOTSCount = (int)[[otsController buttons] count]; + EXPECT_EQ(oldOTSCount, oldChildCount - oldDisplayedButtons); + BookmarkButton* targetButton = [[otsController buttons] objectAtIndex:0]; + ASSERT_TRUE(targetButton); + [otsController dragButton:draggedButton + to:[targetButton center] + copy:YES]; + // There should still be the same number of buttons in the bar + // and off-the-side should have one more. + int newDisplayedButtons = [bar_ displayedButtonCount]; + int newChildCount = root->GetChildCount(); + int newOTSCount = (int)[[otsController buttons] count]; + EXPECT_EQ(oldDisplayedButtons, newDisplayedButtons); + EXPECT_EQ(oldChildCount + 1, newChildCount); + EXPECT_EQ(oldOTSCount + 1, newOTSCount); + EXPECT_EQ(newOTSCount, newChildCount - newDisplayedButtons); +} + +TEST_F(BookmarkBarControllerDragDropTest, ControllerForNode) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Find the main bar controller. + const void* expectedController = bar_; + const void* actualController = [bar_ controllerForNode:root]; + EXPECT_EQ(expectedController, actualController); +} + } // namespace diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.h b/chrome/browser/cocoa/bookmark_bar_folder_controller.h index fef2a56..8b88253 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_controller.h +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.h @@ -110,6 +110,16 @@ // Return the parent button that owns the bookmark folder we represent. - (BookmarkButton*)parentButton; +// Offset our folder menu window. This is usually needed in response to a +// parent folder menu window or the bookmark bar changing position due to +// the dragging of a bookmark node from the parent into this folder menu. +- (void)offsetFolderMenuWindow:(NSSize)offset; + +// Re-layout the window menu in case some buttons were added or removed, +// specifically as a result of the bookmark bar changing configuration +// and altering the contents of the off-the-side folder. +- (void)reconfigureMenu; + // Actions from a context menu over a button or folder. - (IBAction)cutBookmark:(id)sender; - (IBAction)copyBookmark:(id)sender; diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm index 29d7185..461d555 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_controller.mm +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller.mm @@ -17,7 +17,6 @@ #import "chrome/browser/cocoa/browser_window_controller.h" #import "chrome/browser/cocoa/event_utils.h" - namespace { // Frequency of the scrolling timer in seconds. @@ -57,6 +56,12 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; - (void)removeScrollTracking; - (void)endScroll; - (void)addScrollTimerWithDelta:(CGFloat)delta; + +// Determine the best button width (which will be the widest button or the +// maximum allowable button width, whichever is less) and resize all buttons. +// Return the new width (so that the window can be adjusted, if necessary). +- (CGFloat)adjustButtonWidths; + @end @implementation BookmarkBarFolderController @@ -69,6 +74,10 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; ofType:@"nib"]; if ((self = [super initWithWindowNibPath:nibPath owner:self])) { parentButton_.reset([button retain]); +#if 1 + // We want the button to remain bordered as part of the menu path. + [[button cell] setShowsBorderOnlyWhileMouseInside:NO]; +#endif parentController_.reset([parentController retain]); barController_ = barController; // WEAK buttons_.reset([[NSMutableArray alloc] init]); @@ -82,6 +91,11 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; } - (void)dealloc { +#if 1 + // The button is no longer part of the menu path. + [[parentButton_ cell] setShowsBorderOnlyWhileMouseInside:YES]; + [parentButton_ setNeedsDisplay]; +#endif [self removeScrollTracking]; [self endScroll]; [hoverState_ draggingExited]; @@ -143,9 +157,13 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; // The "+2" is needed because, sometimes, Cocoa is off by a tad when // returning the value it thinks it needs. CGFloat desired = [cell cellSize].width + 2; - frame.size.width = std::min( - std::max(bookmarks::kBookmarkMenuButtonMinimumWidth, desired), - bookmarks::kBookmarkMenuButtonMaximumWidth); + // The width is determined from the maximum of the proposed width + // (provided in |frame|) or the natural width of the title, then + // limited by the abolute minimum and maximum allowable widths. + frame.size.width = + std::min(std::max(bookmarks::kBookmarkMenuButtonMinimumWidth, + std::max(frame.size.width, desired)), + bookmarks::kBookmarkMenuButtonMaximumWidth); BookmarkButton* button = [[[BookmarkButton alloc] initWithFrame:frame] autorelease]; @@ -210,11 +228,13 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; bookmarkBarBottomLeftInScreen.y); } else { // Our parent controller is another BookmarkBarFolderController. - // In this case, start ot the RIGHT of the parent button. + // In this case, start with a slight overlap on the RIGHT of the + // parent button, which looks much more menu-like than with none. // Start to RIGHT of the button. // TODO(jrg): If too far to right, pop left again. // http://crbug.com/36225 - newWindowTopLeft.x = NSMaxX([[parentButton_ window] frame]); + newWindowTopLeft.x = NSMaxX([[parentButton_ window] frame]) - + bookmarks::kBookmarkMenuOverlap; NSPoint top = NSMakePoint(0, (NSMaxY([parentButton_ frame]) + bookmarks::kBookmarkVerticalPadding)); NSPoint topOfWindow = @@ -254,7 +274,7 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; NSRect buttonsOuterFrame = NSMakeRect( bookmarks::kBookmarkHorizontalPadding, height - (bookmarks::kBookmarkBarHeight - - bookmarks::kBookmarkHorizontalPadding), + bookmarks::kBookmarkVerticalPadding), bookmarks::kDefaultBookmarkWidth, (bookmarks::kBookmarkBarHeight - 2 * bookmarks::kBookmarkVerticalPadding)); @@ -281,24 +301,10 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; } } - // Now that we have all our buttons we can determine the real size - // of our window. - CGFloat width = 0.0; - for (BookmarkButton* button in buttons_.get()) { - width = std::max(width, NSWidth([button bounds])); - } - width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth); - - // Things look and feel more menu-like if all the buttons are the - // full width of the window, especially if there are submenus. - for (BookmarkButton* button in buttons_.get()) { - NSRect buttonFrame = [button frame]; - buttonFrame.size.width = width; - [button setFrame:buttonFrame]; - } - width += (2 * bookmarks::kBookmarkVerticalPadding); - - // Finally, set our window size (make sure it fits on screen). + // Now that all buttons have been installed, adjust their sizes to be + // consistent, determine the best size for the window, and set the window. + CGFloat width = + [self adjustButtonWidths] + (2 * bookmarks::kBookmarkVerticalPadding); NSRect windowFrame = NSMakeRect(newWindowTopLeft.x, newWindowTopLeft.y - height, width + kScrollViewContentWidthMargin, @@ -341,6 +347,169 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; [self configureWindowLevel]; } +- (void)offsetFolderMenuWindow:(NSSize)offset { + NSWindow* window = [self window]; + NSRect windowFrame = [window frame]; + windowFrame.origin.x -= offset.width; + windowFrame.origin.y += offset.height; // Yes, in the opposite direction! + [window setFrame:windowFrame display:YES]; + [folderController_ offsetFolderMenuWindow:offset]; +} + +// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:. +- (CGFloat)adjustButtonWidths { + CGFloat width = 0.0; + for (BookmarkButton* button in buttons_.get()) { + width = std::max(width, NSWidth([button bounds])); + } + width = std::min(width, bookmarks::kBookmarkMenuButtonMaximumWidth); + // Things look and feel more menu-like if all the buttons are the + // full width of the window, especially if there are submenus. + for (BookmarkButton* button in buttons_.get()) { + NSRect buttonFrame = [button frame]; + buttonFrame.size.width = width; + [button setFrame:buttonFrame]; + } + return width; +} + +#pragma mark BookmarkButtonControllerProtocol + +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex { + if (buttonIndex == -1) + buttonIndex = [buttons_ count]; + + // Remember the last moved button's frame or the default. + NSRect buttonFrame = NSMakeRect(bookmarks::kBookmarkHorizontalPadding, + NSHeight([mainView_ frame]) + bookmarks::kBookmarkBarHeight + + bookmarks::kBookmarkVerticalPadding, + NSWidth([mainView_ frame]) - 2 * bookmarks::kBookmarkHorizontalPadding + - kScrollViewContentWidthMargin, + bookmarks::kBookmarkBarHeight - 2 * bookmarks::kBookmarkVerticalPadding); + BookmarkButton* button = nil; // Remember so it can be inavalidated. + for (NSInteger i = 0; i < buttonIndex; ++i) { + button = [buttons_ objectAtIndex:i]; + buttonFrame = [button frame]; + buttonFrame.origin.y += bookmarks::kBookmarkBarHeight; + [button setFrame:buttonFrame]; + } + [[button cell] mouseExited:nil]; // De-highlight. + buttonFrame.origin.y -= bookmarks::kBookmarkBarHeight; + BookmarkButton* newButton = [self makeButtonForNode:node + frame:buttonFrame]; + [buttons_ insertObject:newButton atIndex:buttonIndex]; + [mainView_ addSubview:newButton]; + + // Close any child folder(s) which may still be open. + [self closeBookmarkFolder:self]; + + // Update all button widths in case more width is needed. + CGFloat windowWidth = + [self adjustButtonWidths] + (2 * bookmarks::kBookmarkVerticalPadding) + + kScrollViewContentWidthMargin; + + // Update vertical metrics of the window and the main view (using sizers + // does not do what we want). + NSWindow* window = [self window]; + NSRect frame = [mainView_ frame]; + const CGFloat verticalDelta = + bookmarks::kBookmarkBarHeight - bookmarks::kBookmarkVerticalPadding; + frame.size.height += verticalDelta; + frame.size.width = windowWidth; + [mainView_ setFrame:frame]; + frame = [window frame]; + frame.origin.y -= bookmarks::kBookmarkBarHeight; + frame.size.height += bookmarks::kBookmarkBarHeight; + frame.size.width = windowWidth; + [window setFrame:frame display:YES]; +} + +- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { + if (fromIndex != toIndex) { + if (toIndex == -1) + toIndex = [buttons_ count]; + BookmarkButton* movedButton = [buttons_ objectAtIndex:fromIndex]; + [buttons_ removeObjectAtIndex:fromIndex]; + NSRect movedFrame = [movedButton frame]; + NSPoint toOrigin = movedFrame.origin; + [movedButton setHidden:YES]; + if (fromIndex < toIndex) { + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex - 1]; + toOrigin = [targetButton frame].origin; + for (NSInteger i = fromIndex; i < toIndex; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect frame = [button frame]; + frame.origin.y += bookmarks::kBookmarkBarHeight; + [button setFrameOrigin:frame.origin]; + } + } else { + BookmarkButton* targetButton = [buttons_ objectAtIndex:toIndex]; + toOrigin = [targetButton frame].origin; + for (NSInteger i = fromIndex - 1; i >= toIndex; --i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.y -= bookmarks::kBookmarkBarHeight; + [button setFrameOrigin:buttonFrame.origin]; + } + } + [buttons_ insertObject:movedButton atIndex:toIndex]; + [movedButton setFrameOrigin:toOrigin]; + [movedButton setHidden:NO]; + } +} + +// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 +- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { + // TODO(mrossetti): Get disappearing animation to work. http://crbug.com/42360 + NSWindow* window = [self window]; + BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; + NSRect poofFrame = [oldButton bounds]; + NSPoint poofPoint = NSMakePoint(NSMidX(poofFrame), NSMidY(poofFrame)); + poofPoint = [oldButton convertPoint:poofPoint toView:nil]; + poofPoint = [[oldButton window] convertBaseToScreen:poofPoint]; + [oldButton removeFromSuperview]; + if (animate) + NSShowAnimationEffect(NSAnimationEffectDisappearingItemDefault, poofPoint, + NSZeroSize, nil, nil, nil); + [buttons_ removeObjectAtIndex:buttonIndex]; + for (NSInteger i = 0; i < buttonIndex; ++i) { + BookmarkButton* button = [buttons_ objectAtIndex:i]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.y -= bookmarks::kBookmarkBarHeight; + [button setFrame:buttonFrame]; + } + // Search for and adjust submenus, if necessary. + NSInteger buttonCount = [buttons_ count]; + BookmarkButton* subButton = [folderController_ parentButton]; + for (NSInteger i = buttonIndex; i < buttonCount; ++i) { + BookmarkButton* aButton = [buttons_ objectAtIndex:i]; + // If this button is showing its menu then we need to move the menu, too. + if (aButton == subButton) + [folderController_ offsetFolderMenuWindow:NSMakeSize(0.0, + bookmarks::kBookmarkBarHeight)]; + } + + // Resize the window and the main view (using sizers does not work). + NSRect frame = [window frame]; + frame.origin.y += bookmarks::kBookmarkBarHeight; + frame.size.height -= bookmarks::kBookmarkBarHeight; + [window setFrame:frame display:YES]; + frame = [mainView_ frame]; + frame.origin.y += bookmarks::kBookmarkBarHeight; + frame.size.height -= bookmarks::kBookmarkBarHeight; + [mainView_ setFrame:frame]; +} + +- (id<BookmarkButtonControllerProtocol>)controllerForNode: + (const BookmarkNode*)node { + // See if we are holding this node, otherwise see if it is in our + // hierarchy of visible folder menus. + if ([parentButton_ bookmarkNode] == node) + return self; + return [folderController_ controllerForNode:node]; +} + // Start a "scroll up" timer. - (void)beginScrollWindowUp { [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount]; @@ -381,10 +550,12 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; // view that contains the button. It appears that a mouseExited: // gets lost, so the button stays highlit forever. We accomodate // here. +#if 0 if (buttonThatMouseIsIn_) { [[buttonThatMouseIsIn_ cell] setShowsBorderOnlyWhileMouseInside:NO]; [[buttonThatMouseIsIn_ cell] setShowsBorderOnlyWhileMouseInside:YES]; } +#endif // We update the window size after shifting the scroll to avoid a race. CGFloat screenHeightMinusMargin = (NSHeight(screenFrame) - @@ -540,6 +711,9 @@ const CGFloat kScrollWindowVerticalMargin = 0.0; - (void)fillPasteboard:(NSPasteboard*)pboard forDragOfButton:(BookmarkButton*)button { [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; + + // Close our folder menu and submenus since we know we're going to be dragged. + [self closeBookmarkFolder:self]; } // Find something like std::is_between<T>? I can't believe one doesn't exist. @@ -588,6 +762,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { return nil; } +// TODO(jrg): Refactor BookmarkBarFolder common code. http://crbug.com/35966 // Most of the work (e.g. drop indicator) is taken care of in the // folder_view. Here we handle hover open issues for subfolders. // Caution: there are subtle differences between this one and @@ -596,6 +771,19 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { NSPoint currentLocation = [info draggingLocation]; BookmarkButton* button = [self buttonForDroppingOnAtPoint:currentLocation]; + // Don't allow drops that would result in cycles. + if (button) { + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + if (data && [info draggingSource]) { + BookmarkButton* sourceButton = nil; + [data getBytes:&sourceButton length:sizeof(sourceButton)]; + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + const BookmarkNode* destNode = [button bookmarkNode]; + if (destNode->HasAncestor(sourceNode)) + button = nil; + } + } // Delegate handling of dragging over a button to the |hoverState_| member. return [hoverState_ draggingEnteredButton:button]; } @@ -698,17 +886,19 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { destIndex += [[parentButton_ cell] startingChildIndex]; } - if (copy) - [self bookmarkModel]->Copy(sourceNode, destParent, destIndex); - else - [self bookmarkModel]->Move(sourceNode, destParent, destIndex); - - [self closeAllBookmarkFolders]; // For a hover open, if needed. - - // Movement of a node triggers observers (like us) to rebuild the - // bar so we don't have to do so explicitly. + // Prevent cycles. + BOOL wasCopiedOrMoved = NO; + if (!destParent->HasAncestor(sourceNode)) { + if (copy) + [self bookmarkModel]->Copy(sourceNode, destParent, destIndex); + else + [self bookmarkModel]->Move(sourceNode, destParent, destIndex); + wasCopiedOrMoved = YES; + // Movement of a node triggers observers (like us) to rebuild the + // bar so we don't have to do so explicitly. + } - return YES; + return wasCopiedOrMoved; } // Return YES if we should show the drop indicator, else NO. @@ -844,14 +1034,22 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { } } +- (void)reconfigureMenu { + for (BookmarkButton* button in buttons_.get()) + [button removeFromSuperview]; + [buttons_ removeAllObjects]; + [self configureWindow]; +} + #pragma mark Methods Forwarded to BookmarkBarController - (IBAction)cutBookmark:(id)sender { + [self closeBookmarkFolder:self]; [barController_ cutBookmark:sender]; } - (IBAction)copyBookmark:(id)sender { - [barController_ cutBookmark:sender]; + [barController_ copyBookmark:sender]; } - (IBAction)pasteBookmark:(id)sender { @@ -859,6 +1057,7 @@ static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { } - (IBAction)deleteBookmark:(id)sender { + [self closeBookmarkFolder:self]; [barController_ deleteBookmark:sender]; } diff --git a/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm index a0f8d8e..65b0c90 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_folder_controller_unittest.mm @@ -6,11 +6,15 @@ #include "base/basictypes.h" #include "base/scoped_nsobject.h" +#import "chrome/browser/cocoa/bookmark_bar_constants.h" // namespace bookmarks #import "chrome/browser/cocoa/bookmark_bar_controller.h" #import "chrome/browser/cocoa/bookmark_bar_folder_button_cell.h" #import "chrome/browser/cocoa/bookmark_bar_folder_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_unittest_helper.h" #include "chrome/browser/cocoa/browser_test_helper.h" #import "chrome/browser/cocoa/cocoa_test_helper.h" +#import "chrome/browser/cocoa/view_resizer_pong.h" +#include "chrome/test/model_test_utils.h" #include "testing/gtest/include/gtest/gtest.h" #include "testing/platform_test.h" @@ -222,7 +226,8 @@ TEST_F(BookmarkBarFolderControllerTest, Position) { [bbfc2 window]; [bbfc2 setRealTopLeft:YES]; pt = [bbfc2 windowTopLeft]; - EXPECT_EQ(pt.x, NSMaxX([[bbfc.get() window] frame])); + // We're now overlapping the window a bit. + EXPECT_EQ(pt.x, NSMaxX([[bbfc.get() window] frame]) - 1); } TEST_F(BookmarkBarFolderControllerTest, DropDestination) { @@ -366,6 +371,573 @@ TEST_F(BookmarkBarFolderControllerTest, SimpleScroll) { } } +class BookmarkBarFolderControllerDragDropTest : public CocoaTest { + public: + BrowserTestHelper helper_; + scoped_nsobject<NSView> parent_view_; + scoped_nsobject<ViewResizerPong> resizeDelegate_; + scoped_nsobject<BookmarkBarController> bar_; + + BookmarkBarFolderControllerDragDropTest() { + resizeDelegate_.reset([[ViewResizerPong alloc] init]); + NSRect parent_frame = NSMakeRect(0, 0, 800, 50); + parent_view_.reset([[NSView alloc] initWithFrame:parent_frame]); + [parent_view_ setHidden:YES]; + bar_.reset([[BookmarkBarController alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth(parent_frame) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + InstallAndToggleBar(bar_.get()); + } + + void InstallAndToggleBar(BookmarkBarController* bar) { + // Force loading of the nib. + [bar view]; + // Awkwardness to look like we've been installed. + [parent_view_ addSubview:[bar view]]; + NSRect frame = [[[bar view] superview] frame]; + frame.origin.y = 100; + [[[bar view] superview] setFrame:frame]; + + // Make sure it's on in a window so viewDidMoveToWindow is called + [[test_window() contentView] addSubview:parent_view_]; + + // Make sure it's open so certain things aren't no-ops. + [bar updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + } +}; + +#if CGFLOAT_IS_DOUBLE +#define CGFLOAT_EPSILON DBL_EPSILON +#else +#define CGFLOAT_EPSILON FLT_EPSILON +#endif + +#define CGFLOAT_EQ(expected, actual) \ + (actual >= (expected - CGFLOAT_EPSILON) && \ + actual <= (expected + CGFLOAT_EPSILON)) + +#define EXPECT_CGFLOAT_EQ(expected, actual) \ + EXPECT_TRUE(CGFLOAT_EQ(actual, expected) + +#define EXPECT_NSRECT_EQ(expected, actual) \ + EXPECT_TRUE(CGFLOAT_EQ(expected.origin.x, actual.origin.x) && \ + CGFLOAT_EQ(expected.origin.y, actual.origin.y) && \ + CGFLOAT_EQ(expected.size.width, actual.size.width) && \ + CGFLOAT_EQ(expected.size.height, actual.size.height)) << \ + "Rects do not match: " << \ + [NSStringFromRect(expected) UTF8String] << \ + " != " << [NSStringFromRect(actual) UTF8String]; + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragMoveBarBookmarkToFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu and drag in a button from the bar. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"]; + NSRect oldToFolderFrame = [toFolder frame]; + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + NSRect oldToWindowFrame = [toWindow frame]; + // Drag a bar button onto a bookmark (i.e. not a folder) in a folder + // so it should end up below the target bookmark. + BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"1b"]; + ASSERT_TRUE(draggedButton); + CGFloat horizontalShift = + NSWidth([draggedButton frame]) + bookmarks::kBookmarkHorizontalPadding; + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton center] + copy:NO]; + // The button should have landed just after "2f1b". + const std::wstring expected_string(L"2f:[ 2f1b 1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ " + "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root)); + + // Verify the window still appears by looking for its controller. + EXPECT_TRUE([bar_ folderController]); + + // Gather the new frames. + NSRect newToFolderFrame = [toFolder frame]; + NSRect newToWindowFrame = [toWindow frame]; + // The toFolder should have shifted left horizontally but not vertically. + NSRect expectedToFolderFrame = + NSOffsetRect(oldToFolderFrame, -horizontalShift, 0); + EXPECT_NSRECT_EQ(expectedToFolderFrame, newToFolderFrame); + // The toWindow should have shifted left horizontally, down vertically, + // and grown vertically. + NSRect expectedToWindowFrame = oldToWindowFrame; + expectedToWindowFrame.origin.x -= horizontalShift; + expectedToWindowFrame.origin.y -= bookmarks::kBookmarkBarHeight; + expectedToWindowFrame.size.height += bookmarks::kBookmarkBarHeight; + EXPECT_NSRECT_EQ(expectedToWindowFrame, newToWindowFrame); + + // Move the button back to the bar at the beginning. + draggedButton = [folderController buttonWithTitleEqualTo:@"1b"]; + ASSERT_TRUE(draggedButton); + targetButton = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(targetButton); + [bar_ dragButton:draggedButton + to:[targetButton left] + copy:NO]; + EXPECT_EQ(model_string, model_test_utils::ModelStringFromNode(root)); + // Don't check the folder window since it's not supposed to be showing. +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragCopyBarBookmarkToFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu and copy in a button from the bar. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(toFolder); + NSRect oldToFolderFrame = [toFolder frame]; + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + NSRect oldToWindowFrame = [toWindow frame]; + // Drag a bar button onto a bookmark (i.e. not a folder) in a folder + // so it should end up below the target bookmark. + BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"1b"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton center] + copy:YES]; + // The button should have landed just after "2f1b". + const std::wstring expected_1(L"1b 2f:[ 2f1b 1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ " + "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + EXPECT_EQ(expected_1, model_test_utils::ModelStringFromNode(root)); + + // Gather the new frames. + NSRect newToFolderFrame = [toFolder frame]; + NSRect newToWindowFrame = [toWindow frame]; + // The toFolder should have shifted. + EXPECT_NSRECT_EQ(oldToFolderFrame, newToFolderFrame); + // The toWindow should have shifted down vertically and grown vertically. + NSRect expectedToWindowFrame = oldToWindowFrame; + expectedToWindowFrame.origin.y -= bookmarks::kBookmarkBarHeight; + expectedToWindowFrame.size.height += bookmarks::kBookmarkBarHeight; + EXPECT_NSRECT_EQ(expectedToWindowFrame, newToWindowFrame); + + // Copy the button back to the bar after "3b". + draggedButton = [folderController buttonWithTitleEqualTo:@"1b"]; + ASSERT_TRUE(draggedButton); + targetButton = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(targetButton); + [bar_ dragButton:draggedButton + to:[targetButton left] + copy:YES]; + const std::wstring expected_2(L"1b 2f:[ 2f1b 1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 1b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ " + "4f2f1b 4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + EXPECT_EQ(expected_2, model_test_utils::ModelStringFromNode(root)); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, + DragMoveBarBookmarkToSubfolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu and a subfolder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + NSRect oldToWindowFrame = [toWindow frame]; + BookmarkButton* toSubfolder = [folderController buttonWithTitleEqualTo:@"4f2f"]; + ASSERT_TRUE(toSubfolder); + NSRect oldToSubfolderFrame = [toSubfolder frame]; + [[toSubfolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toSubfolder]; + BookmarkBarFolderController* subfolderController = + [folderController folderController]; + EXPECT_TRUE(subfolderController); + NSWindow* toSubwindow = [subfolderController window]; + EXPECT_TRUE(toSubwindow); + NSRect oldToSubwindowFrame = [toSubwindow frame]; + // Drag a bar button onto a bookmark (i.e. not a folder) in a folder + // so it should end up below the target bookmark. + BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"5b"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = + [subfolderController buttonWithTitleEqualTo:@"4f2f3b"]; + ASSERT_TRUE(targetButton); + [subfolderController dragButton:draggedButton + to:[targetButton center] + copy:NO]; + // The button should have landed just after "2f". + const std::wstring expected_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ " + "4f2f1b 4f2f2b 4f2f3b 5b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] "); + EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root)); + + // Check the window layouts. The folder window should not have changed, + // but the subfolder window should have shifted vertically and grown. + NSRect newToWindowFrame = [toWindow frame]; + EXPECT_NSRECT_EQ(oldToWindowFrame, newToWindowFrame); + NSRect newToSubwindowFrame = [toSubwindow frame]; + NSRect expectedToSubwindowFrame = oldToSubwindowFrame; + expectedToSubwindowFrame.origin.y -= bookmarks::kBookmarkBarHeight; + expectedToSubwindowFrame.size.height += bookmarks::kBookmarkBarHeight; + EXPECT_NSRECT_EQ(expectedToSubwindowFrame, newToSubwindowFrame); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragMoveWithinFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + NSRect oldToWindowFrame = [toWindow frame]; + // Drag a folder button to the top within the same parent. + BookmarkButton* draggedButton = [folderController buttonWithTitleEqualTo:@"4f2f"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"4f1f"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton top] + copy:NO]; + // The button should have landed above "4f1f". + const std::wstring expected_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 4f:[ 4f2f:[ 4f2f1b 4f2f2b 4f2f3b ] " + "4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root)); + + // The window should not have gone away. + EXPECT_TRUE([bar_ folderController]); + + // The folder window should not have changed. + NSRect newToWindowFrame = [toWindow frame]; + EXPECT_NSRECT_EQ(oldToWindowFrame, newToWindowFrame); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragParentOntoChild) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + // Drag a folder button to one of its children. + BookmarkButton* draggedButton = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"4f3f"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton top] + copy:NO]; + // The model should not have changed. + EXPECT_EQ(model_string, model_test_utils::ModelStringFromNode(root)); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragMoveChildToParent) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f:[ 4f2f1b " + "4f2f2b 4f2f3b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu and a subfolder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"4f"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + BookmarkButton* toSubfolder = [folderController buttonWithTitleEqualTo:@"4f2f"]; + ASSERT_TRUE(toSubfolder); + [[toSubfolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toSubfolder]; + BookmarkBarFolderController* subfolderController = + [folderController folderController]; + EXPECT_TRUE(subfolderController); + + // Drag a subfolder bookmark to the parent folder. + BookmarkButton* draggedButton = + [subfolderController buttonWithTitleEqualTo:@"4f2f3b"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"4f2f"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton top] + copy:NO]; + // The button should have landed above "4f2f". + const std::wstring expected_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4f:[ 4f1f:[ 4f1f1b 4f1f2b 4f1f3b ] 4f2f3b 4f2f:[ " + "4f2f1b 4f2f2b ] 4f3f:[ 4f3f1b 4f3f2b 4f3f3b ] ] 5b "); + EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root)); + + // The window should not have gone away. + EXPECT_TRUE([bar_ folderController]); + // The subfolder should have gone away. + EXPECT_FALSE([folderController folderController]); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, DragWindowResizing) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring + model_string(L"a b:[ b1 b2 b3 ] reallyReallyLongBookmarkName c "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"b"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* toWindow = [folderController window]; + EXPECT_TRUE(toWindow); + CGFloat oldWidth = NSWidth([toWindow frame]); + // Drag the bookmark with a long name to the folder. + BookmarkButton* draggedButton = + [bar_ buttonWithTitleEqualTo:@"reallyReallyLongBookmarkName"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [folderController buttonWithTitleEqualTo:@"b1"]; + ASSERT_TRUE(targetButton); + [folderController dragButton:draggedButton + to:[targetButton center] + copy:NO]; + // Verify the model change. + const std::wstring + expected_string(L"a b:[ b1 reallyReallyLongBookmarkName b2 b3 ] c "); + EXPECT_EQ(expected_string, model_test_utils::ModelStringFromNode(root)); + // Verify the window grew. Just test a reasonable width gain. + CGFloat newWidth = NSWidth([toWindow frame]); + EXPECT_LT(oldWidth + 30.0, newWidth); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, MoveRemoveAddButtons) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2b 2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Pop up a folder menu. + BookmarkButton* toFolder = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(toFolder); + [[toFolder target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:toFolder]; + BookmarkBarFolderController* folder = [bar_ folderController]; + EXPECT_TRUE(folder); + + // Remember how many buttons are showing. + NSArray* buttons = [folder buttons]; + NSUInteger oldDisplayedButtons = [buttons count]; + + // Move a button around a bit. + [folder moveButtonFromIndex:0 toIndex:2]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f2b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f1b"]); + EXPECT_EQ(oldDisplayedButtons, [buttons count]); + [folder moveButtonFromIndex:2 toIndex:0]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f2b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f3b"]); + EXPECT_EQ(oldDisplayedButtons, [buttons count]); + + // Add a couple of buttons. + const BookmarkNode* node = root->GetChild(2); // Purloin an existing node. + [folder addButtonForNode:node atIndex:0]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f2b"]); + EXPECT_TRUE([[[buttons objectAtIndex:3] title] isEqualToString:@"2f3b"]); + EXPECT_EQ(oldDisplayedButtons + 1, [buttons count]); + node = root->GetChild(3); + [folder addButtonForNode:node atIndex:-1]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f1b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f2b"]); + EXPECT_TRUE([[[buttons objectAtIndex:3] title] isEqualToString:@"2f3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:4] title] isEqualToString:@"4b"]); + EXPECT_EQ(oldDisplayedButtons + 2, [buttons count]); + + // Remove a couple of buttons. + [folder removeButton:4 animate:NO]; + [folder removeButton:1 animate:NO]; + EXPECT_TRUE([[[buttons objectAtIndex:0] title] isEqualToString:@"3b"]); + EXPECT_TRUE([[[buttons objectAtIndex:1] title] isEqualToString:@"2f2b"]); + EXPECT_TRUE([[[buttons objectAtIndex:2] title] isEqualToString:@"2f3b"]); + EXPECT_EQ(oldDisplayedButtons, [buttons count]); +} + +TEST_F(BookmarkBarFolderControllerDragDropTest, ControllerForNode) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Find the main bar controller. + const void* expectedController = bar_; + const void* actualController = [bar_ controllerForNode:root]; + EXPECT_EQ(expectedController, actualController); + + // Pop up the folder menu. + BookmarkButton* targetFolder = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(targetFolder); + [[targetFolder target] + performSelector:@selector(openBookmarkFolderFromButton:) + withObject:targetFolder]; + BookmarkBarFolderController* folder = [bar_ folderController]; + EXPECT_TRUE(folder); + + // Find the folder controller using the folder controller. + const BookmarkNode* targetNode = root->GetChild(1); + expectedController = folder; + actualController = [bar_ controllerForNode:targetNode]; + EXPECT_EQ(expectedController, actualController); + + // Find the folder controller from the bar. + actualController = [folder controllerForNode:targetNode]; + EXPECT_EQ(expectedController, actualController); +} + +@interface BookmarkBarControllerNoDelete : BookmarkBarController +- (IBAction)deleteBookmark:(id)sender; +@end + +@implementation BookmarkBarControllerNoDelete +- (IBAction)deleteBookmark:(id)sender { + // NOP +} +@end + +class BookmarkBarFolderControllerClosingTest : public + BookmarkBarFolderControllerDragDropTest { + public: + BookmarkBarFolderControllerClosingTest() { + bar_.reset([[BookmarkBarControllerNoDelete alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth([parent_view_ frame]) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + InstallAndToggleBar(bar_.get()); + } +}; + +TEST_F(BookmarkBarFolderControllerClosingTest, DeleteClosesFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::wstring model_string(L"1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b ] " + "2f3b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::wstring actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Open the folder menu and submenu. + BookmarkButton* target = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(target); + [[target target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:target]; + BookmarkBarFolderController* folder = [bar_ folderController]; + EXPECT_TRUE(folder); + BookmarkButton* subTarget = [folder buttonWithTitleEqualTo:@"2f2f"]; + ASSERT_TRUE(subTarget); + [[subTarget target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:subTarget]; + BookmarkBarFolderController* subFolder = [folder folderController]; + EXPECT_TRUE(subFolder); + + // Delete the folder node and verify the window closed down by looking + // for its controller again. + [folder deleteBookmark:folder]; + EXPECT_FALSE([folder folderController]); +} + // TODO(jrg): draggingEntered: and draggingExited: trigger timers so // they are hard to test. Factor out "fire timers" into routines // which can be overridden to fire immediately to make behavior diff --git a/chrome/browser/cocoa/bookmark_bar_folder_view.mm b/chrome/browser/cocoa/bookmark_bar_folder_view.mm index d712944..15a4db6 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_view.mm +++ b/chrome/browser/cocoa/bookmark_bar_folder_view.mm @@ -22,7 +22,6 @@ } - (void)drawRect:(NSRect)rect { - // TODO(jrg): copied from bookmark_bar_view but orientation changed. // Code dup sucks but I'm not sure I can take 16 lines and make it // generic for horiz vs vertical while keeping things simple. @@ -105,10 +104,6 @@ // Awkwardness since views open and close out from under us. if (inDrag_) { inDrag_ = NO; - - // This line makes sure menus get closed when a drag isn't - // completed. - [[self controller] closeAllBookmarkFolders]; } [self draggingExited:info]; diff --git a/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm b/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm index 96189f5..83472af 100644 --- a/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm +++ b/chrome/browser/cocoa/bookmark_bar_folder_view_unittest.mm @@ -122,6 +122,21 @@ disposition:(WindowOpenDisposition)disposition { } +- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)animate { +} + +- (id<BookmarkButtonControllerProtocol>)controllerForNode: + (const BookmarkNode*)node { + return nil; +} + +- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { +} + +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex { +} + @end namespace { diff --git a/chrome/browser/cocoa/bookmark_bar_unittest_helper.h b/chrome/browser/cocoa/bookmark_bar_unittest_helper.h new file mode 100644 index 0000000..402f8a1 --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_unittest_helper.h @@ -0,0 +1,48 @@ +// Copyright (c) 2009 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. + +#ifndef CHROME_BROWSER_COCOA_AUTOCOMPLETE_BOOKMARK_BAR_UNITTEST_HELPER_H_ +#define CHROME_BROWSER_COCOA_AUTOCOMPLETE_BOOKMARK_BAR_UNITTEST_HELPER_H_ + +#import <Foundation/Foundation.h> + +#import "chrome/browser/cocoa/bookmark_bar_controller.h" +#import "chrome/browser/cocoa/bookmark_bar_folder_controller.h" +#import "chrome/browser/cocoa/bookmark_button.h" + +@interface BookmarkBarController (BookmarkBarUnitTestHelper) + +// Return the bookmark button from this bar controller with the given +// |title|, otherwise nil. This does not recurse into folders. +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title; + +@end + + +@interface BookmarkBarFolderController (BookmarkBarUnitTestHelper) + +// Return the bookmark button from this folder controller with the given +// |title|, otherwise nil. This does not recurse into subfolders. +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title; + +@end + + +@interface BookmarkButton (BookmarkBarUnitTestHelper) + +// Return the center of the button in the base coordinate system of the +// containing window. Useful for simulating mouse clicks or drags. +- (NSPoint)center; + +// Return the top of the button in the base coordinate system of the +// containing window. +- (NSPoint)top; + +// Return the center-left point of the button in the base coordinate system +// of the containing window. +- (NSPoint)left; + +@end + +#endif // CHROME_BROWSER_COCOA_AUTOCOMPLETE_TEXT_FIELD_UNITTEST_HELPER_H_ diff --git a/chrome/browser/cocoa/bookmark_bar_unittest_helper.mm b/chrome/browser/cocoa/bookmark_bar_unittest_helper.mm new file mode 100644 index 0000000..114197e --- /dev/null +++ b/chrome/browser/cocoa/bookmark_bar_unittest_helper.mm @@ -0,0 +1,67 @@ +// Copyright (c) 2009 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_unittest_helper.h" + +@interface NSArray (BookmarkBarUnitTestHelper) + +// A helper function for scanning an array of buttons looking for the +// button with the given |title|. +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title; + +@end + + +@implementation NSArray (BookmarkBarUnitTestHelper) + +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title { + for (BookmarkButton* button in self) { + if ([[button title] isEqualToString:title]) + return button; + } + return nil; +} + +@end + +@implementation BookmarkBarController (BookmarkBarUnitTestHelper) + +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title { + return [[self buttons] buttonWithTitleEqualTo:title]; +} + +@end + +@implementation BookmarkBarFolderController(BookmarkBarUnitTestHelper) + +- (BookmarkButton*)buttonWithTitleEqualTo:(NSString*)title { + return [[self buttons] buttonWithTitleEqualTo:title]; +} + +@end + +@implementation BookmarkButton(BookmarkBarUnitTestHelper) + +- (NSPoint)center { + NSRect frame = [self frame]; + NSPoint center = NSMakePoint(NSMidX(frame), NSMidY(frame)); + center = [[self superview] convertPoint:center toView:nil]; + return center; +} + +- (NSPoint)top { + NSRect frame = [self frame]; + NSPoint top = NSMakePoint(NSMidX(frame), NSMaxY(frame)); + top = [[self superview] convertPoint:top toView:nil]; + return top; +} + +- (NSPoint)left { + NSRect frame = [self frame]; + NSPoint left = NSMakePoint(NSMinX(frame), NSMidY(frame)); + left = [[self superview] convertPoint:left toView:nil]; + return left; +} + +@end diff --git a/chrome/browser/cocoa/bookmark_button.h b/chrome/browser/cocoa/bookmark_button.h index cb62053..62acadc 100644 --- a/chrome/browser/cocoa/bookmark_button.h +++ b/chrome/browser/cocoa/bookmark_button.h @@ -46,6 +46,7 @@ class ThemeProvider; // or NSWindowController. The BookmarkButton doesn't use this // protocol directly; it is used when BookmarkButton controllers talk // to each other. +// // Other than the top level owner (the bookmark bar), all bookmark // button controllers have a parent controller. @protocol BookmarkButtonControllerProtocol @@ -116,6 +117,35 @@ class ThemeProvider; - (void)openAll:(const BookmarkNode*)node disposition:(WindowOpenDisposition)disposition; +// There are several operations which may affect the contents of a bookmark +// button controller after it has been created, primary of which are +// cut/paste/delete and drag/drop. Such changes may involve coordinating +// the bookmark button contents of two controllers (such as when a bookmark is +// dragged from one folder to another). The bookmark bar controller +// coordinates in response to notifications propogated by the bookmark model +// through BookmarkBarBridge calls. The following three functions are +// implemented by the controllers and are dispatched by the bookmark bar +// controller in response to notifications coming in from the BookmarkBarBridge. + +// Add a button for the given node to the bar or folder menu. This is safe +// to call when a folder menu window is open as that window will be updated. +// And index of -1 means to append to the end (bottom). +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex; + +// Move a button from one place in the menu to another. This is safe +// to call when a folder menu window is open as that window will be updated. +- (void)moveButtonFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex; + +// Remove the bookmark button at the given index. Show the poof animation +// if |animate:| is YES. It may be obvious, but this is safe +// to call when a folder menu window is open as that window will be updated. +- (void)removeButton:(NSInteger)buttonIndex animate:(BOOL)poof; + +// Determine the controller containing the button representing |node|, if any. +- (id<BookmarkButtonControllerProtocol>)controllerForNode: + (const BookmarkNode*)node; + @end // @protocol BookmarkButtonControllerProtocol diff --git a/chrome/browser/cocoa/bookmark_button.mm b/chrome/browser/cocoa/bookmark_button.mm index fea9d25..00a6b77 100644 --- a/chrome/browser/cocoa/bookmark_button.mm +++ b/chrome/browser/cocoa/bookmark_button.mm @@ -53,7 +53,6 @@ static const CGFloat kDragImageOpacity = 0.7; if ([self delegate]) { // Ask our delegate to fill the pasteboard for us. NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; - [[self delegate] fillPasteboard:pboard forDragOfButton:self]; // At the moment, moving bookmarks causes their buttons (like me!) diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 1087624..3b4cc66 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -128,6 +128,8 @@ 'test/chrome_process_util.cc', 'test/chrome_process_util.h', 'test/chrome_process_util_mac.cc', + 'test/model_test_utils.cc', + 'test/model_test_utils.h', 'test/profile_mock.h', 'test/test_browser_window.h', 'test/test_location_bar.h', @@ -637,6 +639,8 @@ 'browser/cocoa/bookmark_bar_folder_view_unittest.mm', 'browser/cocoa/bookmark_bar_folder_window_unittest.mm', 'browser/cocoa/bookmark_bar_toolbar_view_unittest.mm', + 'browser/cocoa/bookmark_bar_unittest_helper.h', + 'browser/cocoa/bookmark_bar_unittest_helper.mm', 'browser/cocoa/bookmark_bar_view_unittest.mm', 'browser/cocoa/bookmark_bubble_controller_unittest.mm', 'browser/cocoa/bookmark_button_unittest.mm', diff --git a/chrome/test/model_test_utils.cc b/chrome/test/model_test_utils.cc new file mode 100644 index 0000000..218cfd8 --- /dev/null +++ b/chrome/test/model_test_utils.cc @@ -0,0 +1,82 @@ +// 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. + +#include "model_test_utils.h" + +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "googleurl/src/gurl.h" + +namespace model_test_utils { + +std::wstring ModelStringFromNode(const BookmarkNode* node) { + // Since the children of the node are not available as a vector, + // we'll just have to do it the hard way. + int child_count = node->GetChildCount(); + std::wstring child_string; + for (int i = 0; i < child_count; ++i) { + const BookmarkNode* child = node->GetChild(i); + if (child->is_folder()) { + child_string += child->GetTitle() + L":[ " + ModelStringFromNode(child) + + L"] "; + } else { + child_string += child->GetTitle() + L" "; + } + } + return child_string; +} + +// Helper function which does the actual work of creating the nodes for +// a particular level in the hierarchy. +std::wstring::size_type AddNodesFromString(BookmarkModel& model, + const BookmarkNode* node, + const std::wstring& model_string, + std::wstring::size_type start_pos) { + DCHECK(node); + int index = node->GetChildCount(); + static const std::wstring folder_tell(L":["); + std::wstring::size_type end_pos = model_string.find(' ', start_pos); + while (end_pos != std::wstring::npos) { + std::wstring::size_type part_length = end_pos - start_pos; + std::wstring node_name = model_string.substr(start_pos, part_length); + // Are we at the end of a folder group? + if (node_name != L"]") { + // No, is it a folder? + std::wstring tell; + if (part_length > 2) + tell = node_name.substr(part_length - 2, 2); + if (tell == folder_tell) { + node_name = node_name.substr(0, part_length - 2); + const BookmarkNode* new_node = model.AddGroup(node, index, node_name); + end_pos = AddNodesFromString(model, new_node, model_string, + end_pos + 1); + } else { + std::string url_string("http://"); + url_string += std::string(node_name.begin(), node_name.end()); + url_string += ".com"; + model.AddURL(node, index, node_name, GURL(url_string)); + ++end_pos; + } + ++index; + start_pos = end_pos; + end_pos = model_string.find(' ', start_pos); + } else { + ++end_pos; + break; + } + } + return end_pos; +} + +void AddNodesFromModelString(BookmarkModel& model, + const BookmarkNode* node, + const std::wstring& model_string) { + DCHECK(node); + const std::wstring folder_tell(L":["); + std::wstring::size_type start_pos = 0; + std::wstring::size_type end_pos = + AddNodesFromString(model, node, model_string, start_pos); + DCHECK(end_pos == std::wstring::npos); +} + +} // namespace model_test_utils diff --git a/chrome/test/model_test_utils.h b/chrome/test/model_test_utils.h new file mode 100644 index 0000000..1d5c3c6 --- /dev/null +++ b/chrome/test/model_test_utils.h @@ -0,0 +1,40 @@ +// 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. + +#ifndef CHROME_TEST_MODEL_TEST_UTILS_H_ +#define CHROME_TEST_MODEL_TEST_UTILS_H_ + +#include <string> + +class BookmarkModel; +class BookmarkNode; + +namespace model_test_utils { + +// Return the descendants of |node| as a string useful for verifying node +// modifications. The format of the resulting string is: +// +// result = node " " , { node " " } +// node = bookmark title | folder +// folder = folder title ":[ " { node " " } "]" +// bookmark title = (* string with no spaces *) +// folder title = (* string with no spaces *) +// +// Example: "a f1:[ b d c ] d f2:[ e f g ] h " +std::wstring ModelStringFromNode(const BookmarkNode* node); + +// Create and add the node hierarchy specified by |nodeString| to the +// bookmark node given by |node|. The string has the same format as +// specified for ModelStringFromNode. The new nodes added to |node| +// are appended to the end of node's existing subnodes, if any. +// |model| must be the model of which |node| is a member. +// NOTE: The string format is very rigid and easily broken if not followed +// exactly (since we're using a very simple parser). +void AddNodesFromModelString(BookmarkModel& model, + const BookmarkNode* node, + const std::wstring& model_string); + +} // namespace model_test_utils + +#endif // CHROME_TEST_MODEL_TEST_UTILS_H_ |