bookmark_menu_bridge.mm revision 6d86b77056ed63eb6871182f42a9fd5f07550f90
1// Copyright (c) 2012 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#import <AppKit/AppKit.h>
6
7#include "base/strings/sys_string_conversions.h"
8#include "chrome/app/chrome_command_ids.h"
9#import "chrome/browser/app_controller_mac.h"
10#include "chrome/browser/bookmarks/bookmark_model_factory.h"
11#include "chrome/browser/bookmarks/chrome_bookmark_client.h"
12#include "chrome/browser/bookmarks/chrome_bookmark_client_factory.h"
13#include "chrome/browser/prefs/incognito_mode_prefs.h"
14#include "chrome/browser/profiles/profile.h"
15#include "chrome/browser/profiles/profile_manager.h"
16#include "chrome/browser/ui/browser_list.h"
17#include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h"
18#import "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_cocoa_controller.h"
19#include "components/bookmarks/browser/bookmark_model.h"
20#include "grit/generated_resources.h"
21#include "grit/theme_resources.h"
22#include "grit/ui_resources.h"
23#include "ui/base/l10n/l10n_util.h"
24#include "ui/base/resource/resource_bundle.h"
25#include "ui/gfx/image/image.h"
26
27BookmarkMenuBridge::BookmarkMenuBridge(Profile* profile, NSMenu* menu)
28    : menuIsValid_(false),
29      profile_(profile),
30      controller_([[BookmarkMenuCocoaController alloc] initWithBridge:this
31                                                              andMenu:menu]) {
32  if (GetBookmarkModel())
33    ObserveBookmarkModel();
34}
35
36BookmarkMenuBridge::~BookmarkMenuBridge() {
37  BookmarkModel* model = GetBookmarkModel();
38  if (model)
39    model->RemoveObserver(this);
40  [controller_ release];
41}
42
43NSMenu* BookmarkMenuBridge::BookmarkMenu() {
44  return [controller_ menu];
45}
46
47void BookmarkMenuBridge::BookmarkModelLoaded(BookmarkModel* model,
48                                             bool ids_reassigned) {
49  InvalidateMenu();
50}
51
52void BookmarkMenuBridge::UpdateMenu(NSMenu* bookmark_menu) {
53  UpdateMenuInternal(bookmark_menu, false);
54}
55
56void BookmarkMenuBridge::UpdateSubMenu(NSMenu* bookmark_menu) {
57  UpdateMenuInternal(bookmark_menu, true);
58}
59
60void BookmarkMenuBridge::UpdateMenuInternal(NSMenu* bookmark_menu,
61                                            bool is_submenu) {
62  DCHECK(bookmark_menu);
63  if (menuIsValid_)
64    return;
65
66  BookmarkModel* model = GetBookmarkModel();
67  if (!model || !model->loaded())
68    return;
69
70  if (!folder_image_) {
71    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
72    folder_image_.reset(
73        rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER).CopyNSImage());
74  }
75
76  ClearBookmarkMenu(bookmark_menu);
77
78  // Add at most one separator for the bookmark bar and the managed bookmarks
79  // folder.
80  ChromeBookmarkClient* client =
81      ChromeBookmarkClientFactory::GetForProfile(profile_);
82  const BookmarkNode* barNode = model->bookmark_bar_node();
83  const BookmarkNode* managedNode = client->managed_node();
84  if (!barNode->empty() || !managedNode->empty())
85    [bookmark_menu addItem:[NSMenuItem separatorItem]];
86  if (!managedNode->empty()) {
87    // Most users never see this node, so the image is only loaded if needed.
88    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
89    NSImage* image =
90        rb.GetNativeImageNamed(IDR_BOOKMARK_BAR_FOLDER_MANAGED).ToNSImage();
91    AddNodeAsSubmenu(bookmark_menu, managedNode, image, !is_submenu);
92  }
93  if (!barNode->empty())
94    AddNodeToMenu(barNode, bookmark_menu, !is_submenu);
95
96  // If the "Other Bookmarks" folder has any content, make a submenu for it and
97  // fill it in.
98  if (!model->other_node()->empty()) {
99    [bookmark_menu addItem:[NSMenuItem separatorItem]];
100    AddNodeAsSubmenu(bookmark_menu,
101                     model->other_node(),
102                     folder_image_,
103                     !is_submenu);
104  }
105
106  // If the "Mobile Bookmarks" folder has any content, make a submenu for it and
107  // fill it in.
108  if (!model->mobile_node()->empty()) {
109    // Add a separator if we did not already add one due to a non-empty
110    // "Other Bookmarks" folder.
111    if (model->other_node()->empty())
112      [bookmark_menu addItem:[NSMenuItem separatorItem]];
113
114    AddNodeAsSubmenu(bookmark_menu,
115                     model->mobile_node(),
116                     folder_image_,
117                     !is_submenu);
118  }
119
120  menuIsValid_ = true;
121}
122
123void BookmarkMenuBridge::BookmarkModelBeingDeleted(BookmarkModel* model) {
124  NSMenu* bookmark_menu = BookmarkMenu();
125  if (bookmark_menu == nil)
126    return;
127
128  ClearBookmarkMenu(bookmark_menu);
129}
130
131void BookmarkMenuBridge::BookmarkNodeMoved(BookmarkModel* model,
132                                           const BookmarkNode* old_parent,
133                                           int old_index,
134                                           const BookmarkNode* new_parent,
135                                           int new_index) {
136  InvalidateMenu();
137}
138
139void BookmarkMenuBridge::BookmarkNodeAdded(BookmarkModel* model,
140                                           const BookmarkNode* parent,
141                                           int index) {
142  InvalidateMenu();
143}
144
145void BookmarkMenuBridge::BookmarkNodeRemoved(
146    BookmarkModel* model,
147    const BookmarkNode* parent,
148    int old_index,
149    const BookmarkNode* node,
150    const std::set<GURL>& removed_urls) {
151  InvalidateMenu();
152}
153
154void BookmarkMenuBridge::BookmarkAllUserNodesRemoved(
155    BookmarkModel* model,
156    const std::set<GURL>& removed_urls) {
157  InvalidateMenu();
158}
159
160void BookmarkMenuBridge::BookmarkNodeChanged(BookmarkModel* model,
161                                             const BookmarkNode* node) {
162  NSMenuItem* item = MenuItemForNode(node);
163  if (item)
164    ConfigureMenuItem(node, item, true);
165}
166
167void BookmarkMenuBridge::BookmarkNodeFaviconChanged(BookmarkModel* model,
168                                                    const BookmarkNode* node) {
169  NSMenuItem* item = MenuItemForNode(node);
170  if (item)
171    ConfigureMenuItem(node, item, false);
172}
173
174void BookmarkMenuBridge::BookmarkNodeChildrenReordered(
175    BookmarkModel* model, const BookmarkNode* node) {
176  InvalidateMenu();
177}
178
179void BookmarkMenuBridge::ResetMenu() {
180  ClearBookmarkMenu(BookmarkMenu());
181}
182
183void BookmarkMenuBridge::BuildMenu() {
184  UpdateMenu(BookmarkMenu());
185}
186
187// Watch for changes.
188void BookmarkMenuBridge::ObserveBookmarkModel() {
189  BookmarkModel* model = GetBookmarkModel();
190  model->AddObserver(this);
191  if (model->loaded())
192    BookmarkModelLoaded(model, false);
193}
194
195BookmarkModel* BookmarkMenuBridge::GetBookmarkModel() {
196  if (!profile_)
197    return NULL;
198  return BookmarkModelFactory::GetForProfile(profile_);
199}
200
201Profile* BookmarkMenuBridge::GetProfile() {
202  return profile_;
203}
204
205void BookmarkMenuBridge::ClearBookmarkMenu(NSMenu* menu) {
206  bookmark_nodes_.clear();
207  // Recursively delete all menus that look like a bookmark. Also delete all
208  // separator items since we explicitly add them back in. This deletes
209  // everything except the first item ("Add Bookmark...").
210  NSArray* items = [menu itemArray];
211  for (NSMenuItem* item in items) {
212    // Convention: items in the bookmark list which are bookmarks have
213    // an action of openBookmarkMenuItem:.  Also, assume all items
214    // with submenus are submenus of bookmarks.
215    if (([item action] == @selector(openBookmarkMenuItem:)) ||
216        ([item action] == @selector(openAllBookmarks:)) ||
217        ([item action] == @selector(openAllBookmarksNewWindow:)) ||
218        ([item action] == @selector(openAllBookmarksIncognitoWindow:)) ||
219        [item hasSubmenu] ||
220        [item isSeparatorItem]) {
221      // This will eventually [obj release] all its kids, if it has
222      // any.
223      [menu removeItem:item];
224    } else {
225      // Leave it alone.
226    }
227  }
228}
229
230void BookmarkMenuBridge::AddNodeAsSubmenu(NSMenu* menu,
231                                          const BookmarkNode* node,
232                                          NSImage* image,
233                                          bool add_extra_items) {
234  NSString* title = SysUTF16ToNSString(node->GetTitle());
235  NSMenuItem* items = [[[NSMenuItem alloc]
236                            initWithTitle:title
237                                   action:nil
238                            keyEquivalent:@""] autorelease];
239  [items setImage:image];
240  [menu addItem:items];
241  NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
242  [menu setSubmenu:submenu forItem:items];
243  AddNodeToMenu(node, submenu, add_extra_items);
244}
245
246// TODO(jrg): limit the number of bookmarks in the menubar?
247void BookmarkMenuBridge::AddNodeToMenu(const BookmarkNode* node, NSMenu* menu,
248                                       bool add_extra_items) {
249  int child_count = node->child_count();
250  if (!child_count) {
251    NSString* empty_string = l10n_util::GetNSString(IDS_MENU_EMPTY_SUBMENU);
252    NSMenuItem* item =
253        [[[NSMenuItem alloc] initWithTitle:empty_string
254                                    action:nil
255                             keyEquivalent:@""] autorelease];
256    [menu addItem:item];
257  } else for (int i = 0; i < child_count; i++) {
258    const BookmarkNode* child = node->GetChild(i);
259    NSString* title = [BookmarkMenuCocoaController menuTitleForNode:child];
260    NSMenuItem* item =
261        [[[NSMenuItem alloc] initWithTitle:title
262                                    action:nil
263                             keyEquivalent:@""] autorelease];
264    [menu addItem:item];
265    bookmark_nodes_[child] = item;
266    if (child->is_folder()) {
267      [item setImage:folder_image_];
268      NSMenu* submenu = [[[NSMenu alloc] initWithTitle:title] autorelease];
269      [menu setSubmenu:submenu forItem:item];
270      AddNodeToMenu(child, submenu, add_extra_items);  // recursive call
271    } else {
272      ConfigureMenuItem(child, item, false);
273    }
274  }
275
276  if (add_extra_items) {
277    // Add menus for 'Open All Bookmarks'.
278    [menu addItem:[NSMenuItem separatorItem]];
279    bool enabled = child_count != 0;
280
281    IncognitoModePrefs::Availability incognito_availability =
282        IncognitoModePrefs::GetAvailability(profile_->GetPrefs());
283    bool incognito_enabled =
284        enabled && incognito_availability != IncognitoModePrefs::DISABLED;
285
286    AddItemToMenu(IDC_BOOKMARK_BAR_OPEN_ALL,
287                  IDS_BOOKMARK_BAR_OPEN_ALL,
288                  node, menu, enabled);
289    AddItemToMenu(IDC_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW,
290                  IDS_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW,
291                  node, menu, enabled);
292    AddItemToMenu(IDC_BOOKMARK_BAR_OPEN_ALL_INCOGNITO,
293                  IDS_BOOKMARK_BAR_OPEN_ALL_INCOGNITO,
294                  node, menu, incognito_enabled);
295  }
296}
297
298void BookmarkMenuBridge::AddItemToMenu(int command_id,
299                                       int message_id,
300                                       const BookmarkNode* node,
301                                       NSMenu* menu,
302                                       bool enabled) {
303  NSString* title = l10n_util::GetNSStringWithFixup(message_id);
304  SEL action;
305  if (!enabled) {
306    // A nil action makes a menu item appear disabled. NSMenuItem setEnabled
307    // will not reflect the disabled state until the item title is set again.
308    action = nil;
309  } else if (command_id == IDC_BOOKMARK_BAR_OPEN_ALL) {
310    action = @selector(openAllBookmarks:);
311  } else if (command_id == IDC_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW) {
312    action = @selector(openAllBookmarksNewWindow:);
313  } else {
314    action = @selector(openAllBookmarksIncognitoWindow:);
315  }
316  NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title
317                                                 action:action
318                                          keyEquivalent:@""] autorelease];
319  [item setTarget:controller_];
320  [item setTag:node->id()];
321  [item setEnabled:enabled];
322  [menu addItem:item];
323}
324
325void BookmarkMenuBridge::ConfigureMenuItem(const BookmarkNode* node,
326                                           NSMenuItem* item,
327                                           bool set_title) {
328  if (set_title)
329    [item setTitle:[BookmarkMenuCocoaController menuTitleForNode:node]];
330  [item setTarget:controller_];
331  [item setAction:@selector(openBookmarkMenuItem:)];
332  [item setTag:node->id()];
333  if (node->is_url())
334    [item setToolTip:[BookmarkMenuCocoaController tooltipForNode:node]];
335  // Check to see if we have a favicon.
336  NSImage* favicon = nil;
337  BookmarkModel* model = GetBookmarkModel();
338  if (model) {
339    const gfx::Image& image = model->GetFavicon(node);
340    if (!image.IsEmpty())
341      favicon = image.ToNSImage();
342  }
343  // If we do not have a loaded favicon, use the default site image instead.
344  if (!favicon) {
345    ResourceBundle& rb = ResourceBundle::GetSharedInstance();
346    favicon = rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).ToNSImage();
347  }
348  [item setImage:favicon];
349}
350
351NSMenuItem* BookmarkMenuBridge::MenuItemForNode(const BookmarkNode* node) {
352  if (!node)
353    return nil;
354  std::map<const BookmarkNode*, NSMenuItem*>::iterator it =
355      bookmark_nodes_.find(node);
356  if (it == bookmark_nodes_.end())
357    return nil;
358  return it->second;
359}
360