// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/cocoa/history_menu_bridge.h" #include "app/mac/nsimage_cache.h" #include "base/callback.h" #include "base/stl_util-inl.h" #include "base/string_number_conversions.h" #include "base/string_util.h" #include "base/sys_string_conversions.h" #include "chrome/app/chrome_command_ids.h" // IDC_HISTORY_MENU #import "chrome/browser/app_controller_mac.h" #include "chrome/browser/history/page_usage_data.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/sessions/session_types.h" #import "chrome/browser/ui/cocoa/history_menu_cocoa_controller.h" #include "chrome/common/url_constants.h" #include "content/common/notification_registrar.h" #include "content/common/notification_service.h" #include "grit/app_resources.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "skia/ext/skia_utils_mac.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/codec/png_codec.h" #include "ui/gfx/image.h" namespace { // Menus more than this many chars long will get trimmed. const NSUInteger kMaximumMenuWidthInChars = 50; // When trimming, use this many chars from each side. const NSUInteger kMenuTrimSizeInChars = 25; // Number of days to consider when getting the number of most visited items. const int kMostVisitedScope = 90; // The number of most visisted results to get. const int kMostVisitedCount = 9; // The number of recently closed items to get. const unsigned int kRecentlyClosedCount = 10; } // namespace HistoryMenuBridge::HistoryItem::HistoryItem() : icon_requested(false), menu_item(nil), session_id(0) { } HistoryMenuBridge::HistoryItem::HistoryItem(const HistoryItem& copy) : title(copy.title), url(copy.url), icon_requested(false), menu_item(nil), session_id(copy.session_id) { } HistoryMenuBridge::HistoryItem::~HistoryItem() { } HistoryMenuBridge::HistoryMenuBridge(Profile* profile) : controller_([[HistoryMenuCocoaController alloc] initWithBridge:this]), profile_(profile), history_service_(NULL), tab_restore_service_(NULL), create_in_progress_(false), need_recreate_(false) { // If we don't have a profile, do not bother initializing our data sources. // This shouldn't happen except in unit tests. if (profile_) { // Check to see if the history service is ready. Because it loads async, it // may not be ready when the Bridge is created. If this happens, register // for a notification that tells us the HistoryService is ready. HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); if (hs != NULL && hs->BackendLoaded()) { history_service_ = hs; Init(); } tab_restore_service_ = profile_->GetTabRestoreService(); if (tab_restore_service_) { tab_restore_service_->AddObserver(this); tab_restore_service_->LoadTabsFromLastSession(); } } ResourceBundle& rb = ResourceBundle::GetSharedInstance(); default_favicon_.reset([app::mac::GetCachedImageWithName(@"nav.pdf") retain]); // Set the static icons in the menu. NSMenuItem* item = [HistoryMenu() itemWithTag:IDC_SHOW_HISTORY]; [item setImage:rb.GetNativeImageNamed(IDR_HISTORY_FAVICON)]; // The service is not ready for use yet, so become notified when it does. if (!history_service_) { registrar_.Add(this, NotificationType::HISTORY_LOADED, NotificationService::AllSources()); } } // Note that all requests sent to either the history service or the favicon // service will be automatically cancelled by their respective Consumers, so // task cancellation is not done manually here in the dtor. HistoryMenuBridge::~HistoryMenuBridge() { // Unregister ourselves as observers and notifications. const NotificationSource& src = NotificationService::AllSources(); if (history_service_) { registrar_.Remove(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, src); registrar_.Remove(this, NotificationType::HISTORY_URL_VISITED, src); registrar_.Remove(this, NotificationType::HISTORY_URLS_DELETED, src); } else { registrar_.Remove(this, NotificationType::HISTORY_LOADED, src); } if (tab_restore_service_) tab_restore_service_->RemoveObserver(this); // Since the map owns the HistoryItems, delete anything that still exists. std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.begin(); while (it != menu_item_map_.end()) { HistoryItem* item = it->second; menu_item_map_.erase(it++); delete item; } } void HistoryMenuBridge::Observe(NotificationType type, const NotificationSource& source, const NotificationDetails& details) { // A history service is now ready. Check to see if it's the one for the main // profile. If so, perform final initialization. if (type == NotificationType::HISTORY_LOADED) { HistoryService* hs = profile_->GetHistoryService(Profile::EXPLICIT_ACCESS); if (hs != NULL && hs->BackendLoaded()) { history_service_ = hs; Init(); // Found our HistoryService, so stop listening for this notification. registrar_.Remove(this, NotificationType::HISTORY_LOADED, NotificationService::AllSources()); } } // All other notification types that we observe indicate that the history has // changed and we need to rebuild. need_recreate_ = true; CreateMenu(); } void HistoryMenuBridge::TabRestoreServiceChanged(TabRestoreService* service) { const TabRestoreService::Entries& entries = service->entries(); // Clear the history menu before rebuilding. NSMenu* menu = HistoryMenu(); ClearMenuSection(menu, kRecentlyClosed); // Index for the next menu item. NSInteger index = [menu indexOfItemWithTag:kRecentlyClosedTitle] + 1; NSUInteger added_count = 0; for (TabRestoreService::Entries::const_iterator it = entries.begin(); it != entries.end() && added_count < kRecentlyClosedCount; ++it) { TabRestoreService::Entry* entry = *it; // If this is a window, create a submenu for all of its tabs. if (entry->type == TabRestoreService::WINDOW) { TabRestoreService::Window* entry_win = (TabRestoreService::Window*)entry; std::vector<TabRestoreService::Tab>& tabs = entry_win->tabs; if (!tabs.size()) continue; // Create the item for the parent/window. Do not set the title yet because // the actual number of items that are in the menu will not be known until // things like the NTP are filtered out, which is done when the tab items // are actually created. HistoryItem* item = new HistoryItem(); item->session_id = entry_win->id; // Create the submenu. scoped_nsobject<NSMenu> submenu([[NSMenu alloc] init]); // Create standard items within the window submenu. NSString* restore_title = l10n_util::GetNSString( IDS_HISTORY_CLOSED_RESTORE_WINDOW_MAC); scoped_nsobject<NSMenuItem> restore_item( [[NSMenuItem alloc] initWithTitle:restore_title action:@selector(openHistoryMenuItem:) keyEquivalent:@""]); [restore_item setTarget:controller_.get()]; // Duplicate the HistoryItem otherwise the different NSMenuItems will // point to the same HistoryItem, which would then be double-freed when // removing the items from the map or in the dtor. HistoryItem* dup_item = new HistoryItem(*item); menu_item_map_.insert(std::make_pair(restore_item.get(), dup_item)); [submenu addItem:restore_item.get()]; [submenu addItem:[NSMenuItem separatorItem]]; // Loop over the window's tabs and add them to the submenu. NSInteger subindex = [[submenu itemArray] count]; std::vector<TabRestoreService::Tab>::const_iterator it; for (it = tabs.begin(); it != tabs.end(); ++it) { TabRestoreService::Tab tab = *it; HistoryItem* tab_item = HistoryItemForTab(tab); if (tab_item) { item->tabs.push_back(tab_item); AddItemToMenu(tab_item, submenu.get(), kRecentlyClosed + 1, subindex++); } } // Now that the number of tabs that has been added is known, set the title // of the parent menu item. if (item->tabs.size() == 1) { item->title = l10n_util::GetStringUTF16( IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_SINGLE); } else { item->title =l10n_util::GetStringFUTF16( IDS_NEW_TAB_RECENTLY_CLOSED_WINDOW_MULTIPLE, base::IntToString16(item->tabs.size())); } // Sometimes it is possible for there to not be any subitems for a given // window; if that is the case, do not add the entry to the main menu. if ([[submenu itemArray] count] > 2) { // Create the menu item parent. NSMenuItem* parent_item = AddItemToMenu(item, menu, kRecentlyClosed, index++); [parent_item setSubmenu:submenu.get()]; ++added_count; } } else if (entry->type == TabRestoreService::TAB) { TabRestoreService::Tab* tab = static_cast<TabRestoreService::Tab*>(entry); HistoryItem* item = HistoryItemForTab(*tab); if (item) { AddItemToMenu(item, menu, kRecentlyClosed, index++); ++added_count; } } } } void HistoryMenuBridge::TabRestoreServiceDestroyed( TabRestoreService* service) { // Intentionally left blank. We hold a weak reference to the service. } HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForMenuItem( NSMenuItem* item) { std::map<NSMenuItem*, HistoryItem*>::iterator it = menu_item_map_.find(item); if (it != menu_item_map_.end()) { return it->second; } return NULL; } HistoryService* HistoryMenuBridge::service() { return history_service_; } Profile* HistoryMenuBridge::profile() { return profile_; } NSMenu* HistoryMenuBridge::HistoryMenu() { NSMenu* history_menu = [[[NSApp mainMenu] itemWithTag:IDC_HISTORY_MENU] submenu]; return history_menu; } void HistoryMenuBridge::ClearMenuSection(NSMenu* menu, NSInteger tag) { for (NSMenuItem* menu_item in [menu itemArray]) { if ([menu_item tag] == tag && [menu_item target] == controller_.get()) { // This is an item that should be removed, so find the corresponding model // item. HistoryItem* item = HistoryItemForMenuItem(menu_item); // Cancel favicon requests that could hold onto stale pointers. Also // remove the item from the mapping. if (item) { CancelFaviconRequest(item); menu_item_map_.erase(menu_item); delete item; } // If this menu item has a submenu, recurse. if ([menu_item hasSubmenu]) { ClearMenuSection([menu_item submenu], tag + 1); } // Now actually remove the item from the menu. [menu removeItem:menu_item]; } } } NSMenuItem* HistoryMenuBridge::AddItemToMenu(HistoryItem* item, NSMenu* menu, NSInteger tag, NSInteger index) { NSString* title = base::SysUTF16ToNSString(item->title); std::string url_string = item->url.possibly_invalid_spec(); // If we don't have a title, use the URL. if ([title isEqualToString:@""]) title = base::SysUTF8ToNSString(url_string); NSString* full_title = title; if ([title length] > kMaximumMenuWidthInChars) { // TODO(rsesek): use app/text_elider.h once it uses string16 and can // take out the middle of strings. title = [NSString stringWithFormat:@"%@…%@", [title substringToIndex:kMenuTrimSizeInChars], [title substringFromIndex:([title length] - kMenuTrimSizeInChars)]]; } item->menu_item.reset( [[NSMenuItem alloc] initWithTitle:title action:nil keyEquivalent:@""]); [item->menu_item setTarget:controller_]; [item->menu_item setAction:@selector(openHistoryMenuItem:)]; [item->menu_item setTag:tag]; if (item->icon.get()) [item->menu_item setImage:item->icon.get()]; else if (!item->tabs.size()) [item->menu_item setImage:default_favicon_.get()]; // Add a tooltip. NSString* tooltip = [NSString stringWithFormat:@"%@\n%s", full_title, url_string.c_str()]; [item->menu_item setToolTip:tooltip]; [menu insertItem:item->menu_item.get() atIndex:index]; menu_item_map_.insert(std::make_pair(item->menu_item.get(), item)); return item->menu_item.get(); } void HistoryMenuBridge::Init() { const NotificationSource& source = NotificationService::AllSources(); registrar_.Add(this, NotificationType::HISTORY_TYPED_URLS_MODIFIED, source); registrar_.Add(this, NotificationType::HISTORY_URL_VISITED, source); registrar_.Add(this, NotificationType::HISTORY_URLS_DELETED, source); } void HistoryMenuBridge::CreateMenu() { // If we're currently running CreateMenu(), wait until it finishes. if (create_in_progress_) return; create_in_progress_ = true; need_recreate_ = false; history_service_->QuerySegmentUsageSince( &cancelable_request_consumer_, base::Time::Now() - base::TimeDelta::FromDays(kMostVisitedScope), kMostVisitedCount, NewCallback(this, &HistoryMenuBridge::OnVisitedHistoryResults)); } void HistoryMenuBridge::OnVisitedHistoryResults( CancelableRequestProvider::Handle handle, std::vector<PageUsageData*>* results) { NSMenu* menu = HistoryMenu(); ClearMenuSection(menu, kMostVisited); NSInteger top_item = [menu indexOfItemWithTag:kMostVisitedTitle] + 1; size_t count = results->size(); for (size_t i = 0; i < count; ++i) { PageUsageData* history_item = (*results)[i]; HistoryItem* item = new HistoryItem(); item->title = history_item->GetTitle(); item->url = history_item->GetURL(); if (history_item->HasFavicon()) { const SkBitmap* icon = history_item->GetFavicon(); item->icon.reset([gfx::SkBitmapToNSImage(*icon) retain]); } else { GetFaviconForHistoryItem(item); } // This will add |item| to the |menu_item_map_|, which takes ownership. AddItemToMenu(item, HistoryMenu(), kMostVisited, top_item + i); } // We are already invalid by the time we finished, darn. if (need_recreate_) CreateMenu(); create_in_progress_ = false; } HistoryMenuBridge::HistoryItem* HistoryMenuBridge::HistoryItemForTab( const TabRestoreService::Tab& entry) { if (entry.navigations.empty()) return NULL; const TabNavigation& current_navigation = entry.navigations.at(entry.current_navigation_index); if (current_navigation.virtual_url() == GURL(chrome::kChromeUINewTabURL)) return NULL; HistoryItem* item = new HistoryItem(); item->title = current_navigation.title(); item->url = current_navigation.virtual_url(); item->session_id = entry.id; // Tab navigations don't come with icons, so we always have to request them. GetFaviconForHistoryItem(item); return item; } void HistoryMenuBridge::GetFaviconForHistoryItem(HistoryItem* item) { FaviconService* service = profile_->GetFaviconService(Profile::EXPLICIT_ACCESS); FaviconService::Handle handle = service->GetFaviconForURL(item->url, history::FAVICON, &favicon_consumer_, NewCallback(this, &HistoryMenuBridge::GotFaviconData)); favicon_consumer_.SetClientData(service, handle, item); item->icon_handle = handle; item->icon_requested = true; } void HistoryMenuBridge::GotFaviconData(FaviconService::Handle handle, history::FaviconData favicon) { // Since we're going to do Cocoa-y things, make sure this is the main thread. DCHECK([NSThread isMainThread]); HistoryItem* item = favicon_consumer_.GetClientData( profile_->GetFaviconService(Profile::EXPLICIT_ACCESS), handle); DCHECK(item); item->icon_requested = false; item->icon_handle = NULL; // Convert the raw data to Skia and then to a NSImage. // TODO(rsesek): Is there an easier way to do this? SkBitmap icon; if (favicon.is_valid() && gfx::PNGCodec::Decode(favicon.image_data->front(), favicon.image_data->size(), &icon)) { NSImage* image = gfx::SkBitmapToNSImage(icon); if (image) { // The conversion was successful. item->icon.reset([image retain]); [item->menu_item setImage:item->icon.get()]; } } } void HistoryMenuBridge::CancelFaviconRequest(HistoryItem* item) { DCHECK(item); if (item->icon_requested) { FaviconService* service = profile_->GetFaviconService(Profile::EXPLICIT_ACCESS); service->CancelRequest(item->icon_handle); item->icon_requested = false; item->icon_handle = NULL; } }