Refactor views app list services to allow more code sharing
[chromium-blink-merge.git] / chrome / browser / ui / toolbar / back_forward_menu_model.cc
blob2c8ec53d2e3b8b9cec7adcb9ac4676b3d150daf1
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"
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 "components/favicon_base/favicon_types.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,
45 ModelType model_type)
46 : 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 {
56 return true;
59 int BackForwardMenuModel::GetItemCount() const {
60 int items = GetHistoryItemCount();
62 if (items > 0) {
63 int chapter_stops = 0;
65 // Next, we count ChapterStops, if any.
66 if (items == kMaxHistoryItems)
67 chapter_stops = GetChapterStopCount(items);
69 if (chapter_stops)
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.
74 items += 2;
77 return items;
80 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
81 return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
84 ui::MenuSeparatorType BackForwardMenuModel::GetSeparatorTypeAt(
85 int index) const {
86 return ui::NORMAL_SEPARATOR;
89 int BackForwardMenuModel::GetCommandIdAt(int index) const {
90 return index;
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
103 // super long.
104 NavigationEntry* entry = GetNavigationEntry(index);
105 Profile* profile =
106 Profile::FromBrowserContext(GetWebContents()->GetBrowserContext());
107 base::string16 menu_text(entry->GetTitleForDisplay(
108 profile->GetPrefs()->GetString(prefs::kAcceptLanguages)));
109 menu_text =
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, '&');
117 #endif
119 return menu_text;
122 bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
123 // This object is only used for a single showing of a menu.
124 return false;
127 bool BackForwardMenuModel::GetAcceleratorAt(
128 int index,
129 ui::Accelerator* accelerator) const {
130 return false;
133 bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
134 return false;
137 int BackForwardMenuModel::GetGroupIdAt(int index) const {
138 return false;
141 bool BackForwardMenuModel::GetIconAt(int index, gfx::Image* icon) {
142 if (!ItemHasIcon(index))
143 return false;
145 if (index == GetItemCount() - 1) {
146 *icon = ResourceBundle::GetSharedInstance().GetNativeImageNamed(
147 IDR_HISTORY_FAVICON);
148 } else {
149 NavigationEntry* entry = GetNavigationEntry(index);
150 *icon = entry->GetFavicon().image;
151 if (!entry->GetFavicon().valid && menu_model_delegate()) {
152 FetchFavicon(entry);
156 return true;
159 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
160 int index) const {
161 return NULL;
164 bool BackForwardMenuModel::IsEnabledAt(int index) const {
165 return index < GetItemCount() && !IsSeparator(index);
168 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
169 return NULL;
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)));
188 return;
191 // Log whether it was a history or chapter click.
192 if (index < GetHistoryItemCount()) {
193 content::RecordComputedAction(
194 BuildActionName("HistoryClick", index));
195 } else {
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_,
204 controller_index,
205 disposition)) {
206 NOTREACHED();
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
246 // anything.
247 if (requested_favicons_.find(entry->GetUniqueID()) !=
248 requested_favicons_.end()) {
249 return;
251 requested_favicons_.insert(entry->GetUniqueID());
252 FaviconService* favicon_service = FaviconServiceFactory::GetForProfile(
253 browser_->profile(), Profile::EXPLICIT_ACCESS);
254 if (!favicon_service)
255 return;
257 favicon_service->GetFaviconImageForURL(
258 FaviconService::FaviconForURLParams(
259 entry->GetURL(), favicon_base::FAVICON, gfx::kFaviconSize),
260 base::Bind(&BackForwardMenuModel::OnFavIconDataAvailable,
261 base::Unretained(this),
262 entry->GetUniqueID()),
263 &cancelable_task_tracker_);
266 void BackForwardMenuModel::OnFavIconDataAvailable(
267 int navigation_entry_unique_id,
268 const favicon_base::FaviconImageResult& image_result) {
269 if (!image_result.image.IsEmpty()) {
270 // Find the current model_index for the unique id.
271 NavigationEntry* entry = NULL;
272 int model_index = -1;
273 for (int i = 0; i < GetItemCount() - 1; i++) {
274 if (IsSeparator(i))
275 continue;
276 if (GetNavigationEntry(i)->GetUniqueID() == navigation_entry_unique_id) {
277 model_index = i;
278 entry = GetNavigationEntry(i);
279 break;
283 if (!entry)
284 // The NavigationEntry wasn't found, this can happen if the user
285 // navigates to another page and a NavigatationEntry falls out of the
286 // range of kMaxHistoryItems.
287 return;
289 // Now that we have a valid NavigationEntry, decode the favicon and assign
290 // it to the NavigationEntry.
291 entry->GetFavicon().valid = true;
292 entry->GetFavicon().url = image_result.icon_url;
293 entry->GetFavicon().image = image_result.image;
294 if (menu_model_delegate()) {
295 menu_model_delegate()->OnIconChanged(model_index);
300 int BackForwardMenuModel::GetHistoryItemCount() const {
301 WebContents* contents = GetWebContents();
302 int items = 0;
304 if (model_type_ == FORWARD_MENU) {
305 // Only count items from n+1 to end (if n is current entry)
306 items = contents->GetController().GetEntryCount() -
307 contents->GetController().GetCurrentEntryIndex() - 1;
308 } else {
309 items = contents->GetController().GetCurrentEntryIndex();
312 if (items > kMaxHistoryItems)
313 items = kMaxHistoryItems;
314 else if (items < 0)
315 items = 0;
317 return items;
320 int BackForwardMenuModel::GetChapterStopCount(int history_items) const {
321 WebContents* contents = GetWebContents();
323 int chapter_stops = 0;
324 int current_entry = contents->GetController().GetCurrentEntryIndex();
326 if (history_items == kMaxHistoryItems) {
327 int chapter_id = current_entry;
328 if (model_type_ == FORWARD_MENU) {
329 chapter_id += history_items;
330 } else {
331 chapter_id -= history_items;
334 do {
335 chapter_id = GetIndexOfNextChapterStop(chapter_id,
336 model_type_ == FORWARD_MENU);
337 if (chapter_id != -1)
338 ++chapter_stops;
339 } while (chapter_id != -1 && chapter_stops < kMaxChapterStops);
342 return chapter_stops;
345 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from,
346 bool forward) const {
347 WebContents* contents = GetWebContents();
348 NavigationController& controller = contents->GetController();
350 int max_count = controller.GetEntryCount();
351 if (start_from < 0 || start_from >= max_count)
352 return -1; // Out of bounds.
354 if (forward) {
355 if (start_from < max_count - 1) {
356 // We want to advance over the current chapter stop, so we add one.
357 // We don't need to do this when direction is backwards.
358 start_from++;
359 } else {
360 return -1;
364 NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from);
365 const GURL& url = start_entry->GetURL();
367 if (!forward) {
368 // When going backwards we return the first entry we find that has a
369 // different domain.
370 for (int i = start_from - 1; i >= 0; --i) {
371 if (!net::registry_controlled_domains::SameDomainOrHost(url,
372 controller.GetEntryAtIndex(i)->GetURL(),
373 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))
374 return i;
376 // We have reached the beginning without finding a chapter stop.
377 return -1;
378 } else {
379 // When going forwards we return the entry before the entry that has a
380 // different domain.
381 for (int i = start_from + 1; i < max_count; ++i) {
382 if (!net::registry_controlled_domains::SameDomainOrHost(url,
383 controller.GetEntryAtIndex(i)->GetURL(),
384 net::registry_controlled_domains::EXCLUDE_PRIVATE_REGISTRIES))
385 return i - 1;
387 // Last entry is always considered a chapter stop.
388 return max_count - 1;
392 int BackForwardMenuModel::FindChapterStop(int offset,
393 bool forward,
394 int skip) const {
395 if (offset < 0 || skip < 0)
396 return -1;
398 if (!forward)
399 offset *= -1;
401 WebContents* contents = GetWebContents();
402 int entry = contents->GetController().GetCurrentEntryIndex() + offset;
403 for (int i = 0; i < skip + 1; i++)
404 entry = GetIndexOfNextChapterStop(entry, forward);
406 return entry;
409 bool BackForwardMenuModel::ItemHasCommand(int index) const {
410 return index < GetItemCount() && !IsSeparator(index);
413 bool BackForwardMenuModel::ItemHasIcon(int index) const {
414 return index < GetItemCount() && !IsSeparator(index);
417 base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
418 return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
421 WebContents* BackForwardMenuModel::GetWebContents() const {
422 // We use the test web contents if the unit test has specified it.
423 return test_web_contents_ ?
424 test_web_contents_ :
425 browser_->tab_strip_model()->GetActiveWebContents();
428 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const {
429 WebContents* contents = GetWebContents();
430 int history_items = GetHistoryItemCount();
432 DCHECK_GE(index, 0);
434 // Convert anything above the History items separator.
435 if (index < history_items) {
436 if (model_type_ == FORWARD_MENU) {
437 index += contents->GetController().GetCurrentEntryIndex() + 1;
438 } else {
439 // Back menu is reverse.
440 index = contents->GetController().GetCurrentEntryIndex() - (index + 1);
442 return index;
444 if (index == history_items)
445 return -1; // Don't translate the separator for history items.
447 if (index >= history_items + 1 + GetChapterStopCount(history_items))
448 return -1; // This is beyond the last chapter stop so we abort.
450 // This menu item is a chapter stop located between the two separators.
451 index = FindChapterStop(history_items,
452 model_type_ == FORWARD_MENU,
453 index - history_items - 1);
455 return index;
458 NavigationEntry* BackForwardMenuModel::GetNavigationEntry(int index) const {
459 int controller_index = MenuIndexToNavEntryIndex(index);
460 NavigationController& controller = GetWebContents()->GetController();
461 if (controller_index >= 0 && controller_index < controller.GetEntryCount())
462 return controller.GetEntryAtIndex(controller_index);
464 NOTREACHED();
465 return NULL;
468 std::string BackForwardMenuModel::BuildActionName(
469 const std::string& action, int index) const {
470 DCHECK(!action.empty());
471 DCHECK(index >= -1);
472 std::string metric_string;
473 if (model_type_ == FORWARD_MENU)
474 metric_string += "ForwardMenu_";
475 else
476 metric_string += "BackMenu_";
477 metric_string += action;
478 if (index != -1) {
479 // +1 is for historical reasons (indices used to start at 1).
480 metric_string += base::IntToString(index + 1);
482 return metric_string;