blob: 538ba478a955bf827fc92b981defacffe394466a [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <stack>
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_base_controller.h"
#include "base/auto_reset.h"
#include "base/logging.h"
#include "base/mac/bundle_locations.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_model.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/profiles/profile.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_all_tabs_controller.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_editor_controller.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_name_folder_controller.h"
#import "chrome/browser/ui/cocoa/bookmarks/bookmark_tree_browser_cell.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#include "grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/l10n/l10n_util_mac.h"
@interface BookmarkEditorBaseController ()
// Return the folder tree object for the given path.
- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)path;
// (Re)build the folder tree from the BookmarkModel's current state.
- (void)buildFolderTree;
// Notifies the controller that the bookmark model has changed.
// |selection| specifies if the current selection should be
// maintained (usually YES).
- (void)modelChangedPreserveSelection:(BOOL)preserve;
// Notifies the controller that a node has been removed.
- (void)nodeRemoved:(const BookmarkNode*)node
fromParent:(const BookmarkNode*)parent;
// Given a folder node, collect an array containing BookmarkFolderInfos
// describing its subchildren which are also folders.
- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node;
// Scan the folder tree stemming from the given tree folder and create
// any newly added folders. Pass down info for the folder which was
// selected before we began creating folders.
- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)treeFolder
selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo;
// Scan the folder tree looking for the given bookmark node and return
// the selection path thereto.
- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)node;
// Implementation of getExpandedNodes. See description in header for details.
- (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
folder:(BookmarkFolderInfo*)info
path:(std::vector<NSUInteger>*)path
root:(id)root;
@end
// static; implemented for each platform. Update this function for new
// classes derived from BookmarkEditorBaseController.
void BookmarkEditor::Show(gfx::NativeWindow parent_window,
Profile* profile,
const EditDetails& details,
Configuration configuration) {
if (details.type == EditDetails::EXISTING_NODE &&
details.existing_node->is_folder()) {
BookmarkNameFolderController* controller =
[[BookmarkNameFolderController alloc]
initWithParentWindow:parent_window
profile:profile
node:details.existing_node];
[controller runAsModalSheet];
return;
}
if (details.type == EditDetails::NEW_FOLDER && details.urls.empty()) {
BookmarkNameFolderController* controller =
[[BookmarkNameFolderController alloc]
initWithParentWindow:parent_window
profile:profile
parent:details.parent_node
newIndex:details.index];
[controller runAsModalSheet];
return;
}
BookmarkEditorBaseController* controller = nil;
if (details.type == EditDetails::NEW_FOLDER) {
controller = [[BookmarkAllTabsController alloc]
initWithParentWindow:parent_window
profile:profile
parent:details.parent_node
url:details.url
title:details.title
configuration:configuration];
} else {
controller = [[BookmarkEditorController alloc]
initWithParentWindow:parent_window
profile:profile
parent:details.parent_node
node:details.existing_node
url:details.url
title:details.title
configuration:configuration];
}
[controller runAsModalSheet];
}
// Adapter to tell BookmarkEditorBaseController when bookmarks change.
class BookmarkEditorBaseControllerBridge : public BookmarkModelObserver {
public:
BookmarkEditorBaseControllerBridge(BookmarkEditorBaseController* controller)
: controller_(controller),
importing_(false)
{ }
virtual void Loaded(BookmarkModel* model, bool ids_reassigned) OVERRIDE {
[controller_ modelChangedPreserveSelection:YES];
}
virtual void BookmarkNodeMoved(BookmarkModel* model,
const BookmarkNode* old_parent,
int old_index,
const BookmarkNode* new_parent,
int new_index) OVERRIDE {
if (!importing_ && new_parent->GetChild(new_index)->is_folder())
[controller_ modelChangedPreserveSelection:YES];
}
virtual void BookmarkNodeAdded(BookmarkModel* model,
const BookmarkNode* parent,
int index) OVERRIDE {
if (!importing_ && parent->GetChild(index)->is_folder())
[controller_ modelChangedPreserveSelection:YES];
}
virtual void BookmarkNodeRemoved(BookmarkModel* model,
const BookmarkNode* parent,
int old_index,
const BookmarkNode* node) OVERRIDE {
[controller_ nodeRemoved:node fromParent:parent];
if (node->is_folder())
[controller_ modelChangedPreserveSelection:NO];
}
virtual void BookmarkAllNodesRemoved(BookmarkModel* model) OVERRIDE {
[controller_ modelChangedPreserveSelection:NO];
}
virtual void BookmarkNodeChanged(BookmarkModel* model,
const BookmarkNode* node) OVERRIDE {
if (!importing_ && node->is_folder())
[controller_ modelChangedPreserveSelection:YES];
}
virtual void BookmarkNodeChildrenReordered(
BookmarkModel* model,
const BookmarkNode* node) OVERRIDE {
if (!importing_)
[controller_ modelChangedPreserveSelection:YES];
}
virtual void BookmarkNodeFaviconChanged(BookmarkModel* model,
const BookmarkNode* node) OVERRIDE {
// I care nothing for these 'favicons': I only show folders.
}
virtual void ExtensiveBookmarkChangesBeginning(
BookmarkModel* model) OVERRIDE {
importing_ = true;
}
// Invoked after a batch import finishes. This tells observers to update
// themselves if they were waiting for the update to finish.
virtual void ExtensiveBookmarkChangesEnded(BookmarkModel* model) OVERRIDE {
importing_ = false;
[controller_ modelChangedPreserveSelection:YES];
}
private:
BookmarkEditorBaseController* controller_; // weak
bool importing_;
};
#pragma mark -
@implementation BookmarkEditorBaseController
@synthesize initialName = initialName_;
@synthesize displayName = displayName_;
@synthesize okEnabled = okEnabled_;
- (id)initWithParentWindow:(NSWindow*)parentWindow
nibName:(NSString*)nibName
profile:(Profile*)profile
parent:(const BookmarkNode*)parent
url:(const GURL&)url
title:(const base::string16&)title
configuration:(BookmarkEditor::Configuration)configuration {
NSString* nibpath = [base::mac::FrameworkBundle()
pathForResource:nibName
ofType:@"nib"];
if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
parentWindow_ = parentWindow;
profile_ = profile;
parentNode_ = parent;
url_ = url;
title_ = title;
configuration_ = configuration;
initialName_ = [@"" retain];
observer_.reset(new BookmarkEditorBaseControllerBridge(self));
[self bookmarkModel]->AddObserver(observer_.get());
}
return self;
}
- (void)dealloc {
[self bookmarkModel]->RemoveObserver(observer_.get());
[initialName_ release];
[displayName_ release];
[super dealloc];
}
- (void)awakeFromNib {
[self setDisplayName:[self initialName]];
if (configuration_ != BookmarkEditor::SHOW_TREE) {
// Remember the tree view's height; we will shrink our frame by that much.
NSRect frame = [[self window] frame];
CGFloat browserHeight = [folderTreeView_ frame].size.height;
frame.size.height -= browserHeight;
frame.origin.y += browserHeight;
// Remove the folder tree and "new folder" button.
[folderTreeView_ removeFromSuperview];
[newFolderButton_ removeFromSuperview];
// Finally, commit the size change.
[[self window] setFrame:frame display:YES];
}
// Build up a tree of the current folder configuration.
[self buildFolderTree];
}
- (void)windowDidLoad {
if (configuration_ == BookmarkEditor::SHOW_TREE) {
[self selectNodeInBrowser:parentNode_];
}
}
/* TODO(jrg):
// Implementing this informal protocol allows us to open the sheet
// somewhere other than at the top of the window. NOTE: this means
// that I, the controller, am also the window's delegate.
- (NSRect)window:(NSWindow*)window willPositionSheet:(NSWindow*)sheet
usingRect:(NSRect)rect {
// adjust rect.origin.y to be the bottom of the toolbar
return rect;
}
*/
// TODO(jrg): consider NSModalSession.
- (void)runAsModalSheet {
// Lock down floating bar when in full-screen mode. Don't animate
// otherwise the pane will be misplaced.
[[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
lockBarVisibilityForOwner:self withAnimation:NO delay:NO];
[NSApp beginSheet:[self window]
modalForWindow:parentWindow_
modalDelegate:self
didEndSelector:@selector(didEndSheet:returnCode:contextInfo:)
contextInfo:nil];
}
- (BOOL)okEnabled {
return YES;
}
- (IBAction)ok:(id)sender {
NSWindow* window = [self window];
[window makeFirstResponder:window];
// At least one of these two functions should be provided by derived classes.
BOOL hasWillCommit = [self respondsToSelector:@selector(willCommit)];
BOOL hasDidCommit = [self respondsToSelector:@selector(didCommit)];
DCHECK(hasWillCommit || hasDidCommit);
BOOL shouldContinue = YES;
if (hasWillCommit) {
NSNumber* hasWillContinue = [self performSelector:@selector(willCommit)];
if (hasWillContinue && [hasWillContinue isKindOfClass:[NSNumber class]])
shouldContinue = [hasWillContinue boolValue];
}
if (shouldContinue)
[self createNewFolders];
if (hasDidCommit) {
NSNumber* hasDidContinue = [self performSelector:@selector(didCommit)];
if (hasDidContinue && [hasDidContinue isKindOfClass:[NSNumber class]])
shouldContinue = [hasDidContinue boolValue];
}
if (shouldContinue)
[NSApp endSheet:window];
}
- (IBAction)cancel:(id)sender {
[NSApp endSheet:[self window]];
}
- (void)didEndSheet:(NSWindow*)sheet
returnCode:(int)returnCode
contextInfo:(void*)contextInfo {
[sheet close];
[[BrowserWindowController browserWindowControllerForWindow:parentWindow_]
releaseBarVisibilityForOwner:self withAnimation:YES delay:NO];
}
- (void)windowWillClose:(NSNotification*)notification {
[self autorelease];
}
#pragma mark Folder Tree Management
- (BookmarkModel*)bookmarkModel {
return BookmarkModelFactory::GetForProfile(profile_);
}
- (Profile*)profile {
return profile_;
}
- (const BookmarkNode*)parentNode {
return parentNode_;
}
- (const GURL&)url {
return url_;
}
- (const base::string16&)title{
return title_;
}
- (BookmarkFolderInfo*)folderForIndexPath:(NSIndexPath*)indexPath {
NSUInteger pathCount = [indexPath length];
BookmarkFolderInfo* item = nil;
NSArray* treeNode = [self folderTreeArray];
for (NSUInteger i = 0; i < pathCount; ++i) {
item = [treeNode objectAtIndex:[indexPath indexAtPosition:i]];
treeNode = [item children];
}
return item;
}
- (NSIndexPath*)selectedIndexPath {
NSIndexPath* selectedIndexPath = nil;
NSArray* selections = [self tableSelectionPaths];
if ([selections count]) {
DCHECK([selections count] == 1); // Should be exactly one selection.
selectedIndexPath = [selections objectAtIndex:0];
}
return selectedIndexPath;
}
- (BookmarkFolderInfo*)selectedFolder {
BookmarkFolderInfo* item = nil;
NSIndexPath* selectedIndexPath = [self selectedIndexPath];
if (selectedIndexPath) {
item = [self folderForIndexPath:selectedIndexPath];
}
return item;
}
- (const BookmarkNode*)selectedNode {
const BookmarkNode* selectedNode = NULL;
// Determine a new parent node only if the browser is showing.
if (configuration_ == BookmarkEditor::SHOW_TREE) {
BookmarkFolderInfo* folderInfo = [self selectedFolder];
if (folderInfo)
selectedNode = [folderInfo folderNode];
} else {
// If the tree is not showing then we use the original parent.
selectedNode = parentNode_;
}
return selectedNode;
}
- (void)expandNodes:(const BookmarkExpandedStateTracker::Nodes&)nodes {
id treeControllerRoot = [folderTreeController_ arrangedObjects];
for (BookmarkExpandedStateTracker::Nodes::const_iterator i = nodes.begin();
i != nodes.end(); ++i) {
NSIndexPath* path = [self selectionPathForNode:*i];
id folderNode = [treeControllerRoot descendantNodeAtIndexPath:path];
[folderTreeView_ expandItem:folderNode];
}
}
- (BookmarkExpandedStateTracker::Nodes)getExpandedNodes {
BookmarkExpandedStateTracker::Nodes nodes;
std::vector<NSUInteger> path;
NSArray* folderNodes = [self folderTreeArray];
NSUInteger i = 0;
for (BookmarkFolderInfo* folder in folderNodes) {
path.push_back(i);
[self getExpandedNodes:&nodes
folder:folder
path:&path
root:[folderTreeController_ arrangedObjects]];
path.clear();
++i;
}
return nodes;
}
- (void)getExpandedNodes:(BookmarkExpandedStateTracker::Nodes*)nodes
folder:(BookmarkFolderInfo*)folder
path:(std::vector<NSUInteger>*)path
root:(id)root {
NSIndexPath* indexPath = [NSIndexPath indexPathWithIndexes:&(path->front())
length:path->size()];
id node = [root descendantNodeAtIndexPath:indexPath];
if (![folderTreeView_ isItemExpanded:node])
return;
nodes->insert([folder folderNode]);
NSArray* children = [folder children];
NSUInteger i = 0;
for (BookmarkFolderInfo* childFolder in children) {
path->push_back(i);
[self getExpandedNodes:nodes folder:childFolder path:path root:root];
path->pop_back();
++i;
}
}
- (NSArray*)folderTreeArray {
return folderTreeArray_.get();
}
- (NSArray*)tableSelectionPaths {
return tableSelectionPaths_.get();
}
- (void)setTableSelectionPath:(NSIndexPath*)tableSelectionPath {
[self setTableSelectionPaths:[NSArray arrayWithObject:tableSelectionPath]];
}
- (void)setTableSelectionPaths:(NSArray*)tableSelectionPaths {
tableSelectionPaths_.reset([tableSelectionPaths retain]);
}
- (void)selectNodeInBrowser:(const BookmarkNode*)node {
DCHECK(configuration_ == BookmarkEditor::SHOW_TREE);
NSIndexPath* selectionPath = [self selectionPathForNode:node];
[self willChangeValueForKey:@"okEnabled"];
[self setTableSelectionPath:selectionPath];
[self didChangeValueForKey:@"okEnabled"];
}
- (NSIndexPath*)selectionPathForNode:(const BookmarkNode*)desiredNode {
// Back up the parent chaing for desiredNode, building up a stack
// of ancestor nodes. Then crawl down the folderTreeArray looking
// for each ancestor in order while building up the selectionPath.
std::stack<const BookmarkNode*> nodeStack;
BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
const BookmarkNode* rootNode = model->root_node();
const BookmarkNode* node = desiredNode;
while (node != rootNode) {
DCHECK(node);
nodeStack.push(node);
node = node->parent();
}
NSUInteger stackSize = nodeStack.size();
NSIndexPath* path = nil;
NSArray* folders = [self folderTreeArray];
while (!nodeStack.empty()) {
node = nodeStack.top();
nodeStack.pop();
// Find node in the current folders array.
NSUInteger i = 0;
for (BookmarkFolderInfo *folderInfo in folders) {
const BookmarkNode* testNode = [folderInfo folderNode];
if (testNode == node) {
path = path ? [path indexPathByAddingIndex:i] :
[NSIndexPath indexPathWithIndex:i];
folders = [folderInfo children];
break;
}
++i;
}
}
DCHECK([path length] == stackSize);
return path;
}
- (NSMutableArray*)addChildFoldersFromNode:(const BookmarkNode*)node {
NSMutableArray* childFolders = nil;
int childCount = node->child_count();
for (int i = 0; i < childCount; ++i) {
const BookmarkNode* childNode = node->GetChild(i);
if (childNode->is_folder() && childNode->IsVisible()) {
NSString* childName = base::SysUTF16ToNSString(childNode->GetTitle());
NSMutableArray* children = [self addChildFoldersFromNode:childNode];
BookmarkFolderInfo* folderInfo =
[BookmarkFolderInfo bookmarkFolderInfoWithFolderName:childName
folderNode:childNode
children:children];
if (!childFolders)
childFolders = [NSMutableArray arrayWithObject:folderInfo];
else
[childFolders addObject:folderInfo];
}
}
return childFolders;
}
- (void)buildFolderTree {
// Build up a tree of the current folder configuration.
BookmarkModel* model = BookmarkModelFactory::GetForProfile(profile_);
const BookmarkNode* rootNode = model->root_node();
NSMutableArray* baseArray = [self addChildFoldersFromNode:rootNode];
DCHECK(baseArray);
[self willChangeValueForKey:@"folderTreeArray"];
folderTreeArray_.reset([baseArray retain]);
[self didChangeValueForKey:@"folderTreeArray"];
}
- (void)modelChangedPreserveSelection:(BOOL)preserve {
if (creatingNewFolders_)
return;
const BookmarkNode* selectedNode = [self selectedNode];
[self buildFolderTree];
if (preserve &&
selectedNode &&
configuration_ == BookmarkEditor::SHOW_TREE)
[self selectNodeInBrowser:selectedNode];
}
- (void)nodeRemoved:(const BookmarkNode*)node
fromParent:(const BookmarkNode*)parent {
if (node->is_folder()) {
if (parentNode_ == node || parentNode_->HasAncestor(node)) {
parentNode_ = [self bookmarkModel]->bookmark_bar_node();
if (configuration_ != BookmarkEditor::SHOW_TREE) {
// The user can't select a different folder, so just close up shop.
[self cancel:self];
return;
}
}
if (configuration_ == BookmarkEditor::SHOW_TREE) {
// For safety's sake, in case deleted node was an ancestor of selection,
// go back to a known safe place.
[self selectNodeInBrowser:parentNode_];
}
}
}
#pragma mark New Folder Handler
- (void)createNewFoldersForFolder:(BookmarkFolderInfo*)folderInfo
selectedFolderInfo:(BookmarkFolderInfo*)selectedFolderInfo {
NSArray* subfolders = [folderInfo children];
const BookmarkNode* parentNode = [folderInfo folderNode];
DCHECK(parentNode);
NSUInteger i = 0;
for (BookmarkFolderInfo* subFolderInfo in subfolders) {
if ([subFolderInfo newFolder]) {
BookmarkModel* model = [self bookmarkModel];
const BookmarkNode* newFolder =
model->AddFolder(parentNode, i,
base::SysNSStringToUTF16([subFolderInfo folderName]));
// Update our dictionary with the actual folder node just created.
[subFolderInfo setFolderNode:newFolder];
[subFolderInfo setNewFolder:NO];
}
[self createNewFoldersForFolder:subFolderInfo
selectedFolderInfo:selectedFolderInfo];
++i;
}
}
- (IBAction)newFolder:(id)sender {
// Create a new folder off of the selected folder node.
BookmarkFolderInfo* parentInfo = [self selectedFolder];
if (parentInfo) {
NSIndexPath* selection = [self selectedIndexPath];
NSString* newFolderName =
l10n_util::GetNSStringWithFixup(IDS_BOOKMARK_EDITOR_NEW_FOLDER_NAME);
BookmarkFolderInfo* folderInfo =
[BookmarkFolderInfo bookmarkFolderInfoWithFolderName:newFolderName];
[self willChangeValueForKey:@"folderTreeArray"];
NSMutableArray* children = [parentInfo children];
if (children) {
[children addObject:folderInfo];
} else {
children = [NSMutableArray arrayWithObject:folderInfo];
[parentInfo setChildren:children];
}
[self didChangeValueForKey:@"folderTreeArray"];
// Expose the parent folder children.
[folderTreeView_ expandItem:parentInfo];
// Select the new folder node and put the folder name into edit mode.
selection = [selection indexPathByAddingIndex:[children count] - 1];
[self setTableSelectionPath:selection];
NSInteger row = [folderTreeView_ selectedRow];
DCHECK(row >= 0);
// Put the cell into single-line mode before putting it into edit mode.
// TODO(kushi.p): Remove this when the project hits a 10.6+ only state.
NSCell* folderCell = [folderTreeView_ preparedCellAtColumn:0 row:row];
if ([folderCell
respondsToSelector:@selector(setUsesSingleLineMode:)]) {
[folderCell setUsesSingleLineMode:YES];
}
[folderTreeView_ editColumn:0 row:row withEvent:nil select:YES];
}
}
- (void)createNewFolders {
base::AutoReset<BOOL> creatingNewFoldersSetter(&creatingNewFolders_, YES);
// Scan the tree looking for nodes marked 'newFolder' and create those nodes.
NSArray* folderTreeArray = [self folderTreeArray];
for (BookmarkFolderInfo *folderInfo in folderTreeArray) {
[self createNewFoldersForFolder:folderInfo
selectedFolderInfo:[self selectedFolder]];
}
}
#pragma mark For Unit Test Use Only
- (BOOL)okButtonEnabled {
return [okButton_ isEnabled];
}
- (void)selectTestNodeInBrowser:(const BookmarkNode*)node {
[self selectNodeInBrowser:node];
}
@end // BookmarkEditorBaseController
@implementation BookmarkFolderInfo
@synthesize folderName = folderName_;
@synthesize folderNode = folderNode_;
@synthesize children = children_;
@synthesize newFolder = newFolder_;
+ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName
folderNode:(const BookmarkNode*)folderNode
children:(NSMutableArray*)children {
return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
folderNode:folderNode
children:children
newFolder:NO]
autorelease];
}
+ (id)bookmarkFolderInfoWithFolderName:(NSString*)folderName {
return [[[BookmarkFolderInfo alloc] initWithFolderName:folderName
folderNode:NULL
children:nil
newFolder:YES]
autorelease];
}
- (id)initWithFolderName:(NSString*)folderName
folderNode:(const BookmarkNode*)folderNode
children:(NSMutableArray*)children
newFolder:(BOOL)newFolder {
if ((self = [super init])) {
// A folderName is always required, and if newFolder is NO then there
// should be a folderNode. Children is optional.
DCHECK(folderName && (newFolder || folderNode));
if (folderName && (newFolder || folderNode)) {
folderName_ = [folderName copy];
folderNode_ = folderNode;
children_ = [children retain];
newFolder_ = newFolder;
} else {
NOTREACHED(); // Invalid init.
[self release];
self = nil;
}
}
return self;
}
- (id)init {
NOTREACHED(); // Should never be called.
return [self initWithFolderName:nil folderNode:nil children:nil newFolder:NO];
}
- (void)dealloc {
[folderName_ release];
[children_ release];
[super dealloc];
}
// Implementing isEqual: allows the NSTreeController to preserve the selection
// and open/shut state of outline items when the data changes.
- (BOOL)isEqual:(id)other {
return [other isKindOfClass:[BookmarkFolderInfo class]] &&
folderNode_ == [(BookmarkFolderInfo*)other folderNode];
}
@end