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