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