// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/tabs/base_tab_strip.h" #include "base/logging.h" #include "chrome/browser/ui/view_ids.h" #include "chrome/browser/ui/views/tabs/dragged_tab_controller.h" #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" #include "views/widget/root_view.h" #include "views/window/window.h" #if defined(OS_WIN) #include "views/widget/widget_win.h" #endif namespace { // Animation delegate used when a dragged tab is released. When done sets the // dragging state to false. class ResetDraggingStateDelegate : public views::BoundsAnimator::OwnedAnimationDelegate { public: explicit ResetDraggingStateDelegate(BaseTab* tab) : tab_(tab) { } virtual void AnimationEnded(const ui::Animation* animation) { tab_->set_dragging(false); } virtual void AnimationCanceled(const ui::Animation* animation) { tab_->set_dragging(false); } private: BaseTab* tab_; DISALLOW_COPY_AND_ASSIGN(ResetDraggingStateDelegate); }; } // namespace // AnimationDelegate used when removing a tab. Does the necessary cleanup when // done. class BaseTabStrip::RemoveTabDelegate : public views::BoundsAnimator::OwnedAnimationDelegate { public: RemoveTabDelegate(BaseTabStrip* tab_strip, BaseTab* tab) : tabstrip_(tab_strip), tab_(tab) { } virtual void AnimationEnded(const ui::Animation* animation) { CompleteRemove(); } virtual void AnimationCanceled(const ui::Animation* animation) { // We can be canceled for two interesting reasons: // . The tab we reference was dragged back into the tab strip. In this case // we don't want to remove the tab (closing is false). // . The drag was completed before the animation completed // (DestroyDraggedSourceTab). In this case we need to remove the tab // (closing is true). if (tab_->closing()) CompleteRemove(); } private: void CompleteRemove() { if (!tab_->closing()) { // The tab was added back yet we weren't canceled. This shouldn't happen. NOTREACHED(); return; } tabstrip_->RemoveAndDeleteTab(tab_); HighlightCloseButton(); } // When the animation completes, we send the Container a message to simulate // a mouse moved event at the current mouse position. This tickles the Tab // the mouse is currently over to show the "hot" state of the close button. void HighlightCloseButton() { if (tabstrip_->IsDragSessionActive() || !tabstrip_->ShouldHighlightCloseButtonAfterRemove()) { // This function is not required (and indeed may crash!) for removes // spawned by non-mouse closes and drag-detaches. return; } #if defined(OS_WIN) views::Widget* widget = tabstrip_->GetWidget(); // This can be null during shutdown. See http://crbug.com/42737. if (!widget) return; // Force the close button (that slides under the mouse) to highlight by // saying the mouse just moved, but sending the same coordinates. DWORD pos = GetMessagePos(); POINT cursor_point = {GET_X_LPARAM(pos), GET_Y_LPARAM(pos)}; MapWindowPoints(NULL, widget->GetNativeView(), &cursor_point, 1); static_cast(widget)->ResetLastMouseMoveFlag(); // Return to message loop - otherwise we may disrupt some operation that's // in progress. SendMessage(widget->GetNativeView(), WM_MOUSEMOVE, 0, MAKELPARAM(cursor_point.x, cursor_point.y)); #else NOTIMPLEMENTED(); #endif } BaseTabStrip* tabstrip_; BaseTab* tab_; DISALLOW_COPY_AND_ASSIGN(RemoveTabDelegate); }; BaseTabStrip::BaseTabStrip(TabStripController* controller, Type type) : controller_(controller), type_(type), attaching_dragged_tab_(false), ALLOW_THIS_IN_INITIALIZER_LIST(bounds_animator_(this)) { } BaseTabStrip::~BaseTabStrip() { } void BaseTabStrip::AddTabAt(int model_index, const TabRendererData& data) { BaseTab* tab = CreateTab(); tab->SetData(data); TabData d = { tab, gfx::Rect() }; tab_data_.insert(tab_data_.begin() + ModelIndexToTabIndex(model_index), d); AddChildView(tab); // Don't animate the first tab, it looks weird, and don't animate anything // if the containing window isn't visible yet. if (tab_count() > 1 && GetWindow() && GetWindow()->IsVisible()) StartInsertTabAnimation(model_index); else DoLayout(); } void BaseTabStrip::MoveTab(int from_model_index, int to_model_index) { int from_tab_data_index = ModelIndexToTabIndex(from_model_index); BaseTab* tab = tab_data_[from_tab_data_index].tab; tab_data_.erase(tab_data_.begin() + from_tab_data_index); TabData data = {tab, gfx::Rect()}; int to_tab_data_index = ModelIndexToTabIndex(to_model_index); tab_data_.insert(tab_data_.begin() + to_tab_data_index, data); StartMoveTabAnimation(); } void BaseTabStrip::SetTabData(int model_index, const TabRendererData& data) { BaseTab* tab = GetBaseTabAtModelIndex(model_index); bool mini_state_changed = tab->data().mini != data.mini; tab->SetData(data); if (mini_state_changed) { if (GetWindow() && GetWindow()->IsVisible()) StartMiniTabAnimation(); else DoLayout(); } } BaseTab* BaseTabStrip::GetBaseTabAtModelIndex(int model_index) const { return base_tab_at_tab_index(ModelIndexToTabIndex(model_index)); } int BaseTabStrip::GetModelIndexOfBaseTab(const BaseTab* tab) const { for (int i = 0, model_index = 0; i < tab_count(); ++i) { BaseTab* current_tab = base_tab_at_tab_index(i); if (!current_tab->closing()) { if (current_tab == tab) return model_index; model_index++; } else if (current_tab == tab) { return -1; } } return -1; } int BaseTabStrip::GetModelCount() const { return controller_->GetCount(); } bool BaseTabStrip::IsValidModelIndex(int model_index) const { return controller_->IsValidIndex(model_index); } int BaseTabStrip::ModelIndexToTabIndex(int model_index) const { int current_model_index = 0; for (int i = 0; i < tab_count(); ++i) { if (!base_tab_at_tab_index(i)->closing()) { if (current_model_index == model_index) return i; current_model_index++; } } return static_cast(tab_data_.size()); } bool BaseTabStrip::IsDragSessionActive() const { return drag_controller_.get() != NULL; } bool BaseTabStrip::IsActiveDropTarget() const { for (int i = 0; i < tab_count(); ++i) { BaseTab* tab = base_tab_at_tab_index(i); if (tab->dragging()) return true; } return false; } bool BaseTabStrip::IsTabStripEditable() const { return !IsDragSessionActive() && !IsActiveDropTarget(); } bool BaseTabStrip::IsTabStripCloseable() const { return !IsDragSessionActive(); } void BaseTabStrip::UpdateLoadingAnimations() { controller_->UpdateLoadingAnimations(); } void BaseTabStrip::SelectTab(BaseTab* tab) { int model_index = GetModelIndexOfBaseTab(tab); if (IsValidModelIndex(model_index)) controller_->SelectTab(model_index); } void BaseTabStrip::ExtendSelectionTo(BaseTab* tab) { int model_index = GetModelIndexOfBaseTab(tab); if (IsValidModelIndex(model_index)) controller_->ExtendSelectionTo(model_index); } void BaseTabStrip::ToggleSelected(BaseTab* tab) { int model_index = GetModelIndexOfBaseTab(tab); if (IsValidModelIndex(model_index)) controller_->ToggleSelected(model_index); } void BaseTabStrip::AddSelectionFromAnchorTo(BaseTab* tab) { int model_index = GetModelIndexOfBaseTab(tab); if (IsValidModelIndex(model_index)) controller_->AddSelectionFromAnchorTo(model_index); } void BaseTabStrip::CloseTab(BaseTab* tab) { // Find the closest model index. We do this so that the user can rapdily close // tabs and have the close click close the next tab. int model_index = 0; for (int i = 0; i < tab_count(); ++i) { BaseTab* current_tab = base_tab_at_tab_index(i); if (current_tab == tab) break; if (!current_tab->closing()) model_index++; } if (IsValidModelIndex(model_index)) controller_->CloseTab(model_index); } void BaseTabStrip::ShowContextMenuForTab(BaseTab* tab, const gfx::Point& p) { controller_->ShowContextMenuForTab(tab, p); } bool BaseTabStrip::IsActiveTab(const BaseTab* tab) const { int model_index = GetModelIndexOfBaseTab(tab); return IsValidModelIndex(model_index) && controller_->IsActiveTab(model_index); } bool BaseTabStrip::IsTabSelected(const BaseTab* tab) const { int model_index = GetModelIndexOfBaseTab(tab); return IsValidModelIndex(model_index) && controller_->IsTabSelected(model_index); } bool BaseTabStrip::IsTabPinned(const BaseTab* tab) const { if (tab->closing()) return false; int model_index = GetModelIndexOfBaseTab(tab); return IsValidModelIndex(model_index) && controller_->IsTabPinned(model_index); } bool BaseTabStrip::IsTabCloseable(const BaseTab* tab) const { int model_index = GetModelIndexOfBaseTab(tab); return !IsValidModelIndex(model_index) || controller_->IsTabCloseable(model_index); } void BaseTabStrip::MaybeStartDrag(BaseTab* tab, const views::MouseEvent& event) { // Don't accidentally start any drag operations during animations if the // mouse is down... during an animation tabs are being resized automatically, // so the View system can misinterpret this easily if the mouse is down that // the user is dragging. if (IsAnimating() || tab->closing() || controller_->HasAvailableDragActions() == 0) { return; } int model_index = GetModelIndexOfBaseTab(tab); if (!IsValidModelIndex(model_index)) { CHECK(false); return; } drag_controller_.reset(new DraggedTabController()); std::vector tabs; int size_to_selected = 0; int x = tab->GetMirroredXInView(event.x()); int y = event.y(); // Build the set of selected tabs to drag and calculate the offset from the // first selected tab. for (int i = 0; i < tab_count(); ++i) { BaseTab* other_tab = base_tab_at_tab_index(i); if (IsTabSelected(other_tab) && !other_tab->closing()) { tabs.push_back(other_tab); if (other_tab == tab) { size_to_selected = GetSizeNeededForTabs(tabs); if (type() == HORIZONTAL_TAB_STRIP) x = size_to_selected - tab->width() + x; else y = size_to_selected - tab->height() + y; } } } DCHECK(!tabs.empty()); DCHECK(std::find(tabs.begin(), tabs.end(), tab) != tabs.end()); drag_controller_->Init(this, tab, tabs, gfx::Point(x, y), tab->GetMirroredXInView(event.x())); } void BaseTabStrip::ContinueDrag(const views::MouseEvent& event) { // We can get called even if |MaybeStartDrag| wasn't called in the event of // a TabStrip animation when the mouse button is down. In this case we should // _not_ continue the drag because it can lead to weird bugs. if (drag_controller_.get()) { bool started_drag = drag_controller_->started_drag(); drag_controller_->Drag(); if (drag_controller_->started_drag() && !started_drag) { // The drag just started. Redirect mouse events to us to that the tab that // originated the drag can be safely deleted. GetRootView()->SetMouseHandler(this); } } } bool BaseTabStrip::EndDrag(bool canceled) { if (!drag_controller_.get()) return false; bool started_drag = drag_controller_->started_drag(); drag_controller_->EndDrag(canceled); return started_drag; } BaseTab* BaseTabStrip::GetTabAt(BaseTab* tab, const gfx::Point& tab_in_tab_coordinates) { gfx::Point local_point = tab_in_tab_coordinates; ConvertPointToView(tab, this, &local_point); return GetTabAtLocal(local_point); } void BaseTabStrip::Layout() { // Only do a layout if our size changed. if (last_layout_size_ == size()) return; DoLayout(); } bool BaseTabStrip::OnMouseDragged(const views::MouseEvent& event) { if (drag_controller_.get()) drag_controller_->Drag(); return true; } void BaseTabStrip::OnMouseReleased(const views::MouseEvent& event) { EndDrag(false); } void BaseTabStrip::OnMouseCaptureLost() { EndDrag(true); } void BaseTabStrip::StartMoveTabAnimation() { PrepareForAnimation(); GenerateIdealBounds(); AnimateToIdealBounds(); } void BaseTabStrip::StartRemoveTabAnimation(int model_index) { PrepareForAnimation(); // Mark the tab as closing. BaseTab* tab = GetBaseTabAtModelIndex(model_index); tab->set_closing(true); // Start an animation for the tabs. GenerateIdealBounds(); AnimateToIdealBounds(); // Animate the tab being closed to 0x0. gfx::Rect tab_bounds = tab->bounds(); if (type() == HORIZONTAL_TAB_STRIP) tab_bounds.set_width(0); else tab_bounds.set_height(0); bounds_animator_.AnimateViewTo(tab, tab_bounds); // Register delegate to do cleanup when done, BoundsAnimator takes // ownership of RemoveTabDelegate. bounds_animator_.SetAnimationDelegate(tab, new RemoveTabDelegate(this, tab), true); } void BaseTabStrip::StartMiniTabAnimation() { PrepareForAnimation(); GenerateIdealBounds(); AnimateToIdealBounds(); } bool BaseTabStrip::ShouldHighlightCloseButtonAfterRemove() { return true; } void BaseTabStrip::RemoveAndDeleteTab(BaseTab* tab) { int tab_data_index = TabIndexOfTab(tab); DCHECK(tab_data_index != -1); // Remove the Tab from the TabStrip's list... tab_data_.erase(tab_data_.begin() + tab_data_index); delete tab; } int BaseTabStrip::TabIndexOfTab(BaseTab* tab) const { for (int i = 0; i < tab_count(); ++i) { if (base_tab_at_tab_index(i) == tab) return i; } return -1; } void BaseTabStrip::StopAnimating(bool layout) { if (!IsAnimating()) return; bounds_animator().Cancel(); if (layout) DoLayout(); } void BaseTabStrip::DestroyDragController() { if (IsDragSessionActive()) drag_controller_.reset(NULL); } void BaseTabStrip::StartedDraggingTabs(const std::vector& tabs) { PrepareForAnimation(); // Reset dragging state of existing tabs. for (int i = 0; i < tab_count(); ++i) base_tab_at_tab_index(i)->set_dragging(false); for (size_t i = 0; i < tabs.size(); ++i) { tabs[i]->set_dragging(true); bounds_animator_.StopAnimatingView(tabs[i]); } // Move the dragged tabs to their ideal bounds. GenerateIdealBounds(); // Sets the bounds of the dragged tabs. for (size_t i = 0; i < tabs.size(); ++i) { int tab_data_index = TabIndexOfTab(tabs[i]); DCHECK(tab_data_index != -1); tabs[i]->SetBoundsRect(ideal_bounds(tab_data_index)); } SchedulePaint(); } void BaseTabStrip::StoppedDraggingTabs(const std::vector& tabs) { bool is_first_tab = true; for (size_t i = 0; i < tabs.size(); ++i) StoppedDraggingTab(tabs[i], &is_first_tab); } void BaseTabStrip::PrepareForAnimation() { if (!IsDragSessionActive() && !DraggedTabController::IsAttachedTo(this)) { for (int i = 0; i < tab_count(); ++i) base_tab_at_tab_index(i)->set_dragging(false); } } ui::AnimationDelegate* BaseTabStrip::CreateRemoveTabDelegate(BaseTab* tab) { return new RemoveTabDelegate(this, tab); } void BaseTabStrip::DoLayout() { last_layout_size_ = size(); StopAnimating(false); GenerateIdealBounds(); for (int i = 0; i < tab_count(); ++i) tab_data_[i].tab->SetBoundsRect(tab_data_[i].ideal_bounds); SchedulePaint(); } bool BaseTabStrip::IsAnimating() const { return bounds_animator_.IsAnimating(); } BaseTab* BaseTabStrip::GetTabAtLocal(const gfx::Point& local_point) { views::View* view = GetEventHandlerForPoint(local_point); if (!view) return NULL; // No tab contains the point. // Walk up the view hierarchy until we find a tab, or the TabStrip. while (view && view != this && view->GetID() != VIEW_ID_TAB) view = view->parent(); return view && view->GetID() == VIEW_ID_TAB ? static_cast(view) : NULL; } void BaseTabStrip::StoppedDraggingTab(BaseTab* tab, bool* is_first_tab) { int tab_data_index = TabIndexOfTab(tab); if (tab_data_index == -1) { // The tab was removed before the drag completed. Don't do anything. return; } if (*is_first_tab) { *is_first_tab = false; PrepareForAnimation(); // Animate the view back to its correct position. GenerateIdealBounds(); AnimateToIdealBounds(); } bounds_animator_.AnimateViewTo(tab, ideal_bounds(TabIndexOfTab(tab))); // Install a delegate to reset the dragging state when done. We have to leave // dragging true for the tab otherwise it'll draw beneath the new tab button. bounds_animator_.SetAnimationDelegate( tab, new ResetDraggingStateDelegate(tab), true); }