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
27 const int BackForwardMenuModel::kMaxHistoryItems = 12;
28 const int BackForwardMenuModel::kMaxChapterStops = 5;
29 static const int kMaxWidth = 700;
30
BackForwardMenuModel(Browser * browser,ModelType model_type)31 BackForwardMenuModel::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
~BackForwardMenuModel()39 BackForwardMenuModel::~BackForwardMenuModel() {
40 }
41
HasIcons() const42 bool BackForwardMenuModel::HasIcons() const {
43 return true;
44 }
45
GetItemCount() const46 int 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
GetTypeAt(int index) const67 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const {
68 return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND;
69 }
70
GetCommandIdAt(int index) const71 int BackForwardMenuModel::GetCommandIdAt(int index) const {
72 return index;
73 }
74
GetLabelAt(int index) const75 string16 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
IsItemDynamicAt(int index) const102 bool BackForwardMenuModel::IsItemDynamicAt(int index) const {
103 // This object is only used for a single showing of a menu.
104 return false;
105 }
106
GetAcceleratorAt(int index,ui::Accelerator * accelerator) const107 bool BackForwardMenuModel::GetAcceleratorAt(
108 int index,
109 ui::Accelerator* accelerator) const {
110 return false;
111 }
112
IsItemCheckedAt(int index) const113 bool BackForwardMenuModel::IsItemCheckedAt(int index) const {
114 return false;
115 }
116
GetGroupIdAt(int index) const117 int BackForwardMenuModel::GetGroupIdAt(int index) const {
118 return false;
119 }
120
GetIconAt(int index,SkBitmap * icon)121 bool 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
GetButtonMenuItemAt(int index) const139 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt(
140 int index) const {
141 return NULL;
142 }
143
IsEnabledAt(int index) const144 bool BackForwardMenuModel::IsEnabledAt(int index) const {
145 return index < GetItemCount() && !IsSeparator(index);
146 }
147
GetSubmenuModelAt(int index) const148 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const {
149 return NULL;
150 }
151
HighlightChangedTo(int index)152 void BackForwardMenuModel::HighlightChangedTo(int index) {
153 }
154
ActivatedAt(int index)155 void BackForwardMenuModel::ActivatedAt(int index) {
156 ActivatedAtWithDisposition(index, CURRENT_TAB);
157 }
158
ActivatedAtWithDisposition(int index,int disposition)159 void 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
MenuWillShow()190 void BackForwardMenuModel::MenuWillShow() {
191 UserMetrics::RecordComputedAction(BuildActionName("Popup", -1),
192 browser_->profile());
193 requested_favicons_.clear();
194 load_consumer_.CancelAllRequests();
195 }
196
IsSeparator(int index) const197 bool 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
SetMenuModelDelegate(ui::MenuModelDelegate * menu_model_delegate)216 void BackForwardMenuModel::SetMenuModelDelegate(
217 ui::MenuModelDelegate* menu_model_delegate) {
218 menu_model_delegate_ = menu_model_delegate;
219 }
220
FetchFavicon(NavigationEntry * entry)221 void 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
OnFavIconDataAvailable(FaviconService::Handle handle,history::FaviconData favicon)239 void 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
GetHistoryItemCount() const281 int 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
GetChapterStopCount(int history_items) const301 int 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
GetIndexOfNextChapterStop(int start_from,bool forward) const326 int 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
FindChapterStop(int offset,bool forward,int skip) const371 int 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
ItemHasCommand(int index) const388 bool BackForwardMenuModel::ItemHasCommand(int index) const {
389 return index < GetItemCount() && !IsSeparator(index);
390 }
391
ItemHasIcon(int index) const392 bool BackForwardMenuModel::ItemHasIcon(int index) const {
393 return index < GetItemCount() && !IsSeparator(index);
394 }
395
GetShowFullHistoryLabel() const396 string16 BackForwardMenuModel::GetShowFullHistoryLabel() const {
397 return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK);
398 }
399
GetTabContents() const400 TabContents* 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
MenuIndexToNavEntryIndex(int index) const406 int 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
GetNavigationEntry(int index) const436 NavigationEntry* 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
BuildActionName(const std::string & action,int index) const446 std::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