1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h"
9 #include "base/auto_reset.h"
10 #include "base/logging.h"
11 #include "base/mac/bundle_locations.h"
12 #include "base/mac/mac_util.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
15 #include "chrome/browser/bookmarks/chrome_bookmark_client.h"
16 #include "chrome/browser/bookmarks/chrome_bookmark_client_factory.h"
17 #include "chrome/browser/profiles/profile.h"
18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
19 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
20 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
21 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
22 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
23 #include "chrome/grit/generated_resources.h"
24 #include "components/bookmarks/browser/bookmark_model.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/base/l10n/l10n_util_mac.h"
28 using bookmarks::BookmarkExpandedStateTracker;
30 @interface BookmarkEditorBaseController ()
32 // Return the folder tree object for the given path.
33 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path;
35 // (Re)build the folder tree from the BookmarkModel's current state.
36 - (void)buildFolderTree;
38 // Notifies the controller that the bookmark model has changed.
39 // |selection| specifies if the current selection should be
40 // maintained (usually YES).
41 - (void)modelChangedPreserveSelection:(BOOL)preserve;
43 // Notifies the controller that a node has been removed.
44 - (void)nodeRemoved:(const BookmarkNode*)node
45 fromParent:(const BookmarkNode*)parent;
47 // Given a folder node, collect an array containing BookmarkFolderInfos
48 // describing its subchildren which are also folders.
49 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node;
51 // Scan the folder tree stemming from the given tree folder and create
52 // any newly added folders. Pass down info for the folder which was
53 // selected before we began creating folders.
54 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder
55 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo;
57 // Scan the folder tree looking for the given bookmark node and return
58 // the selection path thereto.
59 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node;
61 // Implementation of getExpandedNodes. See description in header for details.
62 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
63 folder:(BookmarkFolderInfo*)info
64 path:(std::vector<NSUInteger>*)path
68 // static; implemented for each platform. Update this function for new
69 // classes derived from BookmarkEditorBaseController.
70 void BookmarkEditor::Show(gfx::NativeWindow parent_window,
72 const EditDetails& details,
73 Configuration configuration) {
74 if (details.type == EditDetails::EXISTING_NODE &&
75 details.existing_node->is_folder()) {
76 BookmarkNameFolderController* controller =
77 [[BookmarkNameFolderController alloc]
78 initWithParentWindow:parent_window
80 node:details.existing_node];
81 [controller runAsModalSheet];
85 if (details.type == EditDetails::NEW_FOLDER && details.urls.empty()) {
86 BookmarkNameFolderController* controller =
87 [[BookmarkNameFolderController alloc]
88 initWithParentWindow:parent_window
90 parent:details.parent_node
91 newIndex:details.index];
92 [controller runAsModalSheet];
96 BookmarkEditorBaseController* controller = nil;
97 if (details.type == EditDetails::NEW_FOLDER) {
98 controller = [[BookmarkAllTabsController alloc]
99 initWithParentWindow:parent_window
101 parent:details.parent_node
104 configuration:configuration];
106 controller = [[BookmarkEditorController alloc]
107 initWithParentWindow:parent_window
109 parent:details.parent_node
110 node:details.existing_node
113 configuration:configuration];
115 [controller runAsModalSheet];
118 // Adapter to tell BookmarkEditorBaseController when bookmarks change.
119 class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver {
121 BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller)
122 : controller_(controller),
126 virtual void BookmarkModelLoaded(BookmarkModel* model,
127 bool ids_reassigned) OVERRIDE {
128 [controller_ modelChangedPreserveSelection:YES];
131 virtual void BookmarkNodeMoved(BookmarkModel* model,
132 const BookmarkNode* old_parent,
134 const BookmarkNode* new_parent,
135 int new_index) OVERRIDE {
136 if (!importing_ && new_parent->GetChild(new_index)->is_folder())
137 [controller_ modelChangedPreserveSelection:YES];
140 virtual void BookmarkNodeAdded(BookmarkModel* model,
141 const BookmarkNode* parent,
142 int index) OVERRIDE {
143 if (!importing_ && parent->GetChild(index)->is_folder())
144 [controller_ modelChangedPreserveSelection:YES];
147 virtual void BookmarkNodeRemoved(
148 BookmarkModel* model,
149 const BookmarkNode* parent,
151 const BookmarkNode* node,
152 const std::set<GURL>& removed_urls) OVERRIDE {
153 [controller_ nodeRemoved:node fromParent:parent];
154 if (node->is_folder())
155 [controller_ modelChangedPreserveSelection:NO];
158 virtual void BookmarkAllUserNodesRemoved(
159 BookmarkModel* model,
160 const std::set<GURL>& removed_urls) OVERRIDE {
161 [controller_ modelChangedPreserveSelection:NO];
164 virtual void BookmarkNodeChanged(BookmarkModel* model,
165 const BookmarkNode* node) OVERRIDE {
166 if (!importing_ && node->is_folder())
167 [controller_ modelChangedPreserveSelection:YES];
170 virtual void BookmarkNodeChildrenReordered(
171 BookmarkModel* model,
172 const BookmarkNode* node) OVERRIDE {
174 [controller_ modelChangedPreserveSelection:YES];
177 virtual void BookmarkNodeFaviconChanged(BookmarkModel* model,
178 const BookmarkNode* node) OVERRIDE {
179 // I care nothing for these 'favicons': I only show folders.
182 virtual void ExtensiveBookmarkChangesBeginning(
183 BookmarkModel* model) OVERRIDE {
187 // Invoked after a batch import finishes. This tells observers to update
188 // themselves if they were waiting for the update to finish.
189 virtual void ExtensiveBookmarkChangesEnded(BookmarkModel* model) OVERRIDE {
191 [controller_ modelChangedPreserveSelection:YES];
195 BookmarkEditorBaseController* controller_; // weak
202 @implementation BookmarkEditorBaseController
204 @synthesize initialName = initialName_;
205 @synthesize displayName = displayName_;
207 - (id)initWithParentWindow:(NSWindow*)parentWindow
208 nibName:(NSString*)nibName
209 profile:(Profile*)profile
210 parent:(const BookmarkNode*)parent
212 title:(const base::string16&)title
213 configuration:(BookmarkEditor::Configuration)configuration {
214 NSString* nibpath = [base::mac::FrameworkBundle()
215 pathForResource:nibName
217 if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
218 parentWindow_ = parentWindow;
220 parentNode_ = parent;
223 configuration_ = configuration;
224 initialName_ = [@"" retain];
225 observer_.reset(new BookmarkEditorBaseControllerBridge(self));
226 [self bookmarkModel]->AddObserver(observer_.get());
232 [self bookmarkModel]->RemoveObserver(observer_.get());
233 [initialName_ release];
234 [displayName_ release];
238 - (void)awakeFromNib {
239 [self setDisplayName:[self initialName]];
241 if (configuration_ != BookmarkEditor::SHOW_TREE) {
242 // Remember the tree view's height; we will shrink our frame by that much.
243 NSRect frame = [[self window] frame];
244 CGFloat browserHeight = [folderTreeView_ frame].size.height;
245 frame.size.height -= browserHeight;
246 frame.origin.y += browserHeight;
247 // Remove the folder tree and "new folder" button.
248 [folderTreeView_ removeFromSuperview];
249 [newFolderButton_ removeFromSuperview];
250 // Finally, commit the size change.
251 [[self window] setFrame:frame display:YES];
254 // Build up a tree of the current folder configuration.
255 [self buildFolderTree];
258 - (void)windowDidLoad {
259 if (configuration_ == BookmarkEditor::SHOW_TREE) {
260 [self selectNodeInBrowser:parentNode_];
265 // Implementing this informal protocol allows us to open the sheet
266 // somewhere other than at the top of the window. NOTE: this means
267 // that I, the controller, am also the window's delegate.
268 - (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet
269 usingRect:(NSRect)rect {
270 // adjust rect.origin.y to be the bottom of the toolbar
275 // TODO(jrg): consider NSModalSession.
276 - (void)runAsModalSheet {
277 // Lock down floating bar when in full-screen mode. Don't animate
278 // otherwise the pane will be misplaced.
279 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
280 lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
281 [NSApp beginSheet:[self window]
282 modalForWindow:parentWindow_
284 didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
288 // This constant has to match the name of the method after it.
289 NSString* const kOkEnabledName = @"okEnabled";
294 - (IBAction)ok:(id)sender {
295 NSWindow* window = [self window];
296 [window makeFirstResponder:window];
297 // At least one of these two functions should be provided by derived classes.
298 BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)];
299 BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)];
300 DCHECK(hasWillCommit || hasDidCommit);
301 BOOL shouldContinue = YES;
303 NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)];
304 if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]])
305 shouldContinue = [hasWillContinue boolValue];
308 [self createNewFolders];
310 NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)];
311 if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]])
312 shouldContinue = [hasDidContinue boolValue];
315 [NSApp endSheet:window];
318 - (IBAction)cancel:(id)sender {
319 [NSApp endSheet:[self window]];
322 - (void)didEndSheet:(NSWindow*)sheet
323 returnCode:(int)returnCode
324 contextInfo:(void*)contextInfo {
326 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
327 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
330 - (void)windowWillClose:(NSNotification*)notification {
334 #pragma mark Folder Tree Management
336 - (BookmarkModel*)bookmarkModel {
337 return BookmarkModelFactory::GetForProfile(profile_);
340 - (Profile*)profile {
344 - (const BookmarkNode*)parentNode {
352 - (const base::string16&)title{
356 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath {
357 NSUInteger pathCount = [indexPath length];
358 BookmarkFolderInfo* item = nil;
359 NSArray* treeNode = [self folderTreeArray];
360 for (NSUInteger i = 0; i < pathCount; ++i) {
361 item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]];
362 treeNode = [item children];
367 - (NSIndexPath*)selectedIndexPath {
368 NSIndexPath* selectedIndexPath = nil;
369 NSArray* selections = [self tableSelectionPaths];
370 if ([selections count]) {
371 DCHECK([selections count] == 1); // Should be exactly one selection.
372 selectedIndexPath = [selections objectAtIndex:0];
374 return selectedIndexPath;
377 - (BookmarkFolderInfo*)selectedFolder {
378 BookmarkFolderInfo* item = nil;
379 NSIndexPath* selectedIndexPath = [self selectedIndexPath];
380 if (selectedIndexPath) {
381 item = [self folderForIndexPath:selectedIndexPath];
386 - (const BookmarkNode*)selectedNode {
387 const BookmarkNode* selectedNode = NULL;
388 // Determine a new parent node only if the browser is showing.
389 if (configuration_ == BookmarkEditor::SHOW_TREE) {
390 BookmarkFolderInfo* folderInfo = [self selectedFolder];
392 selectedNode = [folderInfo folderNode];
394 // If the tree is not showing then we use the original parent.
395 selectedNode = parentNode_;
400 - (void)expandNodes:(const BookmarkExpandedStateTracker::Nodes&)nodes {
401 id treeControllerRoot = [folderTreeController_ arrangedObjects];
402 for (BookmarkExpandedStateTracker::Nodes::const_iterator i = nodes.begin();
403 i != nodes.end(); ++i) {
404 NSIndexPath* path = [self selectionPathForNode:*i];
405 id folderNode = [treeControllerRoot descendantNodeAtIndexPath:path];
406 [folderTreeView_ expandItem:folderNode];
410 - (BookmarkExpandedStateTracker::Nodes)getExpandedNodes {
411 BookmarkExpandedStateTracker::Nodes nodes;
412 std::vector<NSUInteger> path;
413 NSArray* folderNodes = [self folderTreeArray];
415 for (BookmarkFolderInfo* folder in folderNodes) {
417 [self getExpandedNodes:&nodes
420 root:[folderTreeController_ arrangedObjects]];
427 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
428 folder:(BookmarkFolderInfo*)folder
429 path:(std::vector<NSUInteger>*)path
431 NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:&(path->front())
432 length:path->size()];
433 id node = [root descendantNodeAtIndexPath:indexPath];
434 if (![folderTreeView_ isItemExpanded:node])
436 nodes->insert([folder folderNode]);
437 NSArray* children = [folder children];
439 for (BookmarkFolderInfo* childFolder in children) {
441 [self getExpandedNodes:nodes folder:childFolder path:path root:root];
447 - (NSArray*)folderTreeArray {
448 return folderTreeArray_.get();
451 - (NSArray*)tableSelectionPaths {
452 return tableSelectionPaths_.get();
455 - (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath {
456 [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]];
459 - (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths {
460 tableSelectionPaths_.reset([tableSelectionPaths retain]);
463 - (void)selectNodeInBrowser:(const BookmarkNode*)node {
464 DCHECK(configuration_ == BookmarkEditor::SHOW_TREE);
465 NSIndexPath* selectionPath = [self selectionPathForNode:node];
466 [self willChangeValueForKey:kOkEnabledName];
467 [self setTableSelectionPath:selectionPath];
468 [self didChangeValueForKey:kOkEnabledName];
471 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode {
472 // Back up the parent chaing for desiredNode, building up a stack
473 // of ancestor nodes. Then crawl down the folderTreeArray looking
474 // for each ancestor in order while building up the selectionPath.
475 std::stack<const BookmarkNode*> nodeStack;
476 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
477 const BookmarkNode* rootNode = model->root_node();
478 const BookmarkNode* node = desiredNode;
479 while (node != rootNode) {
481 nodeStack.push(node);
482 node = node->parent();
484 NSUInteger stackSize = nodeStack.size();
486 NSIndexPath* path = nil;
487 NSArray* folders = [self folderTreeArray];
488 while (!nodeStack.empty()) {
489 node = nodeStack.top();
491 // Find node in the current folders array.
493 for (BookmarkFolderInfo *folderInfo in folders) {
494 const BookmarkNode* testNode = [folderInfo folderNode];
495 if (testNode == node) {
496 path = path ? [path indexPathByAddingIndex:i] :
497 [NSIndexPath indexPathWithIndex:i];
498 folders = [folderInfo children];
504 DCHECK([path length] == stackSize);
508 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node {
509 ChromeBookmarkClient* client =
510 ChromeBookmarkClientFactory::GetForProfile(profile_);
511 NSMutableArray* childFolders = nil;
512 int childCount = node->child_count();
513 for (int i = 0; i < childCount; ++i) {
514 const BookmarkNode* childNode = node->GetChild(i);
515 if (childNode->is_folder() && childNode->IsVisible() &&
516 client->CanBeEditedByUser(childNode)) {
517 NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle());
518 NSMutableArray* children = [self addChildFoldersFromNode:childNode];
519 BookmarkFolderInfo* folderInfo =
520 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName
524 childFolders = [NSMutableArray arrayWithObject:folderInfo];
526 [childFolders addObject:folderInfo];
532 - (void)buildFolderTree {
533 // Build up a tree of the current folder configuration.
534 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
535 const BookmarkNode* rootNode = model->root_node();
536 NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode];
538 [self willChangeValueForKey:@"folderTreeArray"];
539 folderTreeArray_.reset([baseArray retain]);
540 [self didChangeValueForKey:@"folderTreeArray"];
543 - (void)modelChangedPreserveSelection:(BOOL)preserve {
544 if (creatingNewFolders_)
546 const BookmarkNode* selectedNode = [self selectedNode];
547 [self buildFolderTree];
550 configuration_ == BookmarkEditor::SHOW_TREE)
551 [self selectNodeInBrowser:selectedNode];
554 - (void)nodeRemoved:(const BookmarkNode*)node
555 fromParent:(const BookmarkNode*)parent {
556 if (node->is_folder()) {
557 if (parentNode_ == node || parentNode_->HasAncestor(node)) {
558 parentNode_ = [self bookmarkModel]->bookmark_bar_node();
559 if (configuration_ != BookmarkEditor::SHOW_TREE) {
560 // The user can't select a different folder, so just close up shop.
566 if (configuration_ == BookmarkEditor::SHOW_TREE) {
567 // For safety's sake, in case deleted node was an ancestor of selection,
568 // go back to a known safe place.
569 [self selectNodeInBrowser:parentNode_];
574 #pragma mark New Folder Handler
576 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo
577 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo {
578 NSArray* subfolders = [folderInfo children];
579 const BookmarkNode* parentNode = [folderInfo folderNode];
582 for (BookmarkFolderInfo* subFolderInfo in subfolders) {
583 if ([subFolderInfo newFolder]) {
584 BookmarkModel* model = [self bookmarkModel];
585 const BookmarkNode* newFolder =
586 model->AddFolder(parentNode, i,
587 base::SysNSStringToUTF16([subFolderInfo folderName]));
588 // Update our dictionary with the actual folder node just created.
589 [subFolderInfo setFolderNode:newFolder];
590 [subFolderInfo setNewFolder:NO];
592 [self createNewFoldersForFolder:subFolderInfo
593 selectedFolderInfo:selectedFolderInfo];
598 - (IBAction)newFolder:(id)sender {
599 // Create a new folder off of the selected folder node.
600 BookmarkFolderInfo* parentInfo = [self selectedFolder];
602 NSIndexPath* selection = [self selectedIndexPath];
603 NSString* newFolderName =
604 l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME);
605 BookmarkFolderInfo* folderInfo =
606 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName];
607 [self willChangeValueForKey:@"folderTreeArray"];
608 NSMutableArray* children = [parentInfo children];
610 [children addObject:folderInfo];
612 children = [NSMutableArray arrayWithObject:folderInfo];
613 [parentInfo setChildren:children];
615 [self didChangeValueForKey:@"folderTreeArray"];
617 // Expose the parent folder children.
618 [folderTreeView_ expandItem:parentInfo];
620 // Select the new folder node and put the folder name into edit mode.
621 selection = [selection indexPathByAddingIndex:[children count] - 1];
622 [self setTableSelectionPath:selection];
623 NSInteger row = [folderTreeView_ selectedRow];
626 // Put the cell into single-line mode before putting it into edit mode.
627 // TODO(kushi.p): Remove this when the project hits a 10.6+ only state.
628 NSCell* folderCell = [folderTreeView_ preparedCellAtColumn:0 row:row];
630 respondsToSelector:@selector(setUsesSingleLineMode:)]) {
631 [folderCell setUsesSingleLineMode:YES];
634 [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES];
638 - (void)createNewFolders {
639 base::AutoReset<BOOL> creatingNewFoldersSetter(&creatingNewFolders_, YES);
640 // Scan the tree looking for nodes marked 'newFolder' and create those nodes.
641 NSArray* folderTreeArray = [self folderTreeArray];
642 for (BookmarkFolderInfo *folderInfo in folderTreeArray) {
643 [self createNewFoldersForFolder:folderInfo
644 selectedFolderInfo:[self selectedFolder]];
648 #pragma mark For Unit Test Use Only
650 - (BOOL)okButtonEnabled {
651 return [okButton_ isEnabled];
654 - (void)selectTestNodeInBrowser:(const BookmarkNode*)node {
655 [self selectNodeInBrowser:node];
658 @end // BookmarkEditorBaseController
660 @implementation BookmarkFolderInfo
662 @synthesize folderName = folderName_;
663 @synthesize folderNode = folderNode_;
664 @synthesize children = children_;
665 @synthesize newFolder = newFolder_;
667 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
668 folderNode:(const BookmarkNode*)folderNode
669 children:(NSMutableArray*)children {
670 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
671 folderNode:folderNode
677 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName {
678 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
685 - (id)initWithFolderName:(NSString*)folderName
686 folderNode:(const BookmarkNode*)folderNode
687 children:(NSMutableArray*)children
688 newFolder:(BOOL)newFolder {
689 if ((self = [super init])) {
690 // A folderName is always required, and if newFolder is NO then there
691 // should be a folderNode. Children is optional.
692 DCHECK(folderName && (newFolder || folderNode));
693 if (folderName && (newFolder || folderNode)) {
694 folderName_ = [folderName copy];
695 folderNode_ = folderNode;
696 children_ = [children retain];
697 newFolder_ = newFolder;
699 NOTREACHED(); // Invalid init.
708 NOTREACHED(); // Should never be called.
709 return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO];
713 [folderName_ release];
718 // Implementing isEqual: allows the NSTreeController to preserve the selection
719 // and open/shut state of outline items when the data changes.
720 - (BOOL)isEqual:(id)other {
721 return [other isKindOfClass:[BookmarkFolderInfo class]] &&
722 folderNode_ == [(BookmarkFolderInfo*)other folderNode];