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
18 namespace {
19
20 // Animation delegate used when a dragged tab is released. When done sets the
21 // dragging state to false.
22 class ResetDraggingStateDelegate
23 : public views::BoundsAnimator::OwnedAnimationDelegate {
24 public:
ResetDraggingStateDelegate(BaseTab * tab)25 explicit ResetDraggingStateDelegate(BaseTab* tab) : tab_(tab) {
26 }
27
AnimationEnded(const ui::Animation * animation)28 virtual void AnimationEnded(const ui::Animation* animation) {
29 tab_->set_dragging(false);
30 }
31
AnimationCanceled(const ui::Animation * animation)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.
46 class BaseTabStrip::RemoveTabDelegate
47 : public views::BoundsAnimator::OwnedAnimationDelegate {
48 public:
RemoveTabDelegate(BaseTabStrip * tab_strip,BaseTab * tab)49 RemoveTabDelegate(BaseTabStrip* tab_strip, BaseTab* tab)
50 : tabstrip_(tab_strip),
51 tab_(tab) {
52 }
53
AnimationEnded(const ui::Animation * animation)54 virtual void AnimationEnded(const ui::Animation* animation) {
55 CompleteRemove();
56 }
57
AnimationCanceled(const ui::Animation * animation)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:
CompleteRemove()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.
HighlightCloseButton()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
BaseTabStrip(TabStripController * controller,Type type)118 BaseTabStrip::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
~BaseTabStrip()125 BaseTabStrip::~BaseTabStrip() {
126 }
127
AddTabAt(int model_index,const TabRendererData & data)128 void 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
MoveTab(int from_model_index,int to_model_index)145 void 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
SetTabData(int model_index,const TabRendererData & data)157 void 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
GetBaseTabAtModelIndex(int model_index) const170 BaseTab* BaseTabStrip::GetBaseTabAtModelIndex(int model_index) const {
171 return base_tab_at_tab_index(ModelIndexToTabIndex(model_index));
172 }
173
GetModelIndexOfBaseTab(const BaseTab * tab) const174 int 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
GetModelCount() const188 int BaseTabStrip::GetModelCount() const {
189 return controller_->GetCount();
190 }
191
IsValidModelIndex(int model_index) const192 bool BaseTabStrip::IsValidModelIndex(int model_index) const {
193 return controller_->IsValidIndex(model_index);
194 }
195
ModelIndexToTabIndex(int model_index) const196 int 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
IsDragSessionActive() const208 bool BaseTabStrip::IsDragSessionActive() const {
209 return drag_controller_.get() != NULL;
210 }
211
IsActiveDropTarget() const212 bool 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
IsTabStripEditable() const221 bool BaseTabStrip::IsTabStripEditable() const {
222 return !IsDragSessionActive() && !IsActiveDropTarget();
223 }
224
IsTabStripCloseable() const225 bool BaseTabStrip::IsTabStripCloseable() const {
226 return !IsDragSessionActive();
227 }
228
UpdateLoadingAnimations()229 void BaseTabStrip::UpdateLoadingAnimations() {
230 controller_->UpdateLoadingAnimations();
231 }
232
SelectTab(BaseTab * tab)233 void BaseTabStrip::SelectTab(BaseTab* tab) {
234 int model_index = GetModelIndexOfBaseTab(tab);
235 if (IsValidModelIndex(model_index))
236 controller_->SelectTab(model_index);
237 }
238
ExtendSelectionTo(BaseTab * tab)239 void BaseTabStrip::ExtendSelectionTo(BaseTab* tab) {
240 int model_index = GetModelIndexOfBaseTab(tab);
241 if (IsValidModelIndex(model_index))
242 controller_->ExtendSelectionTo(model_index);
243 }
244
ToggleSelected(BaseTab * tab)245 void BaseTabStrip::ToggleSelected(BaseTab* tab) {
246 int model_index = GetModelIndexOfBaseTab(tab);
247 if (IsValidModelIndex(model_index))
248 controller_->ToggleSelected(model_index);
249 }
250
AddSelectionFromAnchorTo(BaseTab * tab)251 void BaseTabStrip::AddSelectionFromAnchorTo(BaseTab* tab) {
252 int model_index = GetModelIndexOfBaseTab(tab);
253 if (IsValidModelIndex(model_index))
254 controller_->AddSelectionFromAnchorTo(model_index);
255 }
256
CloseTab(BaseTab * tab)257 void 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
ShowContextMenuForTab(BaseTab * tab,const gfx::Point & p)273 void BaseTabStrip::ShowContextMenuForTab(BaseTab* tab, const gfx::Point& p) {
274 controller_->ShowContextMenuForTab(tab, p);
275 }
276
IsActiveTab(const BaseTab * tab) const277 bool BaseTabStrip::IsActiveTab(const BaseTab* tab) const {
278 int model_index = GetModelIndexOfBaseTab(tab);
279 return IsValidModelIndex(model_index) &&
280 controller_->IsActiveTab(model_index);
281 }
282
IsTabSelected(const BaseTab * tab) const283 bool BaseTabStrip::IsTabSelected(const BaseTab* tab) const {
284 int model_index = GetModelIndexOfBaseTab(tab);
285 return IsValidModelIndex(model_index) &&
286 controller_->IsTabSelected(model_index);
287 }
288
IsTabPinned(const BaseTab * tab) const289 bool 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
IsTabCloseable(const BaseTab * tab) const298 bool BaseTabStrip::IsTabCloseable(const BaseTab* tab) const {
299 int model_index = GetModelIndexOfBaseTab(tab);
300 return !IsValidModelIndex(model_index) ||
301 controller_->IsTabCloseable(model_index);
302 }
303
MaybeStartDrag(BaseTab * tab,const views::MouseEvent & event)304 void 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
ContinueDrag(const views::MouseEvent & event)345 void 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
EndDrag(bool canceled)360 bool 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
GetTabAt(BaseTab * tab,const gfx::Point & tab_in_tab_coordinates)368 BaseTab* 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
Layout()375 void BaseTabStrip::Layout() {
376 // Only do a layout if our size changed.
377 if (last_layout_size_ == size())
378 return;
379 DoLayout();
380 }
381
OnMouseDragged(const views::MouseEvent & event)382 bool BaseTabStrip::OnMouseDragged(const views::MouseEvent& event) {
383 if (drag_controller_.get())
384 drag_controller_->Drag();
385 return true;
386 }
387
OnMouseReleased(const views::MouseEvent & event)388 void BaseTabStrip::OnMouseReleased(const views::MouseEvent& event) {
389 EndDrag(false);
390 }
391
OnMouseCaptureLost()392 void BaseTabStrip::OnMouseCaptureLost() {
393 EndDrag(true);
394 }
395
StartMoveTabAnimation()396 void BaseTabStrip::StartMoveTabAnimation() {
397 PrepareForAnimation();
398 GenerateIdealBounds();
399 AnimateToIdealBounds();
400 }
401
StartRemoveTabAnimation(int model_index)402 void 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
StartMiniTabAnimation()427 void BaseTabStrip::StartMiniTabAnimation() {
428 PrepareForAnimation();
429
430 GenerateIdealBounds();
431 AnimateToIdealBounds();
432 }
433
ShouldHighlightCloseButtonAfterRemove()434 bool BaseTabStrip::ShouldHighlightCloseButtonAfterRemove() {
435 return true;
436 }
437
RemoveAndDeleteTab(BaseTab * tab)438 void 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
TabIndexOfTab(BaseTab * tab) const449 int 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
StopAnimating(bool layout)457 void BaseTabStrip::StopAnimating(bool layout) {
458 if (!IsAnimating())
459 return;
460
461 bounds_animator().Cancel();
462
463 if (layout)
464 DoLayout();
465 }
466
DestroyDragController()467 void BaseTabStrip::DestroyDragController() {
468 if (IsDragSessionActive())
469 drag_controller_.reset(NULL);
470 }
471
StartedDraggingTabs(const std::vector<BaseTab * > & tabs)472 void 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
StoppedDraggingTabs(const std::vector<BaseTab * > & tabs)496 void 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
PrepareForAnimation()502 void 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
CreateRemoveTabDelegate(BaseTab * tab)509 ui::AnimationDelegate* BaseTabStrip::CreateRemoveTabDelegate(BaseTab* tab) {
510 return new RemoveTabDelegate(this, tab);
511 }
512
DoLayout()513 void 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
IsAnimating() const526 bool BaseTabStrip::IsAnimating() const {
527 return bounds_animator_.IsAnimating();
528 }
529
GetTabAtLocal(const gfx::Point & local_point)530 BaseTab* 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
StoppedDraggingTab(BaseTab * tab,bool * is_first_tab)543 void 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