| // 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; |
| } |