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