blob: b4e24acf5b34279a8713da2342011223fee3abdd [file] [log] [blame]
// Copyright (c) 2012 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/toolbar/back_forward_menu_model.h"
#include <stddef.h>
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/numerics/ranges.h"
#include "base/stl_util.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/url_constants.h"
#include "components/favicon_base/favicon_types.h"
#include "components/grit/components_scaled_resources.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/favicon_status.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/web_contents.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "ui/base/accelerators/menu_label_accelerator_util.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/window_open_disposition.h"
#include "ui/gfx/text_elider.h"
using base::UserMetricsAction;
using content::NavigationController;
using content::NavigationEntry;
using content::WebContents;
const int BackForwardMenuModel::kMaxHistoryItems = 12;
const int BackForwardMenuModel::kMaxChapterStops = 5;
static const int kMaxBackForwardMenuWidth = 700;
BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
ModelType model_type)
: browser_(browser), model_type_(model_type) {}
BackForwardMenuModel::~BackForwardMenuModel() {}
bool BackForwardMenuModel::HasIcons() const {
return true;
}
int BackForwardMenuModel::GetItemCount() const {
int items = GetHistoryItemCount();
if (items <= 0)
return items;
int chapter_stops = 0;
// Next, we count ChapterStops, if any.
if (items == kMaxHistoryItems)
chapter_stops = GetChapterStopCount(items);
if (chapter_stops)
items += chapter_stops + 1; // Chapter stops also need a separator.
// If the menu is not empty, add two positions in the end
// for a separator and a "Show Full History" item.
items += 2;
return items;
}
ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
}
ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
int index) const {
return ui::NORMAL_SEPARATOR;
}
int BackForwardMenuModel::GetCommandIdAt(int index) const {
return index;
}
base::string16 BackForwardMenuModel::GetLabelAt(int index) const {
// Return label "Show Full History" for the last item of the menu.
if (index == GetItemCount() - 1)
return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
// Return an empty string for a separator.
if (IsSeparator(index))
return base::string16();
// Return the entry title, escaping any '&' characters and eliding it if it's
// super long.
NavigationEntry* entry = GetNavigationEntry(index);
base::string16 menu_text(entry->GetTitleForDisplay());
menu_text = ui::EscapeMenuLabelAmpersands(menu_text);
menu_text =
gfx::ElideText(menu_text, gfx::FontList(), kMaxBackForwardMenuWidth,
gfx::ELIDE_TAIL, gfx::Typesetter::NATIVE);
return menu_text;
}
bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
// This object is only used for a single showing of a menu.
return false;
}
bool BackForwardMenuModel::GetAcceleratorAt(
int index,
ui::Accelerator* accelerator) const {
return false;
}
bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
return false;
}
int BackForwardMenuModel::GetGroupIdAt(int index) const {
return false;
}
bool BackForwardMenuModel::GetIconAt(int index, gfx::Image* icon) {
if (!ItemHasIcon(index))
return false;
if (index == GetItemCount() - 1) {
*icon = ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
IDR_HISTORY_FAVICON);
} else {
NavigationEntry* entry = GetNavigationEntry(index);
*icon = entry->GetFavicon().image;
if (!entry->GetFavicon().valid && menu_model_delegate()) {
FetchFavicon(entry);
}
}
return true;
}
ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
int index) const {
return nullptr;
}
bool BackForwardMenuModel::IsEnabledAt(int index) const {
return index < GetItemCount() && !IsSeparator(index);
}
ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
return nullptr;
}
void BackForwardMenuModel::ActivatedAt(int index) {
ActivatedAt(index, 0);
}
void BackForwardMenuModel::ActivatedAt(int index, int event_flags) {
DCHECK(!IsSeparator(index));
// Execute the command for the last item: "Show Full History".
if (index == GetItemCount() - 1) {
base::RecordComputedAction(BuildActionName("ShowFullHistory", -1));
ShowSingletonTabOverwritingNTP(
browser_, GetSingletonTabNavigateParams(
browser_, GURL(chrome::kChromeUIHistoryURL)));
return;
}
// Log whether it was a history or chapter click.
int items = GetHistoryItemCount();
if (index < items) {
base::RecordComputedAction(BuildActionName("HistoryClick", index));
} else {
const int chapter_index = index - items - 1;
base::RecordComputedAction(BuildActionName("ChapterClick", chapter_index));
}
int controller_index = MenuIndexToNavEntryIndex(index);
UMA_HISTOGRAM_BOOLEAN(
"Navigation.BackForward.NavigatingToEntryMarkedToBeSkipped",
GetWebContents()->GetController().IsEntryMarkedToBeSkipped(
controller_index));
WindowOpenDisposition disposition =
ui::DispositionFromEventFlags(event_flags);
if (!chrome::NavigateToIndexWithDisposition(browser_,
controller_index,
disposition)) {
NOTREACHED();
}
}
void BackForwardMenuModel::MenuWillShow() {
base::RecordComputedAction(BuildActionName("Popup", -1));
requested_favicons_.clear();
cancelable_task_tracker_.TryCancelAll();
}
bool BackForwardMenuModel::IsSeparator(int index) const {
int history_items = GetHistoryItemCount();
// If the index is past the number of history items + separator,
// we then consider if it is a chapter-stop entry.
if (index > history_items) {
// We either are in ChapterStop area, or at the end of the list (the "Show
// Full History" link).
int chapter_stops = GetChapterStopCount(history_items);
if (chapter_stops == 0)
return false; // We must have reached the "Show Full History" link.
// Otherwise, look to see if we have reached the separator for the
// chapter-stops. If not, this is a chapter stop.
return (index == history_items + 1 + chapter_stops);
}
// Look to see if we have reached the separator for the history items.
return index == history_items;
}
void BackForwardMenuModel::FetchFavicon(NavigationEntry* entry) {
// If the favicon has already been requested for this menu, don't do
// anything.
if (base::ContainsKey(requested_favicons_, entry->GetUniqueID()))
return;
requested_favicons_.insert(entry->GetUniqueID());
favicon::FaviconService* favicon_service =
FaviconServiceFactory::GetForProfile(browser_->profile(),
ServiceAccessType::EXPLICIT_ACCESS);
if (!favicon_service)
return;
favicon_service->GetFaviconImageForPageURL(
entry->GetURL(),
base::Bind(&BackForwardMenuModel::OnFavIconDataAvailable,
base::Unretained(this),
entry->GetUniqueID()),
&cancelable_task_tracker_);
}
void BackForwardMenuModel::OnFavIconDataAvailable(
int navigation_entry_unique_id,
const favicon_base::FaviconImageResult& image_result) {
if (image_result.image.IsEmpty())
return;
// Find the current model_index for the unique id.
NavigationEntry* entry = nullptr;
int model_index = -1;
for (int i = 0; i < GetItemCount() - 1; i++) {
if (IsSeparator(i))
continue;
if (GetNavigationEntry(i)->GetUniqueID() == navigation_entry_unique_id) {
model_index = i;
entry = GetNavigationEntry(i);
break;
}
}
if (!entry) {
// The NavigationEntry wasn't found, this can happen if the user
// navigates to another page and a NavigatationEntry falls out of the
// range of kMaxHistoryItems.
return;
}
// Now that we have a valid NavigationEntry, decode the favicon and assign
// it to the NavigationEntry.
entry->GetFavicon().valid = true;
entry->GetFavicon().url = image_result.icon_url;
entry->GetFavicon().image = image_result.image;
if (menu_model_delegate()) {
menu_model_delegate()->OnIconChanged(model_index);
}
}
int BackForwardMenuModel::GetHistoryItemCount() const {
WebContents* contents = GetWebContents();
int items = contents->GetController().GetCurrentEntryIndex();
if (model_type_ == ModelType::kForward) {
// Only count items from n+1 to end (if n is current entry)
items = contents->GetController().GetEntryCount() - items - 1;
}
return base::ClampToRange(items, 0, kMaxHistoryItems);
}
int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
if (history_items != kMaxHistoryItems)
return 0;
WebContents* contents = GetWebContents();
int current_entry = contents->GetController().GetCurrentEntryIndex();
const bool forward = model_type_ == ModelType::kForward;
int chapter_id = current_entry;
if (forward)
chapter_id += history_items;
else
chapter_id -= history_items;
int chapter_stops = 0;
do {
chapter_id = GetIndexOfNextChapterStop(chapter_id, forward);
if (chapter_id == -1)
break;
++chapter_stops;
} while (chapter_stops < kMaxChapterStops);
return chapter_stops;
}
int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
bool forward) const {
if (start_from < 0)
return -1; // Out of bounds.
// We want to advance over the current chapter stop, so we add one.
// We don't need to do this when direction is backwards.
if (forward)
start_from++;
NavigationController& controller = GetWebContents()->GetController();
const int max_count = controller.GetEntryCount();
if (start_from >= max_count)
return -1; // Out of bounds.
NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
const GURL& url = start_entry->GetURL();
auto same_domain_func = [&controller, &url](int i) {
return net::registry_controlled_domains::SameDomainOrHost(
url, controller.GetEntryAtIndex(i)->GetURL(),
net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
};
if (forward) {
// When going forwards we return the entry before the entry that has a
// different domain.
for (int i = start_from + 1; i < max_count; ++i) {
if (!same_domain_func(i))
return i - 1;
}
// Last entry is always considered a chapter stop.
return max_count - 1;
}
// When going backwards we return the first entry we find that has a
// different domain.
for (int i = start_from - 1; i >= 0; --i) {
if (!same_domain_func(i))
return i;
}
// We have reached the beginning without finding a chapter stop.
return -1;
}
int BackForwardMenuModel::FindChapterStop(int offset,
bool forward,
int skip) const {
if (offset < 0 || skip < 0)
return -1;
if (!forward)
offset *= -1;
WebContents* contents = GetWebContents();
int entry = contents->GetController().GetCurrentEntryIndex() + offset;
for (int i = 0; i < skip + 1; i++)
entry = GetIndexOfNextChapterStop(entry, forward);
return entry;
}
bool BackForwardMenuModel::ItemHasCommand(int index) const {
return index < GetItemCount() && !IsSeparator(index);
}
bool BackForwardMenuModel::ItemHasIcon(int index) const {
return index < GetItemCount() && !IsSeparator(index);
}
base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
}
WebContents* BackForwardMenuModel::GetWebContents() const {
// We use the test web contents if the unit test has specified it.
return test_web_contents_ ?
test_web_contents_ :
browser_->tab_strip_model()->GetActiveWebContents();
}
int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
WebContents* contents = GetWebContents();
int history_items = GetHistoryItemCount();
DCHECK_GE(index, 0);
// Convert anything above the History items separator.
if (index < history_items) {
if (model_type_ == ModelType::kForward) {
index += contents->GetController().GetCurrentEntryIndex() + 1;
} else {
// Back menu is reverse.
index = contents->GetController().GetCurrentEntryIndex() - (index + 1);
}
return index;
}
if (index == history_items)
return -1; // Don't translate the separator for history items.
if (index >= history_items + 1 + GetChapterStopCount(history_items))
return -1; // This is beyond the last chapter stop so we abort.
// This menu item is a chapter stop located between the two separators.
return FindChapterStop(history_items, model_type_ == ModelType::kForward,
index - history_items - 1);
}
NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
int controller_index = MenuIndexToNavEntryIndex(index);
NavigationController& controller = GetWebContents()->GetController();
if (controller_index >= 0 && controller_index < controller.GetEntryCount())
return controller.GetEntryAtIndex(controller_index);
NOTREACHED();
return nullptr;
}
std::string BackForwardMenuModel::BuildActionName(
const std::string& action, int index) const {
DCHECK(!action.empty());
DCHECK_GE(index, -1);
std::string metric_string;
if (model_type_ == ModelType::kForward)
metric_string += "ForwardMenu_";
else
metric_string += "BackMenu_";
metric_string += action;
if (index != -1) {
// +1 is for historical reasons (indices used to start at 1).
metric_string += base::NumberToString(index + 1);
}
return metric_string;
}