diff options
Diffstat (limited to 'chrome/browser/ui/cocoa/bookmarks')
71 files changed, 16682 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h new file mode 100644 index 0000000..505211c --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h @@ -0,0 +1,46 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_ +#pragma once + +#include <utility> +#include <vector> + +#include "base/string16.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h" + +// A list of pairs containing the name and URL associated with each +// currently active tab in the active browser window. +typedef std::pair<string16, GURL> ActiveTabNameURLPair; +typedef std::vector<ActiveTabNameURLPair> ActiveTabsNameURLPairVector; + +// A controller for the Bookmark All Tabs sheet which is presented upon +// selecting the Bookmark All Tabs... menu item shown by the contextual +// menu in the bookmarks bar. +@interface BookmarkAllTabsController : BookmarkEditorBaseController { + @private + ActiveTabsNameURLPairVector activeTabPairsVector_; +} + +- (id)initWithParentWindow:(NSWindow*)parentWindow + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + configuration:(BookmarkEditor::Configuration)configuration; + +@end + +@interface BookmarkAllTabsController(TestingAPI) + +// Initializes the list of all tab names and URLs. Overridden by unit test +// to provide canned test data. +- (void)UpdateActiveTabPairs; + +// Provides testing access to tab pairs list. +- (ActiveTabsNameURLPairVector*)activeTabPairsVector; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_ALL_TABS_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm new file mode 100644 index 0000000..5e8b0f2 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm @@ -0,0 +1,88 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h" + +#include "app/l10n_util_mac.h" +#include "base/string16.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h" +#include "grit/generated_resources.h" + +@implementation BookmarkAllTabsController + +- (id)initWithParentWindow:(NSWindow*)parentWindow + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + configuration:(BookmarkEditor::Configuration)configuration { + NSString* nibName = @"BookmarkAllTabs"; + if ((self = [super initWithParentWindow:parentWindow + nibName:nibName + profile:profile + parent:parent + configuration:configuration])) { + } + return self; +} + +- (void)awakeFromNib { + [self setInitialName: + l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME)]; + [super awakeFromNib]; +} + +#pragma mark Bookmark Editing + +- (void)UpdateActiveTabPairs { + activeTabPairsVector_.clear(); + Browser* browser = BrowserList::GetLastActive(); + TabStripModel* tabstrip_model = browser->tabstrip_model(); + const int tabCount = tabstrip_model->count(); + for (int i = 0; i < tabCount; ++i) { + TabContents* tc = tabstrip_model->GetTabContentsAt(i)->tab_contents(); + const string16 tabTitle = tc->GetTitle(); + const GURL& tabURL(tc->GetURL()); + ActiveTabNameURLPair tabPair(tabTitle, tabURL); + activeTabPairsVector_.push_back(tabPair); + } +} + +// Called by -[BookmarkEditorBaseController ok:]. Creates the container +// folder for the tabs and then the bookmarks in that new folder. +// Returns a BOOL as an NSNumber indicating that the commit may proceed. +- (NSNumber*)didCommit { + NSString* name = [[self displayName] stringByTrimmingCharactersInSet: + [NSCharacterSet newlineCharacterSet]]; + std::wstring newTitle = base::SysNSStringToWide(name); + const BookmarkNode* newParentNode = [self selectedNode]; + int newIndex = newParentNode->GetChildCount(); + // Create the new folder which will contain all of the tab URLs. + NSString* newFolderName = [self displayName]; + string16 newFolderString = base::SysNSStringToUTF16(newFolderName); + BookmarkModel* model = [self bookmarkModel]; + const BookmarkNode* newFolder = model->AddGroup(newParentNode, newIndex, + newFolderString); + // Get a list of all open tabs, create nodes for them, and add + // to the new folder node. + [self UpdateActiveTabPairs]; + int i = 0; + for (ActiveTabsNameURLPairVector::const_iterator it = + activeTabPairsVector_.begin(); + it != activeTabPairsVector_.end(); ++it, ++i) { + model->AddURL(newFolder, i, it->first, it->second); + } + return [NSNumber numberWithBool:YES]; +} + +- (ActiveTabsNameURLPairVector*)activeTabPairsVector { + return &activeTabPairsVector_; +} + +@end // BookmarkAllTabsController + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm new file mode 100644 index 0000000..9f8a7d8 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm @@ -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. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +@interface BookmarkAllTabsControllerOverride : BookmarkAllTabsController +@end + +@implementation BookmarkAllTabsControllerOverride + +- (void)UpdateActiveTabPairs { + ActiveTabsNameURLPairVector* activeTabPairsVector = + [self activeTabPairsVector]; + activeTabPairsVector->clear(); + activeTabPairsVector->push_back( + ActiveTabNameURLPair(ASCIIToUTF16("at-0"), GURL("http://at-0.com"))); + activeTabPairsVector->push_back( + ActiveTabNameURLPair(ASCIIToUTF16("at-1"), GURL("http://at-1.com"))); + activeTabPairsVector->push_back( + ActiveTabNameURLPair(ASCIIToUTF16("at-2"), GURL("http://at-2.com"))); +} + +@end + +class BookmarkAllTabsControllerTest : public CocoaTest { + public: + BrowserTestHelper helper_; + const BookmarkNode* parent_node_; + BookmarkAllTabsControllerOverride* controller_; + const BookmarkNode* group_a_; + + BookmarkAllTabsControllerTest() { + BookmarkModel& model(*(helper_.profile()->GetBookmarkModel())); + const BookmarkNode* root = model.GetBookmarkBarNode(); + group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a")); + model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com")); + model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com")); + model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com")); + } + + virtual BookmarkAllTabsControllerOverride* CreateController() { + return [[BookmarkAllTabsControllerOverride alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:group_a_ + configuration:BookmarkEditor::SHOW_TREE]; + } + + virtual void SetUp() { + CocoaTest::SetUp(); + controller_ = CreateController(); + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } +}; + +TEST_F(BookmarkAllTabsControllerTest, BookmarkAllTabs) { + // OK button should always be enabled. + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ selectTestNodeInBrowser:group_a_]; + [controller_ setDisplayName:@"ALL MY TABS"]; + [controller_ ok:nil]; + EXPECT_EQ(4, group_a_->GetChildCount()); + const BookmarkNode* folderChild = group_a_->GetChild(3); + EXPECT_EQ(folderChild->GetTitle(), ASCIIToUTF16("ALL MY TABS")); + EXPECT_EQ(3, folderChild->GetChildCount()); +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h new file mode 100644 index 0000000..811c450 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h @@ -0,0 +1,60 @@ +// 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. + +// C++ bridge class between Chromium and Cocoa to connect the +// Bookmarks (model) with the Bookmark Bar (view). +// +// There is exactly one BookmarkBarBridge per BookmarkBarController / +// BrowserWindowController / Browser. + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_ +#pragma once + +#include "base/basictypes.h" +#include "chrome/browser/bookmarks/bookmark_model_observer.h" + +class Browser; +@class BookmarkBarController; + +class BookmarkBarBridge : public BookmarkModelObserver { + public: + BookmarkBarBridge(BookmarkBarController* controller, + BookmarkModel* model); + virtual ~BookmarkBarBridge(); + + // Overridden from BookmarkModelObserver + virtual void Loaded(BookmarkModel* model); + virtual void BookmarkModelBeingDeleted(BookmarkModel* model); + virtual void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index); + virtual void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index); + virtual void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node); + virtual void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node); + virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node); + virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node); + + virtual void BookmarkImportBeginning(BookmarkModel* model); + virtual void BookmarkImportEnding(BookmarkModel* model); + + private: + BookmarkBarController* controller_; // weak; owns me + BookmarkModel* model_; // weak; it is owned by a Profile. + bool batch_mode_; + + DISALLOW_COPY_AND_ASSIGN(BookmarkBarBridge); +}; + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_BRIDGE_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm new file mode 100644 index 0000000..54f5e81 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm @@ -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 "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" + +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" + +BookmarkBarBridge::BookmarkBarBridge(BookmarkBarController* controller, + BookmarkModel* model) + : controller_(controller), + model_(model), + batch_mode_(false) { + model_->AddObserver(this); + + // Bookmark loading is async; it may may not have happened yet. + // We will be notified when that happens with the AddObserver() call. + if (model->IsLoaded()) + Loaded(model); +} + +BookmarkBarBridge::~BookmarkBarBridge() { + model_->RemoveObserver(this); +} + +void BookmarkBarBridge::Loaded(BookmarkModel* model) { + [controller_ loaded:model]; +} + +void BookmarkBarBridge::BookmarkModelBeingDeleted(BookmarkModel* model) { + [controller_ beingDeleted:model]; +} + +void BookmarkBarBridge::BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) { + [controller_ nodeMoved:model + oldParent:old_parent oldIndex:old_index + newParent:new_parent newIndex:new_index]; +} + +void BookmarkBarBridge::BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + if (!batch_mode_) { + [controller_ nodeAdded:model parent:parent index:index]; + } +} + +void BookmarkBarBridge::BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) { + [controller_ nodeRemoved:model parent:parent index:old_index]; +} + +void BookmarkBarBridge::BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) { + [controller_ nodeChanged:model node:node]; +} + +void BookmarkBarBridge::BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node) { + [controller_ nodeFavIconLoaded:model node:node]; +} + +void BookmarkBarBridge::BookmarkNodeChildrenReordered( + BookmarkModel* model, const BookmarkNode* node) { + [controller_ nodeChildrenReordered:model node:node]; +} + +void BookmarkBarBridge::BookmarkImportBeginning(BookmarkModel* model) { + batch_mode_ = true; +} + +void BookmarkBarBridge::BookmarkImportEnding(BookmarkModel* model) { + batch_mode_ = false; + [controller_ loaded:model]; +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm new file mode 100644 index 0000000..067d327 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm @@ -0,0 +1,135 @@ +// 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 "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#include "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +// TODO(jrg): use OCMock. + +namespace { + +// Information needed to open a URL, as passed to the +// BookmarkBarController's delegate. +typedef std::pair<GURL,WindowOpenDisposition> OpenInfo; + +} // The namespace must end here -- I need to use OpenInfo in + // FakeBookmarkBarController but can't place + // FakeBookmarkBarController itself in the namespace ("error: + // Objective-C declarations may only appear in global scope") + +// Oddly, we are our own delegate. +@interface FakeBookmarkBarController : BookmarkBarController { + @public + scoped_nsobject<NSMutableArray> callbacks_; + std::vector<OpenInfo> opens_; +} +@end + +@implementation FakeBookmarkBarController + +- (id)initWithBrowser:(Browser*)browser { + if ((self = [super initWithBrowser:browser + initialWidth:100 // arbitrary + delegate:nil + resizeDelegate:nil])) { + callbacks_.reset([[NSMutableArray alloc] init]); + } + return self; +} + +- (void)loaded:(BookmarkModel*)model { + [callbacks_ addObject:[NSNumber numberWithInt:0]]; +} + +- (void)beingDeleted:(BookmarkModel*)model { + [callbacks_ addObject:[NSNumber numberWithInt:1]]; +} + +- (void)nodeMoved:(BookmarkModel*)model + oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex + newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { + [callbacks_ addObject:[NSNumber numberWithInt:2]]; +} + +- (void)nodeAdded:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index { + [callbacks_ addObject:[NSNumber numberWithInt:3]]; +} + +- (void)nodeChanged:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [callbacks_ addObject:[NSNumber numberWithInt:4]]; +} + +- (void)nodeFavIconLoaded:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [callbacks_ addObject:[NSNumber numberWithInt:5]]; +} + +- (void)nodeChildrenReordered:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [callbacks_ addObject:[NSNumber numberWithInt:6]]; +} + +- (void)nodeRemoved:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index { + [callbacks_ addObject:[NSNumber numberWithInt:7]]; +} + +// Save the request. +- (void)openBookmarkURL:(const GURL&)url + disposition:(WindowOpenDisposition)disposition { + opens_.push_back(OpenInfo(url, disposition)); +} + +@end + + +class BookmarkBarBridgeTest : public CocoaTest { + public: + BrowserTestHelper browser_test_helper_; +}; + +// Call all the callbacks; make sure they are all redirected to the objc object. +TEST_F(BookmarkBarBridgeTest, TestRedirect) { + Browser* browser = browser_test_helper_.browser(); + Profile* profile = browser_test_helper_.profile(); + BookmarkModel* model = profile->GetBookmarkModel(); + + scoped_nsobject<NSView> parentView([[NSView alloc] + initWithFrame:NSMakeRect(0,0,100,100)]); + scoped_nsobject<NSView> webView([[NSView alloc] + initWithFrame:NSMakeRect(0,0,100,100)]); + scoped_nsobject<NSView> infoBarsView( + [[NSView alloc] initWithFrame:NSMakeRect(0,0,100,100)]); + + scoped_nsobject<FakeBookmarkBarController> + controller([[FakeBookmarkBarController alloc] initWithBrowser:browser]); + EXPECT_TRUE(controller.get()); + scoped_ptr<BookmarkBarBridge> bridge(new BookmarkBarBridge(controller.get(), + model)); + EXPECT_TRUE(bridge.get()); + + bridge->Loaded(NULL); + bridge->BookmarkModelBeingDeleted(NULL); + bridge->BookmarkNodeMoved(NULL, NULL, 0, NULL, 0); + bridge->BookmarkNodeAdded(NULL, NULL, 0); + bridge->BookmarkNodeChanged(NULL, NULL); + bridge->BookmarkNodeFavIconLoaded(NULL, NULL); + bridge->BookmarkNodeChildrenReordered(NULL, NULL); + bridge->BookmarkNodeRemoved(NULL, NULL, 0, NULL); + + // 8 calls above plus an initial Loaded() in init routine makes 9 + EXPECT_TRUE([controller.get()->callbacks_ count] == 9); + + for (int x = 1; x < 9; x++) { + NSNumber* num = [NSNumber numberWithInt:x-1]; + EXPECT_NSEQ(num, [controller.get()->callbacks_ objectAtIndex:x]); + } +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h new file mode 100644 index 0000000..b4e4bef --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h @@ -0,0 +1,38 @@ +// 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. + +// Constants used for positioning the bookmark bar. These aren't placed in a +// different file because they're conditionally included in cross platform code +// and thus no Objective-C++ stuff. + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_ +#pragma once + +namespace bookmarks { + +// Correction used for computing other values based on the height. +const int kVisualHeightOffset = 2; + +// Bar height, when opened in "always visible" mode. This is actually a little +// smaller than it should be (by |kVisualHeightOffset| points) because of the +// visual overlap with the main toolbar. When using this to compute values +// other than the actual height of the toolbar, be sure to add +// |kVisualHeightOffset|. +const int kBookmarkBarHeight = 26; + +// Our height, when visible in "new tab page" mode. +const int kNTPBookmarkBarHeight = 40; + +// The amount of space between the inner bookmark bar and the outer toolbar on +// new tab pages. +const int kNTPBookmarkBarPadding = + (kNTPBookmarkBarHeight - (kBookmarkBarHeight + kVisualHeightOffset)) / 2; + +// The height of buttons in the bookmark bar. +const int kBookmarkButtonHeight = kBookmarkBarHeight + kVisualHeightOffset; + +} // namespace bookmarks + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONSTANTS_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h new file mode 100644 index 0000000..64591ba --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h @@ -0,0 +1,399 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> +#include <map> + +#include "base/scoped_nsobject.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#include "chrome/browser/ui/cocoa/tab_strip_model_observer_bridge.h" +#import "chrome/common/chrome_application_mac.h" +#include "webkit/glue/window_open_disposition.h" + +@class BookmarkBarController; +@class BookmarkBarFolderController; +@class BookmarkBarView; +@class BookmarkButton; +@class BookmarkButtonCell; +@class BookmarkFolderTarget; +class BookmarkModel; +@class BookmarkMenu; +class BookmarkNode; +class Browser; +class GURL; +class PrefService; +class TabContents; +@class ToolbarController; +@protocol ViewResizer; + +namespace bookmarks { + +// Magic numbers from Cole +// TODO(jrg): create an objc-friendly version of bookmark_bar_constants.h? + +// Used as a maximum width for buttons on the bar. +const CGFloat kDefaultBookmarkWidth = 150.0; + +// Horizontal frame inset for buttons in the bookmark bar. +const CGFloat kBookmarkHorizontalPadding = 1.0; + +// Vertical frame inset for buttons in the bookmark bar. +const CGFloat kBookmarkVerticalPadding = 2.0; + +// Used as a min/max width for buttons on menus (not on the bar). +const CGFloat kBookmarkMenuButtonMinimumWidth = 100.0; +const CGFloat kBookmarkMenuButtonMaximumWidth = 485.0; + +// Horizontal separation between a menu button and both edges of its menu. +const CGFloat kBookmarkSubMenuHorizontalPadding = 5.0; + +// TODO(mrossetti): Add constant (kBookmarkVerticalSeparation) for the gap +// between buttons in a folder menu. Right now we're using +// kBookmarkVerticalPadding, which is dual purpose and wrong. +// http://crbug.com/59057 + +// Convenience constant giving the vertical distance from the top extent of one +// folder button to the next button. +const CGFloat kBookmarkButtonVerticalSpan = + kBookmarkButtonHeight + kBookmarkVerticalPadding; + +// The minimum separation between a folder menu and the edge of the screen. +// If the menu gets closer to the edge of the screen (either right or left) +// then it is pops up in the opposite direction. +// (See -[BookmarkBarFolderController childFolderWindowLeftForWidth:]). +const CGFloat kBookmarkHorizontalScreenPadding = 8.0; + +// Our NSScrollView is supposed to be just barely big enough to fit its +// contentView. It is actually a hair too small. +// This turns on horizontal scrolling which, although slight, is awkward. +// Make sure our window (and NSScrollView) are wider than its documentView +// by at least this much. +const CGFloat kScrollViewContentWidthMargin = 2; + +// Make subfolder menus overlap their parent menu a bit to give a better +// perception of a menuing system. +const CGFloat kBookmarkMenuOverlap = 5.0; + +// Delay before opening a subfolder (and closing the previous one) +// when hovering over a folder button. +const NSTimeInterval kHoverOpenDelay = 0.3; + +// Delay on hover before a submenu opens when dragging. +// Experimentally a drag hover open delay needs to be bigger than a +// normal (non-drag) menu hover open such as used in the bookmark folder. +// TODO(jrg): confirm feel of this constant with ui-team. +// http://crbug.com/36276 +const NSTimeInterval kDragHoverOpenDelay = 0.7; + +// Notes on use of kDragHoverCloseDelay in +// -[BookmarkBarFolderController draggingEntered:]. +// +// We have an implicit delay on stop-hover-open before a submenu +// closes. This cannot be zero since it's nice to move the mouse in a +// direct line from "current position" to "position of item in +// submenu". However, by doing so, it's possible to overlap a +// different button on the current menu. Example: +// +// Folder1 +// Folder2 ---> Sub1 +// Folder3 Sub2 +// Sub3 +// +// If you hover over the F in Folder2 to open the sub, and then want to +// select Sub3, a direct line movement of the mouse may cross over +// Folder3. Without this delay, that'll cause Sub to be closed before +// you get there, since a "hover over" of Folder3 gets activated. +// It's subtle but without the delay it feels broken. +// +// This is only really a problem with vertical menu --> vertical menu +// movement; the bookmark bar (horizontal menu, sort of) seems fine, +// perhaps because mouse move direction is purely vertical so there is +// no opportunity for overlap. +const NSTimeInterval kDragHoverCloseDelay = 0.4; + +} // namespace bookmarks + +// The interface for the bookmark bar controller's delegate. Currently, the +// delegate is the BWC and is responsible for ensuring that the toolbar is +// displayed correctly (as specified by |-getDesiredToolbarHeightCompression| +// and |-toolbarDividerOpacity|) at the beginning and at the end of an animation +// (or after a state change). +@protocol BookmarkBarControllerDelegate + +// Sent when the state has changed (after any animation), but before the final +// display update. +- (void)bookmarkBar:(BookmarkBarController*)controller + didChangeFromState:(bookmarks::VisualState)oldState + toState:(bookmarks::VisualState)newState; + +// Sent before the animation begins. +- (void)bookmarkBar:(BookmarkBarController*)controller +willAnimateFromState:(bookmarks::VisualState)oldState + toState:(bookmarks::VisualState)newState; + +@end + +// A controller for the bookmark bar in the browser window. Handles showing +// and hiding based on the preference in the given profile. +@interface BookmarkBarController : + NSViewController<BookmarkBarState, + BookmarkBarToolbarViewController, + BookmarkButtonDelegate, + BookmarkButtonControllerProtocol, + CrApplicationEventHookProtocol, + NSUserInterfaceValidations> { + @private + // The visual state of the bookmark bar. If an animation is running, this is + // set to the "destination" and |lastVisualState_| is set to the "original" + // state. This is set to |kInvalidState| on initialization (when the + // appropriate state is not yet known). + bookmarks::VisualState visualState_; + + // The "original" state of the bookmark bar if an animation is running, + // otherwise it should be |kInvalidState|. + bookmarks::VisualState lastVisualState_; + + Browser* browser_; // weak; owned by its window + BookmarkModel* bookmarkModel_; // weak; part of the profile owned by the + // top-level Browser object. + + // Our initial view width, which is applied in awakeFromNib. + CGFloat initialWidth_; + + // BookmarkNodes have a 64bit id. NSMenuItems have a 32bit tag used + // to represent the bookmark node they refer to. This map provides + // a mapping from one to the other, so we can properly identify the + // node from the item. When adding items in, we start with seedId_. + int32 seedId_; + std::map<int32,int64> menuTagMap_; + + // Our bookmark buttons, ordered from L-->R. + scoped_nsobject<NSMutableArray> buttons_; + + // The folder image so we can use one copy for all buttons + scoped_nsobject<NSImage> folderImage_; + + // The default image, so we can use one copy for all buttons. + scoped_nsobject<NSImage> defaultImage_; + + // If the bar is disabled, we hide it and ignore show/hide commands. + // Set when using fullscreen mode. + BOOL barIsEnabled_; + + // Bridge from Chrome-style C++ notifications (e.g. derived from + // BookmarkModelObserver) + scoped_ptr<BookmarkBarBridge> bridge_; + + // Delegate that is informed about state changes in the bookmark bar. + id<BookmarkBarControllerDelegate> delegate_; // weak + + // Delegate that can resize us. + id<ViewResizer> resizeDelegate_; // weak + + // Logic for dealing with a click on a bookmark folder button. + scoped_nsobject<BookmarkFolderTarget> folderTarget_; + + // A controller for a pop-up bookmark folder window (custom menu). + // This is not a scoped_nsobject because it owns itself (when its + // window closes the controller gets autoreleased). + BookmarkBarFolderController* folderController_; + + // Are watching for a "click outside" or other event which would + // signal us to close the bookmark bar folder menus? + BOOL watchingForExitEvent_; + + IBOutlet BookmarkBarView* buttonView_; // Contains 'no items' text fields. + IBOutlet BookmarkButton* offTheSideButton_; // aka the chevron. + IBOutlet NSMenu* buttonContextMenu_; + + NSRect originalNoItemsRect_; // Original, pre-resized field rect. + NSRect originalImportBookmarksRect_; // Original, pre-resized field rect. + + // "Other bookmarks" button on the right side. + scoped_nsobject<BookmarkButton> otherBookmarksButton_; + + // We have a special menu for folder buttons. This starts as a copy + // of the bar menu. + scoped_nsobject<BookmarkMenu> buttonFolderContextMenu_; + + // When doing a drag, this is folder button "hovered over" which we + // may want to open after a short delay. There are cases where a + // mouse-enter can open a folder (e.g. if the menus are "active") + // but that doesn't use this variable or need a delay so "hover" is + // the wrong term. + scoped_nsobject<BookmarkButton> hoverButton_; + + // We save the view width when we add bookmark buttons. This lets + // us avoid a rebuild until we've grown the window bigger than our + // initial build. + CGFloat savedFrameWidth_; + + // The number of buttons we display in the bookmark bar. This does + // not include the "off the side" chevron or the "Other Bookmarks" + // button. We use this number to determine if we need to display + // the chevron, and to know what to place in the chevron's menu. + // 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 displayedButtonCount_; + + // A state flag which tracks when the bar's folder menus should be shown. + // An initial click in any of the folder buttons turns this on and + // one of the following will turn it off: another click in the button, + // the window losing focus, a click somewhere other than in the bar + // or a folder menu. + BOOL showFolderMenus_; + + // Set to YES to prevent any node animations. Useful for unit testing so that + // incomplete animations do not cause valgrind complaints. + BOOL ignoreAnimations_; +} + +@property(readonly, nonatomic) bookmarks::VisualState visualState; +@property(readonly, nonatomic) bookmarks::VisualState lastVisualState; +@property(assign, nonatomic) id<BookmarkBarControllerDelegate> delegate; + +// Initializes the bookmark bar controller with the given browser +// profile and delegates. +- (id)initWithBrowser:(Browser*)browser + initialWidth:(CGFloat)initialWidth + delegate:(id<BookmarkBarControllerDelegate>)delegate + resizeDelegate:(id<ViewResizer>)resizeDelegate; + +// Updates the bookmark bar (from its current, possibly in-transition) state to +// the one appropriate for the new conditions. +- (void)updateAndShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar + withAnimation:(BOOL)animate; + +// Update the visible state of the bookmark bar. +- (void)updateVisibility; + +// Turn on or off the bookmark bar and prevent or reallow its appearance. On +// disable, toggle off if shown. On enable, show only if needed. App and popup +// windows do not show a bookmark bar. +- (void)setBookmarkBarEnabled:(BOOL)enabled; + +// Returns the amount by which the toolbar above should be compressed. +- (CGFloat)getDesiredToolbarHeightCompression; + +// Gets the appropriate opacity for the toolbar's divider; 0 means that it +// shouldn't be shown. +- (CGFloat)toolbarDividerOpacity; + +// Updates the sizes and positions of the subviews. +// TODO(viettrungluu): I'm not convinced this should be public, but I currently +// need it for animations. Try not to propagate its use. +- (void)layoutSubviews; + +// Called by our view when it is moved to a window. +- (void)viewDidMoveToWindow; + +// Import bookmarks from another browser. +- (IBAction)importBookmarks:(id)sender; + +// Provide a favIcon for a bookmark node. May return nil. +- (NSImage*)favIconForNode:(const BookmarkNode*)node; + +// Used for situations where the bookmark bar folder menus should no longer +// be actively popping up. Called when the window loses focus, a click has +// occured outside the menus or a bookmark has been activated. (Note that this +// differs from the behavior of the -[BookmarkButtonControllerProtocol +// closeAllBookmarkFolders] method in that the latter does not terminate menu +// tracking since it may be being called in response to actions (such as +// dragging) where a 'stale' menu presentation should first be collapsed before +// presenting a new menu.) +- (void)closeFolderAndStopTrackingMenus; + +// Checks if operations such as edit or delete are allowed. +- (BOOL)canEditBookmark:(const BookmarkNode*)node; + +// Actions for manipulating bookmarks. +// Open a normal bookmark or folder from a button, ... +- (IBAction)openBookmark:(id)sender; +- (IBAction)openBookmarkFolderFromButton:(id)sender; +// From the "off the side" button, ... +- (IBAction)openOffTheSideFolderFromButton:(id)sender; +// From a context menu over the button, ... +- (IBAction)openBookmarkInNewForegroundTab:(id)sender; +- (IBAction)openBookmarkInNewWindow:(id)sender; +- (IBAction)openBookmarkInIncognitoWindow:(id)sender; +- (IBAction)editBookmark:(id)sender; +- (IBAction)cutBookmark:(id)sender; +- (IBAction)copyBookmark:(id)sender; +- (IBAction)pasteBookmark:(id)sender; +- (IBAction)deleteBookmark:(id)sender; +// From a context menu over the bar, ... +- (IBAction)openAllBookmarks:(id)sender; +- (IBAction)openAllBookmarksNewWindow:(id)sender; +- (IBAction)openAllBookmarksIncognitoWindow:(id)sender; +// Or from a context menu over either the bar or a button. +- (IBAction)addPage:(id)sender; +- (IBAction)addFolder:(id)sender; + +@end + +// Redirects from BookmarkBarBridge, the C++ object which glues us to +// the rest of Chromium. Internal to BookmarkBarController. +@interface BookmarkBarController(BridgeRedirect) +- (void)loaded:(BookmarkModel*)model; +- (void)beingDeleted:(BookmarkModel*)model; +- (void)nodeAdded:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index; +- (void)nodeChanged:(BookmarkModel*)model + node:(const BookmarkNode*)node; +- (void)nodeMoved:(BookmarkModel*)model + oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex + newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex; +- (void)nodeRemoved:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index; +- (void)nodeFavIconLoaded:(BookmarkModel*)model + node:(const BookmarkNode*)node; +- (void)nodeChildrenReordered:(BookmarkModel*)model + node:(const BookmarkNode*)node; +@end + +// These APIs should only be used by unit tests (or used internally). +@interface BookmarkBarController(InternalOrTestingAPI) +- (BookmarkBarView*)buttonView; +- (NSMutableArray*)buttons; +- (NSMenu*)offTheSideMenu; +- (NSButton*)offTheSideButton; +- (BOOL)offTheSideButtonIsHidden; +- (BookmarkButton*)otherBookmarksButton; +- (BookmarkBarFolderController*)folderController; +- (id)folderTarget; +- (int)displayedButtonCount; +- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition; +- (void)clearBookmarkBar; +- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node; +- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell xOffset:(int*)xOffset; +- (void)checkForBookmarkButtonGrowth:(NSButton*)button; +- (void)frameDidChange; +- (int64)nodeIdFromMenuTag:(int32)tag; +- (int32)menuTagFromNodeId:(int64)menuid; +- (const BookmarkNode*)nodeFromMenuItem:(id)sender; +- (void)updateTheme:(ThemeProvider*)themeProvider; +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point; +- (BOOL)isEventAnExitEvent:(NSEvent*)event; +- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX; + +// The following are for testing purposes only and are not used internally. +- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node; +- (NSMenu*)buttonContextMenu; +- (void)setButtonContextMenu:(id)menu; +// Set to YES in order to prevent animations. +- (void)setIgnoreAnimations:(BOOL)ignore; +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm new file mode 100644 index 0000000..c08aae0 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm @@ -0,0 +1,2497 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" + +#include "app/l10n_util_mac.h" +#include "app/resource_bundle.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_editor.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/bookmarks/bookmark_utils.h" +#include "chrome/browser/metrics/user_metrics.h" +#include "chrome/browser/prefs/pref_service.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/tab_contents/tab_contents.h" +#include "chrome/browser/tab_contents/tab_contents_view.h" +#import "chrome/browser/themes/browser_theme_provider.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#import "chrome/browser/ui/cocoa/background_gradient_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/event_utils.h" +#import "chrome/browser/ui/cocoa/fullscreen_controller.h" +#import "chrome/browser/ui/cocoa/import_settings_dialog.h" +#import "chrome/browser/ui/cocoa/menu_button.h" +#import "chrome/browser/ui/cocoa/themed_window.h" +#import "chrome/browser/ui/cocoa/toolbar_controller.h" +#import "chrome/browser/ui/cocoa/view_id_util.h" +#import "chrome/browser/ui/cocoa/view_resizer.h" +#include "chrome/common/pref_names.h" +#include "grit/app_resources.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "skia/ext/skia_utils_mac.h" + +// Bookmark bar state changing and animations +// +// The bookmark bar has three real states: "showing" (a normal bar attached to +// the toolbar), "hidden", and "detached" (pretending to be part of the web +// content on the NTP). It can, or at least should be able to, animate between +// these states. There are several complications even without animation: +// - The placement of the bookmark bar is done by the BWC, and it needs to know +// the state in order to place the bookmark bar correctly (immediately below +// the toolbar when showing, below the infobar when detached). +// - The "divider" (a black line) needs to be drawn by either the toolbar (when +// the bookmark bar is hidden or detached) or by the bookmark bar (when it is +// showing). It should not be drawn by both. +// - The toolbar needs to vertically "compress" when the bookmark bar is +// showing. This ensures the proper display of both the bookmark bar and the +// toolbar, and gives a padded area around the bookmark bar items for right +// clicks, etc. +// +// Our model is that the BWC controls us and also the toolbar. We try not to +// talk to the browser nor the toolbar directly, instead centralizing control in +// the BWC. The key method by which the BWC controls us is +// |-updateAndShowNormalBar:showDetachedBar:withAnimation:|. This invokes state +// changes, and at appropriate times we request that the BWC do things for us +// via either the resize delegate or our general delegate. If the BWC needs any +// information about what it should do, or tell the toolbar to do, it can then +// query us back (e.g., |-isShownAs...|, |-getDesiredToolbarHeightCompression|, +// |-toolbarDividerOpacity|, etc.). +// +// Animation-related complications: +// - Compression of the toolbar is touchy during animation. It must not be +// compressed while the bookmark bar is animating to/from showing (from/to +// hidden), otherwise it would look like the bookmark bar's contents are +// sliding out of the controls inside the toolbar. As such, we have to make +// sure that the bookmark bar is shown at the right location and at the +// right height (at various points in time). +// - Showing the divider is also complicated during animation between hidden +// and showing. We have to make sure that the toolbar does not show the +// divider despite the fact that it's not compressed. The exception to this +// is at the beginning/end of the animation when the toolbar is still +// uncompressed but the bookmark bar has height 0. If we're not careful, we +// get a flicker at this point. +// - We have to ensure that we do the right thing if we're told to change state +// while we're running an animation. The generic/easy thing to do is to jump +// to the end state of our current animation, and (if the new state change +// again involves an animation) begin the new animation. We can do better +// than that, however, and sometimes just change the current animation to go +// to the new end state (e.g., by "reversing" the animation in the showing -> +// hidden -> showing case). We also have to ensure that demands to +// immediately change state are always honoured. +// +// Pointers to animation logic: +// - |-moveToVisualState:withAnimation:| starts animations, deciding which ones +// we know how to handle. +// - |-doBookmarkBarAnimation| has most of the actual logic. +// - |-getDesiredToolbarHeightCompression| and |-toolbarDividerOpacity| contain +// related logic. +// - The BWC's |-layoutSubviews| needs to know how to position things. +// - The BWC should implement |-bookmarkBar:didChangeFromState:toState:| and +// |-bookmarkBar:willAnimateFromState:toState:| in order to inform the +// toolbar of required changes. + +namespace { + +// Overlap (in pixels) between the toolbar and the bookmark bar (when showing in +// normal mode). +const CGFloat kBookmarkBarOverlap = 3.0; + +// Duration of the bookmark bar animations. +const NSTimeInterval kBookmarkBarAnimationDuration = 0.12; + +} // namespace + +@interface BookmarkBarController(Private) + +// Determines the appropriate state for the given situation. ++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar; + +// Moves to the given next state (from the current state), possibly animating. +// If |animate| is NO, it will stop any running animation and jump to the given +// state. If YES, it may either (depending on implementation) jump to the end of +// the current animation and begin the next one, or stop the current animation +// mid-flight and animate to the next state. +- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState + withAnimation:(BOOL)animate; + +// Return the backdrop to the bookmark bar as various types. +- (BackgroundGradientView*)backgroundGradientView; +- (AnimatableView*)animatableView; + +// Create buttons for all items in the given bookmark node tree. +// 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; + +// Stops any current animation in its tracks (midway). +- (void)stopCurrentAnimation; + +// Show/hide the bookmark bar. +// if |animate| is YES, the changes are made using the animator; otherwise they +// are made immediately. +- (void)showBookmarkBarWithAnimation:(BOOL)animate; + +// Handles animating the resize of the content view. Returns YES if it handled +// the animation, NO if not (and hence it should be done instantly). +- (BOOL)doBookmarkBarAnimation; + +// |point| is in the base coordinate system of the destination window; +// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be +// made and inserted into the new location while leaving the bookmark in +// the old location, otherwise move the bookmark by removing from its old +// location and inserting into the new location. +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy; + +// Returns the index in the model for a drag to the location given by +// |point|. This is determined by finding the first button before the center +// of which |point| falls, scanning left to right. Note that, currently, only +// the x-coordinate of |point| is considered. Though not currently implemented, +// we may check for errors, in which case this would return negative value; +// callers should check for this. +- (int)indexForDragToPoint:(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; +- (void)clearMenuTagMap; +- (int)preferredHeight; +- (void)addNonBookmarkButtonsToView; +- (void)addButtonsToView; +- (void)centerNoItemsLabel; +- (void)setNodeForBarMenu; + +- (void)watchForExitEvent:(BOOL)watch; + +@end + +@implementation BookmarkBarController + +@synthesize visualState = visualState_; +@synthesize lastVisualState = lastVisualState_; +@synthesize delegate = delegate_; + +- (id)initWithBrowser:(Browser*)browser + initialWidth:(float)initialWidth + delegate:(id<BookmarkBarControllerDelegate>)delegate + resizeDelegate:(id<ViewResizer>)resizeDelegate { + if ((self = [super initWithNibName:@"BookmarkBar" + bundle:mac_util::MainAppBundle()])) { + // Initialize to an invalid state. + visualState_ = bookmarks::kInvalidState; + lastVisualState_ = bookmarks::kInvalidState; + + browser_ = browser; + initialWidth_ = initialWidth; + bookmarkModel_ = browser_->profile()->GetBookmarkModel(); + buttons_.reset([[NSMutableArray alloc] init]); + delegate_ = delegate; + resizeDelegate_ = resizeDelegate; + folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]); + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + folderImage_.reset( + [rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]); + defaultImage_.reset([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) retain]); + + // Register for theme changes, bookmark button pulsing, ... + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(themeDidChangeNotification:) + name:kBrowserThemeDidChangeNotification + object:nil]; + [defaultCenter addObserver:self + selector:@selector(pulseBookmarkNotification:) + name:bookmark_button::kPulseBookmarkButtonNotification + object:nil]; + + // This call triggers an awakeFromNib, which builds the bar, which + // might uses folderImage_. So make sure it happens after + // folderImage_ is loaded. + [[self animatableView] setResizeDelegate:resizeDelegate]; + } + return self; +} + +- (void)pulseBookmarkNotification:(NSNotification*)notification { + NSDictionary* dict = [notification userInfo]; + const BookmarkNode* node = NULL; + NSValue *value = [dict objectForKey:bookmark_button::kBookmarkKey]; + DCHECK(value); + if (value) + node = static_cast<const BookmarkNode*>([value pointerValue]); + NSNumber* number = [dict + objectForKey:bookmark_button::kBookmarkPulseFlagKey]; + DCHECK(number); + BOOL doPulse = number ? [number boolValue] : NO; + + // 3 cases: + // button on the bar: flash it + // button in "other bookmarks" folder: flash other bookmarks + // button in "off the side" folder: flash the chevron + for (BookmarkButton* button in [self buttons]) { + if ([button bookmarkNode] == node) { + [button setIsContinuousPulsing:doPulse]; + return; + } + } + if ([otherBookmarksButton_ bookmarkNode] == node) { + [otherBookmarksButton_ setIsContinuousPulsing:doPulse]; + return; + } + if (node->GetParent() == bookmarkModel_->GetBookmarkBarNode()) { + [offTheSideButton_ setIsContinuousPulsing:doPulse]; + return; + } + + NOTREACHED() << "no bookmark button found to pulse!"; +} + +- (void)dealloc { + // We better stop any in-flight animation if we're being killed. + [[self animatableView] stopAnimation]; + + // Remove our view from its superview so it doesn't attempt to reference + // it when the controller is gone. + //TODO(dmaclach): Remove -- http://crbug.com/25845 + [[self view] removeFromSuperview]; + + // Be sure there is no dangling pointer. + if ([[self view] respondsToSelector:@selector(setController:)]) + [[self view] performSelector:@selector(setController:) withObject:nil]; + + // For safety, make sure the buttons can no longer call us. + for (BookmarkButton* button in buttons_.get()) { + [button setDelegate:nil]; + [button setTarget:nil]; + [button setAction:nil]; + } + + bridge_.reset(NULL); + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [self watchForExitEvent:NO]; + [super dealloc]; +} + +- (void)awakeFromNib { + // We default to NOT open, which means height=0. + DCHECK([[self view] isHidden]); // Hidden so it's OK to change. + + // Set our initial height to zero, since that is what the superview + // expects. We will resize ourselves open later if needed. + [[self view] setFrame:NSMakeRect(0, 0, initialWidth_, 0)]; + + // Complete init of the "off the side" button, as much as we can. + [offTheSideButton_ setDraggable:NO]; + + // We are enabled by default. + barIsEnabled_ = YES; + + // Remember the original sizes of the 'no items' and 'import bookmarks' + // fields to aid in resizing when the window frame changes. + originalNoItemsRect_ = [[buttonView_ noItemTextfield] frame]; + originalImportBookmarksRect_ = [[buttonView_ importBookmarksButton] frame]; + + // To make life happier when the bookmark bar is floating, the chevron is a + // child of the button view. + [offTheSideButton_ removeFromSuperview]; + [buttonView_ addSubview:offTheSideButton_]; + + // Copy the bar menu so we know if it's from the bar or a folder. + // Then we set its represented item to be the bookmark bar. + buttonFolderContextMenu_.reset([[[self view] menu] copy]); + + // When resized we may need to add new buttons, or remove them (if + // no longer visible), or add/remove the "off the side" menu. + [[self view] setPostsFrameChangedNotifications:YES]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(frameDidChange) + name:NSViewFrameDidChangeNotification + object:[self view]]; + + // Watch for things going to or from fullscreen. + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(willEnterOrLeaveFullscreen:) + name:kWillEnterFullscreenNotification + object:nil]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(willEnterOrLeaveFullscreen:) + name:kWillLeaveFullscreenNotification + object:nil]; + + // Don't pass ourself along (as 'self') until our init is completely + // done. Thus, this call is (almost) last. + bridge_.reset(new BookmarkBarBridge(self, bookmarkModel_)); +} + +// Called by our main view (a BookmarkBarView) when it gets moved to a +// window. We perform operations which need to know the relevant +// window (e.g. watch for a window close) so they can't be performed +// earlier (such as in awakeFromNib). +- (void)viewDidMoveToWindow { + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + + // Remove any existing notifications before registering for new ones. + [defaultCenter removeObserver:self + name:NSWindowWillCloseNotification + object:nil]; + [defaultCenter removeObserver:self + name:NSWindowDidResignKeyNotification + object:nil]; + + [defaultCenter addObserver:self + selector:@selector(parentWindowWillClose:) + name:NSWindowWillCloseNotification + object:[[self view] window]]; + [defaultCenter addObserver:self + selector:@selector(parentWindowDidResignKey:) + name:NSWindowDidResignKeyNotification + object:[[self view] window]]; +} + +// When going fullscreen we can run into trouble. Our view is removed +// from the non-fullscreen window before the non-fullscreen window +// loses key, so our parentDidResignKey: callback never gets called. +// In addition, a bookmark folder controller needs to be autoreleased +// (in case it's in the event chain when closed), but the release +// implicitly needs to happen while it's connected to the original +// (non-fullscreen) window to "unlock bar visibility". Such a +// contract isn't honored when going fullscreen with the menu option +// (not with the keyboard shortcut). We fake it as best we can here. +// We have a similar problem leaving fullscreen. +- (void)willEnterOrLeaveFullscreen:(NSNotification*)notification { + if (folderController_) { + [self childFolderWillClose:folderController_]; + [self closeFolderAndStopTrackingMenus]; + } +} + +// NSNotificationCenter callback. +- (void)parentWindowWillClose:(NSNotification*)notification { + [self closeFolderAndStopTrackingMenus]; +} + +// NSNotificationCenter callback. +- (void)parentWindowDidResignKey:(NSNotification*)notification { + [self closeFolderAndStopTrackingMenus]; +} + +// Change the layout of the bookmark bar's subviews in response to a visibility +// change (e.g., show or hide the bar) or style change (attached or floating). +- (void)layoutSubviews { + NSRect frame = [[self view] frame]; + NSRect buttonViewFrame = NSMakeRect(0, 0, NSWidth(frame), NSHeight(frame)); + + // The state of our morph (if any); 1 is total bubble, 0 is the regular bar. + CGFloat morph = [self detachedMorphProgress]; + + // Add padding to the detached bookmark bar. + buttonViewFrame = NSInsetRect(buttonViewFrame, + morph * bookmarks::kNTPBookmarkBarPadding, + morph * bookmarks::kNTPBookmarkBarPadding); + + [buttonView_ setFrame:buttonViewFrame]; +} + +// We don't change a preference; we only change visibility. Preference changing +// (global state) is handled in |BrowserWindowCocoa::ToggleBookmarkBar()|. We +// simply update based on what we're told. +- (void)updateVisibility { + [self showBookmarkBarWithAnimation:NO]; +} + +- (void)setBookmarkBarEnabled:(BOOL)enabled { + if (enabled != barIsEnabled_) { + barIsEnabled_ = enabled; + [self updateVisibility]; + } +} + +- (CGFloat)getDesiredToolbarHeightCompression { + // Some special cases.... + if (!barIsEnabled_) + return 0; + + if ([self isAnimationRunning]) { + // No toolbar compression when animating between hidden and showing, nor + // between showing and detached. + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState] || + [self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) + return 0; + + // If we ever need any other animation cases, code would go here. + } + + return [self isInState:bookmarks::kShowingState] ? kBookmarkBarOverlap : 0; +} + +- (CGFloat)toolbarDividerOpacity { + // Some special cases.... + if ([self isAnimationRunning]) { + // In general, the toolbar shouldn't show a divider while we're animating + // between showing and hidden. The exception is when our height is < 1, in + // which case we can't draw it. It's all-or-nothing (no partial opacity). + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState]) + return (NSHeight([[self view] frame]) < 1) ? 1 : 0; + + // The toolbar should show the divider when animating between showing and + // detached (but opacity will vary). + if ([self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) + return static_cast<CGFloat>([self detachedMorphProgress]); + + // If we ever need any other animation cases, code would go here. + } + + // In general, only show the divider when it's in the normal showing state. + return [self isInState:bookmarks::kShowingState] ? 0 : 1; +} + +- (NSImage*)favIconForNode:(const BookmarkNode*)node { + if (!node) + return defaultImage_; + + if (node->is_folder()) + return folderImage_; + + const SkBitmap& favIcon = bookmarkModel_->GetFavIcon(node); + if (!favIcon.isNull()) + return gfx::SkBitmapToNSImage(favIcon); + + return defaultImage_; +} + +- (void)closeFolderAndStopTrackingMenus { + showFolderMenus_ = NO; + [self closeAllBookmarkFolders]; +} + +- (BOOL)canEditBookmark:(const BookmarkNode*)node { + // Don't allow edit/delete of the bar node, or of "Other Bookmarks" + if ((node == nil) || + (node == bookmarkModel_->other_node()) || + (node == bookmarkModel_->GetBookmarkBarNode())) + return NO; + return YES; +} + +#pragma mark Actions + +- (IBAction)openBookmark:(id)sender { + [self closeFolderAndStopTrackingMenus]; + DCHECK([sender respondsToSelector:@selector(bookmarkNode)]); + const BookmarkNode* node = [sender bookmarkNode]; + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); + [self openURL:node->GetURL() disposition:disposition]; +} + +// Redirect to our logic shared with BookmarkBarFolderController. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + if (sender != offTheSideButton_) { + // Toggle presentation of bar folder menus. + showFolderMenus_ = !showFolderMenus_; + [folderTarget_ openBookmarkFolderFromButton:sender]; + } else { + // Off-the-side requires special handling. + [self openOffTheSideFolderFromButton:sender]; + } +} + +// The button that sends this one is special; the "off the side" +// button (chevron) opens like a folder button but isn't exactly a +// parent folder. +- (IBAction)openOffTheSideFolderFromButton:(id)sender { + DCHECK([sender isKindOfClass:[BookmarkButton class]]); + DCHECK([[sender cell] isKindOfClass:[BookmarkButtonCell class]]); + [[sender cell] setStartingChildIndex:displayedButtonCount_]; + [folderTarget_ openBookmarkFolderFromButton:sender]; +} + +- (IBAction)openBookmarkInNewForegroundTab:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:NEW_FOREGROUND_TAB]; + [self closeAllBookmarkFolders]; +} + +- (IBAction)openBookmarkInNewWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:NEW_WINDOW]; +} + +- (IBAction)openBookmarkInIncognitoWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) + [self openURL:node->GetURL() disposition:OFF_THE_RECORD]; +} + +- (IBAction)editBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (!node) + return; + + if (node->is_folder()) { + BookmarkNameFolderController* controller = + [[BookmarkNameFolderController alloc] + initWithParentWindow:[[self view] window] + profile:browser_->profile() + node:node]; + [controller runAsModalSheet]; + return; + } + + // There is no real need to jump to a platform-common routine at + // this point (which just jumps back to objc) other than consistency + // across platforms. + // + // TODO(jrg): identify when we NO_TREE. I can see it in the code + // for the other platforms but can't find a way to trigger it in the + // UI. + BookmarkEditor::Show([[self view] window], + browser_->profile(), + node->GetParent(), + BookmarkEditor::EditDetails(node), + BookmarkEditor::SHOW_TREE); +} + +- (IBAction)cutBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, true); + } +} + +- (IBAction)copyBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + std::vector<const BookmarkNode*> nodes; + nodes.push_back(node); + bookmark_utils::CopyToClipboard(bookmarkModel_, nodes, false); + } +} + +// Paste the copied node immediately after the node for which the context +// menu has been presented if the node is a non-folder bookmark, otherwise +// past at the end of the folder node. +- (IBAction)pasteBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + int index = -1; + if (node != bookmarkModel_->GetBookmarkBarNode() && !node->is_folder()) { + const BookmarkNode* parent = node->GetParent(); + index = parent->IndexOfChild(node) + 1; + if (index > parent->GetChildCount()) + index = -1; + node = parent; + } + bookmark_utils::PasteFromClipboard(bookmarkModel_, node, index); + } +} + +- (IBAction)deleteBookmark:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + bookmarkModel_->Remove(node->GetParent(), + node->GetParent()->IndexOfChild(node)); + } +} + +- (IBAction)openAllBookmarks:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:NEW_FOREGROUND_TAB]; + UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarks"), + browser_->profile()); + } +} + +- (IBAction)openAllBookmarksNewWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:NEW_WINDOW]; + UserMetrics::RecordAction(UserMetricsAction("OpenAllBookmarksNewWindow"), + browser_->profile()); + } +} + +- (IBAction)openAllBookmarksIncognitoWindow:(id)sender { + const BookmarkNode* node = [self nodeFromMenuItem:sender]; + if (node) { + [self openAll:node disposition:OFF_THE_RECORD]; + UserMetrics::RecordAction( + UserMetricsAction("OpenAllBookmarksIncognitoWindow"), + browser_->profile()); + } +} + +// May be called from the bar or from a folder button. +// If called from a button, that button becomes the parent. +- (IBAction)addPage:(id)sender { + const BookmarkNode* parent = [self nodeFromMenuItem:sender]; + if (!parent) + parent = bookmarkModel_->GetBookmarkBarNode(); + BookmarkEditor::Show([[self view] window], + browser_->profile(), + parent, + BookmarkEditor::EditDetails(), + BookmarkEditor::SHOW_TREE); +} + +// Might be called from the context menu over the bar OR over a +// button. If called from a button, that button becomes a sibling of +// the new node. If called from the bar, add to the end of the bar. +- (IBAction)addFolder:(id)sender { + const BookmarkNode* senderNode = [self nodeFromMenuItem:sender]; + const BookmarkNode* parent = NULL; + int newIndex = 0; + // If triggered from the bar, folder or "others" folder - add as a child to + // the end. + // If triggered from a bookmark, add as next sibling. + BookmarkNode::Type type = senderNode->type(); + if (type == BookmarkNode::BOOKMARK_BAR || + type == BookmarkNode::OTHER_NODE || + type == BookmarkNode::FOLDER) { + parent = senderNode; + newIndex = parent->GetChildCount(); + } else { + parent = senderNode->GetParent(); + newIndex = parent->IndexOfChild(senderNode) + 1; + } + BookmarkNameFolderController* controller = + [[BookmarkNameFolderController alloc] + initWithParentWindow:[[self view] window] + profile:browser_->profile() + parent:parent + newIndex:newIndex]; + [controller runAsModalSheet]; +} + +- (IBAction)importBookmarks:(id)sender { + [ImportSettingsDialogController showImportSettingsDialogForProfile: + browser_->profile()]; +} + +#pragma mark Private Methods + +// Called after the current theme has changed. +- (void)themeDidChangeNotification:(NSNotification*)aNotification { + ThemeProvider* themeProvider = + static_cast<ThemeProvider*>([[aNotification object] pointerValue]); + [self updateTheme:themeProvider]; +} + +// (Private) Method is the same as [self view], but is provided to be explicit. +- (BackgroundGradientView*)backgroundGradientView { + DCHECK([[self view] isKindOfClass:[BackgroundGradientView class]]); + return (BackgroundGradientView*)[self view]; +} + +// (Private) Method is the same as [self view], but is provided to be explicit. +- (AnimatableView*)animatableView { + DCHECK([[self view] isKindOfClass:[AnimatableView class]]); + return (AnimatableView*)[self view]; +} + +// Position the off-the-side chevron to the left of the otherBookmarks button. +- (void)positionOffTheSideButton { + NSRect frame = [offTheSideButton_ frame]; + if (otherBookmarksButton_.get()) { + frame.origin.x = ([otherBookmarksButton_ frame].origin.x - + (frame.size.width + + bookmarks::kBookmarkHorizontalPadding)); + [offTheSideButton_ setFrame:frame]; + } +} + +// Configure the off-the-side button (e.g. specify the node range, +// check if we should enable or disable it, etc). +- (void)configureOffTheSideButtonContentsAndVisibility { + // If deleting a button while off-the-side is open, buttons may be + // promoted from off-the-side to the bar. Accomodate. + if (folderController_ && + ([folderController_ parentButton] == offTheSideButton_)) { + [folderController_ reconfigureMenu]; + } + + [[offTheSideButton_ cell] setStartingChildIndex:displayedButtonCount_]; + [[offTheSideButton_ cell] + setBookmarkNode:bookmarkModel_->GetBookmarkBarNode()]; + int bookmarkChildren = bookmarkModel_->GetBookmarkBarNode()->GetChildCount(); + if (bookmarkChildren > displayedButtonCount_) { + [offTheSideButton_ setHidden:NO]; + } else { + // If we just deleted the last item in an off-the-side menu so the + // button will be going away, make sure the menu goes away. + if (folderController_ && + ([folderController_ parentButton] == offTheSideButton_)) + [self closeAllBookmarkFolders]; + // (And hide the button, too.) + [offTheSideButton_ setHidden:YES]; + } +} + +// Begin (or end) watching for a click outside this window. Unlike +// normal NSWindows, bookmark folder "fake menu" windows do not become +// key or main. Thus, traditional notification (e.g. WillResignKey) +// won't work. Our strategy is to watch (at the app level) for a +// "click outside" these windows to detect when they logically lose +// focus. +- (void)watchForExitEvent:(BOOL)watch { + CrApplication* app = static_cast<CrApplication*>([NSApplication + sharedApplication]); + DCHECK([app isKindOfClass:[CrApplication class]]); + if (watch) { + if (!watchingForExitEvent_) + [app addEventHook:self]; + } else { + if (watchingForExitEvent_) + [app removeEventHook:self]; + } + watchingForExitEvent_ = watch; +} + +// Keep the "no items" label centered in response to a frame size change. +- (void)centerNoItemsLabel { + // Note that this computation is done in the parent's coordinate system, + // which is unflipped. Also, we want the label to be a fixed distance from + // the bottom, so that it slides up properly (on animating to hidden). + // The textfield sits in the itemcontainer, so to center it we maintain + // equal vertical padding on the top and bottom. + int yoffset = (NSHeight([[buttonView_ noItemTextfield] frame]) - + NSHeight([[buttonView_ noItemContainer] frame])) / 2; + [[buttonView_ noItemContainer] setFrameOrigin:NSMakePoint(0, yoffset)]; +} + +// (Private) +- (void)showBookmarkBarWithAnimation:(BOOL)animate { + if (animate && !ignoreAnimations_) { + // If |-doBookmarkBarAnimation| does the animation, we're done. + if ([self doBookmarkBarAnimation]) + return; + + // Else fall through and do the change instantly. + } + + // Set our height. + [resizeDelegate_ resizeView:[self view] + newHeight:[self preferredHeight]]; + + // Only show the divider if showing the normal bookmark bar. + BOOL showsDivider = [self isInState:bookmarks::kShowingState]; + [[self backgroundGradientView] setShowsDivider:showsDivider]; + + // Make sure we're shown. + [[self view] setHidden:![self isVisible]]; + + // Update everything else. + [self layoutSubviews]; + [self frameDidChange]; +} + +// (Private) +- (BOOL)doBookmarkBarAnimation { + if ([self isAnimatingFromState:bookmarks::kHiddenState + toState:bookmarks::kShowingState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + // Height takes into account the extra height we have since the toolbar + // only compresses when we're done. + [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - + kBookmarkBarOverlap) + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kShowingState + toState:bookmarks::kHiddenState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + [view animateToNewHeight:0 + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kShowingState + toState:bookmarks::kDetachedState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + [view animateToNewHeight:bookmarks::kNTPBookmarkBarHeight + duration:kBookmarkBarAnimationDuration]; + } else if ([self isAnimatingFromState:bookmarks::kDetachedState + toState:bookmarks::kShowingState]) { + [[self backgroundGradientView] setShowsDivider:YES]; + [[self view] setHidden:NO]; + AnimatableView* view = [self animatableView]; + // Height takes into account the extra height we have since the toolbar + // only compresses when we're done. + [view animateToNewHeight:(bookmarks::kBookmarkBarHeight - + kBookmarkBarOverlap) + duration:kBookmarkBarAnimationDuration]; + } else { + // Oops! An animation we don't know how to handle. + return NO; + } + + return YES; +} + +// Enable or disable items. We are the menu delegate for both the bar +// and for bookmark folder buttons. +- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)anItem { + // NSUserInterfaceValidations says that the passed-in object has type + // |id<NSValidatedUserInterfaceItem>|, but this function needs to call the + // NSObject method -isKindOfClass: on the parameter. In theory, this is not + // correct, but this is probably a bug in the method signature. + NSMenuItem* item = static_cast<NSMenuItem*>(anItem); + // Yes for everything we don't explicitly deny. + if (![item isKindOfClass:[NSMenuItem class]]) + return YES; + + // Yes if we're not a special BookmarkMenu. + if (![[item menu] isKindOfClass:[BookmarkMenu class]]) + return YES; + + // No if we think it's a special BookmarkMenu but have trouble. + const BookmarkNode* node = [self nodeFromMenuItem:item]; + if (!node) + return NO; + + // If this is the bar menu, we only have things to do if there are + // buttons. If this is a folder button menu, we only have things to + // do if the folder has items. + NSMenu* menu = [item menu]; + BOOL thingsToDo = NO; + if (menu == [[self view] menu]) { + thingsToDo = [buttons_ count] ? YES : NO; + } else { + if (node && node->is_folder() && node->GetChildCount()) { + thingsToDo = YES; + } + } + + // Disable openAll* if we have nothing to do. + SEL action = [item action]; + if ((!thingsToDo) && + ((action == @selector(openAllBookmarks:)) || + (action == @selector(openAllBookmarksNewWindow:)) || + (action == @selector(openAllBookmarksIncognitoWindow:)))) { + return NO; + } + + if ((action == @selector(editBookmark:)) || + (action == @selector(deleteBookmark:)) || + (action == @selector(cutBookmark:)) || + (action == @selector(copyBookmark:))) { + if (![self canEditBookmark:node]) { + return NO; + } + } + + if (action == @selector(pasteBookmark:) && + !bookmark_utils::CanPasteFromClipboard(node)) + return NO; + + // If this is an incognito window, don't allow "open in incognito". + if ((action == @selector(openBookmarkInIncognitoWindow:)) || + (action == @selector(openAllBookmarksIncognitoWindow:))) { + if (browser_->profile()->IsOffTheRecord()) { + return NO; + } + } + + // Enabled by default. + return YES; +} + +// Actually open the URL. This is the last chance for a unit test to +// override. +- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition { + browser_->OpenURL(url, GURL(), disposition, PageTransition::AUTO_BOOKMARK); +} + +- (void)clearMenuTagMap { + seedId_ = 0; + menuTagMap_.clear(); +} + +- (int)preferredHeight { + DCHECK(![self isAnimationRunning]); + + if (!barIsEnabled_) + return 0; + + switch (visualState_) { + case bookmarks::kShowingState: + return bookmarks::kBookmarkBarHeight; + case bookmarks::kDetachedState: + return bookmarks::kNTPBookmarkBarHeight; + case bookmarks::kHiddenState: + return 0; + case bookmarks::kInvalidState: + default: + NOTREACHED(); + return 0; + } +} + +// Recursively add the given bookmark node and all its children to +// menu, one menu item per node. +- (void)addNode:(const BookmarkNode*)child toMenu:(NSMenu*)menu { + NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child]; + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title + action:nil + keyEquivalent:@""] autorelease]; + [menu addItem:item]; + [item setImage:[self favIconForNode:child]]; + if (child->is_folder()) { + NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; + [menu setSubmenu:submenu forItem:item]; + if (child->GetChildCount()) { + [self addFolderNode:child toMenu:submenu]; // potentially recursive + } else { + [self tagEmptyMenu:submenu]; + } + } else { + [item setTarget:self]; + [item setAction:@selector(openBookmarkMenuItem:)]; + [item setTag:[self menuTagFromNodeId:child->id()]]; + // Add a tooltip + std::string url_string = child->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", + base::SysUTF16ToNSString(child->GetTitle()), + url_string.c_str()]; + [item setToolTip:tooltip]; + } +} + +// Empty menus are odd; if empty, add something to look at. +// Matches windows behavior. +- (void)tagEmptyMenu:(NSMenu*)menu { + NSString* empty_menu_title = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU); + [menu addItem:[[[NSMenuItem alloc] initWithTitle:empty_menu_title + action:NULL + keyEquivalent:@""] autorelease]]; +} + +// Add the children of the given bookmark node (and their children...) +// to menu, one menu item per node. +- (void)addFolderNode:(const BookmarkNode*)node toMenu:(NSMenu*)menu { + for (int i = 0; i < node->GetChildCount(); i++) { + const BookmarkNode* child = node->GetChild(i); + [self addNode:child toMenu:menu]; + } +} + +// Return an autoreleased NSMenu that represents the given bookmark +// folder node. +- (NSMenu *)menuForFolderNode:(const BookmarkNode*)node { + if (!node->is_folder()) + return nil; + NSString* title = base::SysUTF16ToNSString(node->GetTitle()); + NSMenu* menu = [[[NSMenu alloc] initWithTitle:title] autorelease]; + [self addFolderNode:node toMenu:menu]; + + if (![menu numberOfItems]) { + [self tagEmptyMenu:menu]; + } + return menu; +} + +// 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 +// too small. For "FBL" it is 2 pixels too small. +// For a bookmark named "SFGateFooWoo", it is just fine. +- (CGFloat)widthForBookmarkButtonCell:(NSCell*)cell { + CGFloat desired = [cell cellSize].width + 2; + return std::min(desired, bookmarks::kDefaultBookmarkWidth); +} + +- (IBAction)openBookmarkMenuItem:(id)sender { + int64 tag = [self nodeIdFromMenuTag:[sender tag]]; + const BookmarkNode* node = bookmarkModel_->GetNodeByID(tag); + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); + [self openURL:node->GetURL() disposition:disposition]; +} + +// For the given root node of the bookmark bar, show or hide (as +// appropriate) the "no items" container (text which says "bookmarks +// go here"). +- (void)showOrHideNoItemContainerForNode:(const BookmarkNode*)node { + BOOL hideNoItemWarning = node->GetChildCount() > 0; + [[buttonView_ noItemContainer] setHidden:hideNoItemWarning]; +} + +// TODO(jrg): write a "build bar" so there is a nice spot for things +// like the contextual menu which is invoked when not over a +// bookmark. On Safari that menu has a "new folder" option. +- (void)addNodesToButtonList:(const BookmarkNode*)node { + [self showOrHideNoItemContainerForNode:node]; + + CGFloat maxViewX = NSMaxX([[self view] bounds]); + 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]; + } +} + +- (BookmarkButton*)buttonForNode:(const BookmarkNode*)node + xOffset:(int*)xOffset { + BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; + NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:xOffset]; + + scoped_nsobject<BookmarkButton> + button([[BookmarkButton alloc] initWithFrame:frame]); + DCHECK(button.get()); + + // [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]; + + // We cannot set the button cell's text color until it is placed in + // the button (e.g. the [button setCell:cell] call right above). We + // also cannot set the cell's text color until the view is added to + // the hierarchy. If that second part is now true, set the color. + // (If not we'll set the color on the 1st themeChanged: + // notification.) + ThemeProvider* themeProvider = [[[self view] window] themeProvider]; + if (themeProvider) { + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + [cell setTextColor:color]; + } + + 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::SysUTF16ToNSString(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 +// and the "other bookmarks" button. Technically "other bookmarks" is +// a bookmark button but it is treated specially. Only needs to be +// called when these buttons are new or when the bookmark bar is +// cleared (e.g. on a loaded: call). Unlike addButtonsToView below, +// we don't need to add/remove these dynamically in response to window +// resize. +- (void)addNonBookmarkButtonsToView { + [buttonView_ addSubview:otherBookmarksButton_.get()]; + [buttonView_ addSubview:offTheSideButton_]; +} + +// 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 the first time. +- (void)addButtonsToView { + 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]; + } +} + +// Create the button for "Other Bookmarks" on the right of the bar. +- (void)createOtherBookmarksButton { + // Can't create this until the model is loaded, but only need to + // create it once. + if (otherBookmarksButton_.get()) + return; + + // TODO(jrg): remove duplicate code + NSCell* cell = [self cellForBookmarkNode:bookmarkModel_->other_node()]; + int ignored = 0; + NSRect frame = [self frameForBookmarkButtonFromCell:cell xOffset:&ignored]; + frame.origin.x = [[self buttonView] bounds].size.width - frame.size.width; + frame.origin.x -= bookmarks::kBookmarkHorizontalPadding; + BookmarkButton* button = [[BookmarkButton alloc] initWithFrame:frame]; + [button setDraggable:NO]; + otherBookmarksButton_.reset(button); + view_id_util::SetID(button, VIEW_ID_OTHER_BOOKMARKS); + + // Make sure this button, like all other BookmarkButtons, lives + // until the end of the current event loop. + [[button retain] autorelease]; + + // Peg at right; keep same height as bar. + [button setAutoresizingMask:(NSViewMinXMargin)]; + [button setCell:cell]; + [button setDelegate:self]; + [button setTarget:self]; + [button setAction:@selector(openBookmarkFolderFromButton:)]; + [buttonView_ addSubview:button]; + + // Now that it's here, move the chevron over. + [self positionOffTheSideButton]; +} + +// Now that the model is loaded, set the bookmark bar root as the node +// represented by the bookmark bar (default, background) menu. +- (void)setNodeForBarMenu { + const BookmarkNode* node = bookmarkModel_->GetBookmarkBarNode(); + BookmarkMenu* menu = static_cast<BookmarkMenu*>([[self view] menu]); + + // Make sure types are compatible + DCHECK(sizeof(long long) == sizeof(int64)); + [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; +} + +// To avoid problems with sync, changes that may impact the current +// bookmark (e.g. deletion) make sure context menus are closed. This +// prevents deleting a node which no longer exists. +- (void)cancelMenuTracking { + [buttonContextMenu_ cancelTracking]; + [buttonFolderContextMenu_ cancelTracking]; +} + +// Determines the appropriate state for the given situation. ++ (bookmarks::VisualState)visualStateToShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar { + if (showNormalBar) + return bookmarks::kShowingState; + if (showDetachedBar) + return bookmarks::kDetachedState; + return bookmarks::kHiddenState; +} + +- (void)moveToVisualState:(bookmarks::VisualState)nextVisualState + withAnimation:(BOOL)animate { + BOOL isAnimationRunning = [self isAnimationRunning]; + + // No-op if the next state is the same as the "current" one, subject to the + // following conditions: + // - no animation is running; or + // - an animation is running and |animate| is YES ([*] if it's NO, we'd want + // to cancel the animation and jump to the final state). + if ((nextVisualState == visualState_) && (!isAnimationRunning || animate)) + return; + + // If an animation is running, we want to finalize it. Otherwise we'd have to + // be able to animate starting from the middle of one type of animation. We + // assume that animations that we know about can be "reversed". + if (isAnimationRunning) { + // Don't cancel if we're going to reverse the animation. + if (nextVisualState != lastVisualState_) { + [self stopCurrentAnimation]; + [self finalizeVisualState]; + } + + // If we're in case [*] above, we can stop here. + if (nextVisualState == visualState_) + return; + } + + // Now update with the new state change. + lastVisualState_ = visualState_; + visualState_ = nextVisualState; + + // Animate only if told to and if bar is enabled. + if (animate && !ignoreAnimations_ && barIsEnabled_) { + [self closeAllBookmarkFolders]; + // Take care of any animation cases we know how to handle. + + // We know how to handle hidden <-> normal, normal <-> detached.... + if ([self isAnimatingBetweenState:bookmarks::kHiddenState + andState:bookmarks::kShowingState] || + [self isAnimatingBetweenState:bookmarks::kShowingState + andState:bookmarks::kDetachedState]) { + [delegate_ bookmarkBar:self willAnimateFromState:lastVisualState_ + toState:visualState_]; + [self showBookmarkBarWithAnimation:YES]; + return; + } + + // If we ever need any other animation cases, code would go here. + // Let any animation cases which we don't know how to handle fall through to + // the unanimated case. + } + + // Just jump to the state. + [self finalizeVisualState]; +} + +// N.B.: |-moveToVisualState:...| will check if this should be a no-op or not. +- (void)updateAndShowNormalBar:(BOOL)showNormalBar + showDetachedBar:(BOOL)showDetachedBar + withAnimation:(BOOL)animate { + bookmarks::VisualState newVisualState = + [BookmarkBarController visualStateToShowNormalBar:showNormalBar + showDetachedBar:showDetachedBar]; + [self moveToVisualState:newVisualState + withAnimation:animate && !ignoreAnimations_]; +} + +// (Private) +- (void)finalizeVisualState { + // We promise that our delegate that the variables will be finalized before + // the call to |-bookmarkBar:didChangeFromState:toState:|. + bookmarks::VisualState oldVisualState = lastVisualState_; + lastVisualState_ = bookmarks::kInvalidState; + + // Notify our delegate. + [delegate_ bookmarkBar:self didChangeFromState:oldVisualState + toState:visualState_]; + + // Update ourselves visually. + [self updateVisibility]; +} + +// (Private) +- (void)stopCurrentAnimation { + [[self animatableView] stopAnimation]; +} + +// Delegate method for |AnimatableView| (a superclass of +// |BookmarkBarToolbarView|). +- (void)animationDidEnd:(NSAnimation*)animation { + [self finalizeVisualState]; +} + +- (void)reconfigureBookmarkBar { + [self redistributeButtonsOnBarAsNeeded]; + [self positionOffTheSideButton]; + [self configureOffTheSideButtonContentsAndVisibility]; + [self centerNoItemsLabel]; +} + +// Determine if the given |view| can completely fit within the constraint of +// maximum x, given by |maxViewX|, and, if not, narrow the view up to a minimum +// width. If the minimum width is not achievable then hide the view. Return YES +// if the view was hidden. +- (BOOL)shrinkOrHideView:(NSView*)view forMaxX:(CGFloat)maxViewX { + BOOL wasHidden = NO; + // See if the view needs to be narrowed. + NSRect frame = [view frame]; + if (NSMaxX(frame) > maxViewX) { + // Resize if more than 30 pixels are showing, otherwise hide. + if (NSMinX(frame) + 30.0 < maxViewX) { + frame.size.width = maxViewX - NSMinX(frame); + [view setFrame:frame]; + } else { + [view setHidden:YES]; + wasHidden = YES; + } + } + return wasHidden; +} + +// Adjust the horizontal width and the visibility of the "For quick access" +// text field and "Import bookmarks..." button based on the current width +// of the containing |buttonView_| (which is affected by window width). +- (void)adjustNoItemContainerWidthsForMaxX:(CGFloat)maxViewX { + if (![[buttonView_ noItemContainer] isHidden]) { + // Reset initial frames for the two items, then adjust as necessary. + NSTextField* noItemTextfield = [buttonView_ noItemTextfield]; + [noItemTextfield setFrame:originalNoItemsRect_]; + [noItemTextfield setHidden:NO]; + NSButton* importBookmarksButton = [buttonView_ importBookmarksButton]; + [importBookmarksButton setFrame:originalImportBookmarksRect_]; + [importBookmarksButton setHidden:NO]; + // Check each to see if they need to be shrunk or hidden. + if ([self shrinkOrHideView:importBookmarksButton forMaxX:maxViewX]) + [self shrinkOrHideView:noItemTextfield forMaxX:maxViewX]; + } +} + +- (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 setDelegate:nil]; + [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]; + } + + // While we're here, adjust the horizontal width and the visibility + // of the "For quick access" and "Import bookmarks..." text fields. + if (![buttons_ count]) + [self adjustNoItemContainerWidthsForMaxX:maxViewX]; +} + +#pragma mark Private Methods Exposed for Testing + +- (BookmarkBarView*)buttonView { + return buttonView_; +} + +- (NSMutableArray*)buttons { + return buttons_.get(); +} + +- (NSButton*)offTheSideButton { + return offTheSideButton_; +} + +- (BOOL)offTheSideButtonIsHidden { + return [offTheSideButton_ isHidden]; +} + +- (BookmarkButton*)otherBookmarksButton { + return otherBookmarksButton_.get(); +} + +- (BookmarkBarFolderController*)folderController { + return folderController_; +} + +- (id)folderTarget { + return folderTarget_.get(); +} + +- (int)displayedButtonCount { + return displayedButtonCount_; +} + +// Delete all buttons (bookmarks, chevron, "other bookmarks") from the +// bookmark bar; reset knowledge of bookmarks. +- (void)clearBookmarkBar { + for (BookmarkButton* button in buttons_.get()) { + [button setDelegate:nil]; + [button removeFromSuperview]; + } + [buttons_ removeAllObjects]; + [self clearMenuTagMap]; + displayedButtonCount_ = 0; + + // Make sure there are no stale pointers in the pasteboard. This + // can be important if a bookmark is deleted (via bookmark sync) + // while in the middle of a drag. The "drag completed" code + // (e.g. [BookmarkBarView performDragOperationForBookmarkButton:]) is + // careful enough to bail if there is no data found at "drop" time. + // + // Unfortunately the clearContents selector is 10.6 only. The best + // we can do is make sure something else is present in place of the + // stale bookmark. + NSPasteboard* pboard = [NSPasteboard pasteboardWithName:NSDragPboard]; + [pboard declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:self]; + [pboard setString:@"" forType:NSStringPboardType]; +} + +// Return an autoreleased NSCell suitable for a bookmark button. +// TODO(jrg): move much of the cell config into the BookmarkButtonCell class. +- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)node { + NSImage* image = node ? [self favIconForNode:node] : nil; + NSMenu* menu = node && node->is_folder() ? buttonFolderContextMenu_ : + buttonContextMenu_; + BookmarkButtonCell* cell = [BookmarkButtonCell buttonCellForNode:node + contextMenu:menu + cellText:nil + cellImage:image]; + [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; + + // Note: a quirk of setting a cell's text color is that it won't work + // until the cell is associated with a button, so we can't theme the cell yet. + + return cell; +} + +// Returns a frame appropriate for the given bookmark cell, suitable +// for creating an NSButton that will contain it. |xOffset| is the X +// offset for the frame; it is increased to be an appropriate X offset +// for the next button. +- (NSRect)frameForBookmarkButtonFromCell:(NSCell*)cell + xOffset:(int*)xOffset { + DCHECK(xOffset); + NSRect bounds = [buttonView_ bounds]; + bounds.size.height = bookmarks::kBookmarkButtonHeight; + + NSRect frame = NSInsetRect(bounds, + bookmarks::kBookmarkHorizontalPadding, + bookmarks::kBookmarkVerticalPadding); + frame.size.width = [self widthForBookmarkButtonCell:cell]; + + // Add an X offset based on what we've already done + frame.origin.x += *xOffset; + + // And up the X offset for next time. + *xOffset = NSMaxX(frame); + + return frame; +} + +// A bookmark button's contents changed. Check for growth +// (e.g. increase the width up to the maximum). If we grew, move +// other bookmark buttons over. +- (void)checkForBookmarkButtonGrowth:(NSButton*)button { + NSRect frame = [button frame]; + CGFloat desiredSize = [self widthForBookmarkButtonCell:[button cell]]; + CGFloat delta = desiredSize - frame.size.width; + if (delta) { + frame.size.width = desiredSize; + [button setFrame:frame]; + for (NSButton* button in buttons_.get()) { + NSRect buttonFrame = [button frame]; + if (buttonFrame.origin.x > frame.origin.x) { + buttonFrame.origin.x += delta; + [button setFrame:buttonFrame]; + } + } + } + // We may have just crossed a threshold to enable the off-the-side + // button. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +// Called when our controlled frame has changed size. +- (void)frameDidChange { + if (!bookmarkModel_->IsLoaded()) + return; + [self updateTheme:[[[self view] window] themeProvider]]; + [self reconfigureBookmarkBar]; +} + +// Given a NSMenuItem tag, return the appropriate bookmark node id. +- (int64)nodeIdFromMenuTag:(int32)tag { + return menuTagMap_[tag]; +} + +// Create and return a new tag for the given node id. +- (int32)menuTagFromNodeId:(int64)menuid { + int tag = seedId_++; + menuTagMap_[tag] = menuid; + return tag; +} + +// Return the BookmarkNode associated with the given NSMenuItem. Can +// return NULL which means "do nothing". One case where it would +// return NULL is if the bookmark model gets modified while you have a +// context menu open. +- (const BookmarkNode*)nodeFromMenuItem:(id)sender { + const BookmarkNode* node = NULL; + BookmarkMenu* menu = (BookmarkMenu*)[sender menu]; + if ([menu isKindOfClass:[BookmarkMenu class]]) { + int64 id = [menu id]; + node = bookmarkModel_->GetNodeByID(id); + } + return node; +} + +// Adapt appearance of buttons to the current theme. Called after +// theme changes, or when our view is added to the view hierarchy. +// Oddly, the view pings us instead of us pinging our view. This is +// because our trigger is an [NSView viewWillMoveToWindow:], which the +// controller doesn't normally know about. Otherwise we don't have +// access to the theme before we know what window we will be on. +- (void)updateTheme:(ThemeProvider*)themeProvider { + if (!themeProvider) + return; + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + for (BookmarkButton* button in buttons_.get()) { + BookmarkButtonCell* cell = [button cell]; + [cell setTextColor:color]; + } + [[otherBookmarksButton_ cell] setTextColor:color]; +} + +// Return YES if the event indicates an exit from the bookmark bar +// folder menus. E.g. "click outside" of the area we are watching. +// At this time we are watching the area that includes all popup +// bookmark folder windows. +- (BOOL)isEventAnExitEvent:(NSEvent*)event { + NSWindow* eventWindow = [event window]; + NSWindow* myWindow = [[self view] window]; + switch ([event type]) { + case NSLeftMouseDown: + case NSRightMouseDown: + // If the click is in my window but NOT in the bookmark bar, consider + // it a click 'outside'. Clicks directly on an active button (i.e. one + // that is a folder and for which its folder menu is showing) are 'in'. + // All other clicks on the bookmarks bar are counted as 'outside' + // because they should close any open bookmark folder menu. + if (eventWindow == myWindow) { + NSView* hitView = + [[eventWindow contentView] hitTest:[event locationInWindow]]; + if (hitView == [folderController_ parentButton]) + return NO; + if (![hitView isDescendantOf:[self view]] || hitView == buttonView_) + return YES; + } + // If a click in a bookmark bar folder window and that isn't + // one of my bookmark bar folders, YES is click outside. + if (![eventWindow isKindOfClass:[BookmarkBarFolderWindow + class]]) { + return YES; + } + break; + case NSKeyDown: + case NSKeyUp: + // Any key press ends things. + return YES; + case NSLeftMouseDragged: + // We can get here with the following sequence: + // - open a bookmark folder + // - right-click (and unclick) on it to open context menu + // - move mouse to window titlebar then click-drag it by the titlebar + // http://crbug.com/49333 + return YES; + default: + break; + } + return NO; +} + +#pragma mark Drag & Drop + +// Find something like std::is_between<T>? I can't believe one doesn't exist. +static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { + return ((value >= low) && (value <= high)); +} + +// Return the proposed drop target for a hover open button from the +// given array, or nil if none. We use this for distinguishing +// between a hover-open candidate or drop-indicator draw. +// Helper for buttonForDroppingOnAtPoint:. +// Get UI review on "middle half" ness. +// http://crbug.com/36276 +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point + fromArray:(NSArray*)array { + for (BookmarkButton* button in array) { + // Break early if we've gone too far. + if ((NSMinX([button frame]) > point.x) || (![button superview])) + return nil; + // Careful -- this only applies to the bar with horiz buttons. + // Intentionally NOT using NSPointInRect() so that scrolling into + // a submenu doesn't cause it to be closed. + if (ValueInRangeInclusive(NSMinX([button frame]), + point.x, + NSMaxX([button frame]))) { + // Over a button but let's be a little more specific (make sure + // it's over the middle half, not just over it). + NSRect frame = [button frame]; + NSRect middleHalfOfButton = NSInsetRect(frame, frame.size.width / 4, 0); + if (ValueInRangeInclusive(NSMinX(middleHalfOfButton), + point.x, + NSMaxX(middleHalfOfButton))) { + // It makes no sense to drop on a non-folder; there is no hover. + if (![button isFolder]) + return nil; + // Got it! + return button; + } else { + // Over a button but not over the middle half. + return nil; + } + } + } + // Not hovering over a button. + return nil; +} + +// Return the proposed drop target for a hover open button, or nil if +// none. Works with both the bookmark buttons and the "Other +// Bookmarks" button. Point is in [self view] coordinates. +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { + point = [[self view] convertPoint:point + fromView:[[[self view] window] contentView]]; + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point + fromArray:buttons_.get()]; + // One more chance -- try "Other Bookmarks" and "off the side" (if visible). + // This is different than BookmarkBarFolderController. + if (!button) { + NSMutableArray* array = [NSMutableArray array]; + if (![self offTheSideButtonIsHidden]) + [array addObject:offTheSideButton_]; + [array addObject:otherBookmarksButton_]; + button = [self buttonForDroppingOnAtPoint:point + fromArray:array]; + } + return button; +} + +- (int)indexForDragToPoint:(NSPoint)point { + // TODO(jrg): revisit position info based on UI team feedback. + // dropLocation is in bar local coordinates. + NSPoint dropLocation = + [[self view] convertPoint:point + fromView:[[[self view] window] contentView]]; + BookmarkButton* buttonToTheRightOfDraggedButton = nil; + for (BookmarkButton* button in buttons_.get()) { + CGFloat midpoint = NSMidX([button frame]); + if (dropLocation.x <= midpoint) { + buttonToTheRightOfDraggedButton = button; + break; + } + } + if (buttonToTheRightOfDraggedButton) { + const BookmarkNode* afterNode = + [buttonToTheRightOfDraggedButton bookmarkNode]; + 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, displayedButtonCount_); + } + + // If nothing is to my right I am at the end! + return displayedButtonCount_; +} + +// TODO(mrossetti,jrg): Yet more duplicated code. +// http://crbug.com/35966 +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK(sourceNode); + // Drop destination. + const BookmarkNode* destParent = NULL; + int destIndex = 0; + + // First check if we're dropping on a button. If we have one, and + // it's a folder, drop in it. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere on the bar, so find the right spot. + destParent = bookmarkModel_->GetBookmarkBarNode(); + destIndex = [self indexForDragToPoint:point]; + } + + // Be sure we don't try and drop a folder into itself. + if (sourceNode != destParent) { + if (copy) + bookmarkModel_->Copy(sourceNode, destParent, destIndex); + else + bookmarkModel_->Move(sourceNode, destParent, destIndex); + } + + [self closeFolderAndStopTrackingMenus]; + + // Movement of a node triggers observers (like us) to rebuild the + // bar so we don't have to do so explicitly. + + return YES; +} + +- (void)draggingEnded:(id<NSDraggingInfo>)info { + [self closeFolderAndStopTrackingMenus]; +} + +#pragma mark Bridge Notification Handlers + +// TODO(jrg): for now this is brute force. +- (void)loaded:(BookmarkModel*)model { + DCHECK(model == bookmarkModel_); + if (!model->IsLoaded()) + return; + + // If this is a rebuild request while we have a folder open, close it. + // TODO(mrossetti): Eliminate the need for this because it causes the folder + // menu to disappear after a cut/copy/paste/delete change. + // See: http://crbug.com/36614 + if (folderController_) + [self closeAllBookmarkFolders]; + + // Brute force nuke and build. + savedFrameWidth_ = NSWidth([[self view] frame]); + const BookmarkNode* node = model->GetBookmarkBarNode(); + [self clearBookmarkBar]; + [self addNodesToButtonList:node]; + [self createOtherBookmarksButton]; + [self updateTheme:[[[self view] window] themeProvider]]; + [self positionOffTheSideButton]; + [self addNonBookmarkButtonsToView]; + [self addButtonsToView]; + [self configureOffTheSideButtonContentsAndVisibility]; + [self setNodeForBarMenu]; +} + +- (void)beingDeleted:(BookmarkModel*)model { + // The browser may be being torn down; little is safe to do. As an + // example, it may not be safe to clear the pasteboard. + // http://crbug.com/38665 +} + +- (void)nodeAdded:(BookmarkModel*)model + parent:(const BookmarkNode*)newParent index:(int)newIndex { + // If a context menu is open, close it. + [self cancelMenuTracking]; + + const BookmarkNode* newNode = newParent->GetChild(newIndex); + id<BookmarkButtonControllerProtocol> newController = + [self controllerForNode:newParent]; + [newController addButtonForNode:newNode atIndex:newIndex]; + // If we go from 0 --> 1 bookmarks we may need to hide the + // "bookmarks go here" text container. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; +} + +// TODO(jrg): for now this is brute force. +- (void)nodeChanged:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [self loaded:model]; +} + +- (void)nodeMoved:(BookmarkModel*)model + oldParent:(const BookmarkNode*)oldParent oldIndex:(int)oldIndex + newParent:(const BookmarkNode*)newParent newIndex:(int)newIndex { + 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]; + } + // If the bar is one of the parents we may need to update the visibility + // of the "bookmarks go here" presentation. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; + // If we moved the only item on the "off the side" menu somewhere + // else, we may no longer need to show it. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +- (void)nodeRemoved:(BookmarkModel*)model + parent:(const BookmarkNode*)oldParent index:(int)index { + // If a context menu is open, close it. + [self cancelMenuTracking]; + + // 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]; + // If we go from 1 --> 0 bookmarks we may need to show the + // "bookmarks go here" text container. + [self showOrHideNoItemContainerForNode:model->GetBookmarkBarNode()]; + // If we deleted the only item on the "off the side" menu we no + // longer need to show it. + [self configureOffTheSideButtonContentsAndVisibility]; +} + +// TODO(jrg): linear searching is bad. +// Need a BookmarkNode-->NSCell mapping. +// +// TODO(jrg): if the bookmark bar is open on launch, we see the +// buttons all placed, then "scooted over" as the favicons load. If +// this looks bad I may need to change widthForBookmarkButtonCell to +// add space for an image even if not there on the assumption that +// favicons will eventually load. +- (void)nodeFavIconLoaded:(BookmarkModel*)model + node:(const BookmarkNode*)node { + for (BookmarkButton* button in buttons_.get()) { + const BookmarkNode* cellnode = [button bookmarkNode]; + if (cellnode == node) { + [[button cell] setBookmarkCellText:[button title] + image:[self favIconForNode:node]]; + // Adding an image means we might need more room for the + // bookmark. Test for it by growing the button (if needed) + // and shifting everything else over. + [self checkForBookmarkButtonGrowth:button]; + } + } +} + +// TODO(jrg): for now this is brute force. +- (void)nodeChildrenReordered:(BookmarkModel*)model + node:(const BookmarkNode*)node { + [self loaded:model]; +} + +#pragma mark BookmarkBarState Protocol + +// (BookmarkBarState protocol) +- (BOOL)isVisible { + return barIsEnabled_ && (visualState_ == bookmarks::kShowingState || + visualState_ == bookmarks::kDetachedState || + lastVisualState_ == bookmarks::kShowingState || + lastVisualState_ == bookmarks::kDetachedState); +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimationRunning { + return lastVisualState_ != bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isInState:(bookmarks::VisualState)state { + return visualState_ == state && + lastVisualState_ == bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingToState:(bookmarks::VisualState)state { + return visualState_ == state && + lastVisualState_ != bookmarks::kInvalidState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state { + return lastVisualState_ == state; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState + toState:(bookmarks::VisualState)toState { + return lastVisualState_ == fromState && visualState_ == toState; +} + +// (BookmarkBarState protocol) +- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState + andState:(bookmarks::VisualState)toState { + return (lastVisualState_ == fromState && visualState_ == toState) || + (visualState_ == fromState && lastVisualState_ == toState); +} + +// (BookmarkBarState protocol) +- (CGFloat)detachedMorphProgress { + if ([self isInState:bookmarks::kDetachedState]) { + return 1; + } + if ([self isAnimatingToState:bookmarks::kDetachedState]) { + return static_cast<CGFloat>( + [[self animatableView] currentAnimationProgress]); + } + if ([self isAnimatingFromState:bookmarks::kDetachedState]) { + return static_cast<CGFloat>( + 1 - [[self animatableView] currentAnimationProgress]); + } + return 0; +} + +#pragma mark BookmarkBarToolbarViewController Protocol + +- (int)currentTabContentsHeight { + TabContents* tc = browser_->GetSelectedTabContents(); + return tc ? tc->view()->GetContainerSize().height() : 0; +} + +- (ThemeProvider*)themeProvider { + return browser_->profile()->GetThemeProvider(); +} + +#pragma mark BookmarkButtonDelegate Protocol + +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { + [[self folderTarget] fillPasteboard:pboard forDragOfButton:button]; +} + +// BookmarkButtonDelegate protocol implementation. When menus are +// "active" (e.g. you clicked to open one), moving the mouse over +// another folder button should close the 1st and open the 2nd (like +// real menus). We detect and act here. +- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { + DCHECK([sender isKindOfClass:[BookmarkButton class]]); + + // If folder menus are not being shown, do nothing. This is different from + // BookmarkBarFolderController's implementation because the bar should NOT + // automatically open folder menus when the mouse passes over a folder + // button while the BookmarkBarFolderController DOES automically open + // a subfolder menu. + if (!showFolderMenus_) + return; + + // From here down: same logic as BookmarkBarFolderController. + // TODO(jrg): find a way to share these 4 non-comment lines? + // http://crbug.com/35966 + // If already opened, then we exited but re-entered the button, so do nothing. + if ([folderController_ parentButton] == sender) + return; + // Else open a new one if it makes sense to do so. + if ([sender bookmarkNode]->is_folder()) { + [folderTarget_ openBookmarkFolderFromButton:sender]; + } else { + // We're over a non-folder bookmark so close any old folders. + [folderController_ close]; + folderController_ = nil; + } +} + +// BookmarkButtonDelegate protocol implementation. +- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { + // Don't care; do nothing. + // This is different behavior that the folder menus. +} + +- (NSWindow*)browserWindow { + return [[self view] window]; +} + +- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { + return [self canEditBookmark:[button bookmarkNode]]; +} + +- (void)didDragBookmarkToTrash:(BookmarkButton*)button { + // TODO(mrossetti): Refactor BookmarkBarFolder common code. + // http://crbug.com/35966 + const BookmarkNode* node = [button bookmarkNode]; + if (node) { + const BookmarkNode* parent = node->GetParent(); + bookmarkModel_->Remove(parent, + parent->IndexOfChild(node)); + } +} + +#pragma mark BookmarkButtonControllerProtocol + +// Close all bookmark folders. "Folder" here is the fake menu for +// bookmark folders, not a button context menu. +- (void)closeAllBookmarkFolders { + [self watchForExitEvent:NO]; + [folderController_ close]; + folderController_ = nil; +} + +- (void)closeBookmarkFolder:(id)sender { + // We're the top level, so close one means close them all. + [self closeAllBookmarkFolders]; +} + +- (BookmarkModel*)bookmarkModel { + return bookmarkModel_; +} + +// TODO(jrg): much of this logic is duped with +// [BookmarkBarFolderController draggingEntered:] except when noted. +// http://crbug.com/35966 +- (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 + } + if (hoverButton_) { + // Oops, another one triggered or open. + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ + target]]; + // Unlike BookmarkBarFolderController, we do not delay the close + // of the previous one. Given the lack of diagonal movement, + // there is no need, and it feels awkward to do so. See + // comments about kDragHoverCloseDelay in + // bookmark_bar_folder_controller.mm for more details. + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + hoverButton_.reset([button retain]); + DCHECK([[hoverButton_ target] + respondsToSelector:@selector(openBookmarkFolderFromButton:)]); + [[hoverButton_ target] + performSelector:@selector(openBookmarkFolderFromButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverOpenDelay]; + } + if (!button) { + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + [[hoverButton_ target] closeBookmarkFolder:hoverButton_]; + hoverButton_.reset(); + } + } + + // Thrown away but kept to be consistent with the draggingEntered: interface. + return NSDragOperationMove; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + // NOT the same as a cancel --> we may have moved the mouse into the submenu. + if (hoverButton_) { + [NSObject cancelPreviousPerformRequestsWithTarget:[hoverButton_ target]]; + hoverButton_.reset(); + } +} + +- (BOOL)dragShouldLockBarVisibility { + return ![self isInState:bookmarks::kDetachedState] && + ![self isAnimatingToState:bookmarks::kDetachedState]; +} + +// TODO(mrossetti,jrg): Yet more code dup with BookmarkBarFolderController. +// http://crbug.com/35966 +- (BOOL)dragButton:(BookmarkButton*)sourceButton + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + return [self dragBookmark:sourceNode to:point copy:copy]; +} + +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { + BOOL dragged = NO; + std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]); + if (nodes.size()) { + BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); + NSPoint dropPoint = [info draggingLocation]; + for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); + it != nodes.end(); ++it) { + const BookmarkNode* sourceNode = *it; + dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; + } + } + return dragged; +} + +- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { + std::vector<const BookmarkNode*> dragDataNodes; + BookmarkNodeData dragData; + if(dragData.ReadFromDragClipboard()) { + BookmarkModel* bookmarkModel = [self bookmarkModel]; + Profile* profile = bookmarkModel->profile(); + std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile)); + dragDataNodes.assign(nodes.begin(), nodes.end()); + } + return dragDataNodes; +} + +// Return YES if we should show the drop indicator, else NO. +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return ![self buttonForDroppingOnAtPoint:point]; +} + +// Return the x position for a drop indicator. +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { + CGFloat x = 0; + int destIndex = [self indexForDragToPoint:point]; + int numButtons = displayedButtonCount_; + + // If it's a drop strictly between existing buttons ... + if (destIndex >= 0 && destIndex < numButtons) { + // ... put the indicator right between the buttons. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + x = buttonFrame.origin.x - 0.5 * bookmarks::kBookmarkHorizontalPadding; + + // If it's a drop at the end (past the last button, if there are any) ... + } else if (destIndex == numButtons) { + // and if it's past the last button ... + if (numButtons > 0) { + // ... find the last button, and put the indicator to its right. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + x = NSMaxX(buttonFrame) + 0.5 * bookmarks::kBookmarkHorizontalPadding; + + // Otherwise, put it right at the beginning. + } else { + x = 0.5 * bookmarks::kBookmarkHorizontalPadding; + } + } else { + NOTREACHED(); + } + + return x; +} + +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { + // If the bookmarkbar is not in detached mode, lock bar visibility, forcing + // the overlay to stay open when in fullscreen mode. + if (![self isInState:bookmarks::kDetachedState] && + ![self isAnimatingToState:bookmarks::kDetachedState]) { + BrowserWindowController* browserController = + [BrowserWindowController browserWindowControllerForView:[self view]]; + [browserController lockBarVisibilityForOwner:child + withAnimation:NO + delay:NO]; + } +} + +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { + // Release bar visibility, allowing the overlay to close if in fullscreen + // mode. + BrowserWindowController* browserController = + [BrowserWindowController browserWindowControllerForView:[self view]]; + [browserController releaseBarVisibilityForOwner:child + withAnimation:NO + delay:NO]; +} + +// Add a new folder controller as triggered by the given folder button. +- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { + + // If doing a close/open, make sure the fullscreen chrome doesn't + // have a chance to begin animating away in the middle of things. + BrowserWindowController* browserController = + [BrowserWindowController browserWindowControllerForView:[self view]]; + // Confirm we're not re-locking with ourself as an owner before locking. + DCHECK([browserController isBarVisibilityLockedForOwner:self] == NO); + [browserController lockBarVisibilityForOwner:self + withAnimation:NO + delay:NO]; + + if (folderController_) + [self closeAllBookmarkFolders]; + + // Folder controller, like many window controllers, owns itself. + folderController_ = + [[BookmarkBarFolderController alloc] initWithParentButton:parentButton + parentController:nil + barController:self]; + [folderController_ showWindow:self]; + + // Only BookmarkBarController has this; the + // BookmarkBarFolderController does not. + [self watchForExitEvent:YES]; + + // No longer need to hold the lock; the folderController_ now owns it. + [browserController releaseBarVisibilityForOwner:self + withAnimation:NO + delay:NO]; +} + +- (void)openAll:(const BookmarkNode*)node + disposition:(WindowOpenDisposition)disposition { + [self closeFolderAndStopTrackingMenus]; + bookmark_utils::OpenAll([[self view] window], + browser_->profile(), + browser_, + node, + disposition); +} + +- (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): Duplicate code with BookmarkBarFolderController. +// http://crbug.com/35966 +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { + DCHECK([urls count] == [titles count]); + BOOL nodesWereAdded = NO; + // Figure out where these new bookmarks nodes are to be added. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + const BookmarkNode* destParent = NULL; + int destIndex = 0; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere on the bar, so find the right spot. + destParent = bookmarkModel_->GetBookmarkBarNode(); + destIndex = [self indexForDragToPoint:point]; + } + + // Don't add the bookmarks if the destination index shows an error. + if (destIndex >= 0) { + // Create and add the new bookmark nodes. + size_t urlCount = [urls count]; + for (size_t i = 0; i < urlCount; ++i) { + GURL gurl; + const char* string = [[urls objectAtIndex:i] UTF8String]; + if (string) + gurl = GURL(string); + // We only expect to receive valid URLs. + DCHECK(gurl.is_valid()); + if (gurl.is_valid()) { + bookmarkModel_->AddURL(destParent, + destIndex++, + base::SysNSStringToUTF16( + [titles objectAtIndex:i]), + gurl); + nodesWereAdded = YES; + } + } + } + return nodesWereAdded; +} + +// 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]) { + // The button being removed is showing in the bar. + BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; + if (oldButton == [folderController_ parentButton]) { + // If we are deleting a button whose folder is currently open, close it! + [self closeAllBookmarkFolders]; + } + NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; + NSRect oldFrame = [oldButton frame]; + [oldButton setDelegate:nil]; + [oldButton removeFromSuperview]; + if (animate && !ignoreAnimations_ && [self isVisible]) + 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]; + } else if (folderController_ && + [folderController_ parentButton] == offTheSideButton_) { + // The button being removed is in the OTS (off-the-side) and the OTS + // menu is showing so we need to remove the button. + NSInteger index = buttonIndex - displayedButtonCount_; + [folderController_ removeButton:index animate:YES]; + } +} + +- (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]; +} + +#pragma mark BookmarkButtonControllerProtocol + +// NOT an override of a standard Cocoa call made to NSViewControllers. +- (void)hookForEvent:(NSEvent*)theEvent { + if ([self isEventAnExitEvent:theEvent]) + [self closeFolderAndStopTrackingMenus]; +} + +#pragma mark TestingAPI Only + +- (NSMenu*)buttonContextMenu { + return buttonContextMenu_; +} + +// Intentionally ignores ownership issues; used for testing and we try +// to minimize touching the object passed in (likely a mock). +- (void)setButtonContextMenu:(id)menu { + buttonContextMenu_ = menu; +} + +- (void)setIgnoreAnimations:(BOOL)ignore { + ignoreAnimations_ = ignore; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm new file mode 100644 index 0000000..80f6bc7 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm @@ -0,0 +1,2169 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "app/theme_provider.h" +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#include "base/string16.h" +#include "base/string_util.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "chrome/browser/ui/cocoa/test_event_utils.h" +#import "chrome/browser/ui/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" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" + +// Just like a BookmarkBarController but openURL: is stubbed out. +@interface BookmarkBarControllerNoOpen : BookmarkBarController { + @public + std::vector<GURL> urls_; + std::vector<WindowOpenDisposition> dispositions_; +} +@end + +@implementation BookmarkBarControllerNoOpen +- (void)openURL:(GURL)url disposition:(WindowOpenDisposition)disposition { + urls_.push_back(url); + dispositions_.push_back(disposition); +} +- (void)clear { + urls_.clear(); + dispositions_.clear(); +} +@end + + +// NSCell that is pre-provided with a desired size that becomes the +// return value for -(NSSize)cellSize:. +@interface CellWithDesiredSize : NSCell { + @private + NSSize cellSize_; +} +@property (nonatomic, readonly) NSSize cellSize; +@end + +@implementation CellWithDesiredSize + +@synthesize cellSize = cellSize_; + +- (id)initTextCell:(NSString*)string desiredSize:(NSSize)size { + if ((self = [super initTextCell:string])) { + cellSize_ = size; + } + return self; +} + +@end + +// Remember the number of times we've gotten a frameDidChange notification. +@interface BookmarkBarControllerTogglePong : BookmarkBarControllerNoOpen { + @private + int toggles_; +} +@property (nonatomic, readonly) int toggles; +@end + +@implementation BookmarkBarControllerTogglePong + +@synthesize toggles = toggles_; + +- (void)frameDidChange { + toggles_++; +} + +@end + +// Remembers if a notification callback was called. +@interface BookmarkBarControllerNotificationPong : BookmarkBarControllerNoOpen { + BOOL windowWillCloseReceived_; + BOOL windowDidResignKeyReceived_; +} +@property (nonatomic, readonly) BOOL windowWillCloseReceived; +@property (nonatomic, readonly) BOOL windowDidResignKeyReceived; +@end + +@implementation BookmarkBarControllerNotificationPong +@synthesize windowWillCloseReceived = windowWillCloseReceived_; +@synthesize windowDidResignKeyReceived = windowDidResignKeyReceived_; + +// Override NSNotificationCenter callback. +- (void)parentWindowWillClose:(NSNotification*)notification { + windowWillCloseReceived_ = YES; +} + +// NSNotificationCenter callback. +- (void)parentWindowDidResignKey:(NSNotification*)notification { + windowDidResignKeyReceived_ = YES; +} +@end + +// Remembers if and what kind of openAll was performed. +@interface BookmarkBarControllerOpenAllPong : BookmarkBarControllerNoOpen { + WindowOpenDisposition dispositionDetected_; +} +@property (nonatomic) WindowOpenDisposition dispositionDetected; +@end + +@implementation BookmarkBarControllerOpenAllPong +@synthesize dispositionDetected = dispositionDetected_; + +// Intercede for the openAll:disposition: method. +- (void)openAll:(const BookmarkNode*)node + disposition:(WindowOpenDisposition)disposition { + [self setDispositionDetected:disposition]; +} + +@end + +// Just like a BookmarkBarController but intercedes when providing +// pasteboard drag data. +@interface BookmarkBarControllerDragData : BookmarkBarController { + const BookmarkNode* dragDataNode_; // Weak +} +- (void)setDragDataNode:(const BookmarkNode*)node; +@end + +@implementation BookmarkBarControllerDragData + +- (id)initWithBrowser:(Browser*)browser + initialWidth:(CGFloat)initialWidth + delegate:(id<BookmarkBarControllerDelegate>)delegate + resizeDelegate:(id<ViewResizer>)resizeDelegate { + if ((self = [super initWithBrowser:browser + initialWidth:initialWidth + delegate:delegate + resizeDelegate:resizeDelegate])) { + dragDataNode_ = NULL; + } + return self; +} + +- (void)setDragDataNode:(const BookmarkNode*)node { + dragDataNode_ = node; +} + +- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { + std::vector<const BookmarkNode*> dragDataNodes; + if(dragDataNode_) { + dragDataNodes.push_back(dragDataNode_); + } + return dragDataNodes; +} + +@end + + +class FakeTheme : public ThemeProvider { + public: + FakeTheme(NSColor* color) : color_(color) { } + scoped_nsobject<NSColor> color_; + + virtual void Init(Profile* profile) { } + virtual SkBitmap* GetBitmapNamed(int id) const { return nil; } + virtual SkColor GetColor(int id) const { return SkColor(); } + virtual bool GetDisplayProperty(int id, int* result) const { return false; } + virtual bool ShouldUseNativeFrame() const { return false; } + virtual bool HasCustomImage(int id) const { return false; } + virtual RefCountedMemory* GetRawData(int id) const { return NULL; } + virtual NSImage* GetNSImageNamed(int id, bool allow_default) const { + return nil; + } + virtual NSColor* GetNSImageColorNamed(int id, bool allow_default) const { + return nil; + } + virtual NSColor* GetNSColor(int id, bool allow_default) const { + return color_.get(); + } + virtual NSColor* GetNSColorTint(int id, bool allow_default) const { + return nil; + } + virtual NSGradient* GetNSGradient(int id) const { + return nil; + } +}; + + +@interface FakeDragInfo : NSObject { + @public + NSPoint dropLocation_; + NSDragOperation sourceMask_; +} +@property (nonatomic, assign) NSPoint dropLocation; +- (void)setDraggingSourceOperationMask:(NSDragOperation)mask; +@end + +@implementation FakeDragInfo + +@synthesize dropLocation = dropLocation_; + +- (id)init { + if ((self = [super init])) { + dropLocation_ = NSZeroPoint; + sourceMask_ = NSDragOperationMove; + } + return self; +} + +// NSDraggingInfo protocol functions. + +- (id)draggingPasteboard { + return self; +} + +- (id)draggingSource { + return self; +} + +- (NSDragOperation)draggingSourceOperationMask { + return sourceMask_; +} + +- (NSPoint)draggingLocation { + return dropLocation_; +} + +// Other functions. + +- (void)setDraggingSourceOperationMask:(NSDragOperation)mask { + sourceMask_ = mask; +} + +@end + + +namespace { + +class BookmarkBarControllerTestBase : public CocoaTest { + public: + BrowserTestHelper helper_; + scoped_nsobject<NSView> parent_view_; + scoped_nsobject<ViewResizerPong> resizeDelegate_; + + 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]; + } + + 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 + 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_; + scoped_nsobject<BookmarkBarControllerNoOpen> bar_; + + 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) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* node = model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("A title"), gurl); + [menu_ setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; + return menu_item_; + } + + // Does NOT take ownership of node. + NSMenuItem* ItemForBookmarkBarMenu(const BookmarkNode* node) { + [menu_ setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; + return menu_item_; + } + + BookmarkBarControllerNoOpen* noOpenBar() { + return (BookmarkBarControllerNoOpen*)bar_.get(); + } +}; + +TEST_F(BookmarkBarControllerTest, ShowWhenShowBookmarkBarTrue) { + [bar_ updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + EXPECT_TRUE([bar_ isInState:bookmarks::kShowingState]); + EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + EXPECT_FALSE([[bar_ view] isHidden]); + EXPECT_GT([resizeDelegate_ height], 0); + EXPECT_GT([[bar_ view] frame].size.height, 0); +} + +TEST_F(BookmarkBarControllerTest, HideWhenShowBookmarkBarFalse) { + [bar_ updateAndShowNormalBar:NO + showDetachedBar:NO + withAnimation:NO]; + EXPECT_FALSE([bar_ isInState:bookmarks::kShowingState]); + EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]); + EXPECT_FALSE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + EXPECT_TRUE([[bar_ view] isHidden]); + EXPECT_EQ(0, [resizeDelegate_ height]); + EXPECT_EQ(0, [[bar_ view] frame].size.height); +} + +TEST_F(BookmarkBarControllerTest, HideWhenShowBookmarkBarTrueButDisabled) { + [bar_ setBookmarkBarEnabled:NO]; + [bar_ updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + EXPECT_TRUE([bar_ isInState:bookmarks::kShowingState]); + EXPECT_FALSE([bar_ isInState:bookmarks::kDetachedState]); + EXPECT_FALSE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + EXPECT_TRUE([[bar_ view] isHidden]); + EXPECT_EQ(0, [resizeDelegate_ height]); + EXPECT_EQ(0, [[bar_ view] frame].size.height); +} + +TEST_F(BookmarkBarControllerTest, ShowOnNewTabPage) { + [bar_ updateAndShowNormalBar:NO + showDetachedBar:YES + withAnimation:NO]; + EXPECT_FALSE([bar_ isInState:bookmarks::kShowingState]); + EXPECT_TRUE([bar_ isInState:bookmarks::kDetachedState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + EXPECT_FALSE([[bar_ view] isHidden]); + EXPECT_GT([resizeDelegate_ height], 0); + EXPECT_GT([[bar_ view] frame].size.height, 0); + + // Make sure no buttons fall off the bar, either now or when resized + // bigger or smaller. + CGFloat sizes[] = { 300.0, -100.0, 200.0, -420.0 }; + CGFloat previousX = 0.0; + for (unsigned x = 0; x < arraysize(sizes); x++) { + // Confirm the buttons moved from the last check (which may be + // init but that's fine). + CGFloat newX = [[bar_ offTheSideButton] frame].origin.x; + EXPECT_NE(previousX, newX); + previousX = newX; + + // Confirm the buttons have a reasonable bounds. Recall that |-frame| + // returns rectangles in the superview's coordinates. + NSRect buttonViewFrame = + [[bar_ buttonView] convertRect:[[bar_ buttonView] frame] + fromView:[[bar_ buttonView] superview]]; + EXPECT_EQ([bar_ buttonView], [[bar_ offTheSideButton] superview]); + EXPECT_TRUE(NSContainsRect(buttonViewFrame, + [[bar_ offTheSideButton] frame])); + EXPECT_EQ([bar_ buttonView], [[bar_ otherBookmarksButton] superview]); + EXPECT_TRUE(NSContainsRect(buttonViewFrame, + [[bar_ otherBookmarksButton] frame])); + + // Now move them implicitly. + // We confirm FrameChangeNotification works in the next unit test; + // we simply assume it works here to resize or reposition the + // buttons above. + NSRect frame = [[bar_ view] frame]; + frame.size.width += sizes[x]; + [[bar_ view] setFrame:frame]; + } +} + +// Test whether |-updateAndShowNormalBar:...| sets states as we expect. Make +// sure things don't crash. +TEST_F(BookmarkBarControllerTest, StateChanges) { + // First, go in one-at-a-time cycle. + [bar_ updateAndShowNormalBar:NO + showDetachedBar:NO + withAnimation:NO]; + EXPECT_EQ(bookmarks::kHiddenState, [bar_ visualState]); + EXPECT_FALSE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + [bar_ updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + [bar_ updateAndShowNormalBar:YES + showDetachedBar:YES + withAnimation:NO]; + EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + [bar_ updateAndShowNormalBar:NO + showDetachedBar:YES + withAnimation:NO]; + EXPECT_EQ(bookmarks::kDetachedState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + + // Now try some "jumps". + for (int i = 0; i < 2; i++) { + [bar_ updateAndShowNormalBar:NO + showDetachedBar:NO + withAnimation:NO]; + EXPECT_EQ(bookmarks::kHiddenState, [bar_ visualState]); + EXPECT_FALSE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + [bar_ updateAndShowNormalBar:YES + showDetachedBar:YES + withAnimation:NO]; + EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + } + + // Now try some "jumps". + for (int i = 0; i < 2; i++) { + [bar_ updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + EXPECT_EQ(bookmarks::kShowingState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + [bar_ updateAndShowNormalBar:NO + showDetachedBar:YES + withAnimation:NO]; + EXPECT_EQ(bookmarks::kDetachedState, [bar_ visualState]); + EXPECT_TRUE([bar_ isVisible]); + EXPECT_FALSE([bar_ isAnimationRunning]); + } +} + +// Make sure we're watching for frame change notifications. +TEST_F(BookmarkBarControllerTest, FrameChangeNotification) { + scoped_nsobject<BookmarkBarControllerTogglePong> bar; + bar.reset( + [[BookmarkBarControllerTogglePong alloc] + initWithBrowser:helper_.browser() + initialWidth:100 // arbitrary + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + InstallAndToggleBar(bar.get()); + + // Send a frame did change notification for the pong's view. + [[NSNotificationCenter defaultCenter] + postNotificationName:NSViewFrameDidChangeNotification + object:[bar view]]; + + EXPECT_GT([bar toggles], 0); +} + +// Confirm our "no items" container goes away when we add the 1st +// bookmark, and comes back when we delete the bookmark. +TEST_F(BookmarkBarControllerTest, NoItemContainerGoesAway) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* bar = model->GetBookmarkBarNode(); + + [bar_ loaded:model]; + BookmarkBarView* view = [bar_ buttonView]; + DCHECK(view); + NSView* noItemContainer = [view noItemContainer]; + DCHECK(noItemContainer); + + EXPECT_FALSE([noItemContainer isHidden]); + const BookmarkNode* node = model->AddURL(bar, bar->GetChildCount(), + ASCIIToUTF16("title"), + GURL("http://www.google.com")); + EXPECT_TRUE([noItemContainer isHidden]); + model->Remove(bar, bar->IndexOfChild(node)); + EXPECT_FALSE([noItemContainer isHidden]); + + // Now try it using a bookmark from the Other Bookmarks. + const BookmarkNode* otherBookmarks = model->other_node(); + node = model->AddURL(otherBookmarks, otherBookmarks->GetChildCount(), + ASCIIToUTF16("TheOther"), + GURL("http://www.other.com")); + EXPECT_FALSE([noItemContainer isHidden]); + // Move it from Other Bookmarks to the bar. + model->Move(node, bar, 0); + EXPECT_TRUE([noItemContainer isHidden]); + // Move it back to Other Bookmarks from the bar. + model->Move(node, otherBookmarks, 0); + EXPECT_FALSE([noItemContainer isHidden]); +} + +// Confirm off the side button only enabled when reasonable. +TEST_F(BookmarkBarControllerTest, OffTheSideButtonHidden) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ setIgnoreAnimations:YES]; + + [bar_ loaded:model]; + EXPECT_TRUE([bar_ offTheSideButtonIsHidden]); + + for (int i = 0; i < 2; i++) { + model->SetURLStarred(GURL("http://www.foo.com"), ASCIIToUTF16("small"), + true); + EXPECT_TRUE([bar_ offTheSideButtonIsHidden]); + } + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + for (int i = 0; i < 20; i++) { + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("super duper wide title"), + GURL("http://superfriends.hall-of-justice.edu")); + } + EXPECT_FALSE([bar_ offTheSideButtonIsHidden]); + + // Open the "off the side" and start deleting nodes. Make sure + // deletion of the last node in "off the side" causes the folder to + // close. + EXPECT_FALSE([bar_ offTheSideButtonIsHidden]); + NSButton* offTheSideButton = [bar_ offTheSideButton]; + // Open "off the side" menu. + [bar_ openOffTheSideFolderFromButton:offTheSideButton]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + [bbfc setIgnoreAnimations:YES]; + while (parent->GetChildCount()) { + // We've completed the job so we're done. + if ([bar_ offTheSideButtonIsHidden]) + break; + // Delete the last button. + model->Remove(parent, parent->GetChildCount()-1); + // If last one make sure the menu is closed and the button is hidden. + // Else make sure menu stays open. + if ([bar_ offTheSideButtonIsHidden]) { + EXPECT_FALSE([bar_ folderController]); + } else { + EXPECT_TRUE([bar_ folderController]); + } + } +} + +// http://crbug.com/46175 is a crash when deleting bookmarks from the +// off-the-side menu while it is open. This test tries to bang hard +// in this area to reproduce the crash. +TEST_F(BookmarkBarControllerTest, DeleteFromOffTheSideWhileItIsOpen) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ setIgnoreAnimations:YES]; + [bar_ loaded:model]; + + // Add a lot of bookmarks (per the bug). + const BookmarkNode* parent = model->GetBookmarkBarNode(); + for (int i = 0; i < 100; i++) { + std::ostringstream title; + title << "super duper wide title " << i; + model->AddURL(parent, parent->GetChildCount(), ASCIIToUTF16(title.str()), + GURL("http://superfriends.hall-of-justice.edu")); + } + EXPECT_FALSE([bar_ offTheSideButtonIsHidden]); + + // Open "off the side" menu. + NSButton* offTheSideButton = [bar_ offTheSideButton]; + [bar_ openOffTheSideFolderFromButton:offTheSideButton]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + [bbfc setIgnoreAnimations:YES]; + + // Start deleting items; try and delete randomish ones in case it + // makes a difference. + int indices[] = { 2, 4, 5, 1, 7, 9, 2, 0, 10, 9 }; + while (parent->GetChildCount()) { + for (unsigned int i = 0; i < arraysize(indices); i++) { + if (indices[i] < parent->GetChildCount()) { + // First we mouse-enter the button to make things harder. + NSArray* buttons = [bbfc buttons]; + for (BookmarkButton* button in buttons) { + if ([button bookmarkNode] == parent->GetChild(indices[i])) { + [bbfc mouseEnteredButton:button event:nil]; + break; + } + } + // Then we remove the node. This triggers the button to get + // deleted. + model->Remove(parent, indices[i]); + // Force visual update which is otherwise delayed. + [[bbfc window] displayIfNeeded]; + } + } + } +} + +// Test whether |-dragShouldLockBarVisibility| returns NO iff the bar is +// detached. +TEST_F(BookmarkBarControllerTest, TestDragShouldLockBarVisibility) { + [bar_ updateAndShowNormalBar:NO + showDetachedBar:NO + withAnimation:NO]; + EXPECT_TRUE([bar_ dragShouldLockBarVisibility]); + + [bar_ updateAndShowNormalBar:YES + showDetachedBar:NO + withAnimation:NO]; + EXPECT_TRUE([bar_ dragShouldLockBarVisibility]); + + [bar_ updateAndShowNormalBar:YES + showDetachedBar:YES + withAnimation:NO]; + EXPECT_TRUE([bar_ dragShouldLockBarVisibility]); + + [bar_ updateAndShowNormalBar:NO + showDetachedBar:YES + withAnimation:NO]; + EXPECT_FALSE([bar_ dragShouldLockBarVisibility]); +} + +TEST_F(BookmarkBarControllerTest, TagMap) { + int64 ids[] = { 1, 3, 4, 40, 400, 4000, 800000000, 2, 123456789 }; + std::vector<int32> tags; + + // Generate some tags + for (unsigned int i = 0; i < arraysize(ids); i++) { + tags.push_back([bar_ menuTagFromNodeId:ids[i]]); + } + + // Confirm reverse mapping. + for (unsigned int i = 0; i < arraysize(ids); i++) { + EXPECT_EQ(ids[i], [bar_ nodeIdFromMenuTag:tags[i]]); + } + + // Confirm uniqueness. + std::sort(tags.begin(), tags.end()); + for (unsigned int i=0; i<(tags.size()-1); i++) { + EXPECT_NE(tags[i], tags[i+1]); + } +} + +TEST_F(BookmarkBarControllerTest, MenuForFolderNode) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + // First make sure something (e.g. "(empty)" string) is always present. + NSMenu* menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()]; + EXPECT_GT([menu numberOfItems], 0); + + // Test two bookmarks. + GURL gurl("http://www.foo.com"); + model->SetURLStarred(gurl, ASCIIToUTF16("small"), true); + model->SetURLStarred(GURL("http://www.cnn.com"), ASCIIToUTF16("bigger title"), + true); + menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()]; + EXPECT_EQ([menu numberOfItems], 2); + NSMenuItem *item = [menu itemWithTitle:@"bigger title"]; + EXPECT_TRUE(item); + item = [menu itemWithTitle:@"small"]; + EXPECT_TRUE(item); + if (item) { + int64 tag = [bar_ nodeIdFromMenuTag:[item tag]]; + const BookmarkNode* node = model->GetNodeByID(tag); + EXPECT_TRUE(node); + EXPECT_EQ(gurl, node->GetURL()); + } + + // Test with an actual folder as well + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("f1"), GURL("http://framma-lamma.com")); + model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("f2"), GURL("http://framma-lamma-ding-dong.com")); + menu = [bar_ menuForFolderNode:model->GetBookmarkBarNode()]; + EXPECT_EQ([menu numberOfItems], 3); + + item = [menu itemWithTitle:@"group"]; + EXPECT_TRUE(item); + EXPECT_TRUE([item hasSubmenu]); + NSMenu *submenu = [item submenu]; + EXPECT_TRUE(submenu); + EXPECT_EQ(2, [submenu numberOfItems]); + EXPECT_TRUE([submenu itemWithTitle:@"f1"]); + EXPECT_TRUE([submenu itemWithTitle:@"f2"]); +} + +// Confirm openBookmark: forwards the request to the controller's delegate +TEST_F(BookmarkBarControllerTest, OpenBookmark) { + GURL gurl("http://walla.walla.ding.dong.com"); + scoped_ptr<BookmarkNode> node(new BookmarkNode(gurl)); + + scoped_nsobject<BookmarkButtonCell> cell([[BookmarkButtonCell alloc] init]); + [cell setBookmarkNode:node.get()]; + scoped_nsobject<BookmarkButton> button([[BookmarkButton alloc] init]); + [button setCell:cell.get()]; + [cell setRepresentedObject:[NSValue valueWithPointer:node.get()]]; + + [bar_ openBookmark:button]; + EXPECT_EQ(noOpenBar()->urls_[0], node->GetURL()); + EXPECT_EQ(noOpenBar()->dispositions_[0], CURRENT_TAB); +} + +// Confirm opening of bookmarks works from the menus (different +// dispositions than clicking on the button). +TEST_F(BookmarkBarControllerTest, OpenBookmarkFromMenus) { + const char* urls[] = { "http://walla.walla.ding.dong.com", + "http://i_dont_know.com", + "http://cee.enn.enn.dot.com" }; + SEL selectors[] = { @selector(openBookmarkInNewForegroundTab:), + @selector(openBookmarkInNewWindow:), + @selector(openBookmarkInIncognitoWindow:) }; + WindowOpenDisposition dispositions[] = { NEW_FOREGROUND_TAB, + NEW_WINDOW, + OFF_THE_RECORD }; + for (unsigned int i = 0; i < arraysize(dispositions); i++) { + GURL gurl(urls[i]); + [bar_ performSelector:selectors[i] + withObject:ItemForBookmarkBarMenu(gurl)]; + EXPECT_EQ(noOpenBar()->urls_[0], gurl); + EXPECT_EQ(noOpenBar()->dispositions_[0], dispositions[i]); + [bar_ clear]; + } +} + +TEST_F(BookmarkBarControllerTest, TestAddRemoveAndClear) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + NSView* buttonView = [bar_ buttonView]; + EXPECT_EQ(0U, [[bar_ buttons] count]); + unsigned int initial_subview_count = [[buttonView subviews] count]; + + // Make sure a redundant call doesn't choke + [bar_ clearBookmarkBar]; + EXPECT_EQ(0U, [[bar_ buttons] count]); + EXPECT_EQ(initial_subview_count, [[buttonView subviews] count]); + + GURL gurl1("http://superfriends.hall-of-justice.edu"); + // Short titles increase the chances of this test succeeding if the view is + // narrow. + // TODO(viettrungluu): make the test independent of window/view size, font + // metrics, button size and spacing, and everything else. + string16 title1(ASCIIToUTF16("x")); + model->SetURLStarred(gurl1, title1, true); + EXPECT_EQ(1U, [[bar_ buttons] count]); + EXPECT_EQ(1+initial_subview_count, [[buttonView subviews] count]); + + GURL gurl2("http://legion-of-doom.gov"); + string16 title2(ASCIIToUTF16("y")); + model->SetURLStarred(gurl2, title2, true); + EXPECT_EQ(2U, [[bar_ buttons] count]); + EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]); + + for (int i = 0; i < 3; i++) { + // is_starred=false --> remove the bookmark + model->SetURLStarred(gurl2, title2, false); + EXPECT_EQ(1U, [[bar_ buttons] count]); + EXPECT_EQ(1+initial_subview_count, [[buttonView subviews] count]); + + // and bring it back + model->SetURLStarred(gurl2, title2, true); + EXPECT_EQ(2U, [[bar_ buttons] count]); + EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]); + } + + [bar_ clearBookmarkBar]; + EXPECT_EQ(0U, [[bar_ buttons] count]); + EXPECT_EQ(initial_subview_count, [[buttonView subviews] count]); + + // Explicit test of loaded: since this is a convenient spot + [bar_ loaded:model]; + EXPECT_EQ(2U, [[bar_ buttons] count]); + EXPECT_EQ(2+initial_subview_count, [[buttonView subviews] count]); +} + +// Make sure we don't create too many buttons; we only really need +// ones that will be visible. +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(), + ASCIIToUTF16("title"), GURL("http://www.google.com")); + EXPECT_EQ(1U, [[bar_ buttons] count]); + + // Add 30 which we expect to be 'too many'. Make sure we don't see + // 30 buttons. + model->Remove(parent, 0); + EXPECT_EQ(0U, [[bar_ buttons] count]); + for (int i=0; i<30; i++) { + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("title"), GURL("http://www.google.com")); + } + int count = [[bar_ buttons] count]; + EXPECT_LT(count, 30L); + + // Add 10 more (to the front of the list so the on-screen buttons + // would change) and make sure the count stays the same. + for (int i=0; i<10; i++) { + model->AddURL(parent, 0, /* index is 0, so front, not end */ + ASCIIToUTF16("title"), GURL("http://www.google.com")); + } + + // Finally, grow the view and make sure the button count goes up. + NSRect frame = [[bar_ view] frame]; + frame.size.width += 600; + [[bar_ view] setFrame:frame]; + int finalcount = [[bar_ buttons] count]; + EXPECT_GT(finalcount, count); +} + +// Make sure that each button we add marches to the right and does not +// overlap with the previous one. +TEST_F(BookmarkBarControllerTest, TestButtonMarch) { + scoped_nsobject<NSMutableArray> cells([[NSMutableArray alloc] init]); + + CGFloat widths[] = { 10, 10, 100, 10, 500, 500, 80000, 60000, 1, 345 }; + for (unsigned int i = 0; i < arraysize(widths); i++) { + NSCell* cell = [[CellWithDesiredSize alloc] + initTextCell:@"foo" + desiredSize:NSMakeSize(widths[i], 30)]; + [cells addObject:cell]; + [cell release]; + } + + int x_offset = 0; + CGFloat x_end = x_offset; // end of the previous button + for (unsigned int i = 0; i < arraysize(widths); i++) { + NSRect r = [bar_ frameForBookmarkButtonFromCell:[cells objectAtIndex:i] + xOffset:&x_offset]; + EXPECT_GE(r.origin.x, x_end); + x_end = NSMaxX(r); + } +} + +TEST_F(BookmarkBarControllerTest, CheckForGrowth) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + GURL gurl1("http://www.google.com"); + string16 title1(ASCIIToUTF16("x")); + model->SetURLStarred(gurl1, title1, true); + + GURL gurl2("http://www.google.com/blah"); + string16 title2(ASCIIToUTF16("y")); + model->SetURLStarred(gurl2, title2, true); + + EXPECT_EQ(2U, [[bar_ buttons] count]); + CGFloat width_1 = [[[bar_ buttons] objectAtIndex:0] frame].size.width; + CGFloat x_2 = [[[bar_ buttons] objectAtIndex:1] frame].origin.x; + + NSButton* first = [[bar_ buttons] objectAtIndex:0]; + [[first cell] setTitle:@"This is a really big title; watch out mom!"]; + [bar_ checkForBookmarkButtonGrowth:first]; + + // Make sure the 1st button is now wider, the 2nd one is moved over, + // and they don't overlap. + NSRect frame_1 = [[[bar_ buttons] objectAtIndex:0] frame]; + NSRect frame_2 = [[[bar_ buttons] objectAtIndex:1] frame]; + EXPECT_GT(frame_1.size.width, width_1); + EXPECT_GT(frame_2.origin.x, x_2); + EXPECT_GE(frame_2.origin.x, frame_1.origin.x + frame_1.size.width); +} + +TEST_F(BookmarkBarControllerTest, DeleteBookmark) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + const char* urls[] = { "https://secret.url.com", + "http://super.duper.web.site.for.doodz.gov", + "http://www.foo-bar-baz.com/" }; + const BookmarkNode* parent = model->GetBookmarkBarNode(); + for (unsigned int i = 0; i < arraysize(urls); i++) { + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("title"), GURL(urls[i])); + } + EXPECT_EQ(3, parent->GetChildCount()); + const BookmarkNode* middle_node = parent->GetChild(1); + + NSMenuItem* item = ItemForBookmarkBarMenu(middle_node); + [bar_ deleteBookmark:item]; + EXPECT_EQ(2, parent->GetChildCount()); + EXPECT_EQ(parent->GetChild(0)->GetURL(), GURL(urls[0])); + // node 2 moved into spot 1 + EXPECT_EQ(parent->GetChild(1)->GetURL(), GURL(urls[2])); +} + +// TODO(jrg): write a test to confirm that nodeFavIconLoaded calls +// checkForBookmarkButtonGrowth:. + +TEST_F(BookmarkBarControllerTest, Cell) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ loaded:model]; + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("supertitle"), + GURL("http://superfriends.hall-of-justice.edu")); + const BookmarkNode* node = parent->GetChild(0); + + NSCell* cell = [bar_ cellForBookmarkNode:node]; + EXPECT_TRUE(cell); + EXPECT_NSEQ(@"supertitle", [cell title]); + EXPECT_EQ(node, [[cell representedObject] pointerValue]); + EXPECT_TRUE([cell menu]); + + // Empty cells have no menu. + cell = [bar_ cellForBookmarkNode:nil]; + EXPECT_FALSE([cell menu]); + // Even empty cells have a title (of "(empty)") + EXPECT_TRUE([cell title]); + + // cell is autoreleased; no need to release here +} + +// Test drawing, mostly to ensure nothing leaks or crashes. +TEST_F(BookmarkBarControllerTest, Display) { + [[bar_ view] display]; +} + +// Test that middle clicking on a bookmark button results in an open action. +TEST_F(BookmarkBarControllerTest, MiddleClick) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + GURL gurl1("http://www.google.com/"); + string16 title1(ASCIIToUTF16("x")); + model->SetURLStarred(gurl1, title1, true); + + EXPECT_EQ(1U, [[bar_ buttons] count]); + NSButton* first = [[bar_ buttons] objectAtIndex:0]; + EXPECT_TRUE(first); + + [first otherMouseUp:test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0)]; + EXPECT_EQ(noOpenBar()->urls_.size(), 1U); +} + +TEST_F(BookmarkBarControllerTest, DisplaysHelpMessageOnEmpty) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ loaded:model]; + EXPECT_FALSE([[[bar_ buttonView] noItemContainer] isHidden]); +} + +TEST_F(BookmarkBarControllerTest, HidesHelpMessageWithBookmark) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("title"), GURL("http://one.com")); + + [bar_ loaded:model]; + EXPECT_TRUE([[[bar_ buttonView] noItemContainer] isHidden]); +} + +TEST_F(BookmarkBarControllerTest, BookmarkButtonSizing) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("title"), GURL("http://one.com")); + + [bar_ loaded:model]; + + // Make sure the internal bookmark button also is the correct height. + NSArray* buttons = [bar_ buttons]; + EXPECT_GT([buttons count], 0u); + for (NSButton* button in buttons) { + EXPECT_FLOAT_EQ( + (bookmarks::kBookmarkBarHeight + bookmarks::kVisualHeightOffset) - 2 * + bookmarks::kBookmarkVerticalPadding, + [button frame].size.height); + } +} + +TEST_F(BookmarkBarControllerTest, DropBookmarks) { + const char* urls[] = { + "http://qwantz.com", + "http://xkcd.com", + "javascript:alert('lolwut')", + "file://localhost/tmp/local-file.txt" // As if dragged from the desktop. + }; + const char* titles[] = { + "Philosophoraptor", + "Can't draw", + "Inspiration", + "Frum stuf" + }; + EXPECT_EQ(arraysize(urls), arraysize(titles)); + + NSMutableArray* nsurls = [NSMutableArray array]; + NSMutableArray* nstitles = [NSMutableArray array]; + for (size_t i = 0; i < arraysize(urls); ++i) { + [nsurls addObject:base::SysUTF8ToNSString(urls[i])]; + [nstitles addObject:base::SysUTF8ToNSString(titles[i])]; + } + + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + [bar_ addURLs:nsurls withTitles:nstitles at:NSZeroPoint]; + EXPECT_EQ(4, parent->GetChildCount()); + for (int i = 0; i < parent->GetChildCount(); ++i) { + GURL gurl = parent->GetChild(i)->GetURL(); + if (gurl.scheme() == "http" || + gurl.scheme() == "javascript") { + EXPECT_EQ(parent->GetChild(i)->GetURL(), GURL(urls[i])); + } else { + // Be flexible if the scheme needed to be added. + std::string gurl_string = gurl.spec(); + std::string my_string = parent->GetChild(i)->GetURL().spec(); + EXPECT_NE(gurl_string.find(my_string), std::string::npos); + } + EXPECT_EQ(parent->GetChild(i)->GetTitle(), ASCIIToUTF16(titles[i])); + } +} + +TEST_F(BookmarkBarControllerTest, TestButtonOrBar) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + GURL gurl1("http://www.google.com"); + string16 title1(ASCIIToUTF16("x")); + model->SetURLStarred(gurl1, title1, true); + + GURL gurl2("http://www.google.com/gurl_power"); + string16 title2(ASCIIToUTF16("gurl power")); + model->SetURLStarred(gurl2, title2, true); + + NSButton* first = [[bar_ buttons] objectAtIndex:0]; + NSButton* second = [[bar_ buttons] objectAtIndex:1]; + EXPECT_TRUE(first && second); + + NSMenuItem* menuItem = [[[first cell] menu] itemAtIndex:0]; + const BookmarkNode* node = [bar_ nodeFromMenuItem:menuItem]; + EXPECT_TRUE(node); + EXPECT_EQ(node, model->GetBookmarkBarNode()->GetChild(0)); + + menuItem = [[[second cell] menu] itemAtIndex:0]; + node = [bar_ nodeFromMenuItem:menuItem]; + EXPECT_TRUE(node); + EXPECT_EQ(node, model->GetBookmarkBarNode()->GetChild(1)); + + menuItem = [[[bar_ view] menu] itemAtIndex:0]; + node = [bar_ nodeFromMenuItem:menuItem]; + EXPECT_TRUE(node); + EXPECT_EQ(node, model->GetBookmarkBarNode()); +} + +TEST_F(BookmarkBarControllerTest, TestMenuNodeAndDisable) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + NSButton* button = [[bar_ buttons] objectAtIndex:0]; + EXPECT_TRUE(button); + + // Confirm the menu knows which node it is talking about + BookmarkMenu* menu = static_cast<BookmarkMenu*>([[button cell] menu]); + EXPECT_TRUE(menu); + EXPECT_TRUE([menu isKindOfClass:[BookmarkMenu class]]); + EXPECT_EQ(folder->id(), [menu id]); + + // Make sure "Open All" is disabled (nothing to open -- no children!) + // (Assumes "Open All" is the 1st item) + NSMenuItem* item = [menu itemAtIndex:0]; + EXPECT_FALSE([bar_ validateUserInterfaceItem:item]); + + // Now add a child and make sure the item would be enabled. + model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("super duper wide title"), + GURL("http://superfriends.hall-of-justice.edu")); + EXPECT_TRUE([bar_ validateUserInterfaceItem:item]); +} + +TEST_F(BookmarkBarControllerTest, TestDragButton) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + GURL gurls[] = { GURL("http://www.google.com/a"), + GURL("http://www.google.com/b"), + GURL("http://www.google.com/c") }; + string16 titles[] = { ASCIIToUTF16("a"), + ASCIIToUTF16("b"), + ASCIIToUTF16("c") }; + for (unsigned i = 0; i < arraysize(titles); i++) { + model->SetURLStarred(gurls[i], titles[i], true); + } + + EXPECT_EQ([[bar_ buttons] count], arraysize(titles)); + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]); + + [bar_ dragButton:[[bar_ buttons] objectAtIndex:2] + to:NSMakePoint(0, 0) + copy:NO]; + EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:0] title]); + // Make sure a 'copy' did not happen. + EXPECT_EQ([[bar_ buttons] count], arraysize(titles)); + + [bar_ dragButton:[[bar_ buttons] objectAtIndex:1] + to:NSMakePoint(1000, 0) + copy:NO]; + EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:0] title]); + EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:1] title]); + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]); + EXPECT_EQ([[bar_ buttons] count], arraysize(titles)); + + // A drop of the 1st between the next 2. + CGFloat x = NSMinX([[[bar_ buttons] objectAtIndex:2] frame]); + x += [[bar_ view] frame].origin.x; + [bar_ dragButton:[[bar_ buttons] objectAtIndex:0] + to:NSMakePoint(x, 0) + copy:NO]; + EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:0] title]); + EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:1] title]); + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]); + EXPECT_EQ([[bar_ buttons] count], arraysize(titles)); + + // A drop on a non-folder button. (Shouldn't try and go in it.) + x = NSMidX([[[bar_ buttons] objectAtIndex:0] frame]); + x += [[bar_ view] frame].origin.x; + [bar_ dragButton:[[bar_ buttons] objectAtIndex:2] + to:NSMakePoint(x, 0) + copy:NO]; + EXPECT_EQ(arraysize(titles), [[bar_ buttons] count]); + + // A drop on a folder button. + const BookmarkNode* folder = model->AddGroup(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("awesome group")); + DCHECK(folder); + model->AddURL(folder, 0, ASCIIToUTF16("already"), + GURL("http://www.google.com")); + EXPECT_EQ(arraysize(titles) + 1, [[bar_ buttons] count]); + EXPECT_EQ(1, folder->GetChildCount()); + x = NSMidX([[[bar_ buttons] objectAtIndex:0] frame]); + x += [[bar_ view] frame].origin.x; + string16 title = [[[bar_ buttons] objectAtIndex:2] bookmarkNode]->GetTitle(); + [bar_ dragButton:[[bar_ buttons] objectAtIndex:2] + to:NSMakePoint(x, 0) + copy:NO]; + // Gone from the bar + EXPECT_EQ(arraysize(titles), [[bar_ buttons] count]); + // In the folder + EXPECT_EQ(2, folder->GetChildCount()); + // At the end + EXPECT_EQ(title, folder->GetChild(1)->GetTitle()); +} + +TEST_F(BookmarkBarControllerTest, TestCopyButton) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + GURL gurls[] = { GURL("http://www.google.com/a"), + GURL("http://www.google.com/b"), + GURL("http://www.google.com/c") }; + string16 titles[] = { ASCIIToUTF16("a"), + ASCIIToUTF16("b"), + ASCIIToUTF16("c") }; + for (unsigned i = 0; i < arraysize(titles); i++) { + model->SetURLStarred(gurls[i], titles[i], true); + } + EXPECT_EQ([[bar_ buttons] count], arraysize(titles)); + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]); + + // Drag 'a' between 'b' and 'c'. + CGFloat x = NSMinX([[[bar_ buttons] objectAtIndex:2] frame]); + x += [[bar_ view] frame].origin.x; + [bar_ dragButton:[[bar_ buttons] objectAtIndex:0] + to:NSMakePoint(x, 0) + copy:YES]; + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:0] title]); + EXPECT_NSEQ(@"b", [[[bar_ buttons] objectAtIndex:1] title]); + EXPECT_NSEQ(@"a", [[[bar_ buttons] objectAtIndex:2] title]); + EXPECT_NSEQ(@"c", [[[bar_ buttons] objectAtIndex:3] title]); + EXPECT_EQ([[bar_ buttons] count], 4U); +} + +// Fake a theme with colored text. Apply it and make sure bookmark +// buttons have the same colored text. Repeat more than once. +TEST_F(BookmarkBarControllerTest, TestThemedButton) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + model->SetURLStarred(GURL("http://www.foo.com"), ASCIIToUTF16("small"), true); + BookmarkButton* button = [[bar_ buttons] objectAtIndex:0]; + EXPECT_TRUE(button); + + NSArray* colors = [NSArray arrayWithObjects:[NSColor redColor], + [NSColor blueColor], + nil]; + for (NSColor* color in colors) { + FakeTheme theme(color); + [bar_ updateTheme:&theme]; + NSAttributedString* astr = [button attributedTitle]; + EXPECT_TRUE(astr); + EXPECT_NSEQ(@"small", [astr string]); + // Pick a char in the middle to test (index 3) + NSDictionary* attributes = [astr attributesAtIndex:3 effectiveRange:NULL]; + NSColor* newColor = + [attributes objectForKey:NSForegroundColorAttributeName]; + EXPECT_NSEQ(newColor, color); + } +} + +// Test that delegates and targets of buttons are cleared on dealloc. +TEST_F(BookmarkBarControllerTest, TestClearOnDealloc) { + // Make some bookmark buttons. + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + GURL gurls[] = { GURL("http://www.foo.com/"), + GURL("http://www.bar.com/"), + GURL("http://www.baz.com/") }; + string16 titles[] = { ASCIIToUTF16("a"), + ASCIIToUTF16("b"), + ASCIIToUTF16("c") }; + for (size_t i = 0; i < arraysize(titles); i++) + model->SetURLStarred(gurls[i], titles[i], true); + + // Get and retain the buttons so we can examine them after dealloc. + scoped_nsobject<NSArray> buttons([[bar_ buttons] retain]); + EXPECT_EQ([buttons count], arraysize(titles)); + + // Make sure that everything is set. + for (BookmarkButton* button in buttons.get()) { + ASSERT_TRUE([button isKindOfClass:[BookmarkButton class]]); + EXPECT_TRUE([button delegate]); + EXPECT_TRUE([button target]); + EXPECT_TRUE([button action]); + } + + // This will dealloc.... + bar_.reset(); + + // Make sure that everything is cleared. + for (BookmarkButton* button in buttons.get()) { + EXPECT_FALSE([button delegate]); + EXPECT_FALSE([button target]); + EXPECT_FALSE([button action]); + } +} + +TEST_F(BookmarkBarControllerTest, TestFolders) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + + // Create some folder buttons. + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("f1"), GURL("http://framma-lamma.com")); + folder = model->AddGroup(parent, parent->GetChildCount(), + ASCIIToUTF16("empty")); + + EXPECT_EQ([[bar_ buttons] count], 2U); + + // First confirm mouseEntered does nothing if "menus" aren't active. + NSEvent* event = test_event_utils::MakeMouseEvent(NSOtherMouseUp, 0); + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:0] event:event]; + EXPECT_FALSE([bar_ folderController]); + + // Make one active. Entering it is now a no-op. + [bar_ openBookmarkFolderFromButton:[[bar_ buttons] objectAtIndex:0]]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:0] event:event]; + EXPECT_EQ(bbfc, [bar_ folderController]); + + // Enter a different one; a new folderController is active. + [bar_ mouseEnteredButton:[[bar_ buttons] objectAtIndex:1] event:event]; + EXPECT_NE(bbfc, [bar_ folderController]); + + // Confirm exited is a no-op. + [bar_ mouseExitedButton:[[bar_ buttons] objectAtIndex:1] event:event]; + EXPECT_NE(bbfc, [bar_ folderController]); + + // Clean up. + [bar_ closeBookmarkFolder:nil]; +} + +// Verify that the folder menu presentation properly tracks mouse movements +// over the bar. Until there is a click no folder menus should show. After a +// click on a folder folder menus should show until another click on a folder +// button, and a click outside the bar and its folder menus. +TEST_F(BookmarkBarControllerTest, TestFolderButtons) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b 4f:[ 4f1b 4f2b ] "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model and that we do not have a folder controller. + std::string actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + EXPECT_FALSE([bar_ folderController]); + + // Add a real bookmark so we can click on it. + const BookmarkNode* folder = root->GetChild(3); + model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("CLICK ME"), + GURL("http://www.google.com/")); + + // Click on a folder button. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"4f"]; + EXPECT_TRUE(button); + [bar_ openBookmarkFolderFromButton:button]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + + // Make sure a 2nd click on the same button closes things. + [bar_ openBookmarkFolderFromButton:button]; + EXPECT_FALSE([bar_ folderController]); + + // Next open is a different button. + button = [bar_ buttonWithTitleEqualTo:@"2f"]; + EXPECT_TRUE(button); + [bar_ openBookmarkFolderFromButton:button]; + EXPECT_TRUE([bar_ folderController]); + + // Mouse over a non-folder button and confirm controller has gone away. + button = [bar_ buttonWithTitleEqualTo:@"1b"]; + EXPECT_TRUE(button); + NSEvent* event = test_event_utils::MouseEventAtPoint([button center], + NSMouseMoved, 0); + [bar_ mouseEnteredButton:button event:event]; + EXPECT_FALSE([bar_ folderController]); + + // Mouse over the original folder and confirm a new controller. + button = [bar_ buttonWithTitleEqualTo:@"2f"]; + EXPECT_TRUE(button); + [bar_ mouseEnteredButton:button event:event]; + BookmarkBarFolderController* oldBBFC = [bar_ folderController]; + EXPECT_TRUE(oldBBFC); + + // 'Jump' over to a different folder and confirm a new controller. + button = [bar_ buttonWithTitleEqualTo:@"4f"]; + EXPECT_TRUE(button); + [bar_ mouseEnteredButton:button event:event]; + BookmarkBarFolderController* newBBFC = [bar_ folderController]; + EXPECT_TRUE(newBBFC); + EXPECT_NE(oldBBFC, newBBFC); + + // A click on a real bookmark should close and stop tracking the folder menus. + BookmarkButton* bookmarkButton = [newBBFC buttonWithTitleEqualTo:@"CLICK ME"]; + EXPECT_TRUE(bookmarkButton); + [newBBFC openBookmark:bookmarkButton]; + EXPECT_FALSE([bar_ folderController]); + [bar_ mouseEnteredButton:button event:event]; + EXPECT_FALSE([bar_ folderController]); +} + +// Make sure the "off the side" folder looks like a bookmark folder +// but only contains "off the side" items. +TEST_F(BookmarkBarControllerTest, OffTheSideFolder) { + + // It starts hidden. + EXPECT_TRUE([bar_ offTheSideButtonIsHidden]); + + // Create some buttons. + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + for (int x = 0; x < 30; x++) { + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("medium-size-title"), + GURL("http://framma-lamma.com")); + } + // Add a couple more so we can delete one and make sure its button goes away. + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("DELETE_ME"), GURL("http://ashton-tate.com")); + model->AddURL(parent, parent->GetChildCount(), + ASCIIToUTF16("medium-size-title"), + GURL("http://framma-lamma.com")); + + // Should no longer be hidden. + EXPECT_FALSE([bar_ offTheSideButtonIsHidden]); + + // Open it; make sure we have a folder controller. + EXPECT_FALSE([bar_ folderController]); + [bar_ openOffTheSideFolderFromButton:[bar_ offTheSideButton]]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + EXPECT_TRUE(bbfc); + + // Confirm the contents are only buttons which fell off the side by + // making sure that none of the nodes in the off-the-side folder are + // found in bar buttons. Be careful since not all the bar buttons + // may be currently displayed. + NSArray* folderButtons = [bbfc buttons]; + NSArray* barButtons = [bar_ buttons]; + for (BookmarkButton* folderButton in folderButtons) { + for (BookmarkButton* barButton in barButtons) { + if ([barButton superview]) { + EXPECT_NE([folderButton bookmarkNode], [barButton bookmarkNode]); + } + } + } + + // Delete a bookmark in the off-the-side and verify it's gone. + BookmarkButton* button = [bbfc buttonWithTitleEqualTo:@"DELETE_ME"]; + EXPECT_TRUE(button); + model->Remove(parent, parent->GetChildCount() - 2); + button = [bbfc buttonWithTitleEqualTo:@"DELETE_ME"]; + EXPECT_FALSE(button); +} + +TEST_F(BookmarkBarControllerTest, EventToExitCheck) { + NSEvent* event = test_event_utils::MakeMouseEvent(NSMouseMoved, 0); + EXPECT_FALSE([bar_ isEventAnExitEvent:event]); + + BookmarkBarFolderWindow* folderWindow = [[[BookmarkBarFolderWindow alloc] + init] autorelease]; + [[[bar_ view] window] addChildWindow:folderWindow + ordered:NSWindowAbove]; + event = test_event_utils::LeftMouseDownAtPointInWindow(NSMakePoint(1,1), + folderWindow); + EXPECT_FALSE([bar_ isEventAnExitEvent:event]); + + event = test_event_utils::LeftMouseDownAtPointInWindow(NSMakePoint(100,100), + test_window()); + EXPECT_TRUE([bar_ isEventAnExitEvent:event]); + + // Many components are arbitrary (e.g. location, keycode). + event = [NSEvent keyEventWithType:NSKeyDown + location:NSMakePoint(1,1) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + characters:@"x" + charactersIgnoringModifiers:@"x" + isARepeat:NO + keyCode:87]; + EXPECT_TRUE([bar_ isEventAnExitEvent:event]); + + [[[bar_ view] window] removeChildWindow:folderWindow]; +} + +TEST_F(BookmarkBarControllerTest, DropDestination) { + // Make some buttons. + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + model->AddGroup(parent, parent->GetChildCount(), ASCIIToUTF16("group 1")); + model->AddGroup(parent, parent->GetChildCount(), ASCIIToUTF16("group 2")); + EXPECT_EQ([[bar_ buttons] count], 2U); + + // Confirm "off to left" and "off to right" match nothing. + NSPoint p = NSMakePoint(-1, 2); + EXPECT_FALSE([bar_ buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bar_ shouldShowIndicatorShownForPoint:p]); + p = NSMakePoint(50000, 10); + EXPECT_FALSE([bar_ buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bar_ shouldShowIndicatorShownForPoint:p]); + + // Confirm "right in the center" (give or take a pixel) is a match, + // and confirm "just barely in the button" is not. Anything more + // specific seems likely to be tweaked. + CGFloat viewFrameXOffset = [[bar_ view] frame].origin.x; + for (BookmarkButton* button in [bar_ buttons]) { + CGFloat x = NSMidX([button frame]) + viewFrameXOffset; + // Somewhere near the center: a match + EXPECT_EQ(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x-1, 10)]); + EXPECT_EQ(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x+1, 10)]); + EXPECT_FALSE([bar_ shouldShowIndicatorShownForPoint:NSMakePoint(x, 10)]);; + + // On the very edges: NOT a match + x = NSMinX([button frame]) + viewFrameXOffset; + EXPECT_NE(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 9)]); + x = NSMaxX([button frame]) + viewFrameXOffset; + EXPECT_NE(button, + [bar_ buttonForDroppingOnAtPoint:NSMakePoint(x, 11)]); + } +} + +TEST_F(BookmarkBarControllerTest, NodeDeletedWhileMenuIsOpen) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ loaded:model]; + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* initialNode = model->AddURL( + parent, parent->GetChildCount(), + ASCIIToUTF16("initial"), + GURL("http://www.google.com")); + + NSMenuItem* item = ItemForBookmarkBarMenu(initialNode); + 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, noOpenBar()->urls_.size()); + [bar_ clear]; + + // Now delete the node and make sure things are happy (no crash, + // NULL node caught). + model->Remove(parent, parent->IndexOfChild(initialNode)); + EXPECT_EQ(nil, [bar_ nodeFromMenuItem:item]); + // 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, noOpenBar()->urls_.size()); + + // Confirm some more non-crashes. + [bar_ openBookmarkInNewForegroundTab:item]; + [bar_ openBookmarkInIncognitoWindow:item]; + [bar_ editBookmark:item]; + [bar_ copyBookmark:item]; + [bar_ deleteBookmark:item]; + [bar_ openAllBookmarks:item]; + [bar_ openAllBookmarksNewWindow:item]; + [bar_ openAllBookmarksIncognitoWindow:item]; +} + +TEST_F(BookmarkBarControllerTest, NodeDeletedWhileContextMenuIsOpen) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + [bar_ loaded:model]; + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + const BookmarkNode* framma = model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("f1"), + GURL("http://framma-lamma.com")); + + // Mock in a menu + id origMenu = [bar_ buttonContextMenu]; + id fakeMenu = [OCMockObject partialMockForObject:origMenu]; + [[fakeMenu expect] cancelTracking]; + [bar_ setButtonContextMenu:fakeMenu]; + + // Force a delete which should cancelTracking on the menu. + model->Remove(framma->GetParent(), framma->GetParent()->IndexOfChild(framma)); + + // Restore, then confirm cancelTracking was called. + [bar_ setButtonContextMenu:origMenu]; + [fakeMenu verify]; +} + +TEST_F(BookmarkBarControllerTest, CloseFolderOnAnimate) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + model->AddGroup(parent, parent->GetChildCount(), + ASCIIToUTF16("sibbling group")); + model->AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("title a"), + GURL("http://www.google.com/a")); + model->AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("title super duper long long whoa momma title you betcha"), + GURL("http://www.google.com/b")); + BookmarkButton* button = [[bar_ buttons] objectAtIndex:0]; + EXPECT_FALSE([bar_ folderController]); + [bar_ openBookmarkFolderFromButton:button]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + // The following tells us that the folder menu is showing. We want to make + // sure the folder menu goes away if the bookmark bar is hidden. + EXPECT_TRUE(bbfc); + EXPECT_TRUE([bar_ isVisible]); + + // Hide the bookmark bar. + [bar_ updateAndShowNormalBar:NO + showDetachedBar:YES + withAnimation:YES]; + EXPECT_TRUE([bar_ isAnimationRunning]); + + // Now that we've closed the bookmark bar (with animation) the folder menu + // should have been closed thus releasing the folderController. + EXPECT_FALSE([bar_ folderController]); +} + +TEST_F(BookmarkBarControllerTest, MoveRemoveAddButtons) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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_NSEQ(@"2f", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:2] title]); + EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]); + [bar_ moveButtonFromIndex:2 toIndex:0]; + EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:2] title]); + 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_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:2] title]); + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:3] title]); + EXPECT_EQ(oldDisplayedButtons + 1, [bar_ displayedButtonCount]); + node = parent->GetChild(1); + [bar_ addButtonForNode:node atIndex:-1]; + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"1b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:2] title]); + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:3] title]); + EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:4] title]); + EXPECT_EQ(oldDisplayedButtons + 2, [bar_ displayedButtonCount]); + + // Remove a couple of buttons. + [bar_ removeButton:4 animate:NO]; + [bar_ removeButton:1 animate:NO]; + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:2] title]); + EXPECT_EQ(oldDisplayedButtons, [bar_ displayedButtonCount]); +} + +TEST_F(BookmarkBarControllerTest, ShrinkOrHideView) { + NSRect viewFrame = NSMakeRect(0.0, 0.0, 500.0, 50.0); + NSView* view = [[[NSView alloc] initWithFrame:viewFrame] autorelease]; + EXPECT_FALSE([view isHidden]); + [bar_ shrinkOrHideView:view forMaxX:500.0]; + EXPECT_EQ(500.0, NSWidth([view frame])); + EXPECT_FALSE([view isHidden]); + [bar_ shrinkOrHideView:view forMaxX:450.0]; + EXPECT_EQ(450.0, NSWidth([view frame])); + EXPECT_FALSE([view isHidden]); + [bar_ shrinkOrHideView:view forMaxX:40.0]; + EXPECT_EQ(40.0, NSWidth([view frame])); + EXPECT_FALSE([view isHidden]); + [bar_ shrinkOrHideView:view forMaxX:31.0]; + EXPECT_EQ(31.0, NSWidth([view frame])); + EXPECT_FALSE([view isHidden]); + [bar_ shrinkOrHideView:view forMaxX:29.0]; + EXPECT_TRUE([view isHidden]); +} + +class BookmarkBarControllerOpenAllTest : public BookmarkBarControllerTest { +public: + BookmarkBarControllerOpenAllTest() { + resizeDelegate_.reset([[ViewResizerPong alloc] init]); + NSRect parent_frame = NSMakeRect(0, 0, 800, 50); + bar_.reset( + [[BookmarkBarControllerOpenAllPong alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth(parent_frame) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + [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]; + + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + parent_ = model->GetBookmarkBarNode(); + // { one, { two-one, two-two }, three } + model->AddURL(parent_, parent_->GetChildCount(), ASCIIToUTF16("title"), + GURL("http://one.com")); + folder_ = model->AddGroup(parent_, parent_->GetChildCount(), + ASCIIToUTF16("group")); + model->AddURL(folder_, folder_->GetChildCount(), + ASCIIToUTF16("title"), GURL("http://two-one.com")); + model->AddURL(folder_, folder_->GetChildCount(), + ASCIIToUTF16("title"), GURL("http://two-two.com")); + model->AddURL(parent_, parent_->GetChildCount(), + ASCIIToUTF16("title"), GURL("https://three.com")); + } + const BookmarkNode* parent_; // Weak + const BookmarkNode* folder_; // Weak +}; + +TEST_F(BookmarkBarControllerOpenAllTest, OpenAllBookmarks) { + // Our first OpenAll... is from the bar itself. + [bar_ openAllBookmarks:ItemForBookmarkBarMenu(parent_)]; + BookmarkBarControllerOpenAllPong* specialBar = + (BookmarkBarControllerOpenAllPong*)bar_.get(); + EXPECT_EQ([specialBar dispositionDetected], NEW_FOREGROUND_TAB); + + // Now try an OpenAll... from a folder node. + [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset + [bar_ openAllBookmarks:ItemForBookmarkBarMenu(folder_)]; + EXPECT_EQ([specialBar dispositionDetected], NEW_FOREGROUND_TAB); +} + +TEST_F(BookmarkBarControllerOpenAllTest, OpenAllNewWindow) { + // Our first OpenAll... is from the bar itself. + [bar_ openAllBookmarksNewWindow:ItemForBookmarkBarMenu(parent_)]; + BookmarkBarControllerOpenAllPong* specialBar = + (BookmarkBarControllerOpenAllPong*)bar_.get(); + EXPECT_EQ([specialBar dispositionDetected], NEW_WINDOW); + + // Now try an OpenAll... from a folder node. + [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset + [bar_ openAllBookmarksNewWindow:ItemForBookmarkBarMenu(folder_)]; + EXPECT_EQ([specialBar dispositionDetected], NEW_WINDOW); +} + +TEST_F(BookmarkBarControllerOpenAllTest, OpenAllIncognito) { + // Our first OpenAll... is from the bar itself. + [bar_ openAllBookmarksIncognitoWindow:ItemForBookmarkBarMenu(parent_)]; + BookmarkBarControllerOpenAllPong* specialBar = + (BookmarkBarControllerOpenAllPong*)bar_.get(); + EXPECT_EQ([specialBar dispositionDetected], OFF_THE_RECORD); + + // Now try an OpenAll... from a folder node. + [specialBar setDispositionDetected:IGNORE_ACTION]; // Reset + [bar_ openAllBookmarksIncognitoWindow:ItemForBookmarkBarMenu(folder_)]; + EXPECT_EQ([specialBar dispositionDetected], OFF_THE_RECORD); +} + +// Command-click on a folder should open all the bookmarks in it. +TEST_F(BookmarkBarControllerOpenAllTest, CommandClickOnFolder) { + NSButton* first = [[bar_ buttons] objectAtIndex:0]; + EXPECT_TRUE(first); + + // Create the right kind of event; mock NSApp so [NSApp + // currentEvent] finds it. + NSEvent* commandClick = test_event_utils::MouseEventAtPoint(NSZeroPoint, + NSLeftMouseDown, + NSCommandKeyMask); + id fakeApp = [OCMockObject partialMockForObject:NSApp]; + [[[fakeApp stub] andReturn:commandClick] currentEvent]; + id oldApp = NSApp; + NSApp = fakeApp; + size_t originalDispositionCount = noOpenBar()->dispositions_.size(); + + // Click! + [first performClick:first]; + + size_t dispositionCount = noOpenBar()->dispositions_.size(); + EXPECT_EQ(originalDispositionCount+1, dispositionCount); + EXPECT_EQ(noOpenBar()->dispositions_[dispositionCount-1], NEW_BACKGROUND_TAB); + + // Replace NSApp + NSApp = oldApp; +} + +class BookmarkBarControllerNotificationTest : public CocoaTest { + public: + BookmarkBarControllerNotificationTest() { + 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( + [[BookmarkBarControllerNotificationPong alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth(parent_frame) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + + // 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]; + + // Do not add the bar to a window, yet. + } + + BrowserTestHelper helper_; + scoped_nsobject<NSView> parent_view_; + scoped_nsobject<ViewResizerPong> resizeDelegate_; + scoped_nsobject<BookmarkBarControllerNotificationPong> bar_; +}; + +TEST_F(BookmarkBarControllerNotificationTest, DeregistersForNotifications) { + NSWindow* window = [[CocoaTestHelperWindow alloc] init]; + [window setReleasedWhenClosed:YES]; + + // First add the bookmark bar to the temp window, then to another window. + [[window contentView] addSubview:parent_view_]; + [[test_window() contentView] addSubview:parent_view_]; + + // Post a fake windowDidResignKey notification for the temp window and make + // sure the bookmark bar controller wasn't listening. + [[NSNotificationCenter defaultCenter] + postNotificationName:NSWindowDidResignKeyNotification + object:window]; + EXPECT_FALSE([bar_ windowDidResignKeyReceived]); + + // Close the temp window and make sure no notification was received. + [window close]; + EXPECT_FALSE([bar_ windowWillCloseReceived]); +} + + +// 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 +// confirmable. + +// TODO(jrg): add unit test to make sure "Other Bookmarks" responds +// properly to a hover open. + +// TODO(viettrungluu): figure out how to test animations. + +class BookmarkBarControllerDragDropTest : public BookmarkBarControllerTestBase { + public: + scoped_nsobject<BookmarkBarControllerDragData> bar_; + + BookmarkBarControllerDragDropTest() { + bar_.reset( + [[BookmarkBarControllerDragData alloc] + initWithBrowser:helper_.browser() + initialWidth:NSWidth([parent_view_ frame]) + delegate:nil + resizeDelegate:resizeDelegate_.get()]); + InstallAndToggleBar(bar_.get()); + } +}; + +TEST_F(BookmarkBarControllerDragDropTest, DragMoveBarBookmarkToOffTheSide) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + // Insure that the off-the-side is not 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(openOffTheSideFolderFromButton:) + 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, DragOffTheSideToOther) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1bWithLongName 2bWithLongName " + "3bWithLongName 4bWithLongName 5bWithLongName 6bWithLongName " + "7bWithLongName 8bWithLongName 9bWithLongName 10bWithLongName " + "11bWithLongName 12bWithLongName 13bWithLongName 14bWithLongName " + "15bWithLongName 16bWithLongName 17bWithLongName 18bWithLongName " + "19bWithLongName 20bWithLongName "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + const BookmarkNode* other = model.other_node(); + const std::string other_string("1other 2other 3other "); + model_test_utils::AddNodesFromModelString(model, other, other_string); + + // Validate initial model. + std::string actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + std::string actualOtherString = model_test_utils::ModelStringFromNode(other); + EXPECT_EQ(other_string, actualOtherString); + + // 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 oldRootCount = root->GetChildCount(); + int oldOtherCount = other->GetChildCount(); + + // Pop up the off-the-side menu. + BookmarkButton* otsButton = (BookmarkButton*)[bar_ offTheSideButton]; + ASSERT_TRUE(otsButton); + [[otsButton target] performSelector:@selector(openOffTheSideFolderFromButton:) + withObject:otsButton]; + BookmarkBarFolderController* otsController = [bar_ folderController]; + EXPECT_TRUE(otsController); + int oldOTSCount = (int)[[otsController buttons] count]; + EXPECT_EQ(oldOTSCount, oldRootCount - oldDisplayedButtons); + + // Pick an off-the-side button and drag it to the other bookmarks. + BookmarkButton* draggedButton = + [otsController buttonWithTitleEqualTo:@"20bWithLongName"]; + ASSERT_TRUE(draggedButton); + BookmarkButton* targetButton = [bar_ otherBookmarksButton]; + ASSERT_TRUE(targetButton); + [bar_ dragButton:draggedButton to:[targetButton center] copy:NO]; + + // There should one less button in the bar, one less in off-the-side, + // and one more in other bookmarks. + int newRootCount = root->GetChildCount(); + int newOTSCount = (int)[[otsController buttons] count]; + int newOtherCount = other->GetChildCount(); + EXPECT_EQ(oldRootCount - 1, newRootCount); + EXPECT_EQ(oldOTSCount - 1, newOTSCount); + EXPECT_EQ(oldOtherCount + 1, newOtherCount); +} + +TEST_F(BookmarkBarControllerDragDropTest, DragBookmarkData) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + const BookmarkNode* other = model.other_node(); + const std::string other_string("O1b O2b O3f:[ O3f1b O3f2f ] " + "O4f:[ O4f1b O4f2f ] 05b "); + model_test_utils::AddNodesFromModelString(model, other, other_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + actual = model_test_utils::ModelStringFromNode(other); + EXPECT_EQ(other_string, actual); + + // Remember the little ones. + int oldChildCount = root->GetChildCount(); + + BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"3b"]; + ASSERT_TRUE(targetButton); + + // Gen up some dragging data. + const BookmarkNode* newNode = other->GetChild(2); + [bar_ setDragDataNode:newNode]; + scoped_nsobject<FakeDragInfo> dragInfo([[FakeDragInfo alloc] init]); + [dragInfo setDropLocation:[targetButton center]]; + [bar_ dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()]; + + // There should one more button in the bar. + int newChildCount = root->GetChildCount(); + EXPECT_EQ(oldChildCount + 1, newChildCount); + // Verify the model. + const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] O3f:[ O3f1b O3f2f ] 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); + oldChildCount = newChildCount; + + // Now do it over a folder button. + targetButton = [bar_ buttonWithTitleEqualTo:@"2f"]; + ASSERT_TRUE(targetButton); + NSPoint targetPoint = [targetButton center]; + newNode = other->GetChild(2); // Should be O4f. + EXPECT_EQ(newNode->GetTitle(), ASCIIToUTF16("O4f")); + [bar_ setDragDataNode:newNode]; + [dragInfo setDropLocation:targetPoint]; + [bar_ dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()]; + + newChildCount = root->GetChildCount(); + EXPECT_EQ(oldChildCount, newChildCount); + // Verify the model. + const std::string expected1("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b O4f:[ O4f1b O4f2f ] ] O3f:[ O3f1b O3f2f ] " + "3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected1, actual); +} + +TEST_F(BookmarkBarControllerDragDropTest, AddURLs) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + + // Remember the children. + int oldChildCount = root->GetChildCount(); + + BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"3b"]; + ASSERT_TRUE(targetButton); + + NSArray* urls = [NSArray arrayWithObjects: @"http://www.a.com/", + @"http://www.b.com/", nil]; + NSArray* titles = [NSArray arrayWithObjects: @"SiteA", @"SiteB", nil]; + [bar_ addURLs:urls withTitles:titles at:[targetButton center]]; + + // There should two more nodes in the bar. + int newChildCount = root->GetChildCount(); + EXPECT_EQ(oldChildCount + 2, newChildCount); + // Verify the model. + const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] SiteA SiteB 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); +} + +TEST_F(BookmarkBarControllerDragDropTest, ControllerForNode) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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); +} + +TEST_F(BookmarkBarControllerDragDropTest, DropPositionIndicator) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b 2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actualModel = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModel); + + // Test a series of points starting at the right edge of the bar. + BookmarkButton* targetButton = [bar_ buttonWithTitleEqualTo:@"1b"]; + ASSERT_TRUE(targetButton); + NSPoint targetPoint = [targetButton left]; + const CGFloat xDelta = 0.5 * bookmarks::kBookmarkHorizontalPadding; + const CGFloat baseOffset = targetPoint.x; + CGFloat expected = xDelta; + CGFloat actual = [bar_ indicatorPosForDragToPoint:targetPoint]; + EXPECT_CGFLOAT_EQ(expected, actual); + targetButton = [bar_ buttonWithTitleEqualTo:@"2f"]; + actual = [bar_ indicatorPosForDragToPoint:[targetButton right]]; + targetButton = [bar_ buttonWithTitleEqualTo:@"3b"]; + expected = [targetButton left].x - baseOffset + xDelta; + EXPECT_CGFLOAT_EQ(expected, actual); + targetButton = [bar_ buttonWithTitleEqualTo:@"4b"]; + targetPoint = [targetButton right]; + targetPoint.x += 100; // Somewhere off to the right. + expected = NSMaxX([targetButton frame]) + xDelta; + actual = [bar_ indicatorPosForDragToPoint:targetPoint]; + EXPECT_CGFLOAT_EQ(expected, actual); +} + +TEST_F(BookmarkBarControllerDragDropTest, PulseButton) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* root = model->GetBookmarkBarNode(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(root, root->GetChildCount(), + ASCIIToUTF16("title"), gurl); + + BookmarkButton* button = [[bar_ buttons] objectAtIndex:0]; + EXPECT_FALSE([button isContinuousPulsing]); + + NSValue *value = [NSValue valueWithPointer:node]; + NSDictionary *dict = [NSDictionary + dictionaryWithObjectsAndKeys:value, + bookmark_button::kBookmarkKey, + [NSNumber numberWithBool:YES], + bookmark_button::kBookmarkPulseFlagKey, + nil]; + [[NSNotificationCenter defaultCenter] + postNotificationName:bookmark_button::kPulseBookmarkButtonNotification + object:nil + userInfo:dict]; + EXPECT_TRUE([button isContinuousPulsing]); + + dict = [NSDictionary dictionaryWithObjectsAndKeys:value, + bookmark_button::kBookmarkKey, + [NSNumber numberWithBool:NO], + bookmark_button::kBookmarkPulseFlagKey, + nil]; + [[NSNotificationCenter defaultCenter] + postNotificationName:bookmark_button::kPulseBookmarkButtonNotification + object:nil + userInfo:dict]; + EXPECT_FALSE([button isContinuousPulsing]); +} + +TEST_F(BookmarkBarControllerDragDropTest, DragBookmarkDataToTrash) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + + int oldChildCount = root->GetChildCount(); + + // Drag a button to the trash. + BookmarkButton* buttonToDelete = [bar_ buttonWithTitleEqualTo:@"3b"]; + ASSERT_TRUE(buttonToDelete); + EXPECT_TRUE([bar_ canDragBookmarkButtonToTrash:buttonToDelete]); + [bar_ didDragBookmarkToTrash:buttonToDelete]; + + // There should be one less button in the bar. + int newChildCount = root->GetChildCount(); + EXPECT_EQ(oldChildCount - 1, newChildCount); + // Verify the model. + const std::string expected("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); + + // Verify that the other bookmark folder can't be deleted. + BookmarkButton *otherButton = [bar_ otherBookmarksButton]; + EXPECT_FALSE([bar_ canDragBookmarkButtonToTrash:otherButton]); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h new file mode 100644 index 0000000..f599e0a --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h @@ -0,0 +1,31 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_ +#pragma once + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" + +class BookmarkNode; + +// A button cell that handles drawing/highlighting of buttons in the +// bookmark bar. This cell forwards mouseEntered/mouseExited events +// to its control view so that pseudo-menu operations +// (e.g. hover-over to open) can be implemented. +@interface BookmarkBarFolderButtonCell : BookmarkButtonCell { + @private + scoped_nsobject<NSColor> frameColor_; +} + +// Create a button cell which draws without a theme and with a frame +// color provided by the BrowserThemeProvider defaults. ++ (id)buttonCellForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_BUTTON_CELL_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm new file mode 100644 index 0000000..c03500d --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm @@ -0,0 +1,22 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h" + +@implementation BookmarkBarFolderButtonCell + ++ (id)buttonCellForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage { + id buttonCell = + [[[BookmarkBarFolderButtonCell alloc] initForNode:node + contextMenu:contextMenu + cellText:cellText + cellImage:cellImage] + autorelease]; + return buttonCell; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm new file mode 100644 index 0000000..3c20cf7 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm @@ -0,0 +1,24 @@ +// 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 "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" + +namespace { + +class BookmarkBarFolderButtonCellTest : public CocoaTest { +}; + +// Basic creation. +TEST_F(BookmarkBarFolderButtonCellTest, Create) { + scoped_nsobject<BookmarkBarFolderButtonCell> cell; + cell.reset([[BookmarkBarFolderButtonCell buttonCellForNode:nil + contextMenu:nil + cellText:nil + cellImage:nil] retain]); + EXPECT_TRUE(cell); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h new file mode 100644 index 0000000..083efac --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h @@ -0,0 +1,182 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" + +@class BookmarkBarController; +@class BookmarkBarFolderView; +@class BookmarkFolderTarget; +@class BookmarkBarFolderHoverState; + +// A controller for the pop-up windows from bookmark folder buttons +// which look sort of like menus. +@interface BookmarkBarFolderController : + NSWindowController<BookmarkButtonDelegate, + BookmarkButtonControllerProtocol> { + @private + // The button whose click opened us. + scoped_nsobject<BookmarkButton> parentButton_; + + // Bookmark bar folder controller chains are torn down in two ways: + // 1. Clicking "outside" the folder (see use of + // CrApplicationEventHookProtocol in the bookmark bar controller). + // 2. Engaging a different folder (via hover over or explicit click). + // + // In either case, the BookmarkButtonControllerProtocol method + // closeAllBookmarkFolders gets called. For bookmark bar folder + // controllers, this is passed up the chain so we begin with a top + // level "close". + // When any bookmark folder window closes, it necessarily tells + // subcontroller windows to close (down the chain), and autoreleases + // the controller. (Must autorelease since the controller can still + // get delegate events such as windowDidClose). + // + // Bookmark bar folder controllers own their buttons. When doing + // drag and drop of a button from one sub-sub-folder to a different + // sub-sub-folder, we need to make sure the button's pointers stay + // valid until we've dropped (or cancelled). Note that such a drag + // causes the source sub-sub-folder (previous parent window) to go + // away (windows close, controllers autoreleased) since you're + // hovering over a different folder chain for dropping. To keep + // things valid (like the button's target, its delegate, the parent + // cotroller that we have a pointer to below [below], etc), we heep + // strong pointers to our owning controller, so the entire chain + // stays owned. + + // Our parent controller, if we are a nested folder, otherwise nil. + // Strong to insure the object lives as long as we need it. + scoped_nsobject<BookmarkBarFolderController> parentController_; + + // The main bar controller from whence we or a parent sprang. + BookmarkBarController* barController_; // WEAK: It owns us. + + // Our buttons. We do not have buttons for nested folders. + scoped_nsobject<NSMutableArray> buttons_; + + // The scroll view that contains our main button view (below). + IBOutlet NSScrollView* scrollView_; + + // Are we scrollable? If no, the full contents of the folder are + // always visible. + BOOL scrollable_; + + BOOL scrollUpArrowShown_; + BOOL scrollDownArrowShown_; + + // YES if subfolders should grow to the right (the default). + // Direction switches if we'd grow off the screen. + BOOL subFolderGrowthToRight_; + + // The main view of this window (where the buttons go). + IBOutlet BookmarkBarFolderView* mainView_; + + // Weak; we keep track to work around a + // setShowsBorderOnlyWhileMouseInside bug. + BookmarkButton* buttonThatMouseIsIn_; + + // The context menu for a bookmark button which represents an URL. + IBOutlet NSMenu* buttonMenu_; + + // The context menu for a bookmark button which represents a folder. + IBOutlet NSMenu* folderMenu_; + + // We model hover state as a state machine with specific allowable + // transitions. |hoverState_| is the state of this machine at any + // given time. + scoped_nsobject<BookmarkBarFolderHoverState> hoverState_; + + // Logic for dealing with a click on a bookmark folder button. + scoped_nsobject<BookmarkFolderTarget> folderTarget_; + + // A controller for a pop-up bookmark folder window (custom menu). + // We (self) are the parentController_ for our folderController_. + // This is not a scoped_nsobject because it owns itself (when its + // window closes the controller gets autoreleased). + BookmarkBarFolderController* folderController_; + + // Implement basic menu scrolling through this tracking area. + scoped_nsobject<NSTrackingArea> scrollTrackingArea_; + + // Timer to continue scrolling as needed. We own the timer but + // don't release it when done (we invalidate it). + NSTimer* scrollTimer_; + + // Amount to scroll by on each timer fire. Can be + or -. + CGFloat verticalScrollDelta_; + + // We need to know the size of the vertical scrolling arrows so we + // can obscure/unobscure them. + CGFloat verticalScrollArrowHeight_; + + // Set to YES to prevent any node animations. Useful for unit testing so that + // incomplete animations do not cause valgrind complaints. + BOOL ignoreAnimations_; +} + +// Designated initializer. +- (id)initWithParentButton:(BookmarkButton*)button + parentController:(BookmarkBarFolderController*)parentController + barController:(BookmarkBarController*)barController; + +// 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; +- (IBAction)pasteBookmark:(id)sender; +- (IBAction)deleteBookmark:(id)sender; + +// Passed up by a child view to tell us of a desire to scroll. +- (void)scrollWheel:(NSEvent *)theEvent; + +// Forwarded to the associated BookmarkBarController. +- (IBAction)addFolder:(id)sender; +- (IBAction)addPage:(id)sender; +- (IBAction)editBookmark:(id)sender; +- (IBAction)openBookmark:(id)sender; +- (IBAction)openAllBookmarks:(id)sender; +- (IBAction)openAllBookmarksIncognitoWindow:(id)sender; +- (IBAction)openAllBookmarksNewWindow:(id)sender; +- (IBAction)openBookmarkInIncognitoWindow:(id)sender; +- (IBAction)openBookmarkInNewForegroundTab:(id)sender; +- (IBAction)openBookmarkInNewWindow:(id)sender; + +@property (assign, nonatomic) BOOL subFolderGrowthToRight; + +@end + +@interface BookmarkBarFolderController(TestingAPI) +- (NSView*)mainView; +- (NSPoint)windowTopLeftForWidth:(int)windowWidth; +- (NSArray*)buttons; +- (BookmarkBarFolderController*)folderController; +- (id)folderTarget; +- (void)configureWindowLevel; +- (void)performOneScroll:(CGFloat)delta; +- (BookmarkButton*)buttonThatMouseIsIn; +// Set to YES in order to prevent animations. +- (void)setIgnoreAnimations:(BOOL)ignore; + +// Return YES if we can scroll up or down. +- (BOOL)canScrollUp; +- (BOOL)canScrollDown; +// Return YES if the scrollable_ flag has been set. +- (BOOL)scrollable; + +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point; +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm new file mode 100644 index 0000000..b86aab9 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm @@ -0,0 +1,1459 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" + +#include "app/mac/nsimage_cache.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/bookmarks/bookmark_utils.h" +#import "chrome/browser/themes/browser_theme_provider.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/event_utils.h" + +namespace { + +// Frequency of the scrolling timer in seconds. +const NSTimeInterval kBookmarkBarFolderScrollInterval = 0.1; + +// Amount to scroll by per timer fire. We scroll rather slowly; to +// accomodate we do several at a time. +const CGFloat kBookmarkBarFolderScrollAmount = + 3 * bookmarks::kBookmarkButtonVerticalSpan; + +// Amount to scroll for each scroll wheel delta. +const CGFloat kBookmarkBarFolderScrollWheelAmount = + 1 * bookmarks::kBookmarkButtonVerticalSpan; + +// When constraining a scrolling bookmark bar folder window to the +// screen, shrink the "constrain" by this much vertically. Currently +// this is 0.0 to avoid a problem with tracking areas leaving the +// window, but should probably be 8.0 or something. +// TODO(jrg): http://crbug.com/36225 +const CGFloat kScrollWindowVerticalMargin = 0.0; + +} // namespace + +@interface BookmarkBarFolderController(Private) +- (void)configureWindow; +- (void)addOrUpdateScrollTracking; +- (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; + +// Returns the total menu height needed to display |buttonCount| buttons. +// Does not do any fancy tricks like trimming the height to fit on the screen. +- (int)windowHeightForButtonCount:(int)buttonCount; + +// Adjust the height and horizontal position of the window such that the +// scroll arrows are shown as needed and the window appears completely +// on screen. +- (void)adjustWindowForHeight:(int)windowHeight; + +// Show or hide the scroll arrows at the top/bottom of the window. +- (void)showOrHideScrollArrows; + +// |point| is in the base coordinate system of the destination window; +// it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be +// made and inserted into the new location while leaving the bookmark in +// the old location, otherwise move the bookmark by removing from its old +// location and inserting into the new location. +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy; + +@end + +@interface BookmarkButton (BookmarkBarFolderMenuHighlighting) + +// Make the button's border frame always appear when |forceOn| is YES, +// otherwise only border the button when the mouse is inside the button. +- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn; + +// On 10.6 event dispatch for an NSButtonCell's +// showsBorderOnlyWhileMouseInside seems broken if scrolling the +// view that contains the button. It appears that a mouseExited: +// gets lost, so the button stays highlit forever. We accomodate +// here. +- (void)toggleButtonBorderingWhileMouseInside; +@end + +@implementation BookmarkButton (BookmarkBarFolderMenuHighlighting) + +- (void)forceButtonBorderToStayOnAlways:(BOOL)forceOn { + [self setShowsBorderOnlyWhileMouseInside:!forceOn]; + [self setNeedsDisplay]; +} + +- (void)toggleButtonBorderingWhileMouseInside { + BOOL toggle = [self showsBorderOnlyWhileMouseInside]; + [self setShowsBorderOnlyWhileMouseInside:!toggle]; + [self setShowsBorderOnlyWhileMouseInside:toggle]; +} + +@end + +@implementation BookmarkBarFolderController + +@synthesize subFolderGrowthToRight = subFolderGrowthToRight_; + +- (id)initWithParentButton:(BookmarkButton*)button + parentController:(BookmarkBarFolderController*)parentController + barController:(BookmarkBarController*)barController { + NSString* nibPath = + [mac_util::MainAppBundle() pathForResource:@"BookmarkBarFolderWindow" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + parentButton_.reset([button retain]); + + // We want the button to remain bordered as part of the menu path. + [button forceButtonBorderToStayOnAlways:YES]; + + parentController_.reset([parentController retain]); + if (!parentController_) + [self setSubFolderGrowthToRight:YES]; + else + [self setSubFolderGrowthToRight:[parentController + subFolderGrowthToRight]]; + barController_ = barController; // WEAK + buttons_.reset([[NSMutableArray alloc] init]); + folderTarget_.reset([[BookmarkFolderTarget alloc] initWithController:self]); + NSImage* image = app::mac::GetCachedImageWithName(@"menu_overflow_up.pdf"); + DCHECK(image); + verticalScrollArrowHeight_ = [image size].height; + [self configureWindow]; + hoverState_.reset([[BookmarkBarFolderHoverState alloc] init]); + } + return self; +} + +- (void)dealloc { + // The button is no longer part of the menu path. + [parentButton_ forceButtonBorderToStayOnAlways:NO]; + [parentButton_ setNeedsDisplay]; + + [self removeScrollTracking]; + [self endScroll]; + [hoverState_ draggingExited]; + + // Delegate pattern does not retain; make sure pointers to us are removed. + for (BookmarkButton* button in buttons_.get()) { + [button setDelegate:nil]; + [button setTarget:nil]; + [button setAction:nil]; + } + + // Note: we don't need to + // [NSObject cancelPreviousPerformRequestsWithTarget:self]; + // Because all of our performSelector: calls use withDelay: which + // retains us. + [super dealloc]; +} + +// Overriden from NSWindowController to call childFolderWillShow: before showing +// the window. +- (void)showWindow:(id)sender { + [barController_ childFolderWillShow:self]; + [super showWindow:sender]; +} + +- (BookmarkButton*)parentButton { + return parentButton_.get(); +} + +- (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]; +} + +- (void)reconfigureMenu { + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + for (BookmarkButton* button in buttons_.get()) { + [button setDelegate:nil]; + [button removeFromSuperview]; + } + [buttons_ removeAllObjects]; + [self configureWindow]; +} + +#pragma mark Private Methods + +- (BookmarkButtonCell*)cellForBookmarkNode:(const BookmarkNode*)child { + NSImage* image = child ? [barController_ favIconForNode:child] : nil; + NSMenu* menu = child ? child->is_folder() ? folderMenu_ : buttonMenu_ : nil; + BookmarkBarFolderButtonCell* cell = + [BookmarkBarFolderButtonCell buttonCellForNode:child + contextMenu:menu + cellText:nil + cellImage:image]; + [cell setTag:kStandardButtonTypeWithLimitedClickFeedback]; + return cell; +} + +// Redirect to our logic shared with BookmarkBarController. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + [folderTarget_ openBookmarkFolderFromButton:sender]; +} + +// Create a bookmark button for the given node using frame. +// +// If |node| is NULL this is an "(empty)" button. +// Does NOT add this button to our button list. +// Returns an autoreleased button. +// Adjusts the input frame width as appropriate. +// +// TODO(jrg): combine with addNodesToButtonList: code from +// bookmark_bar_controller.mm, and generalize that to use both x and y +// offsets. +// http://crbug.com/35966 +- (BookmarkButton*)makeButtonForNode:(const BookmarkNode*)node + frame:(NSRect)frame { + BookmarkButtonCell* cell = [self cellForBookmarkNode:node]; + DCHECK(cell); + + // We must decide if we draw the folder arrow before we ask the cell + // how big it needs to be. + if (node && node->is_folder()) { + // Warning when combining code with bookmark_bar_controller.mm: + // this call should NOT be made for the bar buttons; only for the + // subfolder buttons. + [cell setDrawFolderArrow:YES]; + } + + // 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; + // 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]; + DCHECK(button); + + [button setCell:cell]; + [button setDelegate:self]; + if (node) { + 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::SysUTF16ToNSString(node->GetTitle()); + std::string urlString = node->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", title, + urlString.c_str()]; + [button setToolTip:tooltip]; + } + } else { + [button setEnabled:NO]; + [button setBordered:NO]; + } + return button; +} + +// Exposed for testing. +- (NSView*)mainView { + return mainView_; +} + +- (id)folderTarget { + return folderTarget_.get(); +} + + +// Our parent controller is another BookmarkBarFolderController, so +// our window is to the right or left of it. We use a little overlap +// since it looks much more menu-like than with none. If we would +// grow off the screen, switch growth to the other direction. Growth +// direction sticks for folder windows which are descendents of us. +// If we have tried both directions and neither fits, degrade to a +// default. +- (CGFloat)childFolderWindowLeftForWidth:(int)windowWidth { + // We may legitimately need to try two times (growth to right and + // left but not in that order). Limit us to three tries in case + // the folder window can't fit on either side of the screen; we + // don't want to loop forever. + CGFloat x; + int tries = 0; + while (tries < 2) { + // Try to grow right. + if ([self subFolderGrowthToRight]) { + tries++; + x = NSMaxX([[parentButton_ window] frame]) - + bookmarks::kBookmarkMenuOverlap; + // If off the screen, switch direction. + if ((x + windowWidth + + bookmarks::kBookmarkHorizontalScreenPadding) > + NSMaxX([[[self window] screen] frame])) { + [self setSubFolderGrowthToRight:NO]; + } else { + return x; + } + } + // Try to grow left. + if (![self subFolderGrowthToRight]) { + tries++; + x = NSMinX([[parentButton_ window] frame]) + + bookmarks::kBookmarkMenuOverlap - + windowWidth; + // If off the screen, switch direction. + if (x < NSMinX([[[self window] screen] frame])) { + [self setSubFolderGrowthToRight:YES]; + } else { + return x; + } + } + } + // Unhappy; do the best we can. + return NSMaxX([[[self window] screen] frame]) - windowWidth; +} + + +// Compute and return the top left point of our window (screen +// coordinates). The top left is positioned in a manner similar to +// cascading menus. Windows may grow to either the right or left of +// their parent (if a sub-folder) so we need to know |windowWidth|. +- (NSPoint)windowTopLeftForWidth:(int)windowWidth { + NSPoint newWindowTopLeft; + if (![parentController_ isKindOfClass:[self class]]) { + // If we're not popping up from one of ourselves, we must be + // popping up from the bookmark bar itself. In this case, start + // BELOW the parent button. Our left is the button left; our top + // is bottom of button's parent view. + NSPoint buttonBottomLeftInScreen = + [[parentButton_ window] + convertBaseToScreen:[parentButton_ + convertPoint:NSZeroPoint toView:nil]]; + NSPoint bookmarkBarBottomLeftInScreen = + [[parentButton_ window] + convertBaseToScreen:[[parentButton_ superview] + convertPoint:NSZeroPoint toView:nil]]; + newWindowTopLeft = NSMakePoint(buttonBottomLeftInScreen.x, + bookmarkBarBottomLeftInScreen.y); + // Make sure the window is on-screen; if not, push left. It is + // intentional that top level folders "push left" slightly + // different than subfolders. + NSRect screenFrame = [[[parentButton_ window] screen] frame]; + CGFloat spillOff = (newWindowTopLeft.x + windowWidth) - NSMaxX(screenFrame); + if (spillOff > 0.0) { + newWindowTopLeft.x = std::max(newWindowTopLeft.x - spillOff, + NSMinX(screenFrame)); + } + } else { + // Parent is a folder; grow right/left. + newWindowTopLeft.x = [self childFolderWindowLeftForWidth:windowWidth]; + NSPoint top = NSMakePoint(0, (NSMaxY([parentButton_ frame]) + + bookmarks::kBookmarkVerticalPadding)); + NSPoint topOfWindow = + [[parentButton_ window] + convertBaseToScreen:[[parentButton_ superview] + convertPoint:top toView:nil]]; + newWindowTopLeft.y = topOfWindow.y; + } + return newWindowTopLeft; +} + +// Set our window level to the right spot so we're above the menubar, dock, etc. +// Factored out so we can override/noop in a unit test. +- (void)configureWindowLevel { + [[self window] setLevel:NSPopUpMenuWindowLevel]; +} + +- (int)windowHeightForButtonCount:(int)buttonCount { + return (buttonCount * bookmarks::kBookmarkButtonVerticalSpan) + + bookmarks::kBookmarkVerticalPadding; +} + +- (void)adjustWindowForHeight:(int)windowHeight { + // Adjust all button widths to be consistent, determine the best size for + // the window, and set the window frame. + CGFloat windowWidth = + [self adjustButtonWidths] + + (2 * bookmarks::kBookmarkSubMenuHorizontalPadding); + NSPoint newWindowTopLeft = [self windowTopLeftForWidth:windowWidth]; + NSSize windowSize = NSMakeSize(windowWidth, windowHeight); + windowSize = [scrollView_ convertSize:windowSize toView:nil]; + NSWindow* window = [self window]; + // If the window is already visible then make sure its top remains stable. + BOOL windowAlreadyShowing = [window isVisible]; + CGFloat deltaY = windowHeight - NSHeight([mainView_ frame]); + if (windowAlreadyShowing) { + NSRect oldFrame = [window frame]; + newWindowTopLeft.y = oldFrame.origin.y + NSHeight(oldFrame); + } + NSRect windowFrame = NSMakeRect(newWindowTopLeft.x, + newWindowTopLeft.y - windowHeight, windowSize.width, windowHeight); + // Make the scrolled content be the right size (full size). + NSRect mainViewFrame = NSMakeRect(0, 0, NSWidth(windowFrame) - + bookmarks::kScrollViewContentWidthMargin, NSHeight(windowFrame)); + [mainView_ setFrame:mainViewFrame]; + // Make sure the window fits on the screen. If not, constrain. + // We'll scroll to allow the user to see all the content. + NSRect screenFrame = [[[self window] screen] frame]; + screenFrame = NSInsetRect(screenFrame, 0, kScrollWindowVerticalMargin); + BOOL wasScrollable = scrollable_; + if (!NSContainsRect(screenFrame, windowFrame)) { + scrollable_ = YES; + windowFrame = NSIntersectionRect(screenFrame, windowFrame); + } else { + scrollable_ = NO; + } + [window setFrame:windowFrame display:NO]; + if (wasScrollable != scrollable_) { + // If scrollability changed then rework visibility of the scroll arrows + // and the scroll offset of the menu view. + NSSize windowLocalSize = + [scrollView_ convertSize:windowFrame.size fromView:nil]; + CGFloat scrollPointY = NSHeight(mainViewFrame) - windowLocalSize.height + + bookmarks::kBookmarkVerticalPadding; + [mainView_ scrollPoint:NSMakePoint(0, scrollPointY)]; + [self showOrHideScrollArrows]; + [self addOrUpdateScrollTracking]; + } else if (scrollable_ && windowAlreadyShowing) { + // If the window was already showing and is still scrollable then make + // sure the main view moves upward, not downward so that the content + // at the bottom of the menu, not the top, appears to move. + // The edge case is when the menu is scrolled all the way to top (hence + // the test of scrollDownArrowShown_) - don't scroll then. + NSView* superView = [mainView_ superview]; + DCHECK([superView isKindOfClass:[NSClipView class]]); + NSClipView* clipView = static_cast<NSClipView*>(superView); + CGFloat scrollPointY = [clipView bounds].origin.y + + bookmarks::kBookmarkVerticalPadding; + if (scrollDownArrowShown_ || deltaY > 0.0) + scrollPointY += deltaY; + [mainView_ scrollPoint:NSMakePoint(0, scrollPointY)]; + } + [window display]; +} + +// Determine window size and position. +// Create buttons for all our nodes. +// TODO(jrg): break up into more and smaller routines for easier unit testing. +- (void)configureWindow { + const BookmarkNode* node = [parentButton_ bookmarkNode]; + DCHECK(node); + int startingIndex = [[parentButton_ cell] startingChildIndex]; + DCHECK_LE(startingIndex, node->GetChildCount()); + // Must have at least 1 button (for "empty") + int buttons = std::max(node->GetChildCount() - startingIndex, 1); + + // Prelim height of the window. We'll trim later as needed. + int height = [self windowHeightForButtonCount:buttons]; + // We'll need this soon... + [self window]; + + // TODO(jrg): combine with frame code in bookmark_bar_controller.mm + // http://crbug.com/35966 + NSRect buttonsOuterFrame = NSMakeRect( + bookmarks::kBookmarkSubMenuHorizontalPadding, + (height - bookmarks::kBookmarkButtonVerticalSpan), + bookmarks::kDefaultBookmarkWidth, + bookmarks::kBookmarkButtonHeight); + + // TODO(jrg): combine with addNodesToButtonList: code from + // bookmark_bar_controller.mm (but use y offset) + // http://crbug.com/35966 + if (!node->GetChildCount()) { + // If no children we are the empty button. + BookmarkButton* button = [self makeButtonForNode:nil + frame:buttonsOuterFrame]; + [buttons_ addObject:button]; + [mainView_ addSubview:button]; + } else { + for (int i = startingIndex; + i < node->GetChildCount(); + i++) { + const BookmarkNode* child = node->GetChild(i); + BookmarkButton* button = [self makeButtonForNode:child + frame:buttonsOuterFrame]; + [buttons_ addObject:button]; + [mainView_ addSubview:button]; + buttonsOuterFrame.origin.y -= bookmarks::kBookmarkButtonVerticalSpan; + } + } + + [self adjustWindowForHeight:height]; + // Finally pop me up. + [self configureWindowLevel]; +} + +// TODO(mrossetti): See if the following can be moved into view's viewWillDraw:. +- (CGFloat)adjustButtonWidths { + CGFloat width = bookmarks::kBookmarkMenuButtonMinimumWidth; + // Use the cell's size as the base for determining the desired width of the + // button rather than the button's current width. -[cell cellSize] always + // returns the 'optimum' size of the cell based on the cell's contents even + // if it's less than the current button size. Relying on the button size + // would result in buttons that could only get wider but we want to handle + // the case where the widest button gets removed from a folder menu. + for (BookmarkButton* button in buttons_.get()) + width = std::max(width, [[button cell] cellSize].width); + 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; +} + +- (BOOL)canScrollUp { + // If removal of an arrow would make things "finished", state as + // such. + CGFloat scrollY = [scrollView_ documentVisibleRect].origin.y; + if (scrollUpArrowShown_) + scrollY -= verticalScrollArrowHeight_; + + if (scrollY <= 0) + return NO; + return YES; +} + +- (BOOL)canScrollDown { + CGFloat arrowAdjustment = 0.0; + + // We do NOT adjust based on the scrollDOWN arrow. This keeps + // things from "jumping"; if removal of the down arrow (at the top + // of the window) would cause a scroll to end, we'll end. + if (scrollUpArrowShown_) + arrowAdjustment += verticalScrollArrowHeight_; + + NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin; + NSRect documentRect = [[scrollView_ documentView] frame]; + + // If we are exactly the right height, return no. We need this + // extra conditional in the case where we've just scrolled/grown + // into position. + if (NSHeight([[self window] frame]) == NSHeight(documentRect)) + return NO; + + if ((scrollPosition.y + NSHeight([[self window] frame])) >= + (NSHeight(documentRect) + arrowAdjustment)) { + return NO; + } + return YES; +} + +- (void)showOrHideScrollArrows { + NSRect frame = [scrollView_ frame]; + CGFloat scrollDelta = 0.0; + BOOL canScrollDown = [self canScrollDown]; + BOOL canScrollUp = [self canScrollUp]; + + if (canScrollUp != scrollUpArrowShown_) { + if (scrollUpArrowShown_) { + frame.origin.y -= verticalScrollArrowHeight_; + frame.size.height += verticalScrollArrowHeight_; + scrollDelta = verticalScrollArrowHeight_; + } else { + frame.origin.y += verticalScrollArrowHeight_; + frame.size.height -= verticalScrollArrowHeight_; + scrollDelta = -verticalScrollArrowHeight_; + } + } + if (canScrollDown != scrollDownArrowShown_) { + if (scrollDownArrowShown_) { + frame.size.height += verticalScrollArrowHeight_; + } else { + frame.size.height -= verticalScrollArrowHeight_; + } + } + scrollUpArrowShown_ = canScrollUp; + scrollDownArrowShown_ = canScrollDown; + [scrollView_ setFrame:frame]; + + // Adjust scroll based on new frame. For example, if we make room + // for an arrow at the bottom, adjust the scroll so the topmost item + // is still fully visible. + if (scrollDelta) { + NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin; + scrollPosition.y -= scrollDelta; + [[scrollView_ documentView] scrollPoint:scrollPosition]; + } +} + +- (BOOL)scrollable { + return scrollable_; +} + +// Start a "scroll up" timer. +- (void)beginScrollWindowUp { + [self addScrollTimerWithDelta:kBookmarkBarFolderScrollAmount]; +} + +// Start a "scroll down" timer. +- (void)beginScrollWindowDown { + [self addScrollTimerWithDelta:-kBookmarkBarFolderScrollAmount]; +} + +// End a scrolling timer. Can be called excessively with no harm. +- (void)endScroll { + if (scrollTimer_) { + [scrollTimer_ invalidate]; + scrollTimer_ = nil; + verticalScrollDelta_ = 0; + } +} + +// Perform a single scroll of the specified amount. +// Scroll up: +// Scroll the documentView by the growth amount. +// If we cannot grow the window, simply scroll the documentView. +// If we can grow the window up without falling off the screen, do it. +// Scroll down: +// Never change the window size; only scroll the documentView. +- (void)performOneScroll:(CGFloat)delta { + NSRect windowFrame = [[self window] frame]; + NSRect screenFrame = [[[self window] screen] frame]; + + // First scroll the "document" area. + NSPoint scrollPosition = [scrollView_ documentVisibleRect].origin; + scrollPosition.y -= delta; + [[scrollView_ documentView] scrollPoint:scrollPosition]; + + if (buttonThatMouseIsIn_) + [buttonThatMouseIsIn_ toggleButtonBorderingWhileMouseInside]; + + // We update the window size after shifting the scroll to avoid a race. + CGFloat screenHeightMinusMargin = (NSHeight(screenFrame) - + (2 * kScrollWindowVerticalMargin)); + if (delta) { + // If we can, grow the window (up). + if (NSHeight(windowFrame) < screenHeightMinusMargin) { + CGFloat growAmount = delta; + // Don't scroll more than enough to "finish". + if (scrollPosition.y < 0) + growAmount += scrollPosition.y; + windowFrame.size.height += growAmount; + windowFrame.size.height = std::min(NSHeight(windowFrame), + screenHeightMinusMargin); + // Watch out for a finish that isn't the full height of the screen. + // We get here if using the scroll wheel to scroll by small amounts. + windowFrame.size.height = std::min(NSHeight(windowFrame), + NSHeight([mainView_ frame])); + // Don't allow scrolling to make the window smaller, ever. This + // conditional is important when processing scrollWheel events. + if (windowFrame.size.height > [[self window] frame].size.height) { + [[self window] setFrame:windowFrame display:YES]; + [self addOrUpdateScrollTracking]; + } + } + } + + // If we're at either end, happiness. + if ((scrollPosition.y <= 0) || + ((scrollPosition.y + NSHeight(windowFrame) >= + NSHeight([mainView_ frame])) && + (windowFrame.size.height == screenHeightMinusMargin))) { + [self endScroll]; + + // If we can't scroll either up or down we are completely done. + // For example, perhaps we've scrolled a little and grown the + // window on-screen until there is now room for everything. + if (![self canScrollUp] && ![self canScrollDown]) { + scrollable_ = NO; + [self removeScrollTracking]; + } + } + + [self showOrHideScrollArrows]; +} + +// Perform a scroll of the window on the screen. +// Called by a timer when scrolling. +- (void)performScroll:(NSTimer*)timer { + DCHECK(verticalScrollDelta_); + [self performOneScroll:verticalScrollDelta_]; +} + + +// Add a timer to fire at a regular interveral which scrolls the +// window vertically |delta|. +- (void)addScrollTimerWithDelta:(CGFloat)delta { + if (scrollTimer_ && verticalScrollDelta_ == delta) + return; + [self endScroll]; + verticalScrollDelta_ = delta; + scrollTimer_ = + [NSTimer scheduledTimerWithTimeInterval:kBookmarkBarFolderScrollInterval + target:self + selector:@selector(performScroll:) + userInfo:nil + repeats:YES]; +} + +// Called as a result of our tracking area. Warning: on the main +// screen (of a single-screened machine), the minimum mouse y value is +// 1, not 0. Also, we do not get events when the mouse is above the +// menubar (to be fixed by setting the proper window level; see +// initializer). +- (void)mouseMoved:(NSEvent*)theEvent { + DCHECK([theEvent window] == [self window]); + + NSPoint eventScreenLocation = + [[theEvent window] convertBaseToScreen:[theEvent locationInWindow]]; + + // We use frame (not visibleFrame) since our bookmark folder is on + // TOP of the menubar. + NSRect visibleRect = [[[self window] screen] frame]; + CGFloat closeToTopOfScreen = NSMaxY(visibleRect) - + verticalScrollArrowHeight_; + CGFloat closeToBottomOfScreen = NSMinY(visibleRect) + + verticalScrollArrowHeight_; + + if (eventScreenLocation.y <= closeToBottomOfScreen) { + [self beginScrollWindowUp]; + } else if (eventScreenLocation.y > closeToTopOfScreen) { + [self beginScrollWindowDown]; + } else { + [self endScroll]; + } +} + +- (void)mouseExited:(NSEvent*)theEvent { + [self endScroll]; +} + +// Add a tracking area so we know when the mouse is pinned to the top +// or bottom of the screen. If that happens, and if the mouse +// position overlaps the window, scroll it. +- (void)addOrUpdateScrollTracking { + [self removeScrollTracking]; + NSView* view = [[self window] contentView]; + scrollTrackingArea_.reset([[NSTrackingArea alloc] + initWithRect:[view bounds] + options:(NSTrackingMouseMoved | + NSTrackingMouseEnteredAndExited | + NSTrackingActiveAlways) + owner:self + userInfo:nil]); + [view addTrackingArea:scrollTrackingArea_]; +} + +// Remove the tracking area associated with scrolling. +- (void)removeScrollTracking { + if (scrollTrackingArea_.get()) { + [[[self window] contentView] removeTrackingArea:scrollTrackingArea_]; + } + scrollTrackingArea_.reset(); +} + +// Delegate callback. +- (void)windowWillClose:(NSNotification*)notification { + // If a "hover open" is pending when the bookmark bar folder is + // closed, be sure it gets cancelled. + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + [barController_ childFolderWillClose:self]; + [self closeBookmarkFolder:self]; + [self autorelease]; +} + +// Close the old hover-open bookmark folder, and open a new one. We +// do both in one step to allow for a delay in closing the old one. +// See comments above kDragHoverCloseDelay (bookmark_bar_controller.h) +// for more details. +- (void)openBookmarkFolderFromButtonAndCloseOldOne:(id)sender { + // If an old submenu exists, close it immediately. + [self closeBookmarkFolder:sender]; + + // Open a new one if meaningful. + if ([sender isFolder]) + [folderTarget_ openBookmarkFolderFromButton:sender]; +} + +- (NSArray*)buttons { + return buttons_.get(); +} + +- (void)close { + [folderController_ close]; + [super close]; +} + +- (void)scrollWheel:(NSEvent *)theEvent { + if (scrollable_) { + // We go negative since an NSScrollView has a flipped coordinate frame. + CGFloat amt = kBookmarkBarFolderScrollWheelAmount * -[theEvent deltaY]; + [self performOneScroll:amt]; + } +} + +#pragma mark Actions Forwarded to Parent BookmarkBarController + +- (IBAction)openBookmark:(id)sender { + [barController_ openBookmark:sender]; +} + +- (IBAction)openBookmarkInNewForegroundTab:(id)sender { + [barController_ openBookmarkInNewForegroundTab:sender]; +} + +- (IBAction)openBookmarkInNewWindow:(id)sender { + [barController_ openBookmarkInNewWindow:sender]; +} + +- (IBAction)openBookmarkInIncognitoWindow:(id)sender { + [barController_ openBookmarkInIncognitoWindow:sender]; +} + +- (IBAction)editBookmark:(id)sender { + [barController_ editBookmark:sender]; +} + +- (IBAction)cutBookmark:(id)sender { + [self closeBookmarkFolder:self]; + [barController_ cutBookmark:sender]; +} + +- (IBAction)copyBookmark:(id)sender { + [barController_ copyBookmark:sender]; +} + +- (IBAction)pasteBookmark:(id)sender { + [barController_ pasteBookmark:sender]; +} + +- (IBAction)deleteBookmark:(id)sender { + [self closeBookmarkFolder:self]; + [barController_ deleteBookmark:sender]; +} + +- (IBAction)openAllBookmarks:(id)sender { + [barController_ openAllBookmarks:sender]; +} + +- (IBAction)openAllBookmarksNewWindow:(id)sender { + [barController_ openAllBookmarksNewWindow:sender]; +} + +- (IBAction)openAllBookmarksIncognitoWindow:(id)sender { + [barController_ openAllBookmarksIncognitoWindow:sender]; +} + +- (IBAction)addPage:(id)sender { + [barController_ addPage:sender]; +} + +- (IBAction)addFolder:(id)sender { + [barController_ addFolder:sender]; +} + +#pragma mark Drag & Drop + +// Find something like std::is_between<T>? I can't believe one doesn't exist. +// http://crbug.com/35966 +static BOOL ValueInRangeInclusive(CGFloat low, CGFloat value, CGFloat high) { + return ((value >= low) && (value <= high)); +} + +// Return the proposed drop target for a hover open button, or nil if none. +// +// TODO(jrg): this is just like the version in +// bookmark_bar_controller.mm, but vertical instead of horizontal. +// Generalize to be axis independent then share code. +// http://crbug.com/35966 +- (BookmarkButton*)buttonForDroppingOnAtPoint:(NSPoint)point { + for (BookmarkButton* button in buttons_.get()) { + // No early break -- makes no assumption about button ordering. + + // Intentionally NOT using NSPointInRect() so that scrolling into + // a submenu doesn't cause it to be closed. + if (ValueInRangeInclusive(NSMinY([button frame]), + point.y, + NSMaxY([button frame]))) { + + // Over a button but let's be a little more specific + // (e.g. over the middle half). + NSRect frame = [button frame]; + NSRect middleHalfOfButton = NSInsetRect(frame, 0, frame.size.height / 4); + if (ValueInRangeInclusive(NSMinY(middleHalfOfButton), + point.y, + NSMaxY(middleHalfOfButton))) { + // It makes no sense to drop on a non-folder; there is no hover. + if (![button isFolder]) + return nil; + // Got it! + return button; + } else { + // Over a button but not over the middle half. + return nil; + } + } + } + // Not hovering over a button. + return nil; +} + +// TODO(jrg): again we have code dup, sort of, with +// bookmark_bar_controller.mm, but the axis is changed. One minor +// difference is accomodation for the "empty" button (which may not +// exist in the future). +// http://crbug.com/35966 +- (int)indexForDragToPoint:(NSPoint)point { + // Identify which buttons we are between. For now, assume a button + // location is at the center point of its view, and that an exact + // match means "place before". + // TODO(jrg): revisit position info based on UI team feedback. + // dropLocation is in bar local coordinates. + // http://crbug.com/36276 + NSPoint dropLocation = + [mainView_ convertPoint:point + fromView:[[self window] contentView]]; + BookmarkButton* buttonToTheTopOfDraggedButton = nil; + // Buttons are laid out in this array from top to bottom (screen + // wise), which means "biggest y" --> "smallest y". + for (BookmarkButton* button in buttons_.get()) { + CGFloat midpoint = NSMidY([button frame]); + if (dropLocation.y > midpoint) { + break; + } + buttonToTheTopOfDraggedButton = button; + } + + // TODO(jrg): On Windows, dropping onto (empty) highlights the + // entire drop location and does not use an insertion point. + // http://crbug.com/35967 + if (!buttonToTheTopOfDraggedButton) { + // We are at the very top (we broke out of the loop on the first try). + return 0; + } + if ([buttonToTheTopOfDraggedButton isEmpty]) { + // There is a button but it's an empty placeholder. + // Default to inserting on top of it. + return 0; + } + const BookmarkNode* beforeNode = [buttonToTheTopOfDraggedButton + bookmarkNode]; + DCHECK(beforeNode); + // Be careful if the number of buttons != number of nodes. + return ((beforeNode->GetParent()->IndexOfChild(beforeNode) + 1) - + [[parentButton_ cell] startingChildIndex]); +} + +// TODO(jrg): Yet more code dup. +// http://crbug.com/35966 +- (BOOL)dragBookmark:(const BookmarkNode*)sourceNode + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK(sourceNode); + + // Drop destination. + const BookmarkNode* destParent = NULL; + int destIndex = 0; + + // First check if we're dropping on a button. If we have one, and + // it's a folder, drop in it. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere in the folder, so find the right spot. + destParent = [parentButton_ bookmarkNode]; + destIndex = [self indexForDragToPoint:point]; + // Be careful if the number of buttons != number of nodes. + destIndex += [[parentButton_ cell] startingChildIndex]; + } + + // 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 wasCopiedOrMoved; +} + +#pragma mark BookmarkButtonDelegate Protocol + +- (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]; +} + +// Called from BookmarkButton. +// Unlike bookmark_bar_controller's version, we DO default to being enabled. +- (void)mouseEnteredButton:(id)sender event:(NSEvent*)event { + buttonThatMouseIsIn_ = sender; + + // Cancel a previous hover if needed. + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + // If already opened, then we exited but re-entered the button + // (without entering another button open), do nothing. + if ([folderController_ parentButton] == sender) + return; + + [self performSelector:@selector(openBookmarkFolderFromButtonAndCloseOldOne:) + withObject:sender + afterDelay:bookmarks::kHoverOpenDelay]; +} + +// Called from the BookmarkButton +- (void)mouseExitedButton:(id)sender event:(NSEvent*)event { + if (buttonThatMouseIsIn_ == sender) + buttonThatMouseIsIn_ = nil; + + // Stop any timer about opening a new hover-open folder. + + // Since a performSelector:withDelay: on self retains self, it is + // possible that a cancelPreviousPerformRequestsWithTarget: reduces + // the refcount to 0, releasing us. That's a bad thing to do while + // this object (or others it may own) is in the event chain. Thus + // we have a retain/autorelease. + [self retain]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + [self autorelease]; +} + +- (NSWindow*)browserWindow { + return [parentController_ browserWindow]; +} + +- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { + return [barController_ canEditBookmark:[button bookmarkNode]]; +} + +- (void)didDragBookmarkToTrash:(BookmarkButton*)button { + // TODO(mrossetti): Refactor BookmarkBarFolder common code. + // http://crbug.com/35966 + const BookmarkNode* node = [button bookmarkNode]; + if (node) { + const BookmarkNode* parent = node->GetParent(); + [self bookmarkModel]->Remove(parent, + parent->IndexOfChild(node)); + } +} + +#pragma mark BookmarkButtonControllerProtocol + +// Recursively close all bookmark folders. +- (void)closeAllBookmarkFolders { + // Closing the top level implicitly closes all children. + [barController_ closeAllBookmarkFolders]; +} + +// Close our bookmark folder (a sub-controller) if we have one. +- (void)closeBookmarkFolder:(id)sender { + if (folderController_) { + [self setSubFolderGrowthToRight:YES]; + [[folderController_ window] close]; + folderController_ = nil; + } +} + +- (BookmarkModel*)bookmarkModel { + return [barController_ bookmarkModel]; +} + +// 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 +// bookmark_bar_controller.mm's version. +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + 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]; +} + +// Unlike bookmark_bar_controller, we need to keep track of dragging state. +// We also need to make sure we cancel the delayed hover close. +- (void)draggingExited:(id<NSDraggingInfo>)info { + // NOT the same as a cancel --> we may have moved the mouse into the submenu. + // Delegate handling of the hover button to the |hoverState_| member. + [hoverState_ draggingExited]; +} + +- (BOOL)dragShouldLockBarVisibility { + return [parentController_ dragShouldLockBarVisibility]; +} + +// TODO(jrg): ARGH more code dup. +// http://crbug.com/35966 +- (BOOL)dragButton:(BookmarkButton*)sourceButton + to:(NSPoint)point + copy:(BOOL)copy { + DCHECK([sourceButton isKindOfClass:[BookmarkButton class]]); + const BookmarkNode* sourceNode = [sourceButton bookmarkNode]; + return [self dragBookmark:sourceNode to:point copy:copy]; +} + +// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. +// http://crbug.com/35966 +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { + BOOL dragged = NO; + std::vector<const BookmarkNode*> nodes([self retrieveBookmarkNodeData]); + if (nodes.size()) { + BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); + NSPoint dropPoint = [info draggingLocation]; + for (std::vector<const BookmarkNode*>::const_iterator it = nodes.begin(); + it != nodes.end(); ++it) { + const BookmarkNode* sourceNode = *it; + dragged = [self dragBookmark:sourceNode to:dropPoint copy:copy]; + } + } + return dragged; +} + +// TODO(mrossetti,jrg): Identical to the same function in BookmarkBarController. +// http://crbug.com/35966 +- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { + std::vector<const BookmarkNode*> dragDataNodes; + BookmarkNodeData dragData; + if(dragData.ReadFromDragClipboard()) { + BookmarkModel* bookmarkModel = [self bookmarkModel]; + Profile* profile = bookmarkModel->profile(); + std::vector<const BookmarkNode*> nodes(dragData.GetNodes(profile)); + dragDataNodes.assign(nodes.begin(), nodes.end()); + } + return dragDataNodes; +} + +// Return YES if we should show the drop indicator, else NO. +// TODO(jrg): ARGH code dup! +// http://crbug.com/35966 +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return ![self buttonForDroppingOnAtPoint:point]; +} + +// Return the y position for a drop indicator. +// +// TODO(jrg): again we have code dup, sort of, with +// bookmark_bar_controller.mm, but the axis is changed. +// http://crbug.com/35966 +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { + CGFloat y = 0; + int destIndex = [self indexForDragToPoint:point]; + int numButtons = static_cast<int>([buttons_ count]); + + // If it's a drop strictly between existing buttons or at the very beginning + if (destIndex >= 0 && destIndex < numButtons) { + // ... put the indicator right between the buttons. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + y = NSMaxY(buttonFrame) + 0.5 * bookmarks::kBookmarkVerticalPadding; + + // If it's a drop at the end (past the last button, if there are any) ... + } else if (destIndex == numButtons) { + // and if it's past the last button ... + if (numButtons > 0) { + // ... find the last button, and put the indicator below it. + BookmarkButton* button = + [buttons_ objectAtIndex:static_cast<NSUInteger>(destIndex - 1)]; + DCHECK(button); + NSRect buttonFrame = [button frame]; + y = buttonFrame.origin.y - 0.5 * bookmarks::kBookmarkVerticalPadding; + + } + } else { + NOTREACHED(); + } + + return y; +} + +- (ThemeProvider*)themeProvider { + return [parentController_ themeProvider]; +} + +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { + // Do nothing. +} + +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { + // Do nothing. +} + +- (BookmarkBarFolderController*)folderController { + return folderController_; +} + +// Add a new folder controller as triggered by the given folder button. +- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton { + if (folderController_) + [self closeBookmarkFolder:self]; + + // Folder controller, like many window controllers, owns itself. + folderController_ = + [[BookmarkBarFolderController alloc] initWithParentButton:parentButton + parentController:self + barController:barController_]; + [folderController_ showWindow:self]; +} + +- (void)openAll:(const BookmarkNode*)node + disposition:(WindowOpenDisposition)disposition { + [barController_ openAll:node disposition:disposition]; +} + +- (void)addButtonForNode:(const BookmarkNode*)node + atIndex:(NSInteger)buttonIndex { + // Propose the frame for the new button. By default, this will be set to the + // topmost button's frame (and there will always be one) offset upward in + // anticipation of insertion. + NSRect newButtonFrame = [[buttons_ objectAtIndex:0] frame]; + newButtonFrame.origin.y += bookmarks::kBookmarkButtonVerticalSpan; + // When adding a button to an empty folder we must remove the 'empty' + // placeholder button. This can be detected by checking for a parent + // child count of 1. + const BookmarkNode* parentNode = node->GetParent(); + if (parentNode->GetChildCount() == 1) { + BookmarkButton* emptyButton = [buttons_ lastObject]; + newButtonFrame = [emptyButton frame]; + [emptyButton setDelegate:nil]; + [emptyButton removeFromSuperview]; + [buttons_ removeLastObject]; + } + + if (buttonIndex == -1 || buttonIndex > (NSInteger)[buttons_ count]) + buttonIndex = [buttons_ count]; + + // Offset upward by one button height all buttons above insertion location. + BookmarkButton* button = nil; // Remember so it can be de-highlighted. + for (NSInteger i = 0; i < buttonIndex; ++i) { + button = [buttons_ objectAtIndex:i]; + // Remember this location in case it's the last button being moved + // which is where the new button will be located. + newButtonFrame = [button frame]; + NSRect buttonFrame = [button frame]; + buttonFrame.origin.y += bookmarks::kBookmarkButtonVerticalSpan; + [button setFrame:buttonFrame]; + } + [[button cell] mouseExited:nil]; // De-highlight. + BookmarkButton* newButton = [self makeButtonForNode:node + frame:newButtonFrame]; + [buttons_ insertObject:newButton atIndex:buttonIndex]; + [mainView_ addSubview:newButton]; + + // Close any child folder(s) which may still be open. + [self closeBookmarkFolder:self]; + + // Prelim height of the window. We'll trim later as needed. + int height = [self windowHeightForButtonCount:[buttons_ count]]; + [self adjustWindowForHeight:height]; +} + +// More code which essentially duplicates that of BookmarkBarController. +// TODO(mrossetti,jrg): http://crbug.com/35966 +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { + DCHECK([urls count] == [titles count]); + BOOL nodesWereAdded = NO; + // Figure out where these new bookmarks nodes are to be added. + BookmarkButton* button = [self buttonForDroppingOnAtPoint:point]; + BookmarkModel* bookmarkModel = [self bookmarkModel]; + const BookmarkNode* destParent = NULL; + int destIndex = 0; + if ([button isFolder]) { + destParent = [button bookmarkNode]; + // Drop it at the end. + destIndex = [button bookmarkNode]->GetChildCount(); + } else { + // Else we're dropping somewhere in the folder, so find the right spot. + destParent = [parentButton_ bookmarkNode]; + destIndex = [self indexForDragToPoint:point]; + // Be careful if the number of buttons != number of nodes. + destIndex += [[parentButton_ cell] startingChildIndex]; + } + + // Create and add the new bookmark nodes. + size_t urlCount = [urls count]; + for (size_t i = 0; i < urlCount; ++i) { + GURL gurl; + const char* string = [[urls objectAtIndex:i] UTF8String]; + if (string) + gurl = GURL(string); + // We only expect to receive valid URLs. + DCHECK(gurl.is_valid()); + if (gurl.is_valid()) { + bookmarkModel->AddURL(destParent, + destIndex++, + base::SysNSStringToUTF16([titles objectAtIndex:i]), + gurl); + nodesWereAdded = YES; + } + } + return nodesWereAdded; +} + +- (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::kBookmarkButtonVerticalSpan; + [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::kBookmarkButtonVerticalSpan; + [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 + BookmarkButton* oldButton = [buttons_ objectAtIndex:buttonIndex]; + NSPoint poofPoint = [oldButton screenLocationForRemoveAnimation]; + + // If a hover-open is pending, cancel it. + if (oldButton == buttonThatMouseIsIn_) { + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + buttonThatMouseIsIn_ = nil; + } + + // Deleting a button causes rearrangement that enables us to lose a + // mouse-exited event. This problem doesn't appear to exist with + // other keep-menu-open options (e.g. add folder). Since the + // showsBorderOnlyWhileMouseInside uses a tracking area, simple + // tricks (e.g. sending an extra mouseExited: to the button) don't + // fix the problem. + // http://crbug.com/54324 + for (NSButton* button in buttons_.get()) { + if ([button showsBorderOnlyWhileMouseInside]) { + [button setShowsBorderOnlyWhileMouseInside:NO]; + [button setShowsBorderOnlyWhileMouseInside:YES]; + } + } + + [oldButton setDelegate:nil]; + [oldButton removeFromSuperview]; + if (animate && !ignoreAnimations_) + 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::kBookmarkButtonVerticalSpan; + [button setFrame:buttonFrame]; + } + // Search for and adjust submenus, if necessary. + NSInteger buttonCount = [buttons_ count]; + if (buttonCount) { + 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)]; + } + } else { + // If all nodes have been removed from this folder then add in the + // 'empty' placeholder button. + NSRect buttonFrame = + NSMakeRect(bookmarks::kBookmarkSubMenuHorizontalPadding, + bookmarks::kBookmarkButtonHeight - + (bookmarks::kBookmarkBarHeight - + bookmarks::kBookmarkVerticalPadding), + bookmarks::kDefaultBookmarkWidth, + (bookmarks::kBookmarkBarHeight - + 2 * bookmarks::kBookmarkVerticalPadding)); + BookmarkButton* button = [self makeButtonForNode:nil + frame:buttonFrame]; + [buttons_ addObject:button]; + [mainView_ addSubview:button]; + buttonCount = 1; + } + + // Propose a height for the window. We'll trim later as needed. + [self adjustWindowForHeight:[self windowHeightForButtonCount:buttonCount]]; +} + +- (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]; +} + +#pragma mark TestingAPI Only + +- (void)setIgnoreAnimations:(BOOL)ignore { + ignoreAnimations_ = ignore; +} + +- (BookmarkButton*)buttonThatMouseIsIn { + return buttonThatMouseIsIn_; +} + +@end // BookmarkBarFolderController diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm new file mode 100644 index 0000000..cae8766 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm @@ -0,0 +1,1552 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/view_resizer_pong.h" +#include "chrome/test/model_test_utils.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +// Add a redirect to make testing easier. +@interface BookmarkBarFolderController(MakeTestingEasier) +- (IBAction)openBookmarkFolderFromButton:(id)sender; +- (void)validateMenuSpacing; +@end + +@implementation BookmarkBarFolderController(MakeTestingEasier) +- (IBAction)openBookmarkFolderFromButton:(id)sender { + [[self folderTarget] openBookmarkFolderFromButton:sender]; +} + +// Utility function to verify that the buttons in this folder are all +// evenly spaced in a progressive manner. +- (void)validateMenuSpacing { + BOOL firstButton = YES; + CGFloat lastVerticalOffset = 0.0; + for (BookmarkButton* button in [self buttons]) { + if (firstButton) { + firstButton = NO; + lastVerticalOffset = [button frame].origin.y; + } else { + CGFloat nextVerticalOffset = [button frame].origin.y; + EXPECT_CGFLOAT_EQ(lastVerticalOffset - + bookmarks::kBookmarkButtonVerticalSpan, + nextVerticalOffset); + lastVerticalOffset = nextVerticalOffset; + } + } +} +@end + +// Don't use a high window level when running unit tests -- it'll +// interfere with anything else you are working on. +// For testing. +@interface BookmarkBarFolderControllerNoLevel : BookmarkBarFolderController +@end + +@implementation BookmarkBarFolderControllerNoLevel +- (void)configureWindowLevel { + // Intentionally empty. +} +@end + +// No window level and the ability to fake the "top left" point of the window. +// For testing. +@interface BookmarkBarFolderControllerLow : BookmarkBarFolderControllerNoLevel { + BOOL realTopLeft_; // Use the real windowTopLeft call? +} +@property (nonatomic) BOOL realTopLeft; +@end + + +@implementation BookmarkBarFolderControllerLow + +@synthesize realTopLeft = realTopLeft_; + +- (NSPoint)windowTopLeftForWidth:(int)width { + return realTopLeft_ ? [super windowTopLeftForWidth:width] : + NSMakePoint(200,200); +} + +@end + + +@interface BookmarkBarFolderControllerPong : BookmarkBarFolderControllerLow { + BOOL childFolderWillShow_; + BOOL childFolderWillClose_; +} +@property (nonatomic, readonly) BOOL childFolderWillShow; +@property (nonatomic, readonly) BOOL childFolderWillClose; +@end + +@implementation BookmarkBarFolderControllerPong +@synthesize childFolderWillShow = childFolderWillShow_; +@synthesize childFolderWillClose = childFolderWillClose_; + +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { + childFolderWillShow_ = YES; +} + +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { + childFolderWillClose_ = YES; +} + +// We don't have a real BookmarkBarController as our parent root so +// we fake this one out. +- (void)closeAllBookmarkFolders { + [self closeBookmarkFolder:self]; +} + +@end + +namespace { +const int kLotsOfNodesCount = 150; +}; + + +// Redirect certain calls so they can be seen by tests. + +@interface BookmarkBarControllerChildFolderRedirect : BookmarkBarController { + BookmarkBarFolderController* childFolderDelegate_; +} +@property (nonatomic, assign) BookmarkBarFolderController* childFolderDelegate; +@end + +@implementation BookmarkBarControllerChildFolderRedirect + +@synthesize childFolderDelegate = childFolderDelegate_; + +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child { + [childFolderDelegate_ childFolderWillShow:child]; +} + +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child { + [childFolderDelegate_ childFolderWillClose:child]; +} + +@end + + +class BookmarkBarFolderControllerTest : public CocoaTest { + public: + BrowserTestHelper helper_; + scoped_nsobject<BookmarkBarControllerChildFolderRedirect> bar_; + const BookmarkNode* folderA_; // owned by model + const BookmarkNode* longTitleNode_; // owned by model + + BookmarkBarFolderControllerTest() { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folderA = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + folderA_ = folderA; + model->AddGroup(parent, parent->GetChildCount(), + ASCIIToUTF16("sibbling group")); + const BookmarkNode* folderB = model->AddGroup(folderA, + folderA->GetChildCount(), + ASCIIToUTF16("subgroup 1")); + model->AddGroup(folderA, + folderA->GetChildCount(), + ASCIIToUTF16("subgroup 2")); + model->AddURL(folderA, folderA->GetChildCount(), ASCIIToUTF16("title a"), + GURL("http://www.google.com/a")); + longTitleNode_ = model->AddURL( + folderA, folderA->GetChildCount(), + ASCIIToUTF16("title super duper long long whoa momma title you betcha"), + GURL("http://www.google.com/b")); + model->AddURL(folderB, folderB->GetChildCount(), ASCIIToUTF16("t"), + GURL("http://www.google.com/c")); + + bar_.reset( + [[BookmarkBarControllerChildFolderRedirect alloc] + initWithBrowser:helper_.browser() + initialWidth:300 + delegate:nil + resizeDelegate:nil]); + [bar_ loaded:model]; + // Make parent frame for bookmark bar then open it. + NSRect frame = [[test_window() contentView] frame]; + frame = NSInsetRect(frame, 100, 200); + NSView* fakeToolbarView = [[[NSView alloc] initWithFrame:frame] + autorelease]; + [[test_window() contentView] addSubview:fakeToolbarView]; + [fakeToolbarView addSubview:[bar_ view]]; + [bar_ setBookmarkBarEnabled:YES]; + } + + // Remove the bookmark with the long title. + void RemoveLongTitleNode() { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + model->Remove(longTitleNode_->GetParent(), + longTitleNode_->GetParent()->IndexOfChild(longTitleNode_)); + } + + // Add LOTS of nodes to our model if needed (e.g. scrolling). + // Returns the number of nodes added. + int AddLotsOfNodes() { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + for (int i = 0; i < kLotsOfNodesCount; i++) { + model->AddURL(folderA_, folderA_->GetChildCount(), + ASCIIToUTF16("repeated title"), + GURL("http://www.google.com/repeated/url")); + } + return kLotsOfNodesCount; + } + + // Return a simple BookmarkBarFolderController. + BookmarkBarFolderControllerPong* SimpleBookmarkBarFolderController() { + BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0]; + BookmarkBarFolderControllerPong* c = + [[BookmarkBarFolderControllerPong alloc] + initWithParentButton:parentButton + parentController:nil + barController:bar_]; + [c window]; // Force nib load. + return c; + } +}; + +TEST_F(BookmarkBarFolderControllerTest, InitCreateAndDelete) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + + // Make sure none of the buttons overlap, that all are inside + // the content frame, and their cells are of the proper class. + NSArray* buttons = [bbfc buttons]; + EXPECT_TRUE([buttons count]); + for (unsigned int i = 0; i < ([buttons count]-1); i++) { + EXPECT_FALSE(NSContainsRect([[buttons objectAtIndex:i] frame], + [[buttons objectAtIndex:i+1] frame])); + } + Class cellClass = [BookmarkBarFolderButtonCell class]; + for (BookmarkButton* button in buttons) { + NSRect r = [[bbfc mainView] convertRect:[button frame] fromView:button]; + // TODO(jrg): remove this adjustment. + NSRect bigger = NSInsetRect([[bbfc mainView] frame], -2, 0); + EXPECT_TRUE(NSContainsRect(bigger, r)); + EXPECT_TRUE([[button cell] isKindOfClass:cellClass]); + } + + // Confirm folder buttons have no tooltip. The important thing + // really is that we insure folders and non-folders are treated + // differently; not sure of any other generic way to do this. + for (BookmarkButton* button in buttons) { + if ([button isFolder]) + EXPECT_FALSE([button toolTip]); + else + EXPECT_TRUE([button toolTip]); + } +} + +// Make sure closing of the window releases the controller. +// (e.g. valgrind shouldn't complain if we do this). +TEST_F(BookmarkBarFolderControllerTest, ReleaseOnClose) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + [bbfc retain]; // stop the scoped_nsobject from doing anything + [[bbfc window] close]; // trigger an autorelease of bbfc.get() +} + +TEST_F(BookmarkBarFolderControllerTest, BasicPosition) { + BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0]; + EXPECT_TRUE(parentButton); + + // If parent is a BookmarkBarController, grow down. + scoped_nsobject<BookmarkBarFolderControllerLow> bbfc; + bbfc.reset([[BookmarkBarFolderControllerLow alloc] + initWithParentButton:parentButton + parentController:nil + barController:bar_]); + [bbfc window]; + [bbfc setRealTopLeft:YES]; + NSPoint pt = [bbfc windowTopLeftForWidth:0]; // screen coords + NSPoint buttonOriginInScreen = + [[parentButton window] + convertBaseToScreen:[parentButton + convertRectToBase:[parentButton frame]].origin]; + // Within margin + EXPECT_LE(abs(pt.x - buttonOriginInScreen.x), + bookmarks::kBookmarkMenuOverlap+1); + EXPECT_LE(abs(pt.y - buttonOriginInScreen.y), + bookmarks::kBookmarkMenuOverlap+1); + + // Make sure we see the window shift left if it spills off the screen + pt = [bbfc windowTopLeftForWidth:0]; + NSPoint shifted = [bbfc windowTopLeftForWidth:9999999]; + EXPECT_LT(shifted.x, pt.x); + + // If parent is a BookmarkBarFolderController, grow right. + scoped_nsobject<BookmarkBarFolderControllerLow> bbfc2; + bbfc2.reset([[BookmarkBarFolderControllerLow alloc] + initWithParentButton:[[bbfc buttons] objectAtIndex:0] + parentController:bbfc.get() + barController:bar_]); + [bbfc2 window]; + [bbfc2 setRealTopLeft:YES]; + pt = [bbfc2 windowTopLeftForWidth:0]; + // We're now overlapping the window a bit. + EXPECT_EQ(pt.x, NSMaxX([[bbfc.get() window] frame]) - + bookmarks::kBookmarkMenuOverlap); +} + +// Confirm we grow right until end of screen, then start growing left +// until end of screen again, then right. +TEST_F(BookmarkBarFolderControllerTest, PositionRightLeftRight) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = parent; + + const int count = 100; + int i; + // Make some super duper deeply nested folders. + for (i=0; i<count; i++) { + folder = model->AddGroup(folder, 0, ASCIIToUTF16("nested folder")); + } + + // Setup initial state for opening all folders. + folder = parent; + BookmarkButton* parentButton = [[bar_ buttons] objectAtIndex:0]; + BookmarkBarFolderController* parentController = nil; + EXPECT_TRUE(parentButton); + + // Open them all. + scoped_nsobject<NSMutableArray> folder_controller_array; + folder_controller_array.reset([[NSMutableArray array] retain]); + for (i=0; i<count; i++) { + BookmarkBarFolderControllerNoLevel* bbfcl = + [[BookmarkBarFolderControllerNoLevel alloc] + initWithParentButton:parentButton + parentController:parentController + barController:bar_]; + [folder_controller_array addObject:bbfcl]; + [bbfcl autorelease]; + [bbfcl window]; + parentController = bbfcl; + parentButton = [[bbfcl buttons] objectAtIndex:0]; + } + + // Make vector of all x positions. + std::vector<CGFloat> leftPositions; + for (i=0; i<count; i++) { + CGFloat x = [[[folder_controller_array objectAtIndex:i] window] + frame].origin.x; + leftPositions.push_back(x); + } + + // Make sure the first few grow right. + for (i=0; i<3; i++) + EXPECT_TRUE(leftPositions[i+1] > leftPositions[i]); + + // Look for the first "grow left". + while (leftPositions[i] > leftPositions[i-1]) + i++; + // Confirm the next few also grow left. + int j; + for (j=i; j<i+3; j++) + EXPECT_TRUE(leftPositions[j+1] < leftPositions[j]); + i = j; + + // Finally, confirm we see a "grow right" once more. + while (leftPositions[i] < leftPositions[i-1]) + i++; + // (No need to EXPECT a final "grow right"; if we didn't find one + // we'd get a C++ array bounds exception). +} + +TEST_F(BookmarkBarFolderControllerTest, DropDestination) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + // Confirm "off the top" and "off the bottom" match no buttons. + NSPoint p = NSMakePoint(NSMidX([[bbfc mainView] frame]), 10000); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:p]); + p = NSMakePoint(NSMidX([[bbfc mainView] frame]), -1); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:p]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:p]); + + // Confirm "right in the center" (give or take a pixel) is a match, + // and confirm "just barely in the button" is not. Anything more + // specific seems likely to be tweaked. We don't loop over all + // buttons because the scroll view makes them not visible. + for (BookmarkButton* button in [bbfc buttons]) { + CGFloat x = NSMidX([button frame]); + CGFloat y = NSMidY([button frame]); + // Somewhere near the center: a match (but only if a folder!) + if ([button isFolder]) { + EXPECT_EQ(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x-1, y+1)]); + EXPECT_EQ(button, + [bbfc buttonForDroppingOnAtPoint:NSMakePoint(x+1, y-1)]); + EXPECT_FALSE([bbfc shouldShowIndicatorShownForPoint:NSMakePoint(x, y)]);; + } else { + // If not a folder we don't drop into it. + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:NSMakePoint(x-1, y+1)]); + EXPECT_FALSE([bbfc buttonForDroppingOnAtPoint:NSMakePoint(x+1, y-1)]); + EXPECT_TRUE([bbfc shouldShowIndicatorShownForPoint:NSMakePoint(x, y)]);; + } + } +} + +TEST_F(BookmarkBarFolderControllerTest, OpenFolder) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + + EXPECT_FALSE([bbfc folderController]); + BookmarkButton* button = [[bbfc buttons] objectAtIndex:0]; + [bbfc openBookmarkFolderFromButton:button]; + id controller = [bbfc folderController]; + EXPECT_TRUE(controller); + EXPECT_EQ([controller parentButton], button); + + // Click the same one --> it gets closed. + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]]; + EXPECT_FALSE([bbfc folderController]); + + // Open a new one --> change. + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:1]]; + EXPECT_NE(controller, [bbfc folderController]); + EXPECT_NE([[bbfc folderController] parentButton], button); + + // Close it --> all gone! + [bbfc closeBookmarkFolder:nil]; + EXPECT_FALSE([bbfc folderController]); +} + +TEST_F(BookmarkBarFolderControllerTest, ChildFolderCallbacks) { + scoped_nsobject<BookmarkBarFolderControllerPong> bbfc; + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + [bar_ setChildFolderDelegate:bbfc.get()]; + + EXPECT_FALSE([bbfc childFolderWillShow]); + [bbfc openBookmarkFolderFromButton:[[bbfc buttons] objectAtIndex:0]]; + EXPECT_TRUE([bbfc childFolderWillShow]); + + EXPECT_FALSE([bbfc childFolderWillClose]); + [bbfc closeBookmarkFolder:nil]; + EXPECT_TRUE([bbfc childFolderWillClose]); + + [bar_ setChildFolderDelegate:nil]; +} + +// Make sure bookmark folders have variable widths. +TEST_F(BookmarkBarFolderControllerTest, ChildFolderWidth) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + [bbfc showWindow:bbfc.get()]; + CGFloat wideWidth = NSWidth([[bbfc window] frame]); + + RemoveLongTitleNode(); + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + CGFloat thinWidth = NSWidth([[bbfc window] frame]); + + // Make sure window size changed as expected. + EXPECT_GT(wideWidth, thinWidth); +} + +// Simple scrolling tests. +TEST_F(BookmarkBarFolderControllerTest, SimpleScroll) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + + int nodecount = AddLotsOfNodes(); + bbfc.reset(SimpleBookmarkBarFolderController()); + EXPECT_TRUE(bbfc.get()); + [bbfc showWindow:bbfc.get()]; + + // Make sure the window fits on the screen. + EXPECT_LT(NSHeight([[bbfc window] frame]), + NSHeight([[NSScreen mainScreen] frame])); + + // Verify the logic used by the scroll arrow code. + EXPECT_TRUE([bbfc canScrollUp]); + EXPECT_FALSE([bbfc canScrollDown]); + + // Scroll it up. Make sure the window has gotten bigger each time. + // Also, for each scroll, make sure our hit test finds a new button + // (to confirm the content area changed). + NSView* savedHit = nil; + for (int i=0; i<3; i++) { + CGFloat height = NSHeight([[bbfc window] frame]); + [bbfc performOneScroll:60]; + EXPECT_GT(NSHeight([[bbfc window] frame]), height); + NSView* hit = [[[bbfc window] contentView] hitTest:NSMakePoint(22, 22)]; + EXPECT_NE(hit, savedHit); + savedHit = hit; + } + + // Keep scrolling up; make sure we never get bigger than the screen. + // Also confirm we never scroll the window off the screen. + bool bothAtOnce = false; + NSRect screenFrame = [[NSScreen mainScreen] frame]; + for (int i = 0; i < nodecount; i++) { + [bbfc performOneScroll:60]; + EXPECT_TRUE(NSContainsRect(screenFrame, + [[bbfc window] frame])); + // Make sure, sometime during our scroll, we have the ability to + // scroll in either direction. + if ([bbfc canScrollUp] && + [bbfc canScrollDown]) + bothAtOnce = true; + } + EXPECT_TRUE(bothAtOnce); + + // Once we've scrolled to the end, our only option should be to scroll back. + EXPECT_FALSE([bbfc canScrollUp]); + EXPECT_TRUE([bbfc canScrollDown]); + + // Now scroll down and make sure the window size does not change. + // Also confirm we never scroll the window off the screen the other + // way. + for (int i=0; i<nodecount+50; i++) { + CGFloat height = NSHeight([[bbfc window] frame]); + [bbfc performOneScroll:-60]; + EXPECT_EQ(height, NSHeight([[bbfc window] frame])); + EXPECT_TRUE(NSContainsRect(screenFrame, + [[bbfc window] frame])); + } +} + +// Folder menu sizing and placementwhile deleting bookmarks and scrolling tests. +TEST_F(BookmarkBarFolderControllerTest, MenuPlacementWhileScrollingDeleting) { + scoped_nsobject<BookmarkBarFolderController> bbfc; + AddLotsOfNodes(); + bbfc.reset(SimpleBookmarkBarFolderController()); + [bbfc showWindow:bbfc.get()]; + NSWindow* menuWindow = [bbfc window]; + BookmarkBarFolderController* folder = [bar_ folderController]; + NSArray* buttons = [folder buttons]; + + // Before scrolling any, delete a bookmark and make sure the window top has + // not moved. Pick a button which is near the top and visible. + CGFloat oldTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]); + BookmarkButton* button = [buttons objectAtIndex:3]; + [folder deleteBookmark:button]; + CGFloat newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]); + EXPECT_CGFLOAT_EQ(oldTop, newTop); + + // Scroll so that both the top and bottom scroll arrows show, make sure + // the top of the window has moved up, then delete a visible button and + // make sure the top has not moved. + oldTop = newTop; + const CGFloat scrollOneBookmark = bookmarks::kBookmarkButtonHeight + + bookmarks::kBookmarkVerticalPadding; + NSUInteger buttonCounter = 0; + NSUInteger extraButtonLimit = 3; + while (![bbfc canScrollDown] || extraButtonLimit > 0) { + [bbfc performOneScroll:scrollOneBookmark]; + ++buttonCounter; + if ([bbfc canScrollDown]) + --extraButtonLimit; + } + newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]); + EXPECT_NE(oldTop, newTop); + oldTop = newTop; + button = [buttons objectAtIndex:buttonCounter + 3]; + [folder deleteBookmark:button]; + newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]); + EXPECT_CGFLOAT_EQ(oldTop, newTop); + + // Scroll so that the top scroll arrow is no longer showing, make sure + // the top of the window has not moved, then delete a visible button and + // make sure the top has not moved. + while ([bbfc canScrollDown]) { + [bbfc performOneScroll:-scrollOneBookmark]; + --buttonCounter; + } + button = [buttons objectAtIndex:buttonCounter + 3]; + [folder deleteBookmark:button]; + newTop = [menuWindow frame].origin.y + NSHeight([menuWindow frame]); + EXPECT_CGFLOAT_EQ(oldTop, newTop); +} + +@interface FakedDragInfo : NSObject { +@public + NSPoint dropLocation_; + NSDragOperation sourceMask_; +} +@property (nonatomic, assign) NSPoint dropLocation; +- (void)setDraggingSourceOperationMask:(NSDragOperation)mask; +@end + +@implementation FakedDragInfo + +@synthesize dropLocation = dropLocation_; + +- (id)init { + if ((self = [super init])) { + dropLocation_ = NSZeroPoint; + sourceMask_ = NSDragOperationMove; + } + return self; +} + +// NSDraggingInfo protocol functions. + +- (id)draggingPasteboard { + return self; +} + +- (id)draggingSource { + return self; +} + +- (NSDragOperation)draggingSourceOperationMask { + return sourceMask_; +} + +- (NSPoint)draggingLocation { + return dropLocation_; +} + +// Other functions. + +- (void)setDraggingSourceOperationMask:(NSDragOperation)mask { + sourceMask_ = mask; +} + +@end + + +class BookmarkBarFolderControllerMenuTest : public CocoaTest { + public: + BrowserTestHelper helper_; + scoped_nsobject<NSView> parent_view_; + scoped_nsobject<ViewResizerPong> resizeDelegate_; + scoped_nsobject<BookmarkBarController> bar_; + + BookmarkBarFolderControllerMenuTest() { + 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]; + } +}; + +TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveBarBookmarkToFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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::string expected_string("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; + CGFloat diff = (bookmarks::kBookmarkBarHeight + + 2*bookmarks::kBookmarkVerticalPadding); + expectedToWindowFrame.origin.y -= diff; + expectedToWindowFrame.size.height += diff; + EXPECT_NSRECT_EQ(expectedToWindowFrame, newToWindowFrame); + + // Check button spacing. + [folderController validateMenuSpacing]; + + // 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(BookmarkBarFolderControllerMenuTest, DragCopyBarBookmarkToFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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::string expected_1("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; + CGFloat diff = (bookmarks::kBookmarkBarHeight + + 2*bookmarks::kBookmarkVerticalPadding); + expectedToWindowFrame.origin.y -= diff; + expectedToWindowFrame.size.height += diff; + 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::string expected_2("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(BookmarkBarFolderControllerMenuTest, DragMoveBarBookmarkToSubfolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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); + [[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::string expected_string("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 button spacing. + [folderController validateMenuSpacing]; + [subfolderController validateMenuSpacing]; + + // 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 + bookmarks::kVisualHeightOffset; + expectedToSubwindowFrame.size.height += + bookmarks::kBookmarkBarHeight + bookmarks::kVisualHeightOffset; + EXPECT_NSRECT_EQ(expectedToSubwindowFrame, newToSubwindowFrame); +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveWithinFolder) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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::string expected_string("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); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DragParentOntoChild) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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)); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DragMoveChildToParent) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("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::string 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::string expected_string("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)); + + // Check button spacing. + [folderController validateMenuSpacing]; + // The window should not have gone away. + EXPECT_TRUE([bar_ folderController]); + // The subfolder should have gone away. + EXPECT_FALSE([folderController folderController]); +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DragWindowResizing) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string + model_string("a b:[ b1 b2 b3 ] reallyReallyLongBookmarkName c "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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::string + expected_string("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(BookmarkBarFolderControllerMenuTest, MoveRemoveAddButtons) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b 2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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_NSEQ(@"2f2b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:2] title]); + EXPECT_EQ(oldDisplayedButtons, [buttons count]); + [folder moveButtonFromIndex:2 toIndex:0]; + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:2] title]); + 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_NSEQ(@"3b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:2] title]); + EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:3] title]); + EXPECT_EQ(oldDisplayedButtons + 1, [buttons count]); + node = root->GetChild(3); + [folder addButtonForNode:node atIndex:-1]; + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f1b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:2] title]); + EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:3] title]); + EXPECT_NSEQ(@"4b", [[buttons objectAtIndex:4] title]); + EXPECT_EQ(oldDisplayedButtons + 2, [buttons count]); + + // Remove a couple of buttons. + [folder removeButton:4 animate:NO]; + [folder removeButton:1 animate:NO]; + EXPECT_NSEQ(@"3b", [[buttons objectAtIndex:0] title]); + EXPECT_NSEQ(@"2f2b", [[buttons objectAtIndex:1] title]); + EXPECT_NSEQ(@"2f3b", [[buttons objectAtIndex:2] title]); + EXPECT_EQ(oldDisplayedButtons, [buttons count]); + + // Check button spacing. + [folder validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, ControllerForNode) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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); +} + +TEST_F(BookmarkBarFolderControllerMenuTest, MenuSizingAndScrollArrows) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2b 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actualModelString = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actualModelString); + + const BookmarkNode* parent = model.GetBookmarkBarNode(); + const BookmarkNode* folder = model.AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("BIG")); + + // Pop open the new folder window and verify it has one (empty) item. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"BIG"]; + [[button target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:button]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSWindow* folderMenu = [folderController window]; + EXPECT_TRUE(folderMenu); + CGFloat expectedHeight = (CGFloat)bookmarks::kBookmarkButtonHeight + + (2*bookmarks::kBookmarkVerticalPadding); + NSRect menuFrame = [folderMenu frame]; + CGFloat menuHeight = NSHeight(menuFrame); + EXPECT_CGFLOAT_EQ(expectedHeight, menuHeight); + EXPECT_FALSE([folderController scrollable]); + + // Now add a real bookmark and reopen. + model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("a"), + GURL("http://a.com/")); + folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + folderMenu = [folderController window]; + EXPECT_TRUE(folderMenu); + menuFrame = [folderMenu frame]; + menuHeight = NSHeight(menuFrame); + EXPECT_CGFLOAT_EQ(expectedHeight, menuHeight); + CGFloat menuWidth = NSWidth(menuFrame); + button = [folderController buttonWithTitleEqualTo:@"a"]; + CGFloat buttonWidth = NSWidth([button frame]); + CGFloat expectedWidth = + buttonWidth + (2 * bookmarks::kBookmarkSubMenuHorizontalPadding); + EXPECT_CGFLOAT_EQ(expectedWidth, menuWidth); + + // Add a wider bookmark and make sure the button widths match. + model.AddURL(folder, folder->GetChildCount(), + ASCIIToUTF16("A really, really long name"), + GURL("http://www.google.com/a")); + EXPECT_LT(menuWidth, NSWidth([folderMenu frame])); + EXPECT_LT(buttonWidth, NSWidth([button frame])); + buttonWidth = NSWidth([button frame]); + BookmarkButton* buttonB = + [folderController buttonWithTitleEqualTo:@"A really, really long name"]; + EXPECT_TRUE(buttonB); + CGFloat buttonWidthB = NSWidth([buttonB frame]); + EXPECT_CGFLOAT_EQ(buttonWidth, buttonWidthB); + // Add a bunch of bookmarks until the window grows no more, then check for + // a scroll down arrow. + CGFloat oldMenuHeight = 0.0; // It just has to be different for first run. + menuHeight = NSHeight([folderMenu frame]); + NSUInteger tripWire = 0; // Prevent a runaway. + while (![folderController scrollable] && ++tripWire < 100) { + model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("B"), + GURL("http://b.com/")); + oldMenuHeight = menuHeight; + menuHeight = NSHeight([folderMenu frame]); + } + EXPECT_TRUE([folderController scrollable]); + EXPECT_TRUE([folderController canScrollUp]); + + // Remove one bookmark and make sure the scroll down arrow has been removed. + // We'll remove the really long node so we can see if the buttons get resized. + menuWidth = NSWidth([folderMenu frame]); + buttonWidth = NSWidth([button frame]); + model.Remove(folder, 1); + EXPECT_FALSE([folderController scrollable]); + EXPECT_FALSE([folderController canScrollUp]); + EXPECT_FALSE([folderController canScrollDown]); + + // Check the size. It should have reduced. + EXPECT_GT(menuWidth, NSWidth([folderMenu frame])); + EXPECT_GT(buttonWidth, NSWidth([button frame])); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +// See http://crbug.com/46101 +TEST_F(BookmarkBarFolderControllerMenuTest, HoverThenDeleteBookmark) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const BookmarkNode* folder = model.AddGroup(root, + root->GetChildCount(), + ASCIIToUTF16("BIG")); + for (int i = 0; i < kLotsOfNodesCount; i++) + model.AddURL(folder, folder->GetChildCount(), ASCIIToUTF16("kid"), + GURL("http://kid.com/smile")); + + // Pop open the new folder window and hover one of its kids. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"BIG"]; + [[button target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:button]; + BookmarkBarFolderController* bbfc = [bar_ folderController]; + NSArray* buttons = [bbfc buttons]; + + // Hover over a button and verify that it is now known. + button = [buttons objectAtIndex:3]; + BookmarkButton* buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn]; + EXPECT_FALSE(buttonThatMouseIsIn); + [bbfc mouseEnteredButton:button event:nil]; + buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn]; + EXPECT_EQ(button, buttonThatMouseIsIn); + + // Delete the bookmark and verify that it is now not known. + model.Remove(folder, 3); + buttonThatMouseIsIn = [bbfc buttonThatMouseIsIn]; + EXPECT_FALSE(buttonThatMouseIsIn); +} + +// Just like a BookmarkBarFolderController but intercedes when providing +// pasteboard drag data. +@interface BookmarkBarFolderControllerDragData : BookmarkBarFolderController { + const BookmarkNode* dragDataNode_; // Weak +} +- (void)setDragDataNode:(const BookmarkNode*)node; +@end + +@implementation BookmarkBarFolderControllerDragData + +- (id)initWithParentButton:(BookmarkButton*)button + parentController:(BookmarkBarFolderController*)parentController + barController:(BookmarkBarController*)barController { + if ((self = [super initWithParentButton:button + parentController:parentController + barController:barController])) { + dragDataNode_ = NULL; + } + return self; +} + +- (void)setDragDataNode:(const BookmarkNode*)node { + dragDataNode_ = node; +} + +- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData { + std::vector<const BookmarkNode*> dragDataNodes; + if(dragDataNode_) { + dragDataNodes.push_back(dragDataNode_); + } + return dragDataNodes; +} + +@end + +TEST_F(BookmarkBarFolderControllerMenuTest, DragBookmarkData) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + const BookmarkNode* other = model.other_node(); + const std::string other_string("O1b O2b O3f:[ O3f1b O3f2f ] " + "O4f:[ O4f1b O4f2f ] 05b "); + model_test_utils::AddNodesFromModelString(model, other, other_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + actual = model_test_utils::ModelStringFromNode(other); + EXPECT_EQ(other_string, actual); + + // Pop open a folder. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"]; + scoped_nsobject<BookmarkBarFolderControllerDragData> folderController; + folderController.reset([[BookmarkBarFolderControllerDragData alloc] + initWithParentButton:button + parentController:nil + barController:bar_]); + BookmarkButton* targetButton = + [folderController buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(targetButton); + + // Gen up some dragging data. + const BookmarkNode* newNode = other->GetChild(2); + [folderController setDragDataNode:newNode]; + scoped_nsobject<FakedDragInfo> dragInfo([[FakedDragInfo alloc] init]); + [dragInfo setDropLocation:[targetButton top]]; + [folderController dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()]; + + // Verify the model. + const std::string expected("1b 2f:[ O3f:[ O3f1b O3f2f ] 2f1b 2f2f:[ 2f2f1b " + "2f2f2b 2f2f3b ] 2f3b ] 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); + + // Now drag over a folder button. + targetButton = [folderController buttonWithTitleEqualTo:@"2f2f"]; + ASSERT_TRUE(targetButton); + newNode = other->GetChild(2); // Should be O4f. + EXPECT_EQ(newNode->GetTitle(), ASCIIToUTF16("O4f")); + [folderController setDragDataNode:newNode]; + [dragInfo setDropLocation:[targetButton center]]; + [folderController dragBookmarkData:(id<NSDraggingInfo>)dragInfo.get()]; + + // Verify the model. + const std::string expectedA("1b 2f:[ O3f:[ O3f1b O3f2f ] 2f1b 2f2f:[ " + "2f2f1b 2f2f2b 2f2f3b O4f:[ O4f1b O4f2f ] ] " + "2f3b ] 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expectedA, actual); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DragBookmarkDataToTrash) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + + const BookmarkNode* folderNode = root->GetChild(1); + int oldFolderChildCount = folderNode->GetChildCount(); + + // Pop open a folder. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"]; + scoped_nsobject<BookmarkBarFolderControllerDragData> folderController; + folderController.reset([[BookmarkBarFolderControllerDragData alloc] + initWithParentButton:button + parentController:nil + barController:bar_]); + + // Drag a button to the trash. + BookmarkButton* buttonToDelete = + [folderController buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(buttonToDelete); + EXPECT_TRUE([folderController canDragBookmarkButtonToTrash:buttonToDelete]); + [folderController didDragBookmarkToTrash:buttonToDelete]; + + // There should be one less button in the folder. + int newFolderChildCount = folderNode->GetChildCount(); + EXPECT_EQ(oldFolderChildCount - 1, newFolderChildCount); + // Verify the model. + const std::string expected("1b 2f:[ 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, AddURLs) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + + // Pop open a folder. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"]; + [[button target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:button]; + BookmarkBarFolderController* folderController = [bar_ folderController]; + EXPECT_TRUE(folderController); + NSArray* buttons = [folderController buttons]; + EXPECT_TRUE(buttons); + + // Remember how many buttons are showing. + int oldDisplayedButtons = [buttons count]; + + BookmarkButton* targetButton = + [folderController buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(targetButton); + + NSArray* urls = [NSArray arrayWithObjects: @"http://www.a.com/", + @"http://www.b.com/", nil]; + NSArray* titles = [NSArray arrayWithObjects: @"SiteA", @"SiteB", nil]; + [folderController addURLs:urls withTitles:titles at:[targetButton top]]; + + // There should two more buttons in the folder. + int newDisplayedButtons = [buttons count]; + EXPECT_EQ(oldDisplayedButtons + 2, newDisplayedButtons); + // Verify the model. + const std::string expected("1b 2f:[ SiteA SiteB 2f1b 2f2f:[ 2f2f1b 2f2f2b " + "2f2f3b ] 2f3b ] 3b 4b "); + actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(expected, actual); + + // Check button spacing. + [folderController validateMenuSpacing]; +} + +TEST_F(BookmarkBarFolderControllerMenuTest, DropPositionIndicator) { + BookmarkModel& model(*helper_.profile()->GetBookmarkModel()); + const BookmarkNode* root = model.GetBookmarkBarNode(); + const std::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b 2f2f3b ] " + "2f3b ] 3b 4b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string actual = model_test_utils::ModelStringFromNode(root); + EXPECT_EQ(model_string, actual); + + // Pop open the folder. + BookmarkButton* button = [bar_ buttonWithTitleEqualTo:@"2f"]; + [[button target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:button]; + BookmarkBarFolderController* folder = [bar_ folderController]; + EXPECT_TRUE(folder); + + // Test a series of points starting at the top of the folder. + const CGFloat yOffset = 0.5 * bookmarks::kBookmarkVerticalPadding; + BookmarkButton* targetButton = [folder buttonWithTitleEqualTo:@"2f1b"]; + ASSERT_TRUE(targetButton); + NSPoint targetPoint = [targetButton top]; + CGFloat pos = [folder indicatorPosForDragToPoint:targetPoint]; + EXPECT_CGFLOAT_EQ(targetPoint.y + yOffset, pos); + pos = [folder indicatorPosForDragToPoint:[targetButton bottom]]; + targetButton = [folder buttonWithTitleEqualTo:@"2f2f"]; + EXPECT_CGFLOAT_EQ([targetButton top].y + yOffset, pos); + pos = [folder indicatorPosForDragToPoint:NSMakePoint(10,0)]; + targetButton = [folder buttonWithTitleEqualTo:@"2f3b"]; + EXPECT_CGFLOAT_EQ([targetButton bottom].y - yOffset, pos); +} + +@interface BookmarkBarControllerNoDelete : BookmarkBarController +- (IBAction)deleteBookmark:(id)sender; +@end + +@implementation BookmarkBarControllerNoDelete +- (IBAction)deleteBookmark:(id)sender { + // NOP +} +@end + +class BookmarkBarFolderControllerClosingTest : public + BookmarkBarFolderControllerMenuTest { + 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::string model_string("1b 2f:[ 2f1b 2f2f:[ 2f2f1b 2f2f2b ] " + "2f3b ] 3b "); + model_test_utils::AddNodesFromModelString(model, root, model_string); + + // Validate initial model. + std::string 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 +// confirmable. +// There is a similar problem with mouseEnteredButton: and +// mouseExitedButton:. diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h new file mode 100644 index 0000000..373e0e6 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h @@ -0,0 +1,78 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" + +// Hover state machine. Encapsulates the hover state for +// BookmarkBarFolderController. +// A strict call order is implied with these calls. It is ONLY valid to make +// the following state transitions: +// From: To: Via: +// closed opening scheduleOpen...: +// opening closed cancelPendingOpen...: or +// open scheduleOpen...: completes. +// open closing scheduleClose...: +// closing open cancelPendingClose...: or +// closed scheduleClose...: completes. +// +@interface BookmarkBarFolderHoverState : NSObject { + @private + // Enumeration of the valid states that the |hoverButton_| member can be in. + // Because the opening and closing of hover views can be done asyncronously + // there are periods where the hover state is in transtion between open and + // closed. During those times of transition the opening or closing operation + // can be cancelled. We serialize the opening and closing of the + // |hoverButton_| using this state information. This serialization is to + // avoid race conditions where one hover button is being opened while another + // is closing. + enum HoverState { + kHoverStateClosed = 0, + kHoverStateOpening = 1, + kHoverStateOpen = 2, + kHoverStateClosing = 3 + }; + + // Like normal menus, hovering over a folder button causes it to + // open. This variable is set when a hover is initiated (but has + // not necessarily fired yet). + scoped_nsobject<BookmarkButton> hoverButton_; + + // We model hover state as a state machine with specific allowable + // transitions. |hoverState_| is the state of this machine at any + // given time. + HoverState hoverState_; +} + +// Designated initializer. +- (id)init; + +// The BookmarkBarFolderHoverState decides when it is appropriate to hide +// and show the button that the BookmarkBarFolderController drags over. +- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button; + +// The BookmarkBarFolderHoverState decides the fate of the hover button +// when the BookmarkBarFolderController's view is exited. +- (void)draggingExited; + +@end + +// Exposing these for unit testing purposes. They are used privately in the +// implementation as well. +@interface BookmarkBarFolderHoverState(PrivateAPI) +// State change APIs. +- (void)scheduleCloseBookmarkFolderOnHoverButton; +- (void)cancelPendingCloseBookmarkFolderOnHoverButton; +- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)hoverButton; +- (void)cancelPendingOpenBookmarkFolderOnHoverButton; +@end + +// Exposing these for unit testing purposes. They are used only in tests. +@interface BookmarkBarFolderHoverState(TestingAPI) +// Accessors and setters for button and hover state. +- (BookmarkButton*)hoverButton; +- (HoverState)hoverState; +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm new file mode 100644 index 0000000..b762bb3 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm @@ -0,0 +1,171 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" + +@interface BookmarkBarFolderHoverState(Private) +- (void)setHoverState:(HoverState)state; +- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button; +- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button; +@end + +@implementation BookmarkBarFolderHoverState + +- (id)init { + if ((self = [super init])) { + hoverState_ = kHoverStateClosed; + } + return self; +} + +- (NSDragOperation)draggingEnteredButton:(BookmarkButton*)button { + if ([button isFolder]) { + if (hoverButton_ == button) { + // CASE A: hoverButton_ == button implies we've dragged over + // the same folder so no need to open or close anything new. + } else if (hoverButton_ && + hoverButton_ != button) { + // CASE B: we have a hoverButton_ but it is different from the new button. + // This implies we've dragged over a new folder, so we'll close the old + // and open the new. + // Note that we only schedule the open or close if we have no other tasks + // currently pending. + + if (hoverState_ == kHoverStateOpen) { + // Close the old. + [self scheduleCloseBookmarkFolderOnHoverButton]; + } else if (hoverState_ == kHoverStateClosed) { + // Open the new. + [self scheduleOpenBookmarkFolderOnHoverButton:button]; + } + } else if (!hoverButton_) { + // CASE C: we don't have a current hoverButton_ but we have dragged onto + // a new folder so we open the new one. + [self scheduleOpenBookmarkFolderOnHoverButton:button]; + } + } else if (!button) { + if (hoverButton_) { + // CASE D: We have a hoverButton_ but we've moved onto an area that + // requires no hover. We close the hoverButton_ in this case. This + // means cancelling if the open is pending (i.e. |kHoverStateOpening|) + // or closing if we don't alrealy have once in progress. + + // Intiate close only if we have not already done so. + if (hoverState_ == kHoverStateOpening) { + // Cancel the pending open. + [self cancelPendingOpenBookmarkFolderOnHoverButton]; + } else if (hoverState_ != kHoverStateClosing) { + // Schedule the close. + [self scheduleCloseBookmarkFolderOnHoverButton]; + } + } else { + // CASE E: We have neither a hoverButton_ nor a new button that requires + // a hover. In this case we do nothing. + } + } + + return NSDragOperationMove; +} + +- (void)draggingExited { + if (hoverButton_) { + if (hoverState_ == kHoverStateOpening) { + [self cancelPendingOpenBookmarkFolderOnHoverButton]; + } else if (hoverState_ == kHoverStateClosing) { + [self cancelPendingCloseBookmarkFolderOnHoverButton]; + } + } +} + +// Schedule close of hover button. Transition to kHoverStateClosing state. +- (void)scheduleCloseBookmarkFolderOnHoverButton { + DCHECK(hoverButton_); + [self setHoverState:kHoverStateClosing]; + [self performSelector:@selector(closeBookmarkFolderOnHoverButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverCloseDelay]; +} + +// Cancel pending hover close. Transition to kHoverStateOpen state. +- (void)cancelPendingCloseBookmarkFolderOnHoverButton { + [self setHoverState:kHoverStateOpen]; + [NSObject + cancelPreviousPerformRequestsWithTarget:self + selector:@selector(closeBookmarkFolderOnHoverButton:) + object:hoverButton_]; +} + +// Schedule open of hover button. Transition to kHoverStateOpening state. +- (void)scheduleOpenBookmarkFolderOnHoverButton:(BookmarkButton*)button { + DCHECK(button); + hoverButton_.reset([button retain]); + [self setHoverState:kHoverStateOpening]; + [self performSelector:@selector(openBookmarkFolderOnHoverButton:) + withObject:hoverButton_ + afterDelay:bookmarks::kDragHoverOpenDelay]; +} + +// Cancel pending hover open. Transition to kHoverStateClosed state. +- (void)cancelPendingOpenBookmarkFolderOnHoverButton { + [self setHoverState:kHoverStateClosed]; + [NSObject + cancelPreviousPerformRequestsWithTarget:self + selector:@selector(openBookmarkFolderOnHoverButton:) + object:hoverButton_]; + hoverButton_.reset(); +} + +// Hover button accessor. For testing only. +- (BookmarkButton*)hoverButton { + return hoverButton_; +} + +// Hover state accessor. For testing only. +- (HoverState)hoverState { + return hoverState_; +} + +// This method encodes the rules of our |hoverButton_| state machine. Only +// specific state transitions are allowable (encoded in the DCHECK). +// Note that there is no state for simultaneously opening and closing. A +// pending open must complete before scheduling a close, and vice versa. And +// it is not possible to make a transition directly from open to closed, and +// vice versa. +- (void)setHoverState:(HoverState)state { + DCHECK( + (hoverState_ == kHoverStateClosed && state == kHoverStateOpening) || + (hoverState_ == kHoverStateOpening && state == kHoverStateClosed) || + (hoverState_ == kHoverStateOpening && state == kHoverStateOpen) || + (hoverState_ == kHoverStateOpen && state == kHoverStateClosing) || + (hoverState_ == kHoverStateClosing && state == kHoverStateOpen) || + (hoverState_ == kHoverStateClosing && state == kHoverStateClosed) + ) << "bad transition: old = " << hoverState_ << " new = " << state; + + hoverState_ = state; +} + +// Called after a delay to close a previously hover-opened folder. +// Note: this method is not meant to be invoked directly, only through +// a delayed call to |scheduleCloseBookmarkFolderOnHoverButton:|. +- (void)closeBookmarkFolderOnHoverButton:(BookmarkButton*)button { + [NSObject + cancelPreviousPerformRequestsWithTarget:self + selector:@selector(closeBookmarkFolderOnHoverButton:) + object:hoverButton_]; + [self setHoverState:kHoverStateClosed]; + [[button target] closeBookmarkFolder:button]; + hoverButton_.reset(); +} + +// Called after a delay to open a new hover folder. +// Note: this method is not meant to be invoked directly, only through +// a delayed call to |scheduleOpenBookmarkFolderOnHoverButton:|. +- (void)openBookmarkFolderOnHoverButton:(BookmarkButton*)button { + [self setHoverState:kHoverStateOpen]; + [[button target] performSelector:@selector(openBookmarkFolderFromButton:) + withObject:button]; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm new file mode 100644 index 0000000..3d0a50f --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm @@ -0,0 +1,77 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/message_loop.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h" +#import "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" + +namespace { + +typedef CocoaTest BookmarkBarFolderHoverStateTest; + +// Hover state machine interface. +// A strict call order is implied with these calls. It is ONLY valid to make +// these specific state transitions. +TEST(BookmarkBarFolderHoverStateTest, HoverState) { + BrowserTestHelper helper; + scoped_nsobject<BookmarkBarFolderHoverState> bbfhs; + bbfhs.reset([[BookmarkBarFolderHoverState alloc] init]); + + // Initial state. + EXPECT_FALSE([bbfhs hoverButton]); + ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]); + + scoped_nsobject<BookmarkButton> button; + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0, 0, 20, 20)]); + + // Test transition from closed to opening. + ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]); + [bbfhs scheduleOpenBookmarkFolderOnHoverButton:button]; + ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]); + + // Test transition from opening to closed (aka cancel open). + [bbfhs cancelPendingOpenBookmarkFolderOnHoverButton]; + ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]); + ASSERT_EQ(nil, [bbfhs hoverButton]); + + // Test transition from closed to opening. + ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]); + [bbfhs scheduleOpenBookmarkFolderOnHoverButton:button]; + ASSERT_EQ(kHoverStateOpening, [bbfhs hoverState]); + + // Test transition from opening to opened. + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + new MessageLoop::QuitTask, + bookmarks::kDragHoverOpenDelay * 1000.0 * 1.5); + MessageLoop::current()->Run(); + ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]); + ASSERT_EQ(button, [bbfhs hoverButton]); + + // Test transition from opening to opened. + [bbfhs scheduleCloseBookmarkFolderOnHoverButton]; + ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]); + + // Test transition from closing to open (aka cancel close). + [bbfhs cancelPendingCloseBookmarkFolderOnHoverButton]; + ASSERT_EQ(kHoverStateOpen, [bbfhs hoverState]); + ASSERT_EQ(button, [bbfhs hoverButton]); + + // Test transition from closing to closed. + [bbfhs scheduleCloseBookmarkFolderOnHoverButton]; + ASSERT_EQ(kHoverStateClosing, [bbfhs hoverState]); + MessageLoop::current()->PostDelayedTask( + FROM_HERE, + new MessageLoop::QuitTask, + bookmarks::kDragHoverCloseDelay * 1000.0 * 1.5); + MessageLoop::current()->Run(); + ASSERT_EQ(kHoverStateClosed, [bbfhs hoverState]); + ASSERT_EQ(nil, [bbfhs hoverButton]); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h new file mode 100644 index 0000000..8f60b8e --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h @@ -0,0 +1,29 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +@protocol BookmarkButtonControllerProtocol; +@class BookmarkBarFolderController; + +// Main content view for a bookmark bar folder "menu" window. This is +// logically similar to a BookmarkBarView but is oriented vertically. +@interface BookmarkBarFolderView : NSView { + @private + BOOL inDrag_; // Are we in the middle of a drag? + BOOL dropIndicatorShown_; + CGFloat dropIndicatorPosition_; // y position + // The following |controller_| is weak; used for testing only. See the imple- + // mentation comment for - (id<BookmarkButtonControllerProtocol>)controller. + BookmarkBarFolderController* controller_; +} +// Return the controller that owns this view. +- (id<BookmarkButtonControllerProtocol>)controller; +@end + +@interface BookmarkBarFolderView() // TestingOrInternalAPI +@property (assign) BOOL dropIndicatorShown; +@property (readonly) CGFloat dropIndicatorPosition; +- (void)setController:(id)controller; +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm new file mode 100644 index 0000000..5a451a8 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm @@ -0,0 +1,204 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h" + +#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h" +#include "chrome/browser/metrics/user_metrics.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "third_party/mozilla/NSPasteboard+Utils.h" + +@implementation BookmarkBarFolderView + +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize dropIndicatorPosition = dropIndicatorPosition_; + +- (void)awakeFromNib { + NSArray* types = [NSArray arrayWithObjects: + NSStringPboardType, + NSHTMLPboardType, + NSURLPboardType, + kBookmarkButtonDragType, + kBookmarkDictionaryListPboardType, + nil]; + [self registerForDraggedTypes:types]; +} + +- (void)dealloc { + [self unregisterDraggedTypes]; + [super dealloc]; +} + +- (id<BookmarkButtonControllerProtocol>)controller { + // When needed for testing, set the local data member |controller_| to + // the test controller. + return controller_ ? controller_ : [[self window] windowController]; +} + +- (void)setController:(id)controller { + controller_ = controller; +} + +- (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. + // TODO(jrg): when throwing it all away and using animations, try + // hard to make a common routine for both. + // http://crbug.com/35966, http://crbug.com/35968 + + // Draw the bookmark-button-dragging drop indicator if necessary. + if (dropIndicatorShown_) { + const CGFloat kBarHeight = 1; + const CGFloat kBarHorizPad = 4; + const CGFloat kBarOpacity = 0.85; + + NSRect uglyBlackBar = + NSMakeRect(kBarHorizPad, dropIndicatorPosition_, + NSWidth([self bounds]) - 2*kBarHorizPad, + kBarHeight); + NSColor* uglyBlackBarColor = [NSColor blackColor]; + [[uglyBlackBarColor colorWithAlphaComponent:kBarOpacity] setFill]; + [[NSBezierPath bezierPathWithRect:uglyBlackBar] fill]; + } +} + +// TODO(mrossetti,jrg): Identical to -[BookmarkBarView +// dragClipboardContainsBookmarks]. http://crbug.com/35966 +// Shim function to assist in unit testing. +- (BOOL)dragClipboardContainsBookmarks { + return bookmark_pasteboard_helper_mac::DragClipboardContainsBookmarks(); +} + +// Virtually identical to [BookmarkBarView draggingEntered:]. +// TODO(jrg): find a way to share code. Lack of multiple inheritance +// makes things more of a pain but there should be no excuse for laziness. +// http://crbug.com/35966 +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + inDrag_ = YES; + if ([[info draggingPasteboard] dataForType:kBookmarkButtonDragType] || + [self dragClipboardContainsBookmarks] || + [[info draggingPasteboard] containsURLData]) { + // Find the position of the drop indicator. + BOOL showIt = [[self controller] + shouldShowIndicatorShownForPoint:[info draggingLocation]]; + if (!showIt) { + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } + } else { + CGFloat y = + [[self controller] + indicatorPosForDragToPoint:[info draggingLocation]]; + + // Need an update if the indicator wasn't previously shown or if it has + // moved. + if (!dropIndicatorShown_ || dropIndicatorPosition_ != y) { + dropIndicatorShown_ = YES; + dropIndicatorPosition_ = y; + [self setNeedsDisplay:YES]; + } + } + + [[self controller] draggingEntered:info]; // allow hover-open to work + return [info draggingSource] ? NSDragOperationMove : NSDragOperationCopy; + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + [[self controller] draggingExited:info]; + + // Regardless of the type of dragging which ended, we need to get rid of the + // drop indicator if one was shown. + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } +} + +- (void)draggingEnded:(id<NSDraggingInfo>)info { + // Awkwardness since views open and close out from under us. + if (inDrag_) { + inDrag_ = NO; + } + + [self draggingExited:info]; +} + +- (BOOL)wantsPeriodicDraggingUpdates { + // TODO(jrg): This should probably return |YES| and the controller should + // slide the existing bookmark buttons interactively to the side to make + // room for the about-to-be-dropped bookmark. + // http://crbug.com/35968 + return NO; +} + +- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info { + // For now it's the same as draggingEntered:. + // TODO(jrg): once we return YES for wantsPeriodicDraggingUpdates, + // this should ping the [self controller] to perform animations. + // http://crbug.com/35968 + return [self draggingEntered:info]; +} + +- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)info { + return YES; +} + +// This code is practically identical to the same function in BookmarkBarView +// with the only difference being how the controller is retrieved. +// TODO(mrossetti,jrg): http://crbug.com/35966 +// Implement NSDraggingDestination protocol method +// performDragOperation: for URLs. +- (BOOL)performDragOperationForURL:(id<NSDraggingInfo>)info { + NSPasteboard* pboard = [info draggingPasteboard]; + DCHECK([pboard containsURLData]); + + NSArray* urls = nil; + NSArray* titles = nil; + [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES]; + + return [[self controller] addURLs:urls + withTitles:titles + at:[info draggingLocation]]; +} + +// This code is practically identical to the same function in BookmarkBarView +// with the only difference being how the controller is retrieved. +// http://crbug.com/35966 +// Implement NSDraggingDestination protocol method +// performDragOperation: for bookmark buttons. +- (BOOL)performDragOperationForBookmarkButton:(id<NSDraggingInfo>)info { + BOOL doDrag = NO; + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + // [info draggingSource] is nil if not the same application. + if (data && [info draggingSource]) { + BookmarkButton* button = nil; + [data getBytes:&button length:sizeof(button)]; + BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); + doDrag = [[self controller] dragButton:button + to:[info draggingLocation] + copy:copy]; + UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_DragEnd")); + } + return doDrag; +} + +- (BOOL)performDragOperation:(id<NSDraggingInfo>)info { + if ([[self controller] dragBookmarkData:info]) + return YES; + NSPasteboard* pboard = [info draggingPasteboard]; + if ([pboard dataForType:kBookmarkButtonDragType] && + [self performDragOperationForBookmarkButton:info]) + return YES; + if ([pboard containsURLData] && [self performDragOperationForURL:info]) + return YES; + return NO; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm new file mode 100644 index 0000000..07aca2b --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm @@ -0,0 +1,211 @@ +// 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 "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/url_drop_target.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" +#import "third_party/mozilla/NSPasteboard+Utils.h" + +namespace { + const CGFloat kFakeIndicatorPos = 7.0; +}; + +// Fake DraggingInfo, fake BookmarkBarController, fake NSPasteboard... +@interface FakeDraggingInfo : NSObject { + @public + BOOL dragButtonToPong_; + BOOL dragURLsPong_; + BOOL dragBookmarkDataPong_; + BOOL dropIndicatorShown_; + BOOL draggingEnteredCalled_; + // Only mock one type of drag data at a time. + NSString* dragDataType_; +} +@property (readwrite) BOOL dropIndicatorShown; +@property (readwrite) BOOL draggingEnteredCalled; +@property (copy) NSString* dragDataType; +@end + +@implementation FakeDraggingInfo + +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize draggingEnteredCalled = draggingEnteredCalled_; +@synthesize dragDataType = dragDataType_; + +- (id)init { + if ((self = [super init])) { + dropIndicatorShown_ = YES; + } + return self; +} + +- (void)dealloc { + [dragDataType_ release]; + [super dealloc]; +} + +- (void)reset { + [dragDataType_ release]; + dragDataType_ = nil; + dragButtonToPong_ = NO; + dragURLsPong_ = NO; + dragBookmarkDataPong_ = NO; + dropIndicatorShown_ = YES; + draggingEnteredCalled_ = NO; +} + +// NSDragInfo mocking functions. + +- (id)draggingPasteboard { + return self; +} + +// So we can look local. +- (id)draggingSource { + return self; +} + +- (NSDragOperation)draggingSourceOperationMask { + return NSDragOperationCopy | NSDragOperationMove; +} + +- (NSPoint)draggingLocation { + return NSMakePoint(10, 10); +} + +// NSPasteboard mocking functions. + +- (BOOL)containsURLData { + NSArray* urlTypes = [URLDropTargetHandler handledDragTypes]; + if (dragDataType_) + return [urlTypes containsObject:dragDataType_]; + return NO; +} + +- (NSData*)dataForType:(NSString*)type { + if (dragDataType_ && [dragDataType_ isEqualToString:type]) + return [NSData data]; // Return something, anything. + return nil; +} + +// Fake a controller for callback ponging + +- (BOOL)dragButton:(BookmarkButton*)button to:(NSPoint)point copy:(BOOL)copy { + dragButtonToPong_ = YES; + return YES; +} + +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { + dragURLsPong_ = YES; + return YES; +} + +- (void)getURLs:(NSArray**)outUrls + andTitles:(NSArray**)outTitles + convertingFilenames:(BOOL)convertFilenames { +} + +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { + dragBookmarkDataPong_ = YES; + return NO; +} + +// Confirm the pongs. + +- (BOOL)dragButtonToPong { + return dragButtonToPong_; +} + +- (BOOL)dragURLsPong { + return dragURLsPong_; +} + +- (BOOL)dragBookmarkDataPong { + return dragBookmarkDataPong_; +} + +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { + return kFakeIndicatorPos; +} + +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return dropIndicatorShown_; +} + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + draggingEnteredCalled_ = YES; + return NSDragOperationNone; +} + +@end + +namespace { + +class BookmarkBarFolderViewTest : public CocoaTest { + public: + virtual void SetUp() { + CocoaTest::SetUp(); + view_.reset([[BookmarkBarFolderView alloc] init]); + } + + scoped_nsobject<BookmarkBarFolderView> view_; +}; + +TEST_F(BookmarkBarFolderViewTest, BookmarkButtonDragAndDrop) { + [view_ awakeFromNib]; + scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]); + [view_ setController:info.get()]; + [info reset]; + + [info setDragDataType:kBookmarkButtonDragType]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([view_ performDragOperation:(id)info.get()]); + EXPECT_TRUE([info dragButtonToPong]); + EXPECT_FALSE([info dragURLsPong]); + EXPECT_TRUE([info dragBookmarkDataPong]); +} + +TEST_F(BookmarkBarFolderViewTest, URLDragAndDrop) { + [view_ awakeFromNib]; + scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]); + [view_ setController:info.get()]; + [info reset]; + + NSArray* dragTypes = [URLDropTargetHandler handledDragTypes]; + for (NSString* type in dragTypes) { + [info setDragDataType:type]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([view_ performDragOperation:(id)info.get()]); + EXPECT_FALSE([info dragButtonToPong]); + EXPECT_TRUE([info dragURLsPong]); + EXPECT_TRUE([info dragBookmarkDataPong]); + [info reset]; + } +} + +TEST_F(BookmarkBarFolderViewTest, BookmarkButtonDropIndicator) { + [view_ awakeFromNib]; + scoped_nsobject<FakeDraggingInfo> info([[FakeDraggingInfo alloc] init]); + [view_ setController:info.get()]; + [info reset]; + + [info setDragDataType:kBookmarkButtonDragType]; + EXPECT_FALSE([info draggingEnteredCalled]); + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([info draggingEnteredCalled]); // Ensure controller pinged. + EXPECT_TRUE([view_ dropIndicatorShown]); + EXPECT_EQ([view_ dropIndicatorPosition], kFakeIndicatorPos); + + [info setDropIndicatorShown:NO]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_FALSE([view_ dropIndicatorShown]); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h new file mode 100644 index 0000000..cd97d9e --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h @@ -0,0 +1,34 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_ +#pragma once + +#import <Cocoa/Cocoa.h> +#import "base/mac/cocoa_protocols.h" +#include "base/scoped_nsobject.h" + + +// Window for a bookmark folder "menu". This menu pops up when you +// click on a bookmark button that represents a folder of bookmarks. +// This window is borderless. +@interface BookmarkBarFolderWindow : NSWindow +@end + +// Content view for the above window. "Stock" other than the drawing +// of rounded corners. Only used in the nib. +@interface BookmarkBarFolderWindowContentView : NSView { + // Arrows to show ability to scroll up and down as needed. + scoped_nsobject<NSImage> arrowUpImage_; + scoped_nsobject<NSImage> arrowDownImage_; +} +@end + +// Scroll view that contains the main view (where the buttons go). +@interface BookmarkBarFolderWindowScrollView : NSScrollView +@end + + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_FOLDER_WINDOW_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm new file mode 100644 index 0000000..65cb664 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm @@ -0,0 +1,136 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" + +#import "base/logging.h" +#include "app/mac/nsimage_cache.h" +#import "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/image_utils.h" +#import "third_party/GTM/AppKit/GTMNSColor+Luminance.h" +#import "third_party/GTM/AppKit/GTMNSBezierPath+RoundRect.h" + + +@implementation BookmarkBarFolderWindow + +- (id)initWithContentRect:(NSRect)contentRect + styleMask:(NSUInteger)windowStyle + backing:(NSBackingStoreType)bufferingType + defer:(BOOL)deferCreation { + if ((self = [super initWithContentRect:contentRect + styleMask:NSBorderlessWindowMask // override + backing:bufferingType + defer:deferCreation])) { + [self setBackgroundColor:[NSColor clearColor]]; + [self setOpaque:NO]; + } + return self; +} + +@end + + +namespace { +// Corner radius for our bookmark bar folder window. +// Copied from bubble_view.mm. +const CGFloat kViewCornerRadius = 4.0; +} + +@implementation BookmarkBarFolderWindowContentView + +- (void)awakeFromNib { + arrowUpImage_.reset( + [app::mac::GetCachedImageWithName(@"menu_overflow_up.pdf") retain]); + arrowDownImage_.reset( + [app::mac::GetCachedImageWithName(@"menu_overflow_down.pdf") retain]); +} + +// Draw the arrows at the top and bottom of the folder window as a +// visual indication that scrolling is possible. We always draw the +// scrolling arrows; when not relevant (e.g. when not scrollable), the +// scroll view overlaps the window and the arrows aren't visible. +- (void)drawScrollArrows:(NSRect)rect { + NSRect visibleRect = [self bounds]; + + // On top + NSRect imageRect = NSZeroRect; + imageRect.size = [arrowUpImage_ size]; + NSRect drawRect = NSOffsetRect( + imageRect, + (NSWidth(visibleRect) - NSWidth(imageRect)) / 2, + NSHeight(visibleRect) - NSHeight(imageRect)); + [arrowUpImage_ drawInRect:drawRect + fromRect:imageRect + operation:NSCompositeSourceOver + fraction:1.0 + neverFlipped:YES]; + + // On bottom + imageRect = NSZeroRect; + imageRect.size = [arrowDownImage_ size]; + drawRect = NSOffsetRect(imageRect, + (NSWidth(visibleRect) - NSWidth(imageRect)) / 2, + 0); + [arrowDownImage_ drawInRect:drawRect + fromRect:imageRect + operation:NSCompositeSourceOver + fraction:1.0 + neverFlipped:YES]; +} + +- (void)drawRect:(NSRect)rect { + NSRect bounds = [self bounds]; + // Like NSMenus, only the bottom corners are rounded. + NSBezierPath* bezier = + [NSBezierPath gtm_bezierPathWithRoundRect:bounds + topLeftCornerRadius:0 + topRightCornerRadius:0 + bottomLeftCornerRadius:kViewCornerRadius + bottomRightCornerRadius:kViewCornerRadius]; + [bezier closePath]; + + // TODO(jrg): share code with info_bubble_view.mm? Or bubble_view.mm? + NSColor* base_color = [NSColor colorWithCalibratedWhite:0.5 alpha:1.0]; + NSColor* startColor = + [base_color gtm_colorAdjustedFor:GTMColorationLightHighlight + faded:YES]; + NSColor* midColor = + [base_color gtm_colorAdjustedFor:GTMColorationLightMidtone + faded:YES]; + NSColor* endColor = + [base_color gtm_colorAdjustedFor:GTMColorationLightShadow + faded:YES]; + NSColor* glowColor = + [base_color gtm_colorAdjustedFor:GTMColorationLightPenumbra + faded:YES]; + + scoped_nsobject<NSGradient> gradient( + [[NSGradient alloc] initWithColorsAndLocations:startColor, 0.0, + midColor, 0.25, + endColor, 0.5, + glowColor, 0.75, + nil]); + [gradient drawInBezierPath:bezier angle:0.0]; + + [self drawScrollArrows:rect]; +} + +@end + + +@implementation BookmarkBarFolderWindowScrollView + +// We want "draw background" of the NSScrollView in the xib to be NOT +// checked. That allows us to round the bottom corners of the folder +// window. However that also allows some scrollWheel: events to leak +// into the NSWindow behind it (even in a different application). +// Better to plug the scroll leak than to round corners for M5. +- (void)scrollWheel:(NSEvent *)theEvent { + DCHECK([[[self window] windowController] + respondsToSelector:@selector(scrollWheel:)]); + [[[self window] windowController] scrollWheel:theEvent]; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm new file mode 100644 index 0000000..7dd6c06 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm @@ -0,0 +1,49 @@ +// 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 "base/scoped_ptr.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h" +#include "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +class BookmarkBarFolderWindowTest : public CocoaTest { +}; + +TEST_F(BookmarkBarFolderWindowTest, Borderless) { + scoped_nsobject<BookmarkBarFolderWindow> window_; + window_.reset([[BookmarkBarFolderWindow alloc] + initWithContentRect:NSMakeRect(0,0,20,20) + styleMask:0 + backing:NSBackingStoreBuffered + defer:NO]); + EXPECT_EQ(NSBorderlessWindowMask, [window_ styleMask]); +} + + +class BookmarkBarFolderWindowContentViewTest : public CocoaTest { + public: + BookmarkBarFolderWindowContentViewTest() { + view_.reset([[BookmarkBarFolderWindowContentView alloc] + initWithFrame:NSMakeRect(0, 0, 100, 100)]); + [[test_window() contentView] addSubview:view_.get()]; + } + scoped_nsobject<BookmarkBarFolderWindowContentView> view_; + scoped_nsobject<BookmarkBarFolderWindowScrollView> scroll_view_; +}; + +TEST_VIEW(BookmarkBarFolderWindowContentViewTest, view_); + + +class BookmarkBarFolderWindowScrollViewTest : public CocoaTest { + public: + BookmarkBarFolderWindowScrollViewTest() { + scroll_view_.reset([[BookmarkBarFolderWindowScrollView alloc] + initWithFrame:NSMakeRect(0, 0, 100, 100)]); + [[test_window() contentView] addSubview:scroll_view_.get()]; + } + scoped_nsobject<BookmarkBarFolderWindowScrollView> scroll_view_; +}; + +TEST_VIEW(BookmarkBarFolderWindowScrollViewTest, scroll_view_); diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h new file mode 100644 index 0000000..c21c75d --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h @@ -0,0 +1,62 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +namespace bookmarks { + +// States for the bookmark bar. +enum VisualState { + kInvalidState = 0, + kHiddenState = 1, + kShowingState = 2, + kDetachedState = 3, +}; + +} // namespace bookmarks + +// The interface for controllers (etc.) which can give information about the +// bookmark bar's state. +@protocol BookmarkBarState + +// Returns YES if the bookmark bar is currently visible (as a normal toolbar or +// as a detached bar on the NTP), NO otherwise. +- (BOOL)isVisible; + +// Returns YES if an animation is currently running, NO otherwise. +- (BOOL)isAnimationRunning; + +// Returns YES if the bookmark bar is in the given state and not in an +// animation, NO otherwise. +- (BOOL)isInState:(bookmarks::VisualState)state; + +// Returns YES if the bookmark bar is animating from the given state (to any +// other state), NO otherwise. +- (BOOL)isAnimatingToState:(bookmarks::VisualState)state; + +// Returns YES if the bookmark bar is animating to the given state (from any +// other state), NO otherwise. +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state; + +// Returns YES if the bookmark bar is animating from the first given state to +// the second given state, NO otherwise. +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState + toState:(bookmarks::VisualState)toState; + +// Returns YES if the bookmark bar is animating between the two given states (in +// either direction), NO otherwise. +- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState + andState:(bookmarks::VisualState)toState; + +// Returns how morphed into the detached bubble the bookmark bar should be (1 = +// completely detached, 0 = normal). +- (CGFloat)detachedMorphProgress; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_STATE_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h new file mode 100644 index 0000000..1942ebd --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h @@ -0,0 +1,44 @@ +// 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. + +// The BookmarkBarToolbarView is responsible for drawing the background of the +// BookmarkBar's toolbar in either of its two display modes - permanently +// attached (slimline with a stroke at the bottom edge) or New Tab Page style +// (padded with a round rect border and the New Tab Page theme behind). + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#import "chrome/browser/ui/cocoa/animatable_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h" + +@class BookmarkBarView; +class TabContents; +class ThemeProvider; + +// An interface to allow mocking of a BookmarkBarController by the +// BookmarkBarToolbarView. +@protocol BookmarkBarToolbarViewController <BookmarkBarState> +// Displaying the bookmark toolbar background in bubble (floating) mode requires +// the size of the currently selected tab to properly calculate where the +// background image is joined. +- (int)currentTabContentsHeight; + +// Current theme provider, passed to the cross platform NtpBackgroundUtil class. +- (ThemeProvider*)themeProvider; + +@end + +@interface BookmarkBarToolbarView : AnimatableView { + @private + // The controller which tells us how we should be drawing (as normal or as a + // floating bar). + IBOutlet id<BookmarkBarToolbarViewController> controller_; +} +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_TOOLBAR_VIEW_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm new file mode 100644 index 0000000..760de17 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm @@ -0,0 +1,135 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" + +#include "app/theme_provider.h" +#include "gfx/rect.h" +#include "chrome/browser/ntp_background_util.h" +#include "chrome/browser/themes/browser_theme_provider.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/themed_window.h" +#include "gfx/canvas_skia_paint.h" + +const CGFloat kBorderRadius = 3.0; + +@interface BookmarkBarToolbarView (Private) +- (void)drawRectAsBubble:(NSRect)rect; +@end + +@implementation BookmarkBarToolbarView + +- (BOOL)isOpaque { + return [controller_ isInState:bookmarks::kDetachedState]; +} + +- (void)drawRect:(NSRect)rect { + if ([controller_ isInState:bookmarks::kDetachedState] || + [controller_ isAnimatingToState:bookmarks::kDetachedState] || + [controller_ isAnimatingFromState:bookmarks::kDetachedState]) { + [self drawRectAsBubble:rect]; + } else { + NSPoint phase = [[self window] themePatternPhase]; + [[NSGraphicsContext currentContext] setPatternPhase:phase]; + [self drawBackground]; + } +} + +- (void)drawRectAsBubble:(NSRect)rect { + // The state of our morph; 1 is total bubble, 0 is the regular bar. We use it + // to morph the bubble to a regular bar (shape and colour). + CGFloat morph = [controller_ detachedMorphProgress]; + + NSRect bounds = [self bounds]; + + ThemeProvider* themeProvider = [controller_ themeProvider]; + if (!themeProvider) + return; + + NSGraphicsContext* context = [NSGraphicsContext currentContext]; + [context saveGraphicsState]; + + // Draw the background. + { + // CanvasSkiaPaint draws to the NSGraphicsContext during its destructor, so + // explicitly scope this. + // + // Paint the entire bookmark bar, even if the damage rect is much smaller + // because PaintBackgroundDetachedMode() assumes that area's origin is + // (0, 0) and that its size is the size of the bookmark bar. + // + // In practice, this sounds worse than it is because redraw time is still + // minimal compared to the pause between frames of animations. We were + // already repainting the rest of the bookmark bar below without setting a + // clip area, anyway. Also, the only time we weren't asked to redraw the + // whole bookmark bar is when the find bar is drawn over it. + gfx::CanvasSkiaPaint canvas(bounds, true); + gfx::Rect area(0, 0, NSWidth(bounds), NSHeight(bounds)); + + NtpBackgroundUtil::PaintBackgroundDetachedMode(themeProvider, &canvas, + area, [controller_ currentTabContentsHeight]); + } + + // Draw our bookmark bar border on top of the background. + NSRect frameRect = + NSMakeRect( + morph * bookmarks::kNTPBookmarkBarPadding, + morph * bookmarks::kNTPBookmarkBarPadding, + NSWidth(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding, + NSHeight(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding); + // Now draw a bezier path with rounded rectangles around the area. + frameRect = NSInsetRect(frameRect, morph * 0.5, morph * 0.5); + NSBezierPath* border = + [NSBezierPath bezierPathWithRoundedRect:frameRect + xRadius:(morph * kBorderRadius) + yRadius:(morph * kBorderRadius)]; + + // Draw the rounded rectangle. + NSColor* toolbarColor = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_TOOLBAR, true); + CGFloat alpha = morph * [toolbarColor alphaComponent]; + [[toolbarColor colorWithAlphaComponent:alpha] set]; // Set with opacity. + [border fill]; + + // Fade in/out the background. + [context saveGraphicsState]; + [border setClip]; + CGContextRef cgContext = (CGContextRef)[context graphicsPort]; + CGContextBeginTransparencyLayer(cgContext, NULL); + CGContextSetAlpha(cgContext, 1 - morph); + [context setPatternPhase:[[self window] themePatternPhase]]; + [self drawBackground]; + CGContextEndTransparencyLayer(cgContext); + [context restoreGraphicsState]; + + // Draw the border of the rounded rectangle. + NSColor* borderColor = themeProvider->GetNSColor( + BrowserThemeProvider::COLOR_TOOLBAR_BUTTON_STROKE, true); + alpha = morph * [borderColor alphaComponent]; + [[borderColor colorWithAlphaComponent:alpha] set]; // Set with opacity. + [border stroke]; + + // Fade in/out the divider. + // TODO(viettrungluu): It's not obvious that this divider lines up exactly + // with |BackgroundGradientView|'s (in fact, it probably doesn't). + NSColor* strokeColor = [self strokeColor]; + alpha = (1 - morph) * [strokeColor alphaComponent]; + [[strokeColor colorWithAlphaComponent:alpha] set]; + NSBezierPath* divider = [NSBezierPath bezierPath]; + NSPoint dividerStart = + NSMakePoint(morph * bookmarks::kNTPBookmarkBarPadding + morph * 0.5, + morph * bookmarks::kNTPBookmarkBarPadding + morph * 0.5); + CGFloat dividerWidth = + NSWidth(bounds) - 2 * morph * bookmarks::kNTPBookmarkBarPadding - 2 * 0.5; + [divider moveToPoint:dividerStart]; + [divider relativeLineToPoint:NSMakePoint(dividerWidth, 0)]; + [divider stroke]; + + // Restore the graphics context. + [context restoreGraphicsState]; +} + +@end // @implementation BookmarkBarToolbarView diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm new file mode 100644 index 0000000..24d971a --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm @@ -0,0 +1,191 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "app/theme_provider.h" +#include "base/scoped_nsobject.h" +#include "chrome/browser/themes/browser_theme_provider.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "grit/theme_resources.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" +#include "third_party/skia/include/core/SkBitmap.h" +#include "third_party/skia/include/core/SkColor.h" + +using ::testing::_; +using ::testing::DoAll; +using ::testing::Return; +using ::testing::SetArgumentPointee; + +// When testing the floating drawing, we need to have a source of theme data. +class MockThemeProvider : public ThemeProvider { + public: + // Cross platform methods + MOCK_METHOD1(Init, void(Profile*)); + MOCK_CONST_METHOD1(GetBitmapNamed, SkBitmap*(int)); + MOCK_CONST_METHOD1(GetColor, SkColor(int)); + MOCK_CONST_METHOD2(GetDisplayProperty, bool(int, int*)); + MOCK_CONST_METHOD0(ShouldUseNativeFrame, bool()); + MOCK_CONST_METHOD1(HasCustomImage, bool(int)); + MOCK_CONST_METHOD1(GetRawData, RefCountedMemory*(int)); + + // OSX stuff + MOCK_CONST_METHOD2(GetNSImageNamed, NSImage*(int, bool)); + MOCK_CONST_METHOD2(GetNSImageColorNamed, NSColor*(int, bool)); + MOCK_CONST_METHOD2(GetNSColor, NSColor*(int, bool)); + MOCK_CONST_METHOD2(GetNSColorTint, NSColor*(int, bool)); + MOCK_CONST_METHOD1(GetNSGradient, NSGradient*(int)); +}; + +// Allows us to inject our fake controller below. +@interface BookmarkBarToolbarView (TestingAPI) +-(void)setController:(id<BookmarkBarToolbarViewController>)controller; +@end + +@implementation BookmarkBarToolbarView (TestingAPI) +-(void)setController:(id<BookmarkBarToolbarViewController>)controller { + controller_ = controller; +} +@end + +// Allows us to control which way the view is rendered. +@interface DrawDetachedBarFakeController : + NSObject<BookmarkBarState, BookmarkBarToolbarViewController> { + @private + int currentTabContentsHeight_; + ThemeProvider* themeProvider_; + bookmarks::VisualState visualState_; +} +@property (nonatomic, assign) int currentTabContentsHeight; +@property (nonatomic, assign) ThemeProvider* themeProvider; +@property (nonatomic, assign) bookmarks::VisualState visualState; + +// |BookmarkBarState| protocol: +- (BOOL)isVisible; +- (BOOL)isAnimationRunning; +- (BOOL)isInState:(bookmarks::VisualState)state; +- (BOOL)isAnimatingToState:(bookmarks::VisualState)state; +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state; +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState + toState:(bookmarks::VisualState)toState; +- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState + andState:(bookmarks::VisualState)toState; +- (CGFloat)detachedMorphProgress; +@end + +@implementation DrawDetachedBarFakeController +@synthesize currentTabContentsHeight = currentTabContentsHeight_; +@synthesize themeProvider = themeProvider_; +@synthesize visualState = visualState_; + +- (id)init { + if ((self = [super init])) { + [self setVisualState:bookmarks::kHiddenState]; + } + return self; +} + +- (BOOL)isVisible { return YES; } +- (BOOL)isAnimationRunning { return NO; } +- (BOOL)isInState:(bookmarks::VisualState)state + { return ([self visualState] == state) ? YES : NO; } +- (BOOL)isAnimatingToState:(bookmarks::VisualState)state { return NO; } +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)state { return NO; } +- (BOOL)isAnimatingFromState:(bookmarks::VisualState)fromState + toState:(bookmarks::VisualState)toState { return NO; } +- (BOOL)isAnimatingBetweenState:(bookmarks::VisualState)fromState + andState:(bookmarks::VisualState)toState { return NO; } +- (CGFloat)detachedMorphProgress { return 1; } +@end + +class BookmarkBarToolbarViewTest : public CocoaTest { + public: + BookmarkBarToolbarViewTest() { + controller_.reset([[DrawDetachedBarFakeController alloc] init]); + NSRect frame = NSMakeRect(0, 0, 400, 40); + scoped_nsobject<BookmarkBarToolbarView> view( + [[BookmarkBarToolbarView alloc] initWithFrame:frame]); + view_ = view.get(); + [[test_window() contentView] addSubview:view_]; + [view_ setController:controller_.get()]; + } + + scoped_nsobject<DrawDetachedBarFakeController> controller_; + BookmarkBarToolbarView* view_; +}; + +TEST_VIEW(BookmarkBarToolbarViewTest, view_) + +// Test drawing (part 1), mostly to ensure nothing leaks or crashes. +TEST_F(BookmarkBarToolbarViewTest, DisplayAsNormalBar) { + [controller_.get() setVisualState:bookmarks::kShowingState]; + [view_ display]; +} + +// Test drawing (part 2), mostly to ensure nothing leaks or crashes. +TEST_F(BookmarkBarToolbarViewTest, DisplayAsDetachedBarWithNoImage) { + [controller_.get() setVisualState:bookmarks::kDetachedState]; + + // Tests where we don't have a background image, only a color. + MockThemeProvider provider; + EXPECT_CALL(provider, GetColor(BrowserThemeProvider::COLOR_NTP_BACKGROUND)) + .WillRepeatedly(Return(SK_ColorWHITE)); + EXPECT_CALL(provider, HasCustomImage(IDR_THEME_NTP_BACKGROUND)) + .WillRepeatedly(Return(false)); + [controller_.get() setThemeProvider:&provider]; + + [view_ display]; +} + +// Actions used in DisplayAsDetachedBarWithBgImage. +ACTION(SetBackgroundTiling) { + *arg1 = BrowserThemeProvider::NO_REPEAT; + return true; +} + +ACTION(SetAlignLeft) { + *arg1 = BrowserThemeProvider::ALIGN_LEFT; + return true; +} + +// Test drawing (part 3), mostly to ensure nothing leaks or crashes. +TEST_F(BookmarkBarToolbarViewTest, DisplayAsDetachedBarWithBgImage) { + [controller_.get() setVisualState:bookmarks::kDetachedState]; + + // Tests where we have a background image, with positioning information. + MockThemeProvider provider; + + // Advertise having an image. + EXPECT_CALL(provider, GetColor(BrowserThemeProvider::COLOR_NTP_BACKGROUND)) + .WillRepeatedly(Return(SK_ColorRED)); + EXPECT_CALL(provider, HasCustomImage(IDR_THEME_NTP_BACKGROUND)) + .WillRepeatedly(Return(true)); + + // Return the correct tiling/alignment information. + EXPECT_CALL(provider, + GetDisplayProperty(BrowserThemeProvider::NTP_BACKGROUND_TILING, _)) + .WillRepeatedly(SetBackgroundTiling()); + EXPECT_CALL(provider, + GetDisplayProperty(BrowserThemeProvider::NTP_BACKGROUND_ALIGNMENT, _)) + .WillRepeatedly(SetAlignLeft()); + + // Create a dummy bitmap full of not-red to blit with. + SkBitmap fake_bg; + fake_bg.setConfig(SkBitmap::kARGB_8888_Config, 800, 800); + fake_bg.allocPixels(); + fake_bg.eraseColor(SK_ColorGREEN); + EXPECT_CALL(provider, GetBitmapNamed(IDR_THEME_NTP_BACKGROUND)) + .WillRepeatedly(Return(&fake_bg)); + + [controller_.get() setThemeProvider:&provider]; + [controller_.get() setCurrentTabContentsHeight:200]; + + [view_ display]; +} + +// TODO(viettrungluu): write more unit tests, especially after my refactoring. diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h new file mode 100644 index 0000000..d0221b2 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h @@ -0,0 +1,57 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_ +#pragma once + +#import <Foundation/Foundation.h> + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/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 bottom of the button in the base coordinate system of the +// containing window. +- (NSPoint)bottom; + +// Return the center-left point of the button in the base coordinate system +// of the containing window. +- (NSPoint)left; + +// Return the center-right point of the button in the base coordinate system +// of the containing window. +- (NSPoint)right; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_UNITTEST_HELPER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm new file mode 100644 index 0000000..7cddec4 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm @@ -0,0 +1,81 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/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)bottom { + NSRect frame = [self frame]; + NSPoint bottom = NSMakePoint(NSMidX(frame), NSMinY(frame)); + bottom = [[self superview] convertPoint:bottom toView:nil]; + return bottom; +} + +- (NSPoint)left { + NSRect frame = [self frame]; + NSPoint left = NSMakePoint(NSMinX(frame), NSMidY(frame)); + left = [[self superview] convertPoint:left toView:nil]; + return left; +} + +- (NSPoint)right { + NSRect frame = [self frame]; + NSPoint right = NSMakePoint(NSMaxX(frame), NSMidY(frame)); + right = [[self superview] convertPoint:right toView:nil]; + return right; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h new file mode 100644 index 0000000..abcbdf0 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h @@ -0,0 +1,41 @@ +// 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. +// +// A simple custom NSView for the bookmark bar used to prevent clicking and +// dragging from moving the browser window. + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#import "chrome/browser/ui/cocoa/background_gradient_view.h" + +@class BookmarkBarController; + +@interface BookmarkBarView : BackgroundGradientView { + @private + BOOL dropIndicatorShown_; + CGFloat dropIndicatorPosition_; // x position + + IBOutlet BookmarkBarController* controller_; + IBOutlet NSTextField* noItemTextfield_; + IBOutlet NSButton* importBookmarksButton_; + NSView* noItemContainer_; +} +- (NSTextField*)noItemTextfield; +- (NSButton*)importBookmarksButton; +- (BookmarkBarController*)controller; + +@property (nonatomic, assign) IBOutlet NSView* noItemContainer; +@end + +@interface BookmarkBarView() // TestingOrInternalAPI +@property (nonatomic, readonly) BOOL dropIndicatorShown; +@property (nonatomic, readonly) CGFloat dropIndicatorPosition; +- (void)setController:(id)controller; +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BAR_VIEW_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm new file mode 100644 index 0000000..5083367 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm @@ -0,0 +1,259 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" + +#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h" +#include "chrome/browser/metrics/user_metrics.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "chrome/browser/ui/cocoa/themed_window.h" +#import "chrome/browser/ui/cocoa/view_id_util.h" +#import "chrome/browser/themes/browser_theme_provider.h" +#import "third_party/mozilla/NSPasteboard+Utils.h" + +@interface BookmarkBarView (Private) +- (void)themeDidChangeNotification:(NSNotification*)aNotification; +- (void)updateTheme:(ThemeProvider*)themeProvider; +@end + +@implementation BookmarkBarView + +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize dropIndicatorPosition = dropIndicatorPosition_; +@synthesize noItemContainer = noItemContainer_; + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + // This probably isn't strictly necessary, but can't hurt. + [self unregisterDraggedTypes]; + [super dealloc]; + + // To be clear, our controller_ is an IBOutlet and owns us, so we + // don't deallocate it explicitly. It is owned by the browser + // window controller, so gets deleted with a browser window is + // closed. +} + +- (void)awakeFromNib { + NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; + [defaultCenter addObserver:self + selector:@selector(themeDidChangeNotification:) + name:kBrowserThemeDidChangeNotification + object:nil]; + + DCHECK(controller_) << "Expected this to be hooked up via Interface Builder"; + NSArray* types = [NSArray arrayWithObjects: + NSStringPboardType, + NSHTMLPboardType, + NSURLPboardType, + kBookmarkButtonDragType, + kBookmarkDictionaryListPboardType, + nil]; + [self registerForDraggedTypes:types]; +} + +// We need the theme to color the bookmark buttons properly. But our +// controller desn't have access to it until it's placed in the view +// hierarchy. This is the spot where we close the loop. +- (void)viewWillMoveToWindow:(NSWindow*)window { + ThemeProvider* themeProvider = [window themeProvider]; + [self updateTheme:themeProvider]; + [controller_ updateTheme:themeProvider]; +} + +- (void)viewDidMoveToWindow { + [controller_ viewDidMoveToWindow]; +} + +// Called after the current theme has changed. +- (void)themeDidChangeNotification:(NSNotification*)aNotification { + ThemeProvider* themeProvider = + static_cast<ThemeProvider*>([[aNotification object] pointerValue]); + [self updateTheme:themeProvider]; +} + +// Adapt appearance to the current theme. Called after theme changes and before +// this is shown for the first time. +- (void)updateTheme:(ThemeProvider*)themeProvider { + if (!themeProvider) + return; + + NSColor* color = + themeProvider->GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, + true); + [noItemTextfield_ setTextColor:color]; +} + +// Mouse down events on the bookmark bar should not allow dragging the parent +// window around. +- (BOOL)mouseDownCanMoveWindow { + return NO; +} + +-(NSTextField*)noItemTextfield { + return noItemTextfield_; +} + +-(NSButton*)importBookmarksButton { + return importBookmarksButton_; +} + +- (BookmarkBarController*)controller { + return controller_; +} + +-(void)drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + // Draw the bookmark-button-dragging drop indicator if necessary. + if (dropIndicatorShown_) { + const CGFloat kBarWidth = 1; + const CGFloat kBarHalfWidth = kBarWidth / 2.0; + const CGFloat kBarVertPad = 4; + const CGFloat kBarOpacity = 0.85; + + // Prevent the indicator from being clipped on the left. + CGFloat xLeft = MAX(dropIndicatorPosition_ - kBarHalfWidth, 0); + + NSRect uglyBlackBar = + NSMakeRect(xLeft, kBarVertPad, + kBarWidth, NSHeight([self bounds]) - 2 * kBarVertPad); + NSColor* uglyBlackBarColor = [[self window] themeProvider]-> + GetNSColor(BrowserThemeProvider::COLOR_BOOKMARK_TEXT, true); + [[uglyBlackBarColor colorWithAlphaComponent:kBarOpacity] setFill]; + [[NSBezierPath bezierPathWithRect:uglyBlackBar] fill]; + } +} + +// Shim function to assist in unit testing. +- (BOOL)dragClipboardContainsBookmarks { + return bookmark_pasteboard_helper_mac::DragClipboardContainsBookmarks(); +} + +// NSDraggingDestination methods + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + if ([[info draggingPasteboard] dataForType:kBookmarkButtonDragType] || + [self dragClipboardContainsBookmarks] || + [[info draggingPasteboard] containsURLData]) { + // We only show the drop indicator if we're not in a position to + // perform a hover-open since it doesn't make sense to do both. + BOOL showIt = [controller_ shouldShowIndicatorShownForPoint: + [info draggingLocation]]; + if (!showIt) { + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } + } else { + CGFloat x = + [controller_ indicatorPosForDragToPoint:[info draggingLocation]]; + // Need an update if the indicator wasn't previously shown or if it has + // moved. + if (!dropIndicatorShown_ || dropIndicatorPosition_ != x) { + dropIndicatorShown_ = YES; + dropIndicatorPosition_ = x; + [self setNeedsDisplay:YES]; + } + } + + [controller_ draggingEntered:info]; // allow hover-open to work. + return [info draggingSource] ? NSDragOperationMove : NSDragOperationCopy; + } + return NSDragOperationNone; +} + +- (void)draggingExited:(id<NSDraggingInfo>)info { + // Regardless of the type of dragging which ended, we need to get rid of the + // drop indicator if one was shown. + if (dropIndicatorShown_) { + dropIndicatorShown_ = NO; + [self setNeedsDisplay:YES]; + } +} + +- (void)draggingEnded:(id<NSDraggingInfo>)info { + // For now, we just call |-draggingExited:|. + [self draggingExited:info]; +} + +- (BOOL)wantsPeriodicDraggingUpdates { + // TODO(port): This should probably return |YES| and the controller should + // slide the existing bookmark buttons interactively to the side to make + // room for the about-to-be-dropped bookmark. + return NO; +} + +- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info { + // For now it's the same as draggingEntered:. + // TODO(jrg): once we return YES for wantsPeriodicDraggingUpdates, + // this should ping the controller_ to perform animations. + return [self draggingEntered:info]; +} + +- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)info { + return YES; +} + +// Implement NSDraggingDestination protocol method +// performDragOperation: for URLs. +- (BOOL)performDragOperationForURL:(id<NSDraggingInfo>)info { + NSPasteboard* pboard = [info draggingPasteboard]; + DCHECK([pboard containsURLData]); + + NSArray* urls = nil; + NSArray* titles = nil; + [pboard getURLs:&urls andTitles:&titles convertingFilenames:YES]; + + return [controller_ addURLs:urls + withTitles:titles + at:[info draggingLocation]]; +} + +// Implement NSDraggingDestination protocol method +// performDragOperation: for bookmark buttons. +- (BOOL)performDragOperationForBookmarkButton:(id<NSDraggingInfo>)info { + BOOL rtn = NO; + NSData* data = [[info draggingPasteboard] + dataForType:kBookmarkButtonDragType]; + // [info draggingSource] is nil if not the same application. + if (data && [info draggingSource]) { + BookmarkButton* button = nil; + [data getBytes:&button length:sizeof(button)]; + BOOL copy = !([info draggingSourceOperationMask] & NSDragOperationMove); + rtn = [controller_ dragButton:button + to:[info draggingLocation] + copy:copy]; + UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragEnd")); + } + return rtn; +} + +- (BOOL)performDragOperation:(id<NSDraggingInfo>)info { + if ([controller_ dragBookmarkData:info]) + return YES; + NSPasteboard* pboard = [info draggingPasteboard]; + if ([pboard dataForType:kBookmarkButtonDragType]) { + if ([self performDragOperationForBookmarkButton:info]) + return YES; + // Fall through.... + } + if ([pboard containsURLData]) { + if ([self performDragOperationForURL:info]) + return YES; + } + return NO; +} + +- (void)setController:(id)controller { + controller_ = controller; +} + +- (ViewID)viewID { + return VIEW_ID_BOOKMARK_BAR; +} + +@end // @implementation BookmarkBarView diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm new file mode 100644 index 0000000..c847a61 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm @@ -0,0 +1,215 @@ +// 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 "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/url_drop_target.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" +#import "third_party/mozilla/NSPasteboard+Utils.h" + +namespace { + const CGFloat kFakeIndicatorPos = 7.0; +}; + +// Fake DraggingInfo, fake BookmarkBarController, fake NSPasteboard... +@interface FakeBookmarkDraggingInfo : NSObject { + @public + BOOL dragButtonToPong_; + BOOL dragURLsPong_; + BOOL dragBookmarkDataPong_; + BOOL dropIndicatorShown_; + BOOL draggingEnteredCalled_; + // Only mock one type of drag data at a time. + NSString* dragDataType_; +} +@property (nonatomic) BOOL dropIndicatorShown; +@property (nonatomic) BOOL draggingEnteredCalled; +@property (nonatomic, copy) NSString* dragDataType; +@end + +@implementation FakeBookmarkDraggingInfo + +@synthesize dropIndicatorShown = dropIndicatorShown_; +@synthesize draggingEnteredCalled = draggingEnteredCalled_; +@synthesize dragDataType = dragDataType_; + +- (id)init { + if ((self = [super init])) { + dropIndicatorShown_ = YES; + } + return self; +} + +- (void)dealloc { + [dragDataType_ release]; + [super dealloc]; +} + +- (void)reset { + [dragDataType_ release]; + dragDataType_ = nil; + dragButtonToPong_ = NO; + dragURLsPong_ = NO; + dragBookmarkDataPong_ = NO; + dropIndicatorShown_ = YES; + draggingEnteredCalled_ = NO; +} + +// NSDragInfo mocking functions. + +- (id)draggingPasteboard { + return self; +} + +// So we can look local. +- (id)draggingSource { + return self; +} + +- (NSDragOperation)draggingSourceOperationMask { + return NSDragOperationCopy | NSDragOperationMove; +} + +- (NSPoint)draggingLocation { + return NSMakePoint(10, 10); +} + +// NSPasteboard mocking functions. + +- (BOOL)containsURLData { + NSArray* urlTypes = [URLDropTargetHandler handledDragTypes]; + if (dragDataType_) + return [urlTypes containsObject:dragDataType_]; + return NO; +} + +- (NSData*)dataForType:(NSString*)type { + if (dragDataType_ && [dragDataType_ isEqualToString:type]) + return [NSData data]; // Return something, anything. + return nil; +} + +// Fake a controller for callback ponging + +- (BOOL)dragButton:(BookmarkButton*)button to:(NSPoint)point copy:(BOOL)copy { + dragButtonToPong_ = YES; + return YES; +} + +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point { + dragURLsPong_ = YES; + return YES; +} + +- (void)getURLs:(NSArray**)outUrls + andTitles:(NSArray**)outTitles + convertingFilenames:(BOOL)convertFilenames { +} + +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info { + dragBookmarkDataPong_ = YES; + return NO; +} + +// Confirm the pongs. + +- (BOOL)dragButtonToPong { + return dragButtonToPong_; +} + +- (BOOL)dragURLsPong { + return dragURLsPong_; +} + +- (BOOL)dragBookmarkDataPong { + return dragBookmarkDataPong_; +} + +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point { + return kFakeIndicatorPos; +} + +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point { + return dropIndicatorShown_; +} + +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info { + draggingEnteredCalled_ = YES; + return NSDragOperationNone; +} + +@end + +namespace { + +class BookmarkBarViewTest : public CocoaTest { + public: + virtual void SetUp() { + CocoaTest::SetUp(); + view_.reset([[BookmarkBarView alloc] init]); + } + + scoped_nsobject<BookmarkBarView> view_; +}; + +TEST_F(BookmarkBarViewTest, CanDragWindow) { + EXPECT_FALSE([view_ mouseDownCanMoveWindow]); +} + +TEST_F(BookmarkBarViewTest, BookmarkButtonDragAndDrop) { + scoped_nsobject<FakeBookmarkDraggingInfo> + info([[FakeBookmarkDraggingInfo alloc] init]); + [view_ setController:info.get()]; + [info reset]; + + [info setDragDataType:kBookmarkButtonDragType]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([view_ performDragOperation:(id)info.get()]); + EXPECT_TRUE([info dragButtonToPong]); + EXPECT_FALSE([info dragURLsPong]); + EXPECT_TRUE([info dragBookmarkDataPong]); +} + +TEST_F(BookmarkBarViewTest, URLDragAndDrop) { + scoped_nsobject<FakeBookmarkDraggingInfo> + info([[FakeBookmarkDraggingInfo alloc] init]); + [view_ setController:info.get()]; + [info reset]; + + NSArray* dragTypes = [URLDropTargetHandler handledDragTypes]; + for (NSString* type in dragTypes) { + [info setDragDataType:type]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([view_ performDragOperation:(id)info.get()]); + EXPECT_FALSE([info dragButtonToPong]); + EXPECT_TRUE([info dragURLsPong]); + EXPECT_TRUE([info dragBookmarkDataPong]); + [info reset]; + } +} + +TEST_F(BookmarkBarViewTest, BookmarkButtonDropIndicator) { + scoped_nsobject<FakeBookmarkDraggingInfo> + info([[FakeBookmarkDraggingInfo alloc] init]); + [view_ setController:info.get()]; + + [info reset]; + [info setDragDataType:kBookmarkButtonDragType]; + EXPECT_FALSE([info draggingEnteredCalled]); + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_TRUE([info draggingEnteredCalled]); // Ensure controller pinged. + EXPECT_TRUE([view_ dropIndicatorShown]); + EXPECT_EQ([view_ dropIndicatorPosition], kFakeIndicatorPos); + + [info setDropIndicatorShown:NO]; + EXPECT_EQ([view_ draggingEntered:(id)info.get()], NSDragOperationMove); + EXPECT_FALSE([view_ dropIndicatorShown]); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h new file mode 100644 index 0000000..3ed8f7b --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h @@ -0,0 +1,81 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#import "base/mac/cocoa_protocols.h" +#include "base/scoped_ptr.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h" + +class BookmarkBubbleNotificationBridge; +class BookmarkModel; +class BookmarkNode; +@class BookmarkBubbleController; +@class InfoBubbleView; + + +// Controller for the bookmark bubble. The bookmark bubble is a +// bubble that pops up when clicking on the STAR next to the URL to +// add or remove it as a bookmark. This bubble allows for editing of +// the bookmark in various ways (name, folder, etc.) +@interface BookmarkBubbleController : NSWindowController<NSWindowDelegate> { + @private + NSWindow* parentWindow_; // weak + + // Both weak; owned by the current browser's profile + BookmarkModel* model_; // weak + const BookmarkNode* node_; // weak + + // The bookmark node whose button we asked to pulse. + const BookmarkNode* pulsingBookmarkNode_; // weak + + BOOL alreadyBookmarked_; + + // Ping me when the bookmark model changes out from under us. + scoped_ptr<BookmarkModelObserverForCocoa> bookmark_observer_; + + // Ping me when other Chrome things change out from under us. + scoped_ptr<BookmarkBubbleNotificationBridge> chrome_observer_; + + IBOutlet NSTextField* bigTitle_; // "Bookmark" or "Bookmark Added!" + IBOutlet NSTextField* nameTextField_; + IBOutlet NSPopUpButton* folderPopUpButton_; + IBOutlet InfoBubbleView* bubble_; // to set arrow position +} + +@property (readonly, nonatomic) const BookmarkNode* node; + +// |node| is the bookmark node we edit in this bubble. +// |alreadyBookmarked| tells us if the node was bookmarked before the +// user clicked on the star. (if NO, this is a brand new bookmark). +// The owner of this object is responsible for showing the bubble if +// it desires it to be visible on the screen. It is not shown by the +// init routine. Closing of the window happens implicitly on dealloc. +- (id)initWithParentWindow:(NSWindow*)parentWindow + model:(BookmarkModel*)model + node:(const BookmarkNode*)node + alreadyBookmarked:(BOOL)alreadyBookmarked; + +// Actions for buttons in the dialog. +- (IBAction)ok:(id)sender; +- (IBAction)remove:(id)sender; +- (IBAction)cancel:(id)sender; + +// These actions send a -editBookmarkNode: action up the responder chain. +- (IBAction)edit:(id)sender; +- (IBAction)folderChanged:(id)sender; + +@end + + +// Exposed only for unit testing. +@interface BookmarkBubbleController(ExposedForUnitTesting) +- (void)addFolderNodes:(const BookmarkNode*)parent + toPopUpButton:(NSPopUpButton*)button + indentation:(int)indentation; +- (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent; +- (void)setParentFolderSelection:(const BookmarkNode*)parent; ++ (NSString*)chooseAnotherFolderString; +- (NSPopUpButton*)folderPopUpButton; +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm new file mode 100644 index 0000000..ae66081 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm @@ -0,0 +1,428 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h" + +#include "app/l10n_util_mac.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" // TODO(viettrungluu): remove +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/metrics/user_metrics.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/info_bubble_view.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" +#include "chrome/common/notification_service.h" +#include "grit/generated_resources.h" + + +// Simple class to watch for tab creation/destruction and close the bubble. +// Bridge between Chrome-style notifications and ObjC-style notifications. +class BookmarkBubbleNotificationBridge : public NotificationObserver { + public: + BookmarkBubbleNotificationBridge(BookmarkBubbleController* controller, + SEL selector); + virtual ~BookmarkBubbleNotificationBridge() {} + void Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details); + private: + NotificationRegistrar registrar_; + BookmarkBubbleController* controller_; // weak; owns us. + SEL selector_; // SEL sent to controller_ on notification. +}; + +BookmarkBubbleNotificationBridge::BookmarkBubbleNotificationBridge( + BookmarkBubbleController* controller, SEL selector) + : controller_(controller), selector_(selector) { + // registrar_ will automatically RemoveAll() when destroyed so we + // don't need to do so explicitly. + registrar_.Add(this, NotificationType::TAB_CONTENTS_CONNECTED, + NotificationService::AllSources()); + registrar_.Add(this, NotificationType::TAB_CLOSED, + NotificationService::AllSources()); +} + +// At this time all notifications instigate the same behavior (go +// away) so we don't bother checking which notification came in. +void BookmarkBubbleNotificationBridge::Observe( + NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + [controller_ performSelector:selector_ withObject:controller_]; +} + + +// An object to represent the ChooseAnotherFolder item in the pop up. +@interface ChooseAnotherFolder : NSObject +@end + +@implementation ChooseAnotherFolder +@end + +@interface BookmarkBubbleController (PrivateAPI) +- (void)updateBookmarkNode; +- (void)fillInFolderList; +- (void)parentWindowWillClose:(NSNotification*)notification; +@end + +@implementation BookmarkBubbleController + +@synthesize node = node_; + ++ (id)chooseAnotherFolderObject { + // Singleton object to act as a representedObject for the "choose another + // folder" item in the pop up. + static ChooseAnotherFolder* object = nil; + if (!object) { + object = [[ChooseAnotherFolder alloc] init]; + } + return object; +} + +- (id)initWithParentWindow:(NSWindow*)parentWindow + model:(BookmarkModel*)model + node:(const BookmarkNode*)node + alreadyBookmarked:(BOOL)alreadyBookmarked { + NSString* nibPath = + [mac_util::MainAppBundle() pathForResource:@"BookmarkBubble" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibPath owner:self])) { + parentWindow_ = parentWindow; + model_ = model; + node_ = node; + alreadyBookmarked_ = alreadyBookmarked; + + // Watch to see if the parent window closes, and if so, close this one. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + [center addObserver:self + selector:@selector(parentWindowWillClose:) + name:NSWindowWillCloseNotification + object:parentWindow_]; + } + return self; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +// If this is a new bookmark somewhere visible (e.g. on the bookmark +// bar), pulse it. Else, call ourself recursively with our parent +// until we find something visible to pulse. +- (void)startPulsingBookmarkButton:(const BookmarkNode*)node { + while (node) { + if ((node->GetParent() == model_->GetBookmarkBarNode()) || + (node == model_->other_node())) { + pulsingBookmarkNode_ = node; + NSValue *value = [NSValue valueWithPointer:node]; + NSDictionary *dict = [NSDictionary + dictionaryWithObjectsAndKeys:value, + bookmark_button::kBookmarkKey, + [NSNumber numberWithBool:YES], + bookmark_button::kBookmarkPulseFlagKey, + nil]; + [[NSNotificationCenter defaultCenter] + postNotificationName:bookmark_button::kPulseBookmarkButtonNotification + object:self + userInfo:dict]; + return; + } + node = node->GetParent(); + } +} + +- (void)stopPulsingBookmarkButton { + if (!pulsingBookmarkNode_) + return; + NSValue *value = [NSValue valueWithPointer:pulsingBookmarkNode_]; + pulsingBookmarkNode_ = NULL; + NSDictionary *dict = [NSDictionary + dictionaryWithObjectsAndKeys:value, + bookmark_button::kBookmarkKey, + [NSNumber numberWithBool:NO], + bookmark_button::kBookmarkPulseFlagKey, + nil]; + [[NSNotificationCenter defaultCenter] + postNotificationName:bookmark_button::kPulseBookmarkButtonNotification + object:self + userInfo:dict]; +} + +// Close the bookmark bubble without changing anything. Unlike a +// typical dialog's OK/Cancel, where Cancel is "do nothing", all +// buttons on the bubble have the capacity to change the bookmark +// model. This is an IBOutlet-looking entry point to remove the +// dialog without touching the model. +- (void)dismissWithoutEditing:(id)sender { + [self close]; +} + +- (void)parentWindowWillClose:(NSNotification*)notification { + [self close]; +} + +- (void)windowWillClose:(NSNotification*)notification { + // We caught a close so we don't need to watch for the parent closing. + [[NSNotificationCenter defaultCenter] removeObserver:self]; + bookmark_observer_.reset(NULL); + chrome_observer_.reset(NULL); + [self stopPulsingBookmarkButton]; + [self autorelease]; +} + +// We want this to be a child of a browser window. addChildWindow: +// (called from this function) will bring the window on-screen; +// unfortunately, [NSWindowController showWindow:] will also bring it +// on-screen (but will cause unexpected changes to the window's +// position). We cannot have an addChildWindow: and a subsequent +// showWindow:. Thus, we have our own version. +- (void)showWindow:(id)sender { + BrowserWindowController* bwc = + [BrowserWindowController browserWindowControllerForWindow:parentWindow_]; + [bwc lockBarVisibilityForOwner:self withAnimation:NO delay:NO]; + NSWindow* window = [self window]; // completes nib load + [bubble_ setArrowLocation:info_bubble::kTopRight]; + // Insure decent positioning even in the absence of a browser controller, + // which will occur for some unit tests. + NSPoint arrowtip = bwc ? [bwc bookmarkBubblePoint] : + NSMakePoint([window frame].size.width, [window frame].size.height); + NSPoint origin = [parentWindow_ convertBaseToScreen:arrowtip]; + NSPoint bubbleArrowtip = [bubble_ arrowTip]; + bubbleArrowtip = [bubble_ convertPoint:bubbleArrowtip toView:nil]; + origin.y -= bubbleArrowtip.y; + origin.x -= bubbleArrowtip.x; + [window setFrameOrigin:origin]; + [parentWindow_ addChildWindow:window ordered:NSWindowAbove]; + // Default is IDS_BOOMARK_BUBBLE_PAGE_BOOKMARK; "Bookmark". + // If adding for the 1st time the string becomes "Bookmark Added!" + if (!alreadyBookmarked_) { + NSString* title = + l10n_util::GetNSString(IDS_BOOMARK_BUBBLE_PAGE_BOOKMARKED); + [bigTitle_ setStringValue:title]; + } + + [self fillInFolderList]; + + // Ping me when things change out from under us. Unlike a normal + // dialog, the bookmark bubble's cancel: means "don't add this as a + // bookmark", not "cancel editing". We must take extra care to not + // touch the bookmark in this selector. + bookmark_observer_.reset(new BookmarkModelObserverForCocoa( + node_, model_, + self, + @selector(dismissWithoutEditing:))); + chrome_observer_.reset(new BookmarkBubbleNotificationBridge( + self, @selector(dismissWithoutEditing:))); + + // Pulse something interesting on the bookmark bar. + [self startPulsingBookmarkButton:node_]; + + [window makeKeyAndOrderFront:self]; +} + +- (void)close { + [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] + releaseBarVisibilityForOwner:self withAnimation:YES delay:NO]; + [parentWindow_ removeChildWindow:[self window]]; + + // If you quit while the bubble is open, sometimes we get a + // DidResignKey before we get our parent's WindowWillClose and + // sometimes not. We protect against a multiple close (or reference + // to parentWindow_ at a bad time) by clearing it out once we're + // done, and by removing ourself from future notifications. + [[NSNotificationCenter defaultCenter] + removeObserver:self + name:NSWindowWillCloseNotification + object:parentWindow_]; + parentWindow_ = nil; + + [super close]; +} + +// Shows the bookmark editor sheet for more advanced editing. +- (void)showEditor { + [self ok:self]; + // Send the action up through the responder chain. + [NSApp sendAction:@selector(editBookmarkNode:) to:nil from:self]; +} + +- (IBAction)edit:(id)sender { + UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Edit"), + model_->profile()); + [self showEditor]; +} + +- (IBAction)ok:(id)sender { + [self stopPulsingBookmarkButton]; // before parent changes + [self updateBookmarkNode]; + [self close]; +} + +// By implementing this, ESC causes the window to go away. If clicking the +// star was what prompted this bubble to appear (i.e., not already bookmarked), +// remove the bookmark. +- (IBAction)cancel:(id)sender { + if (!alreadyBookmarked_) { + // |-remove:| calls |-close| so don't do it. + [self remove:sender]; + } else { + [self ok:sender]; + } +} + +- (IBAction)remove:(id)sender { + [self stopPulsingBookmarkButton]; + // TODO(viettrungluu): get rid of conversion and utf_string_conversions.h. + model_->SetURLStarred(node_->GetURL(), node_->GetTitle(), false); + UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_Unstar"), + model_->profile()); + node_ = NULL; // no longer valid + [self ok:sender]; +} + +// The controller is the target of the pop up button box action so it can +// handle when "choose another folder" was picked. +- (IBAction)folderChanged:(id)sender { + DCHECK([sender isEqual:folderPopUpButton_]); + // It is possible that due to model change our parent window has been closed + // but the popup is still showing and able to notify the controller of a + // folder change. We ignore the sender in this case. + if (!parentWindow_) + return; + NSMenuItem* selected = [folderPopUpButton_ selectedItem]; + ChooseAnotherFolder* chooseItem = [[self class] chooseAnotherFolderObject]; + if ([[selected representedObject] isEqual:chooseItem]) { + UserMetrics::RecordAction( + UserMetricsAction("BookmarkBubble_EditFromCombobox"), + model_->profile()); + [self showEditor]; + } +} + +// The controller is the delegate of the window so it receives did resign key +// notifications. When key is resigned mirror Windows behavior and close the +// window. +- (void)windowDidResignKey:(NSNotification*)notification { + NSWindow* window = [self window]; + DCHECK_EQ([notification object], window); + if ([window isVisible]) { + // If the window isn't visible, it is already closed, and this notification + // has been sent as part of the closing operation, so no need to close. + [self ok:self]; + } +} + +// Look at the dialog; if the user has changed anything, update the +// bookmark node to reflect this. +- (void)updateBookmarkNode { + if (!node_) return; + + // First the title... + NSString* oldTitle = base::SysUTF16ToNSString(node_->GetTitle()); + NSString* newTitle = [nameTextField_ stringValue]; + if (![oldTitle isEqual:newTitle]) { + model_->SetTitle(node_, base::SysNSStringToUTF16(newTitle)); + UserMetrics::RecordAction( + UserMetricsAction("BookmarkBubble_ChangeTitleInBubble"), + model_->profile()); + } + // Then the parent folder. + const BookmarkNode* oldParent = node_->GetParent(); + NSMenuItem* selectedItem = [folderPopUpButton_ selectedItem]; + id representedObject = [selectedItem representedObject]; + if ([representedObject isEqual:[[self class] chooseAnotherFolderObject]]) { + // "Choose another folder..." + return; + } + const BookmarkNode* newParent = + static_cast<const BookmarkNode*>([representedObject pointerValue]); + DCHECK(newParent); + if (oldParent != newParent) { + int index = newParent->GetChildCount(); + model_->Move(node_, newParent, index); + UserMetrics::RecordAction(UserMetricsAction("BookmarkBubble_ChangeParent"), + model_->profile()); + } +} + +// Fill in all information related to the folder pop up button. +- (void)fillInFolderList { + [nameTextField_ setStringValue:base::SysUTF16ToNSString(node_->GetTitle())]; + DCHECK([folderPopUpButton_ numberOfItems] == 0); + [self addFolderNodes:model_->root_node() + toPopUpButton:folderPopUpButton_ + indentation:0]; + NSMenu* menu = [folderPopUpButton_ menu]; + NSString* title = [[self class] chooseAnotherFolderString]; + NSMenuItem *item = [menu addItemWithTitle:title + action:NULL + keyEquivalent:@""]; + ChooseAnotherFolder* obj = [[self class] chooseAnotherFolderObject]; + [item setRepresentedObject:obj]; + // Finally, select the current parent. + NSValue* parentValue = [NSValue valueWithPointer:node_->GetParent()]; + NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue]; + [folderPopUpButton_ selectItemAtIndex:idx]; +} + +@end // BookmarkBubbleController + + +@implementation BookmarkBubbleController(ExposedForUnitTesting) + ++ (NSString*)chooseAnotherFolderString { + return l10n_util::GetNSStringWithFixup( + IDS_BOOMARK_BUBBLE_CHOOSER_ANOTHER_FOLDER); +} + +// For the given folder node, walk the tree and add folder names to +// the given pop up button. +- (void)addFolderNodes:(const BookmarkNode*)parent + toPopUpButton:(NSPopUpButton*)button + indentation:(int)indentation { + if (!model_->is_root(parent)) { + NSString* title = base::SysUTF16ToNSString(parent->GetTitle()); + NSMenu* menu = [button menu]; + NSMenuItem* item = [menu addItemWithTitle:title + action:NULL + keyEquivalent:@""]; + [item setRepresentedObject:[NSValue valueWithPointer:parent]]; + [item setIndentationLevel:indentation]; + ++indentation; + } + for (int i = 0; i < parent->GetChildCount(); i++) { + const BookmarkNode* child = parent->GetChild(i); + if (child->is_folder()) + [self addFolderNodes:child + toPopUpButton:button + indentation:indentation]; + } +} + +- (void)setTitle:(NSString*)title parentFolder:(const BookmarkNode*)parent { + [nameTextField_ setStringValue:title]; + [self setParentFolderSelection:parent]; +} + +// Pick a specific parent node in the selection by finding the right +// pop up button index. +- (void)setParentFolderSelection:(const BookmarkNode*)parent { + // Expectation: There is a parent mapping for all items in the + // folderPopUpButton except the last one ("Choose another folder..."). + NSMenu* menu = [folderPopUpButton_ menu]; + NSValue* parentValue = [NSValue valueWithPointer:parent]; + NSInteger idx = [menu indexOfItemWithRepresentedObject:parentValue]; + DCHECK(idx != -1); + [folderPopUpButton_ selectItemAtIndex:idx]; +} + +- (NSPopUpButton*)folderPopUpButton { + return folderPopUpButton_; +} + +@end // implementation BookmarkBubbleController(ExposedForUnitTesting) diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm new file mode 100644 index 0000000..ef3a47a --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm @@ -0,0 +1,490 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#include "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/info_bubble_window.h" +#include "chrome/common/notification_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +// Watch for bookmark pulse notifications so we can confirm they were sent. +@interface BookmarkPulseObserver : NSObject { + int notifications_; +} +@property (assign, nonatomic) int notifications; +@end + + +@implementation BookmarkPulseObserver + +@synthesize notifications = notifications_; + +- (id)init { + if ((self = [super init])) { + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(pulseBookmarkNotification:) + name:bookmark_button::kPulseBookmarkButtonNotification + object:nil]; + } + return self; +} + +- (void)pulseBookmarkNotification:(NSNotificationCenter *)notification { + notifications_++; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [super dealloc]; +} + +@end + + +namespace { + +class BookmarkBubbleControllerTest : public CocoaTest { + public: + static int edits_; + BrowserTestHelper helper_; + BookmarkBubbleController* controller_; + + BookmarkBubbleControllerTest() : controller_(nil) { + edits_ = 0; + } + + virtual void TearDown() { + [controller_ close]; + CocoaTest::TearDown(); + } + + // Returns a controller but ownership not transferred. + // Only one of these will be valid at a time. + BookmarkBubbleController* ControllerForNode(const BookmarkNode* node) { + if (controller_ && !IsWindowClosing()) { + [controller_ close]; + controller_ = nil; + } + controller_ = [[BookmarkBubbleController alloc] + initWithParentWindow:test_window() + model:helper_.profile()->GetBookmarkModel() + node:node + alreadyBookmarked:YES]; + EXPECT_TRUE([controller_ window]); + // The window must be gone or we'll fail a unit test with windows left open. + [static_cast<InfoBubbleWindow*>([controller_ window]) setDelayOnClose:NO]; + [controller_ showWindow:nil]; + return controller_; + } + + BookmarkModel* GetBookmarkModel() { + return helper_.profile()->GetBookmarkModel(); + } + + bool IsWindowClosing() { + return [static_cast<InfoBubbleWindow*>([controller_ window]) isClosing]; + } +}; + +// static +int BookmarkBubbleControllerTest::edits_; + +// Confirm basics about the bubble window (e.g. that it is inside the +// parent window) +TEST_F(BookmarkBubbleControllerTest, TestBubbleWindow) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + GURL("http://www.google.com")); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + NSWindow* window = [controller window]; + EXPECT_TRUE(window); + EXPECT_TRUE(NSContainsRect([test_window() frame], + [window frame])); +} + +// Test that we can handle closing the parent window +TEST_F(BookmarkBubbleControllerTest, TestClosingParentWindow) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + GURL("http://www.google.com")); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + NSWindow* window = [controller window]; + EXPECT_TRUE(window); + base::mac::ScopedNSAutoreleasePool pool; + [test_window() performClose:NSApp]; +} + + +// Confirm population of folder list +TEST_F(BookmarkBubbleControllerTest, TestFillInFolder) { + // Create some folders, including a nested folder + BookmarkModel* model = GetBookmarkModel(); + EXPECT_TRUE(model); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("one")); + EXPECT_TRUE(node1); + const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1, + ASCIIToUTF16("two")); + EXPECT_TRUE(node2); + const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2, + ASCIIToUTF16("three")); + EXPECT_TRUE(node3); + const BookmarkNode* node4 = model->AddGroup(node2, 0, ASCIIToUTF16("sub")); + EXPECT_TRUE(node4); + const BookmarkNode* node5 = model->AddURL(node1, 0, ASCIIToUTF16("title1"), + GURL("http://www.google.com")); + EXPECT_TRUE(node5); + const BookmarkNode* node6 = model->AddURL(node3, 0, ASCIIToUTF16("title2"), + GURL("http://www.google.com")); + EXPECT_TRUE(node6); + const BookmarkNode* node7 = model->AddURL(node4, 0, ASCIIToUTF16("title3"), + GURL("http://www.google.com/reader")); + EXPECT_TRUE(node7); + + BookmarkBubbleController* controller = ControllerForNode(node4); + EXPECT_TRUE(controller); + + NSArray* titles = + [[[controller folderPopUpButton] itemArray] valueForKey:@"title"]; + EXPECT_TRUE([titles containsObject:@"one"]); + EXPECT_TRUE([titles containsObject:@"two"]); + EXPECT_TRUE([titles containsObject:@"three"]); + EXPECT_TRUE([titles containsObject:@"sub"]); + EXPECT_FALSE([titles containsObject:@"title1"]); + EXPECT_FALSE([titles containsObject:@"title2"]); +} + +// Confirm ability to handle folders with blank name. +TEST_F(BookmarkBubbleControllerTest, TestFolderWithBlankName) { + // Create some folders, including a nested folder + BookmarkModel* model = GetBookmarkModel(); + EXPECT_TRUE(model); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("one")); + EXPECT_TRUE(node1); + const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1, + ASCIIToUTF16("")); + EXPECT_TRUE(node2); + const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2, + ASCIIToUTF16("three")); + EXPECT_TRUE(node3); + const BookmarkNode* node2_1 = model->AddURL(node2, 0, ASCIIToUTF16("title1"), + GURL("http://www.google.com")); + EXPECT_TRUE(node2_1); + + BookmarkBubbleController* controller = ControllerForNode(node1); + EXPECT_TRUE(controller); + + // One of the items should be blank and its node should be node2. + NSArray* items = [[controller folderPopUpButton] itemArray]; + EXPECT_GT([items count], 4U); + BOOL blankFolderFound = NO; + for (NSMenuItem* item in [[controller folderPopUpButton] itemArray]) { + if ([[item title] length] == 0 && + static_cast<const BookmarkNode*>([[item representedObject] + pointerValue]) == node2) { + blankFolderFound = YES; + break; + } + } + EXPECT_TRUE(blankFolderFound); +} + + +// Click on edit; bubble gets closed. +TEST_F(BookmarkBubbleControllerTest, TestEdit) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + GURL("http://www.google.com")); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + EXPECT_EQ(edits_, 0); + EXPECT_FALSE(IsWindowClosing()); + [controller edit:controller]; + EXPECT_EQ(edits_, 1); + EXPECT_TRUE(IsWindowClosing()); +} + +// CallClose; bubble gets closed. +// Also confirm pulse notifications get sent. +TEST_F(BookmarkBubbleControllerTest, TestClose) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL( + model->GetBookmarkBarNode(), 0, ASCIIToUTF16("Bookie markie title"), + GURL("http://www.google.com")); + EXPECT_EQ(edits_, 0); + + scoped_nsobject<BookmarkPulseObserver> observer([[BookmarkPulseObserver alloc] + init]); + EXPECT_EQ([observer notifications], 0); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + EXPECT_FALSE(IsWindowClosing()); + EXPECT_EQ([observer notifications], 1); + [controller ok:controller]; + EXPECT_EQ(edits_, 0); + EXPECT_TRUE(IsWindowClosing()); + EXPECT_EQ([observer notifications], 2); +} + +// User changes title and parent folder in the UI +TEST_F(BookmarkBubbleControllerTest, TestUserEdit) { + BookmarkModel* model = GetBookmarkModel(); + EXPECT_TRUE(model); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + const BookmarkNode* node = model->AddURL(bookmarkBarNode, + 0, + ASCIIToUTF16("short-title"), + GURL("http://www.google.com")); + const BookmarkNode* grandma = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("grandma")); + EXPECT_TRUE(grandma); + const BookmarkNode* grandpa = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("grandpa")); + EXPECT_TRUE(grandpa); + + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + // simulate a user edit + [controller setTitle:@"oops" parentFolder:grandma]; + [controller edit:controller]; + + // Make sure bookmark has changed + EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("oops")); + EXPECT_EQ(node->GetParent()->GetTitle(), ASCIIToUTF16("grandma")); +} + +// Confirm happiness with parent nodes that have the same name. +TEST_F(BookmarkBubbleControllerTest, TestNewParentSameName) { + BookmarkModel* model = GetBookmarkModel(); + EXPECT_TRUE(model); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + for (int i=0; i<2; i++) { + const BookmarkNode* node = model->AddURL(bookmarkBarNode, + 0, + ASCIIToUTF16("short-title"), + GURL("http://www.google.com")); + EXPECT_TRUE(node); + const BookmarkNode* group = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("NAME")); + EXPECT_TRUE(group); + group = model->AddGroup(bookmarkBarNode, 0, ASCIIToUTF16("NAME")); + EXPECT_TRUE(group); + group = model->AddGroup(bookmarkBarNode, 0, ASCIIToUTF16("NAME")); + EXPECT_TRUE(group); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + // simulate a user edit + [controller setParentFolderSelection:bookmarkBarNode->GetChild(i)]; + [controller edit:controller]; + + // Make sure bookmark has changed, and that the parent is what we + // expect. This proves nobody did searching based on name. + EXPECT_EQ(node->GetParent(), bookmarkBarNode->GetChild(i)); + } +} + +// Confirm happiness with nodes with the same Name +TEST_F(BookmarkBubbleControllerTest, TestDuplicateNodeNames) { + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("NAME")); + EXPECT_TRUE(node1); + const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("NAME")); + EXPECT_TRUE(node2); + BookmarkBubbleController* controller = ControllerForNode(bookmarkBarNode); + EXPECT_TRUE(controller); + + NSPopUpButton* button = [controller folderPopUpButton]; + [controller setParentFolderSelection:node1]; + NSMenuItem* item = [button selectedItem]; + id itemObject = [item representedObject]; + EXPECT_NSEQ([NSValue valueWithPointer:node1], itemObject); + [controller setParentFolderSelection:node2]; + item = [button selectedItem]; + itemObject = [item representedObject]; + EXPECT_NSEQ([NSValue valueWithPointer:node2], itemObject); +} + +// Click the "remove" button +TEST_F(BookmarkBubbleControllerTest, TestRemove) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + gurl); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + EXPECT_TRUE(model->IsBookmarked(gurl)); + + [controller remove:controller]; + EXPECT_FALSE(model->IsBookmarked(gurl)); + EXPECT_TRUE(IsWindowClosing()); +} + +// Confirm picking "choose another folder" caused edit: to be called. +TEST_F(BookmarkBubbleControllerTest, PopUpSelectionChanged) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, ASCIIToUTF16("super-title"), + gurl); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + NSPopUpButton* button = [controller folderPopUpButton]; + [button selectItemWithTitle:[[controller class] chooseAnotherFolderString]]; + EXPECT_EQ(edits_, 0); + [button sendAction:[button action] to:[button target]]; + EXPECT_EQ(edits_, 1); +} + +// Create a controller that simulates the bookmark just now being created by +// the user clicking the star, then sending the "cancel" command to represent +// them pressing escape. The bookmark should not be there. +TEST_F(BookmarkBubbleControllerTest, EscapeRemovesNewBookmark) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + gurl); + BookmarkBubbleController* controller = + [[BookmarkBubbleController alloc] + initWithParentWindow:test_window() + model:helper_.profile()->GetBookmarkModel() + node:node + alreadyBookmarked:NO]; // The last param is the key difference. + EXPECT_TRUE([controller window]); + // Calls release on controller. + [controller cancel:nil]; + EXPECT_FALSE(model->IsBookmarked(gurl)); +} + +// Create a controller where the bookmark already existed prior to clicking +// the star and test that sending a cancel command doesn't change the state +// of the bookmark. +TEST_F(BookmarkBubbleControllerTest, EscapeDoesntTouchExistingBookmark) { + BookmarkModel* model = GetBookmarkModel(); + GURL gurl("http://www.google.com"); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + gurl); + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + + [(id)controller cancel:nil]; + EXPECT_TRUE(model->IsBookmarked(gurl)); +} + +// Confirm indentation of items in pop-up menu +TEST_F(BookmarkBubbleControllerTest, TestMenuIndentation) { + // Create some folders, including a nested folder + BookmarkModel* model = GetBookmarkModel(); + EXPECT_TRUE(model); + const BookmarkNode* bookmarkBarNode = model->GetBookmarkBarNode(); + EXPECT_TRUE(bookmarkBarNode); + const BookmarkNode* node1 = model->AddGroup(bookmarkBarNode, 0, + ASCIIToUTF16("one")); + EXPECT_TRUE(node1); + const BookmarkNode* node2 = model->AddGroup(bookmarkBarNode, 1, + ASCIIToUTF16("two")); + EXPECT_TRUE(node2); + const BookmarkNode* node2_1 = model->AddGroup(node2, 0, + ASCIIToUTF16("two dot one")); + EXPECT_TRUE(node2_1); + const BookmarkNode* node3 = model->AddGroup(bookmarkBarNode, 2, + ASCIIToUTF16("three")); + EXPECT_TRUE(node3); + + BookmarkBubbleController* controller = ControllerForNode(node1); + EXPECT_TRUE(controller); + + // Compare the menu item indents against expectations. + static const int kExpectedIndent[] = {0, 1, 1, 2, 1, 0}; + NSArray* items = [[controller folderPopUpButton] itemArray]; + ASSERT_GE([items count], 6U); + for(int itemNo = 0; itemNo < 6; itemNo++) { + NSMenuItem* item = [items objectAtIndex:itemNo]; + EXPECT_EQ(kExpectedIndent[itemNo], [item indentationLevel]) + << "Unexpected indent for menu item #" << itemNo; + } +} + +// Confirm bubble goes away when a new tab is created. +TEST_F(BookmarkBubbleControllerTest, BubbleGoesAwayOnNewTab) { + + BookmarkModel* model = GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, + ASCIIToUTF16("Bookie markie title"), + GURL("http://www.google.com")); + EXPECT_EQ(edits_, 0); + + BookmarkBubbleController* controller = ControllerForNode(node); + EXPECT_TRUE(controller); + EXPECT_FALSE(IsWindowClosing()); + + // We can't actually create a new tab here, e.g. + // helper_.browser()->AddTabWithURL(...); + // Many of our browser objects (Browser, Profile, RequestContext) + // are "just enough" to run tests without being complete. Instead + // we fake the notification that would be triggered by a tab + // creation. + NotificationService::current()->Notify( + NotificationType::TAB_CONTENTS_CONNECTED, + Source<TabContentsDelegate>(NULL), + Details<TabContents>(NULL)); + + // Confirm bubble going bye-bye. + EXPECT_TRUE(IsWindowClosing()); +} + + +} // namespace + +@implementation NSApplication (BookmarkBubbleUnitTest) +// Add handler for the editBookmarkNode: action to NSApp for testing purposes. +// Normally this would be sent up the responder tree correctly, but since +// tests run in the background, key window and main window are never set on +// NSApplication. Adding it to NSApplication directly removes the need for +// worrying about what the current window with focus is. +- (void)editBookmarkNode:(id)sender { + EXPECT_TRUE([sender respondsToSelector:@selector(node)]); + BookmarkBubbleControllerTest::edits_++; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h new file mode 100644 index 0000000..0bea5a5 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.h @@ -0,0 +1,243 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> +#include <vector> +#import "chrome/browser/ui/cocoa/draggable_button.h" +#include "webkit/glue/window_open_disposition.h" + +@class BookmarkBarFolderController; +@class BookmarkButton; +struct BookmarkNodeData; +class BookmarkModel; +class BookmarkNode; +@class BrowserWindowController; +class ThemeProvider; + +// Protocol for a BookmarkButton's delegate, responsible for doing +// things on behalf of a bookmark button. +@protocol BookmarkButtonDelegate + +// Fill the given pasteboard with appropriate data when the given button is +// dragged. Since the delegate has no way of providing pasteboard data later, +// all data must actually be put into the pasteboard and not merely promised. +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button; + +// Bookmark buttons pass mouseEntered: and mouseExited: events to +// their delegate. This allows the delegate to decide (for example) +// which one, if any, should perform a hover-open. +- (void)mouseEnteredButton:(id)button event:(NSEvent*)event; +- (void)mouseExitedButton:(id)button event:(NSEvent*)event; + +// Returns YES if a drag operation should lock the fullscreen overlay bar +// visibility before starting. For example, dragging a bookmark button should +// not lock the overlay if the bookmark bar is currently showing in detached +// mode on the NTP. +- (BOOL)dragShouldLockBarVisibility; + +// Returns the top-level window for this button. +- (NSWindow*)browserWindow; + +// Returns YES if the bookmark button can be dragged to the trash, NO otherwise. +- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button; + +// This is called after the user has dropped the bookmark button on the trash. +// The delegate can use this event to delete the bookmark. +- (void)didDragBookmarkToTrash:(BookmarkButton*)button; + +@end + + +// Protocol to be implemented by controllers that logically own +// bookmark buttons. The controller may be either an NSViewController +// 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 + +// Close all bookmark folders, walking up the ownership chain. +- (void)closeAllBookmarkFolders; + +// Close just my bookmark folder. +- (void)closeBookmarkFolder:(id)sender; + +// Return the bookmark model for this controller. +- (BookmarkModel*)bookmarkModel; + +// Perform drag enter/exit operations, such as hover-open and hover-close. +- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info; +- (void)draggingExited:(id<NSDraggingInfo>)info; + +// Returns YES if a drag operation should lock the fullscreen overlay bar +// visibility before starting. For example, dragging a bookmark button should +// not lock the overlay if the bookmark bar is currently showing in detached +// mode on the NTP. +- (BOOL)dragShouldLockBarVisibility; + +// Perform the actual DnD of a bookmark or bookmark button. + +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. |copy| is YES if a copy is to be +// made and inserted into the new location while leaving the bookmark in +// the old location, otherwise move the bookmark by removing from its old +// location and inserting into the new location. +- (BOOL)dragButton:(BookmarkButton*)sourceButton + to:(NSPoint)point + copy:(BOOL)copy; + +// Determine if the pasteboard from |info| has dragging data containing +// bookmark(s) and perform the drag and return YES, otherwise return NO. +- (BOOL)dragBookmarkData:(id<NSDraggingInfo>)info; + +// Determine if the drag pasteboard has any drag data of type +// kBookmarkDictionaryListPboardType and, if so, return those elements +// otherwise return an empty vector. +- (std::vector<const BookmarkNode*>)retrieveBookmarkNodeData; + +// Return YES if we should show the drop indicator, else NO. In some +// cases (e.g. hover open) we don't want to show the drop indicator. +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. +- (BOOL)shouldShowIndicatorShownForPoint:(NSPoint)point; + +// The x or y coordinate of (the middle of) the indicator to draw for +// a drag of the source button to the given point (given in window +// coordinates). +// |point| is in the base coordinate system of the destination window; +// |it comes from an id<NSDraggingInfo>. +// TODO(viettrungluu,jrg): instead of this, make buttons move around. +// http://crbug.com/35968 +- (CGFloat)indicatorPosForDragToPoint:(NSPoint)point; + +// Return the theme provider associated with this browser window. +- (ThemeProvider*)themeProvider; + +// Called just before a child folder puts itself on screen. +- (void)childFolderWillShow:(id<BookmarkButtonControllerProtocol>)child; + +// Called just before a child folder closes. +- (void)childFolderWillClose:(id<BookmarkButtonControllerProtocol>)child; + +// Return a controller's folder controller for a subfolder, or nil. +- (BookmarkBarFolderController*)folderController; + +// Add a new folder controller as triggered by the given folder button. +// If there is a current folder controller, close it. +- (void)addNewFolderControllerWithParentButton:(BookmarkButton*)parentButton; + +// Open all of the nodes for the given node with disposition. +- (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; + +// Given a list or |urls| and |titles|, create new bookmark nodes and add +// them to the bookmark model such that they will be 1) added to the folder +// represented by the button at |point| if it is a folder, or 2) inserted +// into the parent of the non-folder bookmark at |point| in front of that +// button. Returns YES if at least one bookmark was added. +// TODO(mrossetti): Change function to use a pair-like structure for +// URLs and titles. http://crbug.com/44411 +- (BOOL)addURLs:(NSArray*)urls withTitles:(NSArray*)titles at:(NSPoint)point; + +// 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 + + +// Class for bookmark bar buttons that can be drag sources. +@interface BookmarkButton : DraggableButton { + @private + IBOutlet NSObject<BookmarkButtonDelegate>* delegate_; // Weak. + + // Saved pointer to the BWC for the browser window that contains this button. + // Used to lock and release bar visibility during a drag. The pointer is + // saved because the bookmark button is no longer a part of a window at the + // end of a drag operation (or, in fact, can be dragged to a completely + // different window), so there is no way to retrieve the same BWC object after + // a drag. + BrowserWindowController* visibilityDelegate_; // weak + + NSPoint dragMouseOffset_; + NSPoint dragEndScreenLocation_; + BOOL dragPending_; +} + +@property(assign, nonatomic) NSObject<BookmarkButtonDelegate>* delegate; + +// Return the bookmark node associated with this button, or NULL. +- (const BookmarkNode*)bookmarkNode; + +// Return YES if this is a folder button (the node has subnodes). +- (BOOL)isFolder; + +// At this time we represent an empty folder (e.g. the string +// '(empty)') as a disabled button with no associated node. +// +// TODO(jrg): improve; things work but are slightly ugly since "empty" +// and "one disabled button" are not the same thing. +// http://crbug.com/35967 +- (BOOL)isEmpty; + +// Turn on or off pulsing of a bookmark button. +// Triggered by the bookmark bubble. +- (void)setIsContinuousPulsing:(BOOL)flag; + +// Return continuous pulse state. +- (BOOL)isContinuousPulsing; + +// Return the location in screen coordinates where the remove animation should +// be displayed. +- (NSPoint)screenLocationForRemoveAnimation; + +@end // @interface BookmarkButton + + +@interface BookmarkButton(TestingAPI) +- (void)beginDrag:(NSEvent*)event; +@end + +namespace bookmark_button { + +// Notifications for pulsing of bookmarks. +extern NSString* const kPulseBookmarkButtonNotification; + +// Key for userInfo dict of a kPulseBookmarkButtonNotification. +// Value is a [NSValue valueWithPointer:]; pointer is a (const BookmarkNode*). +extern NSString* const kBookmarkKey; + +// Key for userInfo dict of a kPulseBookmarkButtonNotification. +// Value is a [NSNumber numberWithBool:] to turn pulsing on or off. +extern NSString* const kBookmarkPulseFlagKey; + +}; diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm new file mode 100644 index 0000000..885e5c8 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm @@ -0,0 +1,238 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" + +#include "base/logging.h" +#import "base/scoped_nsobject.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/metrics/user_metrics.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#import "chrome/browser/ui/cocoa/view_id_util.h" + +// The opacity of the bookmark button drag image. +static const CGFloat kDragImageOpacity = 0.7; + + +namespace bookmark_button { + +NSString* const kPulseBookmarkButtonNotification = + @"PulseBookmarkButtonNotification"; +NSString* const kBookmarkKey = @"BookmarkKey"; +NSString* const kBookmarkPulseFlagKey = @"BookmarkPulseFlagKey"; + +}; + +@interface BookmarkButton(Private) + +// Make a drag image for the button. +- (NSImage*)dragImage; + +@end // @interface BookmarkButton(Private) + + +@implementation BookmarkButton + +@synthesize delegate = delegate_; + +- (id)initWithFrame:(NSRect)frameRect { + // BookmarkButton's ViewID may be changed to VIEW_ID_OTHER_BOOKMARKS in + // BookmarkBarController, so we can't just override -viewID method to return + // it. + if ((self = [super initWithFrame:frameRect])) + view_id_util::SetID(self, VIEW_ID_BOOKMARK_BAR_ELEMENT); + return self; +} + +- (void)dealloc { + if ([[self cell] respondsToSelector:@selector(safelyStopPulsing)]) + [[self cell] safelyStopPulsing]; + view_id_util::UnsetID(self); + [super dealloc]; +} + +- (const BookmarkNode*)bookmarkNode { + return [[self cell] bookmarkNode]; +} + +- (BOOL)isFolder { + const BookmarkNode* node = [self bookmarkNode]; + return (node && node->is_folder()); +} + +- (BOOL)isEmpty { + return [self bookmarkNode] ? NO : YES; +} + +- (void)setIsContinuousPulsing:(BOOL)flag { + [[self cell] setIsContinuousPulsing:flag]; +} + +- (BOOL)isContinuousPulsing { + return [[self cell] isContinuousPulsing]; +} + +- (NSPoint)screenLocationForRemoveAnimation { + NSPoint point; + + if (dragPending_) { + // Use the position of the mouse in the drag image as the location. + point = dragEndScreenLocation_; + point.x += dragMouseOffset_.x; + if ([self isFlipped]) { + point.y += [self bounds].size.height - dragMouseOffset_.y; + } else { + point.y += dragMouseOffset_.y; + } + } else { + // Use the middle of this button as the location. + NSRect bounds = [self bounds]; + point = NSMakePoint(NSMidX(bounds), NSMidY(bounds)); + point = [self convertPoint:point toView:nil]; + point = [[self window] convertBaseToScreen:point]; + } + + return point; +} + +// By default, NSButton ignores middle-clicks. +// But we want them. +- (void)otherMouseUp:(NSEvent*)event { + [self performClick:self]; +} + +// Overridden from DraggableButton. +- (void)beginDrag:(NSEvent*)event { + // Don't allow a drag of the empty node. + // The empty node is a placeholder for "(empty)", to be revisited. + if ([self isEmpty]) + return; + + if (![self delegate]) { + NOTREACHED(); + return; + } + // 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!) + // to be destroyed and rebuilt. Make sure we don't go away while on + // the stack. + [self retain]; + + // Lock bar visibility, forcing the overlay to stay visible if we are in + // fullscreen mode. + if ([[self delegate] dragShouldLockBarVisibility]) { + DCHECK(!visibilityDelegate_); + NSWindow* window = [[self delegate] browserWindow]; + visibilityDelegate_ = + [BrowserWindowController browserWindowControllerForWindow:window]; + [visibilityDelegate_ lockBarVisibilityForOwner:self + withAnimation:NO + delay:NO]; + } + const BookmarkNode* node = [self bookmarkNode]; + const BookmarkNode* parent = node ? node->GetParent() : NULL; + if (parent && parent->type() == BookmarkNode::FOLDER) { + UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_DragStart")); + } else { + UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_DragStart")); + } + + dragMouseOffset_ = [self convertPointFromBase:[event locationInWindow]]; + dragPending_ = YES; + + CGFloat yAt = [self bounds].size.height; + NSSize dragOffset = NSMakeSize(0.0, 0.0); + [self dragImage:[self dragImage] at:NSMakePoint(0, yAt) offset:dragOffset + event:event pasteboard:pboard source:self slideBack:YES]; + + // And we're done. + dragPending_ = NO; + [self autorelease]; +} + +// Overridden to release bar visibility. +- (void)endDrag { + // visibilityDelegate_ can be nil if we're detached, and that's fine. + [visibilityDelegate_ releaseBarVisibilityForOwner:self + withAnimation:YES + delay:YES]; + visibilityDelegate_ = nil; + [super endDrag]; +} + +- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { + NSDragOperation operation = NSDragOperationCopy; + if (isLocal) { + operation |= NSDragOperationMove; + } + if ([delegate_ canDragBookmarkButtonToTrash:self]) { + operation |= NSDragOperationDelete; + } + return operation; +} + +- (void)draggedImage:(NSImage *)anImage + endedAt:(NSPoint)aPoint + operation:(NSDragOperation)operation { + if (operation & NSDragOperationDelete) { + dragEndScreenLocation_ = aPoint; + [delegate_ didDragBookmarkToTrash:self]; + } +} + +// mouseEntered: and mouseExited: are called from our +// BookmarkButtonCell. We redirect this information to our delegate. +// The controller can then perform menu-like actions (e.g. "hover over +// to open menu"). +- (void)mouseEntered:(NSEvent*)event { + [delegate_ mouseEnteredButton:self event:event]; +} + +// See comments above mouseEntered:. +- (void)mouseExited:(NSEvent*)event { + [delegate_ mouseExitedButton:self event:event]; +} + +@end + +@implementation BookmarkButton(Private) + +- (NSImage*)dragImage { + NSRect bounds = [self bounds]; + + // Grab the image from the screen and put it in an |NSImage|. We can't use + // this directly since we need to clip it and set its opacity. This won't work + // if the source view is clipped. Fortunately, we don't display clipped + // bookmark buttons. + [self lockFocus]; + scoped_nsobject<NSBitmapImageRep> + bitmap([[NSBitmapImageRep alloc] initWithFocusedViewRect:bounds]); + [self unlockFocus]; + scoped_nsobject<NSImage> image([[NSImage alloc] initWithSize:[bitmap size]]); + [image addRepresentation:bitmap]; + + // Make an autoreleased |NSImage|, which will be returned, and draw into it. + // By default, the |NSImage| will be completely transparent. + NSImage* dragImage = + [[[NSImage alloc] initWithSize:[bitmap size]] autorelease]; + [dragImage lockFocus]; + + // Draw the image with the appropriate opacity, clipping it tightly. + GradientButtonCell* cell = static_cast<GradientButtonCell*>([self cell]); + DCHECK([cell isKindOfClass:[GradientButtonCell class]]); + [[cell clipPathForFrame:bounds inView:self] setClip]; + [image drawAtPoint:NSMakePoint(0, 0) + fromRect:NSMakeRect(0, 0, NSWidth(bounds), NSHeight(bounds)) + operation:NSCompositeSourceOver + fraction:kDragImageOpacity]; + + [dragImage unlockFocus]; + return dragImage; +} + +@end // @implementation BookmarkButton(Private) diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h new file mode 100644 index 0000000..2e28cf4 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h @@ -0,0 +1,65 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_ +#pragma once + +#import "base/mac/cocoa_protocols.h" +#import "chrome/browser/ui/cocoa/gradient_button_cell.h" + +class BookmarkNode; + +// A button cell that handles drawing/highlighting of buttons in the +// bookmark bar. This cell forwards mouseEntered/mouseExited events +// to its control view so that pseudo-menu operations +// (e.g. hover-over to open) can be implemented. +@interface BookmarkButtonCell : GradientButtonCell<NSMenuDelegate> { + @private + BOOL empty_; // is this an "empty" button placeholder button cell? + + // Starting index of bookmarkFolder children that we care to use. + int startingChildIndex_; + + // Should we draw the folder arrow as needed? Not used for the bar + // itself but used on the folder windows. + BOOL drawFolderArrow_; + + // Arrow for folders + scoped_nsobject<NSImage> arrowImage_; +} + +@property (nonatomic, readwrite, assign) const BookmarkNode* bookmarkNode; +@property (nonatomic, readwrite, assign) int startingChildIndex; +@property (nonatomic, readwrite, assign) BOOL drawFolderArrow; + +// Create a button cell which draws with a theme. ++ (id)buttonCellForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage; + +// Initialize a button cell which draws with a theme. +// Designated initializer. +- (id)initForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage; + +- (BOOL)empty; // returns YES if empty. +- (void)setEmpty:(BOOL)empty; + +// |-setBookmarkCellText:image:| is used to set the text and image of +// a BookmarkButtonCell, and align the image to the left (NSImageLeft) +// if there is text in the title, and centered (NSImageCenter) if +// there is not. If |title| is nil, do not reset the title. +- (void)setBookmarkCellText:(NSString*)title + image:(NSImage*)image; + +// Set the color of text in this cell. +- (void)setTextColor:(NSColor*)color; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_BUTTON_CELL_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm new file mode 100644 index 0000000..a78d627 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm @@ -0,0 +1,246 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" + +#include "app/l10n_util_mac.h" +#include "app/mac/nsimage_cache.h" +#include "base/logging.h" +#include "base/sys_string_conversions.h" +#import "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/metrics/user_metrics.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/image_utils.h" +#include "grit/generated_resources.h" + + +@interface BookmarkButtonCell(Private) +- (void)configureBookmarkButtonCell; +@end + + +@implementation BookmarkButtonCell + +@synthesize startingChildIndex = startingChildIndex_; +@synthesize drawFolderArrow = drawFolderArrow_; + ++ (id)buttonCellForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage { + id buttonCell = + [[[BookmarkButtonCell alloc] initForNode:node + contextMenu:contextMenu + cellText:cellText + cellImage:cellImage] + autorelease]; + return buttonCell; +} + +- (id)initForNode:(const BookmarkNode*)node + contextMenu:(NSMenu*)contextMenu + cellText:(NSString*)cellText + cellImage:(NSImage*)cellImage { + if ((self = [super initTextCell:cellText])) { + [self configureBookmarkButtonCell]; + + [self setBookmarkNode:node]; + + if (node) { + NSString* title = base::SysUTF16ToNSString(node->GetTitle()); + [self setBookmarkCellText:title image:cellImage]; + [self setMenu:contextMenu]; + } else { + [self setEmpty:YES]; + [self setBookmarkCellText:l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU) + image:nil]; + } + } + + return self; +} + +- (id)initTextCell:(NSString*)string { + return [self initForNode:nil contextMenu:nil cellText:string cellImage:nil]; +} + +// Used by the off-the-side menu, the only case where a +// BookmarkButtonCell is loaded from a nib. +- (void)awakeFromNib { + [self configureBookmarkButtonCell]; +} + +// Perform all normal init routines specific to the BookmarkButtonCell. +- (void)configureBookmarkButtonCell { + [self setButtonType:NSMomentaryPushInButton]; + [self setBezelStyle:NSShadowlessSquareBezelStyle]; + [self setShowsBorderOnlyWhileMouseInside:YES]; + [self setControlSize:NSSmallControlSize]; + [self setAlignment:NSLeftTextAlignment]; + [self setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]]; + [self setWraps:NO]; + // NSLineBreakByTruncatingMiddle seems more common on OSX but let's + // try to match Windows for a bit to see what happens. + [self setLineBreakMode:NSLineBreakByTruncatingTail]; + + // Theming doesn't work for bookmark buttons yet (cell text is chucked). + [super setShouldTheme:NO]; +} + +- (BOOL)empty { + return empty_; +} + +- (void)setEmpty:(BOOL)empty { + empty_ = empty; + [self setShowsBorderOnlyWhileMouseInside:!empty]; +} + +- (NSSize)cellSizeForBounds:(NSRect)aRect { + NSSize size = [super cellSizeForBounds:aRect]; + // Cocoa seems to slightly underestimate how much space we need, so we + // compensate here to avoid a clipped rendering. + size.width += 2; + size.height += 4; + return size; +} + +- (void)setBookmarkCellText:(NSString*)title + image:(NSImage*)image { + title = [title stringByReplacingOccurrencesOfString:@"\n" + withString:@" "]; + title = [title stringByReplacingOccurrencesOfString:@"\r" + withString:@" "]; + // If there is no title, squeeze things tight by displaying only the image; by + // default, Cocoa leaves extra space in an attempt to display an empty title. + if ([title length]) { + [self setImagePosition:NSImageLeft]; + [self setTitle:title]; + } else { + [self setImagePosition:NSImageOnly]; + } + + if (image) + [self setImage:image]; +} + +- (void)setBookmarkNode:(const BookmarkNode*)node { + [self setRepresentedObject:[NSValue valueWithPointer:node]]; +} + +- (const BookmarkNode*)bookmarkNode { + return static_cast<const BookmarkNode*>([[self representedObject] + pointerValue]); +} + +// We share the context menu among all bookmark buttons. To allow us +// to disambiguate when needed (e.g. "open bookmark"), we set the +// menu's associated bookmark node ID to be our represented object. +- (NSMenu*)menu { + if (empty_) + return nil; + BookmarkMenu* menu = (BookmarkMenu*)[super menu]; + const BookmarkNode* node = + static_cast<const BookmarkNode*>([[self representedObject] pointerValue]); + + if (node->GetParent() && node->GetParent()->type() == BookmarkNode::FOLDER) { + UserMetrics::RecordAction(UserMetricsAction("BookmarkBarFolder_CtxMenu")); + } else { + UserMetrics::RecordAction(UserMetricsAction("BookmarkBar_CtxMenu")); + } + + [menu setRepresentedObject:[NSNumber numberWithLongLong:node->id()]]; + + return menu; +} + +// Unfortunately, NSCell doesn't already have something like this. +// TODO(jrg): consider placing in GTM. +- (void)setTextColor:(NSColor*)color { + + // We can't properly set the cell's text color without a control. + // In theory we could just save the next for later and wait until + // the cell is moved to a control, but there is no obvious way to + // accomplish that (e.g. no "cellDidMoveToControl" notification.) + DCHECK([self controlView]); + + scoped_nsobject<NSMutableParagraphStyle> style([NSMutableParagraphStyle new]); + [style setAlignment:NSLeftTextAlignment]; + NSDictionary* dict = [NSDictionary + dictionaryWithObjectsAndKeys:color, + NSForegroundColorAttributeName, + [self font], NSFontAttributeName, + style.get(), NSParagraphStyleAttributeName, + nil]; + scoped_nsobject<NSAttributedString> ats([[NSAttributedString alloc] + initWithString:[self title] + attributes:dict]); + NSButton* button = static_cast<NSButton*>([self controlView]); + if (button) { + DCHECK([button isKindOfClass:[NSButton class]]); + [button setAttributedTitle:ats.get()]; + } +} + +// To implement "hover open a bookmark button to open the folder" +// which feels like menus, we override NSButtonCell's mouseEntered: +// and mouseExited:, then and pass them along to our owning control. +// Note: as verified in a debugger, mouseEntered: does NOT increase +// the retainCount of the cell or its owning control. +- (void)mouseEntered:(NSEvent*)event { + [super mouseEntered:event]; + [[self controlView] mouseEntered:event]; +} + +// See comment above mouseEntered:, above. +- (void)mouseExited:(NSEvent*)event { + [[self controlView] mouseExited:event]; + [super mouseExited:event]; +} + +- (void)setDrawFolderArrow:(BOOL)draw { + drawFolderArrow_ = draw; + if (draw && !arrowImage_) { + arrowImage_.reset( + [app::mac::GetCachedImageWithName(@"menu_hierarchy_arrow.pdf") retain]); + } +} + +// Add extra size for the arrow so it doesn't overlap the text. +// Does not sanity check to be sure this is actually a folder node. +- (NSSize)cellSize { + NSSize cellSize = [super cellSize]; + if (drawFolderArrow_) { + cellSize.width += [arrowImage_ size].width; // plus margin? + } + return cellSize; +} + +// Override cell drawing to add a submenu arrow like a real menu. +- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { + // First draw "everything else". + [super drawInteriorWithFrame:cellFrame inView:controlView]; + + // If asked to do so, and if a folder, draw the arrow. + if (!drawFolderArrow_) + return; + BookmarkButton* button = static_cast<BookmarkButton*>([self controlView]); + DCHECK([button respondsToSelector:@selector(isFolder)]); + if ([button isFolder]) { + NSRect imageRect = NSZeroRect; + imageRect.size = [arrowImage_ size]; + NSRect drawRect = NSOffsetRect(imageRect, + NSWidth(cellFrame) - NSWidth(imageRect), + (NSHeight(cellFrame) / 2.0) - + (NSHeight(imageRect) / 2.0)); + [arrowImage_ drawInRect:drawRect + fromRect:imageRect + operation:NSCompositeSourceOver + fraction:[self isEnabled] ? 1.0 : 0.5 + neverFlipped:YES]; + } +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm new file mode 100644 index 0000000..ff26512 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm @@ -0,0 +1,183 @@ +// 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 "app/resource_bundle.h" +#include "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "grit/app_resources.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +// Simple class to remember how many mouseEntered: and mouseExited: +// calls it gets. Only used by BookmarkMouseForwarding but placed +// at the top of the file to keep it outside the anon namespace. +@interface ButtonRemembersMouseEnterExit : NSButton { + @public + int enters_; + int exits_; +} +@end + +@implementation ButtonRemembersMouseEnterExit +- (void)mouseEntered:(NSEvent*)event { + enters_++; +} +- (void)mouseExited:(NSEvent*)event { + exits_++; +} +@end + + +namespace { + +class BookmarkButtonCellTest : public CocoaTest { + public: + BrowserTestHelper helper_; +}; + +// Make sure it's not totally bogus +TEST_F(BookmarkButtonCellTest, SizeForBounds) { + NSRect frame = NSMakeRect(0, 0, 50, 30); + scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]); + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + [view setCell:cell.get()]; + [[test_window() contentView] addSubview:view]; + + NSRect r = NSMakeRect(0, 0, 100, 100); + NSSize size = [cell.get() cellSizeForBounds:r]; + EXPECT_TRUE(size.width > 0 && size.height > 0); + EXPECT_TRUE(size.width < 200 && size.height < 200); +} + +// Make sure icon-only buttons are squeezed tightly. +TEST_F(BookmarkButtonCellTest, IconOnlySqueeze) { + NSRect frame = NSMakeRect(0, 0, 50, 30); + scoped_nsobject<NSButton> view([[NSButton alloc] initWithFrame:frame]); + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + [view setCell:cell.get()]; + [[test_window() contentView] addSubview:view]; + + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + scoped_nsobject<NSImage> image([rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON) + retain]); + EXPECT_TRUE(image.get()); + + NSRect r = NSMakeRect(0, 0, 100, 100); + [cell setBookmarkCellText:@" " image:image]; + CGFloat two_space_width = [cell.get() cellSizeForBounds:r].width; + [cell setBookmarkCellText:@" " image:image]; + CGFloat one_space_width = [cell.get() cellSizeForBounds:r].width; + [cell setBookmarkCellText:@"" image:image]; + CGFloat zero_space_width = [cell.get() cellSizeForBounds:r].width; + + // Make sure the switch to "no title" is more significant than we + // would otherwise see by decreasing the length of the title. + CGFloat delta1 = two_space_width - one_space_width; + CGFloat delta2 = one_space_width - zero_space_width; + EXPECT_GT(delta2, delta1); + +} + +// Make sure the default from the base class is overridden. +TEST_F(BookmarkButtonCellTest, MouseEnterStuff) { + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + // Setting the menu should have no affect since we either share or + // dynamically compose the menu given a node. + [cell setMenu:[[[BookmarkMenu alloc] initWithTitle:@"foo"] autorelease]]; + EXPECT_FALSE([cell menu]); + + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* node = model->GetBookmarkBarNode(); + [cell setEmpty:NO]; + [cell setBookmarkNode:node]; + EXPECT_TRUE([cell showsBorderOnlyWhileMouseInside]); + EXPECT_TRUE([cell menu]); + + [cell setEmpty:YES]; + EXPECT_FALSE([cell.get() showsBorderOnlyWhileMouseInside]); + EXPECT_FALSE([cell menu]); +} + +TEST_F(BookmarkButtonCellTest, BookmarkNode) { + BookmarkModel& model(*(helper_.profile()->GetBookmarkModel())); + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + + const BookmarkNode* node = model.GetBookmarkBarNode(); + [cell setBookmarkNode:node]; + EXPECT_EQ(node, [cell bookmarkNode]); + + node = model.other_node(); + [cell setBookmarkNode:node]; + EXPECT_EQ(node, [cell bookmarkNode]); +} + +TEST_F(BookmarkButtonCellTest, BookmarkMouseForwarding) { + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initTextCell:@"Testing"]); + scoped_nsobject<ButtonRemembersMouseEnterExit> + button([[ButtonRemembersMouseEnterExit alloc] + initWithFrame:NSMakeRect(0,0,50,50)]); + [button setCell:cell.get()]; + EXPECT_EQ(0, button.get()->enters_); + EXPECT_EQ(0, button.get()->exits_); + NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved + location:NSMakePoint(10,10) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + eventNumber:0 + clickCount:0 + pressure:0]; + [cell mouseEntered:event]; + EXPECT_TRUE(button.get()->enters_ && !button.get()->exits_); + + for (int i = 0; i < 3; i++) + [cell mouseExited:event]; + EXPECT_EQ(button.get()->enters_, 1); + EXPECT_EQ(button.get()->exits_, 3); +} + +// Confirms a cell created in a nib is initialized properly +TEST_F(BookmarkButtonCellTest, Awake) { + scoped_nsobject<BookmarkButtonCell> cell([[BookmarkButtonCell alloc] init]); + [cell awakeFromNib]; + EXPECT_EQ(NSLeftTextAlignment, [cell alignment]); +} + +// Subfolder arrow details. +TEST_F(BookmarkButtonCellTest, FolderArrow) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* bar = model->GetBookmarkBarNode(); + const BookmarkNode* node = model->AddURL(bar, bar->GetChildCount(), + ASCIIToUTF16("title"), + GURL("http://www.google.com")); + scoped_nsobject<BookmarkButtonCell> cell( + [[BookmarkButtonCell alloc] initForNode:node + contextMenu:nil + cellText:@"small" + cellImage:nil]); + EXPECT_TRUE(cell.get()); + + NSSize size = [cell cellSize]; + // sanity check + EXPECT_GE(size.width, 2); + EXPECT_GE(size.height, 2); + + // Once we turn on arrow drawing make sure there is now room for it. + [cell setDrawFolderArrow:YES]; + NSSize arrowSize = [cell cellSize]; + EXPECT_GT(arrowSize.width, size.width); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm new file mode 100644 index 0000000..93bd769 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm @@ -0,0 +1,174 @@ +// 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 "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h" +#import "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#import "chrome/browser/ui/cocoa/test_event_utils.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +// Fake BookmarkButton delegate to get a pong on mouse entered/exited +@interface FakeButtonDelegate : NSObject<BookmarkButtonDelegate> { + @public + int entered_; + int exited_; + BOOL canDragToTrash_; + int didDragToTrashCount_; +} +@end + +@implementation FakeButtonDelegate + +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { +} + +- (void)mouseEnteredButton:(id)buton event:(NSEvent*)event { + entered_++; +} + +- (void)mouseExitedButton:(id)buton event:(NSEvent*)event { + exited_++; +} + +- (BOOL)dragShouldLockBarVisibility { + return NO; +} + +- (NSWindow*)browserWindow { + return nil; +} + +- (BOOL)canDragBookmarkButtonToTrash:(BookmarkButton*)button { + return canDragToTrash_; +} + +- (void)didDragBookmarkToTrash:(BookmarkButton*)button { + didDragToTrashCount_++; +} + +@end + +namespace { + +class BookmarkButtonTest : public CocoaTest { +}; + +// Make sure nothing leaks +TEST_F(BookmarkButtonTest, Create) { + scoped_nsobject<BookmarkButton> button; + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); +} + +// Test folder and empty node queries. +TEST_F(BookmarkButtonTest, FolderAndEmptyOrNot) { + BrowserTestHelper helper_; + scoped_nsobject<BookmarkButton> button; + scoped_nsobject<BookmarkButtonCell> cell; + + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); + cell.reset([[BookmarkButtonCell alloc] initTextCell:@"hi mom"]); + [button setCell:cell]; + + EXPECT_TRUE([button isEmpty]); + EXPECT_FALSE([button isFolder]); + EXPECT_FALSE([button bookmarkNode]); + + NSEvent* downEvent = + test_event_utils::LeftMouseDownAtPoint(NSMakePoint(10,10)); + // Since this returns (does not actually begin a modal drag), success! + [button beginDrag:downEvent]; + + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* node = model->GetBookmarkBarNode(); + [cell setBookmarkNode:node]; + EXPECT_FALSE([button isEmpty]); + EXPECT_TRUE([button isFolder]); + EXPECT_EQ([button bookmarkNode], node); + + node = model->AddURL(node, 0, ASCIIToUTF16("hi mom"), + GURL("http://www.google.com")); + [cell setBookmarkNode:node]; + EXPECT_FALSE([button isEmpty]); + EXPECT_FALSE([button isFolder]); + EXPECT_EQ([button bookmarkNode], node); +} + +TEST_F(BookmarkButtonTest, MouseEnterExitRedirect) { + NSEvent* moveEvent = + test_event_utils::MouseEventAtPoint(NSMakePoint(10,10), NSMouseMoved, 0); + scoped_nsobject<BookmarkButton> button; + scoped_nsobject<BookmarkButtonCell> cell; + scoped_nsobject<FakeButtonDelegate> + delegate([[FakeButtonDelegate alloc] init]); + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); + cell.reset([[BookmarkButtonCell alloc] initTextCell:@"hi mom"]); + [button setCell:cell]; + [button setDelegate:delegate]; + + EXPECT_EQ(0, delegate.get()->entered_); + EXPECT_EQ(0, delegate.get()->exited_); + + [button mouseEntered:moveEvent]; + EXPECT_EQ(1, delegate.get()->entered_); + EXPECT_EQ(0, delegate.get()->exited_); + + [button mouseExited:moveEvent]; + [button mouseExited:moveEvent]; + EXPECT_EQ(1, delegate.get()->entered_); + EXPECT_EQ(2, delegate.get()->exited_); +} + +TEST_F(BookmarkButtonTest, DragToTrash) { + BrowserTestHelper helper_; + + scoped_nsobject<BookmarkButton> button; + scoped_nsobject<BookmarkButtonCell> cell; + scoped_nsobject<FakeButtonDelegate> + delegate([[FakeButtonDelegate alloc] init]); + button.reset([[BookmarkButton alloc] initWithFrame:NSMakeRect(0,0,500,500)]); + cell.reset([[BookmarkButtonCell alloc] initTextCell:@"hi mom"]); + [button setCell:cell]; + [button setDelegate:delegate]; + + // Add a deletable bookmark to the button. + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* barNode = model->GetBookmarkBarNode(); + const BookmarkNode* node = model->AddURL(barNode, 0, ASCIIToUTF16("hi mom"), + GURL("http://www.google.com")); + [cell setBookmarkNode:node]; + + // Verify that if canDragBookmarkButtonToTrash is NO then the button can't + // be dragged to the trash. + delegate.get()->canDragToTrash_ = NO; + NSDragOperation operation = [button draggingSourceOperationMaskForLocal:NO]; + EXPECT_EQ(0u, operation & NSDragOperationDelete); + operation = [button draggingSourceOperationMaskForLocal:YES]; + EXPECT_EQ(0u, operation & NSDragOperationDelete); + + // Verify that if canDragBookmarkButtonToTrash is YES then the button can + // be dragged to the trash. + delegate.get()->canDragToTrash_ = YES; + operation = [button draggingSourceOperationMaskForLocal:NO]; + EXPECT_EQ(NSDragOperationDelete, operation & NSDragOperationDelete); + operation = [button draggingSourceOperationMaskForLocal:YES]; + EXPECT_EQ(NSDragOperationDelete, operation & NSDragOperationDelete); + + // Verify that canDragBookmarkButtonToTrash is called when expected. + delegate.get()->canDragToTrash_ = YES; + EXPECT_EQ(0, delegate.get()->didDragToTrashCount_); + [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationCopy]; + EXPECT_EQ(0, delegate.get()->didDragToTrashCount_); + [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationMove]; + EXPECT_EQ(0, delegate.get()->didDragToTrashCount_); + [button draggedImage:nil endedAt:NSZeroPoint operation:NSDragOperationDelete]; + EXPECT_EQ(1, delegate.get()->didDragToTrashCount_); +} + +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h new file mode 100644 index 0000000..53d4361 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h @@ -0,0 +1,30 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "chrome/browser/bookmarks/bookmark_node_data.h" +#include "chrome/browser/ui/cocoa/web_contents_drag_source.h" + +// A class that handles tracking and event processing for a drag and drop +// originating from the content area. +@interface BookmarkDragSource : WebContentsDragSource { + @private + // Our drop data. Should only be initialized once. + std::vector<BookmarkNodeData::Element> dropData_; + + Profile* profile_; +} + +// Initialize a DragDataSource object for a drag (originating on the given +// contentsView and with the given dropData and pboard). Fill the pasteboard +// with data types appropriate for dropData. +- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView + dropData: + (const std::vector<BookmarkNodeData::Element>&)dropData + profile:(Profile*)profile + pasteboard:(NSPasteboard*)pboard + dragOperationMask:(NSDragOperation)dragOperationMask; + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm new file mode 100644 index 0000000..85ea1b9 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h" + +#include "chrome/browser/bookmarks/bookmark_pasteboard_helper_mac.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/tab_contents/tab_contents_view_mac.h" + +@implementation BookmarkDragSource + +- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView + dropData: + (const std::vector<BookmarkNodeData::Element>&)dropData + profile:(Profile*)profile + pasteboard:(NSPasteboard*)pboard + dragOperationMask:(NSDragOperation)dragOperationMask { + self = [super initWithContentsView:contentsView + pasteboard:pboard + dragOperationMask:dragOperationMask]; + if (self) { + dropData_ = dropData; + profile_ = profile; + } + + return self; +} + +- (void)fillPasteboard { + bookmark_pasteboard_helper_mac::WriteToDragClipboard(dropData_, + profile_->GetPath().value()); +} + +- (NSImage*)dragImage { + // TODO(feldstein): Do something better than this. Should have badging + // and a single drag image. + // http://crbug.com/37264 + return [NSImage imageNamed:NSImageNameMultipleDocuments]; +} + +@end // @implementation BookmarkDragSource + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h new file mode 100644 index 0000000..34e7032 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h @@ -0,0 +1,171 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#import "base/mac/cocoa_protocols.h" +#include "base/scoped_ptr.h" +#include "base/scoped_nsobject.h" +#include "chrome/browser/bookmarks/bookmark_editor.h" + +class BookmarkEditorBaseControllerBridge; +class BookmarkModel; +@class BookmarkTreeBrowserCell; + +// A base controller class for bookmark creation and editing dialogs which +// present the current bookmark folder structure in a tree view. Do not +// instantiate this controller directly -- use one of its derived classes. +// NOTE: If a derived class is intended to be dispatched via the +// BookmarkEditor::Show static function found in the accompanying +// implementation, that function will need to be update. +@interface BookmarkEditorBaseController : NSWindowController { + @private + IBOutlet NSButton* newFolderButton_; + IBOutlet NSButton* okButton_; // Used for unit testing only. + IBOutlet NSTreeController* folderTreeController_; + IBOutlet NSOutlineView* folderTreeView_; + + NSWindow* parentWindow_; // weak + Profile* profile_; // weak + const BookmarkNode* parentNode_; // weak; owned by the model + BookmarkEditor::Configuration configuration_; + NSString* initialName_; + NSString* displayName_; // Bound to a text field in the dialog. + BOOL okEnabled_; // Bound to the OK button. + // An array of BookmarkFolderInfo where each item describes a folder in the + // BookmarkNode structure. + scoped_nsobject<NSArray> folderTreeArray_; + // Bound to the table view giving a path to the current selections, of which + // there should only ever be one. + scoped_nsobject<NSArray> tableSelectionPaths_; + // C++ bridge object that observes the BookmarkModel for me. + scoped_ptr<BookmarkEditorBaseControllerBridge> observer_; +} + +@property (nonatomic, copy) NSString* initialName; +@property (nonatomic, copy) NSString* displayName; +@property (nonatomic, assign) BOOL okEnabled; +@property (nonatomic, retain, readonly) NSArray* folderTreeArray; +@property (nonatomic, copy) NSArray* tableSelectionPaths; + +// Designated initializer. Derived classes should call through to this init. +- (id)initWithParentWindow:(NSWindow*)parentWindow + nibName:(NSString*)nibName + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + configuration:(BookmarkEditor::Configuration)configuration; + +// Run the bookmark editor as a modal sheet. Does not block. +- (void)runAsModalSheet; + +// Create a new folder at the end of the selected parent folder, give it +// an untitled name, and put it into editing mode. +- (IBAction)newFolder:(id)sender; + +// The cancel action will dismiss the dialog. Derived classes which +// override cancel:, must call this after accessing any dialog-related +// data. +- (IBAction)cancel:(id)sender; + +// The OK action will dismiss the dialog. This action is bound +// to the OK button of a dialog which presents a tree view of a profile's +// folder hierarchy and allows the creation of new folders within that tree. +// When the OK button is pressed, this function will: 1) call the derived +// class's -[willCommit] function, 2) create any new folders created by +// the user while the dialog is presented, 3) call the derived class's +// -[didCommit] function, and then 4) dismiss the dialog. At least one +// of -[willCommit] and -[didCommit] must be provided by the derived class +// and should return a NSNumber containing a BOOL or nil ('nil' means YES) +// indicating if the operation should be allowed to continue. +// Note: A derived class should not override the ok: action. +- (IBAction)ok:(id)sender; + +// Methods for use by derived classes only. + +// Determine and returns the rightmost selected/highlighted element (node) +// in the bookmark tree view if the tree view is showing, otherwise returns +// the original |parentNode_|. If the tree view is showing but nothing is +// selected then the root node is returned. +- (const BookmarkNode*)selectedNode; + +// Select/highlight the given node within the browser tree view. If the +// node is nil then select the bookmark bar node. Exposed for unit test. +- (void)selectNodeInBrowser:(const BookmarkNode*)node; + +// Notifications called when the BookmarkModel changes out from under me. +- (void)nodeRemoved:(const BookmarkNode*)node + fromParent:(const BookmarkNode*)parent; +- (void)modelChangedPreserveSelection:(BOOL)preserve; + +// Accessors +- (BookmarkModel*)bookmarkModel; +- (const BookmarkNode*)parentNode; + +@end + +// Describes the profile's bookmark folder structure: the folder name, the +// original BookmarkNode pointer (if the folder already exists), a BOOL +// indicating if the folder is new (meaning: created during this session +// but not yet committed to the bookmark structure), and an NSArray of +// child folder BookmarkFolderInfo's following this same structure. +@interface BookmarkFolderInfo : NSObject { + @private + NSString* folderName_; + const BookmarkNode* folderNode_; // weak + NSMutableArray* children_; + BOOL newFolder_; +} + +@property (nonatomic, copy) NSString* folderName; +@property (nonatomic, assign) const BookmarkNode* folderNode; +@property (nonatomic, retain) NSMutableArray* children; +@property (nonatomic, assign) BOOL newFolder; + +// Convenience creator for adding a new folder to the editor's bookmark +// structure. This folder will be added to the bookmark model when the +// user accepts the dialog. |folderName| must be provided. ++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName; + +// Designated initializer. |folderName| must be provided. For folders which +// already exist in the bookmark model, |folderNode| and |children| (if any +// children are already attached to this folder) must be provided and +// |newFolder| should be NO. For folders which the user has added during +// this session and which have not been committed yet, |newFolder| should be +// YES and |folderNode| and |children| should be NULL/nil. +- (id)initWithFolderName:(NSString*)folderName + folderNode:(const BookmarkNode*)folderNode + children:(NSMutableArray*)children + newFolder:(BOOL)newFolder; + +// Convenience creator used during construction of the editor's bookmark +// structure. |folderName| and |folderNode| must be provided. |children| +// is optional. Private: exposed here for unit testing purposes. ++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName + folderNode:(const BookmarkNode*)folderNode + children:(NSMutableArray*)children; + +@end + +@interface BookmarkEditorBaseController(TestingAPI) + +@property (nonatomic, readonly) BOOL okButtonEnabled; + +// Create any newly added folders. New folders are nodes in folderTreeArray +// which are marked as being new (i.e. their kFolderTreeNewFolderKey +// dictionary item is YES). This is called by -[ok:]. +- (void)createNewFolders; + +// Select the given bookmark node within the tree view. +- (void)selectTestNodeInBrowser:(const BookmarkNode*)node; + +// Return the dictionary for the folder selected in the tree. +- (BookmarkFolderInfo*)selectedFolder; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_BASE_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm new file mode 100644 index 0000000..ffc9d69 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm @@ -0,0 +1,604 @@ +// 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 <stack> + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h" + +#include "app/l10n_util.h" +#include "app/l10n_util_mac.h" +#include "base/logging.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/profiles/profile.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h" +#import "chrome/browser/ui/cocoa/browser_window_controller.h" +#include "grit/generated_resources.h" + +@interface BookmarkEditorBaseController () + +// Return the folder tree object for the given path. +- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path; + +// (Re)build the folder tree from the BookmarkModel's current state. +- (void)buildFolderTree; + +// Notifies the controller that the bookmark model has changed. +// |selection| specifies if the current selection should be +// maintained (usually YES). +- (void)modelChangedPreserveSelection:(BOOL)preserve; + +// Notifies the controller that a node has been removed. +- (void)nodeRemoved:(const BookmarkNode*)node + fromParent:(const BookmarkNode*)parent; + +// Given a folder node, collect an array containing BookmarkFolderInfos +// describing its subchildren which are also folders. +- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node; + +// Scan the folder tree stemming from the given tree folder and create +// any newly added folders. Pass down info for the folder which was +// selected before we began creating folders. +- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder + selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo; + +// Scan the folder tree looking for the given bookmark node and return +// the selection path thereto. +- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node; + +@end + +// static; implemented for each platform. Update this function for new +// classes derived from BookmarkEditorBaseController. +void BookmarkEditor::Show(gfx::NativeWindow parent_hwnd, + Profile* profile, + const BookmarkNode* parent, + const EditDetails& details, + Configuration configuration) { + BookmarkEditorBaseController* controller = nil; + if (details.type == EditDetails::NEW_FOLDER) { + controller = [[BookmarkAllTabsController alloc] + initWithParentWindow:parent_hwnd + profile:profile + parent:parent + configuration:configuration]; + } else { + controller = [[BookmarkEditorController alloc] + initWithParentWindow:parent_hwnd + profile:profile + parent:parent + node:details.existing_node + configuration:configuration]; + } + [controller runAsModalSheet]; +} + +// Adapter to tell BookmarkEditorBaseController when bookmarks change. +class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver { + public: + BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller) + : controller_(controller), + importing_(false) + { } + + virtual void Loaded(BookmarkModel* model) { + [controller_ modelChangedPreserveSelection:YES]; + } + + virtual void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) { + if (!importing_ && new_parent->GetChild(new_index)->is_folder()) + [controller_ modelChangedPreserveSelection:YES]; + } + + virtual void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + if (!importing_ && parent->GetChild(index)->is_folder()) + [controller_ modelChangedPreserveSelection:YES]; + } + + virtual void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) { + [controller_ nodeRemoved:node fromParent:parent]; + if (node->is_folder()) + [controller_ modelChangedPreserveSelection:NO]; + } + + virtual void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) { + if (!importing_ && node->is_folder()) + [controller_ modelChangedPreserveSelection:YES]; + } + + virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) { + if (!importing_) + [controller_ modelChangedPreserveSelection:YES]; + } + + virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node) { + // I care nothing for these 'favicons': I only show folders. + } + + virtual void BookmarkImportBeginning(BookmarkModel* model) { + importing_ = true; + } + + // Invoked after a batch import finishes. This tells observers to update + // themselves if they were waiting for the update to finish. + virtual void BookmarkImportEnding(BookmarkModel* model) { + importing_ = false; + [controller_ modelChangedPreserveSelection:YES]; + } + + private: + BookmarkEditorBaseController* controller_; // weak + bool importing_; +}; + + +#pragma mark - + +@implementation BookmarkEditorBaseController + +@synthesize initialName = initialName_; +@synthesize displayName = displayName_; +@synthesize okEnabled = okEnabled_; + +- (id)initWithParentWindow:(NSWindow*)parentWindow + nibName:(NSString*)nibName + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + configuration:(BookmarkEditor::Configuration)configuration { + NSString* nibpath = [mac_util::MainAppBundle() + pathForResource:nibName + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibpath owner:self])) { + parentWindow_ = parentWindow; + profile_ = profile; + parentNode_ = parent; + configuration_ = configuration; + initialName_ = [@"" retain]; + observer_.reset(new BookmarkEditorBaseControllerBridge(self)); + [self bookmarkModel]->AddObserver(observer_.get()); + } + return self; +} + +- (void)dealloc { + [self bookmarkModel]->RemoveObserver(observer_.get()); + [initialName_ release]; + [displayName_ release]; + [super dealloc]; +} + +- (void)awakeFromNib { + [self setDisplayName:[self initialName]]; + + if (configuration_ != BookmarkEditor::SHOW_TREE) { + // Remember the tree view's height; we will shrink our frame by that much. + NSRect frame = [[self window] frame]; + CGFloat browserHeight = [folderTreeView_ frame].size.height; + frame.size.height -= browserHeight; + frame.origin.y += browserHeight; + // Remove the folder tree and "new folder" button. + [folderTreeView_ removeFromSuperview]; + [newFolderButton_ removeFromSuperview]; + // Finally, commit the size change. + [[self window] setFrame:frame display:YES]; + } + + // Build up a tree of the current folder configuration. + [self buildFolderTree]; +} + +- (void)windowDidLoad { + if (configuration_ == BookmarkEditor::SHOW_TREE) { + [self selectNodeInBrowser:parentNode_]; + } +} + +/* TODO(jrg): +// Implementing this informal protocol allows us to open the sheet +// somewhere other than at the top of the window. NOTE: this means +// that I, the controller, am also the window's delegate. +- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet + usingRect:(NSRect)rect { + // adjust rect.origin.y to be the bottom of the toolbar + return rect; +} +*/ + +// TODO(jrg): consider NSModalSession. +- (void)runAsModalSheet { + // Lock down floating bar when in full-screen mode. Don't animate + // otherwise the pane will be misplaced. + [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] + lockBarVisibilityForOwner:self withAnimation:NO delay:NO]; + [NSApp beginSheet:[self window] + modalForWindow:parentWindow_ + modalDelegate:self + didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (BOOL)okEnabled { + return YES; +} + +- (IBAction)ok:(id)sender { + // At least one of these two functions should be provided by derived classes. + BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)]; + BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)]; + DCHECK(hasWillCommit || hasDidCommit); + BOOL shouldContinue = YES; + if (hasWillCommit) { + NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)]; + if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]]) + shouldContinue = [hasWillContinue boolValue]; + } + if (shouldContinue) + [self createNewFolders]; + if (hasDidCommit) { + NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)]; + if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]]) + shouldContinue = [hasDidContinue boolValue]; + } + if (shouldContinue) + [NSApp endSheet:[self window]]; +} + +- (IBAction)cancel:(id)sender { + [NSApp endSheet:[self window]]; +} + +- (void)didEndSheet:(NSWindow*)sheet + returnCode:(int)returnCode + contextInfo:(void*)contextInfo { + [sheet close]; + [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] + releaseBarVisibilityForOwner:self withAnimation:YES delay:NO]; +} + +- (void)windowWillClose:(NSNotification*)notification { + [self autorelease]; +} + +#pragma mark Folder Tree Management + +- (BookmarkModel*)bookmarkModel { + return profile_->GetBookmarkModel(); +} + +- (const BookmarkNode*)parentNode { + return parentNode_; +} + +- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath { + NSUInteger pathCount = [indexPath length]; + BookmarkFolderInfo* item = nil; + NSArray* treeNode = [self folderTreeArray]; + for (NSUInteger i = 0; i < pathCount; ++i) { + item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]]; + treeNode = [item children]; + } + return item; +} + +- (NSIndexPath*)selectedIndexPath { + NSIndexPath* selectedIndexPath = nil; + NSArray* selections = [self tableSelectionPaths]; + if ([selections count]) { + DCHECK([selections count] == 1); // Should be exactly one selection. + selectedIndexPath = [selections objectAtIndex:0]; + } + return selectedIndexPath; +} + +- (BookmarkFolderInfo*)selectedFolder { + BookmarkFolderInfo* item = nil; + NSIndexPath* selectedIndexPath = [self selectedIndexPath]; + if (selectedIndexPath) { + item = [self folderForIndexPath:selectedIndexPath]; + } + return item; +} + +- (const BookmarkNode*)selectedNode { + const BookmarkNode* selectedNode = NULL; + // Determine a new parent node only if the browser is showing. + if (configuration_ == BookmarkEditor::SHOW_TREE) { + BookmarkFolderInfo* folderInfo = [self selectedFolder]; + if (folderInfo) + selectedNode = [folderInfo folderNode]; + } else { + // If the tree is not showing then we use the original parent. + selectedNode = parentNode_; + } + return selectedNode; +} + +- (NSArray*)folderTreeArray { + return folderTreeArray_.get(); +} + +- (NSArray*)tableSelectionPaths { + return tableSelectionPaths_.get(); +} + +- (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath { + [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]]; +} + +- (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths { + tableSelectionPaths_.reset([tableSelectionPaths retain]); +} + +- (void)selectNodeInBrowser:(const BookmarkNode*)node { + DCHECK(configuration_ == BookmarkEditor::SHOW_TREE); + NSIndexPath* selectionPath = [self selectionPathForNode:node]; + [self willChangeValueForKey:@"okEnabled"]; + [self setTableSelectionPath:selectionPath]; + [self didChangeValueForKey:@"okEnabled"]; +} + +- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode { + // Back up the parent chaing for desiredNode, building up a stack + // of ancestor nodes. Then crawl down the folderTreeArray looking + // for each ancestor in order while building up the selectionPath. + std::stack<const BookmarkNode*> nodeStack; + BookmarkModel* model = profile_->GetBookmarkModel(); + const BookmarkNode* rootNode = model->root_node(); + const BookmarkNode* node = desiredNode; + while (node != rootNode) { + DCHECK(node); + nodeStack.push(node); + node = node->GetParent(); + } + NSUInteger stackSize = nodeStack.size(); + + NSIndexPath* path = nil; + NSArray* folders = [self folderTreeArray]; + while (!nodeStack.empty()) { + node = nodeStack.top(); + nodeStack.pop(); + // Find node in the current folders array. + NSUInteger i = 0; + for (BookmarkFolderInfo *folderInfo in folders) { + const BookmarkNode* testNode = [folderInfo folderNode]; + if (testNode == node) { + path = path ? [path indexPathByAddingIndex:i] : + [NSIndexPath indexPathWithIndex:i]; + folders = [folderInfo children]; + break; + } + ++i; + } + } + DCHECK([path length] == stackSize); + return path; +} + +- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node { + NSMutableArray* childFolders = nil; + int childCount = node->GetChildCount(); + for (int i = 0; i < childCount; ++i) { + const BookmarkNode* childNode = node->GetChild(i); + if (childNode->type() != BookmarkNode::URL) { + NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle()); + NSMutableArray* children = [self addChildFoldersFromNode:childNode]; + BookmarkFolderInfo* folderInfo = + [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName + folderNode:childNode + children:children]; + if (!childFolders) + childFolders = [NSMutableArray arrayWithObject:folderInfo]; + else + [childFolders addObject:folderInfo]; + } + } + return childFolders; +} + +- (void)buildFolderTree { + // Build up a tree of the current folder configuration. + BookmarkModel* model = profile_->GetBookmarkModel(); + const BookmarkNode* rootNode = model->root_node(); + NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode]; + DCHECK(baseArray); + [self willChangeValueForKey:@"folderTreeArray"]; + folderTreeArray_.reset([baseArray retain]); + [self didChangeValueForKey:@"folderTreeArray"]; +} + +- (void)modelChangedPreserveSelection:(BOOL)preserve { + const BookmarkNode* selectedNode = [self selectedNode]; + [self buildFolderTree]; + if (preserve && + selectedNode && + configuration_ == BookmarkEditor::SHOW_TREE) + [self selectNodeInBrowser:selectedNode]; +} + +- (void)nodeRemoved:(const BookmarkNode*)node + fromParent:(const BookmarkNode*)parent { + if (node->is_folder()) { + if (parentNode_ == node || parentNode_->HasAncestor(node)) { + parentNode_ = [self bookmarkModel]->GetBookmarkBarNode(); + if (configuration_ != BookmarkEditor::SHOW_TREE) { + // The user can't select a different folder, so just close up shop. + [self cancel:self]; + return; + } + } + + if (configuration_ == BookmarkEditor::SHOW_TREE) { + // For safety's sake, in case deleted node was an ancestor of selection, + // go back to a known safe place. + [self selectNodeInBrowser:parentNode_]; + } + } +} + +#pragma mark New Folder Handler + +- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo + selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo { + NSArray* subfolders = [folderInfo children]; + const BookmarkNode* parentNode = [folderInfo folderNode]; + DCHECK(parentNode); + NSUInteger i = 0; + for (BookmarkFolderInfo* subFolderInfo in subfolders) { + if ([subFolderInfo newFolder]) { + BookmarkModel* model = [self bookmarkModel]; + const BookmarkNode* newFolder = + model->AddGroup(parentNode, i, + base::SysNSStringToUTF16([subFolderInfo folderName])); + // Update our dictionary with the actual folder node just created. + [subFolderInfo setFolderNode:newFolder]; + [subFolderInfo setNewFolder:NO]; + // If the newly created folder was selected, update the selection path. + if (subFolderInfo == selectedFolderInfo) { + NSIndexPath* selectionPath = [self selectionPathForNode:newFolder]; + [self setTableSelectionPath:selectionPath]; + } + } + [self createNewFoldersForFolder:subFolderInfo + selectedFolderInfo:selectedFolderInfo]; + ++i; + } +} + +- (IBAction)newFolder:(id)sender { + // Create a new folder off of the selected folder node. + BookmarkFolderInfo* parentInfo = [self selectedFolder]; + if (parentInfo) { + NSIndexPath* selection = [self selectedIndexPath]; + NSString* newFolderName = + l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); + BookmarkFolderInfo* folderInfo = + [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName]; + [self willChangeValueForKey:@"folderTreeArray"]; + NSMutableArray* children = [parentInfo children]; + if (children) { + [children addObject:folderInfo]; + } else { + children = [NSMutableArray arrayWithObject:folderInfo]; + [parentInfo setChildren:children]; + } + [self didChangeValueForKey:@"folderTreeArray"]; + + // Expose the parent folder children. + [folderTreeView_ expandItem:parentInfo]; + + // Select the new folder node and put the folder name into edit mode. + selection = [selection indexPathByAddingIndex:[children count] - 1]; + [self setTableSelectionPath:selection]; + NSInteger row = [folderTreeView_ selectedRow]; + DCHECK(row >= 0); + [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES]; + } +} + +- (void)createNewFolders { + // Turn off notifications while "importing" folders (as created in the sheet). + observer_->BookmarkImportBeginning([self bookmarkModel]); + // Scan the tree looking for nodes marked 'newFolder' and create those nodes. + NSArray* folderTreeArray = [self folderTreeArray]; + for (BookmarkFolderInfo *folderInfo in folderTreeArray) { + [self createNewFoldersForFolder:folderInfo + selectedFolderInfo:[self selectedFolder]]; + } + // Notifications back on. + observer_->BookmarkImportEnding([self bookmarkModel]); +} + +#pragma mark For Unit Test Use Only + +- (BOOL)okButtonEnabled { + return [okButton_ isEnabled]; +} + +- (void)selectTestNodeInBrowser:(const BookmarkNode*)node { + [self selectNodeInBrowser:node]; +} + +@end // BookmarkEditorBaseController + +@implementation BookmarkFolderInfo + +@synthesize folderName = folderName_; +@synthesize folderNode = folderNode_; +@synthesize children = children_; +@synthesize newFolder = newFolder_; + ++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName + folderNode:(const BookmarkNode*)folderNode + children:(NSMutableArray*)children { + return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName + folderNode:folderNode + children:children + newFolder:NO] + autorelease]; +} + ++ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName { + return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName + folderNode:NULL + children:nil + newFolder:YES] + autorelease]; +} + +- (id)initWithFolderName:(NSString*)folderName + folderNode:(const BookmarkNode*)folderNode + children:(NSMutableArray*)children + newFolder:(BOOL)newFolder { + if ((self = [super init])) { + // A folderName is always required, and if newFolder is NO then there + // should be a folderNode. Children is optional. + DCHECK(folderName && (newFolder || folderNode)); + if (folderName && (newFolder || folderNode)) { + folderName_ = [folderName copy]; + folderNode_ = folderNode; + children_ = [children retain]; + newFolder_ = newFolder; + } else { + NOTREACHED(); // Invalid init. + [self release]; + self = nil; + } + } + return self; +} + +- (id)init { + NOTREACHED(); // Should never be called. + return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO]; +} + +- (void)dealloc { + [folderName_ release]; + [children_ release]; + [super dealloc]; +} + +// Implementing isEqual: allows the NSTreeController to preserve the selection +// and open/shut state of outline items when the data changes. +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[BookmarkFolderInfo class]] && + folderNode_ == [(BookmarkFolderInfo*)other folderNode]; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm new file mode 100644 index 0000000..6325346 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm @@ -0,0 +1,235 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "app/l10n_util_mac.h" +#include "base/scoped_nsobject.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "grit/generated_resources.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +class BookmarkEditorBaseControllerTest : public CocoaTest { + public: + BrowserTestHelper browser_helper_; + BookmarkEditorBaseController* controller_; // weak + const BookmarkNode* group_a_; + const BookmarkNode* group_b_; + const BookmarkNode* group_b_0_; + const BookmarkNode* group_b_3_; + const BookmarkNode* group_c_; + + BookmarkEditorBaseControllerTest() { + // Set up a small bookmark hierarchy, which will look as follows: + // a b c d + // a-0 b-0 c-0 + // a-1 b-00 c-1 + // a-2 b-1 c-2 + // b-2 c-3 + // b-3 + // b-30 + // b-31 + // b-4 + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + const BookmarkNode* root = model.GetBookmarkBarNode(); + group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a")); + model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com")); + model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com")); + model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com")); + + group_b_ = model.AddGroup(root, 1, ASCIIToUTF16("b")); + group_b_0_ = model.AddGroup(group_b_, 0, ASCIIToUTF16("b-0")); + model.AddURL(group_b_0_, 0, ASCIIToUTF16("bb-0"), GURL("http://bb-0.com")); + model.AddURL(group_b_, 1, ASCIIToUTF16("b-1"), GURL("http://b-1.com")); + model.AddURL(group_b_, 2, ASCIIToUTF16("b-2"), GURL("http://b-2.com")); + group_b_3_ = model.AddGroup(group_b_, 3, ASCIIToUTF16("b-3")); + model.AddURL(group_b_3_, 0, ASCIIToUTF16("b-30"), GURL("http://b-30.com")); + model.AddURL(group_b_3_, 1, ASCIIToUTF16("b-31"), GURL("http://b-31.com")); + model.AddURL(group_b_, 4, ASCIIToUTF16("b-4"), GURL("http://b-4.com")); + + group_c_ = model.AddGroup(root, 2, ASCIIToUTF16("c")); + model.AddURL(group_c_, 0, ASCIIToUTF16("c-0"), GURL("http://c-0.com")); + model.AddURL(group_c_, 1, ASCIIToUTF16("c-1"), GURL("http://c-1.com")); + model.AddURL(group_c_, 2, ASCIIToUTF16("c-2"), GURL("http://c-2.com")); + model.AddURL(group_c_, 3, ASCIIToUTF16("c-3"), GURL("http://c-3.com")); + + model.AddURL(root, 3, ASCIIToUTF16("d"), GURL("http://d-0.com")); + } + + virtual BookmarkEditorBaseController* CreateController() { + return [[BookmarkEditorBaseController alloc] + initWithParentWindow:test_window() + nibName:@"BookmarkAllTabs" + profile:browser_helper_.profile() + parent:group_b_0_ + configuration:BookmarkEditor::SHOW_TREE]; + } + + virtual void SetUp() { + CocoaTest::SetUp(); + controller_ = CreateController(); + EXPECT_TRUE([controller_ window]); + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } +}; + +TEST_F(BookmarkEditorBaseControllerTest, VerifyBookmarkTestModel) { + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + const BookmarkNode& root(*model.GetBookmarkBarNode()); + EXPECT_EQ(4, root.GetChildCount()); + // a + const BookmarkNode* child = root.GetChild(0); + EXPECT_EQ(3, child->GetChildCount()); + const BookmarkNode* subchild = child->GetChild(0); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + // b + child = root.GetChild(1); + EXPECT_EQ(5, child->GetChildCount()); + subchild = child->GetChild(0); + EXPECT_EQ(1, subchild->GetChildCount()); + const BookmarkNode* subsubchild = subchild->GetChild(0); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(3); + EXPECT_EQ(2, subchild->GetChildCount()); + subsubchild = subchild->GetChild(0); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subsubchild = subchild->GetChild(1); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subchild = child->GetChild(4); + EXPECT_EQ(0, subchild->GetChildCount()); + // c + child = root.GetChild(2); + EXPECT_EQ(4, child->GetChildCount()); + subchild = child->GetChild(0); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(3); + EXPECT_EQ(0, subchild->GetChildCount()); + // d + child = root.GetChild(3); + EXPECT_EQ(0, child->GetChildCount()); + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, NodeSelection) { + EXPECT_TRUE([controller_ folderTreeArray]); + [controller_ selectTestNodeInBrowser:group_b_3_]; + const BookmarkNode* node = [controller_ selectedNode]; + EXPECT_EQ(node, group_b_3_); + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, CreateFolder) { + EXPECT_EQ(2, group_b_3_->GetChildCount()); + [controller_ selectTestNodeInBrowser:group_b_3_]; + NSString* expectedName = + l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); + [controller_ setDisplayName:expectedName]; + [controller_ newFolder:nil]; + NSArray* selectionPaths = [controller_ tableSelectionPaths]; + EXPECT_EQ(1U, [selectionPaths count]); + NSIndexPath* selectionPath = [selectionPaths objectAtIndex:0]; + EXPECT_EQ(4U, [selectionPath length]); + BookmarkFolderInfo* newFolderInfo = [controller_ selectedFolder]; + EXPECT_TRUE(newFolderInfo); + NSString* newFolderName = [newFolderInfo folderName]; + EXPECT_NSEQ(expectedName, newFolderName); + [controller_ createNewFolders]; + // Verify that the tab folder was added to the new folder. + EXPECT_EQ(3, group_b_3_->GetChildCount()); + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, CreateTwoFolders) { + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + const BookmarkNode* bar = model->GetBookmarkBarNode(); + // Create 2 folders which are children of the bar. + [controller_ selectTestNodeInBrowser:bar]; + [controller_ newFolder:nil]; + [controller_ selectTestNodeInBrowser:bar]; + [controller_ newFolder:nil]; + // If we do NOT crash on createNewFolders, success! + // (e.g. http://crbug.com/47877 is fixed). + [controller_ createNewFolders]; + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, SelectedFolderDeleted) { + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + [controller_ selectTestNodeInBrowser:group_b_3_]; + EXPECT_EQ(group_b_3_, [controller_ selectedNode]); + + // Delete the selected node, and verify it's no longer selected: + model.Remove(group_b_, 3); + EXPECT_NE(group_b_3_, [controller_ selectedNode]); + + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, SelectedFoldersParentDeleted) { + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + const BookmarkNode* root = model.GetBookmarkBarNode(); + [controller_ selectTestNodeInBrowser:group_b_3_]; + EXPECT_EQ(group_b_3_, [controller_ selectedNode]); + + // Delete the selected node's parent, and verify it's no longer selected: + model.Remove(root, 1); + EXPECT_NE(group_b_3_, [controller_ selectedNode]); + + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorBaseControllerTest, FolderAdded) { + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + const BookmarkNode* root = model.GetBookmarkBarNode(); + + // Add a group node to the model, and verify it can be selected in the tree: + const BookmarkNode* group_added = model.AddGroup(root, 0, + ASCIIToUTF16("added")); + [controller_ selectTestNodeInBrowser:group_added]; + EXPECT_EQ(group_added, [controller_ selectedNode]); + + [controller_ cancel:nil]; +} + + +class BookmarkFolderInfoTest : public CocoaTest { }; + +TEST_F(BookmarkFolderInfoTest, Construction) { + NSMutableArray* children = [NSMutableArray arrayWithObject:@"child"]; + // We just need a pointer, and any pointer will do. + const BookmarkNode* fakeNode = + reinterpret_cast<const BookmarkNode*>(&children); + BookmarkFolderInfo* info = + [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:@"name" + folderNode:fakeNode + children:children]; + EXPECT_TRUE(info); + EXPECT_EQ([info folderName], @"name"); + EXPECT_EQ([info children], children); + EXPECT_EQ([info folderNode], fakeNode); +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h new file mode 100644 index 0000000..30f1c75 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h @@ -0,0 +1,36 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_ +#pragma once + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h" + +// A controller for the bookmark editor, opened by 1) Edit... from the +// context menu of a bookmark button, and 2) Bookmark this Page...'s Edit +// button. +@interface BookmarkEditorController : BookmarkEditorBaseController { + @private + const BookmarkNode* node_; // weak; owned by the model + scoped_nsobject<NSString> initialUrl_; + NSString* displayURL_; // Bound to a text field in the dialog. + IBOutlet NSTextField* urlField_; +} + +@property (nonatomic, copy) NSString* displayURL; + +- (id)initWithParentWindow:(NSWindow*)parentWindow + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + node:(const BookmarkNode*)node + configuration:(BookmarkEditor::Configuration)configuration; + +@end + +@interface BookmarkEditorController (UnitTesting) +- (NSColor *)urlFieldColor; +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_EDITOR_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm new file mode 100644 index 0000000..88ed4bf --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm @@ -0,0 +1,143 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" + +#include "app/l10n_util.h" +#include "base/string16.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" + +@interface BookmarkEditorController (Private) + +// Grab the url from the text field and convert. +- (GURL)GURLFromUrlField; + +@end + +@implementation BookmarkEditorController + +@synthesize displayURL = displayURL_; + ++ (NSSet*)keyPathsForValuesAffectingOkEnabled { + return [NSSet setWithObject:@"displayURL"]; +} + +- (id)initWithParentWindow:(NSWindow*)parentWindow + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + node:(const BookmarkNode*)node + configuration:(BookmarkEditor::Configuration)configuration { + if ((self = [super initWithParentWindow:parentWindow + nibName:@"BookmarkEditor" + profile:profile + parent:parent + configuration:configuration])) { + // "Add Page..." has no "node" so this may be NULL. + node_ = node; + } + return self; +} + +- (void)dealloc { + [displayURL_ release]; + [super dealloc]; +} + +- (void)awakeFromNib { + // Set text fields to match our bookmark. If the node is NULL we + // arrived here from an "Add Page..." item in a context menu. + if (node_) { + [self setInitialName:base::SysUTF16ToNSString(node_->GetTitle())]; + std::string url_string = node_->GetURL().possibly_invalid_spec(); + initialUrl_.reset([[NSString stringWithUTF8String:url_string.c_str()] + retain]); + } else { + initialUrl_.reset([@"" retain]); + } + [self setDisplayURL:initialUrl_]; + [super awakeFromNib]; +} + +- (void)nodeRemoved:(const BookmarkNode*)node + fromParent:(const BookmarkNode*)parent +{ + // Be conservative; it is needed (e.g. "Add Page...") + node_ = NULL; + [self cancel:self]; +} + +#pragma mark Bookmark Editing + +// If possible, return a valid GURL from the URL text field. +- (GURL)GURLFromUrlField { + NSString* url = [self displayURL]; + GURL newURL = GURL([url UTF8String]); + if (!newURL.is_valid()) { + // Mimic observed friendliness from Windows + newURL = GURL([[NSString stringWithFormat:@"http://%@", url] UTF8String]); + } + return newURL; +} + +// Enable the OK button if there is a valid URL. +- (BOOL)okEnabled { + BOOL okEnabled = NO; + if ([[self displayURL] length]) { + GURL newURL = [self GURLFromUrlField]; + okEnabled = (newURL.is_valid()) ? YES : NO; + } + if (okEnabled) + [urlField_ setBackgroundColor:[NSColor whiteColor]]; + else + [urlField_ setBackgroundColor:[NSColor colorWithCalibratedRed:1.0 + green:0.67 + blue:0.67 + alpha:1.0]]; + return okEnabled; +} + +// The the bookmark's URL is assumed to be valid (otherwise the OK button +// should not be enabled). Previously existing bookmarks for which the +// parent has not changed are updated in-place. Those for which the parent +// has changed are removed with a new node created under the new parent. +// Called by -[BookmarkEditorBaseController ok:]. +- (NSNumber*)didCommit { + NSString* name = [[self displayName] stringByTrimmingCharactersInSet: + [NSCharacterSet newlineCharacterSet]]; + string16 newTitle = base::SysNSStringToUTF16(name); + const BookmarkNode* newParentNode = [self selectedNode]; + GURL newURL = [self GURLFromUrlField]; + if (!newURL.is_valid()) { + // Shouldn't be reached -- OK button should be disabled if not valid! + NOTREACHED(); + return [NSNumber numberWithBool:NO]; + } + + // Determine where the new/replacement bookmark is to go. + BookmarkModel* model = [self bookmarkModel]; + // If there was an old node then we update the node, and move it to its new + // parent if the parent has changed (rather than deleting it from the old + // parent and adding to the new -- which also prevents the 'poofing' that + // occurs when a node is deleted). + if (node_) { + model->SetURL(node_, newURL); + model->SetTitle(node_, newTitle); + const BookmarkNode* oldParentNode = [self parentNode]; + if (newParentNode != oldParentNode) + model->Move(node_, newParentNode, newParentNode->GetChildCount()); + } else { + // Otherwise, add a new bookmark at the end of the newly selected folder. + model->AddURL(newParentNode, newParentNode->GetChildCount(), newTitle, + newURL); + } + return [NSNumber numberWithBool:YES]; +} + +- (NSColor *)urlFieldColor { + return [urlField_ backgroundColor]; +} + +@end // BookmarkEditorController + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm new file mode 100644 index 0000000..8f49c6d --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm @@ -0,0 +1,423 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/string16.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +class BookmarkEditorControllerTest : public CocoaTest { + public: + BrowserTestHelper browser_helper_; + const BookmarkNode* default_node_; + const BookmarkNode* default_parent_; + const char* default_name_; + string16 default_title_; + BookmarkEditorController* controller_; + + virtual void SetUp() { + CocoaTest::SetUp(); + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + default_parent_ = model->GetBookmarkBarNode(); + default_name_ = "http://www.zim-bop-a-dee.com/"; + default_title_ = ASCIIToUTF16("ooh title"); + const BookmarkNode* default_node = model->AddURL(default_parent_, 0, + default_title_, + GURL(default_name_)); + controller_ = [[BookmarkEditorController alloc] + initWithParentWindow:test_window() + profile:browser_helper_.profile() + parent:default_parent_ + node:default_node + configuration:BookmarkEditor::NO_TREE]; + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } +}; + +TEST_F(BookmarkEditorControllerTest, NoEdit) { + [controller_ cancel:nil]; + ASSERT_EQ(default_parent_->GetChildCount(), 1); + const BookmarkNode* child = default_parent_->GetChild(0); + EXPECT_EQ(child->GetTitle(), default_title_); + EXPECT_EQ(child->GetURL(), GURL(default_name_)); +} + +TEST_F(BookmarkEditorControllerTest, EditTitle) { + [controller_ setDisplayName:@"whamma jamma bamma"]; + [controller_ ok:nil]; + ASSERT_EQ(default_parent_->GetChildCount(), 1); + const BookmarkNode* child = default_parent_->GetChild(0); + EXPECT_EQ(child->GetTitle(), ASCIIToUTF16("whamma jamma bamma")); + EXPECT_EQ(child->GetURL(), GURL(default_name_)); +} + +TEST_F(BookmarkEditorControllerTest, EditURL) { + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ setDisplayURL:@"http://yellow-sneakers.com/"]; + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ ok:nil]; + ASSERT_EQ(default_parent_->GetChildCount(), 1); + const BookmarkNode* child = default_parent_->GetChild(0); + EXPECT_EQ(child->GetTitle(), default_title_); + EXPECT_EQ(child->GetURL(), GURL("http://yellow-sneakers.com/")); +} + +TEST_F(BookmarkEditorControllerTest, EditAndFixPrefix) { + [controller_ setDisplayURL:@"x"]; + [controller_ ok:nil]; + ASSERT_EQ(default_parent_->GetChildCount(), 1); + const BookmarkNode* child = default_parent_->GetChild(0); + EXPECT_TRUE(child->GetURL().is_valid()); +} + +TEST_F(BookmarkEditorControllerTest, NodeDeleted) { + // Delete the bookmark being edited and verify the sheet cancels itself: + ASSERT_TRUE([test_window() attachedSheet]); + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + model->Remove(default_parent_, 0); + ASSERT_FALSE([test_window() attachedSheet]); +} + +TEST_F(BookmarkEditorControllerTest, EditAndConfirmOKButton) { + // Confirm OK button enabled/disabled as appropriate: + // First test the URL. + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ setDisplayURL:@""]; + EXPECT_FALSE([controller_ okButtonEnabled]); + [controller_ setDisplayURL:@"http://www.cnn.com"]; + EXPECT_TRUE([controller_ okButtonEnabled]); + // Then test the name. + [controller_ setDisplayName:@""]; + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ setDisplayName:@" "]; + EXPECT_TRUE([controller_ okButtonEnabled]); + // Then little mix of both. + [controller_ setDisplayName:@"name"]; + EXPECT_TRUE([controller_ okButtonEnabled]); + [controller_ setDisplayURL:@""]; + EXPECT_FALSE([controller_ okButtonEnabled]); + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorControllerTest, GoodAndBadURLsChangeColor) { + // Confirm that the background color of the URL edit field changes + // based on whether it contains a valid or invalid URL. + [controller_ setDisplayURL:@"http://www.cnn.com"]; + NSColor *urlColorA = [controller_ urlFieldColor]; + EXPECT_TRUE(urlColorA); + [controller_ setDisplayURL:@""]; + NSColor *urlColorB = [controller_ urlFieldColor]; + EXPECT_TRUE(urlColorB); + EXPECT_NSNE(urlColorA, urlColorB); + [controller_ setDisplayURL:@"http://www.google.com"]; + [controller_ cancel:nil]; + urlColorB = [controller_ urlFieldColor]; + EXPECT_TRUE(urlColorB); + EXPECT_NSEQ(urlColorA, urlColorB); +} + +class BookmarkEditorControllerNoNodeTest : public CocoaTest { + public: + BrowserTestHelper browser_helper_; + BookmarkEditorController* controller_; + + virtual void SetUp() { + CocoaTest::SetUp(); + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + controller_ = [[BookmarkEditorController alloc] + initWithParentWindow:test_window() + profile:browser_helper_.profile() + parent:parent + node:NULL + configuration:BookmarkEditor::NO_TREE]; + + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } +}; + +TEST_F(BookmarkEditorControllerNoNodeTest, NoNodeNoTree) { + EXPECT_EQ(@"", [controller_ displayName]); + EXPECT_EQ(@"", [controller_ displayURL]); + EXPECT_FALSE([controller_ okButtonEnabled]); + [controller_ cancel:nil]; +} + +class BookmarkEditorControllerYesNodeTest : public CocoaTest { + public: + BrowserTestHelper browser_helper_; + string16 default_title_; + const char* url_name_; + BookmarkEditorController* controller_; + + virtual void SetUp() { + CocoaTest::SetUp(); + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + default_title_ = ASCIIToUTF16("wooh title"); + url_name_ = "http://www.zoom-baby-doo-da.com/"; + const BookmarkNode* node = model->AddURL(parent, 0, default_title_, + GURL(url_name_)); + controller_ = [[BookmarkEditorController alloc] + initWithParentWindow:test_window() + profile:browser_helper_.profile() + parent:parent + node:node + configuration:BookmarkEditor::NO_TREE]; + + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } +}; + +TEST_F(BookmarkEditorControllerYesNodeTest, YesNodeShowTree) { + EXPECT_NSEQ(base::SysUTF16ToNSString(default_title_), + [controller_ displayName]); + EXPECT_NSEQ([NSString stringWithCString:url_name_ + encoding:NSUTF8StringEncoding], + [controller_ displayURL]); + [controller_ cancel:nil]; +} + +class BookmarkEditorControllerTreeTest : public CocoaTest { + + public: + BrowserTestHelper browser_helper_; + BookmarkEditorController* controller_; + const BookmarkNode* group_a_; + const BookmarkNode* group_b_; + const BookmarkNode* group_bb_; + const BookmarkNode* group_c_; + const BookmarkNode* bookmark_bb_3_; + GURL bb3_url_1_; + GURL bb3_url_2_; + + BookmarkEditorControllerTreeTest() { + // Set up a small bookmark hierarchy, which will look as follows: + // a b c d + // a-0 b-0 c-0 + // a-1 bb-0 c-1 + // a-2 bb-1 c-2 + // bb-2 + // bb-3 + // bb-4 + // b-1 + // b-2 + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + const BookmarkNode* root = model.GetBookmarkBarNode(); + group_a_ = model.AddGroup(root, 0, ASCIIToUTF16("a")); + model.AddURL(group_a_, 0, ASCIIToUTF16("a-0"), GURL("http://a-0.com")); + model.AddURL(group_a_, 1, ASCIIToUTF16("a-1"), GURL("http://a-1.com")); + model.AddURL(group_a_, 2, ASCIIToUTF16("a-2"), GURL("http://a-2.com")); + + group_b_ = model.AddGroup(root, 1, ASCIIToUTF16("b")); + model.AddURL(group_b_, 0, ASCIIToUTF16("b-0"), GURL("http://b-0.com")); + group_bb_ = model.AddGroup(group_b_, 1, ASCIIToUTF16("bb")); + model.AddURL(group_bb_, 0, ASCIIToUTF16("bb-0"), GURL("http://bb-0.com")); + model.AddURL(group_bb_, 1, ASCIIToUTF16("bb-1"), GURL("http://bb-1.com")); + model.AddURL(group_bb_, 2, ASCIIToUTF16("bb-2"), GURL("http://bb-2.com")); + + // To find it later, this bookmark name must always have a URL + // of http://bb-3.com or https://bb-3.com + bb3_url_1_ = GURL("http://bb-3.com"); + bb3_url_2_ = GURL("https://bb-3.com"); + bookmark_bb_3_ = model.AddURL(group_bb_, 3, ASCIIToUTF16("bb-3"), + bb3_url_1_); + + model.AddURL(group_bb_, 4, ASCIIToUTF16("bb-4"), GURL("http://bb-4.com")); + model.AddURL(group_b_, 2, ASCIIToUTF16("b-1"), GURL("http://b-2.com")); + model.AddURL(group_b_, 3, ASCIIToUTF16("b-2"), GURL("http://b-3.com")); + + group_c_ = model.AddGroup(root, 2, ASCIIToUTF16("c")); + model.AddURL(group_c_, 0, ASCIIToUTF16("c-0"), GURL("http://c-0.com")); + model.AddURL(group_c_, 1, ASCIIToUTF16("c-1"), GURL("http://c-1.com")); + model.AddURL(group_c_, 2, ASCIIToUTF16("c-2"), GURL("http://c-2.com")); + model.AddURL(group_c_, 3, ASCIIToUTF16("c-3"), GURL("http://c-3.com")); + + model.AddURL(root, 3, ASCIIToUTF16("d"), GURL("http://d-0.com")); + } + + virtual BookmarkEditorController* CreateController() { + return [[BookmarkEditorController alloc] + initWithParentWindow:test_window() + profile:browser_helper_.profile() + parent:group_bb_ + node:bookmark_bb_3_ + configuration:BookmarkEditor::SHOW_TREE]; + } + + virtual void SetUp() { + controller_ = CreateController(); + [controller_ runAsModalSheet]; + } + + virtual void TearDown() { + controller_ = NULL; + CocoaTest::TearDown(); + } + + // After changing a node, pointers to the node may be invalid. This + // is because the node itself may not be updated; it may removed and + // a new one is added in that location. (Implementation detail of + // BookmarkEditorController). This method updates the class's + // bookmark_bb_3_ so that it points to the new node for testing. + void UpdateBB3() { + std::vector<const BookmarkNode*> nodes; + BookmarkModel* model = browser_helper_.profile()->GetBookmarkModel(); + model->GetNodesByURL(bb3_url_1_, &nodes); + if (nodes.size() == 0) + model->GetNodesByURL(bb3_url_2_, &nodes); + DCHECK(nodes.size()); + bookmark_bb_3_ = nodes[0]; + } + +}; + +TEST_F(BookmarkEditorControllerTreeTest, VerifyBookmarkTestModel) { + BookmarkModel& model(*(browser_helper_.profile()->GetBookmarkModel())); + model.root_node(); + const BookmarkNode& root(*model.GetBookmarkBarNode()); + EXPECT_EQ(4, root.GetChildCount()); + const BookmarkNode* child = root.GetChild(0); + EXPECT_EQ(3, child->GetChildCount()); + const BookmarkNode* subchild = child->GetChild(0); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + + child = root.GetChild(1); + EXPECT_EQ(4, child->GetChildCount()); + subchild = child->GetChild(0); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(5, subchild->GetChildCount()); + const BookmarkNode* subsubchild = subchild->GetChild(0); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subsubchild = subchild->GetChild(1); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subsubchild = subchild->GetChild(2); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subsubchild = subchild->GetChild(3); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subsubchild = subchild->GetChild(4); + EXPECT_EQ(0, subsubchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(3); + EXPECT_EQ(0, subchild->GetChildCount()); + + child = root.GetChild(2); + EXPECT_EQ(4, child->GetChildCount()); + subchild = child->GetChild(0); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(1); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(2); + EXPECT_EQ(0, subchild->GetChildCount()); + subchild = child->GetChild(3); + EXPECT_EQ(0, subchild->GetChildCount()); + + child = root.GetChild(3); + EXPECT_EQ(0, child->GetChildCount()); + [controller_ cancel:nil]; +} + +TEST_F(BookmarkEditorControllerTreeTest, RenameBookmarkInPlace) { + const BookmarkNode* oldParent = bookmark_bb_3_->GetParent(); + [controller_ setDisplayName:@"NEW NAME"]; + [controller_ ok:nil]; + UpdateBB3(); + const BookmarkNode* newParent = bookmark_bb_3_->GetParent(); + ASSERT_EQ(newParent, oldParent); + int childIndex = newParent->IndexOfChild(bookmark_bb_3_); + ASSERT_EQ(3, childIndex); +} + +TEST_F(BookmarkEditorControllerTreeTest, ChangeBookmarkURLInPlace) { + const BookmarkNode* oldParent = bookmark_bb_3_->GetParent(); + [controller_ setDisplayURL:@"https://bb-3.com"]; + [controller_ ok:nil]; + UpdateBB3(); + const BookmarkNode* newParent = bookmark_bb_3_->GetParent(); + ASSERT_EQ(newParent, oldParent); + int childIndex = newParent->IndexOfChild(bookmark_bb_3_); + ASSERT_EQ(3, childIndex); +} + +TEST_F(BookmarkEditorControllerTreeTest, ChangeBookmarkGroup) { + [controller_ selectTestNodeInBrowser:group_c_]; + [controller_ ok:nil]; + UpdateBB3(); + const BookmarkNode* parent = bookmark_bb_3_->GetParent(); + ASSERT_EQ(parent, group_c_); + int childIndex = parent->IndexOfChild(bookmark_bb_3_); + ASSERT_EQ(4, childIndex); +} + +TEST_F(BookmarkEditorControllerTreeTest, ChangeNameAndBookmarkGroup) { + [controller_ setDisplayName:@"NEW NAME"]; + [controller_ selectTestNodeInBrowser:group_c_]; + [controller_ ok:nil]; + UpdateBB3(); + const BookmarkNode* parent = bookmark_bb_3_->GetParent(); + ASSERT_EQ(parent, group_c_); + int childIndex = parent->IndexOfChild(bookmark_bb_3_); + ASSERT_EQ(4, childIndex); + EXPECT_EQ(bookmark_bb_3_->GetTitle(), ASCIIToUTF16("NEW NAME")); +} + +TEST_F(BookmarkEditorControllerTreeTest, AddFolderWithGroupSelected) { + // Folders are NOT added unless the OK button is pressed. + [controller_ newFolder:nil]; + [controller_ cancel:nil]; + EXPECT_EQ(5, group_bb_->GetChildCount()); +} + +class BookmarkEditorControllerTreeNoNodeTest : + public BookmarkEditorControllerTreeTest { + public: + virtual BookmarkEditorController* CreateController() { + return [[BookmarkEditorController alloc] + initWithParentWindow:test_window() + profile:browser_helper_.profile() + parent:group_bb_ + node:nil + configuration:BookmarkEditor::SHOW_TREE]; + } + +}; + +TEST_F(BookmarkEditorControllerTreeNoNodeTest, NewBookmarkNoNode) { + [controller_ setDisplayName:@"NEW BOOKMARK"]; + [controller_ setDisplayURL:@"http://NEWURL.com"]; + [controller_ ok:nil]; + const BookmarkNode* new_node = group_bb_->GetChild(5); + ASSERT_EQ(0, new_node->GetChildCount()); + EXPECT_EQ(new_node->GetTitle(), ASCIIToUTF16("NEW BOOKMARK")); + EXPECT_EQ(new_node->GetURL(), GURL("http://NEWURL.com")); +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h new file mode 100644 index 0000000..e2af266 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h @@ -0,0 +1,50 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +@class BookmarkButton; +@protocol BookmarkButtonControllerProtocol; +class BookmarkNode; + +// Target (in the target/action sense) of a bookmark folder button. +// Since ObjC doesn't have multiple inheritance we use has-a instead +// of is-a to share behavior between the BookmarkBarFolderController +// (NSWindowController) and the BookmarkBarController +// (NSViewController). +// +// This class is unit tested in the context of a BookmarkBarController. +@interface BookmarkFolderTarget : NSObject { + // The owner of the bookmark folder button + id<BookmarkButtonControllerProtocol> controller_; // weak +} + +- (id)initWithController:(id<BookmarkButtonControllerProtocol>)controller; + +// Main IBAction for a button click. +- (IBAction)openBookmarkFolderFromButton:(id)sender; + +// Copies the given bookmark node to the given pasteboard, declaring appropriate +// types (to paste a URL with a title). +- (void)copyBookmarkNode:(const BookmarkNode*)node + toPasteboard:(NSPasteboard*)pboard; + +// Fill the given pasteboard with appropriate data when the given button is +// dragged. Since the delegate has no way of providing pasteboard data later, +// all data must actually be put into the pasteboard and not merely promised. +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button; + +@end + +// The (internal) |NSPasteboard| type string for bookmark button drags, used for +// dragging buttons around the bookmark bar. The data for this type is just a +// pointer to the |BookmarkButton| being dragged. +extern NSString* kBookmarkButtonDragType; + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_FOLDER_TARGET_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm new file mode 100644 index 0000000..95531b2 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm @@ -0,0 +1,118 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" + +#include "base/logging.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#import "chrome/browser/ui/cocoa/event_utils.h" +#import "third_party/mozilla/NSPasteboard+Utils.h" + +NSString* kBookmarkButtonDragType = @"ChromiumBookmarkButtonDragType"; + +@implementation BookmarkFolderTarget + +- (id)initWithController:(id<BookmarkButtonControllerProtocol>)controller { + if ((self = [super init])) { + controller_ = controller; + } + return self; +} + +// This IBAction is called when the user clicks (mouseUp, really) on a +// "folder" bookmark button. (In this context, "Click" does not +// include right-click to open a context menu which follows a +// different path). Scenarios when folder X is clicked: +// *Predicate* *Action* +// (nothing) Open Folder X +// Folder X open Close folder X +// Folder Y open Close Y, open X +// Cmd-click Open All with proper disposition +// +// Note complication in which a click-drag engages drag and drop, not +// a click-to-open. Thus the path to get here is a little twisted. +- (IBAction)openBookmarkFolderFromButton:(id)sender { + DCHECK(sender); + // Watch out for a modifier click. For example, command-click + // should open all. + // + // NOTE: we cannot use [[sender cell] mouseDownFlags] because we + // thwart the normal mouse click mechanism to make buttons + // draggable. Thus we must use [NSApp currentEvent]. + // + // Holding command while using the scroll wheel (or moving around + // over a bookmark folder) can confuse us. Unless we check the + // event type, we are not sure if this is an "open folder" due to a + // hover-open or "open folder" due to a click. It doesn't matter + // (both do the same thing) unless a modifier is held, since + // command-click should "open all" but command-move should not. + // WindowOpenDispositionFromNSEvent does not consider the event + // type; only the modifiers. Thus the need for an extra + // event-type-check here. + DCHECK([sender bookmarkNode]->is_folder()); + NSEvent* event = [NSApp currentEvent]; + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent(event); + if (([event type] != NSMouseEntered) && + ([event type] != NSMouseMoved) && + ([event type] != NSScrollWheel) && + (disposition == NEW_BACKGROUND_TAB)) { + [controller_ closeAllBookmarkFolders]; + [controller_ openAll:[sender bookmarkNode] disposition:disposition]; + return; + } + + // If click on same folder, close it and be done. + // Else we clicked on a different folder so more work to do. + if ([[controller_ folderController] parentButton] == sender) { + [controller_ closeBookmarkFolder:controller_]; + return; + } + + [controller_ addNewFolderControllerWithParentButton:sender]; +} + +- (void)copyBookmarkNode:(const BookmarkNode*)node + toPasteboard:(NSPasteboard*)pboard { + if (!node) { + NOTREACHED(); + return; + } + + if (node->is_folder()) { + // TODO(viettrungluu): I'm not sure what we should do, so just declare the + // "additional" types we're given for now. Maybe we want to add a list of + // URLs? Would we then have to recurse if there were subfolders? + // In the meanwhile, we *must* set it to a known state. (If this survives to + // a 10.6-only release, it can be replaced with |-clearContents|.) + [pboard declareTypes:[NSArray array] owner:nil]; + } else { + const std::string spec = node->GetURL().spec(); + NSString* url = base::SysUTF8ToNSString(spec); + NSString* title = base::SysUTF16ToNSString(node->GetTitle()); + [pboard declareURLPasteboardWithAdditionalTypes:[NSArray array] + owner:nil]; + [pboard setDataForURL:url title:title]; + } +} + +- (void)fillPasteboard:(NSPasteboard*)pboard + forDragOfButton:(BookmarkButton*)button { + if (const BookmarkNode* node = [button bookmarkNode]) { + // Put the bookmark information into the pasteboard, and then write our own + // data for |kBookmarkButtonDragType|. + [self copyBookmarkNode:node toPasteboard:pboard]; + [pboard addTypes:[NSArray arrayWithObject:kBookmarkButtonDragType] + owner:nil]; + [pboard setData:[NSData dataWithBytes:&button length:sizeof(button)] + forType:kBookmarkButtonDragType]; + } else { + NOTREACHED(); + } +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm new file mode 100644 index 0000000..0142bfb --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm @@ -0,0 +1,125 @@ +// 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 "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_button.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#include "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" +#import "third_party/ocmock/OCMock/OCMock.h" + +@interface OCMockObject(PreventRetainCycle) +- (void)clearRecordersAndExpectations; +@end + +@implementation OCMockObject(PreventRetainCycle) + +// We need a mechanism to clear the invocation handlers to break a +// retain cycle (see below; search for "retain cycle"). +- (void)clearRecordersAndExpectations { + [recorders removeAllObjects]; + [expectations removeAllObjects]; +} + +@end + + +class BookmarkFolderTargetTest : public CocoaTest { + public: + virtual void SetUp() { + CocoaTest::SetUp(); + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + bmbNode_ = model->GetBookmarkBarNode(); + } + virtual void TearDown() { + pool_.Recycle(); + CocoaTest::TearDown(); + } + + BrowserTestHelper helper_; + const BookmarkNode* bmbNode_; + base::mac::ScopedNSAutoreleasePool pool_; +}; + +TEST_F(BookmarkFolderTargetTest, StartWithNothing) { + // Need a fake "button" which has a bookmark node. + id sender = [OCMockObject mockForClass:[BookmarkButton class]]; + [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode]; + + // Fake controller + id controller = [OCMockObject mockForClass:[BookmarkBarFolderController + class]]; + // No current folder + [[[controller stub] andReturn:nil] folderController]; + + // Make sure we get an addNew + [[controller expect] addNewFolderControllerWithParentButton:sender]; + + scoped_nsobject<BookmarkFolderTarget> target( + [[BookmarkFolderTarget alloc] initWithController:controller]); + + [target openBookmarkFolderFromButton:sender]; + [controller verify]; +} + +TEST_F(BookmarkFolderTargetTest, ReopenSameFolder) { + // Need a fake "button" which has a bookmark node. + id sender = [OCMockObject mockForClass:[BookmarkButton class]]; + [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode]; + + // Fake controller + id controller = [OCMockObject mockForClass:[BookmarkBarFolderController + class]]; + // YES a current folder. Self-mock that as well, so "same" will be + // true. Note this creates a retain cycle in OCMockObject; we + // accomodate at the end of this function. + [[[controller stub] andReturn:controller] folderController]; + [[[controller stub] andReturn:sender] parentButton]; + + // The folder is open, so a click should close just that folder (and + // any subfolders). + [[controller expect] closeBookmarkFolder:controller]; + + scoped_nsobject<BookmarkFolderTarget> target( + [[BookmarkFolderTarget alloc] initWithController:controller]); + + [target openBookmarkFolderFromButton:sender]; + [controller verify]; + + // Our use of OCMockObject means an object can return itself. This + // creates a retain cycle, since OCMock retains all objects used in + // mock creation. Clear out the invocation handlers of all + // OCMockRecorders we used to break the cycles. + [controller clearRecordersAndExpectations]; +} + +TEST_F(BookmarkFolderTargetTest, ReopenNotSame) { + // Need a fake "button" which has a bookmark node. + id sender = [OCMockObject mockForClass:[BookmarkButton class]]; + [[[sender stub] andReturnValue:OCMOCK_VALUE(bmbNode_)] bookmarkNode]; + + // Fake controller + id controller = [OCMockObject mockForClass:[BookmarkBarFolderController + class]]; + // YES a current folder but NOT same. + [[[controller stub] andReturn:controller] folderController]; + [[[controller stub] andReturn:nil] parentButton]; + + // Insure the controller gets a chance to decide which folders to + // close and open. + [[controller expect] addNewFolderControllerWithParentButton:sender]; + + scoped_nsobject<BookmarkFolderTarget> target( + [[BookmarkFolderTarget alloc] initWithController:controller]); + + [target openBookmarkFolderFromButton:sender]; + [controller verify]; + + // Break retain cycles. + [controller clearRecordersAndExpectations]; +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h new file mode 100644 index 0000000..d4ac001 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h @@ -0,0 +1,20 @@ +// 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 <Cocoa/Cocoa.h> + +#include "base/basictypes.h" + + +// The context menu for bookmark buttons needs to know which +// BookmarkNode it is talking about. For example, "Open All" is +// disabled if the bookmark node is a folder and has no children. +@interface BookmarkMenu : NSMenu { + @private + int64 id_; // id of the bookmark node we represent. +} +- (void)setRepresentedObject:(id)object; +@property (nonatomic) int64 id; +@end + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm new file mode 100644 index 0000000..274edc7 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm @@ -0,0 +1,22 @@ +// 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/ui/cocoa/bookmarks/bookmark_menu.h" + + +@implementation BookmarkMenu + +@synthesize id = id_; + +// Convention in the bookmark bar controller: the bookmark button +// cells have a BookmarkNode as their represented object. This object +// is placed in a BookmarkMenu at the time a cell is asked for its +// menu. +- (void)setRepresentedObject:(id)object { + if ([object isKindOfClass:[NSNumber class]]) { + id_ = static_cast<int64>([object longLongValue]); + } +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h new file mode 100644 index 0000000..db64b2c --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h @@ -0,0 +1,123 @@ +// 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. + +// C++ controller for the bookmark menu; one per AppController (which +// means there is only one). When bookmarks are changed, this class +// takes care of updating Cocoa bookmark menus. This is not named +// BookmarkMenuController to help avoid confusion between languages. +// This class needs to be C++, not ObjC, since it derives from +// BookmarkModelObserver. +// +// Most Chromium Cocoa menu items are static from a nib (e.g. New +// Tab), but may be enabled/disabled under certain circumstances +// (e.g. Cut and Paste). In addition, most Cocoa menu items have +// firstResponder: as a target. Unusually, bookmark menu items are +// created dynamically. They also have a target of +// BookmarkMenuCocoaController instead of firstResponder. +// See BookmarkMenuBridge::AddNodeToMenu()). + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_ +#pragma once + +#include <map> + +#include "base/scoped_nsobject.h" +#include "chrome/browser/bookmarks/bookmark_model_observer.h" + +class BookmarkNode; +class Profile; +@class NSImage; +@class NSMenu; +@class NSMenuItem; +@class BookmarkMenuCocoaController; + +class BookmarkMenuBridge : public BookmarkModelObserver { + public: + BookmarkMenuBridge(Profile* profile); + virtual ~BookmarkMenuBridge(); + + // Overridden from BookmarkModelObserver + virtual void Loaded(BookmarkModel* model); + virtual void BookmarkModelBeingDeleted(BookmarkModel* model); + virtual void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index); + virtual void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index); + virtual void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node); + virtual void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node); + virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node); + virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node); + + // Rebuilds the bookmark menu, if it has been marked invalid. + void UpdateMenu(NSMenu* bookmark_menu); + + // I wish I had a "friend @class" construct. + BookmarkModel* GetBookmarkModel(); + Profile* GetProfile(); + + protected: + // Clear all bookmarks from the given bookmark menu. + void ClearBookmarkMenu(NSMenu* menu); + + // Mark the bookmark menu as being invalid. + void InvalidateMenu() { menuIsValid_ = false; } + + // Helper for adding the node as a submenu to the menu with the + // given title. + void AddNodeAsSubmenu(NSMenu* menu, + const BookmarkNode* node, + NSString* title); + + // Helper for recursively adding items to our bookmark menu + // All children of |node| will be added to |menu|. + // TODO(jrg): add a counter to enforce maximum nodes added + void AddNodeToMenu(const BookmarkNode* node, NSMenu* menu); + + // This configures an NSMenuItem with all the data from a BookmarkNode. This + // is used to update existing menu items, as well as to configure newly + // created ones, like in AddNodeToMenu(). + // |set_title| is optional since it is only needed when we get a + // node changed notification. On initial build of the menu we set + // the title as part of alloc/init. + void ConfigureMenuItem(const BookmarkNode* node, NSMenuItem* item, + bool set_title); + + // Returns the NSMenuItem for a given BookmarkNode. + NSMenuItem* MenuItemForNode(const BookmarkNode* node); + + // Return the Bookmark menu. + virtual NSMenu* BookmarkMenu(); + + // Start watching the bookmarks for changes. + void ObserveBookmarkModel(); + + private: + friend class BookmarkMenuBridgeTest; + + // True iff the menu is up-to-date with the actual BookmarkModel. + bool menuIsValid_; + + Profile* profile_; // weak + BookmarkMenuCocoaController* controller_; // strong + + // The folder image so we can use one copy for all. + scoped_nsobject<NSImage> folder_image_; + + // In order to appropriately update items in the bookmark menu, without + // forcing a rebuild, map the model's nodes to menu items. + std::map<const BookmarkNode*, NSMenuItem*> bookmark_nodes_; +}; + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_BRIDGE_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm new file mode 100644 index 0000000..3792fe3 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm @@ -0,0 +1,253 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <AppKit/AppKit.h> + +#include "app/l10n_util.h" +#include "app/resource_bundle.h" +#include "app/mac/nsimage_cache.h" +#include "base/sys_string_conversions.h" +#import "chrome/browser/app_controller_mac.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/profiles/profile_manager.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_list.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" +#include "grit/generated_resources.h" +#include "grit/theme_resources.h" +#include "skia/ext/skia_utils_mac.h" + +BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile) + : menuIsValid_(false), + profile_(profile), + controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this]) { + if (GetBookmarkModel()) + ObserveBookmarkModel(); +} + +BookmarkMenuBridge::~BookmarkMenuBridge() { + BookmarkModel *model = GetBookmarkModel(); + if (model) + model->RemoveObserver(this); + [controller_ release]; +} + +NSMenu* BookmarkMenuBridge::BookmarkMenu() { + return [controller_ menu]; +} + +void BookmarkMenuBridge::Loaded(BookmarkModel* model) { + InvalidateMenu(); +} + +void BookmarkMenuBridge::UpdateMenu(NSMenu* bookmark_menu) { + DCHECK(bookmark_menu); + if (menuIsValid_) + return; + BookmarkModel* model = GetBookmarkModel(); + if (!model || !model->IsLoaded()) + return; + + if (!folder_image_) { + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + folder_image_.reset( + [rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER) retain]); + } + + ClearBookmarkMenu(bookmark_menu); + + // Add bookmark bar items, if any. + const BookmarkNode* barNode = model->GetBookmarkBarNode(); + CHECK(barNode); + if (barNode->GetChildCount()) { + [bookmark_menu addItem:[NSMenuItem separatorItem]]; + AddNodeToMenu(barNode, bookmark_menu); + } + + // Create a submenu for "other bookmarks", and fill it in. + NSString* other_items_title = + l10n_util::GetNSString(IDS_BOOMARK_BAR_OTHER_FOLDER_NAME); + [bookmark_menu addItem:[NSMenuItem separatorItem]]; + AddNodeAsSubmenu(bookmark_menu, + model->other_node(), + other_items_title); + + menuIsValid_ = true; +} + +void BookmarkMenuBridge::BookmarkModelBeingDeleted(BookmarkModel* model) { + NSMenu* bookmark_menu = BookmarkMenu(); + if (bookmark_menu == nil) + return; + + ClearBookmarkMenu(bookmark_menu); +} + +void BookmarkMenuBridge::BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) { + InvalidateMenu(); +} + +void BookmarkMenuBridge::BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + InvalidateMenu(); +} + +void BookmarkMenuBridge::BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) { + InvalidateMenu(); +} + +void BookmarkMenuBridge::BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) { + NSMenuItem* item = MenuItemForNode(node); + if (item) + ConfigureMenuItem(node, item, true); +} + +void BookmarkMenuBridge::BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node) { + NSMenuItem* item = MenuItemForNode(node); + if (item) + ConfigureMenuItem(node, item, false); +} + +void BookmarkMenuBridge::BookmarkNodeChildrenReordered( + BookmarkModel* model, const BookmarkNode* node) { + InvalidateMenu(); +} + +// Watch for changes. +void BookmarkMenuBridge::ObserveBookmarkModel() { + BookmarkModel* model = GetBookmarkModel(); + model->AddObserver(this); + if (model->IsLoaded()) + Loaded(model); +} + +BookmarkModel* BookmarkMenuBridge::GetBookmarkModel() { + if (!profile_) + return NULL; + return profile_->GetBookmarkModel(); +} + +Profile* BookmarkMenuBridge::GetProfile() { + return profile_; +} + +void BookmarkMenuBridge::ClearBookmarkMenu(NSMenu* menu) { + bookmark_nodes_.clear(); + // Recursively delete all menus that look like a bookmark. Assume + // all items with submenus contain only bookmarks. Also delete all + // separator items since we explicirly add them back in. This should + // deletes everything except the first item ("Add Bookmark..."). + NSArray* items = [menu itemArray]; + for (NSMenuItem* item in items) { + // Convention: items in the bookmark list which are bookmarks have + // an action of openBookmarkMenuItem:. Also, assume all items + // with submenus are submenus of bookmarks. + if (([item action] == @selector(openBookmarkMenuItem:)) || + [item hasSubmenu] || + [item isSeparatorItem]) { + // This will eventually [obj release] all its kids, if it has + // any. + [menu removeItem:item]; + } else { + // Leave it alone. + } + } +} + +void BookmarkMenuBridge::AddNodeAsSubmenu(NSMenu* menu, + const BookmarkNode* node, + NSString* title) { + NSMenuItem* items = [[[NSMenuItem alloc] + initWithTitle:title + action:nil + keyEquivalent:@""] autorelease]; + [items setImage:folder_image_]; + [menu addItem:items]; + NSMenu* other_submenu = [[[NSMenu alloc] initWithTitle:title] + autorelease]; + [menu setSubmenu:other_submenu forItem:items]; + AddNodeToMenu(node, other_submenu); +} + +// TODO(jrg): limit the number of bookmarks in the menubar? +void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node, NSMenu* menu) { + int child_count = node->GetChildCount(); + if (!child_count) { + NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU); + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:empty_string + action:nil + keyEquivalent:@""] autorelease]; + [menu addItem:item]; + } else for (int i = 0; i < child_count; i++) { + const BookmarkNode* child = node->GetChild(i); + NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child]; + NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title + action:nil + keyEquivalent:@""] autorelease]; + [menu addItem:item]; + bookmark_nodes_[child] = item; + if (child->is_folder()) { + [item setImage:folder_image_]; + NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; + [menu setSubmenu:submenu forItem:item]; + AddNodeToMenu(child, submenu); // recursive call + } else { + ConfigureMenuItem(child, item, false); + } + } +} + +void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node, + NSMenuItem* item, + bool set_title) { + if (set_title) { + NSString* title = [BookmarkMenuCocoaController menuTitleForNode:node]; + [item setTitle:title]; + } + [item setTarget:controller_]; + [item setAction:@selector(openBookmarkMenuItem:)]; + [item setTag:node->id()]; + // Add a tooltip + std::string url_string = node->GetURL().possibly_invalid_spec(); + NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", + base::SysUTF16ToNSString(node->GetTitle()), + url_string.c_str()]; + [item setToolTip:tooltip]; + + // Check to see if we have a favicon. + NSImage* favicon = nil; + BookmarkModel* model = GetBookmarkModel(); + if (model) { + const SkBitmap& bitmap = model->GetFavIcon(node); + if (!bitmap.isNull()) + favicon = gfx::SkBitmapToNSImage(bitmap); + } + // Either we do not have a loaded favicon or the conversion from SkBitmap + // failed. Use the default site image instead. + if (!favicon) + favicon = app::mac::GetCachedImageWithName(@"nav.pdf"); + [item setImage:favicon]; +} + +NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) { + if (!node) + return nil; + std::map<const BookmarkNode*, NSMenuItem*>::iterator it = + bookmark_nodes_.find(node); + if (it == bookmark_nodes_.end()) + return nil; + return it->second; +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm new file mode 100644 index 0000000..cec14eb --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm @@ -0,0 +1,317 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <AppKit/AppKit.h> + +#import "base/scoped_nsobject.h" +#include "base/string16.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#include "chrome/browser/ui/browser.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +class TestBookmarkMenuBridge : public BookmarkMenuBridge { + public: + TestBookmarkMenuBridge(Profile* profile) + : BookmarkMenuBridge(profile), + menu_([[NSMenu alloc] initWithTitle:@"test"]) { + } + virtual ~TestBookmarkMenuBridge() {} + + scoped_nsobject<NSMenu> menu_; + + protected: + // Overridden from BookmarkMenuBridge. + virtual NSMenu* BookmarkMenu() { + return menu_; + } +}; + +// TODO(jrg): see refactor comment in bookmark_bar_state_controller_unittest.mm +class BookmarkMenuBridgeTest : public PlatformTest { + public: + + void SetUp() { + bridge_.reset(new TestBookmarkMenuBridge(browser_test_helper_.profile())); + EXPECT_TRUE(bridge_.get()); + } + + // We are a friend of BookmarkMenuBridge (and have access to + // protected methods), but none of the classes generated by TEST_F() + // are. This (and AddNodeToMenu()) are simple wrappers to let + // derived test classes have access to protected methods. + void ClearBookmarkMenu(BookmarkMenuBridge* bridge, NSMenu* menu) { + bridge->ClearBookmarkMenu(menu); + } + + void InvalidateMenu() { bridge_->InvalidateMenu(); } + bool menu_is_valid() { return bridge_->menuIsValid_; } + + void AddNodeToMenu(BookmarkMenuBridge* bridge, const BookmarkNode* root, + NSMenu* menu) { + bridge->AddNodeToMenu(root, menu); + } + + NSMenuItem* MenuItemForNode(BookmarkMenuBridge* bridge, + const BookmarkNode* node) { + return bridge->MenuItemForNode(node); + } + + NSMenuItem* AddItemToMenu(NSMenu *menu, NSString *title, SEL selector) { + NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:title action:NULL + keyEquivalent:@""] autorelease]; + if (selector) + [item setAction:selector]; + [menu addItem:item]; + return item; + } + + BrowserTestHelper browser_test_helper_; + scoped_ptr<TestBookmarkMenuBridge> bridge_; +}; + +TEST_F(BookmarkMenuBridgeTest, TestBookmarkMenuAutoSeparator) { + BookmarkModel* model = bridge_->GetBookmarkModel(); + bridge_->Loaded(model); + NSMenu* menu = bridge_->menu_.get(); + bridge_->UpdateMenu(menu); + // The bare menu after loading has a separator and an "Other Bookmarks" + // submenu. + EXPECT_EQ(2, [menu numberOfItems]); + // Add a bookmark and reload and there should be 4 items: the previous + // menu contents plus a new separator and the new bookmark. + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const char* url = "http://www.zim-bop-a-dee.com/"; + model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url)); + bridge_->UpdateMenu(menu); + EXPECT_EQ(4, [menu numberOfItems]); + // Remove the new bookmark and reload and we should have 2 items again + // because the separator should have been removed as well. + model->Remove(parent, 0); + bridge_->UpdateMenu(menu); + EXPECT_EQ(2, [menu numberOfItems]); +} + +// Test that ClearBookmarkMenu() removes all bookmark menus. +TEST_F(BookmarkMenuBridgeTest, TestClearBookmarkMenu) { + NSMenu* menu = bridge_->menu_.get(); + + AddItemToMenu(menu, @"hi mom", nil); + AddItemToMenu(menu, @"not", @selector(openBookmarkMenuItem:)); + NSMenuItem* item = AddItemToMenu(menu, @"hi mom", nil); + [item setSubmenu:[[[NSMenu alloc] initWithTitle:@"bar"] autorelease]]; + AddItemToMenu(menu, @"not", @selector(openBookmarkMenuItem:)); + AddItemToMenu(menu, @"zippy", @selector(length)); + [menu addItem:[NSMenuItem separatorItem]]; + + ClearBookmarkMenu(bridge_.get(), menu); + + // Make sure all bookmark items are removed, all items with + // submenus removed, and all separator items are gone. + EXPECT_EQ(2, [menu numberOfItems]); + for (NSMenuItem *item in [menu itemArray]) { + EXPECT_NSNE(@"not", [item title]); + } +} + +// Test invalidation +TEST_F(BookmarkMenuBridgeTest, TestInvalidation) { + BookmarkModel* model = bridge_->GetBookmarkModel(); + bridge_->Loaded(model); + + EXPECT_FALSE(menu_is_valid()); + bridge_->UpdateMenu(bridge_->menu_); + EXPECT_TRUE(menu_is_valid()); + + InvalidateMenu(); + EXPECT_FALSE(menu_is_valid()); + InvalidateMenu(); + EXPECT_FALSE(menu_is_valid()); + bridge_->UpdateMenu(bridge_->menu_); + EXPECT_TRUE(menu_is_valid()); + bridge_->UpdateMenu(bridge_->menu_); + EXPECT_TRUE(menu_is_valid()); + + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const char* url = "http://www.zim-bop-a-dee.com/"; + model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url)); + + EXPECT_FALSE(menu_is_valid()); + bridge_->UpdateMenu(bridge_->menu_); + EXPECT_TRUE(menu_is_valid()); +} + +// Test that AddNodeToMenu() properly adds bookmark nodes as menus, +// including the recursive case. +TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMenu) { + string16 empty; + NSMenu* menu = bridge_->menu_.get(); + + BookmarkModel* model = bridge_->GetBookmarkModel(); + const BookmarkNode* root = model->GetBookmarkBarNode(); + EXPECT_TRUE(model && root); + + const char* short_url = "http://foo/"; + const char* long_url = "http://super-duper-long-url--." + "that.cannot.possibly.fit.even-in-80-columns" + "or.be.reasonably-displayed-in-a-menu" + "without.looking-ridiculous.com/"; // 140 chars total + + // 3 nodes; middle one has a child, last one has a HUGE URL + // Set their titles to be the same as the URLs + const BookmarkNode* node = NULL; + model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url)); + bridge_->UpdateMenu(menu); + int prev_count = [menu numberOfItems] - 1; // "extras" added at this point + node = model->AddGroup(root, 1, empty); + model->AddURL(root, 2, ASCIIToUTF16(long_url), GURL(long_url)); + + // And the submenu fo the middle one + model->AddURL(node, 0, empty, GURL("http://sub")); + bridge_->UpdateMenu(menu); + + EXPECT_EQ((NSInteger)(prev_count+3), [menu numberOfItems]); + + // Verify the 1st one is there with the right action. + NSMenuItem* item = [menu itemWithTitle:[NSString + stringWithUTF8String:short_url]]; + EXPECT_TRUE(item); + EXPECT_EQ(@selector(openBookmarkMenuItem:), [item action]); + EXPECT_EQ(NO, [item hasSubmenu]); + NSMenuItem* short_item = item; + NSMenuItem* long_item = nil; + + // Now confirm we have 2 submenus (the one we added, plus "other") + int subs = 0; + for (item in [menu itemArray]) { + if ([item hasSubmenu]) + subs++; + } + EXPECT_EQ(2, subs); + + for (item in [menu itemArray]) { + if ([[item title] hasPrefix:@"http://super-duper"]) { + long_item = item; + break; + } + } + EXPECT_TRUE(long_item); + + // Make sure a short title looks fine + NSString* s = [short_item title]; + EXPECT_NSEQ([NSString stringWithUTF8String:short_url], s); + + // Make sure a super-long title gets trimmed + s = [long_item title]; + EXPECT_TRUE([s length] < strlen(long_url)); + + // Confirm tooltips and confirm they are not trimmed (like the item + // name might be). Add tolerance for URL fixer-upping; + // e.g. http://foo becomes http://foo/) + EXPECT_GE([[short_item toolTip] length], (2*strlen(short_url) - 5)); + EXPECT_GE([[long_item toolTip] length], (2*strlen(long_url) - 5)); + + // Make sure the favicon is non-nil (should be either the default site + // icon or a favicon, if present). + EXPECT_TRUE([short_item image]); + EXPECT_TRUE([long_item image]); +} + +// Makes sure our internal map of BookmarkNode to NSMenuItem works. +TEST_F(BookmarkMenuBridgeTest, TestGetMenuItemForNode) { + string16 empty; + NSMenu* menu = bridge_->menu_.get(); + + BookmarkModel* model = bridge_->GetBookmarkModel(); + const BookmarkNode* bookmark_bar = model->GetBookmarkBarNode(); + const BookmarkNode* root = model->AddGroup(bookmark_bar, 0, empty); + EXPECT_TRUE(model && root); + + model->AddURL(root, 0, ASCIIToUTF16("Test Item"), GURL("http://test")); + AddNodeToMenu(bridge_.get(), root, menu); + EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); + + model->AddURL(root, 1, ASCIIToUTF16("Test 2"), GURL("http://second-test")); + AddNodeToMenu(bridge_.get(), root, menu); + EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); + EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(1))); + + const BookmarkNode* removed_node = root->GetChild(0); + EXPECT_EQ(2, root->GetChildCount()); + model->Remove(root, 0); + EXPECT_EQ(1, root->GetChildCount()); + bridge_->UpdateMenu(menu); + EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node)); + EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); + + const BookmarkNode empty_node(GURL("http://no-where/")); + EXPECT_FALSE(MenuItemForNode(bridge_.get(), &empty_node)); + EXPECT_FALSE(MenuItemForNode(bridge_.get(), NULL)); +} + +// Test that Loaded() adds both the bookmark bar nodes and the "other" nodes. +TEST_F(BookmarkMenuBridgeTest, TestAddNodeToOther) { + NSMenu* menu = bridge_->menu_.get(); + + BookmarkModel* model = bridge_->GetBookmarkModel(); + const BookmarkNode* root = model->other_node(); + EXPECT_TRUE(model && root); + + const char* short_url = "http://foo/"; + model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url)); + + bridge_->UpdateMenu(menu); + ASSERT_GT([menu numberOfItems], 0); + NSMenuItem* other = [menu itemAtIndex:([menu numberOfItems]-1)]; + EXPECT_TRUE(other); + EXPECT_TRUE([other hasSubmenu]); + ASSERT_GT([[other submenu] numberOfItems], 0); + EXPECT_NSEQ(@"http://foo/", [[[other submenu] itemAtIndex:0] title]); +} + +TEST_F(BookmarkMenuBridgeTest, TestFavIconLoading) { + NSMenu* menu = bridge_->menu_; + + BookmarkModel* model = bridge_->GetBookmarkModel(); + const BookmarkNode* root = model->GetBookmarkBarNode(); + EXPECT_TRUE(model && root); + + const BookmarkNode* node = + model->AddURL(root, 0, ASCIIToUTF16("Test Item"), + GURL("http://favicon-test")); + bridge_->UpdateMenu(menu); + NSMenuItem* item = [menu itemWithTitle:@"Test Item"]; + EXPECT_TRUE([item image]); + [item setImage:nil]; + bridge_->BookmarkNodeFavIconLoaded(model, node); + EXPECT_TRUE([item image]); +} + +TEST_F(BookmarkMenuBridgeTest, TestChangeTitle) { + NSMenu* menu = bridge_->menu_; + BookmarkModel* model = bridge_->GetBookmarkModel(); + const BookmarkNode* root = model->GetBookmarkBarNode(); + EXPECT_TRUE(model && root); + + const BookmarkNode* node = + model->AddURL(root, 0, ASCIIToUTF16("Test Item"), + GURL("http://title-test")); + bridge_->UpdateMenu(menu); + NSMenuItem* item = [menu itemWithTitle:@"Test Item"]; + EXPECT_TRUE([item image]); + + model->SetTitle(node, ASCIIToUTF16("New Title")); + + item = [menu itemWithTitle:@"Test Item"]; + EXPECT_FALSE(item); + item = [menu itemWithTitle:@"New Title"]; + EXPECT_TRUE(item); +} + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h new file mode 100644 index 0000000..ac7def4 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h @@ -0,0 +1,46 @@ +// 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. + +// Controller (MVC) for the bookmark menu. +// All bookmark menu item commands get directed here. +// Unfortunately there is already a C++ class named BookmarkMenuController. + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#import "base/mac/cocoa_protocols.h" + +class BookmarkNode; +class BookmarkMenuBridge; + +@interface BookmarkMenuCocoaController : NSObject<NSMenuDelegate> { + @private + BookmarkMenuBridge* bridge_; // weak; owns me +} + +// The Bookmarks menu +@property (nonatomic, readonly) NSMenu* menu; + +// Return an autoreleased string to be used as a menu title for the +// given bookmark node. ++ (NSString*)menuTitleForNode:(const BookmarkNode*)node; + +- (id)initWithBridge:(BookmarkMenuBridge *)bridge; + +// Called by any Bookmark menu item. +// The menu item's tag is the bookmark ID. +- (IBAction)openBookmarkMenuItem:(id)sender; + +@end // BookmarkMenuCocoaController + + +@interface BookmarkMenuCocoaController (ExposedForUnitTests) +- (const BookmarkNode*)nodeForIdentifier:(int)identifier; +- (void)openURLForNode:(const BookmarkNode*)node; +@end // BookmarkMenuCocoaController (ExposedForUnitTests) + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MENU_COCOA_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm new file mode 100644 index 0000000..1d2fb2d --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm @@ -0,0 +1,96 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" + +#include "app/text_elider.h" +#include "base/sys_string_conversions.h" +#include "base/utf_string_conversions.h" +#include "chrome/app/chrome_command_ids.h" // IDC_BOOKMARK_MENU +#import "chrome/browser/app_controller_mac.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/ui/browser.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h" +#include "chrome/browser/ui/cocoa/event_utils.h" +#include "webkit/glue/window_open_disposition.h" + +namespace { + +// Menus more than this many pixels wide will get trimmed +// TODO(jrg): ask UI dudes what a good value is. +const NSUInteger kMaximumMenuPixelsWide = 300; + +} + +@implementation BookmarkMenuCocoaController + ++ (NSString*)menuTitleForNode:(const BookmarkNode*)node { + NSFont* nsfont = [NSFont menuBarFontOfSize:0]; // 0 means "default" + gfx::Font font(base::SysNSStringToWide([nsfont fontName]), + static_cast<int>([nsfont pointSize])); + string16 title = gfx::ElideText(node->GetTitle(), + font, + kMaximumMenuPixelsWide, + false); + return base::SysUTF16ToNSString(title); +} + +- (id)initWithBridge:(BookmarkMenuBridge *)bridge { + if ((self = [super init])) { + bridge_ = bridge; + DCHECK(bridge_); + [[self menu] setDelegate:self]; + } + return self; +} + +- (void)dealloc { + [[self menu] setDelegate:nil]; + [super dealloc]; +} + +- (NSMenu*)menu { + return [[[NSApp mainMenu] itemWithTag:IDC_BOOKMARK_MENU] submenu]; +} + +- (BOOL)validateMenuItem:(NSMenuItem*)menuItem { + AppController* controller = [NSApp delegate]; + return [controller keyWindowIsNotModal]; +} + +// NSMenu delegate method: called just before menu is displayed. +- (void)menuNeedsUpdate:(NSMenu*)menu { + bridge_->UpdateMenu(menu); +} + +// Return the a BookmarkNode that has the given id (called +// "identifier" here to avoid conflict with objc's concept of "id"). +- (const BookmarkNode*)nodeForIdentifier:(int)identifier { + return bridge_->GetBookmarkModel()->GetNodeByID(identifier); +} + +// Open the URL of the given BookmarkNode in the current tab. +- (void)openURLForNode:(const BookmarkNode*)node { + Browser* browser = Browser::GetTabbedBrowser(bridge_->GetProfile(), true); + if (!browser) + browser = Browser::Create(bridge_->GetProfile()); + WindowOpenDisposition disposition = + event_utils::WindowOpenDispositionFromNSEvent([NSApp currentEvent]); + browser->OpenURL(node->GetURL(), GURL(), disposition, + PageTransition::AUTO_BOOKMARK); +} + +- (IBAction)openBookmarkMenuItem:(id)sender { + NSInteger tag = [sender tag]; + int identifier = tag; + const BookmarkNode* node = [self nodeForIdentifier:identifier]; + DCHECK(node); + if (!node) + return; // shouldn't be reached + + [self openURLForNode:node]; +} + +@end // BookmarkMenuCocoaController + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm new file mode 100644 index 0000000..22930bc --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm @@ -0,0 +1,66 @@ +// 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 "base/string16.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h" +#include "chrome/browser/ui/browser.h" +#include "testing/gtest/include/gtest/gtest.h" + +@interface FakeBookmarkMenuController : BookmarkMenuCocoaController { + @public + BrowserTestHelper* helper_; + const BookmarkNode* nodes_[2]; + BOOL opened_[2]; +} +@end + +@implementation FakeBookmarkMenuController + +- (id)init { + if ((self = [super init])) { + string16 empty; + helper_ = new BrowserTestHelper(); + BookmarkModel* model = helper_->browser()->profile()->GetBookmarkModel(); + const BookmarkNode* bookmark_bar = model->GetBookmarkBarNode(); + nodes_[0] = model->AddURL(bookmark_bar, 0, empty, GURL("http://0.com")); + nodes_[1] = model->AddURL(bookmark_bar, 1, empty, GURL("http://1.com")); + } + return self; +} + +- (void)dealloc { + delete helper_; + [super dealloc]; +} + +- (const BookmarkNode*)nodeForIdentifier:(int)identifier { + if ((identifier < 0) || (identifier >= 2)) + return NULL; + return nodes_[identifier]; +} + +- (void)openURLForNode:(const BookmarkNode*)node { + std::string url = node->GetURL().possibly_invalid_spec(); + if (url.find("http://0.com") != std::string::npos) + opened_[0] = YES; + if (url.find("http://1.com") != std::string::npos) + opened_[1] = YES; +} + +@end // FakeBookmarkMenuController + + +TEST(BookmarkMenuCocoaControllerTest, TestOpenItem) { + FakeBookmarkMenuController *c = [[FakeBookmarkMenuController alloc] init]; + NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease]; + for (int i = 0; i < 2; i++) { + [item setTag:i]; + ASSERT_EQ(c->opened_[i], NO); + [c openBookmarkMenuItem:item]; + ASSERT_NE(c->opened_[i], NO); + } + [c release]; +} diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm new file mode 100644 index 0000000..ef251b0 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm @@ -0,0 +1,29 @@ +// 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. + +#include "base/scoped_nsobject.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace { + +class BookmarkMenuTest : public CocoaTest { +}; + +TEST_F(BookmarkMenuTest, Basics) { + scoped_nsobject<BookmarkMenu> menu([[BookmarkMenu alloc] + initWithTitle:@"title"]); + scoped_nsobject<NSMenuItem> item([[NSMenuItem alloc] initWithTitle:@"item" + action:NULL + keyEquivalent:@""]); + [menu addItem:item]; + long long l = 103849459459598948LL; // arbitrary + NSNumber* number = [NSNumber numberWithLongLong:l]; + [menu setRepresentedObject:number]; + EXPECT_EQ(l, [menu id]); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h new file mode 100644 index 0000000..0a7da34 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h @@ -0,0 +1,116 @@ +// 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. + +// C++ bridge class to send a selector to a Cocoa object when the +// bookmark model changes. Some Cocoa objects edit the bookmark model +// and temporarily save a copy of the state (e.g. bookmark button +// editor). As a fail-safe, these objects want an easy cancel if the +// model changes out from under them. For example, if you have the +// bookmark button editor sheet open, then edit the bookmark in the +// bookmark manager, we'd want to simply cancel the editor. +// +// This class is conservative and may result in notifications which +// aren't strictly necessary. For example, node removal only needs to +// cancel an edit if the removed node is a folder (editors often have +// a list of "new parents"). But, just to be sure, notification +// happens on any removal. + +#ifndef CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H +#pragma once + +#import <Cocoa/Cocoa.h> + +#include "base/basictypes.h" +#include "base/scoped_nsobject.h" +#include "chrome/browser/bookmarks/bookmark_model.h" +#include "chrome/browser/bookmarks/bookmark_model_observer.h" + +class BookmarkModelObserverForCocoa : public BookmarkModelObserver { + public: + // When |node| in |model| changes, send |selector| to |object|. + // Assumes |selector| is a selector that takes one arg, like an + // IBOutlet. The arg passed is nil. + // Many notifications happen independently of node + // (e.g. BeingDeleted), so |node| can be nil. + // + // |object| is NOT retained, since the expected use case is for + // ||object| to own the BookmarkModelObserverForCocoa and we don't + // want a retain cycle. + BookmarkModelObserverForCocoa(const BookmarkNode* node, + BookmarkModel* model, + NSObject* object, + SEL selector) { + DCHECK(model); + node_ = node; + model_ = model; + object_ = object; + selector_ = selector; + model_->AddObserver(this); + } + virtual ~BookmarkModelObserverForCocoa() { + model_->RemoveObserver(this); + } + + virtual void BookmarkModelBeingDeleted(BookmarkModel* model) { + Notify(); + } + virtual void BookmarkNodeMoved(BookmarkModel* model, + const BookmarkNode* old_parent, + int old_index, + const BookmarkNode* new_parent, + int new_index) { + // Editors often have a tree of parents, so movement of folders + // must cause a cancel. + Notify(); + } + virtual void BookmarkNodeRemoved(BookmarkModel* model, + const BookmarkNode* parent, + int old_index, + const BookmarkNode* node) { + // See comment in BookmarkNodeMoved. + Notify(); + } + virtual void BookmarkNodeChanged(BookmarkModel* model, + const BookmarkNode* node) { + if ((node_ == node) || (!node_)) + Notify(); + } + virtual void BookmarkImportBeginning(BookmarkModel* model) { + // Be conservative. + Notify(); + } + + // Some notifications we don't care about, but by being pure virtual + // in the base class we must implement them. + virtual void Loaded(BookmarkModel* model) { + } + virtual void BookmarkNodeAdded(BookmarkModel* model, + const BookmarkNode* parent, + int index) { + } + virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model, + const BookmarkNode* node) { + } + virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, + const BookmarkNode* node) { + } + + virtual void BookmarkImportEnding(BookmarkModel* model) { + } + + private: + const BookmarkNode* node_; // Weak; owned by a BookmarkModel. + BookmarkModel* model_; // Weak; it is owned by a Profile. + NSObject* object_; // Weak, like a delegate. + SEL selector_; + + void Notify() { + [object_ performSelector:selector_ withObject:nil]; + } + + DISALLOW_COPY_AND_ASSIGN(BookmarkModelObserverForCocoa); +}; + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_MODEL_OBSERVER_FOR_COCOA_H diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm new file mode 100644 index 0000000..5ff8687 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm @@ -0,0 +1,68 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_ptr.h" +#include "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h" +#import "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" + +// Keep track of bookmark pings. +@interface ObserverPingTracker : NSObject { + @public + int pings; +} +@end + +@implementation ObserverPingTracker +- (void)pingMe:(id)sender { + pings++; +} +@end + +namespace { + +class BookmarkModelObserverForCocoaTest : public CocoaTest { + public: + BrowserTestHelper helper_; + + BookmarkModelObserverForCocoaTest() {} + virtual ~BookmarkModelObserverForCocoaTest() {} + private: + DISALLOW_COPY_AND_ASSIGN(BookmarkModelObserverForCocoaTest); +}; + + +TEST_F(BookmarkModelObserverForCocoaTest, TestCallback) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* node = model->AddURL(model->GetBookmarkBarNode(), + 0, ASCIIToUTF16("super"), + GURL("http://www.google.com")); + + scoped_nsobject<ObserverPingTracker> + pingCount([[ObserverPingTracker alloc] init]); + + scoped_ptr<BookmarkModelObserverForCocoa> + observer(new BookmarkModelObserverForCocoa(node, model, + pingCount, + @selector(pingMe:))); + + EXPECT_EQ(0, pingCount.get()->pings); + + model->SetTitle(node, ASCIIToUTF16("duper")); + EXPECT_EQ(1, pingCount.get()->pings); + model->SetURL(node, GURL("http://www.google.com/reader")); + EXPECT_EQ(2, pingCount.get()->pings); + + model->Move(node, model->other_node(), 0); + EXPECT_EQ(3, pingCount.get()->pings); + + model->Remove(node->GetParent(), 0); + EXPECT_EQ(4, pingCount.get()->pings); +} + +} // namespace diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h new file mode 100644 index 0000000..40f1cb1 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h @@ -0,0 +1,64 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#include "base/scoped_ptr.h" +#include "chrome/browser/bookmarks/bookmark_model.h" + +class BookmarkModelObserverForCocoa; + +// A controller for dialog to let the user create a new folder or +// rename an existing folder. Accessible from a context menu on a +// bookmark button or the bookmark bar. +@interface BookmarkNameFolderController : NSWindowController { + @private + IBOutlet NSTextField* nameField_; + IBOutlet NSButton* okButton_; + + NSWindow* parentWindow_; // weak + Profile* profile_; // weak + + // Weak; owned by the model. Can be NULL (see below). Either node_ + // is non-NULL (renaming a folder), or parent_ is non-NULL (adding a + // new one). + const BookmarkNode* node_; + const BookmarkNode* parent_; + int newIndex_; + + scoped_nsobject<NSString> initialName_; + + // Ping me when things change out from under us. + scoped_ptr<BookmarkModelObserverForCocoa> observer_; +} + +// Use the 1st initializer for a "rename existing folder" request. +// +// Use the 2nd initializer for an "add folder" request. If creating a +// new folder |parent| and |newIndex| specify where to put the new +// node. +- (id)initWithParentWindow:(NSWindow*)window + profile:(Profile*)profile + node:(const BookmarkNode*)node; +- (id)initWithParentWindow:(NSWindow*)window + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + newIndex:(int)newIndex; +- (void)runAsModalSheet; +- (IBAction)cancel:(id)sender; +- (IBAction)ok:(id)sender; +@end + +@interface BookmarkNameFolderController(TestingAPI) +- (NSString*)folderName; +- (void)setFolderName:(NSString*)name; +- (NSButton*)okButton; +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_NAME_FOLDER_CONTROLLER_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm new file mode 100644 index 0000000..b4f23d2 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm @@ -0,0 +1,123 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h" + +#include "app/l10n_util.h" +#include "app/l10n_util_mac.h" +#include "base/mac_util.h" +#include "base/sys_string_conversions.h" +#include "chrome/browser/profiles/profile.h" +#include "chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h" +#include "grit/generated_resources.h" + +@implementation BookmarkNameFolderController + +// Common initializer (private). +- (id)initWithParentWindow:(NSWindow*)window + profile:(Profile*)profile + node:(const BookmarkNode*)node + parent:(const BookmarkNode*)parent + newIndex:(int)newIndex { + NSString* nibpath = [mac_util::MainAppBundle() + pathForResource:@"BookmarkNameFolder" + ofType:@"nib"]; + if ((self = [super initWithWindowNibPath:nibpath owner:self])) { + parentWindow_ = window; + profile_ = profile; + node_ = node; + parent_ = parent; + newIndex_ = newIndex; + if (parent) { + DCHECK_LE(newIndex, parent->GetChildCount()); + } + if (node_) { + initialName_.reset([base::SysUTF16ToNSString(node_->GetTitle()) retain]); + } else { + NSString* newString = + l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); + initialName_.reset([newString retain]); + } + } + return self; +} + +- (id)initWithParentWindow:(NSWindow*)window + profile:(Profile*)profile + node:(const BookmarkNode*)node { + DCHECK(node); + return [self initWithParentWindow:window + profile:profile + node:node + parent:nil + newIndex:0]; +} + +- (id)initWithParentWindow:(NSWindow*)window + profile:(Profile*)profile + parent:(const BookmarkNode*)parent + newIndex:(int)newIndex { + DCHECK(parent); + return [self initWithParentWindow:window + profile:profile + node:nil + parent:parent + newIndex:newIndex]; +} + +- (void)awakeFromNib { + [nameField_ setStringValue:initialName_.get()]; +} + +- (void)runAsModalSheet { + // Ping me when things change out from under us. + observer_.reset(new BookmarkModelObserverForCocoa( + node_, profile_->GetBookmarkModel(), + self, + @selector(cancel:))); + [NSApp beginSheet:[self window] + modalForWindow:parentWindow_ + modalDelegate:self + didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (IBAction)cancel:(id)sender { + [NSApp endSheet:[self window]]; +} + +- (IBAction)ok:(id)sender { + NSString* name = [nameField_ stringValue]; + BookmarkModel* model = profile_->GetBookmarkModel(); + if (node_) { + model->SetTitle(node_, base::SysNSStringToUTF16(name)); + } else { + model->AddGroup(parent_, + newIndex_, + base::SysNSStringToUTF16(name)); + } + [NSApp endSheet:[self window]]; +} + +- (void)didEndSheet:(NSWindow*)sheet + returnCode:(int)returnCode + contextInfo:(void*)contextInfo { + [[self window] orderOut:self]; + observer_.reset(NULL); + [self autorelease]; +} + +- (NSString*)folderName { + return [nameField_ stringValue]; +} + +- (void)setFolderName:(NSString*)name { + [nameField_ setStringValue:name]; +} + +- (NSButton*)okButton { + return okButton_; +} + +@end // BookmarkNameFolderController diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm new file mode 100644 index 0000000..69fb939 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm @@ -0,0 +1,172 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "base/scoped_nsobject.h" +#include "base/utf_string_conversions.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h" +#include "chrome/browser/ui/cocoa/browser_test_helper.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/gtest/include/gtest/gtest.h" +#import "testing/gtest_mac.h" +#include "testing/platform_test.h" + +class BookmarkNameFolderControllerTest : public CocoaTest { + public: + BrowserTestHelper helper_; +}; + + +// Simple add of a node (at the end). +TEST_F(BookmarkNameFolderControllerTest, AddNew) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + EXPECT_EQ(0, parent->GetChildCount()); + + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:parent + newIndex:0]); + [controller window]; // force nib load + + // Do nothing. + [controller cancel:nil]; + EXPECT_EQ(0, parent->GetChildCount()); + + // Change name then cancel. + [controller setFolderName:@"Bozo"]; + [controller cancel:nil]; + EXPECT_EQ(0, parent->GetChildCount()); + + // Add a new folder. + [controller ok:nil]; + EXPECT_EQ(1, parent->GetChildCount()); + EXPECT_TRUE(parent->GetChild(0)->is_folder()); + EXPECT_EQ(ASCIIToUTF16("Bozo"), parent->GetChild(0)->GetTitle()); +} + +// Add new but specify a sibling. +TEST_F(BookmarkNameFolderControllerTest, AddNewWithSibling) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + + // Add 2 nodes. We will place the new folder in the middle of these. + model->AddURL(parent, 0, ASCIIToUTF16("title 1"), + GURL("http://www.google.com")); + model->AddURL(parent, 1, ASCIIToUTF16("title 3"), + GURL("http://www.google.com")); + EXPECT_EQ(2, parent->GetChildCount()); + + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:parent + newIndex:1]); + [controller window]; // force nib load + + // Add a new folder. + [controller setFolderName:@"middle"]; + [controller ok:nil]; + + // Confirm we now have 3, and that the new one is in the middle. + EXPECT_EQ(3, parent->GetChildCount()); + EXPECT_TRUE(parent->GetChild(1)->is_folder()); + EXPECT_EQ(ASCIIToUTF16("middle"), parent->GetChild(1)->GetTitle()); +} + +// Make sure we are allowed to create a folder named "New Folder". +TEST_F(BookmarkNameFolderControllerTest, AddNewDefaultName) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + EXPECT_EQ(0, parent->GetChildCount()); + + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:parent + newIndex:0]); + + [controller window]; // force nib load + + // Click OK without changing the name + [controller ok:nil]; + EXPECT_EQ(1, parent->GetChildCount()); + EXPECT_TRUE(parent->GetChild(0)->is_folder()); +} + +// Make sure we are allowed to create a folder with an empty name. +TEST_F(BookmarkNameFolderControllerTest, AddNewBlankName) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + EXPECT_EQ(0, parent->GetChildCount()); + + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:parent + newIndex:0]); + [controller window]; // force nib load + + // Change the name to blank, click OK. + [controller setFolderName:@""]; + [controller ok:nil]; + EXPECT_EQ(1, parent->GetChildCount()); + EXPECT_TRUE(parent->GetChild(0)->is_folder()); +} + +TEST_F(BookmarkNameFolderControllerTest, Rename) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + const BookmarkNode* folder = model->AddGroup(parent, + parent->GetChildCount(), + ASCIIToUTF16("group")); + + // Rename the folder by creating a controller that originates from + // the node. + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + node:folder]); + [controller window]; // force nib load + + EXPECT_NSEQ(@"group", [controller folderName]); + [controller setFolderName:@"Zobo"]; + [controller ok:nil]; + EXPECT_EQ(1, parent->GetChildCount()); + EXPECT_TRUE(parent->GetChild(0)->is_folder()); + EXPECT_EQ(ASCIIToUTF16("Zobo"), parent->GetChild(0)->GetTitle()); +} + +TEST_F(BookmarkNameFolderControllerTest, EditAndConfirmOKButton) { + BookmarkModel* model = helper_.profile()->GetBookmarkModel(); + const BookmarkNode* parent = model->GetBookmarkBarNode(); + EXPECT_EQ(0, parent->GetChildCount()); + + scoped_nsobject<BookmarkNameFolderController> + controller([[BookmarkNameFolderController alloc] + initWithParentWindow:test_window() + profile:helper_.profile() + parent:parent + newIndex:0]); + [controller window]; // force nib load + + // We start enabled since the default "New Folder" is added for us. + EXPECT_TRUE([[controller okButton] isEnabled]); + + [controller setFolderName:@"Bozo"]; + EXPECT_TRUE([[controller okButton] isEnabled]); + [controller setFolderName:@" "]; + EXPECT_TRUE([[controller okButton] isEnabled]); + + [controller setFolderName:@""]; + EXPECT_TRUE([[controller okButton] isEnabled]); +} + diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h new file mode 100644 index 0000000..08b195d --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h @@ -0,0 +1,35 @@ +// 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_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_ +#define CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_ +#pragma once + +#import <Cocoa/Cocoa.h> + +class BookmarkNode; + +// Provides a custom cell as used in the BookmarkEditor.xib's folder tree +// browser view. This cell customization adds target and action support +// not provided by the NSBrowserCell as well as contextual information +// identifying the bookmark node being edited and the column matrix +// control in which is contained the cell. +@interface BookmarkTreeBrowserCell : NSBrowserCell { + @private + const BookmarkNode* bookmarkNode_; // weak + NSMatrix* matrix_; // weak + id target_; // weak + SEL action_; +} + +@property (nonatomic, assign) NSMatrix* matrix; +@property (nonatomic, assign) id target; +@property (nonatomic, assign) SEL action; + +- (const BookmarkNode*)bookmarkNode; +- (void)setBookmarkNode:(const BookmarkNode*)bookmarkNode; + +@end + +#endif // CHROME_BROWSER_UI_COCOA_BOOKMARKS_BOOKMARK_TREE_BROWSER_CELL_H_ diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm new file mode 100644 index 0000000..6fe7e6e --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm @@ -0,0 +1,23 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h" + +#include "chrome/browser/bookmarks/bookmark_model.h" + +@implementation BookmarkTreeBrowserCell + +@synthesize matrix = matrix_; +@synthesize target = target_; +@synthesize action = action_; + +- (const BookmarkNode*)bookmarkNode { + return bookmarkNode_; +} + +- (void)setBookmarkNode:(const BookmarkNode*)bookmarkNode { + bookmarkNode_ = bookmarkNode; +} + +@end diff --git a/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm new file mode 100644 index 0000000..5171018 --- /dev/null +++ b/chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm @@ -0,0 +1,43 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import <Cocoa/Cocoa.h> + +#include "chrome/browser/bookmarks/bookmark_model.h" +#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h" +#import "chrome/browser/ui/cocoa/cocoa_test_helper.h" +#include "testing/platform_test.h" + +class BookmarkTreeBrowserCellTest : public PlatformTest { + public: + BookmarkTreeBrowserCellTest() { + // Set up our mocks. + GURL gurl; + bookmarkNodeMock_.reset(new BookmarkNode(gurl)); + matrixMock_.reset([[NSMatrix alloc] init]); + targetMock_.reset([[NSObject alloc] init]); + } + + scoped_ptr<BookmarkNode> bookmarkNodeMock_; + scoped_nsobject<NSMatrix> matrixMock_; + scoped_nsobject<NSObject> targetMock_; +}; + +TEST_F(BookmarkTreeBrowserCellTest, BasicAllocDealloc) { + BookmarkTreeBrowserCell* cell = [[[BookmarkTreeBrowserCell alloc] + initTextCell:@"TEST STRING"] autorelease]; + [cell setMatrix:matrixMock_.get()]; + [cell setTarget:targetMock_.get()]; + [cell setAction:@selector(mockAction:)]; + [cell setBookmarkNode:bookmarkNodeMock_.get()]; + + NSMatrix* testMatrix = [cell matrix]; + EXPECT_EQ(testMatrix, matrixMock_.get()); + id testTarget = [cell target]; + EXPECT_EQ(testTarget, targetMock_.get()); + SEL testAction = [cell action]; + EXPECT_EQ(testAction, @selector(mockAction:)); + const BookmarkNode* testBookmarkNode = [cell bookmarkNode]; + EXPECT_EQ(testBookmarkNode, bookmarkNodeMock_.get()); +} |