summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/bookmarks
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui/cocoa/bookmarks')
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h46
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.mm88
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller_unittest.mm82
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.h60
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge.mm82
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_bridge_unittest.mm135
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_constants.h38
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.h399
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller.mm2497
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_controller_unittest.mm2169
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.h31
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell.mm22
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_button_cell_unittest.mm24
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.h182
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller.mm1459
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_controller_unittest.mm1552
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.h78
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state.mm171
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_hover_state_unittest.mm77
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.h29
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view.mm204
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_view_unittest.mm211
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.h34
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window.mm136
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_folder_window_unittest.mm49
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_state.h62
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.h44
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view.mm135
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_toolbar_view_unittest.mm191
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.h57
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_unittest_helper.mm81
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.h41
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view.mm259
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bar_view_unittest.mm215
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.h81
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller.mm428
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_bubble_controller_unittest.mm490
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button.h243
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button.mm238
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.h65
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell.mm246
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_cell_unittest.mm183
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_button_unittest.mm174
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.h30
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_drag_source.mm43
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h171
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.mm604
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller_unittest.mm235
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h36
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.mm143
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller_unittest.mm423
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.h50
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target.mm118
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_folder_target_unittest.mm125
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu.h20
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu.mm22
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h123
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.mm253
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge_unittest.mm317
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h46
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.mm96
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller_unittest.mm66
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_menu_unittest.mm29
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa.h116
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_model_observer_for_cocoa_unittest.mm68
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h64
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.mm123
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller_unittest.mm172
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h35
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.mm23
-rw-r--r--chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell_unittest.mm43
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());
+}