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 "base/bind.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/pref_names.h"
19#include "chrome/common/url_constants.h"
20#include "chrome/grit/generated_resources.h"
21#include "components/favicon_base/favicon_types.h"
22#include "content/public/browser/favicon_status.h"
23#include "content/public/browser/navigation_controller.h"
24#include "content/public/browser/navigation_entry.h"
25#include "content/public/browser/user_metrics.h"
26#include "content/public/browser/web_contents.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/text_elider.h"
33
34using base::UserMetricsAction;
35using content::NavigationController;
36using content::NavigationEntry;
37using content::WebContents;
38
39const int BackForwardMenuModel::kMaxHistoryItems = 12;
40const int BackForwardMenuModel::kMaxChapterStops = 5;
41static const int kMaxWidth = 700;
42
43BackForwardMenuModel::BackForwardMenuModel(Browser* browser,
44                                           ModelType model_type)
45    : browser_(browser),
46      test_web_contents_(NULL),
47      model_type_(model_type),
48      menu_model_delegate_(NULL) {
49}
50
51BackForwardMenuModel::~BackForwardMenuModel() {
52}
53
54bool BackForwardMenuModel::HasIcons() const {
55  return true;
56}
57
58int BackForwardMenuModel::GetItemCount() const {
59  int items = GetHistoryItemCount();
60
61  if (items > 0) {
62    int chapter_stops = 0;
63
64    // Next, we count ChapterStops, if any.
65    if (items == kMaxHistoryItems)
66      chapter_stops = GetChapterStopCount(items);
67
68    if (chapter_stops)
69      items += chapter_stops + 1;  // Chapter stops also need a separator.
70
71    // If the menu is not empty, add two positions in the end
72    // for a separator and a "Show Full History" item.
73    items += 2;
74  }
75
76  return items;
77}
78
79ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
80  return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
81}
82
83ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
84    int index) const {
85  return ui::NORMAL_SEPARATOR;
86}
87
88int BackForwardMenuModel::GetCommandIdAt(int index) const {
89  return index;
90}
91
92base::string16 BackForwardMenuModel::GetLabelAt(int index) const {
93  // Return label "Show Full History" for the last item of the menu.
94  if (index == GetItemCount() - 1)
95    return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
96
97  // Return an empty string for a separator.
98  if (IsSeparator(index))
99    return base::string16();
100
101  // Return the entry title, escaping any '&' characters and eliding it if it's
102  // super long.
103  NavigationEntry* entry = GetNavigationEntry(index);
104  Profile* profile =
105      Profile::FromBrowserContext(GetWebContents()->GetBrowserContext());
106  base::string16 menu_text(entry->GetTitleForDisplay(
107      profile->GetPrefs()->GetString(prefs::kAcceptLanguages)));
108  menu_text =
109      gfx::ElideText(menu_text, gfx::FontList(), kMaxWidth, gfx::ELIDE_TAIL);
110
111#if !defined(OS_MACOSX)
112  for (size_t i = menu_text.find('&'); i != base::string16::npos;
113       i = menu_text.find('&', i + 2)) {
114    menu_text.insert(i, 1, '&');
115  }
116#endif
117
118  return menu_text;
119}
120
121bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
122  // This object is only used for a single showing of a menu.
123  return false;
124}
125
126bool BackForwardMenuModel::GetAcceleratorAt(
127    int index,
128    ui::Accelerator* accelerator) const {
129  return false;
130}
131
132bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
133  return false;
134}
135
136int BackForwardMenuModel::GetGroupIdAt(int index) const {
137  return false;
138}
139
140bool BackForwardMenuModel::GetIconAt(int index, gfx::Image* icon) {
141  if (!ItemHasIcon(index))
142    return false;
143
144  if (index == GetItemCount() - 1) {
145    *icon = ResourceBundle::GetSharedInstance().GetNativeImageNamed(
146        IDR_HISTORY_FAVICON);
147  } else {
148    NavigationEntry* entry = GetNavigationEntry(index);
149    *icon = entry->GetFavicon().image;
150    if (!entry->GetFavicon().valid && menu_model_delegate()) {
151      FetchFavicon(entry);
152    }
153  }
154
155  return true;
156}
157
158ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
159    int index) const {
160  return NULL;
161}
162
163bool BackForwardMenuModel::IsEnabledAt(int index) const {
164  return index < GetItemCount() && !IsSeparator(index);
165}
166
167ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
168  return NULL;
169}
170
171void BackForwardMenuModel::HighlightChangedTo(int index) {
172}
173
174void BackForwardMenuModel::ActivatedAt(int index) {
175  ActivatedAt(index, 0);
176}
177
178void BackForwardMenuModel::ActivatedAt(int index, int event_flags) {
179  DCHECK(!IsSeparator(index));
180
181  // Execute the command for the last item: "Show Full History".
182  if (index == GetItemCount() - 1) {
183    content::RecordComputedAction(BuildActionName("ShowFullHistory", -1));
184    chrome::ShowSingletonTabOverwritingNTP(browser_,
185        chrome::GetSingletonTabNavigateParams(
186            browser_, GURL(chrome::kChromeUIHistoryURL)));
187    return;
188  }
189
190  // Log whether it was a history or chapter click.
191  if (index < GetHistoryItemCount()) {
192    content::RecordComputedAction(
193        BuildActionName("HistoryClick", index));
194  } else {
195    content::RecordComputedAction(
196        BuildActionName("ChapterClick", index - GetHistoryItemCount() - 1));
197  }
198
199  int controller_index = MenuIndexToNavEntryIndex(index);
200  WindowOpenDisposition disposition =
201      ui::DispositionFromEventFlags(event_flags);
202  if (!chrome::NavigateToIndexWithDisposition(browser_,
203                                              controller_index,
204                                              disposition)) {
205    NOTREACHED();
206  }
207}
208
209void BackForwardMenuModel::MenuWillShow() {
210  content::RecordComputedAction(BuildActionName("Popup", -1));
211  requested_favicons_.clear();
212  cancelable_task_tracker_.TryCancelAll();
213}
214
215bool BackForwardMenuModel::IsSeparator(int index) const {
216  int history_items = GetHistoryItemCount();
217  // If the index is past the number of history items + separator,
218  // we then consider if it is a chapter-stop entry.
219  if (index > history_items) {
220    // We either are in ChapterStop area, or at the end of the list (the "Show
221    // Full History" link).
222    int chapter_stops = GetChapterStopCount(history_items);
223    if (chapter_stops == 0)
224      return false;  // We must have reached the "Show Full History" link.
225    // Otherwise, look to see if we have reached the separator for the
226    // chapter-stops. If not, this is a chapter stop.
227    return (index == history_items + 1 + chapter_stops);
228  }
229
230  // Look to see if we have reached the separator for the history items.
231  return index == history_items;
232}
233
234void BackForwardMenuModel::SetMenuModelDelegate(
235      ui::MenuModelDelegate* menu_model_delegate) {
236  menu_model_delegate_ = menu_model_delegate;
237}
238
239ui::MenuModelDelegate* BackForwardMenuModel::GetMenuModelDelegate() const {
240  return menu_model_delegate_;
241}
242
243void BackForwardMenuModel::FetchFavicon(NavigationEntry* entry) {
244  // If the favicon has already been requested for this menu, don't do
245  // anything.
246  if (requested_favicons_.find(entry->GetUniqueID()) !=
247      requested_favicons_.end()) {
248    return;
249  }
250  requested_favicons_.insert(entry->GetUniqueID());
251  FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
252      browser_->profile(), Profile::EXPLICIT_ACCESS);
253  if (!favicon_service)
254    return;
255
256  favicon_service->GetFaviconImageForPageURL(
257      entry->GetURL(),
258      base::Bind(&BackForwardMenuModel::OnFavIconDataAvailable,
259                 base::Unretained(this),
260                 entry->GetUniqueID()),
261      &cancelable_task_tracker_);
262}
263
264void BackForwardMenuModel::OnFavIconDataAvailable(
265    int navigation_entry_unique_id,
266    const favicon_base::FaviconImageResult& image_result) {
267  if (!image_result.image.IsEmpty()) {
268    // Find the current model_index for the unique id.
269    NavigationEntry* entry = NULL;
270    int model_index = -1;
271    for (int i = 0; i < GetItemCount() - 1; i++) {
272      if (IsSeparator(i))
273        continue;
274      if (GetNavigationEntry(i)->GetUniqueID() == navigation_entry_unique_id) {
275        model_index = i;
276        entry = GetNavigationEntry(i);
277        break;
278      }
279    }
280
281    if (!entry)
282      // The NavigationEntry wasn't found, this can happen if the user
283      // navigates to another page and a NavigatationEntry falls out of the
284      // range of kMaxHistoryItems.
285      return;
286
287    // Now that we have a valid NavigationEntry, decode the favicon and assign
288    // it to the NavigationEntry.
289    entry->GetFavicon().valid = true;
290    entry->GetFavicon().url = image_result.icon_url;
291    entry->GetFavicon().image = image_result.image;
292    if (menu_model_delegate()) {
293      menu_model_delegate()->OnIconChanged(model_index);
294    }
295  }
296}
297
298int BackForwardMenuModel::GetHistoryItemCount() const {
299  WebContents* contents = GetWebContents();
300  int items = 0;
301
302  if (model_type_ == FORWARD_MENU) {
303    // Only count items from n+1 to end (if n is current entry)
304    items = contents->GetController().GetEntryCount() -
305            contents->GetController().GetCurrentEntryIndex() - 1;
306  } else {
307    items = contents->GetController().GetCurrentEntryIndex();
308  }
309
310  if (items > kMaxHistoryItems)
311    items = kMaxHistoryItems;
312  else if (items < 0)
313    items = 0;
314
315  return items;
316}
317
318int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
319  WebContents* contents = GetWebContents();
320
321  int chapter_stops = 0;
322  int current_entry = contents->GetController().GetCurrentEntryIndex();
323
324  if (history_items == kMaxHistoryItems) {
325    int chapter_id = current_entry;
326    if (model_type_ == FORWARD_MENU) {
327      chapter_id += history_items;
328    } else {
329      chapter_id -= history_items;
330    }
331
332    do {
333      chapter_id = GetIndexOfNextChapterStop(chapter_id,
334          model_type_ == FORWARD_MENU);
335      if (chapter_id != -1)
336        ++chapter_stops;
337    } while (chapter_id != -1 && chapter_stops < kMaxChapterStops);
338  }
339
340  return chapter_stops;
341}
342
343int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
344                                                    bool forward) const {
345  WebContents* contents = GetWebContents();
346  NavigationController& controller = contents->GetController();
347
348  int max_count = controller.GetEntryCount();
349  if (start_from < 0 || start_from >= max_count)
350    return -1;  // Out of bounds.
351
352  if (forward) {
353    if (start_from < max_count - 1) {
354      // We want to advance over the current chapter stop, so we add one.
355      // We don't need to do this when direction is backwards.
356      start_from++;
357    } else {
358      return -1;
359    }
360  }
361
362  NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
363  const GURL& url = start_entry->GetURL();
364
365  if (!forward) {
366    // When going backwards we return the first entry we find that has a
367    // different domain.
368    for (int i = start_from - 1; i >= 0; --i) {
369      if (!net::registry_controlled_domains::SameDomainOrHost(url,
370              controller.GetEntryAtIndex(i)->GetURL(),
371              net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))
372        return i;
373    }
374    // We have reached the beginning without finding a chapter stop.
375    return -1;
376  } else {
377    // When going forwards we return the entry before the entry that has a
378    // different domain.
379    for (int i = start_from + 1; i < max_count; ++i) {
380      if (!net::registry_controlled_domains::SameDomainOrHost(url,
381              controller.GetEntryAtIndex(i)->GetURL(),
382              net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))
383        return i - 1;
384    }
385    // Last entry is always considered a chapter stop.
386    return max_count - 1;
387  }
388}
389
390int BackForwardMenuModel::FindChapterStop(int offset,
391                                          bool forward,
392                                          int skip) const {
393  if (offset < 0 || skip < 0)
394    return -1;
395
396  if (!forward)
397    offset *= -1;
398
399  WebContents* contents = GetWebContents();
400  int entry = contents->GetController().GetCurrentEntryIndex() + offset;
401  for (int i = 0; i < skip + 1; i++)
402    entry = GetIndexOfNextChapterStop(entry, forward);
403
404  return entry;
405}
406
407bool BackForwardMenuModel::ItemHasCommand(int index) const {
408  return index < GetItemCount() && !IsSeparator(index);
409}
410
411bool BackForwardMenuModel::ItemHasIcon(int index) const {
412  return index < GetItemCount() && !IsSeparator(index);
413}
414
415base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
416  return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
417}
418
419WebContents* BackForwardMenuModel::GetWebContents() const {
420  // We use the test web contents if the unit test has specified it.
421  return test_web_contents_ ?
422      test_web_contents_ :
423      browser_->tab_strip_model()->GetActiveWebContents();
424}
425
426int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
427  WebContents* contents = GetWebContents();
428  int history_items = GetHistoryItemCount();
429
430  DCHECK_GE(index, 0);
431
432  // Convert anything above the History items separator.
433  if (index < history_items) {
434    if (model_type_ == FORWARD_MENU) {
435      index += contents->GetController().GetCurrentEntryIndex() + 1;
436    } else {
437      // Back menu is reverse.
438      index = contents->GetController().GetCurrentEntryIndex() - (index + 1);
439    }
440    return index;
441  }
442  if (index == history_items)
443    return -1;  // Don't translate the separator for history items.
444
445  if (index >= history_items + 1 + GetChapterStopCount(history_items))
446    return -1;  // This is beyond the last chapter stop so we abort.
447
448  // This menu item is a chapter stop located between the two separators.
449  index = FindChapterStop(history_items,
450                          model_type_ == FORWARD_MENU,
451                          index - history_items - 1);
452
453  return index;
454}
455
456NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
457  int controller_index = MenuIndexToNavEntryIndex(index);
458  NavigationController& controller = GetWebContents()->GetController();
459  if (controller_index >= 0 && controller_index < controller.GetEntryCount())
460    return controller.GetEntryAtIndex(controller_index);
461
462  NOTREACHED();
463  return NULL;
464}
465
466std::string BackForwardMenuModel::BuildActionName(
467    const std::string& action, int index) const {
468  DCHECK(!action.empty());
469  DCHECK(index >= -1);
470  std::string metric_string;
471  if (model_type_ == FORWARD_MENU)
472    metric_string += "ForwardMenu_";
473  else
474    metric_string += "BackMenu_";
475  metric_string += action;
476  if (index != -1) {
477    // +1 is for historical reasons (indices used to start at 1).
478    metric_string += base::IntToString(index + 1);
479  }
480  return metric_string;
481}
482