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