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: 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. 46 class 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 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 125 BaseTabStrip::~BaseTabStrip() { 126 } 127 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 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 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 170 BaseTab* BaseTabStrip::GetBaseTabAtModelIndex(int model_index) const { 171 return base_tab_at_tab_index(ModelIndexToTabIndex(model_index)); 172 } 173 174 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 188 int BaseTabStrip::GetModelCount() const { 189 return controller_->GetCount(); 190 } 191 192 bool BaseTabStrip::IsValidModelIndex(int model_index) const { 193 return controller_->IsValidIndex(model_index); 194 } 195 196 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 208 bool BaseTabStrip::IsDragSessionActive() const { 209 return drag_controller_.get() != NULL; 210 } 211 212 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 221 bool BaseTabStrip::IsTabStripEditable() const { 222 return !IsDragSessionActive() && !IsActiveDropTarget(); 223 } 224 225 bool BaseTabStrip::IsTabStripCloseable() const { 226 return !IsDragSessionActive(); 227 } 228 229 void BaseTabStrip::UpdateLoadingAnimations() { 230 controller_->UpdateLoadingAnimations(); 231 } 232 233 void BaseTabStrip::SelectTab(BaseTab* tab) { 234 int model_index = GetModelIndexOfBaseTab(tab); 235 if (IsValidModelIndex(model_index)) 236 controller_->SelectTab(model_index); 237 } 238 239 void BaseTabStrip::ExtendSelectionTo(BaseTab* tab) { 240 int model_index = GetModelIndexOfBaseTab(tab); 241 if (IsValidModelIndex(model_index)) 242 controller_->ExtendSelectionTo(model_index); 243 } 244 245 void BaseTabStrip::ToggleSelected(BaseTab* tab) { 246 int model_index = GetModelIndexOfBaseTab(tab); 247 if (IsValidModelIndex(model_index)) 248 controller_->ToggleSelected(model_index); 249 } 250 251 void BaseTabStrip::AddSelectionFromAnchorTo(BaseTab* tab) { 252 int model_index = GetModelIndexOfBaseTab(tab); 253 if (IsValidModelIndex(model_index)) 254 controller_->AddSelectionFromAnchorTo(model_index); 255 } 256 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 273 void BaseTabStrip::ShowContextMenuForTab(BaseTab* tab, const gfx::Point& p) { 274 controller_->ShowContextMenuForTab(tab, p); 275 } 276 277 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 283 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 289 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 298 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 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 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 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 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 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 382 bool BaseTabStrip::OnMouseDragged(const views::MouseEvent& event) { 383 if (drag_controller_.get()) 384 drag_controller_->Drag(); 385 return true; 386 } 387 388 void BaseTabStrip::OnMouseReleased(const views::MouseEvent& event) { 389 EndDrag(false); 390 } 391 392 void BaseTabStrip::OnMouseCaptureLost() { 393 EndDrag(true); 394 } 395 396 void BaseTabStrip::StartMoveTabAnimation() { 397 PrepareForAnimation(); 398 GenerateIdealBounds(); 399 AnimateToIdealBounds(); 400 } 401 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 427 void BaseTabStrip::StartMiniTabAnimation() { 428 PrepareForAnimation(); 429 430 GenerateIdealBounds(); 431 AnimateToIdealBounds(); 432 } 433 434 bool BaseTabStrip::ShouldHighlightCloseButtonAfterRemove() { 435 return true; 436 } 437 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 449 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 457 void BaseTabStrip::StopAnimating(bool layout) { 458 if (!IsAnimating()) 459 return; 460 461 bounds_animator().Cancel(); 462 463 if (layout) 464 DoLayout(); 465 } 466 467 void BaseTabStrip::DestroyDragController() { 468 if (IsDragSessionActive()) 469 drag_controller_.reset(NULL); 470 } 471 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 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 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 509 ui::AnimationDelegate* BaseTabStrip::CreateRemoveTabDelegate(BaseTab* tab) { 510 return new RemoveTabDelegate(this, tab); 511 } 512 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 526 bool BaseTabStrip::IsAnimating() const { 527 return bounds_animator_.IsAnimating(); 528 } 529 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 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