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