diff options
Diffstat (limited to 'chrome/browser/views/tabs/tab_strip.cc')
-rw-r--r-- | chrome/browser/views/tabs/tab_strip.cc | 1524 |
1 files changed, 1524 insertions, 0 deletions
diff --git a/chrome/browser/views/tabs/tab_strip.cc b/chrome/browser/views/tabs/tab_strip.cc new file mode 100644 index 0000000..2b8dc61 --- /dev/null +++ b/chrome/browser/views/tabs/tab_strip.cc @@ -0,0 +1,1524 @@ +// Copyright (c) 2006-2008 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/views/tabs/tab_strip.h" + +#include "base/gfx/size.h" +#include "chrome/app/theme/theme_resources.h" +#include "chrome/browser/profile.h" +#include "chrome/browser/tab_contents.h" +#include "chrome/browser/tabs/tab_strip_model.h" +#include "chrome/browser/user_metrics.h" +#include "chrome/browser/view_ids.h" +#include "chrome/browser/views/tabs/dragged_tab_controller.h" +#include "chrome/browser/views/tabs/tab.h" +#include "chrome/browser/vista_frame.h" +#include "chrome/browser/web_contents.h" +#include "chrome/common/drag_drop_types.h" +#include "chrome/common/gfx/chrome_canvas.h" +#include "chrome/common/l10n_util.h" +#include "chrome/common/os_exchange_data.h" +#include "chrome/common/pref_names.h" +#include "chrome/common/resource_bundle.h" +#include "chrome/common/slide_animation.h" +#include "chrome/common/stl_util-inl.h" +#include "chrome/common/win_util.h" +#include "chrome/views/image_view.h" +#include "chrome/views/painter.h" + +#include "generated_resources.h" + +#undef min +#undef max + +using ChromeViews::DropTargetEvent; + +static const int kDefaultAnimationDurationMs = 100; +static const int kResizeLayoutAnimationDurationMs = 166; +static const int kReorderAnimationDurationMs = 166; + +static const int kLoadingAnimationFrameTimeMs = 30; +static const int kNewTabButtonHOffset = -5; +static const int kNewTabButtonVOffset = 5; +static const int kResizeTabsTimeMs = 300; +static const int kSuspendAnimationsTimeMs = 200; +static const int kTabHOffset = -16; +static const int kTabStripAnimationVSlop = 40; + +// Size of the drop indicator. +static int drop_indicator_width; +static int drop_indicator_height; + +static inline int Round(double x) { + // Why oh why is this not in a standard header? + return static_cast<int>(floor(x + 0.5)); +} + +/////////////////////////////////////////////////////////////////////////////// +// +// TabAnimation +// +// A base class for all TabStrip animations. +// +class TabStrip::TabAnimation : public AnimationDelegate { + public: + friend class TabStrip; + + // Possible types of animation. + enum Type { + INSERT, + REMOVE, + MOVE, + RESIZE + }; + + TabAnimation(TabStrip* tabstrip, Type type) + : tabstrip_(tabstrip), + animation_(this), + start_selected_width_(0), + start_unselected_width_(0), + end_selected_width_(0), + end_unselected_width_(0), + layout_on_completion_(false), + type_(type) { + } + virtual ~TabAnimation() {} + + Type type() const { return type_; } + + void Start() { + animation_.SetSlideDuration(GetDuration()); + animation_.SetTweenType(SlideAnimation::EASE_OUT); + if (!animation_.IsShowing()) { + animation_.Reset(); + animation_.Show(); + } + } + + void Stop() { + animation_.Stop(); + } + + void set_layout_on_completion(bool layout_on_completion) { + layout_on_completion_ = layout_on_completion; + } + + // Retrieves the width for the Tab at the specified index if an animation is + // active. + static double GetCurrentTabWidth(TabStrip* tabstrip, + TabStrip::TabAnimation* animation, + int index) { + double unselected, selected; + tabstrip->GetCurrentTabWidths(&unselected, &selected); + Tab* tab = tabstrip->GetTabAt(index); + double tab_width = tab->IsSelected() ? selected : unselected; + if (animation) { + double specified_tab_width = animation->GetWidthForTab(index); + if (specified_tab_width != -1) + tab_width = specified_tab_width; + } + return tab_width; + } + + // Overridden from AnimationDelegate: + virtual void AnimationProgressed(const Animation* animation) { + tabstrip_->AnimationLayout(end_unselected_width_); + } + + virtual void AnimationEnded(const Animation* animation) { + tabstrip_->FinishAnimation(this, layout_on_completion_); + // This object is destroyed now, so we can't do anything else after this. + } + + virtual void AnimationCanceled(const Animation* animation) { + AnimationEnded(animation); + } + + protected: + // Returns the duration of the animation. + virtual int GetDuration() const { + return kDefaultAnimationDurationMs; + } + + // Subclasses override to return the width of the Tab at the specified index + // at the current animation frame. -1 indicates the default width should be + // used for the Tab. + virtual double GetWidthForTab(int index) const { + return -1; // Use default. + } + + // Figure out the desired start and end widths for the specified pre- and + // post- animation tab counts. + void GenerateStartAndEndWidths(int start_tab_count, int end_tab_count) { + tabstrip_->GetDesiredTabWidths(start_tab_count, &start_unselected_width_, + &start_selected_width_); + double standard_tab_width = + static_cast<double>(TabRenderer::GetStandardSize().width()); + if (start_tab_count < end_tab_count && + start_unselected_width_ < standard_tab_width) { + double minimum_tab_width = + static_cast<double>(TabRenderer::GetMinimumSize().width()); + start_unselected_width_ -= minimum_tab_width / start_tab_count; + } + tabstrip_->GenerateIdealBounds(); + tabstrip_->GetDesiredTabWidths(end_tab_count, + &end_unselected_width_, + &end_selected_width_); + } + + TabStrip* tabstrip_; + SlideAnimation animation_; + + double start_selected_width_; + double start_unselected_width_; + double end_selected_width_; + double end_unselected_width_; + + private: + // True if a complete re-layout is required upon completion of the animation. + // Subclasses set this if they don't perform a complete layout + // themselves and canceling the animation may leave the strip in an + // inconsistent state. + bool layout_on_completion_; + + const Type type_; + + DISALLOW_EVIL_CONSTRUCTORS(TabAnimation); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Handles insertion of a Tab at |index|. +class InsertTabAnimation : public TabStrip::TabAnimation { + public: + explicit InsertTabAnimation(TabStrip* tabstrip, int index) + : TabAnimation(tabstrip, INSERT), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + GenerateStartAndEndWidths(tab_count - 1, tab_count); + } + virtual ~InsertTabAnimation() {} + + protected: + // Overridden from TabStrip::TabAnimation: + virtual double GetWidthForTab(int index) const { + if (index == index_) { + bool is_selected = tabstrip_->model()->selected_index() == index; + double target_width = + is_selected ? end_unselected_width_ : end_selected_width_; + double start_width = is_selected ? Tab::GetMinimumSelectedSize().width() : + Tab::GetMinimumSize().width(); + double delta = target_width - start_width; + if (delta > 0) + return start_width + (delta * animation_.GetCurrentValue()); + return start_width; + } + if (tabstrip_->GetTabAt(index)->IsSelected()) { + double delta = end_selected_width_ - start_selected_width_; + return start_selected_width_ + (delta * animation_.GetCurrentValue()); + } + double delta = end_unselected_width_ - start_unselected_width_; + return start_unselected_width_ + (delta * animation_.GetCurrentValue()); + } + + private: + int index_; + + DISALLOW_EVIL_CONSTRUCTORS(InsertTabAnimation); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Handles removal of a Tab from |index| +class RemoveTabAnimation : public TabStrip::TabAnimation { + public: + RemoveTabAnimation(TabStrip* tabstrip, int index, TabContents* contents) + : TabAnimation(tabstrip, REMOVE), + index_(index) { + int tab_count = tabstrip->GetTabCount(); + GenerateStartAndEndWidths(tab_count, tab_count - 1); + } + + // Returns the index of the tab being removed. + int index() const { return index_; } + + virtual ~RemoveTabAnimation() { + } + + protected: + // Overridden from TabStrip::TabAnimation: + virtual double GetWidthForTab(int index) const { + Tab* tab = tabstrip_->GetTabAt(index); + if (index == index_) { + // The tab(s) being removed are gradually shrunken depending on the state + // of the animation. + // Removed animated Tabs are never selected. + double start_width = start_unselected_width_; + double target_width = Tab::GetMinimumSize().width() + kTabHOffset; + double delta = start_width - target_width; + return start_width - (delta * animation_.GetCurrentValue()); + } + if (tabstrip_->available_width_for_tabs_ != -1 && + index_ != tabstrip_->GetTabCount() - 1) { + return TabStrip::TabAnimation::GetWidthForTab(index); + } + // All other tabs are sized according to the start/end widths specified at + // the start of the animation. + if (tab->IsSelected()) { + double delta = end_selected_width_ - start_selected_width_; + return start_selected_width_ + (delta * animation_.GetCurrentValue()); + } + double delta = end_unselected_width_ - start_unselected_width_; + return start_unselected_width_ + (delta * animation_.GetCurrentValue()); + } + + virtual void AnimationEnded(const Animation* animation) { + RemoveTabAt(index_); + HighlightCloseButton(); + TabStrip::TabAnimation::AnimationEnded(animation); + } + + private: + // Cleans up the Tab from the TabStrip at the specified |index| once its + // animated removal is complete. + void RemoveTabAt(int index) const { + // Save a pointer to the Tab before we remove the TabData, we'll need this + // later. + Tab* removed = tabstrip_->tab_data_.at(index).tab; + + // Remove the Tab from the TabStrip's list... + tabstrip_->tab_data_.erase(tabstrip_->tab_data_.begin() + index); + + // If the TabContents being detached was removed as a result of a drag + // gesture from its corresponding Tab, we don't want to remove the Tab from + // the child list, because if we do so it'll stop receiving events and the + // drag will stall. So we only remove if a drag isn't active, or the Tab + // was for some other TabContents. + if (!tabstrip_->IsDragSessionActive() || + !tabstrip_->drag_controller_->IsDragSourceTab(removed)) { + tabstrip_->RemoveChildView(removed); + delete removed; + } + } + + // When the animation completes, we send the ViewContainer 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_->available_width_for_tabs_ == -1 || + tabstrip_->IsDragSessionActive()) { + // This function is not required (and indeed may crash!) for removes + // spawned by non-mouse closes and drag-detaches. + return; + } + + POINT pt; + GetCursorPos(&pt); + ChromeViews::ViewContainer* vc = tabstrip_->GetViewContainer(); + RECT wr; + GetWindowRect(vc->GetHWND(), &wr); + pt.x -= wr.left; + pt.y -= wr.top; + + // Return to message loop - otherwise we may disrupt some operation that's + // in progress. + PostMessage(vc->GetHWND(), WM_MOUSEMOVE, 0, MAKELPARAM(pt.x, pt.y)); + } + + int index_; + + DISALLOW_EVIL_CONSTRUCTORS(RemoveTabAnimation); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Handles the movement of a Tab from one position to another. +class MoveTabAnimation : public TabStrip::TabAnimation { + public: + MoveTabAnimation(TabStrip* tabstrip, int tab_a_index, int tab_b_index) + : TabAnimation(tabstrip, MOVE), + start_tab_a_bounds_(tabstrip_->GetIdealBounds(tab_b_index)), + start_tab_b_bounds_(tabstrip_->GetIdealBounds(tab_a_index)) { + tab_a_ = tabstrip_->GetTabAt(tab_a_index); + tab_b_ = tabstrip_->GetTabAt(tab_b_index); + + // Since we don't do a full TabStrip re-layout, we need to force a full + // layout upon completion since we're not guaranteed to be in a good state + // if for example the animation is canceled. + set_layout_on_completion(true); + } + virtual ~MoveTabAnimation() {} + + // Overridden from AnimationDelegate: + virtual void AnimationProgressed(const Animation* animation) { + // Position Tab A + double distance = start_tab_b_bounds_.x() - start_tab_a_bounds_.x(); + double delta = distance * animation_.GetCurrentValue(); + double new_x = start_tab_a_bounds_.x() + delta; + tab_a_->SetBounds(Round(new_x), tab_a_->GetY(), tab_a_->GetWidth(), + tab_a_->GetHeight()); + + // Position Tab B + distance = start_tab_a_bounds_.x() - start_tab_b_bounds_.x(); + delta = distance * animation_.GetCurrentValue(); + new_x = start_tab_b_bounds_.x() + delta; + tab_b_->SetBounds(Round(new_x), tab_b_->GetY(), tab_b_->GetWidth(), + tab_b_->GetHeight()); + + tabstrip_->SchedulePaint(); + } + + protected: + // Overridden from TabStrip::TabAnimation: + virtual int GetDuration() const { return kReorderAnimationDurationMs; } + + private: + // The two tabs being exchanged. + Tab* tab_a_; + Tab* tab_b_; + + // ...and their bounds. + gfx::Rect start_tab_a_bounds_; + gfx::Rect start_tab_b_bounds_; + + DISALLOW_EVIL_CONSTRUCTORS(MoveTabAnimation); +}; + +/////////////////////////////////////////////////////////////////////////////// + +// Handles the animated resize layout of the entire TabStrip from one width +// to another. +class ResizeLayoutAnimation : public TabStrip::TabAnimation { + public: + explicit ResizeLayoutAnimation(TabStrip* tabstrip) + : TabAnimation(tabstrip, RESIZE) { + int tab_count = tabstrip->GetTabCount(); + GenerateStartAndEndWidths(tab_count, tab_count); + InitStartState(); + } + virtual ~ResizeLayoutAnimation() { + } + + // Overridden from AnimationDelegate: + virtual void AnimationEnded(const Animation* animation) { + tabstrip_->resize_layout_scheduled_ = false; + TabStrip::TabAnimation::AnimationEnded(animation); + } + + protected: + // Overridden from TabStrip::TabAnimation: + virtual int GetDuration() const { + return kResizeLayoutAnimationDurationMs; + } + + virtual double GetWidthForTab(int index) const { + if (tabstrip_->GetTabAt(index)->IsSelected()) { + double delta = end_selected_width_ - start_selected_width_; + return start_selected_width_ + (delta * animation_.GetCurrentValue()); + } + double delta = end_unselected_width_ - start_unselected_width_; + return start_unselected_width_ + (delta * animation_.GetCurrentValue()); + } + + private: + // We need to start from the current widths of the Tabs as they were last + // laid out, _not_ the last known good state, which is what'll be done if we + // don't measure the Tab sizes here and just go with the default TabAnimation + // behavior... + void InitStartState() { + for (int i = 0; i < tabstrip_->GetTabCount(); ++i) { + Tab* current_tab = tabstrip_->GetTabAt(i); + if (current_tab->IsSelected()) { + start_selected_width_ = current_tab->GetWidth(); + } else { + start_unselected_width_ = current_tab->GetWidth(); + } + } + } + + DISALLOW_EVIL_CONSTRUCTORS(ResizeLayoutAnimation); +}; + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, public: + +TabStrip::TabStrip(TabStripModel* model) + : model_(model), + resize_layout_factory_(this), + added_as_message_loop_observer_(false), + resize_layout_scheduled_(false), + current_unselected_width_(Tab::GetStandardSize().width()), + current_selected_width_(Tab::GetStandardSize().width()), + available_width_for_tabs_(-1) { + Init(); +} + +TabStrip::~TabStrip() { + // TODO(beng): (1031854) Restore this line once XPFrame/VistaFrame are dead. + //model_->RemoveObserver(this); + + // TODO(beng): remove this if it doesn't work to fix the TabSelectedAt bug. + drag_controller_.reset(NULL); + + // Make sure we unhook ourselves as a message loop observer so that we don't + // crash in the case where the user closes the window after closing a tab + // but before moving the mouse. + RemoveMessageLoopObserver(); +} + +int TabStrip::GetPreferredHeight() { + CSize preferred_size; + GetPreferredSize(&preferred_size); + return preferred_size.cy; +} + +bool TabStrip::HasAvailableDragActions() const { + return model_->delegate()->GetDragActions() != 0; +} + +void TabStrip::ShowApplicationMenu(const gfx::Point& p) { + TabStripModelDelegate* delegate = model_->delegate(); + if (delegate) + delegate->ShowApplicationMenu(p); +} + +bool TabStrip::CanProcessInputEvents() const { + return IsAnimating() == NULL; +} + +bool TabStrip::PointIsWithinWindowCaption(const CPoint& point) { + ChromeViews::View* v = GetViewForPoint(point); + + // If there is no control at this location, claim the hit was in the title + // bar to get a move action. + if (v == this) + return true; + + // If the point is within the bounds of a Tab, the point can be considered + // part of the caption if there are no available drag operations for the Tab. + if (v->GetClassName() == Tab::kTabClassName && !HasAvailableDragActions()) + return true; + + // All other regions, including the new Tab button, should be considered part + // of the containing Window's client area so that regular events can be + // processed for them. + return false; +} + +bool TabStrip::IsCompatibleWith(TabStrip* other) { + return model_->profile() == other->model()->profile(); +} + +bool TabStrip::IsAnimating() const { + return active_animation_.get() != NULL; +} + +void TabStrip::DestroyDragController() { + if (IsDragSessionActive()) + drag_controller_.reset(NULL); +} + +void TabStrip::DestroyDraggedSourceTab(Tab* tab) { + // We could be running an animation that references this Tab. + if (active_animation_.get()) + active_animation_->Stop(); + // Make sure we leave the tab_data_ vector in a consistent state, otherwise + // we'll be pointing to tabs that have been deleted and removed from the + // child view list. + std::vector<TabData>::iterator it = tab_data_.begin(); + for (; it != tab_data_.end(); ++it) { + if (it->tab == tab) { + NOTREACHED() << "Leaving in an inconsistent state!"; + tab_data_.erase(it); + break; + } + } + tab->GetParent()->RemoveChildView(tab); + delete tab; + // Force a layout here, because if we've just quickly drag detached a Tab, + // the stopping of the active animation above may have left the TabStrip in a + // bad (visual) state. + Layout(); +} + +gfx::Rect TabStrip::GetIdealBounds(int index) { + DCHECK(index >= 0 && index < GetTabCount()); + return tab_data_.at(index).ideal_bounds; +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, ChromeViews::View overrides: + +void TabStrip::PaintChildren(ChromeCanvas* canvas) { + // Paint the tabs in reverse order, so they stack to the left. + Tab* selected_tab = NULL; + for (int i = GetTabCount() - 1; i >= 0; --i) { + Tab* tab = GetTabAt(i); + // We must ask the _Tab's_ model, not ourselves, because in some situations + // the model will be different to this object, e.g. when a Tab is being + // removed after its TabContents has been destroyed. + if (!tab->IsSelected()) { + tab->ProcessPaint(canvas); + } else { + selected_tab = tab; + } + } + + if (win_util::ShouldUseVistaFrame()) { + // Make sure unselected tabs are somewhat transparent. + SkPaint paint; + paint.setColor(SkColorSetARGB(200, 255, 255, 255)); + paint.setPorterDuffXfermode(SkPorterDuff::kDstIn_Mode); + paint.setStyle(SkPaint::kFill_Style); + canvas->FillRectInt( + 0, 0, GetWidth(), + GetHeight() - 2, // Visible region that overlaps the toolbar. + paint); + } + + // Paint the selected tab last, so it overlaps all the others. + if (selected_tab) + selected_tab->ProcessPaint(canvas); + + // Paint the New Tab button. + newtab_button_->ProcessPaint(canvas); +} + +void TabStrip::DidChangeBounds(const CRect& prev, const CRect& curr) { + Layout(); +} + +// Overridden to support automation. See automation_proxy_uitest.cc. +ChromeViews::View* TabStrip::GetViewByID(int view_id) const { + if (GetTabCount() > 0) { + if (view_id == VIEW_ID_TAB_LAST) { + return GetTabAt(GetTabCount() - 1); + } else if ((view_id >= VIEW_ID_TAB_0) && (view_id < VIEW_ID_TAB_LAST)) { + int index = view_id - VIEW_ID_TAB_0; + if (index >= 0 && index < GetTabCount()) { + return GetTabAt(index); + } else { + return NULL; + } + } + } + + return View::GetViewByID(view_id); +} + +void TabStrip::Layout() { + // Called from: + // - window resize + // - animation completion + if (active_animation_.get()) + active_animation_->Stop(); + GenerateIdealBounds(); + int tab_count = GetTabCount(); + int tab_right = 0; + for (int i = 0; i < tab_count; ++i) { + const gfx::Rect& bounds = tab_data_.at(i).ideal_bounds; + GetTabAt(i)->SetBounds(bounds.x(), bounds.y(), bounds.width(), + bounds.height()); + tab_right = bounds.right() + kTabHOffset; + } + LayoutNewTabButton(static_cast<double>(tab_right), current_unselected_width_); + SchedulePaint(); +} + +void TabStrip::GetPreferredSize(CSize* preferred_size) { + DCHECK(preferred_size); + preferred_size->cx = 0; + preferred_size->cy = Tab::GetMinimumSize().height(); +} + +void TabStrip::OnDragEntered(const DropTargetEvent& event) { + UpdateDropIndex(event); +} + +int TabStrip::OnDragUpdated(const DropTargetEvent& event) { + UpdateDropIndex(event); + return GetDropEffect(event); +} + +void TabStrip::OnDragExited() { + SetDropIndex(-1, false); +} + +int TabStrip::OnPerformDrop(const DropTargetEvent& event) { + if (!drop_info_.get()) + return DragDropTypes::DRAG_NONE; + + const int drop_index = drop_info_->drop_index; + const bool drop_before = drop_info_->drop_before; + + // Hide the drop indicator. + SetDropIndex(-1, false); + + GURL url; + std::wstring title; + if (!event.GetData().GetURLAndTitle(&url, &title) || !url.is_valid()) + return DragDropTypes::DRAG_NONE; + + if (drop_before) { + UserMetrics::RecordAction(L"Tab_DropURLBetweenTabs", model_->profile()); + + // Insert a new tab. + TabContents* contents = + model_->delegate()->CreateTabContentsForURL( + url, model_->profile(), PageTransition::TYPED, false, NULL); + model_->AddTabContents(contents, drop_index, PageTransition::GENERATED, + true); + } else { + UserMetrics::RecordAction(L"Tab_DropURLOnTab", model_->profile()); + + model_->GetTabContentsAt(drop_index)->controller()-> + LoadURL(url, PageTransition::GENERATED); + model_->SelectTabContentsAt(drop_index, true); + } + + return GetDropEffect(event); +} + +bool TabStrip::GetAccessibleRole(VARIANT* role) { + DCHECK(role); + + role->vt = VT_I4; + role->lVal = ROLE_SYSTEM_GROUPING; + return true; +} + +bool TabStrip::GetAccessibleName(std::wstring* name) { + if (!accessible_name_.empty()) { + (*name).assign(accessible_name_); + return true; + } + return false; +} + +void TabStrip::SetAccessibleName(const std::wstring& name) { + accessible_name_.assign(name); +} + +ChromeViews::View* TabStrip::GetViewForPoint(const CPoint& point) { + return GetViewForPoint(point, false); +} + +ChromeViews::View* TabStrip::GetViewForPoint(const CPoint& point, + bool can_create_floating) { + // Return any view that isn't a Tab or this TabStrip immediately. We don't + // want to interfere. + ChromeViews::View* v = View::GetViewForPoint(point, can_create_floating); + if (v && v != this && v->GetClassName() != Tab::kTabClassName) + return v; + + // The display order doesn't necessarily match the child list order, so we + // walk the display list hit-testing Tabs. Since the selected tab always + // renders on top of adjacent tabs, it needs to be hit-tested before any + // left-adjacent Tab, so we look ahead for it as we walk. + int tab_count = GetTabCount(); + for (int i = 0; i < tab_count; ++i) { + Tab* next_tab = i < (tab_count - 1) ? GetTabAt(i + 1) : NULL; + if (next_tab && next_tab->IsSelected() && IsPointInTab(next_tab, point)) + return next_tab; + Tab* tab = GetTabAt(i); + if (IsPointInTab(tab, point)) + return tab; + } + + // No need to do any floating view stuff, we don't use them in the TabStrip. + return this; +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, TabStripModelObserver implementation: + +void TabStrip::TabInsertedAt(TabContents* contents, + int index, + bool foreground) { + DCHECK(contents); + DCHECK(index == TabStripModel::kNoTab || model_->ContainsIndex(index)); + + if (active_animation_.get()) + active_animation_->Stop(); + + bool contains_tab = false; + Tab* tab = NULL; + // First see if this Tab is one that was dragged out of this TabStrip and is + // now being dragged back in. In this case, the DraggedTabController actually + // has the Tab already constructed and we can just insert it into our list + // again. + if (IsDragSessionActive()) { + tab = drag_controller_->GetDragSourceTabForContents(contents); + if (tab) { + // If the Tab was detached, it would have been animated closed but not + // removed, so we need to reset this property. + tab->set_closing(false); + tab->ValidateLoadingAnimation(TabRenderer::ANIMATION_NONE); + tab->SetVisible(true); + } + + // See if we're already in the list. We don't want to add ourselves twice. + std::vector<TabData>::const_iterator iter = tab_data_.begin(); + for (; iter != tab_data_.end() && !contains_tab; ++iter) { + if (iter->tab == tab) + contains_tab = true; + } + } + + // Otherwise we need to make a new Tab. + if (!tab) + tab = new Tab(this); + + // Only insert if we're not already in the list. + if (!contains_tab) { + if (index == TabStripModel::kNoTab) { + TabData d = { tab, gfx::Rect() }; + tab_data_.push_back(d); + tab->UpdateData(contents); + } else { + TabData d = { tab, gfx::Rect() }; + tab_data_.insert(tab_data_.begin() + index, d); + tab->UpdateData(contents); + } + } + + // We only add the tab to the child list if it's not already - an invisible + // tab maintained by the DraggedTabController will already be parented. + if (!tab->GetParent()) + 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 (GetTabCount() > 1 && IsWindowVisible(GetViewContainer()->GetHWND())) { + StartInsertTabAnimation(index); + } else { + Layout(); + } +} + +void TabStrip::TabDetachedAt(TabContents* contents, int index) { + if (CanUpdateDisplay()) { + GenerateIdealBounds(); + StartRemoveTabAnimation(index, contents); + // Have to do this _after_ calling StartRemoveTabAnimation, so that any + // previous remove is completed fully and index is valid in sync with the + // model index. + GetTabAt(index)->set_closing(true); + } +} + +void TabStrip::TabSelectedAt(TabContents* old_contents, + TabContents* new_contents, + int index, + bool user_gesture) { + DCHECK(index >= 0 && index < GetTabCount()); + if (CanUpdateDisplay()) { + // We have "tiny tabs" if the tabs are so tiny that the unselected ones are + // a different size to the selected ones. + bool tiny_tabs = current_unselected_width_ != current_selected_width_; + if (!IsAnimating() && (!resize_layout_scheduled_ || tiny_tabs)) { + Layout(); + } else { + SchedulePaint(); + } + } +} + +void TabStrip::TabMoved(TabContents* contents, int from_index, int to_index) { + Tab* tab = GetTabAt(from_index); + Tab* other_tab = GetTabAt(to_index); + tab_data_.erase(tab_data_.begin() + from_index); + TabData data = {tab, gfx::Rect()}; + tab_data_.insert(tab_data_.begin() + to_index, data); + GenerateIdealBounds(); + StartMoveTabAnimation(from_index, to_index); +} + +void TabStrip::TabChangedAt(TabContents* contents, int index) { + // Index is in terms of the model. Need to make sure we adjust that index in + // case we have an animation going. + Tab* tab = GetTabAtAdjustForAnimation(index); + tab->UpdateData(contents); + tab->UpdateFromModel(); +} + +void TabStrip::TabValidateAnimations() { + if (model_->TabsAreLoading()) { + if (!loading_animation_timer_.IsRunning()) { + // Loads are happening, and the timer isn't running, so start it. + loading_animation_timer_.Start( + TimeDelta::FromMilliseconds(kLoadingAnimationFrameTimeMs), this, + &TabStrip::LoadingAnimationCallback); + } + } else { + if (loading_animation_timer_.IsRunning()) { + loading_animation_timer_.Stop(); + // Loads are now complete, update the state if a task was scheduled. + LoadingAnimationCallback(); + } + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, Tab::Delegate implementation: + +bool TabStrip::IsTabSelected(const Tab* tab) const { + if (tab->closing()) + return false; + + int tab_count = GetTabCount(); + for (int i = 0, index = 0; i < tab_count; ++i, ++index) { + Tab* current_tab = GetTabAt(i); + if (current_tab->closing()) + --index; + if (current_tab == tab) + return index == model_->selected_index(); + } + return false; +} + +void TabStrip::SelectTab(Tab* tab) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + model_->SelectTabContentsAt(index, true); +} + +void TabStrip::CloseTab(Tab* tab) { + int tab_index = GetIndexOfTab(tab); + if (model_->ContainsIndex(tab_index)) { + TabContents* contents = model_->GetTabContentsAt(tab_index); + if (contents) + UserMetrics::RecordAction(L"CloseTab_Mouse", contents->profile()); + Tab* last_tab = GetTabAt(GetTabCount() - 1); + // Limit the width available to the TabStrip for laying out Tabs, so that + // Tabs are not resized until a later time (when the mouse pointer leaves + // the TabStrip). + available_width_for_tabs_ = GetAvailableWidthForTabs(last_tab); + resize_layout_scheduled_ = true; + AddMessageLoopObserver(); + model_->CloseTabContentsAt(tab_index); + } +} + +bool TabStrip::IsCommandEnabledForTab( + TabStripModel::ContextMenuCommand command_id, const Tab* tab) const { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + return model_->IsContextMenuCommandEnabled(index, command_id); + return false; +} + +void TabStrip::ExecuteCommandForTab( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) + model_->ExecuteContextMenuCommand(index, command_id); +} + +void TabStrip::StartHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + if (command_id == TabStripModel::CommandCloseTabsOpenedBy) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) { + std::vector<int> indices = model_->GetIndexesOpenedBy(index); + std::vector<int>::const_iterator iter = indices.begin(); + for (; iter != indices.end(); ++iter) { + int current_index = *iter; + DCHECK(current_index >= 0 && current_index < GetTabCount()); + Tab* current_tab = GetTabAt(current_index); + current_tab->StartPulse(); + } + } + } else if (command_id == TabStripModel::CommandCloseTabsToRight) { + int index = GetIndexOfTab(tab); + if (model_->ContainsIndex(index)) { + for (int i = index + 1; i < GetTabCount(); ++i) { + Tab* current_tab = GetTabAt(i); + current_tab->StartPulse(); + } + } + } else if (command_id == TabStripModel::CommandCloseOtherTabs) { + for (int i = 0; i < GetTabCount(); ++i) { + Tab* current_tab = GetTabAt(i); + if (current_tab != tab) + current_tab->StartPulse(); + } + } +} + +void TabStrip::StopHighlightTabsForCommand( + TabStripModel::ContextMenuCommand command_id, Tab* tab) { + if (command_id == TabStripModel::CommandCloseTabsOpenedBy || + command_id == TabStripModel::CommandCloseTabsToRight || + command_id == TabStripModel::CommandCloseOtherTabs) { + // Just tell all Tabs to stop pulsing - it's safe. + StopAllHighlighting(); + } +} + +void TabStrip::StopAllHighlighting() { + for (int i = 0; i < GetTabCount(); ++i) + GetTabAt(i)->StopPulse(); +} + +void TabStrip::MaybeStartDrag(Tab* tab, const ChromeViews::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()) + return; + drag_controller_.reset(new DraggedTabController(tab, this)); + drag_controller_->CaptureDragInfo(gfx::Point(event.GetX(), event.GetY())); +} + +void TabStrip::ContinueDrag(const ChromeViews::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()) + drag_controller_->Drag(); +} + +void TabStrip::EndDrag(bool canceled) { + if (drag_controller_.get()) + drag_controller_->EndDrag(canceled); +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, ChromeViews::BaseButton::ButtonListener implementation: + +void TabStrip::ButtonPressed(ChromeViews::BaseButton* sender) { + if (sender == newtab_button_) + model_->AddBlankTab(true); +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, MessageLoop::Observer implementation: + +void TabStrip::WillProcessMessage(const MSG& msg) { +} + +void TabStrip::DidProcessMessage(const MSG& msg) { + // We spy on three different Windows messages here to see if the mouse has + // moved out of the bounds of the tabstrip, which we use as our cue to kick + // of the resize animation. The messages are: + // + // WM_MOUSEMOVE: + // For when the mouse moves from the tabstrip over into the rest of the + // browser UI, i.e. within the bounds of the same windows HWND. + // WM_MOUSELEAVE: + // For when the mouse moves very rapidly from a tab closed in the middle of + // the tabstrip (_not_ the end) out of the bounds of the browser's HWND and + // over some other HWND. + // WM_NCMOUSELEAVE: + // For when the mouse moves very rapidly from the end of the tabstrip (when + // the last tab is closed and the mouse is left floating over the title + // bar). Because the empty area of the tabstrip at the end of the title bar + // is registered by the ChromeFrame as part of the "caption" area of the + // window (the frame's OnNCHitTest method returns HTCAPTION for this + // region), the frame's HWND receives a WM_MOUSEMOVE message immediately, + // because as far as it is concerned the mouse has _left_ the client area + // of the window (and is now over the non-client area). To be notified + // again when the mouse leaves the _non-client_ area, we use the + // WM_NCMOUSELEAVE message, which causes us to re-evaluate the cursor + // position and correctly resize the tabstrip. + // + switch (msg.message) { + case WM_MOUSEMOVE: + case WM_MOUSELEAVE: + case WM_NCMOUSELEAVE: + if (!IsCursorInTabStripZone()) { + // Mouse moved outside the tab slop zone, start a timer to do a resize + // layout after a short while... + if (resize_layout_factory_.empty()) { + MessageLoop::current()->PostDelayedTask(FROM_HERE, + resize_layout_factory_.NewRunnableMethod( + &TabStrip::ResizeLayoutTabs), + kResizeTabsTimeMs); + } + } else { + // Mouse moved quickly out of the tab strip and then into it again, so + // cancel the timer so that the strip doesn't move when the mouse moves + // back over it. + resize_layout_factory_.RevokeAll(); + } + break; + } +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, private: + +void TabStrip::Init() { + model_->AddObserver(this); + newtab_button_ = new ChromeViews::Button; + newtab_button_->SetListener(this, TabStripModel::kNoTab); + ResourceBundle& rb = ResourceBundle::GetSharedInstance(); + SkBitmap* bitmap; + + bitmap = rb.GetBitmapNamed(IDR_NEWTAB_BUTTON); + newtab_button_->SetImage(ChromeViews::Button::BS_NORMAL, bitmap); + newtab_button_->SetImage(ChromeViews::Button::BS_PUSHED, + rb.GetBitmapNamed(IDR_NEWTAB_BUTTON_P)); + newtab_button_->SetImage(ChromeViews::Button::BS_HOT, + rb.GetBitmapNamed(IDR_NEWTAB_BUTTON_H)); + + newtab_button_size_.SetSize(bitmap->width(), bitmap->height()); + actual_newtab_button_size_ = newtab_button_size_; + + newtab_button_->SetAccessibleName(l10n_util::GetString(IDS_ACCNAME_NEWTAB)); + AddChildView(newtab_button_); + + if (drop_indicator_width == 0) { + // Direction doesn't matter, both images are the same size. + SkBitmap* drop_image = GetDropArrowImage(true); + drop_indicator_width = drop_image->width(); + drop_indicator_height = drop_image->height(); + } +} + +Tab* TabStrip::GetTabAt(int index) const { + DCHECK(index >= 0 && index < GetTabCount()); + return tab_data_.at(index).tab; +} + +Tab* TabStrip::GetTabAtAdjustForAnimation(int index) const { + if (active_animation_.get() && + active_animation_->type() == TabAnimation::REMOVE && + index >= + static_cast<RemoveTabAnimation*>(active_animation_.get())->index()) { + index++; + } + return GetTabAt(index); +} + +int TabStrip::GetTabCount() const { + return static_cast<int>(tab_data_.size()); +} + +void TabStrip::GetCurrentTabWidths(double* unselected_width, + double* selected_width) const { + *unselected_width = current_unselected_width_; + *selected_width = current_selected_width_; +} + +void TabStrip::GetDesiredTabWidths(int tab_count, + double* unselected_width, + double* selected_width) const { + const double min_unselected_width = Tab::GetMinimumSize().width(); + const double min_selected_width = Tab::GetMinimumSelectedSize().width(); + if (tab_count == 0) { + // Return immediately to avoid divide-by-zero below. + *unselected_width = min_unselected_width; + *selected_width = min_selected_width; + return; + } + + // Determine how much space we can actually allocate to tabs. + int available_width; + if (available_width_for_tabs_ < 0) { + available_width = GetWidth(); + available_width -= (kNewTabButtonHOffset + newtab_button_size_.width()); + } else { + // Interesting corner case: if |available_width_for_tabs_| > the result + // of the calculation in the conditional arm above, the strip is in + // overflow. We can either use the specified width or the true available + // width here; the first preserves the consistent "leave the last tab under + // the user's mouse so they can close many tabs" behavior at the cost of + // prolonging the glitchy appearance of the overflow state, while the second + // gets us out of overflow as soon as possible but forces the user to move + // their mouse for a few tabs' worth of closing. We choose visual + // imperfection over behavioral imperfection and select the first option. + available_width = available_width_for_tabs_; + } + + // Calculate the desired tab widths by dividing the available space into equal + // portions. Don't let tabs get larger than the "standard width" or smaller + // than the minimum width for each type, respectively. + const int total_offset = kTabHOffset * (tab_count - 1); + const double desired_tab_width = std::min((static_cast<double>( + available_width - total_offset) / static_cast<double>(tab_count)), + static_cast<double>(Tab::GetStandardSize().width())); + *unselected_width = std::max(desired_tab_width, min_unselected_width); + *selected_width = std::max(desired_tab_width, min_selected_width); + + // When there are multiple tabs, we'll have one selected and some unselected + // tabs. If the desired width was between the minimum sizes of these types, + // try to shrink the tabs with the smaller minimum. For example, if we have + // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5. If + // selected tabs have a minimum width of 4 and unselected tabs have a minimum + // width of 1, the above code would set *unselected_width = 2.5, + // *selected_width = 4, which results in a total width of 11.5. Instead, we + // want to set *unselected_width = 2, *selected_width = 4, for a total width + // of 10. + if (tab_count > 1) { + if ((min_unselected_width < min_selected_width) && + (desired_tab_width < min_selected_width)) { + // Unselected width = (total width - selected width) / (num_tabs - 1) + *unselected_width = std::max(static_cast<double>( + available_width - total_offset - min_selected_width) / + static_cast<double>(tab_count - 1), min_unselected_width); + } else if ((min_unselected_width > min_selected_width) && + (desired_tab_width < min_unselected_width)) { + // Selected width = (total width - (unselected width * (num_tabs - 1))) + *selected_width = std::max(available_width - total_offset - + (min_unselected_width * (tab_count - 1)), min_selected_width); + } + } +} + +void TabStrip::ResizeLayoutTabs() { + resize_layout_factory_.RevokeAll(); + + // It is critically important that this is unhooked here, otherwise we will + // keep spying on messages forever. + RemoveMessageLoopObserver(); + + available_width_for_tabs_ = -1; + double unselected, selected; + GetDesiredTabWidths(GetTabCount(), &unselected, &selected); + Tab* first_tab = GetTabAt(0); + int w = Round(first_tab->IsSelected() ? selected : selected); + + // We only want to run the animation if we're not already at the desired + // size. + if (abs(first_tab->GetWidth() - w) > 1) + StartResizeLayoutAnimation(); +} + +bool TabStrip::IsCursorInTabStripZone() { + CRect bounds; + GetLocalBounds(&bounds, true); + CPoint tabstrip_topleft = bounds.TopLeft(); + View::ConvertPointToScreen(this, &tabstrip_topleft); + bounds.MoveToXY(tabstrip_topleft); + bounds.bottom += kTabStripAnimationVSlop; + + CPoint cursor_point; + GetCursorPos(&cursor_point); + + return !!bounds.PtInRect(cursor_point); +} + +void TabStrip::AddMessageLoopObserver() { + if (!added_as_message_loop_observer_) { + MessageLoopForUI::current()->AddObserver(this); + added_as_message_loop_observer_ = true; + } +} + +void TabStrip::RemoveMessageLoopObserver() { + if (added_as_message_loop_observer_) { + MessageLoopForUI::current()->RemoveObserver(this); + added_as_message_loop_observer_ = false; + } +} + +void TabStrip::LoadingAnimationCallback() { + for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) { + Tab* current_tab = GetTabAt(i); + if (current_tab->closing()) { + --index; + } else { + TabContents* contents = model_->GetTabContentsAt(index); + if (!contents || !contents->is_loading()) { + current_tab->ValidateLoadingAnimation(Tab::ANIMATION_NONE); + } else if (contents->response_started()) { + current_tab->ValidateLoadingAnimation(Tab::ANIMATION_WAITING); + } else { + current_tab->ValidateLoadingAnimation(Tab::ANIMATION_LOADING); + } + } + } + + // Make sure the model delegates updates the animation as well. + TabStripModelDelegate* delegate; + if (model_ && (delegate = model_->delegate())) + delegate->ValidateLoadingAnimations(); +} + +gfx::Rect TabStrip::GetDropBounds(int drop_index, + bool drop_before, + bool* is_beneath) { + DCHECK(drop_index != -1); + int center_x; + if (drop_index < GetTabCount()) { + Tab* tab = GetTabAt(drop_index); + if (drop_before) + center_x = tab->GetX() - (kTabHOffset / 2); + else + center_x = tab->GetX() + (tab->GetWidth() / 2); + } else { + Tab* last_tab = GetTabAt(drop_index - 1); + center_x = last_tab->GetX() + last_tab->GetWidth() + (kTabHOffset / 2); + } + + // Mirror the center point if necessary. + center_x = MirroredXCoordinateInsideView(center_x); + + // Determine the screen bounds. + CPoint drop_loc(center_x - drop_indicator_width / 2, -drop_indicator_height); + ConvertPointToScreen(this, &drop_loc); + gfx::Rect drop_bounds(drop_loc.x, drop_loc.y, drop_indicator_width, + drop_indicator_height); + + // If the rect doesn't fit on the monitor, push the arrow to the bottom. + gfx::Rect monitor_bounds = win_util::GetMonitorBoundsForRect(drop_bounds); + *is_beneath = (monitor_bounds.IsEmpty() || + !monitor_bounds.Contains(drop_bounds)); + if (*is_beneath) + drop_bounds.Offset(0, drop_bounds.height() + GetHeight()); + + return drop_bounds; +} + +void TabStrip::UpdateDropIndex(const DropTargetEvent& event) { + // If the UI layout is right-to-left, we need to mirror the mouse + // coordinates since we calculate the drop index based on the + // original (and therefore non-mirrored) positions of the tabs. + const int x = MirroredXCoordinateInsideView(event.GetX()); + for (int i = 0; i < GetTabCount(); ++i) { + Tab* tab = GetTabAt(i); + const int tab_max_x = tab->GetX() + tab->GetWidth(); + const int hot_width = tab->GetWidth() / 3; + if (x < tab_max_x) { + if (x < tab->GetX() + hot_width) + SetDropIndex(i, true); + else if (x >= tab_max_x - hot_width) + SetDropIndex(i + 1, true); + else + SetDropIndex(i, false); + return; + } + } + + // The drop isn't over a tab, add it to the end. + SetDropIndex(GetTabCount(), true); +} + +void TabStrip::SetDropIndex(int index, bool drop_before) { + if (index == -1) { + if (drop_info_.get()) + drop_info_.reset(NULL); + return; + } + + if (drop_info_.get() && drop_info_->drop_index == index && + drop_info_->drop_before == drop_before) { + return; + } + + bool is_beneath; + gfx::Rect drop_bounds = GetDropBounds(index, drop_before, &is_beneath); + + if (!drop_info_.get()) { + drop_info_.reset(new DropInfo(index, drop_before, !is_beneath)); + } else { + drop_info_->drop_index = index; + drop_info_->drop_before = drop_before; + if (is_beneath == drop_info_->point_down) { + drop_info_->point_down = !is_beneath; + drop_info_->arrow_view->SetImage( + GetDropArrowImage(drop_info_->point_down)); + } + } + + // Reposition the window. Need to show it too as the window is initially + // hidden. + + drop_info_->arrow_window->SetWindowPos( + HWND_TOPMOST, drop_bounds.x(), drop_bounds.y(), drop_bounds.width(), + drop_bounds.height(), SWP_NOACTIVATE | SWP_SHOWWINDOW); +} + +int TabStrip::GetDropEffect(const ChromeViews::DropTargetEvent& event) { + const int source_ops = event.GetSourceOperations(); + if (source_ops & DragDropTypes::DRAG_COPY) + return DragDropTypes::DRAG_COPY; + if (source_ops & DragDropTypes::DRAG_LINK) + return DragDropTypes::DRAG_LINK; + return DragDropTypes::DRAG_MOVE; +} + +// static +SkBitmap* TabStrip::GetDropArrowImage(bool is_down) { + return ResourceBundle::GetSharedInstance().GetBitmapNamed( + is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); +} + +// TabStrip::DropInfo ---------------------------------------------------------- + +TabStrip::DropInfo::DropInfo(int drop_index, bool drop_before, bool point_down) + : drop_index(drop_index), + drop_before(drop_before), + point_down(point_down) { + arrow_window = new ChromeViews::HWNDViewContainer(); + arrow_window->set_window_style(WS_POPUP); + arrow_window->set_window_ex_style(WS_EX_TOPMOST | WS_EX_NOACTIVATE | + WS_EX_LAYERED | WS_EX_TRANSPARENT); + + arrow_view = new ChromeViews::ImageView; + arrow_view->SetImage(GetDropArrowImage(point_down)); + + arrow_window->Init( + NULL, + gfx::Rect(0, 0, drop_indicator_width, drop_indicator_height), + true); + arrow_window->SetContentsView(arrow_view); +} + +TabStrip::DropInfo::~DropInfo() { + // Close eventually deletes the window, which deletes arrow_view too. + arrow_window->Close(); +} + +/////////////////////////////////////////////////////////////////////////////// + +// Called from: +// - BasicLayout +// - Tab insertion/removal +// - Tab reorder +void TabStrip::GenerateIdealBounds() { + int tab_count = GetTabCount(); + double unselected, selected; + GetDesiredTabWidths(tab_count, &unselected, &selected); + + current_unselected_width_ = unselected; + current_selected_width_ = selected; + + // NOTE: This currently assumes a tab's height doesn't differ based on + // selected state or the number of tabs in the strip! + int tab_height = Tab::GetStandardSize().height(); + double tab_x = 0; + for (int i = 0; i < tab_count; ++i) { + Tab* tab = GetTabAt(i); + double tab_width = unselected; + if (tab->IsSelected()) + tab_width = selected; + double end_of_tab = tab_x + tab_width; + int rounded_tab_x = Round(tab_x); + gfx::Rect state(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, + tab_height); + tab_data_.at(i).ideal_bounds = state; + tab_x = end_of_tab + kTabHOffset; + } +} + +void TabStrip::LayoutNewTabButton(double last_tab_right, + double unselected_width) { + int delta = abs(Round(unselected_width) - Tab::GetStandardSize().width()); + if (delta > 1 && !resize_layout_scheduled_) { + // We're shrinking tabs, so we need to anchor the New Tab button to the + // right edge of the TabStrip's bounds, rather than the right edge of the + // right-most Tab, otherwise it'll bounce when animating. + newtab_button_->SetBounds(GetWidth() - newtab_button_size_.width(), + kNewTabButtonVOffset, + newtab_button_size_.width(), + newtab_button_size_.height()); + } else { + newtab_button_->SetBounds( + Round(last_tab_right - kTabHOffset) + kNewTabButtonHOffset, + kNewTabButtonVOffset, newtab_button_size_.width(), + newtab_button_size_.height()); + } +} + +// Called from: +// - animation tick +void TabStrip::AnimationLayout(double unselected_width) { + int tab_height = Tab::GetStandardSize().height(); + double tab_x = 0; + for (int i = 0; i < GetTabCount(); ++i) { + TabAnimation* animation = active_animation_.get(); + double tab_width = TabAnimation::GetCurrentTabWidth(this, animation, i); + double end_of_tab = tab_x + tab_width; + int rounded_tab_x = Round(tab_x); + Tab* tab = GetTabAt(i); + tab->SetBounds(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, + tab_height); + tab_x = end_of_tab + kTabHOffset; + } + LayoutNewTabButton(tab_x, unselected_width); + SchedulePaint(); +} + +void TabStrip::StartResizeLayoutAnimation() { + if (active_animation_.get()) + active_animation_->Stop(); + active_animation_.reset(new ResizeLayoutAnimation(this)); + active_animation_->Start(); +} + +void TabStrip::StartInsertTabAnimation(int index) { + // The TabStrip can now use its entire width to lay out Tabs. + available_width_for_tabs_ = -1; + if (active_animation_.get()) + active_animation_->Stop(); + active_animation_.reset(new InsertTabAnimation(this, index)); + active_animation_->Start(); +} + +void TabStrip::StartRemoveTabAnimation(int index, TabContents* contents) { + if (active_animation_.get()) { + // Some animations (e.g. MoveTabAnimation) cause there to be a Layout when + // they're completed (which includes canceled). Since |tab_data_| is now + // inconsistent with TabStripModel, doing this Layout will crash now, so + // we ask the MoveTabAnimation to skip its Layout (the state will be + // corrected by the RemoveTabAnimation we're about to initiate). + active_animation_->set_layout_on_completion(false); + active_animation_->Stop(); + } + active_animation_.reset(new RemoveTabAnimation(this, index, contents)); + active_animation_->Start(); +} + +void TabStrip::StartMoveTabAnimation(int from_index, int to_index) { + if (active_animation_.get()) + active_animation_->Stop(); + active_animation_.reset(new MoveTabAnimation(this, from_index, to_index)); + active_animation_->Start(); +} + +bool TabStrip::CanUpdateDisplay() { + // Don't bother laying out/painting when we're closing all tabs. + if (model_->closing_all()) { + // Make sure any active animation is ended, too. + if (active_animation_.get()) + active_animation_->Stop(); + return false; + } + return true; +} + +void TabStrip::FinishAnimation(TabStrip::TabAnimation* animation, + bool layout) { + active_animation_.reset(NULL); + if (layout) + Layout(); +} + +int TabStrip::GetIndexOfTab(const Tab* tab) const { + for (int i = 0, index = 0; i < GetTabCount(); ++i, ++index) { + Tab* current_tab = GetTabAt(i); + if (current_tab->closing()) { + --index; + } else if (current_tab == tab) { + return index; + } + } + return -1; +} + +int TabStrip::GetAvailableWidthForTabs(Tab* last_tab) const { + return last_tab->GetX() + last_tab->GetWidth(); +} + +bool TabStrip::IsPointInTab(Tab* tab, const CPoint& point_in_tabstrip_coords) { + CPoint point_in_tab_coords(point_in_tabstrip_coords); + View::ConvertPointToView(this, tab, &point_in_tab_coords); + return tab->HitTest(point_in_tab_coords); +} + |