• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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