Home | History | Annotate | Download | only in toolbar
      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 "build/build_config.h"
      6 
      7 #include "chrome/browser/ui/toolbar/back_forward_menu_model.h"
      8 
      9 #include "base/string_number_conversions.h"
     10 #include "chrome/browser/metrics/user_metrics.h"
     11 #include "chrome/browser/prefs/pref_service.h"
     12 #include "chrome/browser/profiles/profile.h"
     13 #include "chrome/browser/ui/browser.h"
     14 #include "chrome/common/pref_names.h"
     15 #include "chrome/common/url_constants.h"
     16 #include "content/browser/tab_contents/navigation_controller.h"
     17 #include "content/browser/tab_contents/navigation_entry.h"
     18 #include "content/browser/tab_contents/tab_contents.h"
     19 #include "grit/generated_resources.h"
     20 #include "grit/theme_resources.h"
     21 #include "net/base/registry_controlled_domain.h"
     22 #include "ui/base/l10n/l10n_util.h"
     23 #include "ui/base/resource/resource_bundle.h"
     24 #include "ui/base/text/text_elider.h"
     25 #include "ui/gfx/codec/png_codec.h"
     26 
     27 const int BackForwardMenuModel::kMaxHistoryItems = 12;
     28 const int BackForwardMenuModel::kMaxChapterStops = 5;
     29 static const int kMaxWidth = 700;
     30 
     31 BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
     32                                            ModelType model_type)
     33     : browser_(browser),
     34       test_tab_contents_(NULL),
     35       model_type_(model_type),
     36       menu_model_delegate_(NULL) {
     37 }
     38 
     39 BackForwardMenuModel::~BackForwardMenuModel() {
     40 }
     41 
     42 bool BackForwardMenuModel::HasIcons() const {
     43   return true;
     44 }
     45 
     46 int BackForwardMenuModel::GetItemCount() const {
     47   int items = GetHistoryItemCount();
     48 
     49   if (items > 0) {
     50     int chapter_stops = 0;
     51 
     52     // Next, we count ChapterStops, if any.
     53     if (items == kMaxHistoryItems)
     54       chapter_stops = GetChapterStopCount(items);
     55 
     56     if (chapter_stops)
     57       items += chapter_stops + 1;  // Chapter stops also need a separator.
     58 
     59     // If the menu is not empty, add two positions in the end
     60     // for a separator and a "Show Full History" item.
     61     items += 2;
     62   }
     63 
     64   return items;
     65 }
     66 
     67 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
     68   return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
     69 }
     70 
     71 int BackForwardMenuModel::GetCommandIdAt(int index) const {
     72   return index;
     73 }
     74 
     75 string16 BackForwardMenuModel::GetLabelAt(int index) const {
     76   // Return label "Show Full History" for the last item of the menu.
     77   if (index == GetItemCount() - 1)
     78     return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
     79 
     80   // Return an empty string for a separator.
     81   if (IsSeparator(index))
     82     return string16();
     83 
     84   // Return the entry title, escaping any '&' characters and eliding it if it's
     85   // super long.
     86   NavigationEntry* entry = GetNavigationEntry(index);
     87   string16 menu_text(entry->GetTitleForDisplay(
     88       GetTabContents()->profile()->GetPrefs()->
     89           GetString(prefs::kAcceptLanguages)));
     90   menu_text = ui::ElideText(menu_text, gfx::Font(), kMaxWidth, false);
     91 
     92 #if !defined(OS_MACOSX)
     93   for (size_t i = menu_text.find('&'); i != string16::npos;
     94        i = menu_text.find('&', i + 2)) {
     95     menu_text.insert(i, 1, '&');
     96   }
     97 #endif
     98 
     99   return menu_text;
    100 }
    101 
    102 bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
    103   // This object is only used for a single showing of a menu.
    104   return false;
    105 }
    106 
    107 bool BackForwardMenuModel::GetAcceleratorAt(
    108     int index,
    109     ui::Accelerator* accelerator) const {
    110   return false;
    111 }
    112 
    113 bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
    114   return false;
    115 }
    116 
    117 int BackForwardMenuModel::GetGroupIdAt(int index) const {
    118   return false;
    119 }
    120 
    121 bool BackForwardMenuModel::GetIconAt(int index, SkBitmap* icon) {
    122   if (!ItemHasIcon(index))
    123     return false;
    124 
    125   if (index == GetItemCount() - 1) {
    126     *icon = *ResourceBundle::GetSharedInstance().GetBitmapNamed(
    127         IDR_HISTORY_FAVICON);
    128   } else {
    129     NavigationEntry* entry = GetNavigationEntry(index);
    130     *icon = entry->favicon().bitmap();
    131     if (!entry->favicon().is_valid() && menu_model_delegate()) {
    132       FetchFavicon(entry);
    133     }
    134   }
    135 
    136   return true;
    137 }
    138 
    139 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
    140     int index) const {
    141   return NULL;
    142 }
    143 
    144 bool BackForwardMenuModel::IsEnabledAt(int index) const {
    145   return index < GetItemCount() && !IsSeparator(index);
    146 }
    147 
    148 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
    149   return NULL;
    150 }
    151 
    152 void BackForwardMenuModel::HighlightChangedTo(int index) {
    153 }
    154 
    155 void BackForwardMenuModel::ActivatedAt(int index) {
    156   ActivatedAtWithDisposition(index, CURRENT_TAB);
    157 }
    158 
    159 void BackForwardMenuModel::ActivatedAtWithDisposition(
    160       int index, int disposition) {
    161   Profile* profile = browser_->profile();
    162 
    163   DCHECK(!IsSeparator(index));
    164 
    165   // Execute the command for the last item: "Show Full History".
    166   if (index == GetItemCount() - 1) {
    167     UserMetrics::RecordComputedAction(BuildActionName("ShowFullHistory", -1),
    168                                       profile);
    169     browser_->ShowSingletonTab(GURL(chrome::kChromeUIHistoryURL));
    170     return;
    171   }
    172 
    173   // Log whether it was a history or chapter click.
    174   if (index < GetHistoryItemCount()) {
    175     UserMetrics::RecordComputedAction(
    176         BuildActionName("HistoryClick", index), profile);
    177   } else {
    178     UserMetrics::RecordComputedAction(
    179         BuildActionName("ChapterClick", index - GetHistoryItemCount() - 1),
    180         profile);
    181   }
    182 
    183   int controller_index = MenuIndexToNavEntryIndex(index);
    184   if (!browser_->NavigateToIndexWithDisposition(
    185           controller_index, static_cast<WindowOpenDisposition>(disposition))) {
    186     NOTREACHED();
    187   }
    188 }
    189 
    190 void BackForwardMenuModel::MenuWillShow() {
    191   UserMetrics::RecordComputedAction(BuildActionName("Popup", -1),
    192                                     browser_->profile());
    193   requested_favicons_.clear();
    194   load_consumer_.CancelAllRequests();
    195 }
    196 
    197 bool BackForwardMenuModel::IsSeparator(int index) const {
    198   int history_items = GetHistoryItemCount();
    199   // If the index is past the number of history items + separator,
    200   // we then consider if it is a chapter-stop entry.
    201   if (index > history_items) {
    202     // We either are in ChapterStop area, or at the end of the list (the "Show
    203     // Full History" link).
    204     int chapter_stops = GetChapterStopCount(history_items);
    205     if (chapter_stops == 0)
    206       return false;  // We must have reached the "Show Full History" link.
    207     // Otherwise, look to see if we have reached the separator for the
    208     // chapter-stops. If not, this is a chapter stop.
    209     return (index == history_items + 1 + chapter_stops);
    210   }
    211 
    212   // Look to see if we have reached the separator for the history items.
    213   return index == history_items;
    214 }
    215 
    216 void BackForwardMenuModel::SetMenuModelDelegate(
    217       ui::MenuModelDelegate* menu_model_delegate) {
    218   menu_model_delegate_ = menu_model_delegate;
    219 }
    220 
    221 void BackForwardMenuModel::FetchFavicon(NavigationEntry* entry) {
    222   // If the favicon has already been requested for this menu, don't do
    223   // anything.
    224   if (requested_favicons_.find(entry->unique_id()) !=
    225       requested_favicons_.end()) {
    226     return;
    227   }
    228   requested_favicons_.insert(entry->unique_id());
    229   FaviconService* favicon_service =
    230       browser_->profile()->GetFaviconService(Profile::EXPLICIT_ACCESS);
    231   if (!favicon_service)
    232     return;
    233   FaviconService::Handle handle = favicon_service->GetFaviconForURL(
    234       entry->url(), history::FAVICON, &load_consumer_,
    235       NewCallback(this, &BackForwardMenuModel::OnFavIconDataAvailable));
    236   load_consumer_.SetClientData(favicon_service, handle, entry->unique_id());
    237 }
    238 
    239 void BackForwardMenuModel::OnFavIconDataAvailable(
    240     FaviconService::Handle handle,
    241     history::FaviconData favicon) {
    242   if (favicon.is_valid()) {
    243     int unique_id = load_consumer_.GetClientDataForCurrentRequest();
    244     // Find the current model_index for the unique_id.
    245     NavigationEntry* entry = NULL;
    246     int model_index = -1;
    247     for (int i = 0; i < GetItemCount() - 1; i++) {
    248       if (IsSeparator(i))
    249         continue;
    250       if (GetNavigationEntry(i)->unique_id() == unique_id) {
    251         model_index = i;
    252         entry = GetNavigationEntry(i);
    253         break;
    254       }
    255     }
    256 
    257     if (!entry)
    258       // The NavigationEntry wasn't found, this can happen if the user
    259       // navigates to another page and a NavigatationEntry falls out of the
    260       // range of kMaxHistoryItems.
    261       return;
    262 
    263     // Now that we have a valid NavigationEntry, decode the favicon and assign
    264     // it to the NavigationEntry.
    265     SkBitmap fav_icon;
    266     if (gfx::PNGCodec::Decode(favicon.image_data->front(),
    267                               favicon.image_data->size(),
    268                               &fav_icon)) {
    269       entry->favicon().set_is_valid(true);
    270       entry->favicon().set_url(favicon.icon_url);
    271       if (fav_icon.empty())
    272         return;
    273       entry->favicon().set_bitmap(fav_icon);
    274       if (menu_model_delegate()) {
    275         menu_model_delegate()->OnIconChanged(model_index);
    276       }
    277     }
    278   }
    279 }
    280 
    281 int BackForwardMenuModel::GetHistoryItemCount() const {
    282   TabContents* contents = GetTabContents();
    283   int items = 0;
    284 
    285   if (model_type_ == FORWARD_MENU) {
    286     // Only count items from n+1 to end (if n is current entry)
    287     items = contents->controller().entry_count() -
    288             contents->controller().GetCurrentEntryIndex() - 1;
    289   } else {
    290     items = contents->controller().GetCurrentEntryIndex();
    291   }
    292 
    293   if (items > kMaxHistoryItems)
    294     items = kMaxHistoryItems;
    295   else if (items < 0)
    296     items = 0;
    297 
    298   return items;
    299 }
    300 
    301 int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
    302   TabContents* contents = GetTabContents();
    303 
    304   int chapter_stops = 0;
    305   int current_entry = contents->controller().GetCurrentEntryIndex();
    306 
    307   if (history_items == kMaxHistoryItems) {
    308     int chapter_id = current_entry;
    309     if (model_type_ == FORWARD_MENU) {
    310       chapter_id += history_items;
    311     } else {
    312       chapter_id -= history_items;
    313     }
    314 
    315     do {
    316       chapter_id = GetIndexOfNextChapterStop(chapter_id,
    317           model_type_ == FORWARD_MENU);
    318       if (chapter_id != -1)
    319         ++chapter_stops;
    320     } while (chapter_id != -1 && chapter_stops < kMaxChapterStops);
    321   }
    322 
    323   return chapter_stops;
    324 }
    325 
    326 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
    327                                                     bool forward) const {
    328   TabContents* contents = GetTabContents();
    329   NavigationController& controller = contents->controller();
    330 
    331   int max_count = controller.entry_count();
    332   if (start_from < 0 || start_from >= max_count)
    333     return -1;  // Out of bounds.
    334 
    335   if (forward) {
    336     if (start_from < max_count - 1) {
    337       // We want to advance over the current chapter stop, so we add one.
    338       // We don't need to do this when direction is backwards.
    339       start_from++;
    340     } else {
    341       return -1;
    342     }
    343   }
    344 
    345   NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
    346   const GURL& url = start_entry->url();
    347 
    348   if (!forward) {
    349     // When going backwards we return the first entry we find that has a
    350     // different domain.
    351     for (int i = start_from - 1; i >= 0; --i) {
    352       if (!net::RegistryControlledDomainService::SameDomainOrHost(url,
    353               controller.GetEntryAtIndex(i)->url()))
    354         return i;
    355     }
    356     // We have reached the beginning without finding a chapter stop.
    357     return -1;
    358   } else {
    359     // When going forwards we return the entry before the entry that has a
    360     // different domain.
    361     for (int i = start_from + 1; i < max_count; ++i) {
    362       if (!net::RegistryControlledDomainService::SameDomainOrHost(url,
    363               controller.GetEntryAtIndex(i)->url()))
    364         return i - 1;
    365     }
    366     // Last entry is always considered a chapter stop.
    367     return max_count - 1;
    368   }
    369 }
    370 
    371 int BackForwardMenuModel::FindChapterStop(int offset,
    372                                           bool forward,
    373                                           int skip) const {
    374   if (offset < 0 || skip < 0)
    375     return -1;
    376 
    377   if (!forward)
    378     offset *= -1;
    379 
    380   TabContents* contents = GetTabContents();
    381   int entry = contents->controller().GetCurrentEntryIndex() + offset;
    382   for (int i = 0; i < skip + 1; i++)
    383     entry = GetIndexOfNextChapterStop(entry, forward);
    384 
    385   return entry;
    386 }
    387 
    388 bool BackForwardMenuModel::ItemHasCommand(int index) const {
    389   return index < GetItemCount() && !IsSeparator(index);
    390 }
    391 
    392 bool BackForwardMenuModel::ItemHasIcon(int index) const {
    393   return index < GetItemCount() && !IsSeparator(index);
    394 }
    395 
    396 string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
    397   return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
    398 }
    399 
    400 TabContents* BackForwardMenuModel::GetTabContents() const {
    401   // We use the test tab contents if the unit test has specified it.
    402   return test_tab_contents_ ? test_tab_contents_ :
    403                               browser_->GetSelectedTabContents();
    404 }
    405 
    406 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
    407   TabContents* contents = GetTabContents();
    408   int history_items = GetHistoryItemCount();
    409 
    410   DCHECK_GE(index, 0);
    411 
    412   // Convert anything above the History items separator.
    413   if (index < history_items) {
    414     if (model_type_ == FORWARD_MENU) {
    415       index += contents->controller().GetCurrentEntryIndex() + 1;
    416     } else {
    417       // Back menu is reverse.
    418       index = contents->controller().GetCurrentEntryIndex() - (index + 1);
    419     }
    420     return index;
    421   }
    422   if (index == history_items)
    423     return -1;  // Don't translate the separator for history items.
    424 
    425   if (index >= history_items + 1 + GetChapterStopCount(history_items))
    426     return -1;  // This is beyond the last chapter stop so we abort.
    427 
    428   // This menu item is a chapter stop located between the two separators.
    429   index = FindChapterStop(history_items,
    430                           model_type_ == FORWARD_MENU,
    431                           index - history_items - 1);
    432 
    433   return index;
    434 }
    435 
    436 NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
    437   int controller_index = MenuIndexToNavEntryIndex(index);
    438   NavigationController& controller = GetTabContents()->controller();
    439   if (controller_index >= 0 && controller_index < controller.entry_count())
    440     return controller.GetEntryAtIndex(controller_index);
    441 
    442   NOTREACHED();
    443   return NULL;
    444 }
    445 
    446 std::string BackForwardMenuModel::BuildActionName(
    447     const std::string& action, int index) const {
    448   DCHECK(!action.empty());
    449   DCHECK(index >= -1);
    450   std::string metric_string;
    451   if (model_type_ == FORWARD_MENU)
    452     metric_string += "ForwardMenu_";
    453   else
    454     metric_string += "BackMenu_";
    455   metric_string += action;
    456   if (index != -1) {
    457     // +1 is for historical reasons (indices used to start at 1).
    458     metric_string += base::IntToString(index + 1);
    459   }
    460   return metric_string;
    461 }
    462