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