summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/bookmark_manager_controller.mm
blob: 99f57b0f2ea64a8b0f611ea2125eca2400237eb3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
// 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<const BookmarkNode*> 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<const BookmarkNode*> 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<int>::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<NSValidatedUserInterfaceItem>)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