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