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 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 39 BackForwardMenuModel::~BackForwardMenuModel() { 40 } 41 42 bool BackForwardMenuModel::HasIcons() const { 43 return true; 44 } 45 46 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 67 ui::MenuModel::ItemType BackForwardMenuModel::GetTypeAt(int index) const { 68 return IsSeparator(index) ? TYPE_SEPARATOR : TYPE_COMMAND; 69 } 70 71 int BackForwardMenuModel::GetCommandIdAt(int index) const { 72 return index; 73 } 74 75 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 102 bool BackForwardMenuModel::IsItemDynamicAt(int index) const { 103 // This object is only used for a single showing of a menu. 104 return false; 105 } 106 107 bool BackForwardMenuModel::GetAcceleratorAt( 108 int index, 109 ui::Accelerator* accelerator) const { 110 return false; 111 } 112 113 bool BackForwardMenuModel::IsItemCheckedAt(int index) const { 114 return false; 115 } 116 117 int BackForwardMenuModel::GetGroupIdAt(int index) const { 118 return false; 119 } 120 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 139 ui::ButtonMenuItemModel* BackForwardMenuModel::GetButtonMenuItemAt( 140 int index) const { 141 return NULL; 142 } 143 144 bool BackForwardMenuModel::IsEnabledAt(int index) const { 145 return index < GetItemCount() && !IsSeparator(index); 146 } 147 148 ui::MenuModel* BackForwardMenuModel::GetSubmenuModelAt(int index) const { 149 return NULL; 150 } 151 152 void BackForwardMenuModel::HighlightChangedTo(int index) { 153 } 154 155 void BackForwardMenuModel::ActivatedAt(int index) { 156 ActivatedAtWithDisposition(index, CURRENT_TAB); 157 } 158 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 190 void BackForwardMenuModel::MenuWillShow() { 191 UserMetrics::RecordComputedAction(BuildActionName("Popup", -1), 192 browser_->profile()); 193 requested_favicons_.clear(); 194 load_consumer_.CancelAllRequests(); 195 } 196 197 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 216 void BackForwardMenuModel::SetMenuModelDelegate( 217 ui::MenuModelDelegate* menu_model_delegate) { 218 menu_model_delegate_ = menu_model_delegate; 219 } 220 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 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 281 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 301 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 326 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 371 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 388 bool BackForwardMenuModel::ItemHasCommand(int index) const { 389 return index < GetItemCount() && !IsSeparator(index); 390 } 391 392 bool BackForwardMenuModel::ItemHasIcon(int index) const { 393 return index < GetItemCount() && !IsSeparator(index); 394 } 395 396 string16 BackForwardMenuModel::GetShowFullHistoryLabel() const { 397 return l10n_util::GetStringUTF16(IDS_SHOWFULLHISTORY_LINK); 398 } 399 400 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 406 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 436 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 446 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