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/views/tabs/browser_tab_strip_controller.h" 6 7 #include "base/auto_reset.h" 8 #include "base/prefs/pref_service.h" 9 #include "base/task_runner_util.h" 10 #include "base/threading/sequenced_worker_pool.h" 11 #include "chrome/browser/autocomplete/autocomplete_classifier.h" 12 #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" 13 #include "chrome/browser/browser_process.h" 14 #include "chrome/browser/chrome_notification_types.h" 15 #include "chrome/browser/extensions/tab_helper.h" 16 #include "chrome/browser/favicon/favicon_tab_helper.h" 17 #include "chrome/browser/profiles/profile.h" 18 #include "chrome/browser/search/search.h" 19 #include "chrome/browser/ui/browser.h" 20 #include "chrome/browser/ui/browser_tabstrip.h" 21 #include "chrome/browser/ui/tabs/tab_menu_model.h" 22 #include "chrome/browser/ui/tabs/tab_strip_model.h" 23 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" 24 #include "chrome/browser/ui/tabs/tab_utils.h" 25 #include "chrome/browser/ui/views/frame/browser_view.h" 26 #include "chrome/browser/ui/views/tabs/tab.h" 27 #include "chrome/browser/ui/views/tabs/tab_renderer_data.h" 28 #include "chrome/browser/ui/views/tabs/tab_strip.h" 29 #include "chrome/common/pref_names.h" 30 #include "chrome/common/url_constants.h" 31 #include "components/metrics/proto/omnibox_event.pb.h" 32 #include "components/omnibox/autocomplete_match.h" 33 #include "content/public/browser/browser_thread.h" 34 #include "content/public/browser/notification_service.h" 35 #include "content/public/browser/plugin_service.h" 36 #include "content/public/browser/user_metrics.h" 37 #include "content/public/browser/web_contents.h" 38 #include "content/public/common/webplugininfo.h" 39 #include "ipc/ipc_message.h" 40 #include "net/base/filename_util.h" 41 #include "ui/base/models/list_selection_model.h" 42 #include "ui/gfx/image/image.h" 43 #include "ui/views/controls/menu/menu_runner.h" 44 #include "ui/views/widget/widget.h" 45 46 using base::UserMetricsAction; 47 using content::WebContents; 48 49 namespace { 50 51 TabRendererData::NetworkState TabContentsNetworkState( 52 WebContents* contents) { 53 if (!contents || !contents->IsLoadingToDifferentDocument()) 54 return TabRendererData::NETWORK_STATE_NONE; 55 if (contents->IsWaitingForResponse()) 56 return TabRendererData::NETWORK_STATE_WAITING; 57 return TabRendererData::NETWORK_STATE_LOADING; 58 } 59 60 bool DetermineTabStripLayoutStacked( 61 PrefService* prefs, 62 chrome::HostDesktopType host_desktop_type, 63 bool* adjust_layout) { 64 *adjust_layout = false; 65 // For ash, always allow entering stacked mode. 66 if (host_desktop_type != chrome::HOST_DESKTOP_TYPE_ASH) 67 return false; 68 *adjust_layout = true; 69 return prefs->GetBoolean(prefs::kTabStripStackedLayout); 70 } 71 72 // Get the MIME type of the file pointed to by the url, based on the file's 73 // extension. Must be called on a thread that allows IO. 74 std::string FindURLMimeType(const GURL& url) { 75 DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); 76 base::FilePath full_path; 77 net::FileURLToFilePath(url, &full_path); 78 79 // Get the MIME type based on the filename. 80 std::string mime_type; 81 net::GetMimeTypeFromFile(full_path, &mime_type); 82 83 return mime_type; 84 } 85 86 } // namespace 87 88 class BrowserTabStripController::TabContextMenuContents 89 : public ui::SimpleMenuModel::Delegate { 90 public: 91 TabContextMenuContents(Tab* tab, 92 BrowserTabStripController* controller) 93 : tab_(tab), 94 controller_(controller), 95 last_command_(TabStripModel::CommandFirst) { 96 model_.reset(new TabMenuModel( 97 this, controller->model_, 98 controller->tabstrip_->GetModelIndexOfTab(tab))); 99 menu_runner_.reset(new views::MenuRunner( 100 model_.get(), 101 views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU)); 102 } 103 104 virtual ~TabContextMenuContents() { 105 if (controller_) 106 controller_->tabstrip_->StopAllHighlighting(); 107 } 108 109 void Cancel() { 110 controller_ = NULL; 111 } 112 113 void RunMenuAt(const gfx::Point& point, ui::MenuSourceType source_type) { 114 if (menu_runner_->RunMenuAt(tab_->GetWidget(), 115 NULL, 116 gfx::Rect(point, gfx::Size()), 117 views::MENU_ANCHOR_TOPLEFT, 118 source_type) == 119 views::MenuRunner::MENU_DELETED) { 120 return; 121 } 122 } 123 124 // Overridden from ui::SimpleMenuModel::Delegate: 125 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE { 126 return false; 127 } 128 virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE { 129 return controller_->IsCommandEnabledForTab( 130 static_cast<TabStripModel::ContextMenuCommand>(command_id), 131 tab_); 132 } 133 virtual bool GetAcceleratorForCommandId( 134 int command_id, 135 ui::Accelerator* accelerator) OVERRIDE { 136 int browser_cmd; 137 return TabStripModel::ContextMenuCommandToBrowserCommand(command_id, 138 &browser_cmd) ? 139 controller_->tabstrip_->GetWidget()->GetAccelerator(browser_cmd, 140 accelerator) : 141 false; 142 } 143 virtual void CommandIdHighlighted(int command_id) OVERRIDE { 144 controller_->StopHighlightTabsForCommand(last_command_, tab_); 145 last_command_ = static_cast<TabStripModel::ContextMenuCommand>(command_id); 146 controller_->StartHighlightTabsForCommand(last_command_, tab_); 147 } 148 virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE { 149 // Executing the command destroys |this|, and can also end up destroying 150 // |controller_|. So stop the highlights before executing the command. 151 controller_->tabstrip_->StopAllHighlighting(); 152 controller_->ExecuteCommandForTab( 153 static_cast<TabStripModel::ContextMenuCommand>(command_id), 154 tab_); 155 } 156 157 virtual void MenuClosed(ui::SimpleMenuModel* /*source*/) OVERRIDE { 158 if (controller_) 159 controller_->tabstrip_->StopAllHighlighting(); 160 } 161 162 private: 163 scoped_ptr<TabMenuModel> model_; 164 scoped_ptr<views::MenuRunner> menu_runner_; 165 166 // The tab we're showing a menu for. 167 Tab* tab_; 168 169 // A pointer back to our hosting controller, for command state information. 170 BrowserTabStripController* controller_; 171 172 // The last command that was selected, so that we can start/stop highlighting 173 // appropriately as the user moves through the menu. 174 TabStripModel::ContextMenuCommand last_command_; 175 176 DISALLOW_COPY_AND_ASSIGN(TabContextMenuContents); 177 }; 178 179 //////////////////////////////////////////////////////////////////////////////// 180 // BrowserTabStripController, public: 181 182 BrowserTabStripController::BrowserTabStripController(Browser* browser, 183 TabStripModel* model) 184 : model_(model), 185 tabstrip_(NULL), 186 browser_(browser), 187 hover_tab_selector_(model), 188 weak_ptr_factory_(this) { 189 model_->AddObserver(this); 190 191 local_pref_registrar_.Init(g_browser_process->local_state()); 192 local_pref_registrar_.Add( 193 prefs::kTabStripStackedLayout, 194 base::Bind(&BrowserTabStripController::UpdateStackedLayout, 195 base::Unretained(this))); 196 } 197 198 BrowserTabStripController::~BrowserTabStripController() { 199 // When we get here the TabStrip is being deleted. We need to explicitly 200 // cancel the menu, otherwise it may try to invoke something on the tabstrip 201 // from its destructor. 202 if (context_menu_contents_.get()) 203 context_menu_contents_->Cancel(); 204 205 model_->RemoveObserver(this); 206 } 207 208 void BrowserTabStripController::InitFromModel(TabStrip* tabstrip) { 209 tabstrip_ = tabstrip; 210 211 UpdateStackedLayout(); 212 213 // Walk the model, calling our insertion observer method for each item within 214 // it. 215 for (int i = 0; i < model_->count(); ++i) 216 AddTab(model_->GetWebContentsAt(i), i, model_->active_index() == i); 217 } 218 219 bool BrowserTabStripController::IsCommandEnabledForTab( 220 TabStripModel::ContextMenuCommand command_id, 221 Tab* tab) const { 222 int model_index = tabstrip_->GetModelIndexOfTab(tab); 223 return model_->ContainsIndex(model_index) ? 224 model_->IsContextMenuCommandEnabled(model_index, command_id) : false; 225 } 226 227 void BrowserTabStripController::ExecuteCommandForTab( 228 TabStripModel::ContextMenuCommand command_id, 229 Tab* tab) { 230 int model_index = tabstrip_->GetModelIndexOfTab(tab); 231 if (model_->ContainsIndex(model_index)) 232 model_->ExecuteContextMenuCommand(model_index, command_id); 233 } 234 235 bool BrowserTabStripController::IsTabPinned(Tab* tab) const { 236 return IsTabPinned(tabstrip_->GetModelIndexOfTab(tab)); 237 } 238 239 const ui::ListSelectionModel& BrowserTabStripController::GetSelectionModel() { 240 return model_->selection_model(); 241 } 242 243 int BrowserTabStripController::GetCount() const { 244 return model_->count(); 245 } 246 247 bool BrowserTabStripController::IsValidIndex(int index) const { 248 return model_->ContainsIndex(index); 249 } 250 251 bool BrowserTabStripController::IsActiveTab(int model_index) const { 252 return model_->active_index() == model_index; 253 } 254 255 int BrowserTabStripController::GetActiveIndex() const { 256 return model_->active_index(); 257 } 258 259 bool BrowserTabStripController::IsTabSelected(int model_index) const { 260 return model_->IsTabSelected(model_index); 261 } 262 263 bool BrowserTabStripController::IsTabPinned(int model_index) const { 264 return model_->ContainsIndex(model_index) && model_->IsTabPinned(model_index); 265 } 266 267 bool BrowserTabStripController::IsNewTabPage(int model_index) const { 268 if (!model_->ContainsIndex(model_index)) 269 return false; 270 271 const WebContents* contents = model_->GetWebContentsAt(model_index); 272 return contents && (contents->GetURL() == GURL(chrome::kChromeUINewTabURL) || 273 chrome::IsInstantNTP(contents)); 274 } 275 276 void BrowserTabStripController::SelectTab(int model_index) { 277 model_->ActivateTabAt(model_index, true); 278 } 279 280 void BrowserTabStripController::ExtendSelectionTo(int model_index) { 281 model_->ExtendSelectionTo(model_index); 282 } 283 284 void BrowserTabStripController::ToggleSelected(int model_index) { 285 model_->ToggleSelectionAt(model_index); 286 } 287 288 void BrowserTabStripController::AddSelectionFromAnchorTo(int model_index) { 289 model_->AddSelectionFromAnchorTo(model_index); 290 } 291 292 void BrowserTabStripController::CloseTab(int model_index, 293 CloseTabSource source) { 294 // Cancel any pending tab transition. 295 hover_tab_selector_.CancelTabTransition(); 296 297 tabstrip_->PrepareForCloseAt(model_index, source); 298 model_->CloseWebContentsAt(model_index, 299 TabStripModel::CLOSE_USER_GESTURE | 300 TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); 301 } 302 303 void BrowserTabStripController::ToggleTabAudioMute(int model_index) { 304 content::WebContents* const contents = model_->GetWebContentsAt(model_index); 305 chrome::SetTabAudioMuted(contents, !chrome::IsTabAudioMuted(contents)); 306 } 307 308 void BrowserTabStripController::ShowContextMenuForTab( 309 Tab* tab, 310 const gfx::Point& p, 311 ui::MenuSourceType source_type) { 312 context_menu_contents_.reset(new TabContextMenuContents(tab, this)); 313 context_menu_contents_->RunMenuAt(p, source_type); 314 } 315 316 void BrowserTabStripController::UpdateLoadingAnimations() { 317 // Don't use the model count here as it's possible for this to be invoked 318 // before we've applied an update from the model (Browser::TabInsertedAt may 319 // be processed before us and invokes this). 320 for (int i = 0, tab_count = tabstrip_->tab_count(); i < tab_count; ++i) { 321 if (model_->ContainsIndex(i)) { 322 Tab* tab = tabstrip_->tab_at(i); 323 WebContents* contents = model_->GetWebContentsAt(i); 324 tab->UpdateLoadingAnimation(TabContentsNetworkState(contents)); 325 } 326 } 327 } 328 329 int BrowserTabStripController::HasAvailableDragActions() const { 330 return model_->delegate()->GetDragActions(); 331 } 332 333 void BrowserTabStripController::OnDropIndexUpdate(int index, 334 bool drop_before) { 335 // Perform a delayed tab transition if hovering directly over a tab. 336 // Otherwise, cancel the pending one. 337 if (index != -1 && !drop_before) { 338 hover_tab_selector_.StartTabTransition(index); 339 } else { 340 hover_tab_selector_.CancelTabTransition(); 341 } 342 } 343 344 void BrowserTabStripController::PerformDrop(bool drop_before, 345 int index, 346 const GURL& url) { 347 chrome::NavigateParams params(browser_, url, ui::PAGE_TRANSITION_LINK); 348 params.tabstrip_index = index; 349 350 if (drop_before) { 351 content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs")); 352 params.disposition = NEW_FOREGROUND_TAB; 353 } else { 354 content::RecordAction(UserMetricsAction("Tab_DropURLOnTab")); 355 params.disposition = CURRENT_TAB; 356 params.source_contents = model_->GetWebContentsAt(index); 357 } 358 params.window_action = chrome::NavigateParams::SHOW_WINDOW; 359 chrome::Navigate(¶ms); 360 } 361 362 bool BrowserTabStripController::IsCompatibleWith(TabStrip* other) const { 363 Profile* other_profile = 364 static_cast<BrowserTabStripController*>(other->controller())->profile(); 365 return other_profile == profile(); 366 } 367 368 void BrowserTabStripController::CreateNewTab() { 369 model_->delegate()->AddTabAt(GURL(), -1, true); 370 } 371 372 void BrowserTabStripController::CreateNewTabWithLocation( 373 const base::string16& location) { 374 // Use autocomplete to clean up the text, going so far as to turn it into 375 // a search query if necessary. 376 AutocompleteMatch match; 377 AutocompleteClassifierFactory::GetForProfile(profile())->Classify( 378 location, false, false, metrics::OmniboxEventProto::BLANK, &match, NULL); 379 if (match.destination_url.is_valid()) 380 model_->delegate()->AddTabAt(match.destination_url, -1, true); 381 } 382 383 bool BrowserTabStripController::IsIncognito() { 384 return browser_->profile()->IsOffTheRecord(); 385 } 386 387 void BrowserTabStripController::StackedLayoutMaybeChanged() { 388 bool adjust_layout = false; 389 bool stacked_layout = 390 DetermineTabStripLayoutStacked(g_browser_process->local_state(), 391 browser_->host_desktop_type(), 392 &adjust_layout); 393 if (!adjust_layout || stacked_layout == tabstrip_->stacked_layout()) 394 return; 395 396 g_browser_process->local_state()->SetBoolean(prefs::kTabStripStackedLayout, 397 tabstrip_->stacked_layout()); 398 } 399 400 void BrowserTabStripController::OnStartedDraggingTabs() { 401 BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); 402 if (browser_view && !immersive_reveal_lock_.get()) { 403 // The top-of-window views should be revealed while the user is dragging 404 // tabs in immersive fullscreen. The top-of-window views may not be already 405 // revealed if the user is attempting to attach a tab to a tabstrip 406 // belonging to an immersive fullscreen window. 407 immersive_reveal_lock_.reset( 408 browser_view->immersive_mode_controller()->GetRevealedLock( 409 ImmersiveModeController::ANIMATE_REVEAL_NO)); 410 } 411 } 412 413 void BrowserTabStripController::OnStoppedDraggingTabs() { 414 immersive_reveal_lock_.reset(); 415 } 416 417 void BrowserTabStripController::CheckFileSupported(const GURL& url) { 418 base::PostTaskAndReplyWithResult( 419 content::BrowserThread::GetBlockingPool(), 420 FROM_HERE, 421 base::Bind(&FindURLMimeType, url), 422 base::Bind(&BrowserTabStripController::OnFindURLMimeTypeCompleted, 423 weak_ptr_factory_.GetWeakPtr(), 424 url)); 425 } 426 427 //////////////////////////////////////////////////////////////////////////////// 428 // BrowserTabStripController, TabStripModelObserver implementation: 429 430 void BrowserTabStripController::TabInsertedAt(WebContents* contents, 431 int model_index, 432 bool is_active) { 433 DCHECK(contents); 434 DCHECK(model_->ContainsIndex(model_index)); 435 AddTab(contents, model_index, is_active); 436 } 437 438 void BrowserTabStripController::TabDetachedAt(WebContents* contents, 439 int model_index) { 440 // Cancel any pending tab transition. 441 hover_tab_selector_.CancelTabTransition(); 442 443 tabstrip_->RemoveTabAt(model_index); 444 } 445 446 void BrowserTabStripController::TabSelectionChanged( 447 TabStripModel* tab_strip_model, 448 const ui::ListSelectionModel& old_model) { 449 tabstrip_->SetSelection(old_model, model_->selection_model()); 450 } 451 452 void BrowserTabStripController::TabMoved(WebContents* contents, 453 int from_model_index, 454 int to_model_index) { 455 // Cancel any pending tab transition. 456 hover_tab_selector_.CancelTabTransition(); 457 458 // Pass in the TabRendererData as the pinned state may have changed. 459 TabRendererData data; 460 SetTabRendererDataFromModel(contents, to_model_index, &data, EXISTING_TAB); 461 tabstrip_->MoveTab(from_model_index, to_model_index, data); 462 } 463 464 void BrowserTabStripController::TabChangedAt(WebContents* contents, 465 int model_index, 466 TabChangeType change_type) { 467 if (change_type == TITLE_NOT_LOADING) { 468 tabstrip_->TabTitleChangedNotLoading(model_index); 469 // We'll receive another notification of the change asynchronously. 470 return; 471 } 472 473 SetTabDataAt(contents, model_index); 474 } 475 476 void BrowserTabStripController::TabReplacedAt(TabStripModel* tab_strip_model, 477 WebContents* old_contents, 478 WebContents* new_contents, 479 int model_index) { 480 SetTabDataAt(new_contents, model_index); 481 } 482 483 void BrowserTabStripController::TabPinnedStateChanged(WebContents* contents, 484 int model_index) { 485 // Currently none of the renderers render pinned state differently. 486 } 487 488 void BrowserTabStripController::TabMiniStateChanged(WebContents* contents, 489 int model_index) { 490 SetTabDataAt(contents, model_index); 491 } 492 493 void BrowserTabStripController::TabBlockedStateChanged(WebContents* contents, 494 int model_index) { 495 SetTabDataAt(contents, model_index); 496 } 497 498 void BrowserTabStripController::SetTabRendererDataFromModel( 499 WebContents* contents, 500 int model_index, 501 TabRendererData* data, 502 TabStatus tab_status) { 503 FaviconTabHelper* favicon_tab_helper = 504 FaviconTabHelper::FromWebContents(contents); 505 506 data->favicon = favicon_tab_helper->GetFavicon().AsImageSkia(); 507 data->network_state = TabContentsNetworkState(contents); 508 data->title = contents->GetTitle(); 509 data->url = contents->GetURL(); 510 data->loading = contents->IsLoading(); 511 data->crashed_status = contents->GetCrashedStatus(); 512 data->incognito = contents->GetBrowserContext()->IsOffTheRecord(); 513 data->mini = model_->IsMiniTab(model_index); 514 data->show_icon = data->mini || favicon_tab_helper->ShouldDisplayFavicon(); 515 data->blocked = model_->IsTabBlocked(model_index); 516 data->app = extensions::TabHelper::FromWebContents(contents)->is_app(); 517 data->media_state = chrome::GetTabMediaStateForContents(contents); 518 } 519 520 void BrowserTabStripController::SetTabDataAt(content::WebContents* web_contents, 521 int model_index) { 522 TabRendererData data; 523 SetTabRendererDataFromModel(web_contents, model_index, &data, EXISTING_TAB); 524 tabstrip_->SetTabData(model_index, data); 525 } 526 527 void BrowserTabStripController::StartHighlightTabsForCommand( 528 TabStripModel::ContextMenuCommand command_id, 529 Tab* tab) { 530 if (command_id == TabStripModel::CommandCloseOtherTabs || 531 command_id == TabStripModel::CommandCloseTabsToRight) { 532 int model_index = tabstrip_->GetModelIndexOfTab(tab); 533 if (IsValidIndex(model_index)) { 534 std::vector<int> indices = 535 model_->GetIndicesClosedByCommand(model_index, command_id); 536 for (std::vector<int>::const_iterator i(indices.begin()); 537 i != indices.end(); ++i) { 538 tabstrip_->StartHighlight(*i); 539 } 540 } 541 } 542 } 543 544 void BrowserTabStripController::StopHighlightTabsForCommand( 545 TabStripModel::ContextMenuCommand command_id, 546 Tab* tab) { 547 if (command_id == TabStripModel::CommandCloseTabsToRight || 548 command_id == TabStripModel::CommandCloseOtherTabs) { 549 // Just tell all Tabs to stop pulsing - it's safe. 550 tabstrip_->StopAllHighlighting(); 551 } 552 } 553 554 void BrowserTabStripController::AddTab(WebContents* contents, 555 int index, 556 bool is_active) { 557 // Cancel any pending tab transition. 558 hover_tab_selector_.CancelTabTransition(); 559 560 TabRendererData data; 561 SetTabRendererDataFromModel(contents, index, &data, NEW_TAB); 562 tabstrip_->AddTabAt(index, data, is_active); 563 } 564 565 void BrowserTabStripController::UpdateStackedLayout() { 566 bool adjust_layout = false; 567 bool stacked_layout = 568 DetermineTabStripLayoutStacked(g_browser_process->local_state(), 569 browser_->host_desktop_type(), 570 &adjust_layout); 571 tabstrip_->set_adjust_layout(adjust_layout); 572 tabstrip_->SetStackedLayout(stacked_layout); 573 } 574 575 void BrowserTabStripController::OnFindURLMimeTypeCompleted( 576 const GURL& url, 577 const std::string& mime_type) { 578 // Check whether the mime type, if given, is known to be supported or whether 579 // there is a plugin that supports the mime type (e.g. PDF). 580 // TODO(bauerb): This possibly uses stale information, but it's guaranteed not 581 // to do disk access. 582 content::WebPluginInfo plugin; 583 tabstrip_->FileSupported( 584 url, 585 mime_type.empty() || 586 net::IsSupportedMimeType(mime_type) || 587 content::PluginService::GetInstance()->GetPluginInfo( 588 -1, // process ID 589 MSG_ROUTING_NONE, // routing ID 590 model_->profile()->GetResourceContext(), 591 url, GURL(), mime_type, false, 592 NULL, &plugin, NULL)); 593 } 594