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/foundation_util.h"
13 #include "base/strings/sys_string_conversions.h"
14 #include "chrome/browser/bookmarks/bookmark_model_factory.h"
15 #include "chrome/browser/bookmarks/managed_bookmark_service_factory.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/ui/browser_dialogs.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 "components/bookmarks/managed/managed_bookmark_service.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/l10n/l10n_util_mac.h"
29 using bookmarks::BookmarkExpandedStateTracker;
30 using bookmarks::BookmarkModel;
31 using bookmarks::BookmarkNode;
33 @interface BookmarkEditorBaseController ()
35 // Return the folder tree object for the given path.
36 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path;
38 // (Re)build the folder tree from the BookmarkModel's current state.
39 - (void)buildFolderTree;
41 // Notifies the controller that the bookmark model has changed.
42 // |selection| specifies if the current selection should be
43 // maintained (usually YES).
44 - (void)modelChangedPreserveSelection:(BOOL)preserve;
46 // Notifies the controller that a node has been removed.
47 - (void)nodeRemoved:(const BookmarkNode*)node
48 fromParent:(const BookmarkNode*)parent;
50 // Given a folder node, collect an array containing BookmarkFolderInfos
51 // describing its subchildren which are also folders.
52 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node;
54 // Scan the folder tree stemming from the given tree folder and create
55 // any newly added folders. Pass down info for the folder which was
56 // selected before we began creating folders.
57 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder
58 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo;
60 // Scan the folder tree looking for the given bookmark node and return
61 // the selection path thereto.
62 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node;
64 // Implementation of getExpandedNodes. See description in header for details.
65 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
66 folder:(BookmarkFolderInfo*)info
67 path:(std::vector<NSUInteger>*)path
71 // static; implemented for each platform. Update this function for new
72 // classes derived from BookmarkEditorBaseController.
73 void BookmarkEditor::Show(gfx::NativeWindow parent_window,
75 const EditDetails& details,
76 Configuration configuration) {
77 if (chrome::ToolkitViewsDialogsEnabled()) {
78 chrome::ShowBookmarkEditorViews(parent_window, profile, details,
83 if (details.type == EditDetails::EXISTING_NODE &&
84 details.existing_node->is_folder()) {
85 BookmarkNameFolderController* controller =
86 [[BookmarkNameFolderController alloc]
87 initWithParentWindow:parent_window
89 node:details.existing_node];
90 [controller runAsModalSheet];
94 if (details.type == EditDetails::NEW_FOLDER && details.urls.empty()) {
95 BookmarkNameFolderController* controller =
96 [[BookmarkNameFolderController alloc]
97 initWithParentWindow:parent_window
99 parent:details.parent_node
100 newIndex:details.index];
101 [controller runAsModalSheet];
105 BookmarkEditorBaseController* controller = nil;
106 if (details.type == EditDetails::NEW_FOLDER) {
107 controller = [[BookmarkAllTabsController alloc]
108 initWithParentWindow:parent_window
110 parent:details.parent_node
113 configuration:configuration];
115 controller = [[BookmarkEditorController alloc]
116 initWithParentWindow:parent_window
118 parent:details.parent_node
119 node:details.existing_node
122 configuration:configuration];
124 [controller runAsModalSheet];
127 // Adapter to tell BookmarkEditorBaseController when bookmarks change.
128 class BookmarkEditorBaseControllerBridge
129 : public bookmarks::BookmarkModelObserver {
131 BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller)
132 : controller_(controller),
136 // bookmarks::BookmarkModelObserver:
137 void BookmarkModelLoaded(BookmarkModel* model, bool ids_reassigned) override {
138 [controller_ modelChangedPreserveSelection:YES];
141 void BookmarkNodeMoved(BookmarkModel* model,
142 const BookmarkNode* old_parent,
144 const BookmarkNode* new_parent,
145 int new_index) override {
146 if (!importing_ && new_parent->GetChild(new_index)->is_folder())
147 [controller_ modelChangedPreserveSelection:YES];
150 void BookmarkNodeAdded(BookmarkModel* model,
151 const BookmarkNode* parent,
152 int index) override {
153 if (!importing_ && parent->GetChild(index)->is_folder())
154 [controller_ modelChangedPreserveSelection:YES];
157 void BookmarkNodeRemoved(BookmarkModel* model,
158 const BookmarkNode* parent,
160 const BookmarkNode* node,
161 const std::set<GURL>& removed_urls) override {
162 [controller_ nodeRemoved:node fromParent:parent];
163 if (node->is_folder())
164 [controller_ modelChangedPreserveSelection:NO];
167 void BookmarkAllUserNodesRemoved(
168 BookmarkModel* model,
169 const std::set<GURL>& removed_urls) override {
170 [controller_ modelChangedPreserveSelection:NO];
173 void BookmarkNodeChanged(BookmarkModel* model,
174 const BookmarkNode* node) override {
175 if (!importing_ && node->is_folder())
176 [controller_ modelChangedPreserveSelection:YES];
179 void BookmarkNodeChildrenReordered(BookmarkModel* model,
180 const BookmarkNode* node) override {
182 [controller_ modelChangedPreserveSelection:YES];
185 void BookmarkNodeFaviconChanged(BookmarkModel* model,
186 const BookmarkNode* node) override {
187 // I care nothing for these 'favicons': I only show folders.
190 void ExtensiveBookmarkChangesBeginning(BookmarkModel* model) override {
194 // Invoked after a batch import finishes. This tells observers to update
195 // themselves if they were waiting for the update to finish.
196 void ExtensiveBookmarkChangesEnded(BookmarkModel* model) override {
198 [controller_ modelChangedPreserveSelection:YES];
202 BookmarkEditorBaseController* controller_; // weak
209 @implementation BookmarkEditorBaseController
211 @synthesize initialName = initialName_;
212 @synthesize displayName = displayName_;
214 - (id)initWithParentWindow:(NSWindow*)parentWindow
215 nibName:(NSString*)nibName
216 profile:(Profile*)profile
217 parent:(const BookmarkNode*)parent
219 title:(const base::string16&)title
220 configuration:(BookmarkEditor::Configuration)configuration {
221 NSString* nibpath = [base::mac::FrameworkBundle()
222 pathForResource:nibName
224 if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
225 parentWindow_ = parentWindow;
227 parentNode_ = parent;
230 configuration_ = configuration;
231 initialName_ = [@"" retain];
232 observer_.reset(new BookmarkEditorBaseControllerBridge(self));
233 [self bookmarkModel]->AddObserver(observer_.get());
239 [self bookmarkModel]->RemoveObserver(observer_.get());
240 [initialName_ release];
241 [displayName_ release];
245 - (void)awakeFromNib {
246 [self setDisplayName:[self initialName]];
248 if (configuration_ != BookmarkEditor::SHOW_TREE) {
249 // Remember the tree view's height; we will shrink our frame by that much.
250 NSRect frame = [[self window] frame];
251 CGFloat browserHeight = [folderTreeView_ frame].size.height;
252 frame.size.height -= browserHeight;
253 frame.origin.y += browserHeight;
254 // Remove the folder tree and "new folder" button.
255 [folderTreeView_ removeFromSuperview];
256 [newFolderButton_ removeFromSuperview];
257 // Finally, commit the size change.
258 [[self window] setFrame:frame display:YES];
261 // Build up a tree of the current folder configuration.
262 [self buildFolderTree];
265 - (void)windowDidLoad {
266 if (configuration_ == BookmarkEditor::SHOW_TREE) {
267 [self selectNodeInBrowser:parentNode_];
272 // Implementing this informal protocol allows us to open the sheet
273 // somewhere other than at the top of the window. NOTE: this means
274 // that I, the controller, am also the window's delegate.
275 - (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet
276 usingRect:(NSRect)rect {
277 // adjust rect.origin.y to be the bottom of the toolbar
282 // TODO(jrg): consider NSModalSession.
283 - (void)runAsModalSheet {
284 // Lock down floating bar when in full-screen mode. Don't animate
285 // otherwise the pane will be misplaced.
286 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
287 lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
288 [NSApp beginSheet:[self window]
289 modalForWindow:parentWindow_
291 didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
295 // This constant has to match the name of the method after it.
296 NSString* const kOkEnabledName = @"okEnabled";
301 - (IBAction)ok:(id)sender {
302 NSWindow* window = [self window];
303 [window makeFirstResponder:window];
304 // At least one of these two functions should be provided by derived classes.
305 BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)];
306 BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)];
307 DCHECK(hasWillCommit || hasDidCommit);
308 BOOL shouldContinue = YES;
310 NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)];
311 if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]])
312 shouldContinue = [hasWillContinue boolValue];
315 [self createNewFolders];
317 NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)];
318 if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]])
319 shouldContinue = [hasDidContinue boolValue];
322 [NSApp endSheet:window];
325 - (IBAction)cancel:(id)sender {
326 [NSApp endSheet:[self window]];
329 - (void)didEndSheet:(NSWindow*)sheet
330 returnCode:(int)returnCode
331 contextInfo:(void*)contextInfo {
333 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
334 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
337 - (void)windowWillClose:(NSNotification*)notification {
341 #pragma mark Folder Tree Management
343 - (BookmarkModel*)bookmarkModel {
344 return BookmarkModelFactory::GetForProfile(profile_);
347 - (Profile*)profile {
351 - (const BookmarkNode*)parentNode {
359 - (const base::string16&)title{
363 - (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath {
364 NSUInteger pathCount = [indexPath length];
365 BookmarkFolderInfo* item = nil;
366 NSArray* treeNode = [self folderTreeArray];
367 for (NSUInteger i = 0; i < pathCount; ++i) {
368 item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]];
369 treeNode = [item children];
374 - (NSIndexPath*)selectedIndexPath {
375 NSIndexPath* selectedIndexPath = nil;
376 NSArray* selections = [self tableSelectionPaths];
377 if ([selections count]) {
378 DCHECK([selections count] == 1); // Should be exactly one selection.
379 selectedIndexPath = [selections objectAtIndex:0];
381 return selectedIndexPath;
384 - (BookmarkFolderInfo*)selectedFolder {
385 BookmarkFolderInfo* item = nil;
386 NSIndexPath* selectedIndexPath = [self selectedIndexPath];
387 if (selectedIndexPath) {
388 item = [self folderForIndexPath:selectedIndexPath];
393 - (const BookmarkNode*)selectedNode {
394 const BookmarkNode* selectedNode = NULL;
395 // Determine a new parent node only if the browser is showing.
396 if (configuration_ == BookmarkEditor::SHOW_TREE) {
397 BookmarkFolderInfo* folderInfo = [self selectedFolder];
399 selectedNode = [folderInfo folderNode];
401 // If the tree is not showing then we use the original parent.
402 selectedNode = parentNode_;
407 - (void)expandNodes:(const BookmarkExpandedStateTracker::Nodes&)nodes {
408 id treeControllerRoot = [folderTreeController_ arrangedObjects];
409 for (BookmarkExpandedStateTracker::Nodes::const_iterator i = nodes.begin();
410 i != nodes.end(); ++i) {
411 NSIndexPath* path = [self selectionPathForNode:*i];
412 id folderNode = [treeControllerRoot descendantNodeAtIndexPath:path];
413 [folderTreeView_ expandItem:folderNode];
417 - (BookmarkExpandedStateTracker::Nodes)getExpandedNodes {
418 BookmarkExpandedStateTracker::Nodes nodes;
419 std::vector<NSUInteger> path;
420 NSArray* folderNodes = [self folderTreeArray];
422 for (BookmarkFolderInfo* folder in folderNodes) {
424 [self getExpandedNodes:&nodes
427 root:[folderTreeController_ arrangedObjects]];
434 - (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
435 folder:(BookmarkFolderInfo*)folder
436 path:(std::vector<NSUInteger>*)path
438 NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:&(path->front())
439 length:path->size()];
440 id node = [root descendantNodeAtIndexPath:indexPath];
441 if (![folderTreeView_ isItemExpanded:node])
443 nodes->insert([folder folderNode]);
444 NSArray* children = [folder children];
446 for (BookmarkFolderInfo* childFolder in children) {
448 [self getExpandedNodes:nodes folder:childFolder path:path root:root];
454 - (NSArray*)folderTreeArray {
455 return folderTreeArray_.get();
458 - (NSArray*)tableSelectionPaths {
459 return tableSelectionPaths_.get();
462 - (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath {
463 [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]];
466 - (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths {
467 tableSelectionPaths_.reset([tableSelectionPaths retain]);
470 - (void)selectNodeInBrowser:(const BookmarkNode*)node {
471 DCHECK(configuration_ == BookmarkEditor::SHOW_TREE);
472 NSIndexPath* selectionPath = [self selectionPathForNode:node];
473 [self willChangeValueForKey:kOkEnabledName];
474 [self setTableSelectionPath:selectionPath];
475 [self didChangeValueForKey:kOkEnabledName];
478 - (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode {
479 // Back up the parent chain for desiredNode, building up a stack
480 // of ancestor nodes. Then crawl down the folderTreeArray looking
481 // for each ancestor in order while building up the selectionPath.
482 std::stack<const BookmarkNode*> nodeStack;
483 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
484 const BookmarkNode* rootNode = model->root_node();
485 const BookmarkNode* node = desiredNode;
486 while (node != rootNode) {
488 nodeStack.push(node);
489 node = node->parent();
491 NSUInteger stackSize = nodeStack.size();
493 NSIndexPath* path = nil;
494 NSArray* folders = [self folderTreeArray];
495 while (!nodeStack.empty()) {
496 node = nodeStack.top();
498 // Find node in the current folders array.
500 for (BookmarkFolderInfo *folderInfo in folders) {
501 const BookmarkNode* testNode = [folderInfo folderNode];
502 if (testNode == node) {
503 path = path ? [path indexPathByAddingIndex:i] :
504 [NSIndexPath indexPathWithIndex:i];
505 folders = [folderInfo children];
511 DCHECK([path length] == stackSize);
515 - (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node {
516 bookmarks::ManagedBookmarkService* managed =
517 ManagedBookmarkServiceFactory::GetForProfile(profile_);
518 NSMutableArray* childFolders = nil;
519 int childCount = node->child_count();
520 for (int i = 0; i < childCount; ++i) {
521 const BookmarkNode* childNode = node->GetChild(i);
522 if (childNode->is_folder() && childNode->IsVisible() &&
523 managed->CanBeEditedByUser(childNode)) {
524 NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle());
525 NSMutableArray* children = [self addChildFoldersFromNode:childNode];
526 BookmarkFolderInfo* folderInfo =
527 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName
531 childFolders = [NSMutableArray arrayWithObject:folderInfo];
533 [childFolders addObject:folderInfo];
539 - (void)buildFolderTree {
540 // Build up a tree of the current folder configuration.
541 BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
542 const BookmarkNode* rootNode = model->root_node();
543 NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode];
545 [self willChangeValueForKey:@"folderTreeArray"];
546 folderTreeArray_.reset([baseArray retain]);
547 [self didChangeValueForKey:@"folderTreeArray"];
550 - (void)modelChangedPreserveSelection:(BOOL)preserve {
551 if (creatingNewFolders_)
553 const BookmarkNode* selectedNode = [self selectedNode];
554 [self buildFolderTree];
557 configuration_ == BookmarkEditor::SHOW_TREE)
558 [self selectNodeInBrowser:selectedNode];
561 - (void)nodeRemoved:(const BookmarkNode*)node
562 fromParent:(const BookmarkNode*)parent {
563 if (node->is_folder()) {
564 if (parentNode_ == node || parentNode_->HasAncestor(node)) {
565 parentNode_ = [self bookmarkModel]->bookmark_bar_node();
566 if (configuration_ != BookmarkEditor::SHOW_TREE) {
567 // The user can't select a different folder, so just close up shop.
573 if (configuration_ == BookmarkEditor::SHOW_TREE) {
574 // For safety's sake, in case deleted node was an ancestor of selection,
575 // go back to a known safe place.
576 [self selectNodeInBrowser:parentNode_];
581 #pragma mark New Folder Handler
583 - (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo
584 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo {
585 NSArray* subfolders = [folderInfo children];
586 const BookmarkNode* parentNode = [folderInfo folderNode];
589 for (BookmarkFolderInfo* subFolderInfo in subfolders) {
590 if ([subFolderInfo newFolder]) {
591 BookmarkModel* model = [self bookmarkModel];
592 const BookmarkNode* newFolder =
593 model->AddFolder(parentNode, i,
594 base::SysNSStringToUTF16([subFolderInfo folderName]));
595 // Update our dictionary with the actual folder node just created.
596 [subFolderInfo setFolderNode:newFolder];
597 [subFolderInfo setNewFolder:NO];
599 [self createNewFoldersForFolder:subFolderInfo
600 selectedFolderInfo:selectedFolderInfo];
605 - (IBAction)newFolder:(id)sender {
606 // Create a new folder off of the selected folder node.
607 BookmarkFolderInfo* parentInfo = [self selectedFolder];
609 NSIndexPath* selection = [self selectedIndexPath];
610 NSString* newFolderName =
611 l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME);
612 BookmarkFolderInfo* folderInfo =
613 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName];
614 [self willChangeValueForKey:@"folderTreeArray"];
615 NSMutableArray* children = [parentInfo children];
617 [children addObject:folderInfo];
619 children = [NSMutableArray arrayWithObject:folderInfo];
620 [parentInfo setChildren:children];
622 [self didChangeValueForKey:@"folderTreeArray"];
624 // Expose the parent folder children.
625 [folderTreeView_ expandItem:parentInfo];
627 // Select the new folder node and put the folder name into edit mode.
628 selection = [selection indexPathByAddingIndex:[children count] - 1];
629 [self setTableSelectionPath:selection];
630 NSInteger row = [folderTreeView_ selectedRow];
633 // Put the cell into single-line mode before putting it into edit mode.
634 NSCell* folderCell = [folderTreeView_ preparedCellAtColumn:0 row:row];
635 [folderCell setUsesSingleLineMode:YES];
637 [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES];
641 - (void)createNewFolders {
642 base::AutoReset<BOOL> creatingNewFoldersSetter(&creatingNewFolders_, YES);
643 // Scan the tree looking for nodes marked 'newFolder' and create those nodes.
644 NSArray* folderTreeArray = [self folderTreeArray];
645 for (BookmarkFolderInfo *folderInfo in folderTreeArray) {
646 [self createNewFoldersForFolder:folderInfo
647 selectedFolderInfo:[self selectedFolder]];
651 #pragma mark For Unit Test Use Only
653 - (BOOL)okButtonEnabled {
654 return [okButton_ isEnabled];
657 - (void)selectTestNodeInBrowser:(const BookmarkNode*)node {
658 [self selectNodeInBrowser:node];
661 - (BOOL)outlineView:(NSOutlineView*)outlineView
662 shouldEditTableColumn:(NSTableColumn*)tableColumn
664 BookmarkFolderInfo* info =
665 base::mac::ObjCCast<BookmarkFolderInfo>([item representedObject]);
666 return info.newFolder;
669 @end // BookmarkEditorBaseController
671 @implementation BookmarkFolderInfo
673 @synthesize folderName = folderName_;
674 @synthesize folderNode = folderNode_;
675 @synthesize children = children_;
676 @synthesize newFolder = newFolder_;
678 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
679 folderNode:(const BookmarkNode*)folderNode
680 children:(NSMutableArray*)children {
681 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
682 folderNode:folderNode
688 + (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName {
689 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
696 - (id)initWithFolderName:(NSString*)folderName
697 folderNode:(const BookmarkNode*)folderNode
698 children:(NSMutableArray*)children
699 newFolder:(BOOL)newFolder {
700 if ((self = [super init])) {
701 // A folderName is always required, and if newFolder is NO then there
702 // should be a folderNode. Children is optional.
703 DCHECK(folderName && (newFolder || folderNode));
704 if (folderName && (newFolder || folderNode)) {
705 folderName_ = [folderName copy];
706 folderNode_ = folderNode;
707 children_ = [children retain];
708 newFolder_ = newFolder;
710 NOTREACHED(); // Invalid init.
719 NOTREACHED(); // Should never be called.
720 return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO];
724 [folderName_ release];
729 // Implementing isEqual: allows the NSTreeController to preserve the selection
730 // and open/shut state of outline items when the data changes.
731 - (BOOL)isEqual:(id)other {
732 return [other isKindOfClass:[BookmarkFolderInfo class]] &&
733 folderNode_ == [(BookmarkFolderInfo*)other folderNode];