1// Copyright (c) 2011 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. 4 5#include <stack> 6 7#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h" 8 9#include "base/logging.h" 10#include "base/mac/mac_util.h" 11#include "base/sys_string_conversions.h" 12#include "chrome/browser/bookmarks/bookmark_model.h" 13#include "chrome/browser/profiles/profile.h" 14#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h" 15#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h" 16#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h" 17#import "chrome/browser/ui/cocoa/browser_window_controller.h" 18#include "grit/generated_resources.h" 19#include "ui/base/l10n/l10n_util.h" 20#include "ui/base/l10n/l10n_util_mac.h" 21 22@interface BookmarkEditorBaseController () 23 24// Return the folder tree object for the given path. 25- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path; 26 27// (Re)build the folder tree from the BookmarkModel's current state. 28- (void)buildFolderTree; 29 30// Notifies the controller that the bookmark model has changed. 31// |selection| specifies if the current selection should be 32// maintained (usually YES). 33- (void)modelChangedPreserveSelection:(BOOL)preserve; 34 35// Notifies the controller that a node has been removed. 36- (void)nodeRemoved:(const BookmarkNode*)node 37 fromParent:(const BookmarkNode*)parent; 38 39// Given a folder node, collect an array containing BookmarkFolderInfos 40// describing its subchildren which are also folders. 41- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node; 42 43// Scan the folder tree stemming from the given tree folder and create 44// any newly added folders. Pass down info for the folder which was 45// selected before we began creating folders. 46- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder 47 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo; 48 49// Scan the folder tree looking for the given bookmark node and return 50// the selection path thereto. 51- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node; 52 53@end 54 55// static; implemented for each platform. Update this function for new 56// classes derived from BookmarkEditorBaseController. 57void BookmarkEditor::Show(gfx::NativeWindow parent_hwnd, 58 Profile* profile, 59 const BookmarkNode* parent, 60 const EditDetails& details, 61 Configuration configuration) { 62 BookmarkEditorBaseController* controller = nil; 63 if (details.type == EditDetails::NEW_FOLDER) { 64 controller = [[BookmarkAllTabsController alloc] 65 initWithParentWindow:parent_hwnd 66 profile:profile 67 parent:parent 68 configuration:configuration]; 69 } else { 70 controller = [[BookmarkEditorController alloc] 71 initWithParentWindow:parent_hwnd 72 profile:profile 73 parent:parent 74 node:details.existing_node 75 configuration:configuration]; 76 } 77 [controller runAsModalSheet]; 78} 79 80// Adapter to tell BookmarkEditorBaseController when bookmarks change. 81class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver { 82 public: 83 BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller) 84 : controller_(controller), 85 importing_(false) 86 { } 87 88 virtual void Loaded(BookmarkModel* model) { 89 [controller_ modelChangedPreserveSelection:YES]; 90 } 91 92 virtual void BookmarkNodeMoved(BookmarkModel* model, 93 const BookmarkNode* old_parent, 94 int old_index, 95 const BookmarkNode* new_parent, 96 int new_index) { 97 if (!importing_ && new_parent->GetChild(new_index)->is_folder()) 98 [controller_ modelChangedPreserveSelection:YES]; 99 } 100 101 virtual void BookmarkNodeAdded(BookmarkModel* model, 102 const BookmarkNode* parent, 103 int index) { 104 if (!importing_ && parent->GetChild(index)->is_folder()) 105 [controller_ modelChangedPreserveSelection:YES]; 106 } 107 108 virtual void BookmarkNodeRemoved(BookmarkModel* model, 109 const BookmarkNode* parent, 110 int old_index, 111 const BookmarkNode* node) { 112 [controller_ nodeRemoved:node fromParent:parent]; 113 if (node->is_folder()) 114 [controller_ modelChangedPreserveSelection:NO]; 115 } 116 117 virtual void BookmarkNodeChanged(BookmarkModel* model, 118 const BookmarkNode* node) { 119 if (!importing_ && node->is_folder()) 120 [controller_ modelChangedPreserveSelection:YES]; 121 } 122 123 virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, 124 const BookmarkNode* node) { 125 if (!importing_) 126 [controller_ modelChangedPreserveSelection:YES]; 127 } 128 129 virtual void BookmarkNodeFaviconLoaded(BookmarkModel* model, 130 const BookmarkNode* node) { 131 // I care nothing for these 'favicons': I only show folders. 132 } 133 134 virtual void BookmarkImportBeginning(BookmarkModel* model) { 135 importing_ = true; 136 } 137 138 // Invoked after a batch import finishes. This tells observers to update 139 // themselves if they were waiting for the update to finish. 140 virtual void BookmarkImportEnding(BookmarkModel* model) { 141 importing_ = false; 142 [controller_ modelChangedPreserveSelection:YES]; 143 } 144 145 private: 146 BookmarkEditorBaseController* controller_; // weak 147 bool importing_; 148}; 149 150 151#pragma mark - 152 153@implementation BookmarkEditorBaseController 154 155@synthesize initialName = initialName_; 156@synthesize displayName = displayName_; 157@synthesize okEnabled = okEnabled_; 158 159- (id)initWithParentWindow:(NSWindow*)parentWindow 160 nibName:(NSString*)nibName 161 profile:(Profile*)profile 162 parent:(const BookmarkNode*)parent 163 configuration:(BookmarkEditor::Configuration)configuration { 164 NSString* nibpath = [base::mac::MainAppBundle() 165 pathForResource:nibName 166 ofType:@"nib"]; 167 if ((self = [super initWithWindowNibPath:nibpath owner:self])) { 168 parentWindow_ = parentWindow; 169 profile_ = profile; 170 parentNode_ = parent; 171 configuration_ = configuration; 172 initialName_ = [@"" retain]; 173 observer_.reset(new BookmarkEditorBaseControllerBridge(self)); 174 [self bookmarkModel]->AddObserver(observer_.get()); 175 } 176 return self; 177} 178 179- (void)dealloc { 180 [self bookmarkModel]->RemoveObserver(observer_.get()); 181 [initialName_ release]; 182 [displayName_ release]; 183 [super dealloc]; 184} 185 186- (void)awakeFromNib { 187 [self setDisplayName:[self initialName]]; 188 189 if (configuration_ != BookmarkEditor::SHOW_TREE) { 190 // Remember the tree view's height; we will shrink our frame by that much. 191 NSRect frame = [[self window] frame]; 192 CGFloat browserHeight = [folderTreeView_ frame].size.height; 193 frame.size.height -= browserHeight; 194 frame.origin.y += browserHeight; 195 // Remove the folder tree and "new folder" button. 196 [folderTreeView_ removeFromSuperview]; 197 [newFolderButton_ removeFromSuperview]; 198 // Finally, commit the size change. 199 [[self window] setFrame:frame display:YES]; 200 } 201 202 // Build up a tree of the current folder configuration. 203 [self buildFolderTree]; 204} 205 206- (void)windowDidLoad { 207 if (configuration_ == BookmarkEditor::SHOW_TREE) { 208 [self selectNodeInBrowser:parentNode_]; 209 } 210} 211 212/* TODO(jrg): 213// Implementing this informal protocol allows us to open the sheet 214// somewhere other than at the top of the window. NOTE: this means 215// that I, the controller, am also the window's delegate. 216- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet 217 usingRect:(NSRect)rect { 218 // adjust rect.origin.y to be the bottom of the toolbar 219 return rect; 220} 221*/ 222 223// TODO(jrg): consider NSModalSession. 224- (void)runAsModalSheet { 225 // Lock down floating bar when in full-screen mode. Don't animate 226 // otherwise the pane will be misplaced. 227 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] 228 lockBarVisibilityForOwner:self withAnimation:NO delay:NO]; 229 [NSApp beginSheet:[self window] 230 modalForWindow:parentWindow_ 231 modalDelegate:self 232 didEndSelector:@selector(didEndSheet:returnCode:contextInfo:) 233 contextInfo:nil]; 234} 235 236- (BOOL)okEnabled { 237 return YES; 238} 239 240- (IBAction)ok:(id)sender { 241 // At least one of these two functions should be provided by derived classes. 242 BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)]; 243 BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)]; 244 DCHECK(hasWillCommit || hasDidCommit); 245 BOOL shouldContinue = YES; 246 if (hasWillCommit) { 247 NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)]; 248 if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]]) 249 shouldContinue = [hasWillContinue boolValue]; 250 } 251 if (shouldContinue) 252 [self createNewFolders]; 253 if (hasDidCommit) { 254 NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)]; 255 if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]]) 256 shouldContinue = [hasDidContinue boolValue]; 257 } 258 if (shouldContinue) 259 [NSApp endSheet:[self window]]; 260} 261 262- (IBAction)cancel:(id)sender { 263 [NSApp endSheet:[self window]]; 264} 265 266- (void)didEndSheet:(NSWindow*)sheet 267 returnCode:(int)returnCode 268 contextInfo:(void*)contextInfo { 269 [sheet close]; 270 [[BrowserWindowController browserWindowControllerForWindow:parentWindow_] 271 releaseBarVisibilityForOwner:self withAnimation:YES delay:NO]; 272} 273 274- (void)windowWillClose:(NSNotification*)notification { 275 [self autorelease]; 276} 277 278#pragma mark Folder Tree Management 279 280- (BookmarkModel*)bookmarkModel { 281 return profile_->GetBookmarkModel(); 282} 283 284- (const BookmarkNode*)parentNode { 285 return parentNode_; 286} 287 288- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath { 289 NSUInteger pathCount = [indexPath length]; 290 BookmarkFolderInfo* item = nil; 291 NSArray* treeNode = [self folderTreeArray]; 292 for (NSUInteger i = 0; i < pathCount; ++i) { 293 item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]]; 294 treeNode = [item children]; 295 } 296 return item; 297} 298 299- (NSIndexPath*)selectedIndexPath { 300 NSIndexPath* selectedIndexPath = nil; 301 NSArray* selections = [self tableSelectionPaths]; 302 if ([selections count]) { 303 DCHECK([selections count] == 1); // Should be exactly one selection. 304 selectedIndexPath = [selections objectAtIndex:0]; 305 } 306 return selectedIndexPath; 307} 308 309- (BookmarkFolderInfo*)selectedFolder { 310 BookmarkFolderInfo* item = nil; 311 NSIndexPath* selectedIndexPath = [self selectedIndexPath]; 312 if (selectedIndexPath) { 313 item = [self folderForIndexPath:selectedIndexPath]; 314 } 315 return item; 316} 317 318- (const BookmarkNode*)selectedNode { 319 const BookmarkNode* selectedNode = NULL; 320 // Determine a new parent node only if the browser is showing. 321 if (configuration_ == BookmarkEditor::SHOW_TREE) { 322 BookmarkFolderInfo* folderInfo = [self selectedFolder]; 323 if (folderInfo) 324 selectedNode = [folderInfo folderNode]; 325 } else { 326 // If the tree is not showing then we use the original parent. 327 selectedNode = parentNode_; 328 } 329 return selectedNode; 330} 331 332- (NSArray*)folderTreeArray { 333 return folderTreeArray_.get(); 334} 335 336- (NSArray*)tableSelectionPaths { 337 return tableSelectionPaths_.get(); 338} 339 340- (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath { 341 [self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]]; 342} 343 344- (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths { 345 tableSelectionPaths_.reset([tableSelectionPaths retain]); 346} 347 348- (void)selectNodeInBrowser:(const BookmarkNode*)node { 349 DCHECK(configuration_ == BookmarkEditor::SHOW_TREE); 350 NSIndexPath* selectionPath = [self selectionPathForNode:node]; 351 [self willChangeValueForKey:@"okEnabled"]; 352 [self setTableSelectionPath:selectionPath]; 353 [self didChangeValueForKey:@"okEnabled"]; 354} 355 356- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode { 357 // Back up the parent chaing for desiredNode, building up a stack 358 // of ancestor nodes. Then crawl down the folderTreeArray looking 359 // for each ancestor in order while building up the selectionPath. 360 std::stack<const BookmarkNode*> nodeStack; 361 BookmarkModel* model = profile_->GetBookmarkModel(); 362 const BookmarkNode* rootNode = model->root_node(); 363 const BookmarkNode* node = desiredNode; 364 while (node != rootNode) { 365 DCHECK(node); 366 nodeStack.push(node); 367 node = node->parent(); 368 } 369 NSUInteger stackSize = nodeStack.size(); 370 371 NSIndexPath* path = nil; 372 NSArray* folders = [self folderTreeArray]; 373 while (!nodeStack.empty()) { 374 node = nodeStack.top(); 375 nodeStack.pop(); 376 // Find node in the current folders array. 377 NSUInteger i = 0; 378 for (BookmarkFolderInfo *folderInfo in folders) { 379 const BookmarkNode* testNode = [folderInfo folderNode]; 380 if (testNode == node) { 381 path = path ? [path indexPathByAddingIndex:i] : 382 [NSIndexPath indexPathWithIndex:i]; 383 folders = [folderInfo children]; 384 break; 385 } 386 ++i; 387 } 388 } 389 DCHECK([path length] == stackSize); 390 return path; 391} 392 393- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node { 394 NSMutableArray* childFolders = nil; 395 int childCount = node->child_count(); 396 for (int i = 0; i < childCount; ++i) { 397 const BookmarkNode* childNode = node->GetChild(i); 398 if (childNode->type() != BookmarkNode::URL) { 399 NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle()); 400 NSMutableArray* children = [self addChildFoldersFromNode:childNode]; 401 BookmarkFolderInfo* folderInfo = 402 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName 403 folderNode:childNode 404 children:children]; 405 if (!childFolders) 406 childFolders = [NSMutableArray arrayWithObject:folderInfo]; 407 else 408 [childFolders addObject:folderInfo]; 409 } 410 } 411 return childFolders; 412} 413 414- (void)buildFolderTree { 415 // Build up a tree of the current folder configuration. 416 BookmarkModel* model = profile_->GetBookmarkModel(); 417 const BookmarkNode* rootNode = model->root_node(); 418 NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode]; 419 DCHECK(baseArray); 420 [self willChangeValueForKey:@"folderTreeArray"]; 421 folderTreeArray_.reset([baseArray retain]); 422 [self didChangeValueForKey:@"folderTreeArray"]; 423} 424 425- (void)modelChangedPreserveSelection:(BOOL)preserve { 426 const BookmarkNode* selectedNode = [self selectedNode]; 427 [self buildFolderTree]; 428 if (preserve && 429 selectedNode && 430 configuration_ == BookmarkEditor::SHOW_TREE) 431 [self selectNodeInBrowser:selectedNode]; 432} 433 434- (void)nodeRemoved:(const BookmarkNode*)node 435 fromParent:(const BookmarkNode*)parent { 436 if (node->is_folder()) { 437 if (parentNode_ == node || parentNode_->HasAncestor(node)) { 438 parentNode_ = [self bookmarkModel]->GetBookmarkBarNode(); 439 if (configuration_ != BookmarkEditor::SHOW_TREE) { 440 // The user can't select a different folder, so just close up shop. 441 [self cancel:self]; 442 return; 443 } 444 } 445 446 if (configuration_ == BookmarkEditor::SHOW_TREE) { 447 // For safety's sake, in case deleted node was an ancestor of selection, 448 // go back to a known safe place. 449 [self selectNodeInBrowser:parentNode_]; 450 } 451 } 452} 453 454#pragma mark New Folder Handler 455 456- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo 457 selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo { 458 NSArray* subfolders = [folderInfo children]; 459 const BookmarkNode* parentNode = [folderInfo folderNode]; 460 DCHECK(parentNode); 461 NSUInteger i = 0; 462 for (BookmarkFolderInfo* subFolderInfo in subfolders) { 463 if ([subFolderInfo newFolder]) { 464 BookmarkModel* model = [self bookmarkModel]; 465 const BookmarkNode* newFolder = 466 model->AddFolder(parentNode, i, 467 base::SysNSStringToUTF16([subFolderInfo folderName])); 468 // Update our dictionary with the actual folder node just created. 469 [subFolderInfo setFolderNode:newFolder]; 470 [subFolderInfo setNewFolder:NO]; 471 // If the newly created folder was selected, update the selection path. 472 if (subFolderInfo == selectedFolderInfo) { 473 NSIndexPath* selectionPath = [self selectionPathForNode:newFolder]; 474 [self setTableSelectionPath:selectionPath]; 475 } 476 } 477 [self createNewFoldersForFolder:subFolderInfo 478 selectedFolderInfo:selectedFolderInfo]; 479 ++i; 480 } 481} 482 483- (IBAction)newFolder:(id)sender { 484 // Create a new folder off of the selected folder node. 485 BookmarkFolderInfo* parentInfo = [self selectedFolder]; 486 if (parentInfo) { 487 NSIndexPath* selection = [self selectedIndexPath]; 488 NSString* newFolderName = 489 l10n_util::GetNSStringWithFixup(IDS_BOOMARK_EDITOR_NEW_FOLDER_NAME); 490 BookmarkFolderInfo* folderInfo = 491 [BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName]; 492 [self willChangeValueForKey:@"folderTreeArray"]; 493 NSMutableArray* children = [parentInfo children]; 494 if (children) { 495 [children addObject:folderInfo]; 496 } else { 497 children = [NSMutableArray arrayWithObject:folderInfo]; 498 [parentInfo setChildren:children]; 499 } 500 [self didChangeValueForKey:@"folderTreeArray"]; 501 502 // Expose the parent folder children. 503 [folderTreeView_ expandItem:parentInfo]; 504 505 // Select the new folder node and put the folder name into edit mode. 506 selection = [selection indexPathByAddingIndex:[children count] - 1]; 507 [self setTableSelectionPath:selection]; 508 NSInteger row = [folderTreeView_ selectedRow]; 509 DCHECK(row >= 0); 510 [folderTreeView_ editColumn:0 row:row withEvent:nil select:YES]; 511 } 512} 513 514- (void)createNewFolders { 515 // Turn off notifications while "importing" folders (as created in the sheet). 516 observer_->BookmarkImportBeginning([self bookmarkModel]); 517 // Scan the tree looking for nodes marked 'newFolder' and create those nodes. 518 NSArray* folderTreeArray = [self folderTreeArray]; 519 for (BookmarkFolderInfo *folderInfo in folderTreeArray) { 520 [self createNewFoldersForFolder:folderInfo 521 selectedFolderInfo:[self selectedFolder]]; 522 } 523 // Notifications back on. 524 observer_->BookmarkImportEnding([self bookmarkModel]); 525} 526 527#pragma mark For Unit Test Use Only 528 529- (BOOL)okButtonEnabled { 530 return [okButton_ isEnabled]; 531} 532 533- (void)selectTestNodeInBrowser:(const BookmarkNode*)node { 534 [self selectNodeInBrowser:node]; 535} 536 537@end // BookmarkEditorBaseController 538 539@implementation BookmarkFolderInfo 540 541@synthesize folderName = folderName_; 542@synthesize folderNode = folderNode_; 543@synthesize children = children_; 544@synthesize newFolder = newFolder_; 545 546+ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName 547 folderNode:(const BookmarkNode*)folderNode 548 children:(NSMutableArray*)children { 549 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName 550 folderNode:folderNode 551 children:children 552 newFolder:NO] 553 autorelease]; 554} 555 556+ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName { 557 return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName 558 folderNode:NULL 559 children:nil 560 newFolder:YES] 561 autorelease]; 562} 563 564- (id)initWithFolderName:(NSString*)folderName 565 folderNode:(const BookmarkNode*)folderNode 566 children:(NSMutableArray*)children 567 newFolder:(BOOL)newFolder { 568 if ((self = [super init])) { 569 // A folderName is always required, and if newFolder is NO then there 570 // should be a folderNode. Children is optional. 571 DCHECK(folderName && (newFolder || folderNode)); 572 if (folderName && (newFolder || folderNode)) { 573 folderName_ = [folderName copy]; 574 folderNode_ = folderNode; 575 children_ = [children retain]; 576 newFolder_ = newFolder; 577 } else { 578 NOTREACHED(); // Invalid init. 579 [self release]; 580 self = nil; 581 } 582 } 583 return self; 584} 585 586- (id)init { 587 NOTREACHED(); // Should never be called. 588 return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO]; 589} 590 591- (void)dealloc { 592 [folderName_ release]; 593 [children_ release]; 594 [super dealloc]; 595} 596 597// Implementing isEqual: allows the NSTreeController to preserve the selection 598// and open/shut state of outline items when the data changes. 599- (BOOL)isEqual:(id)other { 600 return [other isKindOfClass:[BookmarkFolderInfo class]] && 601 folderNode_ == [(BookmarkFolderInfo*)other folderNode]; 602} 603 604@end 605