1// Copyright (c) 2011 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/base_tab_strip.h"
6
7#include "base/logging.h"
8#include "chrome/browser/ui/view_ids.h"
9#include "chrome/browser/ui/views/tabs/dragged_tab_controller.h"
10#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
11#include "views/widget/root_view.h"
12#include "views/window/window.h"
13
14#if defined(OS_WIN)
15#include "views/widget/widget_win.h"
16#endif
17
18namespace {
19
20// Animation delegate used when a dragged tab is released. When done sets the
21// dragging state to false.
22class ResetDraggingStateDelegate
23    : public views::BoundsAnimator::OwnedAnimationDelegate {
24 public:
25  explicit ResetDraggingStateDelegate(BaseTab* tab) : tab_(tab) {
26  }
27
28  virtual void AnimationEnded(const ui::Animation* animation) {
29    tab_->set_dragging(false);
30  }
31
32  virtual void AnimationCanceled(const ui::Animation* animation) {
33    tab_->set_dragging(false);
34  }
35
36 private:
37  BaseTab* tab_;
38
39  DISALLOW_COPY_AND_ASSIGN(ResetDraggingStateDelegate);
40};
41
42}  // namespace
43
44// AnimationDelegate used when removing a tab. Does the necessary cleanup when
45// done.
46class BaseTabStrip::RemoveTabDelegate
47    : public views::BoundsAnimator::OwnedAnimationDelegate {
48 public:
49  RemoveTabDelegate(BaseTabStrip* tab_strip, BaseTab* tab)
50      : tabstrip_(tab_strip),
51        tab_(tab) {
52  }
53
54  virtual void AnimationEnded(const ui::Animation* animation) {
55    CompleteRemove();
56  }
57
58  virtual void AnimationCanceled(const ui::Animation* animation) {
59    // We can be canceled for two interesting reasons:
60    // . The tab we reference was dragged back into the tab strip. In this case
61    //   we don't want to remove the tab (closing is false).
62    // . The drag was completed before the animation completed
63    //   (DestroyDraggedSourceTab). In this case we need to remove the tab
64    //   (closing is true).
65    if (tab_->closing())
66      CompleteRemove();
67  }
68
69 private:
70  void CompleteRemove() {
71    if (!tab_->closing()) {
72      // The tab was added back yet we weren't canceled. This shouldn't happen.
73      NOTREACHED();
74      return;
75    }
76    tabstrip_->RemoveAndDeleteTab(tab_);
77    HighlightCloseButton();
78  }
79
80  // When the animation completes, we send the Container a message to simulate
81  // a mouse moved event at the current mouse position. This tickles the Tab
82  // the mouse is currently over to show the "hot" state of the close button.
83  void HighlightCloseButton() {
84    if (tabstrip_->IsDragSessionActive() ||
85        !tabstrip_->ShouldHighlightCloseButtonAfterRemove()) {
86      // This function is not required (and indeed may crash!) for removes
87      // spawned by non-mouse closes and drag-detaches.
88      return;
89    }
90
91#if defined(OS_WIN)
92    views::Widget* widget = tabstrip_->GetWidget();
93    // This can be null during shutdown. See http://crbug.com/42737.
94    if (!widget)
95      return;
96    // Force the close button (that slides under the mouse) to highlight by
97    // saying the mouse just moved, but sending the same coordinates.
98    DWORD pos = GetMessagePos();
99    POINT cursor_point = {GET_X_LPARAM(pos), GET_Y_LPARAM(pos)};
100    MapWindowPoints(NULL, widget->GetNativeView(), &cursor_point, 1);
101
102    static_cast<views::WidgetWin*>(widget)->ResetLastMouseMoveFlag();
103    // Return to message loop - otherwise we may disrupt some operation that's
104    // in progress.
105    SendMessage(widget->GetNativeView(), WM_MOUSEMOVE, 0,
106                MAKELPARAM(cursor_point.x, cursor_point.y));
107#else
108    NOTIMPLEMENTED();
109#endif
110  }
111
112  BaseTabStrip* tabstrip_;
113  BaseTab* tab_;
114
115  DISALLOW_COPY_AND_ASSIGN(RemoveTabDelegate);
116};
117
118BaseTabStrip::BaseTabStrip(TabStripController* controller, Type type)
119    : controller_(controller),
120      type_(type),
121      attaching_dragged_tab_(false),
122      ALLOW_THIS_IN_INITIALIZER_LIST(bounds_animator_(this)) {
123}
124
125BaseTabStrip::~BaseTabStrip() {
126}
127
128void BaseTabStrip::AddTabAt(int model_index, const TabRendererData& data) {
129  BaseTab* tab = CreateTab();
130  tab->SetData(data);
131
132  TabData d = { tab, gfx::Rect() };
133  tab_data_.insert(tab_data_.begin() + ModelIndexToTabIndex(model_index), d);
134
135  AddChildView(tab);
136
137  // Don't animate the first tab, it looks weird, and don't animate anything
138  // if the containing window isn't visible yet.
139  if (tab_count() > 1 && GetWindow() && GetWindow()->IsVisible())
140    StartInsertTabAnimation(model_index);
141  else
142    DoLayout();
143}
144
145void BaseTabStrip::MoveTab(int from_model_index, int to_model_index) {
146  int from_tab_data_index = ModelIndexToTabIndex(from_model_index);
147  BaseTab* tab = tab_data_[from_tab_data_index].tab;
148  tab_data_.erase(tab_data_.begin() + from_tab_data_index);
149
150  TabData data = {tab, gfx::Rect()};
151  int to_tab_data_index = ModelIndexToTabIndex(to_model_index);
152  tab_data_.insert(tab_data_.begin() + to_tab_data_index, data);
153
154  StartMoveTabAnimation();
155}
156
157void BaseTabStrip::SetTabData(int model_index, const TabRendererData& data) {
158  BaseTab* tab = GetBaseTabAtModelIndex(model_index);
159  bool mini_state_changed = tab->data().mini != data.mini;
160  tab->SetData(data);
161
162  if (mini_state_changed) {
163    if (GetWindow() && GetWindow()->IsVisible())
164      StartMiniTabAnimation();
165    else
166      DoLayout();
167  }
168}
169
170BaseTab* BaseTabStrip::GetBaseTabAtModelIndex(int model_index) const {
171  return base_tab_at_tab_index(ModelIndexToTabIndex(model_index));
172}
173
174int BaseTabStrip::GetModelIndexOfBaseTab(const BaseTab* tab) const {
175  for (int i = 0, model_index = 0; i < tab_count(); ++i) {
176    BaseTab* current_tab = base_tab_at_tab_index(i);
177    if (!current_tab->closing()) {
178      if (current_tab == tab)
179        return model_index;
180      model_index++;
181    } else if (current_tab == tab) {
182      return -1;
183    }
184  }
185  return -1;
186}
187
188int BaseTabStrip::GetModelCount() const {
189  return controller_->GetCount();
190}
191
192bool BaseTabStrip::IsValidModelIndex(int model_index) const {
193  return controller_->IsValidIndex(model_index);
194}
195
196int BaseTabStrip::ModelIndexToTabIndex(int model_index) const {
197  int current_model_index = 0;
198  for (int i = 0; i < tab_count(); ++i) {
199    if (!base_tab_at_tab_index(i)->closing()) {
200      if (current_model_index == model_index)
201        return i;
202      current_model_index++;
203    }
204  }
205  return static_cast<int>(tab_data_.size());
206}
207
208bool BaseTabStrip::IsDragSessionActive() const {
209  return drag_controller_.get() != NULL;
210}
211
212bool BaseTabStrip::IsActiveDropTarget() const {
213  for (int i = 0; i < tab_count(); ++i) {
214    BaseTab* tab = base_tab_at_tab_index(i);
215    if (tab->dragging())
216      return true;
217  }
218  return false;
219}
220
221bool BaseTabStrip::IsTabStripEditable() const {
222  return !IsDragSessionActive() && !IsActiveDropTarget();
223}
224
225bool BaseTabStrip::IsTabStripCloseable() const {
226  return !IsDragSessionActive();
227}
228
229void BaseTabStrip::UpdateLoadingAnimations() {
230  controller_->UpdateLoadingAnimations();
231}
232
233void BaseTabStrip::SelectTab(BaseTab* tab) {
234  int model_index = GetModelIndexOfBaseTab(tab);
235  if (IsValidModelIndex(model_index))
236    controller_->SelectTab(model_index);
237}
238
239void BaseTabStrip::ExtendSelectionTo(BaseTab* tab) {
240  int model_index = GetModelIndexOfBaseTab(tab);
241  if (IsValidModelIndex(model_index))
242    controller_->ExtendSelectionTo(model_index);
243}
244
245void BaseTabStrip::ToggleSelected(BaseTab* tab) {
246  int model_index = GetModelIndexOfBaseTab(tab);
247  if (IsValidModelIndex(model_index))
248    controller_->ToggleSelected(model_index);
249}
250
251void BaseTabStrip::AddSelectionFromAnchorTo(BaseTab* tab) {
252  int model_index = GetModelIndexOfBaseTab(tab);
253  if (IsValidModelIndex(model_index))
254    controller_->AddSelectionFromAnchorTo(model_index);
255}
256
257void BaseTabStrip::CloseTab(BaseTab* tab) {
258  // Find the closest model index. We do this so that the user can rapdily close
259  // tabs and have the close click close the next tab.
260  int model_index = 0;
261  for (int i = 0; i < tab_count(); ++i) {
262    BaseTab* current_tab = base_tab_at_tab_index(i);
263    if (current_tab == tab)
264      break;
265    if (!current_tab->closing())
266      model_index++;
267  }
268
269  if (IsValidModelIndex(model_index))
270    controller_->CloseTab(model_index);
271}
272
273void BaseTabStrip::ShowContextMenuForTab(BaseTab* tab, const gfx::Point& p) {
274  controller_->ShowContextMenuForTab(tab, p);
275}
276
277bool BaseTabStrip::IsActiveTab(const BaseTab* tab) const {
278  int model_index = GetModelIndexOfBaseTab(tab);
279  return IsValidModelIndex(model_index) &&
280      controller_->IsActiveTab(model_index);
281}
282
283bool BaseTabStrip::IsTabSelected(const BaseTab* tab) const {
284  int model_index = GetModelIndexOfBaseTab(tab);
285  return IsValidModelIndex(model_index) &&
286      controller_->IsTabSelected(model_index);
287}
288
289bool BaseTabStrip::IsTabPinned(const BaseTab* tab) const {
290  if (tab->closing())
291    return false;
292
293  int model_index = GetModelIndexOfBaseTab(tab);
294  return IsValidModelIndex(model_index) &&
295      controller_->IsTabPinned(model_index);
296}
297
298bool BaseTabStrip::IsTabCloseable(const BaseTab* tab) const {
299  int model_index = GetModelIndexOfBaseTab(tab);
300  return !IsValidModelIndex(model_index) ||
301      controller_->IsTabCloseable(model_index);
302}
303
304void BaseTabStrip::MaybeStartDrag(BaseTab* tab,
305                                  const views::MouseEvent& event) {
306  // Don't accidentally start any drag operations during animations if the
307  // mouse is down... during an animation tabs are being resized automatically,
308  // so the View system can misinterpret this easily if the mouse is down that
309  // the user is dragging.
310  if (IsAnimating() || tab->closing() ||
311      controller_->HasAvailableDragActions() == 0) {
312    return;
313  }
314  int model_index = GetModelIndexOfBaseTab(tab);
315  if (!IsValidModelIndex(model_index)) {
316    CHECK(false);
317    return;
318  }
319  drag_controller_.reset(new DraggedTabController());
320  std::vector<BaseTab*> tabs;
321  int size_to_selected = 0;
322  int x = tab->GetMirroredXInView(event.x());
323  int y = event.y();
324  // Build the set of selected tabs to drag and calculate the offset from the
325  // first selected tab.
326  for (int i = 0; i < tab_count(); ++i) {
327    BaseTab* other_tab = base_tab_at_tab_index(i);
328    if (IsTabSelected(other_tab) && !other_tab->closing()) {
329      tabs.push_back(other_tab);
330      if (other_tab == tab) {
331        size_to_selected = GetSizeNeededForTabs(tabs);
332        if (type() == HORIZONTAL_TAB_STRIP)
333          x = size_to_selected - tab->width() + x;
334        else
335          y = size_to_selected - tab->height() + y;
336      }
337    }
338  }
339  DCHECK(!tabs.empty());
340  DCHECK(std::find(tabs.begin(), tabs.end(), tab) != tabs.end());
341  drag_controller_->Init(this, tab, tabs, gfx::Point(x, y),
342                         tab->GetMirroredXInView(event.x()));
343}
344
345void BaseTabStrip::ContinueDrag(const views::MouseEvent& event) {
346  // We can get called even if |MaybeStartDrag| wasn't called in the event of
347  // a TabStrip animation when the mouse button is down. In this case we should
348  // _not_ continue the drag because it can lead to weird bugs.
349  if (drag_controller_.get()) {
350    bool started_drag = drag_controller_->started_drag();
351    drag_controller_->Drag();
352    if (drag_controller_->started_drag() && !started_drag) {
353      // The drag just started. Redirect mouse events to us to that the tab that
354      // originated the drag can be safely deleted.
355      GetRootView()->SetMouseHandler(this);
356    }
357  }
358}
359
360bool BaseTabStrip::EndDrag(bool canceled) {
361  if (!drag_controller_.get())
362    return false;
363  bool started_drag = drag_controller_->started_drag();
364  drag_controller_->EndDrag(canceled);
365  return started_drag;
366}
367
368BaseTab* BaseTabStrip::GetTabAt(BaseTab* tab,
369                                const gfx::Point& tab_in_tab_coordinates) {
370  gfx::Point local_point = tab_in_tab_coordinates;
371  ConvertPointToView(tab, this, &local_point);
372  return GetTabAtLocal(local_point);
373}
374
375void BaseTabStrip::Layout() {
376  // Only do a layout if our size changed.
377  if (last_layout_size_ == size())
378    return;
379  DoLayout();
380}
381
382bool BaseTabStrip::OnMouseDragged(const views::MouseEvent&  event) {
383  if (drag_controller_.get())
384    drag_controller_->Drag();
385  return true;
386}
387
388void BaseTabStrip::OnMouseReleased(const views::MouseEvent& event) {
389  EndDrag(false);
390}
391
392void BaseTabStrip::OnMouseCaptureLost() {
393  EndDrag(true);
394}
395
396void BaseTabStrip::StartMoveTabAnimation() {
397  PrepareForAnimation();
398  GenerateIdealBounds();
399  AnimateToIdealBounds();
400}
401
402void BaseTabStrip::StartRemoveTabAnimation(int model_index) {
403  PrepareForAnimation();
404
405  // Mark the tab as closing.
406  BaseTab* tab = GetBaseTabAtModelIndex(model_index);
407  tab->set_closing(true);
408
409  // Start an animation for the tabs.
410  GenerateIdealBounds();
411  AnimateToIdealBounds();
412
413  // Animate the tab being closed to 0x0.
414  gfx::Rect tab_bounds = tab->bounds();
415  if (type() == HORIZONTAL_TAB_STRIP)
416    tab_bounds.set_width(0);
417  else
418    tab_bounds.set_height(0);
419  bounds_animator_.AnimateViewTo(tab, tab_bounds);
420
421  // Register delegate to do cleanup when done, BoundsAnimator takes
422  // ownership of RemoveTabDelegate.
423  bounds_animator_.SetAnimationDelegate(tab, new RemoveTabDelegate(this, tab),
424                                        true);
425}
426
427void BaseTabStrip::StartMiniTabAnimation() {
428  PrepareForAnimation();
429
430  GenerateIdealBounds();
431  AnimateToIdealBounds();
432}
433
434bool BaseTabStrip::ShouldHighlightCloseButtonAfterRemove() {
435  return true;
436}
437
438void BaseTabStrip::RemoveAndDeleteTab(BaseTab* tab) {
439  int tab_data_index = TabIndexOfTab(tab);
440
441  DCHECK(tab_data_index != -1);
442
443  // Remove the Tab from the TabStrip's list...
444  tab_data_.erase(tab_data_.begin() + tab_data_index);
445
446  delete tab;
447}
448
449int BaseTabStrip::TabIndexOfTab(BaseTab* tab) const {
450  for (int i = 0; i < tab_count(); ++i) {
451    if (base_tab_at_tab_index(i) == tab)
452      return i;
453  }
454  return -1;
455}
456
457void BaseTabStrip::StopAnimating(bool layout) {
458  if (!IsAnimating())
459    return;
460
461  bounds_animator().Cancel();
462
463  if (layout)
464    DoLayout();
465}
466
467void BaseTabStrip::DestroyDragController() {
468  if (IsDragSessionActive())
469    drag_controller_.reset(NULL);
470}
471
472void BaseTabStrip::StartedDraggingTabs(const std::vector<BaseTab*>& tabs) {
473  PrepareForAnimation();
474
475  // Reset dragging state of existing tabs.
476  for (int i = 0; i < tab_count(); ++i)
477    base_tab_at_tab_index(i)->set_dragging(false);
478
479  for (size_t i = 0; i < tabs.size(); ++i) {
480    tabs[i]->set_dragging(true);
481    bounds_animator_.StopAnimatingView(tabs[i]);
482  }
483
484  // Move the dragged tabs to their ideal bounds.
485  GenerateIdealBounds();
486
487  // Sets the bounds of the dragged tabs.
488  for (size_t i = 0; i < tabs.size(); ++i) {
489    int tab_data_index = TabIndexOfTab(tabs[i]);
490    DCHECK(tab_data_index != -1);
491    tabs[i]->SetBoundsRect(ideal_bounds(tab_data_index));
492  }
493  SchedulePaint();
494}
495
496void BaseTabStrip::StoppedDraggingTabs(const std::vector<BaseTab*>& tabs) {
497  bool is_first_tab = true;
498  for (size_t i = 0; i < tabs.size(); ++i)
499    StoppedDraggingTab(tabs[i], &is_first_tab);
500}
501
502void BaseTabStrip::PrepareForAnimation() {
503  if (!IsDragSessionActive() && !DraggedTabController::IsAttachedTo(this)) {
504    for (int i = 0; i < tab_count(); ++i)
505      base_tab_at_tab_index(i)->set_dragging(false);
506  }
507}
508
509ui::AnimationDelegate* BaseTabStrip::CreateRemoveTabDelegate(BaseTab* tab) {
510  return new RemoveTabDelegate(this, tab);
511}
512
513void BaseTabStrip::DoLayout() {
514  last_layout_size_ = size();
515
516  StopAnimating(false);
517
518  GenerateIdealBounds();
519
520  for (int i = 0; i < tab_count(); ++i)
521    tab_data_[i].tab->SetBoundsRect(tab_data_[i].ideal_bounds);
522
523  SchedulePaint();
524}
525
526bool BaseTabStrip::IsAnimating() const {
527  return bounds_animator_.IsAnimating();
528}
529
530BaseTab* BaseTabStrip::GetTabAtLocal(const gfx::Point& local_point) {
531  views::View* view = GetEventHandlerForPoint(local_point);
532  if (!view)
533    return NULL;  // No tab contains the point.
534
535  // Walk up the view hierarchy until we find a tab, or the TabStrip.
536  while (view && view != this && view->GetID() != VIEW_ID_TAB)
537    view = view->parent();
538
539  return view && view->GetID() == VIEW_ID_TAB ?
540      static_cast<BaseTab*>(view) : NULL;
541}
542
543void BaseTabStrip::StoppedDraggingTab(BaseTab* tab, bool* is_first_tab) {
544  int tab_data_index = TabIndexOfTab(tab);
545  if (tab_data_index == -1) {
546    // The tab was removed before the drag completed. Don't do anything.
547    return;
548  }
549
550  if (*is_first_tab) {
551    *is_first_tab = false;
552    PrepareForAnimation();
553
554    // Animate the view back to its correct position.
555    GenerateIdealBounds();
556    AnimateToIdealBounds();
557  }
558  bounds_animator_.AnimateViewTo(tab, ideal_bounds(TabIndexOfTab(tab)));
559  // Install a delegate to reset the dragging state when done. We have to leave
560  // dragging true for the tab otherwise it'll draw beneath the new tab button.
561  bounds_animator_.SetAnimationDelegate(
562      tab, new ResetDraggingStateDelegate(tab), true);
563}
564