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.
5 #include "chrome/browser/ui/toolbar/back_forward_menu_model.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/favicon/favicon_types.h"
19 #include "chrome/common/pref_names.h"
20 #include "chrome/common/url_constants.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"
35 using base::UserMetricsAction
;
36 using content::NavigationController
;
37 using content::NavigationEntry
;
38 using content::WebContents
;
40 const int BackForwardMenuModel::kMaxHistoryItems
= 12;
41 const int BackForwardMenuModel::kMaxChapterStops
= 5;
42 static const int kMaxWidth
= 700;
44 BackForwardMenuModel::BackForwardMenuModel(Browser
* browser
,
47 test_web_contents_(NULL
),
48 model_type_(model_type
),
49 menu_model_delegate_(NULL
) {
52 BackForwardMenuModel::~BackForwardMenuModel() {
55 bool BackForwardMenuModel::HasIcons() const {
59 int BackForwardMenuModel::GetItemCount() const {
60 int items
= GetHistoryItemCount();
63 int chapter_stops
= 0;
65 // Next, we count ChapterStops, if any.
66 if (items
== kMaxHistoryItems
)
67 chapter_stops
= GetChapterStopCount(items
);
70 items
+= chapter_stops
+ 1; // Chapter stops also need a separator.
72 // If the menu is not empty, add two positions in the end
73 // for a separator and a "Show Full History" item.
80 ui::MenuModel::ItemType
BackForwardMenuModel::GetTypeAt(int index
) const {
81 return IsSeparator(index
) ? TYPE_SEPARATOR
: TYPE_COMMAND
;
84 ui::MenuSeparatorType
BackForwardMenuModel::GetSeparatorTypeAt(
86 return ui::NORMAL_SEPARATOR
;
89 int BackForwardMenuModel::GetCommandIdAt(int index
) const {
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
);
98 // Return an empty string for a separator.
99 if (IsSeparator(index
))
100 return base::string16();
102 // Return the entry title, escaping any '&' characters and eliding it if it's
104 NavigationEntry
* entry
= GetNavigationEntry(index
);
106 Profile::FromBrowserContext(GetWebContents()->GetBrowserContext());
107 base::string16
menu_text(entry
->GetTitleForDisplay(
108 profile
->GetPrefs()->GetString(prefs::kAcceptLanguages
)));
110 gfx::ElideText(menu_text
, gfx::FontList(), kMaxWidth
, gfx::ELIDE_AT_END
);
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, '&');
122 bool BackForwardMenuModel::IsItemDynamicAt(int index
) const {
123 // This object is only used for a single showing of a menu.
127 bool BackForwardMenuModel::GetAcceleratorAt(
129 ui::Accelerator
* accelerator
) const {
133 bool BackForwardMenuModel::IsItemCheckedAt(int index
) const {
137 int BackForwardMenuModel::GetGroupIdAt(int index
) const {
141 bool BackForwardMenuModel::GetIconAt(int index
, gfx::Image
* icon
) {
142 if (!ItemHasIcon(index
))
145 if (index
== GetItemCount() - 1) {
146 *icon
= ResourceBundle::GetSharedInstance().GetNativeImageNamed(
147 IDR_HISTORY_FAVICON
);
149 NavigationEntry
* entry
= GetNavigationEntry(index
);
150 *icon
= entry
->GetFavicon().image
;
151 if (!entry
->GetFavicon().valid
&& menu_model_delegate()) {
159 ui::ButtonMenuItemModel
* BackForwardMenuModel::GetButtonMenuItemAt(
164 bool BackForwardMenuModel::IsEnabledAt(int index
) const {
165 return index
< GetItemCount() && !IsSeparator(index
);
168 ui::MenuModel
* BackForwardMenuModel::GetSubmenuModelAt(int index
) const {
172 void BackForwardMenuModel::HighlightChangedTo(int index
) {
175 void BackForwardMenuModel::ActivatedAt(int index
) {
176 ActivatedAt(index
, 0);
179 void BackForwardMenuModel::ActivatedAt(int index
, int event_flags
) {
180 DCHECK(!IsSeparator(index
));
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
)));
191 // Log whether it was a history or chapter click.
192 if (index
< GetHistoryItemCount()) {
193 content::RecordComputedAction(
194 BuildActionName("HistoryClick", index
));
196 content::RecordComputedAction(
197 BuildActionName("ChapterClick", index
- GetHistoryItemCount() - 1));
200 int controller_index
= MenuIndexToNavEntryIndex(index
);
201 WindowOpenDisposition disposition
=
202 ui::DispositionFromEventFlags(event_flags
);
203 if (!chrome::NavigateToIndexWithDisposition(browser_
,
210 void BackForwardMenuModel::MenuWillShow() {
211 content::RecordComputedAction(BuildActionName("Popup", -1));
212 requested_favicons_
.clear();
213 cancelable_task_tracker_
.TryCancelAll();
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
);
231 // Look to see if we have reached the separator for the history items.
232 return index
== history_items
;
235 void BackForwardMenuModel::SetMenuModelDelegate(
236 ui::MenuModelDelegate
* menu_model_delegate
) {
237 menu_model_delegate_
= menu_model_delegate
;
240 ui::MenuModelDelegate
* BackForwardMenuModel::GetMenuModelDelegate() const {
241 return menu_model_delegate_
;
244 void BackForwardMenuModel::FetchFavicon(NavigationEntry
* entry
) {
245 // If the favicon has already been requested for this menu, don't do
247 if (requested_favicons_
.find(entry
->GetUniqueID()) !=
248 requested_favicons_
.end()) {
251 requested_favicons_
.insert(entry
->GetUniqueID());
252 FaviconService
* favicon_service
= FaviconServiceFactory::GetForProfile(
253 browser_
->profile(), Profile::EXPLICIT_ACCESS
);
254 if (!favicon_service
)
257 favicon_service
->GetFaviconImageForURL(
258 FaviconService::FaviconForURLParams(entry
->GetURL(),
261 base::Bind(&BackForwardMenuModel::OnFavIconDataAvailable
,
262 base::Unretained(this),
263 entry
->GetUniqueID()),
264 &cancelable_task_tracker_
);
267 void BackForwardMenuModel::OnFavIconDataAvailable(
268 int navigation_entry_unique_id
,
269 const chrome::FaviconImageResult
& image_result
) {
270 if (!image_result
.image
.IsEmpty()) {
271 // Find the current model_index for the unique id.
272 NavigationEntry
* entry
= NULL
;
273 int model_index
= -1;
274 for (int i
= 0; i
< GetItemCount() - 1; i
++) {
277 if (GetNavigationEntry(i
)->GetUniqueID() == navigation_entry_unique_id
) {
279 entry
= GetNavigationEntry(i
);
285 // The NavigationEntry wasn't found, this can happen if the user
286 // navigates to another page and a NavigatationEntry falls out of the
287 // range of kMaxHistoryItems.
290 // Now that we have a valid NavigationEntry, decode the favicon and assign
291 // it to the NavigationEntry.
292 entry
->GetFavicon().valid
= true;
293 entry
->GetFavicon().url
= image_result
.icon_url
;
294 entry
->GetFavicon().image
= image_result
.image
;
295 if (menu_model_delegate()) {
296 menu_model_delegate()->OnIconChanged(model_index
);
301 int BackForwardMenuModel::GetHistoryItemCount() const {
302 WebContents
* contents
= GetWebContents();
305 if (model_type_
== FORWARD_MENU
) {
306 // Only count items from n+1 to end (if n is current entry)
307 items
= contents
->GetController().GetEntryCount() -
308 contents
->GetController().GetCurrentEntryIndex() - 1;
310 items
= contents
->GetController().GetCurrentEntryIndex();
313 if (items
> kMaxHistoryItems
)
314 items
= kMaxHistoryItems
;
321 int BackForwardMenuModel::GetChapterStopCount(int history_items
) const {
322 WebContents
* contents
= GetWebContents();
324 int chapter_stops
= 0;
325 int current_entry
= contents
->GetController().GetCurrentEntryIndex();
327 if (history_items
== kMaxHistoryItems
) {
328 int chapter_id
= current_entry
;
329 if (model_type_
== FORWARD_MENU
) {
330 chapter_id
+= history_items
;
332 chapter_id
-= history_items
;
336 chapter_id
= GetIndexOfNextChapterStop(chapter_id
,
337 model_type_
== FORWARD_MENU
);
338 if (chapter_id
!= -1)
340 } while (chapter_id
!= -1 && chapter_stops
< kMaxChapterStops
);
343 return chapter_stops
;
346 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from
,
347 bool forward
) const {
348 WebContents
* contents
= GetWebContents();
349 NavigationController
& controller
= contents
->GetController();
351 int max_count
= controller
.GetEntryCount();
352 if (start_from
< 0 || start_from
>= max_count
)
353 return -1; // Out of bounds.
356 if (start_from
< max_count
- 1) {
357 // We want to advance over the current chapter stop, so we add one.
358 // We don't need to do this when direction is backwards.
365 NavigationEntry
* start_entry
= controller
.GetEntryAtIndex(start_from
);
366 const GURL
& url
= start_entry
->GetURL();
369 // When going backwards we return the first entry we find that has a
371 for (int i
= start_from
- 1; i
>= 0; --i
) {
372 if (!net::registry_controlled_domains::SameDomainOrHost(url
,
373 controller
.GetEntryAtIndex(i
)->GetURL(),
374 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES
))
377 // We have reached the beginning without finding a chapter stop.
380 // When going forwards we return the entry before the entry that has a
382 for (int i
= start_from
+ 1; i
< max_count
; ++i
) {
383 if (!net::registry_controlled_domains::SameDomainOrHost(url
,
384 controller
.GetEntryAtIndex(i
)->GetURL(),
385 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES
))
388 // Last entry is always considered a chapter stop.
389 return max_count
- 1;
393 int BackForwardMenuModel::FindChapterStop(int offset
,
396 if (offset
< 0 || skip
< 0)
402 WebContents
* contents
= GetWebContents();
403 int entry
= contents
->GetController().GetCurrentEntryIndex() + offset
;
404 for (int i
= 0; i
< skip
+ 1; i
++)
405 entry
= GetIndexOfNextChapterStop(entry
, forward
);
410 bool BackForwardMenuModel::ItemHasCommand(int index
) const {
411 return index
< GetItemCount() && !IsSeparator(index
);
414 bool BackForwardMenuModel::ItemHasIcon(int index
) const {
415 return index
< GetItemCount() && !IsSeparator(index
);
418 base::string16
BackForwardMenuModel::GetShowFullHistoryLabel() const {
419 return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK
);
422 WebContents
* BackForwardMenuModel::GetWebContents() const {
423 // We use the test web contents if the unit test has specified it.
424 return test_web_contents_
?
426 browser_
->tab_strip_model()->GetActiveWebContents();
429 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index
) const {
430 WebContents
* contents
= GetWebContents();
431 int history_items
= GetHistoryItemCount();
435 // Convert anything above the History items separator.
436 if (index
< history_items
) {
437 if (model_type_
== FORWARD_MENU
) {
438 index
+= contents
->GetController().GetCurrentEntryIndex() + 1;
440 // Back menu is reverse.
441 index
= contents
->GetController().GetCurrentEntryIndex() - (index
+ 1);
445 if (index
== history_items
)
446 return -1; // Don't translate the separator for history items.
448 if (index
>= history_items
+ 1 + GetChapterStopCount(history_items
))
449 return -1; // This is beyond the last chapter stop so we abort.
451 // This menu item is a chapter stop located between the two separators.
452 index
= FindChapterStop(history_items
,
453 model_type_
== FORWARD_MENU
,
454 index
- history_items
- 1);
459 NavigationEntry
* BackForwardMenuModel::GetNavigationEntry(int index
) const {
460 int controller_index
= MenuIndexToNavEntryIndex(index
);
461 NavigationController
& controller
= GetWebContents()->GetController();
462 if (controller_index
>= 0 && controller_index
< controller
.GetEntryCount())
463 return controller
.GetEntryAtIndex(controller_index
);
469 std::string
BackForwardMenuModel::BuildActionName(
470 const std::string
& action
, int index
) const {
471 DCHECK(!action
.empty());
473 std::string metric_string
;
474 if (model_type_
== FORWARD_MENU
)
475 metric_string
+= "ForwardMenu_";
477 metric_string
+= "BackMenu_";
478 metric_string
+= action
;
480 // +1 is for historical reasons (indices used to start at 1).
481 metric_string
+= base::IntToString(index
+ 1);
483 return metric_string
;