// Copyright (c) 2009 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. #import "chrome/browser/cocoa/bookmark_manager_controller.h" #include "app/l10n_util_mac.h" #include "app/resource_bundle.h" #include "base/logging.h" #include "base/mac_util.h" #include "base/sys_string_conversions.h" #include "chrome/app/chrome_dll_resource.h" #include "chrome/browser/bookmarks/bookmark_model.h" #include "chrome/browser/bookmarks/bookmark_model_observer.h" #include "chrome/browser/bookmarks/bookmark_utils.h" #import "chrome/browser/cocoa/bookmark_item.h" #import "chrome/browser/cocoa/bookmark_tree_controller.h" #import "chrome/browser/cocoa/browser_window_controller.h" #include "chrome/browser/pref_service.h" #include "chrome/browser/profile.h" #include "chrome/common/pref_names.h" #include "grit/app_resources.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" // Max number of recently-added bookmarks to show. static const int kMaxRecents = 200; // There's at most one BookmarkManagerController at a time. This points to it. static BookmarkManagerController* sInstance; @interface BookmarkManagerController () - (void)nodeChanged:(const BookmarkNode*)node childrenChanged:(BOOL)childrenChanged; - (void)updateRecents; - (void)setupActionMenu; @end // Adapter to tell BookmarkManagerController when bookmarks change. class BookmarkManagerBridge : public BookmarkModelObserver { public: BookmarkManagerBridge(BookmarkManagerController* manager) :manager_(manager) { } virtual void Loaded(BookmarkModel* model) { // Ignore this; model has already loaded by this point. } virtual void BookmarkNodeMoved(BookmarkModel* model, const BookmarkNode* old_parent, int old_index, const BookmarkNode* new_parent, int new_index) { [manager_ nodeChanged:old_parent childrenChanged:YES]; [manager_ nodeChanged:new_parent childrenChanged:YES]; } virtual void BookmarkNodeAdded(BookmarkModel* model, const BookmarkNode* parent, int index) { [manager_ nodeChanged:parent childrenChanged:YES]; } virtual void BookmarkNodeRemoved(BookmarkModel* model, const BookmarkNode* parent, int old_index, const BookmarkNode* node) { [manager_ nodeChanged:parent childrenChanged:YES]; [manager_ forgetNode:node]; } virtual void BookmarkNodeChanged(BookmarkModel* model, const BookmarkNode* node) { [manager_ nodeChanged:node childrenChanged:NO]; } virtual void BookmarkNodeFavIconLoaded(BookmarkModel* model, const BookmarkNode* node) { [manager_ nodeChanged:node childrenChanged:NO]; } virtual void BookmarkNodeChildrenReordered(BookmarkModel* model, const BookmarkNode* node) { [manager_ nodeChanged:node childrenChanged:YES]; } private: BookmarkManagerController* manager_; // weak }; @implementation BookmarkManagerController @synthesize profile = profile_; // Private instance initialization method. - (id)initWithProfile:(Profile*)profile { // Use initWithWindowNibPath:: instead of initWithWindowNibName: so we // can override it in a unit test. NSString* nibPath = [mac_util::MainAppBundle() pathForResource:@"BookmarkManager" ofType:@"nib"]; self = [super initWithWindowNibPath:nibPath owner:self]; if (self != nil) { // Never use an incognito Profile, which can be deleted at any moment when // the user closes its browser window. Use the default one instead. DCHECK(profile); profile_ = profile->GetOriginalProfile(); bridge_.reset(new BookmarkManagerBridge(self)); profile_->GetBookmarkModel()->AddObserver(bridge_.get()); // Initialize the Recents and Search Results groups: ResourceBundle& rb = ResourceBundle::GetSharedInstance(); NSImage* recentIcon = rb.GetNSImageNamed(IDR_BOOKMARK_MANAGER_RECENT_ICON); recentGroup_.reset([[FakeBookmarkItem alloc] initWithTitle:@"Recently Added" icon:recentIcon manager:self]); NSImage* searchIcon = rb.GetNSImageNamed(IDR_BOOKMARK_MANAGER_SEARCH_ICON); searchGroup_.reset([[FakeBookmarkItem alloc] initWithTitle:@"Search Results" icon:searchIcon manager:self]); } return self; } - (void)dealloc { if (self == sInstance) { sInstance = nil; } [groupsController_ removeObserver:self forKeyPath:@"selectedItem"]; [[NSNotificationCenter defaultCenter] removeObserver:self]; if (bridge_.get()) profile_->GetBookmarkModel()->RemoveObserver(bridge_.get()); [super dealloc]; } - (void)awakeFromNib { // Set up the action button's menu. [self setupActionMenu]; // Set the tooltips of the +/- buttons. Chrome's automatic UI localization // doesn't know about tooltips of NSSegmentedCells. NSSegmentedCell* cell = [addRemoveButton_ cell]; [cell setToolTip:l10n_util::GetNSString( IDS_BOOKMARK_MANAGER_TOOLTIP_NEW_FOLDER_MAC) forSegment:0]; [cell setToolTip:l10n_util::GetNSString( IDS_BOOKMARK_MANAGER_TOOLTIP_DELETE_MAC) forSegment:1]; // Synthesize the hierarchy of the left-hand outline view. BookmarkModel* model = [self bookmarkModel]; BookmarkItem* bar = [self itemFromNode:model->GetBookmarkBarNode()]; BookmarkItem* other = [self itemFromNode:model->other_node()]; NSArray* rootItems = [NSArray arrayWithObjects: bar, other, recentGroup_.get(), nil]; root_.reset([[FakeBookmarkItem alloc] initWithTitle:@"" icon:nil manager:self]); [root_ setChildren:rootItems]; [recentGroup_ setParent:root_]; [searchGroup_ setParent:root_]; [groupsController_ setGroup:root_]; // Turning on autosave also loads and applies the settings, which we couldn't // do until setting up the data model, above. NSOutlineView* outline = [groupsController_ outline]; [outline setAutosaveExpandedItems:YES]; if (![outline isItemExpanded:bar] && ![outline isItemExpanded:other]) { // By default, expand the Bookmarks Bar and Other: [groupsController_ expandItem:bar]; [groupsController_ expandItem:other]; } // The Source-List style on the group outline has less space between rows, // so compensate for this by increasing the spacing: NSSize spacing = [[groupsController_ outline] intercellSpacing]; spacing.height += 2; [[groupsController_ outline] setIntercellSpacing:spacing]; [listController_ setShowsLeaves:YES]; [listController_ setFlat:YES]; // Observe selection changes in the groups outline. [groupsController_ addObserver:self forKeyPath:@"selectedItem" options:NSKeyValueObservingOptionInitial context:NULL]; // Register windowDidUpdate: to be called after every user event. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidUpdate:) name:NSWindowDidUpdateNotification object:[self window]]; } // When window closes, get rid of myself too. (NSWindow delegate) - (void)windowWillClose:(NSNotification*)n { [self autorelease]; } #pragma mark - #pragma mark ACCESSORS: // can't synthesize category methods, unfortunately - (BookmarkTreeController*)groupsController { return groupsController_; } - (BookmarkTreeController*)listController { return listController_; } // Returns the groups or list controller, whichever one has focus. - (BookmarkTreeController*)focusedController { id first = [[self window] firstResponder]; if ([first isKindOfClass:[BookmarksOutlineView class]]) return [(BookmarksOutlineView*)first bookmarkController]; return nil; } - (FakeBookmarkItem*)recentGroup { return recentGroup_; } - (FakeBookmarkItem*)searchGroup { return searchGroup_; } - (void)setSearchString:(NSString*)string { [searchField_ setStringValue:string]; [self searchFieldChanged:self]; } #pragma mark - #pragma mark DATA MODEL: // Getter for the |bookmarkModel| property. - (BookmarkModel*)bookmarkModel { return profile_->GetBookmarkModel(); } // Maps a BookmarkNode to a table/outline row item placeholder. - (BookmarkItem*)itemFromNode:(const BookmarkNode*)node { if (!node) return nil; if (!nodeMap_) { nodeMap_.reset([[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality valueOptions:NSPointerFunctionsStrongMemory capacity:500]); } BookmarkItem* item = (BookmarkItem*)NSMapGet(nodeMap_, node); if (!item) { item = [[BookmarkItem alloc] initWithBookmarkNode:node manager:self]; NSMapInsertKnownAbsent(nodeMap_, node, item); [item release]; } return item; } - (BookmarkItem*)bookmarkBarItem { return [self itemFromNode:[self bookmarkModel]->GetBookmarkBarNode()]; } - (BookmarkItem*)otherBookmarksItem { return [self itemFromNode:[self bookmarkModel]->other_node()]; } // Updates the mapping; called by a BookmarkItem if it changes its node. - (void)remapItem:(BookmarkItem*)item forNode:(const BookmarkNode*)node { NSMapInsert(nodeMap_, node, item); } // Removes a BookmarkNode from the node<->item mapping table. - (void)forgetNode:(const BookmarkNode*)node { NSMapRemove(nodeMap_, node); for (int i = node->GetChildCount() - 1 ; i >= 0 ; i--) { [self forgetNode:node->GetChild(i)]; } if (node == [preSearchGroup_ node]) preSearchGroup_.reset(); } // Called when the bookmark model changes; forwards to the sub-controllers. - (void)itemChanged:(BookmarkItem*)item childrenChanged:(BOOL)childrenChanged { if (item) { [item nodeChanged]; [groupsController_ itemChanged:item childrenChanged:childrenChanged]; [listController_ itemChanged:item childrenChanged:childrenChanged]; } // Update the recents or search results if they're visible. if ([groupsController_ selectedItem] == searchGroup_.get()) [self searchFieldChanged:self]; if ([groupsController_ selectedItem] == recentGroup_.get()) [self updateRecents]; } // Called when the bookmark model changes; forwards to the sub-controllers. - (void)nodeChanged:(const BookmarkNode*)node childrenChanged:(BOOL)childrenChanged { BookmarkItem* item = (BookmarkItem*)NSMapGet(nodeMap_, node); if (item) { [self itemChanged:item childrenChanged:childrenChanged]; } } - (void)updateRecents { typedef std::vector NodeVector; NodeVector nodes; bookmark_utils::GetMostRecentlyAddedEntries( [self bookmarkModel], kMaxRecents, &nodes); // Update recentGroup_: NSMutableArray* result = [NSMutableArray arrayWithCapacity:nodes.size()]; for (NodeVector::iterator it = nodes.begin(); it != nodes.end(); ++it) { [result addObject:[self itemFromNode:*it]]; } if (![result isEqual:[recentGroup_ children]]) { [recentGroup_ setChildren:result]; [self itemChanged:recentGroup_ childrenChanged:YES]; } } - (void)updateSearch { typedef std::vector MatchVector; MatchVector matches; NSString* searchString = [searchField_ stringValue]; if ([searchString length] > 0) { // Search in the BookmarkModel: std::wstring text = base::SysNSStringToWide(searchString); bookmark_utils::GetBookmarksContainingText( [self bookmarkModel], base::SysNSStringToWide(searchString), std::numeric_limits::max(), // unlimited result count profile_->GetPrefs()->GetString(prefs::kAcceptLanguages), &matches); } // Update contents of searchGroup_: NSMutableArray* result = [NSMutableArray arrayWithCapacity:matches.size()]; for (MatchVector::iterator it = matches.begin(); it != matches.end(); ++it) { [result addObject:[self itemFromNode:*it]]; } if (![result isEqual:[searchGroup_ children]]) { [searchGroup_ setChildren:result]; [self itemChanged:searchGroup_ childrenChanged:YES]; } // Show searchGroup_ if it's not visible yet: NSArray* rootItems = [root_ children]; if (![rootItems containsObject:searchGroup_]) { [root_ setChildren:[rootItems arrayByAddingObject:searchGroup_]]; [self itemChanged:root_ childrenChanged:YES]; } } - (void)selectedGroupChanged { BOOL showFolders = YES; BookmarkItem* group = [groupsController_ selectedItem]; if (group == recentGroup_.get()) [self updateRecents]; else if (group == searchGroup_.get()) [self updateSearch]; else showFolders = NO; [listController_ setGroup:group]; [listController_ setShowsFolderColumn:showFolders]; } - (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if (object == groupsController_) [self selectedGroupChanged]; } #pragma mark - #pragma mark ACTIONS: // Public entry point to open the bookmark manager. + (BookmarkManagerController*)showBookmarkManager:(Profile*)profile { if (!sInstance) { sInstance = [[self alloc] initWithProfile:profile]; } [sInstance showWindow:self]; return sInstance; } - (void)showGroup:(BookmarkItem*)group { [groupsController_ revealItem:group]; } // Makes an item visible and selects it. - (BOOL)revealItem:(BookmarkItem*)item { return [groupsController_ revealItem:[item parent]] && [listController_ revealItem:item]; } // Shows/hides the "Search Results" item. - (void)setSearchGroupVisible:(BOOL)visible { NSMutableArray* rootItems = [NSMutableArray arrayWithArray:[root_ children]]; if (visible != [rootItems containsObject:searchGroup_]) { if (visible) { [rootItems addObject:searchGroup_]; } else { [rootItems removeObject:searchGroup_]; } [root_ setChildren:rootItems]; [self itemChanged:root_ childrenChanged:YES]; } } // Called when the user modifies the search field. - (IBAction)searchFieldChanged:(id)sender { [self updateSearch]; if ([[searchField_ stringValue] length]) { // There is search text. Show searchGroup_ if it's not visible yet: [self setSearchGroupVisible:YES]; BookmarkItem *sel = [groupsController_ selectedItem]; if (sel != searchGroup_.get()) { // Remember which group used to be selected. preSearchGroup_.reset([sel retain]); // And select searchGroup_. [self showGroup:searchGroup_]; } } else { // No search text. Restore the pre-search group selction: if (preSearchGroup_.get()) { if ([groupsController_ selectedItem] == searchGroup_.get()) { [self showGroup:preSearchGroup_]; } preSearchGroup_.reset(); } // Hide the Search Results group: [self setSearchGroupVisible:NO]; } } - (IBAction)segmentedControlClicked:(id)sender { BookmarkTreeController* controller = [self focusedController]; DCHECK(controller); NSSegmentedCell* cell = [sender cell]; switch ([cell tagForSegment:[cell selectedSegment]]) { case 0: [controller newFolder:sender]; break; case 1: [controller delete:sender]; break; default: NOTREACHED(); } } - (IBAction)delete:(id)sender { [[self focusedController] delete:sender]; } - (IBAction)openItems:(id)sender { [[self focusedController] openItems:sender]; } - (IBAction)revealSelectedItem:(id)sender { [[self focusedController] revealSelectedItem:sender]; } - (IBAction)editTitle:(id)sender { [[self focusedController] editTitle:sender]; } // Called when the user picks a menu or toolbar item when this window is key. - (void)commandDispatch:(id)sender { // Copied from app_controller_mac.mm: // Handle the case where we're dispatching a command from a sender that's in a // browser window. This means that the command came from a background window // and is getting here because the foreground window is not a browser window. if ([sender respondsToSelector:@selector(window)]) { id delegate = [[sender window] windowController]; if ([delegate isKindOfClass:[BrowserWindowController class]]) { [delegate commandDispatch:sender]; return; } } switch ([sender tag]) { case IDC_FIND: [[self window] makeFirstResponder:searchField_]; break; case IDC_SHOW_BOOKMARK_MANAGER: // The Bookmark Manager menu command _closes_ the window if it's frontmost. [self close]; break; default: { // Forward other commands to the AppController -- New Window etc. [[NSApp delegate] commandDispatch:sender]; break; } } } - (BOOL)validateUserInterfaceItem:(id)item { SEL action = [item action]; if (action == @selector(commandDispatch:) || action == @selector(commandDispatchUsingKeyModifiers:)) { NSInteger tag = [item tag]; if (tag == IDC_FIND || tag == IDC_SHOW_BOOKMARK_MANAGER) return YES; // Let the AppController validate other commands -- New Window etc. return [[NSApp delegate] validateUserInterfaceItem:item]; } else if (action == @selector(newFolder:) || action == @selector(delete:) || action == @selector(openItems:) || action == @selector(revealSelectedItem:) || action == @selector(editTitle:)) { return [[self focusedController] validateUserInterfaceItem:item]; } return YES; } - (void)windowDidUpdate:(NSNotification*)n { // After any event, enable/disable the buttons: BookmarkTreeController* tree = [self focusedController]; [addRemoveButton_ setEnabled:[tree validateAction:@selector(newFolder:)] forSegment:0]; [addRemoveButton_ setEnabled:[tree validateAction:@selector(delete:)] forSegment:1]; } // Generates the pull-down menu for the "action" (gear) button. - (void)setupActionMenu { static const int kMenuActionsCount = 3; const struct {int title; SEL action;} kMenuActions[kMenuActionsCount] = { {IDS_BOOMARK_BAR_OPEN_IN_NEW_TAB, @selector(openItems:)}, {IDS_BOOKMARK_BAR_EDIT, @selector(editTitle:)}, {IDS_BOOKMARK_MANAGER_SHOW_IN_FOLDER, @selector(revealSelectedItem:)}, }; [actionButton_ setTarget:self]; NSMenu* menu = [actionButton_ menu]; for (int i = 0; i < kMenuActionsCount; i++) { if (kMenuActions[i].action) { NSString* title = l10n_util::GetNSStringWithFixup(kMenuActions[i].title); [menu addItemWithTitle:title action:kMenuActions[i].action keyEquivalent:@""]; [[[menu itemArray] lastObject] setTarget:self]; } else { [menu addItem:[NSMenuItem separatorItem]]; } } } @end