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