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