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.
5 #include "chrome/browser/ui/cocoa/history_menu_bridge.h"
8 #include "base/bind_helpers.h"
9 #include "base/stl_util.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU
14 #import "chrome/browser/app_controller_mac.h"
15 #include "chrome/browser/chrome_notification_types.h"
16 #include "chrome/browser/favicon/favicon_service_factory.h"
17 #include "chrome/browser/history/history_service_factory.h"
18 #include "chrome/browser/history/page_usage_data.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/sessions/session_types.h"
21 #include "chrome/browser/sessions/tab_restore_service_factory.h"
22 #import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h"
23 #include "chrome/common/url_constants.h"
24 #include "components/favicon_base/favicon_types.h"
25 #include "content/public/browser/notification_registrar.h"
26 #include "content/public/browser/notification_source.h"
27 #include "grit/generated_resources.h"
28 #include "grit/theme_resources.h"
29 #include "grit/ui_resources.h"
30 #include "skia/ext/skia_utils_mac.h"
31 #include "third_party/skia/include/core/SkBitmap.h"
32 #include "ui/base/l10n/l10n_util.h"
33 #include "ui/base/resource/resource_bundle.h"
34 #include "ui/gfx/codec/png_codec.h"
35 #include "ui/gfx/favicon_size.h"
36 #include "ui/gfx/image/image.h"
40 // Menus more than this many chars long will get trimmed.
41 const NSUInteger kMaximumMenuWidthInChars = 50;
43 // When trimming, use this many chars from each side.
44 const NSUInteger kMenuTrimSizeInChars = 25;
46 // Number of days to consider when getting the number of visited items.
47 const int kVisitedScope = 90;
49 // The number of visisted results to get.
50 const int kVisitedCount = 15;
52 // The number of recently closed items to get.
53 const unsigned int kRecentlyClosedCount = 10;
57 HistoryMenuBridge::HistoryItem::HistoryItem()
58 : icon_requested(false),
59 icon_task_id(base::CancelableTaskTracker::kBadTaskId),
63 HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy)
66 icon_requested(false),
67 icon_task_id(base::CancelableTaskTracker::kBadTaskId),
69 session_id(copy.session_id) {}
71 HistoryMenuBridge::HistoryItem::~HistoryItem() {
74 HistoryMenuBridge::HistoryMenuBridge(Profile* profile)
75 : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]),
77 history_service_(NULL),
78 tab_restore_service_(NULL),
79 create_in_progress_(false),
80 need_recreate_(false) {
81 // If we don't have a profile, do not bother initializing our data sources.
82 // This shouldn't happen except in unit tests.
84 // Check to see if the history service is ready. Because it loads async, it
85 // may not be ready when the Bridge is created. If this happens, register
86 // for a notification that tells us the HistoryService is ready.
87 HistoryService* hs = HistoryServiceFactory::GetForProfile(
88 profile_, Profile::EXPLICIT_ACCESS);
89 if (hs != NULL && hs->BackendLoaded()) {
90 history_service_ = hs;
94 tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_);
95 if (tab_restore_service_) {
96 tab_restore_service_->AddObserver(this);
97 // If the tab entries are already loaded, invoke the observer method to
98 // build the "Recently Closed" section. Otherwise it will be when the
100 if (!tab_restore_service_->IsLoaded())
101 tab_restore_service_->LoadTabsFromLastSession();
103 TabRestoreServiceChanged(tab_restore_service_);
107 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
108 default_favicon_.reset(
109 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
111 // Set the static icons in the menu.
112 NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY];
113 [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON).ToNSImage()];
115 // The service is not ready for use yet, so become notified when it does.
116 if (!history_service_) {
118 this, chrome::NOTIFICATION_HISTORY_LOADED,
119 content::Source<Profile>(profile_));
123 // Note that all requests sent to either the history service or the favicon
124 // service will be automatically cancelled by their respective Consumers, so
125 // task cancellation is not done manually here in the dtor.
126 HistoryMenuBridge::~HistoryMenuBridge() {
127 // Unregister ourselves as observers and notifications.
129 if (history_service_) {
130 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
131 content::Source<Profile>(profile_));
132 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
133 content::Source<Profile>(profile_));
134 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
135 content::Source<Profile>(profile_));
137 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_LOADED,
138 content::Source<Profile>(profile_));
141 if (tab_restore_service_)
142 tab_restore_service_->RemoveObserver(this);
144 // Since the map owns the HistoryItems, delete anything that still exists.
145 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin();
146 while (it != menu_item_map_.end()) {
147 HistoryItem* item = it->second;
148 menu_item_map_.erase(it++);
153 void HistoryMenuBridge::Observe(int type,
154 const content::NotificationSource& source,
155 const content::NotificationDetails& details) {
156 // A history service is now ready. Check to see if it's the one for the main
157 // profile. If so, perform final initialization.
158 if (type == chrome::NOTIFICATION_HISTORY_LOADED) {
159 HistoryService* hs = HistoryServiceFactory::GetForProfile(
160 profile_, Profile::EXPLICIT_ACCESS);
161 if (hs != NULL && hs->BackendLoaded()) {
162 history_service_ = hs;
165 // Found our HistoryService, so stop listening for this notification.
166 registrar_.Remove(this,
167 chrome::NOTIFICATION_HISTORY_LOADED,
168 content::Source<Profile>(profile_));
172 // All other notification types that we observe indicate that the history has
173 // changed and we need to rebuild.
174 need_recreate_ = true;
178 void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) {
179 const TabRestoreService::Entries& entries = service->entries();
181 // Clear the history menu before rebuilding.
182 NSMenu* menu = HistoryMenu();
183 ClearMenuSection(menu, kRecentlyClosed);
185 // Index for the next menu item.
186 NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1;
187 NSUInteger added_count = 0;
189 for (TabRestoreService::Entries::const_iterator it = entries.begin();
190 it != entries.end() && added_count < kRecentlyClosedCount; ++it) {
191 TabRestoreService::Entry* entry = *it;
193 // If this is a window, create a submenu for all of its tabs.
194 if (entry->type == TabRestoreService::WINDOW) {
195 TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry;
196 std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs;
200 // Create the item for the parent/window. Do not set the title yet because
201 // the actual number of items that are in the menu will not be known until
202 // things like the NTP are filtered out, which is done when the tab items
203 // are actually created.
204 HistoryItem* item = new HistoryItem();
205 item->session_id = entry_win->id;
207 // Create the submenu.
208 base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]);
210 // Create standard items within the window submenu.
211 NSString* restore_title = l10n_util::GetNSString(
212 IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC);
213 base::scoped_nsobject<NSMenuItem> restore_item(
214 [[NSMenuItem alloc] initWithTitle:restore_title
215 action:@selector(openHistoryMenuItem:)
217 [restore_item setTarget:controller_.get()];
218 // Duplicate the HistoryItem otherwise the different NSMenuItems will
219 // point to the same HistoryItem, which would then be double-freed when
220 // removing the items from the map or in the dtor.
221 HistoryItem* dup_item = new HistoryItem(*item);
222 menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item));
223 [submenu addItem:restore_item.get()];
224 [submenu addItem:[NSMenuItem separatorItem]];
226 // Loop over the window's tabs and add them to the submenu.
227 NSInteger subindex = [[submenu itemArray] count];
228 std::vector<TabRestoreService::Tab>::const_iterator it;
229 for (it = tabs.begin(); it != tabs.end(); ++it) {
230 TabRestoreService::Tab tab = *it;
231 HistoryItem* tab_item = HistoryItemForTab(tab);
233 item->tabs.push_back(tab_item);
234 AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1,
239 // Now that the number of tabs that has been added is known, set the title
240 // of the parent menu item.
241 if (item->tabs.size() == 1) {
242 item->title = l10n_util::GetStringUTF16(
243 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE);
245 item->title =l10n_util::GetStringFUTF16(
246 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE,
247 base::IntToString16(item->tabs.size()));
250 // Sometimes it is possible for there to not be any subitems for a given
251 // window; if that is the case, do not add the entry to the main menu.
252 if ([[submenu itemArray] count] > 2) {
253 // Create the menu item parent.
254 NSMenuItem* parent_item =
255 AddItemToMenu(item, menu, kRecentlyClosed, index++);
256 [parent_item setSubmenu:submenu.get()];
259 } else if (entry->type == TabRestoreService::TAB) {
260 TabRestoreService::Tab* tab =
261 static_cast<TabRestoreService::Tab*>(entry);
262 HistoryItem* item = HistoryItemForTab(*tab);
264 AddItemToMenu(item, menu, kRecentlyClosed, index++);
271 void HistoryMenuBridge::TabRestoreServiceDestroyed(
272 TabRestoreService* service) {
273 // Intentionally left blank. We hold a weak reference to the service.
276 void HistoryMenuBridge::ResetMenu() {
277 NSMenu* menu = HistoryMenu();
278 ClearMenuSection(menu, kVisited);
279 ClearMenuSection(menu, kRecentlyClosed);
282 void HistoryMenuBridge::BuildMenu() {
283 // If the history service is ready, use it. Otherwise, a Notification will
284 // force an update when it's loaded.
285 if (history_service_)
289 HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem(
291 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item);
292 if (it != menu_item_map_.end()) {
298 HistoryService* HistoryMenuBridge::service() {
299 return history_service_;
302 Profile* HistoryMenuBridge::profile() {
306 NSMenu* HistoryMenuBridge::HistoryMenu() {
307 NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU]
312 void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) {
313 for (NSMenuItem* menu_item in [menu itemArray]) {
314 if ([menu_item tag] == tag && [menu_item target] == controller_.get()) {
315 // This is an item that should be removed, so find the corresponding model
317 HistoryItem* item = HistoryItemForMenuItem(menu_item);
319 // Cancel favicon requests that could hold onto stale pointers. Also
320 // remove the item from the mapping.
322 CancelFaviconRequest(item);
323 menu_item_map_.erase(menu_item);
327 // If this menu item has a submenu, recurse.
328 if ([menu_item hasSubmenu]) {
329 ClearMenuSection([menu_item submenu], tag + 1);
332 // Now actually remove the item from the menu.
333 [menu removeItem:menu_item];
338 NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item,
342 NSString* title = base::SysUTF16ToNSString(item->title);
343 std::string url_string = item->url.possibly_invalid_spec();
345 // If we don't have a title, use the URL.
346 if ([title isEqualToString:@""])
347 title = base::SysUTF8ToNSString(url_string);
348 NSString* full_title = title;
349 if ([title length] > kMaximumMenuWidthInChars) {
350 // TODO(rsesek): use app/text_elider.h once it uses base::string16 and can
351 // take out the middle of strings.
352 title = [NSString stringWithFormat:@"%@…%@",
353 [title substringToIndex:kMenuTrimSizeInChars],
354 [title substringFromIndex:([title length] -
355 kMenuTrimSizeInChars)]];
357 item->menu_item.reset(
358 [[NSMenuItem alloc] initWithTitle:title
361 [item->menu_item setTarget:controller_];
362 [item->menu_item setAction:@selector(openHistoryMenuItem:)];
363 [item->menu_item setTag:tag];
364 if (item->icon.get())
365 [item->menu_item setImage:item->icon.get()];
366 else if (!item->tabs.size())
367 [item->menu_item setImage:default_favicon_.get()];
370 NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", full_title,
372 [item->menu_item setToolTip:tooltip];
374 [menu insertItem:item->menu_item.get() atIndex:index];
375 menu_item_map_.insert(std::make_pair(item->menu_item.get(), item));
377 return item->menu_item.get();
380 void HistoryMenuBridge::Init() {
381 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED,
382 content::Source<Profile>(profile_));
383 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URL_VISITED,
384 content::Source<Profile>(profile_));
385 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED,
386 content::Source<Profile>(profile_));
389 void HistoryMenuBridge::CreateMenu() {
390 // If we're currently running CreateMenu(), wait until it finishes.
391 if (create_in_progress_)
393 create_in_progress_ = true;
394 need_recreate_ = false;
396 DCHECK(history_service_);
398 history::QueryOptions options;
399 options.max_count = kVisitedCount;
400 options.SetRecentDayRange(kVisitedScope);
402 history_service_->QueryHistory(
405 &cancelable_request_consumer_,
406 base::Bind(&HistoryMenuBridge::OnVisitedHistoryResults,
407 base::Unretained(this)));
410 void HistoryMenuBridge::OnVisitedHistoryResults(
411 CancelableRequestProvider::Handle handle,
412 history::QueryResults* results) {
413 NSMenu* menu = HistoryMenu();
414 ClearMenuSection(menu, kVisited);
415 NSInteger top_item = [menu indexOfItemWithTag:kVisitedTitle] + 1;
417 size_t count = results->size();
418 for (size_t i = 0; i < count; ++i) {
419 const history::URLResult& result = (*results)[i];
421 HistoryItem* item = new HistoryItem;
422 item->title = result.title();
423 item->url = result.url();
425 // Need to explicitly get the favicon for each row.
426 GetFaviconForHistoryItem(item);
428 // This will add |item| to the |menu_item_map_|, which takes ownership.
429 AddItemToMenu(item, HistoryMenu(), kVisited, top_item + i);
432 // We are already invalid by the time we finished, darn.
436 create_in_progress_ = false;
439 HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab(
440 const TabRestoreService::Tab& entry) {
441 DCHECK(!entry.navigations.empty());
443 const sessions::SerializedNavigationEntry& current_navigation =
444 entry.navigations.at(entry.current_navigation_index);
445 HistoryItem* item = new HistoryItem();
446 item->title = current_navigation.title();
447 item->url = current_navigation.virtual_url();
448 item->session_id = entry.id;
450 // Tab navigations don't come with icons, so we always have to request them.
451 GetFaviconForHistoryItem(item);
456 void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) {
457 FaviconService* service =
458 FaviconServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS);
459 base::CancelableTaskTracker::TaskId task_id = service->GetFaviconImageForURL(
460 FaviconService::FaviconForURLParams(
461 item->url, favicon_base::FAVICON, gfx::kFaviconSize),
463 &HistoryMenuBridge::GotFaviconData, base::Unretained(this), item),
464 &cancelable_task_tracker_);
465 item->icon_task_id = task_id;
466 item->icon_requested = true;
469 void HistoryMenuBridge::GotFaviconData(
471 const favicon_base::FaviconImageResult& image_result) {
472 // Since we're going to do Cocoa-y things, make sure this is the main thread.
473 DCHECK([NSThread isMainThread]);
476 item->icon_requested = false;
477 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;
479 NSImage* image = image_result.image.AsNSImage();
481 item->icon.reset([image retain]);
482 [item->menu_item setImage:item->icon.get()];
486 void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) {
488 if (item->icon_requested) {
489 cancelable_task_tracker_.TryCancel(item->icon_task_id);
490 item->icon_requested = false;
491 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId;