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