// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h" #include "base/auto_reset.h" #include "base/command_line.h" #include "base/prefs/pref_service.h" #include "base/task_runner_util.h" #include "base/threading/sequenced_worker_pool.h" #include "chrome/browser/autocomplete/autocomplete_classifier.h" #include "chrome/browser/autocomplete/autocomplete_classifier_factory.h" #include "chrome/browser/autocomplete/autocomplete_input.h" #include "chrome/browser/autocomplete/autocomplete_match.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/favicon/favicon_tab_helper.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/search/search.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_tabstrip.h" #include "chrome/browser/ui/tabs/tab_menu_model.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h" #include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/tabs/tab.h" #include "chrome/browser/ui/views/tabs/tab_renderer_data.h" #include "chrome/browser/ui/views/tabs/tab_strip.h" #include "chrome/common/pref_names.h" #include "chrome/common/url_constants.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/plugin_service.h" #include "content/public/browser/user_metrics.h" #include "content/public/browser/web_contents.h" #include "content/public/common/webplugininfo.h" #include "ipc/ipc_message.h" #include "net/base/filename_util.h" #include "ui/base/models/list_selection_model.h" #include "ui/gfx/image/image.h" #include "ui/views/controls/menu/menu_runner.h" #include "ui/views/widget/widget.h" using base::UserMetricsAction; using content::WebContents; namespace { TabRendererData::NetworkState TabContentsNetworkState( WebContents* contents) { if (!contents || !contents->IsLoadingToDifferentDocument()) return TabRendererData::NETWORK_STATE_NONE; if (contents->IsWaitingForResponse()) return TabRendererData::NETWORK_STATE_WAITING; return TabRendererData::NETWORK_STATE_LOADING; } bool DetermineTabStripLayoutStacked( PrefService* prefs, chrome::HostDesktopType host_desktop_type, bool* adjust_layout) { *adjust_layout = false; // For ash, always allow entering stacked mode. if (host_desktop_type != chrome::HOST_DESKTOP_TYPE_ASH) return false; *adjust_layout = true; return prefs->GetBoolean(prefs::kTabStripStackedLayout); } // Get the MIME type of the file pointed to by the url, based on the file's // extension. Must be called on a thread that allows IO. std::string FindURLMimeType(const GURL& url) { DCHECK(!content::BrowserThread::CurrentlyOn(content::BrowserThread::UI)); base::FilePath full_path; net::FileURLToFilePath(url, &full_path); // Get the MIME type based on the filename. std::string mime_type; net::GetMimeTypeFromFile(full_path, &mime_type); return mime_type; } } // namespace class BrowserTabStripController::TabContextMenuContents : public ui::SimpleMenuModel::Delegate { public: TabContextMenuContents(Tab* tab, BrowserTabStripController* controller) : tab_(tab), controller_(controller), last_command_(TabStripModel::CommandFirst) { model_.reset(new TabMenuModel( this, controller->model_, controller->tabstrip_->GetModelIndexOfTab(tab))); menu_runner_.reset(new views::MenuRunner(model_.get())); } virtual ~TabContextMenuContents() { if (controller_) controller_->tabstrip_->StopAllHighlighting(); } void Cancel() { controller_ = NULL; } void RunMenuAt(const gfx::Point& point, ui::MenuSourceType source_type) { if (menu_runner_->RunMenuAt(tab_->GetWidget(), NULL, gfx::Rect(point, gfx::Size()), views::MENU_ANCHOR_TOPLEFT, source_type, views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU) == views::MenuRunner::MENU_DELETED) { return; } } // Overridden from ui::SimpleMenuModel::Delegate: virtual bool IsCommandIdChecked(int command_id) const OVERRIDE { return false; } virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE { return controller_->IsCommandEnabledForTab( static_cast<TabStripModel::ContextMenuCommand>(command_id), tab_); } virtual bool GetAcceleratorForCommandId( int command_id, ui::Accelerator* accelerator) OVERRIDE { int browser_cmd; return TabStripModel::ContextMenuCommandToBrowserCommand(command_id, &browser_cmd) ? controller_->tabstrip_->GetWidget()->GetAccelerator(browser_cmd, accelerator) : false; } virtual void CommandIdHighlighted(int command_id) OVERRIDE { controller_->StopHighlightTabsForCommand(last_command_, tab_); last_command_ = static_cast<TabStripModel::ContextMenuCommand>(command_id); controller_->StartHighlightTabsForCommand(last_command_, tab_); } virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE { // Executing the command destroys |this|, and can also end up destroying // |controller_|. So stop the highlights before executing the command. controller_->tabstrip_->StopAllHighlighting(); controller_->ExecuteCommandForTab( static_cast<TabStripModel::ContextMenuCommand>(command_id), tab_); } virtual void MenuClosed(ui::SimpleMenuModel* /*source*/) OVERRIDE { if (controller_) controller_->tabstrip_->StopAllHighlighting(); } private: scoped_ptr<TabMenuModel> model_; scoped_ptr<views::MenuRunner> menu_runner_; // The tab we're showing a menu for. Tab* tab_; // A pointer back to our hosting controller, for command state information. BrowserTabStripController* controller_; // The last command that was selected, so that we can start/stop highlighting // appropriately as the user moves through the menu. TabStripModel::ContextMenuCommand last_command_; DISALLOW_COPY_AND_ASSIGN(TabContextMenuContents); }; //////////////////////////////////////////////////////////////////////////////// // BrowserTabStripController, public: BrowserTabStripController::BrowserTabStripController(Browser* browser, TabStripModel* model) : model_(model), tabstrip_(NULL), browser_(browser), hover_tab_selector_(model), weak_ptr_factory_(this) { model_->AddObserver(this); local_pref_registrar_.Init(g_browser_process->local_state()); local_pref_registrar_.Add( prefs::kTabStripStackedLayout, base::Bind(&BrowserTabStripController::UpdateStackedLayout, base::Unretained(this))); } BrowserTabStripController::~BrowserTabStripController() { // When we get here the TabStrip is being deleted. We need to explicitly // cancel the menu, otherwise it may try to invoke something on the tabstrip // from its destructor. if (context_menu_contents_.get()) context_menu_contents_->Cancel(); model_->RemoveObserver(this); } void BrowserTabStripController::InitFromModel(TabStrip* tabstrip) { tabstrip_ = tabstrip; UpdateStackedLayout(); // Walk the model, calling our insertion observer method for each item within // it. for (int i = 0; i < model_->count(); ++i) AddTab(model_->GetWebContentsAt(i), i, model_->active_index() == i); } bool BrowserTabStripController::IsCommandEnabledForTab( TabStripModel::ContextMenuCommand command_id, Tab* tab) const { int model_index = tabstrip_->GetModelIndexOfTab(tab); return model_->ContainsIndex(model_index) ? model_->IsContextMenuCommandEnabled(model_index, command_id) : false; } void BrowserTabStripController::ExecuteCommandForTab( TabStripModel::ContextMenuCommand command_id, Tab* tab) { int model_index = tabstrip_->GetModelIndexOfTab(tab); if (model_->ContainsIndex(model_index)) model_->ExecuteContextMenuCommand(model_index, command_id); } bool BrowserTabStripController::IsTabPinned(Tab* tab) const { return IsTabPinned(tabstrip_->GetModelIndexOfTab(tab)); } const ui::ListSelectionModel& BrowserTabStripController::GetSelectionModel() { return model_->selection_model(); } int BrowserTabStripController::GetCount() const { return model_->count(); } bool BrowserTabStripController::IsValidIndex(int index) const { return model_->ContainsIndex(index); } bool BrowserTabStripController::IsActiveTab(int model_index) const { return model_->active_index() == model_index; } int BrowserTabStripController::GetActiveIndex() const { return model_->active_index(); } bool BrowserTabStripController::IsTabSelected(int model_index) const { return model_->IsTabSelected(model_index); } bool BrowserTabStripController::IsTabPinned(int model_index) const { return model_->ContainsIndex(model_index) && model_->IsTabPinned(model_index); } bool BrowserTabStripController::IsNewTabPage(int model_index) const { if (!model_->ContainsIndex(model_index)) return false; const WebContents* contents = model_->GetWebContentsAt(model_index); return contents && (contents->GetURL() == GURL(chrome::kChromeUINewTabURL) || chrome::IsInstantNTP(contents)); } void BrowserTabStripController::SelectTab(int model_index) { model_->ActivateTabAt(model_index, true); } void BrowserTabStripController::ExtendSelectionTo(int model_index) { model_->ExtendSelectionTo(model_index); } void BrowserTabStripController::ToggleSelected(int model_index) { model_->ToggleSelectionAt(model_index); } void BrowserTabStripController::AddSelectionFromAnchorTo(int model_index) { model_->AddSelectionFromAnchorTo(model_index); } void BrowserTabStripController::CloseTab(int model_index, CloseTabSource source) { // Cancel any pending tab transition. hover_tab_selector_.CancelTabTransition(); tabstrip_->PrepareForCloseAt(model_index, source); model_->CloseWebContentsAt(model_index, TabStripModel::CLOSE_USER_GESTURE | TabStripModel::CLOSE_CREATE_HISTORICAL_TAB); } void BrowserTabStripController::ShowContextMenuForTab( Tab* tab, const gfx::Point& p, ui::MenuSourceType source_type) { context_menu_contents_.reset(new TabContextMenuContents(tab, this)); context_menu_contents_->RunMenuAt(p, source_type); } void BrowserTabStripController::UpdateLoadingAnimations() { // Don't use the model count here as it's possible for this to be invoked // before we've applied an update from the model (Browser::TabInsertedAt may // be processed before us and invokes this). for (int i = 0, tab_count = tabstrip_->tab_count(); i < tab_count; ++i) { if (model_->ContainsIndex(i)) { Tab* tab = tabstrip_->tab_at(i); WebContents* contents = model_->GetWebContentsAt(i); tab->UpdateLoadingAnimation(TabContentsNetworkState(contents)); } } } int BrowserTabStripController::HasAvailableDragActions() const { return model_->delegate()->GetDragActions(); } void BrowserTabStripController::OnDropIndexUpdate(int index, bool drop_before) { // Perform a delayed tab transition if hovering directly over a tab. // Otherwise, cancel the pending one. if (index != -1 && !drop_before) { hover_tab_selector_.StartTabTransition(index); } else { hover_tab_selector_.CancelTabTransition(); } } void BrowserTabStripController::PerformDrop(bool drop_before, int index, const GURL& url) { chrome::NavigateParams params(browser_, url, content::PAGE_TRANSITION_LINK); params.tabstrip_index = index; if (drop_before) { content::RecordAction(UserMetricsAction("Tab_DropURLBetweenTabs")); params.disposition = NEW_FOREGROUND_TAB; } else { content::RecordAction(UserMetricsAction("Tab_DropURLOnTab")); params.disposition = CURRENT_TAB; params.source_contents = model_->GetWebContentsAt(index); } params.window_action = chrome::NavigateParams::SHOW_WINDOW; chrome::Navigate(¶ms); } bool BrowserTabStripController::IsCompatibleWith(TabStrip* other) const { Profile* other_profile = static_cast<BrowserTabStripController*>(other->controller())->profile(); return other_profile == profile(); } void BrowserTabStripController::CreateNewTab() { model_->delegate()->AddTabAt(GURL(), -1, true); } void BrowserTabStripController::CreateNewTabWithLocation( const base::string16& location) { // Use autocomplete to clean up the text, going so far as to turn it into // a search query if necessary. AutocompleteMatch match; AutocompleteClassifierFactory::GetForProfile(profile())->Classify( location, false, false, AutocompleteInput::BLANK, &match, NULL); if (match.destination_url.is_valid()) model_->delegate()->AddTabAt(match.destination_url, -1, true); } bool BrowserTabStripController::IsIncognito() { return browser_->profile()->IsOffTheRecord(); } void BrowserTabStripController::StackedLayoutMaybeChanged() { bool adjust_layout = false; bool stacked_layout = DetermineTabStripLayoutStacked(g_browser_process->local_state(), browser_->host_desktop_type(), &adjust_layout); if (!adjust_layout || stacked_layout == tabstrip_->stacked_layout()) return; g_browser_process->local_state()->SetBoolean(prefs::kTabStripStackedLayout, tabstrip_->stacked_layout()); } void BrowserTabStripController::OnStartedDraggingTabs() { BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser_); if (browser_view && !immersive_reveal_lock_.get()) { // The top-of-window views should be revealed while the user is dragging // tabs in immersive fullscreen. The top-of-window views may not be already // revealed if the user is attempting to attach a tab to a tabstrip // belonging to an immersive fullscreen window. immersive_reveal_lock_.reset( browser_view->immersive_mode_controller()->GetRevealedLock( ImmersiveModeController::ANIMATE_REVEAL_NO)); } } void BrowserTabStripController::OnStoppedDraggingTabs() { immersive_reveal_lock_.reset(); } void BrowserTabStripController::CheckFileSupported(const GURL& url) { base::PostTaskAndReplyWithResult( content::BrowserThread::GetBlockingPool(), FROM_HERE, base::Bind(&FindURLMimeType, url), base::Bind(&BrowserTabStripController::OnFindURLMimeTypeCompleted, weak_ptr_factory_.GetWeakPtr(), url)); } //////////////////////////////////////////////////////////////////////////////// // BrowserTabStripController, TabStripModelObserver implementation: void BrowserTabStripController::TabInsertedAt(WebContents* contents, int model_index, bool is_active) { DCHECK(contents); DCHECK(model_->ContainsIndex(model_index)); AddTab(contents, model_index, is_active); } void BrowserTabStripController::TabDetachedAt(WebContents* contents, int model_index) { // Cancel any pending tab transition. hover_tab_selector_.CancelTabTransition(); tabstrip_->RemoveTabAt(model_index); } void BrowserTabStripController::TabSelectionChanged( TabStripModel* tab_strip_model, const ui::ListSelectionModel& old_model) { tabstrip_->SetSelection(old_model, model_->selection_model()); } void BrowserTabStripController::TabMoved(WebContents* contents, int from_model_index, int to_model_index) { // Cancel any pending tab transition. hover_tab_selector_.CancelTabTransition(); // Pass in the TabRendererData as the pinned state may have changed. TabRendererData data; SetTabRendererDataFromModel(contents, to_model_index, &data, EXISTING_TAB); tabstrip_->MoveTab(from_model_index, to_model_index, data); } void BrowserTabStripController::TabChangedAt(WebContents* contents, int model_index, TabChangeType change_type) { if (change_type == TITLE_NOT_LOADING) { tabstrip_->TabTitleChangedNotLoading(model_index); // We'll receive another notification of the change asynchronously. return; } SetTabDataAt(contents, model_index); } void BrowserTabStripController::TabReplacedAt(TabStripModel* tab_strip_model, WebContents* old_contents, WebContents* new_contents, int model_index) { SetTabDataAt(new_contents, model_index); } void BrowserTabStripController::TabPinnedStateChanged(WebContents* contents, int model_index) { // Currently none of the renderers render pinned state differently. } void BrowserTabStripController::TabMiniStateChanged(WebContents* contents, int model_index) { SetTabDataAt(contents, model_index); } void BrowserTabStripController::TabBlockedStateChanged(WebContents* contents, int model_index) { SetTabDataAt(contents, model_index); } void BrowserTabStripController::SetTabRendererDataFromModel( WebContents* contents, int model_index, TabRendererData* data, TabStatus tab_status) { FaviconTabHelper* favicon_tab_helper = FaviconTabHelper::FromWebContents(contents); data->favicon = favicon_tab_helper->GetFavicon().AsImageSkia(); data->network_state = TabContentsNetworkState(contents); data->title = contents->GetTitle(); data->url = contents->GetURL(); data->loading = contents->IsLoading(); data->crashed_status = contents->GetCrashedStatus(); data->incognito = contents->GetBrowserContext()->IsOffTheRecord(); data->mini = model_->IsMiniTab(model_index); data->show_icon = data->mini || favicon_tab_helper->ShouldDisplayFavicon(); data->blocked = model_->IsTabBlocked(model_index); data->app = extensions::TabHelper::FromWebContents(contents)->is_app(); data->media_state = chrome::GetTabMediaStateForContents(contents); } void BrowserTabStripController::SetTabDataAt(content::WebContents* web_contents, int model_index) { TabRendererData data; SetTabRendererDataFromModel(web_contents, model_index, &data, EXISTING_TAB); tabstrip_->SetTabData(model_index, data); } void BrowserTabStripController::StartHighlightTabsForCommand( TabStripModel::ContextMenuCommand command_id, Tab* tab) { if (command_id == TabStripModel::CommandCloseOtherTabs || command_id == TabStripModel::CommandCloseTabsToRight) { int model_index = tabstrip_->GetModelIndexOfTab(tab); if (IsValidIndex(model_index)) { std::vector<int> indices = model_->GetIndicesClosedByCommand(model_index, command_id); for (std::vector<int>::const_iterator i(indices.begin()); i != indices.end(); ++i) { tabstrip_->StartHighlight(*i); } } } } void BrowserTabStripController::StopHighlightTabsForCommand( TabStripModel::ContextMenuCommand command_id, Tab* tab) { if (command_id == TabStripModel::CommandCloseTabsToRight || command_id == TabStripModel::CommandCloseOtherTabs) { // Just tell all Tabs to stop pulsing - it's safe. tabstrip_->StopAllHighlighting(); } } void BrowserTabStripController::AddTab(WebContents* contents, int index, bool is_active) { // Cancel any pending tab transition. hover_tab_selector_.CancelTabTransition(); TabRendererData data; SetTabRendererDataFromModel(contents, index, &data, NEW_TAB); tabstrip_->AddTabAt(index, data, is_active); } void BrowserTabStripController::UpdateStackedLayout() { bool adjust_layout = false; bool stacked_layout = DetermineTabStripLayoutStacked(g_browser_process->local_state(), browser_->host_desktop_type(), &adjust_layout); tabstrip_->set_adjust_layout(adjust_layout); tabstrip_->SetStackedLayout(stacked_layout); } void BrowserTabStripController::OnFindURLMimeTypeCompleted( const GURL& url, const std::string& mime_type) { // Check whether the mime type, if given, is known to be supported or whether // there is a plugin that supports the mime type (e.g. PDF). // TODO(bauerb): This possibly uses stale information, but it's guaranteed not // to do disk access. content::WebPluginInfo plugin; tabstrip_->FileSupported( url, mime_type.empty() || net::IsSupportedMimeType(mime_type) || content::PluginService::GetInstance()->GetPluginInfo( -1, // process ID MSG_ROUTING_NONE, // routing ID model_->profile()->GetResourceContext(), url, GURL(), mime_type, false, NULL, &plugin, NULL)); }