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