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