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 <stddef.h>
8
9 #include "base/bind.h"
10 #include "base/callback_helpers.h"
11 #include "base/metrics/histogram_macros.h"
12 #include "base/metrics/user_metrics.h"
13 #include "base/numerics/ranges.h"
14 #include "base/stl_util.h"
15 #include "base/strings/string_number_conversions.h"
16 #include "build/build_config.h"
17 #include "chrome/browser/favicon/favicon_service_factory.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/browser_commands.h"
20 #include "chrome/browser/ui/browser_navigator_params.h"
21 #include "chrome/browser/ui/singleton_tabs.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "chrome/common/url_constants.h"
24 #include "components/favicon_base/favicon_types.h"
25 #include "components/grit/components_scaled_resources.h"
26 #include "components/strings/grit/components_strings.h"
27 #include "content/public/browser/favicon_status.h"
28 #include "content/public/browser/navigation_controller.h"
29 #include "content/public/browser/navigation_entry.h"
30 #include "content/public/browser/web_contents.h"
31 #include "net/base/registry_controlled_domains/registry_controlled_domain.h"
32 #include "ui/base/accelerators/menu_label_accelerator_util.h"
33 #include "ui/base/l10n/l10n_util.h"
34 #include "ui/base/models/image_model.h"
35 #include "ui/base/resource/resource_bundle.h"
36 #include "ui/base/window_open_disposition.h"
37 #include "ui/gfx/text_elider.h"
38
39 using base::UserMetricsAction;
40 using content::NavigationController;
41 using content::NavigationEntry;
42 using content::WebContents;
43
44 const int BackForwardMenuModel::kMaxHistoryItems = 12;
45 const int BackForwardMenuModel::kMaxChapterStops = 5;
46 static const int kMaxBackForwardMenuWidth = 700;
47
BackForwardMenuModel(Browser * browser,ModelType model_type)48 BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
49 ModelType model_type)
50 : browser_(browser), model_type_(model_type) {}
51
~BackForwardMenuModel()52 BackForwardMenuModel::~BackForwardMenuModel() {}
53
HasIcons() const54 bool BackForwardMenuModel::HasIcons() const {
55 return true;
56 }
57
GetItemCount() const58 int BackForwardMenuModel::GetItemCount() const {
59 int items = GetHistoryItemCount();
60 if (items <= 0)
61 return items;
62
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 return items;
76 }
77
GetTypeAt(int index) const78 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
79 return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
80 }
81
GetSeparatorTypeAt(int index) const82 ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
83 int index) const {
84 return ui::NORMAL_SEPARATOR;
85 }
86
GetCommandIdAt(int index) const87 int BackForwardMenuModel::GetCommandIdAt(int index) const {
88 return index;
89 }
90
GetLabelAt(int index) const91 base::string16 BackForwardMenuModel::GetLabelAt(int index) const {
92 // Return label "Show Full History" for the last item of the menu.
93 if (index == GetItemCount() - 1)
94 return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
95
96 // Return an empty string for a separator.
97 if (IsSeparator(index))
98 return base::string16();
99
100 // Return the entry title, escaping any '&' characters and eliding it if it's
101 // super long.
102 NavigationEntry* entry = GetNavigationEntry(index);
103 base::string16 menu_text(entry->GetTitleForDisplay());
104 menu_text = ui::EscapeMenuLabelAmpersands(menu_text);
105 menu_text = gfx::ElideText(menu_text, gfx::FontList(),
106 kMaxBackForwardMenuWidth, gfx::ELIDE_TAIL);
107
108 return menu_text;
109 }
110
IsItemDynamicAt(int index) const111 bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
112 // This object is only used for a single showing of a menu.
113 return false;
114 }
115
GetAcceleratorAt(int index,ui::Accelerator * accelerator) const116 bool BackForwardMenuModel::GetAcceleratorAt(
117 int index,
118 ui::Accelerator* accelerator) const {
119 return false;
120 }
121
IsItemCheckedAt(int index) const122 bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
123 return false;
124 }
125
GetGroupIdAt(int index) const126 int BackForwardMenuModel::GetGroupIdAt(int index) const {
127 return false;
128 }
129
GetIconAt(int index) const130 ui::ImageModel BackForwardMenuModel::GetIconAt(int index) const {
131 if (!ItemHasIcon(index))
132 return ui::ImageModel();
133
134 if (index == GetItemCount() - 1) {
135 return ui::ImageModel::FromImage(
136 ui::ResourceBundle::GetSharedInstance().GetNativeImageNamed(
137 IDR_HISTORY_FAVICON));
138 } else {
139 NavigationEntry* entry = GetNavigationEntry(index);
140 content::FaviconStatus fav_icon = entry->GetFavicon();
141 if (!fav_icon.valid && menu_model_delegate()) {
142 // FetchFavicon is not const because it caches the result, but GetIconAt
143 // is const because it is not be apparent to outside observers that an
144 // internal change is taking place. Compared to spreading const in
145 // unintuitive places (e.g. making menu_model_delegate() const but
146 // returning a non-const while sprinkling virtual on member variables),
147 // this const_cast is the lesser evil.
148 const_cast<BackForwardMenuModel*>(this)->FetchFavicon(entry);
149 }
150 return ui::ImageModel::FromImage(fav_icon.image);
151 }
152 }
153
GetButtonMenuItemAt(int index) const154 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
155 int index) const {
156 return nullptr;
157 }
158
IsEnabledAt(int index) const159 bool BackForwardMenuModel::IsEnabledAt(int index) const {
160 return index < GetItemCount() && !IsSeparator(index);
161 }
162
GetSubmenuModelAt(int index) const163 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
164 return nullptr;
165 }
166
ActivatedAt(int index)167 void BackForwardMenuModel::ActivatedAt(int index) {
168 ActivatedAt(index, 0);
169 }
170
ActivatedAt(int index,int event_flags)171 void BackForwardMenuModel::ActivatedAt(int index, int event_flags) {
172 DCHECK(!IsSeparator(index));
173
174 // Execute the command for the last item: "Show Full History".
175 if (index == GetItemCount() - 1) {
176 base::RecordComputedAction(BuildActionName("ShowFullHistory", -1));
177 ShowSingletonTabOverwritingNTP(
178 browser_, GetSingletonTabNavigateParams(
179 browser_, GURL(chrome::kChromeUIHistoryURL)));
180 return;
181 }
182
183 // Log whether it was a history or chapter click.
184 int items = GetHistoryItemCount();
185 if (index < items) {
186 base::RecordComputedAction(BuildActionName("HistoryClick", index));
187 } else {
188 const int chapter_index = index - items - 1;
189 base::RecordComputedAction(BuildActionName("ChapterClick", chapter_index));
190 }
191
192 int controller_index = MenuIndexToNavEntryIndex(index);
193
194 UMA_HISTOGRAM_BOOLEAN(
195 "Navigation.BackForward.NavigatingToEntryMarkedToBeSkipped",
196 GetWebContents()->GetController().IsEntryMarkedToBeSkipped(
197 controller_index));
198
199 WindowOpenDisposition disposition =
200 ui::DispositionFromEventFlags(event_flags);
201 chrome::NavigateToIndexWithDisposition(browser_, controller_index,
202 disposition);
203 }
204
MenuWillShow()205 void BackForwardMenuModel::MenuWillShow() {
206 base::RecordComputedAction(BuildActionName("Popup", -1));
207 requested_favicons_.clear();
208 cancelable_task_tracker_.TryCancelAll();
209 }
210
IsSeparator(int index) const211 bool BackForwardMenuModel::IsSeparator(int index) const {
212 int history_items = GetHistoryItemCount();
213 // If the index is past the number of history items + separator,
214 // we then consider if it is a chapter-stop entry.
215 if (index > history_items) {
216 // We either are in ChapterStop area, or at the end of the list (the "Show
217 // Full History" link).
218 int chapter_stops = GetChapterStopCount(history_items);
219 if (chapter_stops == 0)
220 return false; // We must have reached the "Show Full History" link.
221 // Otherwise, look to see if we have reached the separator for the
222 // chapter-stops. If not, this is a chapter stop.
223 return (index == history_items + 1 + chapter_stops);
224 }
225
226 // Look to see if we have reached the separator for the history items.
227 return index == history_items;
228 }
229
FetchFavicon(NavigationEntry * entry)230 void BackForwardMenuModel::FetchFavicon(NavigationEntry* entry) {
231 // If the favicon has already been requested for this menu, don't do
232 // anything.
233 if (base::Contains(requested_favicons_, entry->GetUniqueID()))
234 return;
235
236 requested_favicons_.insert(entry->GetUniqueID());
237 favicon::FaviconService* favicon_service =
238 FaviconServiceFactory::GetForProfile(browser_->profile(),
239 ServiceAccessType::EXPLICIT_ACCESS);
240 if (!favicon_service)
241 return;
242
243 favicon_service->GetFaviconImageForPageURL(
244 entry->GetURL(),
245 base::BindOnce(&BackForwardMenuModel::OnFavIconDataAvailable,
246 base::Unretained(this), entry->GetUniqueID()),
247 &cancelable_task_tracker_);
248 }
249
OnFavIconDataAvailable(int navigation_entry_unique_id,const favicon_base::FaviconImageResult & image_result)250 void BackForwardMenuModel::OnFavIconDataAvailable(
251 int navigation_entry_unique_id,
252 const favicon_base::FaviconImageResult& image_result) {
253 if (image_result.image.IsEmpty())
254 return;
255
256 // Find the current model_index for the unique id.
257 NavigationEntry* entry = nullptr;
258 int model_index = -1;
259 for (int i = 0; i < GetItemCount() - 1; i++) {
260 if (IsSeparator(i))
261 continue;
262 if (GetNavigationEntry(i)->GetUniqueID() == navigation_entry_unique_id) {
263 model_index = i;
264 entry = GetNavigationEntry(i);
265 break;
266 }
267 }
268
269 if (!entry) {
270 // The NavigationEntry wasn't found, this can happen if the user
271 // navigates to another page and a NavigatationEntry falls out of the
272 // range of kMaxHistoryItems.
273 return;
274 }
275
276 // Now that we have a valid NavigationEntry, decode the favicon and assign
277 // it to the NavigationEntry.
278 entry->GetFavicon().valid = true;
279 entry->GetFavicon().url = image_result.icon_url;
280 entry->GetFavicon().image = image_result.image;
281 if (menu_model_delegate()) {
282 menu_model_delegate()->OnIconChanged(model_index);
283 }
284 }
285
GetHistoryItemCount() const286 int BackForwardMenuModel::GetHistoryItemCount() const {
287 WebContents* contents = GetWebContents();
288
289 int items = contents->GetController().GetCurrentEntryIndex();
290 if (model_type_ == ModelType::kForward) {
291 // Only count items from n+1 to end (if n is current entry)
292 items = contents->GetController().GetEntryCount() - items - 1;
293 }
294
295 return base::ClampToRange(items, 0, kMaxHistoryItems);
296 }
297
GetChapterStopCount(int history_items) const298 int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
299 if (history_items != kMaxHistoryItems)
300 return 0;
301
302 WebContents* contents = GetWebContents();
303 int current_entry = contents->GetController().GetCurrentEntryIndex();
304
305 const bool forward = model_type_ == ModelType::kForward;
306 int chapter_id = current_entry;
307 if (forward)
308 chapter_id += history_items;
309 else
310 chapter_id -= history_items;
311
312 int chapter_stops = 0;
313 do {
314 chapter_id = GetIndexOfNextChapterStop(chapter_id, forward);
315 if (chapter_id == -1)
316 break;
317 ++chapter_stops;
318 } while (chapter_stops < kMaxChapterStops);
319
320 return chapter_stops;
321 }
322
GetIndexOfNextChapterStop(int start_from,bool forward) const323 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
324 bool forward) const {
325 if (start_from < 0)
326 return -1; // Out of bounds.
327
328 // We want to advance over the current chapter stop, so we add one.
329 // We don't need to do this when direction is backwards.
330 if (forward)
331 start_from++;
332
333 NavigationController& controller = GetWebContents()->GetController();
334 const int max_count = controller.GetEntryCount();
335 if (start_from >= max_count)
336 return -1; // Out of bounds.
337
338 NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
339 const GURL& url = start_entry->GetURL();
340
341 auto same_domain_func = [&controller, &url](int i) {
342 return net::registry_controlled_domains::SameDomainOrHost(
343 url, controller.GetEntryAtIndex(i)->GetURL(),
344 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES);
345 };
346
347 if (forward) {
348 // When going forwards we return the entry before the entry that has a
349 // different domain.
350 for (int i = start_from + 1; i < max_count; ++i) {
351 if (!same_domain_func(i))
352 return i - 1;
353 }
354 // Last entry is always considered a chapter stop.
355 return max_count - 1;
356 }
357
358 // When going backwards we return the first entry we find that has a
359 // different domain.
360 for (int i = start_from - 1; i >= 0; --i) {
361 if (!same_domain_func(i))
362 return i;
363 }
364 // We have reached the beginning without finding a chapter stop.
365 return -1;
366 }
367
FindChapterStop(int offset,bool forward,int skip) const368 int BackForwardMenuModel::FindChapterStop(int offset,
369 bool forward,
370 int skip) const {
371 if (offset < 0 || skip < 0)
372 return -1;
373
374 if (!forward)
375 offset *= -1;
376
377 WebContents* contents = GetWebContents();
378 int entry = contents->GetController().GetCurrentEntryIndex() + offset;
379 for (int i = 0; i < skip + 1; i++)
380 entry = GetIndexOfNextChapterStop(entry, forward);
381
382 return entry;
383 }
384
ItemHasCommand(int index) const385 bool BackForwardMenuModel::ItemHasCommand(int index) const {
386 return index < GetItemCount() && !IsSeparator(index);
387 }
388
ItemHasIcon(int index) const389 bool BackForwardMenuModel::ItemHasIcon(int index) const {
390 return index < GetItemCount() && !IsSeparator(index);
391 }
392
GetShowFullHistoryLabel() const393 base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
394 return l10n_util::GetStringUTF16(IDS_HISTORY_SHOWFULLHISTORY_LINK);
395 }
396
GetWebContents() const397 WebContents* BackForwardMenuModel::GetWebContents() const {
398 // We use the test web contents if the unit test has specified it.
399 return test_web_contents_ ?
400 test_web_contents_ :
401 browser_->tab_strip_model()->GetActiveWebContents();
402 }
403
MenuIndexToNavEntryIndex(int index) const404 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
405 WebContents* contents = GetWebContents();
406 int history_items = GetHistoryItemCount();
407
408 DCHECK_GE(index, 0);
409
410 // Convert anything above the History items separator.
411 if (index < history_items) {
412 if (model_type_ == ModelType::kForward) {
413 index += contents->GetController().GetCurrentEntryIndex() + 1;
414 } else {
415 // Back menu is reverse.
416 index = contents->GetController().GetCurrentEntryIndex() - (index + 1);
417 }
418 return index;
419 }
420 if (index == history_items)
421 return -1; // Don't translate the separator for history items.
422
423 if (index >= history_items + 1 + GetChapterStopCount(history_items))
424 return -1; // This is beyond the last chapter stop so we abort.
425
426 // This menu item is a chapter stop located between the two separators.
427 return FindChapterStop(history_items, model_type_ == ModelType::kForward,
428 index - history_items - 1);
429 }
430
GetNavigationEntry(int index) const431 NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
432 int controller_index = MenuIndexToNavEntryIndex(index);
433 NavigationController& controller = GetWebContents()->GetController();
434
435 DCHECK_GE(controller_index, 0);
436 DCHECK_LT(controller_index, controller.GetEntryCount());
437
438 return controller.GetEntryAtIndex(controller_index);
439 }
440
BuildActionName(const std::string & action,int index) const441 std::string BackForwardMenuModel::BuildActionName(
442 const std::string& action, int index) const {
443 DCHECK(!action.empty());
444 DCHECK_GE(index, -1);
445 std::string metric_string;
446 if (model_type_ == ModelType::kForward)
447 metric_string += "ForwardMenu_";
448 else
449 metric_string += "BackMenu_";
450 metric_string += action;
451 if (index != -1) {
452 // +1 is for historical reasons (indices used to start at 1).
453 metric_string += base::NumberToString(index + 1);
454 }
455 return metric_string;
456 }
457