Home | History | Annotate | Download | only in cocoa
      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