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