blob: 6c79bd1a3cbc4022d755c465b82fe12c3a73de38 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// 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 <algorithm>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/strings/string_number_conversions.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/favicon/favicon_service_factory.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/profiles/profile.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/browser_window.h"
#include "chrome/browser/ui/singleton_tabs.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/url_constants.h"
#include "components/favicon_base/favicon_types.h"
#include "components/feature_engagement/public/feature_constants.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/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/ui_base_features.h"
#include "ui/base/window_open_disposition.h"
#include "ui/base/window_open_disposition_utils.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/text_elider.h"
#include "ui/menus/simple_menu_model.h"
using base::UserMetricsAction;
using content::NavigationController;
using content::NavigationEntry;
using content::WebContents;
const size_t BackForwardMenuModel::kMaxHistoryItems = 12;
const size_t BackForwardMenuModel::kMaxChapterStops = 5;
static const int kMaxBackForwardMenuWidth = 700;
BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
ModelType model_type)
: browser_(browser), model_type_(model_type) {}
BackForwardMenuModel::~BackForwardMenuModel() = default;
base::WeakPtr<ui::MenuModel> BackForwardMenuModel::AsWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
size_t BackForwardMenuModel::GetItemCount() const {
size_t items = GetHistoryItemCount();
if (items == 0) {
return items;
}
size_t 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 current mode is incognito, "Show Full History" should not be
// visible.
if (!ShouldShowFullHistoryBeVisible()) {
return items;
}
// 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(size_t index) const {
return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
}
ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
size_t index) const {
return ui::NORMAL_SEPARATOR;
}
int BackForwardMenuModel::GetCommandIdAt(size_t index) const {
return static_cast<int>(index);
}
std::u16string BackForwardMenuModel::GetLabelAt(size_t index) const {
// Return label "Show Full History" for the last item of the menu.
if (ShouldShowFullHistoryBeVisible() && index == GetItemCount() - 1) {
return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
}
// Return an empty string for a separator.
if (IsSeparator(index)) {
return std::u16string();
}
// Return the entry title, escaping any '&' characters and eliding it if it's
// super long.
NavigationEntry* entry = GetNavigationEntry(index);
std::u16string menu_text(entry->GetTitleForDisplay());
menu_text = ui::EscapeMenuLabelAmpersands(menu_text);
menu_text = gfx::ElideText(menu_text, gfx::FontList(),
kMaxBackForwardMenuWidth, gfx::ELIDE_TAIL);
return menu_text;
}
bool BackForwardMenuModel::IsItemDynamicAt(size_t index) const {
// This object is only used for a single showing of a menu.
return false;
}
bool BackForwardMenuModel::GetAcceleratorAt(
size_t index,
ui::Accelerator* accelerator) const {
return false;
}
bool BackForwardMenuModel::IsItemCheckedAt(size_t index) const {
return false;
}
int BackForwardMenuModel::GetGroupIdAt(size_t index) const {
return false;
}
ui::ImageModel BackForwardMenuModel::GetIconAt(size_t index) const {
if (!ItemHasIcon(index)) {
return ui::ImageModel();
}
// Return icon of "Show Full History" for the last item of the menu.
if (ShouldShowFullHistoryBeVisible() && index == GetItemCount() - 1) {
return ui::ImageModel::FromVectorIcon(
kHistoryIcon, ui::kColorMenuIcon,
ui::SimpleMenuModel::kDefaultIconSize);
}
NavigationEntry* entry = GetNavigationEntry(index);
content::FaviconStatus fav_icon = entry->GetFavicon();
if (!fav_icon.valid && menu_model_delegate()) {
// FetchFavicon is not const because it caches the result, but GetIconAt
// is const because it is not be apparent to outside observers that an
// internal change is taking place. Compared to spreading const in
// unintuitive places (e.g. making menu_model_delegate() const but
// returning a non-const while sprinkling virtual on member variables),
// this const_cast is the lesser evil.
const_cast<BackForwardMenuModel*>(this)->FetchFavicon(entry);
}
// Only apply theming to certain chrome:// favicons.
if (favicon::ShouldThemifyFaviconForEntry(entry)) {
const ui::ColorProvider* const cp = &GetWebContents()->GetColorProvider();
gfx::ImageSkia themed_favicon = favicon::ThemeFavicon(
fav_icon.image.AsImageSkia(), cp->GetColor(ui::kColorMenuIcon),
cp->GetColor(ui::kColorMenuItemBackgroundHighlighted),
cp->GetColor(ui::kColorMenuBackground));
return ui::ImageModel::FromImageSkia(themed_favicon);
}
return ui::ImageModel::FromImage(fav_icon.image);
}
ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
size_t index) const {
return nullptr;
}
bool BackForwardMenuModel::IsEnabledAt(size_t index) const {
return index < GetItemCount() && !IsSeparator(index);
}
ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(size_t index) const {
return nullptr;
}
void BackForwardMenuModel::ActivatedAt(size_t index) {
ActivatedAt(index, 0);
}
void BackForwardMenuModel::ActivatedAt(size_t index, int event_flags) {
DCHECK(!IsSeparator(index));
// Execute the command for the last item: "Show Full History".
if (ShouldShowFullHistoryBeVisible() && index == GetItemCount() - 1) {
base::RecordComputedAction(
BuildActionName("ShowFullHistory", std::nullopt));
ShowSingletonTabOverwritingNTP(browser_, GURL(chrome::kChromeUIHistoryURL));
return;
}
// Log whether it was a history or chapter click.
size_t items = GetHistoryItemCount();
if (index < items) {
base::RecordComputedAction(BuildActionName("HistoryClick", index));
} else {
const auto chapter_index =
(index == items) ? std::nullopt : std::make_optional(index - items - 1);
base::RecordComputedAction(BuildActionName("ChapterClick", chapter_index));
}
CHECK(menu_model_open_timestamp_.has_value());
base::TimeDelta time =
base::TimeTicks::Now() - menu_model_open_timestamp_.value();
base::UmaHistogramLongTimes(
"Navigation.BackForward.TimeFromOpenBackNavigationMenuToActivateItem",
time);
std::optional<size_t> controller_index = MenuIndexToNavEntryIndex(index);
DCHECK(controller_index.has_value());
WindowOpenDisposition disposition =
ui::DispositionFromEventFlags(event_flags);
chrome::NavigateToIndexWithDisposition(browser_, controller_index.value(),
disposition);
}
void BackForwardMenuModel::MenuWillShow() {
base::RecordComputedAction(BuildActionName("Popup", std::nullopt));
requested_favicons_.clear();
cancelable_task_tracker_.TryCancelAll();
menu_model_open_timestamp_ = base::TimeTicks::Now();
// Observe the web contents for navigation changes which could
// happen while the menu is open.
content::WebContentsObserver::Observe(GetWebContents());
// Close the IPH popup if the user opens the menu.
BrowserUserEducationInterface::From(browser_)->NotifyFeaturePromoFeatureUsed(
feature_engagement::kIPHBackNavigationMenuFeature,
FeaturePromoFeatureUsedAction::kClosePromoIfPresent);
}
void BackForwardMenuModel::MenuWillClose() {
content::WebContentsObserver::Observe(nullptr);
CHECK(menu_model_open_timestamp_.has_value());
base::TimeDelta time =
base::TimeTicks::Now() - menu_model_open_timestamp_.value();
base::UmaHistogramLongTimes(
"Navigation.BackForward.TimeFromOpenBackNavigationMenuToCloseMenu", time);
}
void BackForwardMenuModel::NavigationEntryCommitted(
const content::LoadCommittedDetails& load_details) {
if (menu_model_delegate()) {
menu_model_delegate()->OnMenuStructureChanged();
}
}
void BackForwardMenuModel::NavigationEntriesDeleted() {
if (menu_model_delegate()) {
menu_model_delegate()->OnMenuStructureChanged();
}
}
bool BackForwardMenuModel::IsSeparator(size_t index) const {
size_t 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).
size_t 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::Contains(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::BindOnce(&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;
size_t model_index = 0;
for (size_t i = 0; i + 1 < GetItemCount(); ++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(GetCommandIdAt(model_index));
}
}
size_t BackForwardMenuModel::GetHistoryItemCount() const {
WebContents* contents = GetWebContents();
if (!contents) {
return 0;
}
size_t 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 std::min(items, kMaxHistoryItems);
}
size_t BackForwardMenuModel::GetChapterStopCount(size_t history_items) const {
if (history_items != kMaxHistoryItems) {
return 0;
}
WebContents* contents = GetWebContents();
size_t current_entry = contents->GetController().GetCurrentEntryIndex();
const bool forward = model_type_ == ModelType::kForward;
size_t chapter_id = current_entry;
if (!forward && chapter_id < history_items) {
return 0;
}
chapter_id =
forward ? (chapter_id + history_items) : (chapter_id - history_items);
size_t chapter_stops = 0;
do {
const std::optional<size_t> index =
GetIndexOfNextChapterStop(chapter_id, forward);
if (!index.has_value()) {
break;
}
chapter_id = index.value();
++chapter_stops;
} while (chapter_stops < kMaxChapterStops);
return chapter_stops;
}
std::optional<size_t> BackForwardMenuModel::GetIndexOfNextChapterStop(
size_t start_from,
bool forward) const {
// 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 size_t max_count = controller.GetEntryCount();
if (start_from >= max_count) {
return std::nullopt; // Out of bounds.
}
NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
const GURL& url = start_entry->GetURL();
auto same_domain_func = [&controller, &url](size_t 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 (size_t 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 (size_t i = start_from; i > 0; --i) {
if (!same_domain_func(i - 1)) {
return i - 1;
}
}
// We have reached the beginning without finding a chapter stop.
return std::nullopt;
}
std::optional<size_t> BackForwardMenuModel::FindChapterStop(size_t offset,
bool forward,
size_t skip) const {
WebContents* contents = GetWebContents();
size_t entry = contents->GetController().GetCurrentEntryIndex();
if (!forward && entry < offset) {
return std::nullopt;
}
entry = forward ? (entry + offset) : (entry - offset);
for (size_t i = 0; i <= skip; ++i) {
const std::optional<size_t> index =
GetIndexOfNextChapterStop(entry, forward);
if (!index.has_value()) {
return std::nullopt;
}
entry = index.value();
}
return entry;
}
bool BackForwardMenuModel::ItemHasCommand(size_t index) const {
return index < GetItemCount() && !IsSeparator(index);
}
bool BackForwardMenuModel::ItemHasIcon(size_t index) const {
return index < GetItemCount() && !IsSeparator(index);
}
std::u16string 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_.get()
: browser_->tab_strip_model()->GetActiveWebContents();
}
std::optional<size_t> BackForwardMenuModel::MenuIndexToNavEntryIndex(
size_t index) const {
WebContents* contents = GetWebContents();
size_t history_items = GetHistoryItemCount();
// Convert anything above the History items separator.
if (index < history_items) {
const size_t current_index =
contents->GetController().GetCurrentEntryIndex();
const bool forward = model_type_ == ModelType::kForward;
if (!forward && current_index <= index) {
return std::nullopt;
}
return forward ? (current_index + index + 1)
: (current_index - (index + 1));
}
if (index == history_items) {
return std::nullopt; // Don't translate the separator for history items.
}
if (index >= history_items + 1 + GetChapterStopCount(history_items)) {
return std::nullopt; // 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(size_t index) const {
std::optional<size_t> controller_index = MenuIndexToNavEntryIndex(index);
NavigationController& controller = GetWebContents()->GetController();
DCHECK(controller_index.has_value());
DCHECK_LT(controller_index.value(),
static_cast<size_t>(controller.GetEntryCount()));
return controller.GetEntryAtIndex(controller_index.value());
}
std::string BackForwardMenuModel::BuildActionName(
const std::string& action,
std::optional<size_t> index) const {
DCHECK(!action.empty());
std::string metric_string;
if (model_type_ == ModelType::kForward) {
metric_string += "ForwardMenu_";
} else {
metric_string += "BackMenu_";
}
metric_string += action;
if (index.has_value()) {
// +1 is for historical reasons (indices used to start at 1).
metric_string += base::NumberToString(index.value() + 1);
}
return metric_string;
}
bool BackForwardMenuModel::ShouldShowFullHistoryBeVisible() const {
return !browser_->profile()->IsOffTheRecord();
}