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#include "chrome/browser/ui/cocoa/history_menu_bridge.h" 6 7#include "base/bind.h" 8#include "base/strings/string_number_conversions.h" 9#include "base/strings/string_util.h" 10#include "base/strings/sys_string_conversions.h" 11#include "base/strings/utf_string_conversions.h" 12#include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU 13#include "chrome/browser/chrome_notification_types.h" 14#include "chrome/browser/favicon/favicon_service_factory.h" 15#include "chrome/browser/history/history_service_factory.h" 16#include "chrome/browser/profiles/profile.h" 17#include "chrome/browser/sessions/tab_restore_service_factory.h" 18#import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h" 19#include "chrome/grit/generated_resources.h" 20#include "content/public/browser/notification_registrar.h" 21#include "content/public/browser/notification_source.h" 22#include "grit/theme_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#include "ui/gfx/text_elider.h" 27#include "ui/resources/grit/ui_resources.h" 28 29namespace { 30 31// Maximum number of pixels to use for a menu item title. 32const float kTitlePixelWidth = 400; 33 34// Number of days to consider when getting the number of visited items. 35const int kVisitedScope = 90; 36 37// The number of visisted results to get. 38const int kVisitedCount = 15; 39 40// The number of recently closed items to get. 41const unsigned int kRecentlyClosedCount = 10; 42 43} // namespace 44 45HistoryMenuBridge::HistoryItem::HistoryItem() 46 : icon_requested(false), 47 icon_task_id(base::CancelableTaskTracker::kBadTaskId), 48 menu_item(nil), 49 session_id(0) {} 50 51HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy) 52 : title(copy.title), 53 url(copy.url), 54 icon_requested(false), 55 icon_task_id(base::CancelableTaskTracker::kBadTaskId), 56 menu_item(nil), 57 session_id(copy.session_id) {} 58 59HistoryMenuBridge::HistoryItem::~HistoryItem() { 60} 61 62HistoryMenuBridge::HistoryMenuBridge(Profile* profile) 63 : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]), 64 profile_(profile), 65 history_service_(NULL), 66 tab_restore_service_(NULL), 67 create_in_progress_(false), 68 need_recreate_(false) { 69 // If we don't have a profile, do not bother initializing our data sources. 70 // This shouldn't happen except in unit tests. 71 if (profile_) { 72 // Check to see if the history service is ready. Because it loads async, it 73 // may not be ready when the Bridge is created. If this happens, register 74 // for a notification that tells us the HistoryService is ready. 75 HistoryService* hs = HistoryServiceFactory::GetForProfile( 76 profile_, Profile::EXPLICIT_ACCESS); 77 if (hs != NULL && hs->BackendLoaded()) { 78 history_service_ = hs; 79 Init(); 80 } 81 82 tab_restore_service_ = TabRestoreServiceFactory::GetForProfile(profile_); 83 if (tab_restore_service_) { 84 tab_restore_service_->AddObserver(this); 85 // If the tab entries are already loaded, invoke the observer method to 86 // build the "Recently Closed" section. Otherwise it will be when the 87 // backend loads. 88 if (!tab_restore_service_->IsLoaded()) 89 tab_restore_service_->LoadTabsFromLastSession(); 90 else 91 TabRestoreServiceChanged(tab_restore_service_); 92 } 93 } 94 95 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 96 default_favicon_.reset( 97 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); 98 99 // Set the static icons in the menu. 100 NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY]; 101 [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON).ToNSImage()]; 102 103 // The service is not ready for use yet, so become notified when it does. 104 if (!history_service_) { 105 registrar_.Add( 106 this, chrome::NOTIFICATION_HISTORY_LOADED, 107 content::Source<Profile>(profile_)); 108 } 109} 110 111// Note that all requests sent to either the history service or the favicon 112// service will be automatically cancelled by their respective Consumers, so 113// task cancellation is not done manually here in the dtor. 114HistoryMenuBridge::~HistoryMenuBridge() { 115 // Unregister ourselves as observers and notifications. 116 DCHECK(profile_); 117 if (history_service_) { 118 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, 119 content::Source<Profile>(profile_)); 120 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URL_VISITED, 121 content::Source<Profile>(profile_)); 122 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, 123 content::Source<Profile>(profile_)); 124 } else { 125 registrar_.Remove(this, chrome::NOTIFICATION_HISTORY_LOADED, 126 content::Source<Profile>(profile_)); 127 } 128 129 if (tab_restore_service_) 130 tab_restore_service_->RemoveObserver(this); 131 132 // Since the map owns the HistoryItems, delete anything that still exists. 133 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin(); 134 while (it != menu_item_map_.end()) { 135 HistoryItem* item = it->second; 136 menu_item_map_.erase(it++); 137 delete item; 138 } 139} 140 141void HistoryMenuBridge::Observe(int type, 142 const content::NotificationSource& source, 143 const content::NotificationDetails& details) { 144 // A history service is now ready. Check to see if it's the one for the main 145 // profile. If so, perform final initialization. 146 if (type == chrome::NOTIFICATION_HISTORY_LOADED) { 147 HistoryService* hs = HistoryServiceFactory::GetForProfile( 148 profile_, Profile::EXPLICIT_ACCESS); 149 if (hs != NULL && hs->BackendLoaded()) { 150 history_service_ = hs; 151 Init(); 152 153 // Found our HistoryService, so stop listening for this notification. 154 registrar_.Remove(this, 155 chrome::NOTIFICATION_HISTORY_LOADED, 156 content::Source<Profile>(profile_)); 157 } 158 } 159 160 // All other notification types that we observe indicate that the history has 161 // changed and we need to rebuild. 162 need_recreate_ = true; 163 CreateMenu(); 164} 165 166void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) { 167 const TabRestoreService::Entries& entries = service->entries(); 168 169 // Clear the history menu before rebuilding. 170 NSMenu* menu = HistoryMenu(); 171 ClearMenuSection(menu, kRecentlyClosed); 172 173 // Index for the next menu item. 174 NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1; 175 NSUInteger added_count = 0; 176 177 for (TabRestoreService::Entries::const_iterator it = entries.begin(); 178 it != entries.end() && added_count < kRecentlyClosedCount; ++it) { 179 TabRestoreService::Entry* entry = *it; 180 181 // If this is a window, create a submenu for all of its tabs. 182 if (entry->type == TabRestoreService::WINDOW) { 183 TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry; 184 std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs; 185 if (!tabs.size()) 186 continue; 187 188 // Create the item for the parent/window. Do not set the title yet because 189 // the actual number of items that are in the menu will not be known until 190 // things like the NTP are filtered out, which is done when the tab items 191 // are actually created. 192 HistoryItem* item = new HistoryItem(); 193 item->session_id = entry_win->id; 194 195 // Create the submenu. 196 base::scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]); 197 198 // Create standard items within the window submenu. 199 NSString* restore_title = l10n_util::GetNSString( 200 IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC); 201 base::scoped_nsobject<NSMenuItem> restore_item( 202 [[NSMenuItem alloc] initWithTitle:restore_title 203 action:@selector(openHistoryMenuItem:) 204 keyEquivalent:@""]); 205 [restore_item setTarget:controller_.get()]; 206 // Duplicate the HistoryItem otherwise the different NSMenuItems will 207 // point to the same HistoryItem, which would then be double-freed when 208 // removing the items from the map or in the dtor. 209 HistoryItem* dup_item = new HistoryItem(*item); 210 menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item)); 211 [submenu addItem:restore_item.get()]; 212 [submenu addItem:[NSMenuItem separatorItem]]; 213 214 // Loop over the window's tabs and add them to the submenu. 215 NSInteger subindex = [[submenu itemArray] count]; 216 std::vector<TabRestoreService::Tab>::const_iterator it; 217 for (it = tabs.begin(); it != tabs.end(); ++it) { 218 TabRestoreService::Tab tab = *it; 219 HistoryItem* tab_item = HistoryItemForTab(tab); 220 if (tab_item) { 221 item->tabs.push_back(tab_item); 222 AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1, 223 subindex++); 224 } 225 } 226 227 // Now that the number of tabs that has been added is known, set the title 228 // of the parent menu item. 229 if (item->tabs.size() == 1) { 230 item->title = l10n_util::GetStringUTF16( 231 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE); 232 } else { 233 item->title =l10n_util::GetStringFUTF16( 234 IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE, 235 base::IntToString16(item->tabs.size())); 236 } 237 238 // Sometimes it is possible for there to not be any subitems for a given 239 // window; if that is the case, do not add the entry to the main menu. 240 if ([[submenu itemArray] count] > 2) { 241 // Create the menu item parent. 242 NSMenuItem* parent_item = 243 AddItemToMenu(item, menu, kRecentlyClosed, index++); 244 [parent_item setSubmenu:submenu.get()]; 245 ++added_count; 246 } 247 } else if (entry->type == TabRestoreService::TAB) { 248 TabRestoreService::Tab* tab = 249 static_cast<TabRestoreService::Tab*>(entry); 250 HistoryItem* item = HistoryItemForTab(*tab); 251 if (item) { 252 AddItemToMenu(item, menu, kRecentlyClosed, index++); 253 ++added_count; 254 } 255 } 256 } 257} 258 259void HistoryMenuBridge::TabRestoreServiceDestroyed( 260 TabRestoreService* service) { 261 // Intentionally left blank. We hold a weak reference to the service. 262} 263 264void HistoryMenuBridge::ResetMenu() { 265 NSMenu* menu = HistoryMenu(); 266 ClearMenuSection(menu, kVisited); 267 ClearMenuSection(menu, kRecentlyClosed); 268} 269 270void HistoryMenuBridge::BuildMenu() { 271 // If the history service is ready, use it. Otherwise, a Notification will 272 // force an update when it's loaded. 273 if (history_service_) 274 CreateMenu(); 275} 276 277HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem( 278 NSMenuItem* item) { 279 std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item); 280 if (it != menu_item_map_.end()) { 281 return it->second; 282 } 283 return NULL; 284} 285 286HistoryService* HistoryMenuBridge::service() { 287 return history_service_; 288} 289 290Profile* HistoryMenuBridge::profile() { 291 return profile_; 292} 293 294NSMenu* HistoryMenuBridge::HistoryMenu() { 295 NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU] 296 submenu]; 297 return history_menu; 298} 299 300void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) { 301 for (NSMenuItem* menu_item in [menu itemArray]) { 302 if ([menu_item tag] == tag && [menu_item target] == controller_.get()) { 303 // This is an item that should be removed, so find the corresponding model 304 // item. 305 HistoryItem* item = HistoryItemForMenuItem(menu_item); 306 307 // Cancel favicon requests that could hold onto stale pointers. Also 308 // remove the item from the mapping. 309 if (item) { 310 CancelFaviconRequest(item); 311 menu_item_map_.erase(menu_item); 312 delete item; 313 } 314 315 // If this menu item has a submenu, recurse. 316 if ([menu_item hasSubmenu]) { 317 ClearMenuSection([menu_item submenu], tag + 1); 318 } 319 320 // Now actually remove the item from the menu. 321 [menu removeItem:menu_item]; 322 } 323 } 324} 325 326NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item, 327 NSMenu* menu, 328 NSInteger tag, 329 NSInteger index) { 330 // Elide the title of the history item, or use the URL if there is none. 331 std::string url = item->url.possibly_invalid_spec(); 332 base::string16 full_title = item->title; 333 base::string16 title = 334 gfx::ElideText(full_title.empty() ? base::UTF8ToUTF16(url) : full_title, 335 gfx::FontList(gfx::Font([NSFont menuFontOfSize:0])), 336 kTitlePixelWidth, 337 gfx::ELIDE_MIDDLE); 338 339 item->menu_item.reset( 340 [[NSMenuItem alloc] initWithTitle:base::SysUTF16ToNSString(title) 341 action:nil 342 keyEquivalent:@""]); 343 [item->menu_item setTarget:controller_]; 344 [item->menu_item setAction:@selector(openHistoryMenuItem:)]; 345 [item->menu_item setTag:tag]; 346 if (item->icon.get()) 347 [item->menu_item setImage:item->icon.get()]; 348 else if (!item->tabs.size()) 349 [item->menu_item setImage:default_favicon_.get()]; 350 351 // Add a tooltip. 352 NSString* tooltip = [NSString stringWithFormat:@"%@\n%@", 353 base::SysUTF16ToNSString(full_title), base::SysUTF8ToNSString(url)]; 354 [item->menu_item setToolTip:tooltip]; 355 356 [menu insertItem:item->menu_item.get() atIndex:index]; 357 menu_item_map_.insert(std::make_pair(item->menu_item.get(), item)); 358 359 return item->menu_item.get(); 360} 361 362void HistoryMenuBridge::Init() { 363 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_MODIFIED, 364 content::Source<Profile>(profile_)); 365 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URL_VISITED, 366 content::Source<Profile>(profile_)); 367 registrar_.Add(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED, 368 content::Source<Profile>(profile_)); 369} 370 371void HistoryMenuBridge::CreateMenu() { 372 // If we're currently running CreateMenu(), wait until it finishes. 373 if (create_in_progress_) 374 return; 375 create_in_progress_ = true; 376 need_recreate_ = false; 377 378 DCHECK(history_service_); 379 380 history::QueryOptions options; 381 options.max_count = kVisitedCount; 382 options.SetRecentDayRange(kVisitedScope); 383 384 history_service_->QueryHistory( 385 base::string16(), 386 options, 387 base::Bind(&HistoryMenuBridge::OnVisitedHistoryResults, 388 base::Unretained(this)), 389 &cancelable_task_tracker_); 390} 391 392void HistoryMenuBridge::OnVisitedHistoryResults( 393 history::QueryResults* results) { 394 NSMenu* menu = HistoryMenu(); 395 ClearMenuSection(menu, kVisited); 396 NSInteger top_item = [menu indexOfItemWithTag:kVisitedTitle] + 1; 397 398 size_t count = results->size(); 399 for (size_t i = 0; i < count; ++i) { 400 const history::URLResult& result = (*results)[i]; 401 402 HistoryItem* item = new HistoryItem; 403 item->title = result.title(); 404 item->url = result.url(); 405 406 // Need to explicitly get the favicon for each row. 407 GetFaviconForHistoryItem(item); 408 409 // This will add |item| to the |menu_item_map_|, which takes ownership. 410 AddItemToMenu(item, HistoryMenu(), kVisited, top_item + i); 411 } 412 413 // We are already invalid by the time we finished, darn. 414 if (need_recreate_) 415 CreateMenu(); 416 417 create_in_progress_ = false; 418} 419 420HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab( 421 const TabRestoreService::Tab& entry) { 422 DCHECK(!entry.navigations.empty()); 423 424 const sessions::SerializedNavigationEntry& current_navigation = 425 entry.navigations.at(entry.current_navigation_index); 426 HistoryItem* item = new HistoryItem(); 427 item->title = current_navigation.title(); 428 item->url = current_navigation.virtual_url(); 429 item->session_id = entry.id; 430 431 // Tab navigations don't come with icons, so we always have to request them. 432 GetFaviconForHistoryItem(item); 433 434 return item; 435} 436 437void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) { 438 FaviconService* service = 439 FaviconServiceFactory::GetForProfile(profile_, Profile::EXPLICIT_ACCESS); 440 base::CancelableTaskTracker::TaskId task_id = 441 service->GetFaviconImageForPageURL( 442 item->url, 443 base::Bind( 444 &HistoryMenuBridge::GotFaviconData, base::Unretained(this), item), 445 &cancelable_task_tracker_); 446 item->icon_task_id = task_id; 447 item->icon_requested = true; 448} 449 450void HistoryMenuBridge::GotFaviconData( 451 HistoryItem* item, 452 const favicon_base::FaviconImageResult& image_result) { 453 // Since we're going to do Cocoa-y things, make sure this is the main thread. 454 DCHECK([NSThread isMainThread]); 455 456 DCHECK(item); 457 item->icon_requested = false; 458 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId; 459 460 NSImage* image = image_result.image.AsNSImage(); 461 if (image) { 462 item->icon.reset([image retain]); 463 [item->menu_item setImage:item->icon.get()]; 464 } 465} 466 467void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) { 468 DCHECK(item); 469 if (item->icon_requested) { 470 cancelable_task_tracker_.TryCancel(item->icon_task_id); 471 item->icon_requested = false; 472 item->icon_task_id = base::CancelableTaskTracker::kBadTaskId; 473 } 474} 475