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
46using base::UserMetricsAction;
47using content::WebContents;
48
49namespace {
50
51TabRendererData::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
60bool 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.
74std::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
88class 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
182BrowserTabStripController::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
198BrowserTabStripController::~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
208void 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
219bool 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
227void 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
235bool BrowserTabStripController::IsTabPinned(Tab* tab) const {
236  return IsTabPinned(tabstrip_->GetModelIndexOfTab(tab));
237}
238
239const ui::ListSelectionModel& BrowserTabStripController::GetSelectionModel() {
240  return model_->selection_model();
241}
242
243int BrowserTabStripController::GetCount() const {
244  return model_->count();
245}
246
247bool BrowserTabStripController::IsValidIndex(int index) const {
248  return model_->ContainsIndex(index);
249}
250
251bool BrowserTabStripController::IsActiveTab(int model_index) const {
252  return model_->active_index() == model_index;
253}
254
255int BrowserTabStripController::GetActiveIndex() const {
256  return model_->active_index();
257}
258
259bool BrowserTabStripController::IsTabSelected(int model_index) const {
260  return model_->IsTabSelected(model_index);
261}
262
263bool BrowserTabStripController::IsTabPinned(int model_index) const {
264  return model_->ContainsIndex(model_index) && model_->IsTabPinned(model_index);
265}
266
267bool 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
276void BrowserTabStripController::SelectTab(int model_index) {
277  model_->ActivateTabAt(model_index, true);
278}
279
280void BrowserTabStripController::ExtendSelectionTo(int model_index) {
281  model_->ExtendSelectionTo(model_index);
282}
283
284void BrowserTabStripController::ToggleSelected(int model_index) {
285  model_->ToggleSelectionAt(model_index);
286}
287
288void BrowserTabStripController::AddSelectionFromAnchorTo(int model_index) {
289  model_->AddSelectionFromAnchorTo(model_index);
290}
291
292void 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
303void BrowserTabStripController::ToggleTabAudioMute(int model_index) {
304  content::WebContents* const contents = model_->GetWebContentsAt(model_index);
305  chrome::SetTabAudioMuted(contents, !chrome::IsTabAudioMuted(contents));
306}
307
308void 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
316void 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
329int BrowserTabStripController::HasAvailableDragActions() const {
330  return model_->delegate()->GetDragActions();
331}
332
333void 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
344void 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(&params);
360}
361
362bool BrowserTabStripController::IsCompatibleWith(TabStrip* other) const {
363  Profile* other_profile =
364      static_cast<BrowserTabStripController*>(other->controller())->profile();
365  return other_profile == profile();
366}
367
368void BrowserTabStripController::CreateNewTab() {
369  model_->delegate()->AddTabAt(GURL(), -1, true);
370}
371
372void 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
383bool BrowserTabStripController::IsIncognito() {
384  return browser_->profile()->IsOffTheRecord();
385}
386
387void 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
400void 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
413void BrowserTabStripController::OnStoppedDraggingTabs() {
414  immersive_reveal_lock_.reset();
415}
416
417void 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
430void 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
438void 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
446void BrowserTabStripController::TabSelectionChanged(
447    TabStripModel* tab_strip_model,
448    const ui::ListSelectionModel& old_model) {
449  tabstrip_->SetSelection(old_model, model_->selection_model());
450}
451
452void 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
464void 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
476void 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
483void BrowserTabStripController::TabPinnedStateChanged(WebContents* contents,
484                                                      int model_index) {
485  // Currently none of the renderers render pinned state differently.
486}
487
488void BrowserTabStripController::TabMiniStateChanged(WebContents* contents,
489                                                    int model_index) {
490  SetTabDataAt(contents, model_index);
491}
492
493void BrowserTabStripController::TabBlockedStateChanged(WebContents* contents,
494                                                       int model_index) {
495  SetTabDataAt(contents, model_index);
496}
497
498void 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
520void 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
527void 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
544void 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
554void 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
565void 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
575void 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