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/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" 34 35 using base::UserMetricsAction; 36 using content::NavigationController; 37 using content::NavigationEntry; 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 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); 97 98 // Return an empty string for a separator. 99 if (IsSeparator(index)) 100 return base::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 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_TAIL); 111 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, '&'); 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->GetFaviconImageForPageURL( 258 FaviconService::FaviconForPageURLParams( 259 entry->GetURL(), favicon_base::FAVICON, gfx::kFaviconSize), 260 base::Bind(&BackForwardMenuModel::OnFavIconDataAvailable, 261 base::Unretained(this), 262 entry->GetUniqueID()), 263 &cancelable_task_tracker_); 264 } 265 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; 280 } 281 } 282 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; 288 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); 296 } 297 } 298 } 299 300 int BackForwardMenuModel::GetHistoryItemCount() const { 301 WebContents* contents = GetWebContents(); 302 int items = 0; 303 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(); 310 } 311 312 if (items > kMaxHistoryItems) 313 items = kMaxHistoryItems; 314 else if (items < 0) 315 items = 0; 316 317 return items; 318 } 319 320 int BackForwardMenuModel::GetChapterStopCount(int history_items) const { 321 WebContents* contents = GetWebContents(); 322 323 int chapter_stops = 0; 324 int current_entry = contents->GetController().GetCurrentEntryIndex(); 325 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; 332 } 333 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); 340 } 341 342 return chapter_stops; 343 } 344 345 int BackForwardMenuModel::GetIndexOfNextChapterStop(int start_from, 346 bool forward) const { 347 WebContents* contents = GetWebContents(); 348 NavigationController& controller = contents->GetController(); 349 350 int max_count = controller.GetEntryCount(); 351 if (start_from < 0 || start_from >= max_count) 352 return -1; // Out of bounds. 353 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; 361 } 362 } 363 364 NavigationEntry* start_entry = controller.GetEntryAtIndex(start_from); 365 const GURL& url = start_entry->GetURL(); 366 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; 375 } 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; 386 } 387 // Last entry is always considered a chapter stop. 388 return max_count - 1; 389 } 390 } 391 392 int BackForwardMenuModel::FindChapterStop(int offset, 393 bool forward, 394 int skip) const { 395 if (offset < 0 || skip < 0) 396 return -1; 397 398 if (!forward) 399 offset *= -1; 400 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); 405 406 return entry; 407 } 408 409 bool BackForwardMenuModel::ItemHasCommand(int index) const { 410 return index < GetItemCount() && !IsSeparator(index); 411 } 412 413 bool BackForwardMenuModel::ItemHasIcon(int index) const { 414 return index < GetItemCount() && !IsSeparator(index); 415 } 416 417 base::string16 BackForwardMenuModel::GetShowFullHistoryLabel() const { 418 return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK); 419 } 420 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(); 426 } 427 428 int BackForwardMenuModel::MenuIndexToNavEntryIndex(int index) const { 429 WebContents* contents = GetWebContents(); 430 int history_items = GetHistoryItemCount(); 431 432 DCHECK_GE(index, 0); 433 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); 441 } 442 return index; 443 } 444 if (index == history_items) 445 return -1; // Don't translate the separator for history items. 446 447 if (index >= history_items + 1 + GetChapterStopCount(history_items)) 448 return -1; // This is beyond the last chapter stop so we abort. 449 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); 454 455 return index; 456 } 457 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); 463 464 NOTREACHED(); 465 return NULL; 466 } 467 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); 481 } 482 return metric_string; 483 } 484