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.h"
15 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
16 #include "chrome/browser/profiles/profile.h"
17 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
18 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
19 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
20 #import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
21 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
22 #include "grit/generated_resources.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
26 @interface BookmarkEditorBaseController ()
28 // Return the folder tree object for the given path.
29 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path;
31 // (Re)build the folder tree from the BookmarkModel's current state.
32 - (void)buildFolderTree;
34 // Notifies the controller that the bookmark model has changed.
35 // |selection| specifies if the current selection should be
36 // maintained (usually YES).
37 - (void)modelChangedPreserveSelection:(BOOL)preserve;
39 // Notifies the controller that a node has been removed.
40 - (void)nodeRemoved:(const BookmarkNode*)node
41 fromParent:(const BookmarkNode*)parent;
43 // Given a folder node, collect an array containing BookmarkFolderInfos
44 // describing its subchildren which are also folders.
45 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node;
47 // Scan the folder tree stemming from the given tree folder and create
48 // any newly added folders. Pass down info for the folder which was
49 // selected before we began creating folders.
50 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder
51 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo;
53 // Scan the folder tree looking for the given bookmark node and return
54 // the selection path thereto.
55 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node;
57 // Implementation of getExpandedNodes. See description in header for details.
58 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
59 folder:(BookmarkFolderInfo*)info
60 path:(std::vector<NSUInteger>*)path
64 // static; implemented for each platform. Update this function for new
65 // classes derived from BookmarkEditorBaseController.
66 void BookmarkEditor::Show(gfx::NativeWindow parent_window,
68 const EditDetails& details,
69 Configuration configuration) {
70 if (details.type == EditDetails::EXISTING_NODE &&
71 details.existing_node->is_folder()) {
72 BookmarkNameFolderController* controller =
73 [[BookmarkNameFolderController alloc]
74 initWithParentWindow:parent_window
76 node:details.existing_node];
77 [controller runAsModalSheet];
81 if (details.type == EditDetails::NEW_FOLDER && details.urls.empty()) {
82 BookmarkNameFolderController* controller =
83 [[BookmarkNameFolderController alloc]
84 initWithParentWindow:parent_window
86 parent:details.parent_node
87 newIndex:details.index];
88 [controller runAsModalSheet];
92 BookmarkEditorBaseController* controller = nil;
93 if (details.type == EditDetails::NEW_FOLDER) {
94 controller = [[BookmarkAllTabsController alloc]
95 initWithParentWindow:parent_window
97 parent:details.parent_node
100 configuration:configuration];
102 controller = [[BookmarkEditorController alloc]
103 initWithParentWindow:parent_window
105 parent:details.parent_node
106 node:details.existing_node
109 configuration:configuration];
111 [controller runAsModalSheet];
114 // Adapter to tell BookmarkEditorBaseController when bookmarks change.
115 class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver {
117 BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller)
118 : controller_(controller),
122 virtual void BookmarkModelLoaded(BookmarkModel* model,
123 bool ids_reassigned) OVERRIDE {
124 [controller_ modelChangedPreserveSelection:YES];
127 virtual void BookmarkNodeMoved(BookmarkModel* model,
128 const BookmarkNode* old_parent,
130 const BookmarkNode* new_parent,
131 int new_index) OVERRIDE {
132 if (!importing_ && new_parent->GetChild(new_index)->is_folder())
133 [controller_ modelChangedPreserveSelection:YES];
136 virtual void BookmarkNodeAdded(BookmarkModel* model,
137 const BookmarkNode* parent,
138 int index) OVERRIDE {
139 if (!importing_ && parent->GetChild(index)->is_folder())
140 [controller_ modelChangedPreserveSelection:YES];
143 virtual void BookmarkNodeRemoved(BookmarkModel* model,
144 const BookmarkNode* parent,
146 const BookmarkNode* node) OVERRIDE {
147 [controller_ nodeRemoved:node fromParent:parent];
148 if (node->is_folder())
149 [controller_ modelChangedPreserveSelection:NO];
152 virtual void BookmarkAllNodesRemoved(BookmarkModel* model) OVERRIDE {
153 [controller_ modelChangedPreserveSelection:NO];
156 virtual void BookmarkNodeChanged(BookmarkModel* model,
157 const BookmarkNode* node) OVERRIDE {
158 if (!importing_ && node->is_folder())
159 [controller_ modelChangedPreserveSelection:YES];
162 virtual void BookmarkNodeChildrenReordered(
163 BookmarkModel* model,
164 const BookmarkNode* node) OVERRIDE {
166 [controller_ modelChangedPreserveSelection:YES];
169 virtual void BookmarkNodeFaviconChanged(BookmarkModel* model,
170 const BookmarkNode* node) OVERRIDE {
171 // I care nothing for these 'favicons': I only show folders.
174 virtual void ExtensiveBookmarkChangesBeginning(
175 BookmarkModel* model) OVERRIDE {
179 // Invoked after a batch import finishes. This tells observers to update
180 // themselves if they were waiting for the update to finish.
181 virtual void ExtensiveBookmarkChangesEnded(BookmarkModel* model) OVERRIDE {
183 [controller_ modelChangedPreserveSelection:YES];
187 BookmarkEditorBaseController* controller_; // weak
194 @implementation BookmarkEditorBaseController
196 @synthesize initialName = initialName_;
197 @synthesize displayName = displayName_;
199 - (id)initWithParentWindow:(NSWindow*)parentWindow
200 nibName:(NSString*)nibName
201 profile:(Profile*)profile
202 parent:(const BookmarkNode*)parent
204 title:(const base::string16&)title
205 configuration:(BookmarkEditor::Configuration)configuration {
206 NSString* nibpath = [base::mac::FrameworkBundle()
207 pathForResource:nibName
209 if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
210 parentWindow_ = parentWindow;
212 parentNode_ = parent;
215 configuration_ = configuration;
216 initialName_ = [@"" retain];
217 observer_.reset(new BookmarkEditorBaseControllerBridge(self));
218 [self bookmarkModel]->AddObserver(observer_.get());
224 [self bookmarkModel]->RemoveObserver(observer_.get());
225 [initialName_ release];
226 [displayName_ release];
230 - (void)awakeFromNib {
231 [self setDisplayName:[self initialName]];
233 if (configuration_ != BookmarkEditor::SHOW_TREE) {
234 // Remember the tree view's height; we will shrink our frame by that much.
235 NSRect frame = [[self window] frame];
236 CGFloat browserHeight = [folderTreeView_ frame].size.height;
237 frame.size.height -= browserHeight;
238 frame.origin.y += browserHeight;
239 // Remove the folder tree and "new folder" button.
240 [folderTreeView_ removeFromSuperview];
241 [newFolderButton_ removeFromSuperview];
242 // Finally, commit the size change.
243 [[self window] setFrame:frame display:YES];
246 // Build up a tree of the current folder configuration.
247 [self buildFolderTree];
250 - (void)windowDidLoad {
251 if (configuration_ == BookmarkEditor::SHOW_TREE) {
252 [self selectNodeInBrowser:parentNode_];
257 // Implementing this informal protocol allows us to open the sheet
258 // somewhere other than at the top of the window. NOTE: this means
259 // that I, the controller, am also the window's delegate.
260 - (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet
261 usingRect:(NSRect)rect {
262 // adjust rect.origin.y to be the bottom of the toolbar
267 // TODO(jrg): consider NSModalSession.
268 - (void)runAsModalSheet {
269 // Lock down floating bar when in full-screen mode. Don't animate
270 // otherwise the pane will be misplaced.
271 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
272 lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
273 [NSApp beginSheet:[self window]
274 modalForWindow:parentWindow_
276 didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
280 // This constant has to match the name of the method after it.
281 NSString* const kOkEnabledName = @"okEnabled";
286 - (IBAction)ok:(id)sender {
287 NSWindow* window = [self window];
288 [window makeFirstResponder:window];
289 // At least one of these two functions should be provided by derived classes.
290 BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)];
291 BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)];
292 DCHECK(hasWillCommit || hasDidCommit);
293 BOOL shouldContinue = YES;
295 NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)];
296 if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]])
297 shouldContinue = [hasWillContinue boolValue];
300 [self createNewFolders];
302 NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)];
303 if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]])
304 shouldContinue = [hasDidContinue boolValue];
307 [NSApp endSheet:window];
310 - (IBAction)cancel:(id)sender {
311 [NSApp endSheet:[self window]];
314 - (void)didEndSheet:(NSWindow*)sheet
315 returnCode:(int)returnCode
316 contextInfo:(void*)contextInfo {
318 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
319 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
322 - (void)windowWillClose:(NSNotification*)notification {
326 #pragma mark Folder Tree Management
328 - (BookmarkModel*)bookmarkModel {
329 return BookmarkModelFactory::GetForProfile(profile_);
332 - (Profile*)profile {
336 - (const BookmarkNode*)parentNode {
344 - (const base::string16&)title{
348 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath {
349 NSUInteger pathCount = [indexPath length];
350 BookmarkFolderInfo* item = nil;
351 NSArray* treeNode = [self folderTreeArray];
352 for (NSUInteger i = 0; i < pathCount; ++i) {
353 item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]];
354 treeNode = [item children];
359 - (NSIndexPath*)selectedIndexPath {
360 NSIndexPath* selectedIndexPath = nil;
361 NSArray* selections = [self tableSelectionPaths];
362 if ([selections count]) {
363 DCHECK([selections count] == 1); // Should be exactly one selection.
364 selectedIndexPath = [selections objectAtIndex:0];
366 return selectedIndexPath;
369 - (BookmarkFolderInfo*)selectedFolder {
370 BookmarkFolderInfo* item = nil;
371 NSIndexPath* selectedIndexPath = [self selectedIndexPath];
372 if (selectedIndexPath) {
373 item = [self folderForIndexPath:selectedIndexPath];
378 - (const BookmarkNode*)selectedNode {
379 const BookmarkNode* selectedNode = NULL;
380 // Determine a new parent node only if the browser is showing.
381 if (configuration_ == BookmarkEditor::SHOW_TREE) {
382 BookmarkFolderInfo* folderInfo = [self selectedFolder];
384 selectedNode = [folderInfo folderNode];
386 // If the tree is not showing then we use the original parent.
387 selectedNode = parentNode_;
392 - (void)expandNodes:(const BookmarkExpandedStateTracker::Nodes&)nodes {
393 id treeControllerRoot = [folderTreeController_ arrangedObjects];
394 for (BookmarkExpandedStateTracker::Nodes::const_iterator i = nodes.begin();
395 i != nodes.end(); ++i) {
396 NSIndexPath* path = [self selectionPathForNode:*i];
397 id folderNode = [treeControllerRoot descendantNodeAtIndexPath:path];
398 [folderTreeView_ expandItem:folderNode];
402 - (BookmarkExpandedStateTracker::Nodes)getExpandedNodes {
403 BookmarkExpandedStateTracker::Nodes nodes;
404 std::vector<NSUInteger> path;
405 NSArray* folderNodes = [self folderTreeArray];
407 for (BookmarkFolderInfo* folder in folderNodes) {
409 [self getExpandedNodes:&nodes
412 root:[folderTreeController_ arrangedObjects]];
419 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
420 folder:(BookmarkFolderInfo*)folder
421 path:(std::vector<NSUInteger>*)path
423 NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:&(path->front())
424 length:path->size()];
425 id node = [root descendantNodeAtIndexPath:indexPath];
426 if (![folderTreeView_ isItemExpanded:node])
428 nodes->insert([folder folderNode]);
429 NSArray* children = [folder children];
431 for (BookmarkFolderInfo* childFolder in children) {
433 [self getExpandedNodes:nodes folder:childFolder path:path root:root];
439 - (NSArray*)folderTreeArray {
440 return folderTreeArray_.get();
443 - (NSArray*)tableSelectionPaths {
444 return tableSelectionPaths_.get();
447 - (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath {
448 [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]];
451 - (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths {
452 tableSelectionPaths_.reset([tableSelectionPaths retain]);
455 - (void)selectNodeInBrowser:(const BookmarkNode*)node {
456 DCHECK(configuration_ == BookmarkEditor::SHOW_TREE);
457 NSIndexPath* selectionPath = [self selectionPathForNode:node];
458 [self willChangeValueForKey:kOkEnabledName];
459 [self setTableSelectionPath:selectionPath];
460 [self didChangeValueForKey:kOkEnabledName];
463 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode {
464 // Back up the parent chaing for desiredNode, building up a stack
465 // of ancestor nodes. Then crawl down the folderTreeArray looking
466 // for each ancestor in order while building up the selectionPath.
467 std::stack<const BookmarkNode*> nodeStack;
468 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
469 const BookmarkNode* rootNode = model->root_node();
470 const BookmarkNode* node = desiredNode;
471 while (node != rootNode) {
473 nodeStack.push(node);
474 node = node->parent();
476 NSUInteger stackSize = nodeStack.size();
478 NSIndexPath* path = nil;
479 NSArray* folders = [self folderTreeArray];
480 while (!nodeStack.empty()) {
481 node = nodeStack.top();
483 // Find node in the current folders array.
485 for (BookmarkFolderInfo *folderInfo in folders) {
486 const BookmarkNode* testNode = [folderInfo folderNode];
487 if (testNode == node) {
488 path = path ? [path indexPathByAddingIndex:i] :
489 [NSIndexPath indexPathWithIndex:i];
490 folders = [folderInfo children];
496 DCHECK([path length] == stackSize);
500 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node {
501 NSMutableArray* childFolders = nil;
502 int childCount = node->child_count();
503 for (int i = 0; i < childCount; ++i) {
504 const BookmarkNode* childNode = node->GetChild(i);
505 if (childNode->is_folder() && childNode->IsVisible()) {
506 NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle());
507 NSMutableArray* children = [self addChildFoldersFromNode:childNode];
508 BookmarkFolderInfo* folderInfo =
509 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName
513 childFolders = [NSMutableArray arrayWithObject:folderInfo];
515 [childFolders addObject:folderInfo];
521 - (void)buildFolderTree {
522 // Build up a tree of the current folder configuration.
523 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
524 const BookmarkNode* rootNode = model->root_node();
525 NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode];
527 [self willChangeValueForKey:@"folderTreeArray"];
528 folderTreeArray_.reset([baseArray retain]);
529 [self didChangeValueForKey:@"folderTreeArray"];
532 - (void)modelChangedPreserveSelection:(BOOL)preserve {
533 if (creatingNewFolders_)
535 const BookmarkNode* selectedNode = [self selectedNode];
536 [self buildFolderTree];
539 configuration_ == BookmarkEditor::SHOW_TREE)
540 [self selectNodeInBrowser:selectedNode];
543 - (void)nodeRemoved:(const BookmarkNode*)node
544 fromParent:(const BookmarkNode*)parent {
545 if (node->is_folder()) {
546 if (parentNode_ == node || parentNode_->HasAncestor(node)) {
547 parentNode_ = [self bookmarkModel]->bookmark_bar_node();
548 if (configuration_ != BookmarkEditor::SHOW_TREE) {
549 // The user can't select a different folder, so just close up shop.
555 if (configuration_ == BookmarkEditor::SHOW_TREE) {
556 // For safety's sake, in case deleted node was an ancestor of selection,
557 // go back to a known safe place.
558 [self selectNodeInBrowser:parentNode_];
563 #pragma mark New Folder Handler
565 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo
566 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo {
567 NSArray* subfolders = [folderInfo children];
568 const BookmarkNode* parentNode = [folderInfo folderNode];
571 for (BookmarkFolderInfo* subFolderInfo in subfolders) {
572 if ([subFolderInfo newFolder]) {
573 BookmarkModel* model = [self bookmarkModel];
574 const BookmarkNode* newFolder =
575 model->AddFolder(parentNode, i,
576 base::SysNSStringToUTF16([subFolderInfo folderName]));
577 // Update our dictionary with the actual folder node just created.
578 [subFolderInfo setFolderNode:newFolder];
579 [subFolderInfo setNewFolder:NO];
581 [self createNewFoldersForFolder:subFolderInfo
582 selectedFolderInfo:selectedFolderInfo];
587 - (IBAction)newFolder:(id)sender {
588 // Create a new folder off of the selected folder node.
589 BookmarkFolderInfo* parentInfo = [self selectedFolder];
591 NSIndexPath* selection = [self selectedIndexPath];
592 NSString* newFolderName =
593 l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME);
594 BookmarkFolderInfo* folderInfo =
595 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName];
596 [self willChangeValueForKey:@"folderTreeArray"];
597 NSMutableArray* children = [parentInfo children];
599 [children addObject:folderInfo];
601 children = [NSMutableArray arrayWithObject:folderInfo];
602 [parentInfo setChildren:children];
604 [self didChangeValueForKey:@"folderTreeArray"];
606 // Expose the parent folder children.
607 [folderTreeView_ expandItem:parentInfo];
609 // Select the new folder node and put the folder name into edit mode.
610 selection = [selection indexPathByAddingIndex:[children count] - 1];
611 [self setTableSelectionPath:selection];
612 NSInteger row = [folderTreeView_ selectedRow];
615 // Put the cell into single-line mode before putting it into edit mode.
616 // TODO(kushi.p): Remove this when the project hits a 10.6+ only state.
617 NSCell* folderCell = [folderTreeView_ preparedCellAtColumn:0 row:row];
619 respondsToSelector:@selector(setUsesSingleLineMode:)]) {
620 [folderCell setUsesSingleLineMode:YES];
623 [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES];
627 - (void)createNewFolders {
628 base::AutoReset<BOOL> creatingNewFoldersSetter(&creatingNewFolders_, YES);
629 // Scan the tree looking for nodes marked 'newFolder' and create those nodes.
630 NSArray* folderTreeArray = [self folderTreeArray];
631 for (BookmarkFolderInfo *folderInfo in folderTreeArray) {
632 [self createNewFoldersForFolder:folderInfo
633 selectedFolderInfo:[self selectedFolder]];
637 #pragma mark For Unit Test Use Only
639 - (BOOL)okButtonEnabled {
640 return [okButton_ isEnabled];
643 - (void)selectTestNodeInBrowser:(const BookmarkNode*)node {
644 [self selectNodeInBrowser:node];
647 @end // BookmarkEditorBaseController
649 @implementation BookmarkFolderInfo
651 @synthesize folderName = folderName_;
652 @synthesize folderNode = folderNode_;
653 @synthesize children = children_;
654 @synthesize newFolder = newFolder_;
656 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
657 folderNode:(const BookmarkNode*)folderNode
658 children:(NSMutableArray*)children {
659 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
660 folderNode:folderNode
666 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName {
667 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
674 - (id)initWithFolderName:(NSString*)folderName
675 folderNode:(const BookmarkNode*)folderNode
676 children:(NSMutableArray*)children
677 newFolder:(BOOL)newFolder {
678 if ((self = [super init])) {
679 // A folderName is always required, and if newFolder is NO then there
680 // should be a folderNode. Children is optional.
681 DCHECK(folderName && (newFolder || folderNode));
682 if (folderName && (newFolder || folderNode)) {
683 folderName_ = [folderName copy];
684 folderNode_ = folderNode;
685 children_ = [children retain];
686 newFolder_ = newFolder;
688 NOTREACHED(); // Invalid init.
697 NOTREACHED(); // Should never be called.
698 return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO];
702 [folderName_ release];
707 // Implementing isEqual: allows the NSTreeController to preserve the selection
708 // and open/shut state of outline items when the data changes.
709 - (BOOL)isEqual:(id)other {
710 return [other isKindOfClass:[BookmarkFolderInfo class]] &&
711 folderNode_ == [(BookmarkFolderInfo*)other folderNode];