| // 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/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/tab_contents/web_contents.h" |
| #include "chrome/common/drag_drop_types.h" |
| #include "chrome/common/gfx/chrome_canvas.h" |
| #include "chrome/common/gfx/path.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 views::DropTargetEvent; |
| |
| static const int kDefaultAnimationDurationMs = 100; |
| static const int kResizeLayoutAnimationDurationMs = 166; |
| static const int kReorderAnimationDurationMs = 166; |
| |
| 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)); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // NewTabButton |
| // |
| // A subclass of button that hit-tests to the shape of the new tab button. |
| |
| class NewTabButton : public views::Button { |
| public: |
| NewTabButton() {} |
| virtual ~NewTabButton() {} |
| |
| protected: |
| // Overridden from views::View: |
| virtual bool HasHitTestMask() const { |
| return true; |
| } |
| virtual void GetHitTestMask(gfx::Path* path) const { |
| DCHECK(path); |
| |
| SkScalar h = SkIntToScalar(height()); |
| SkScalar w = SkIntToScalar(width()); |
| |
| // These values are defined by the shape of the new tab bitmap. Should that |
| // bitmap ever change, these values will need to be updated. They're so |
| // custom it's not really worth defining constants for. |
| path->moveTo(0, 1); |
| path->lineTo(w - 7, 1); |
| path->lineTo(w - 4, 4); |
| path->lineTo(w, 16); |
| path->lineTo(w - 1, 17); |
| path->lineTo(7, 17); |
| path->lineTo(4, 13); |
| path->lineTo(0, 1); |
| path->close(); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(NewTabButton); |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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::GetMinimumUnselectedSize().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::GetMinimumUnselectedSize().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_; |
| // Make sure target_width is at least abs(kTabHOffset), otherwise if |
| // less than kTabHOffset during layout tabs get negatively offset. |
| double target_width = |
| std::max(abs(kTabHOffset), |
| Tab::GetMinimumUnselectedSize().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 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_->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); |
| views::Widget* widget = tabstrip_->GetWidget(); |
| RECT wr; |
| GetWindowRect(widget->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(widget->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_->y(), tab_a_->width(), |
| tab_a_->height()); |
| |
| // 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_->y(), tab_b_->width(), |
| tab_b_->height()); |
| |
| 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->width(); |
| } else { |
| start_unselected_width_ = current_tab->width(); |
| } |
| } |
| } |
| |
| 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() { |
| return GetPreferredSize().height(); |
| } |
| |
| bool TabStrip::HasAvailableDragActions() const { |
| return model_->delegate()->GetDragActions() != 0; |
| } |
| |
| bool TabStrip::CanProcessInputEvents() const { |
| return IsAnimating() == NULL; |
| } |
| |
| bool TabStrip::PointIsWithinWindowCaption(const gfx::Point& point) { |
| views::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; |
| |
| // Check to see if the point is within the non-button parts of the new tab |
| // button. The button has a non-rectangular shape, so if it's not in the |
| // visual portions of the button we treat it as a click to the caption. |
| gfx::Point point_in_newtab_coords(point); |
| View::ConvertPointToView(this, newtab_button_, &point_in_newtab_coords); |
| if (newtab_button_->bounds().Contains(point) && |
| !newtab_button_->HitTest(point_in_newtab_coords)) { |
| 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) { |
| if (!model_->closing_all()) |
| 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; |
| } |
| |
| void TabStrip::UpdateLoadingAnimations() { |
| 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->waiting_for_response()) { |
| current_tab->ValidateLoadingAnimation(Tab::ANIMATION_WAITING); |
| } else { |
| current_tab->ValidateLoadingAnimation(Tab::ANIMATION_LOADING); |
| } |
| } |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, views::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, width(), |
| height() - 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); |
| } |
| |
| // Overridden to support automation. See automation_proxy_uitest.cc. |
| views::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(); |
| } |
| |
| gfx::Size TabStrip::GetPreferredSize() { |
| return gfx::Size(0, Tab::GetMinimumUnselectedSize().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, GURL(), 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, GURL(), 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); |
| } |
| |
| views::View* TabStrip::GetViewForPoint(const gfx::Point& point) { |
| return GetViewForPoint(point, false); |
| } |
| |
| views::View* TabStrip::GetViewForPoint(const gfx::Point& point, |
| bool can_create_floating) { |
| // Return any view that isn't a Tab or this TabStrip immediately. We don't |
| // want to interfere. |
| views::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 && GetWidget() && |
| IsWindowVisible(GetWidget()->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(); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // 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 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() || !HasAvailableDragActions()) |
| return; |
| drag_controller_.reset(new DraggedTabController(tab, this)); |
| drag_controller_->CaptureDragInfo(gfx::Point(event.x(), event.y())); |
| } |
| |
| void TabStrip::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()) |
| drag_controller_->Drag(); |
| } |
| |
| bool TabStrip::EndDrag(bool canceled) { |
| return drag_controller_.get() ? drag_controller_->EndDrag(canceled) : false; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, views::BaseButton::ButtonListener implementation: |
| |
| void TabStrip::ButtonPressed(views::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 NewTabButton; |
| newtab_button_->SetListener(this, TabStripModel::kNoTab); |
| ResourceBundle& rb = ResourceBundle::GetSharedInstance(); |
| SkBitmap* bitmap; |
| |
| bitmap = rb.GetBitmapNamed(IDR_NEWTAB_BUTTON); |
| newtab_button_->SetImage(views::Button::BS_NORMAL, bitmap); |
| newtab_button_->SetImage(views::Button::BS_PUSHED, |
| rb.GetBitmapNamed(IDR_NEWTAB_BUTTON_P)); |
| newtab_button_->SetImage(views::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::GetMinimumUnselectedSize().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 = width(); |
| 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->width() - w) > 1) |
| StartResizeLayoutAnimation(); |
| } |
| |
| bool TabStrip::IsCursorInTabStripZone() { |
| gfx::Rect bounds = GetLocalBounds(true); |
| gfx::Point tabstrip_topleft(bounds.origin()); |
| View::ConvertPointToScreen(this, &tabstrip_topleft); |
| bounds.set_origin(tabstrip_topleft); |
| bounds.set_height(bounds.height() + kTabStripAnimationVSlop); |
| |
| CPoint cursor_point; |
| GetCursorPos(&cursor_point); |
| |
| return bounds.Contains(cursor_point.x, cursor_point.y); |
| } |
| |
| 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; |
| } |
| } |
| |
| 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->x() - (kTabHOffset / 2); |
| else |
| center_x = tab->x() + (tab->width() / 2); |
| } else { |
| Tab* last_tab = GetTabAt(drop_index - 1); |
| center_x = last_tab->x() + last_tab->width() + (kTabHOffset / 2); |
| } |
| |
| // Mirror the center point if necessary. |
| center_x = MirroredXCoordinateInsideView(center_x); |
| |
| // Determine the screen bounds. |
| gfx::Point 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() + height()); |
| |
| 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.x()); |
| for (int i = 0; i < GetTabCount(); ++i) { |
| Tab* tab = GetTabAt(i); |
| const int tab_max_x = tab->x() + tab->width(); |
| const int hot_width = tab->width() / 3; |
| if (x < tab_max_x) { |
| if (x < tab->x() + 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 views::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 views::WidgetWin; |
| 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 views::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(width() - 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) { |
| // Don't shock users by letting all tabs move when they are focused |
| // on the tab-strip. Wait for later, when they aren't looking. |
| int last_tab_index = GetTabCount() - 2; |
| if (last_tab_index > 0) { |
| Tab* last_tab = GetTabAt(last_tab_index); |
| available_width_for_tabs_ = std::min( |
| GetAvailableWidthForTabs(last_tab) + last_tab->width(), |
| width() - (kNewTabButtonHOffset + newtab_button_size_.width())); |
| } else { |
| 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->x() + last_tab->width(); |
| } |
| |
| bool TabStrip::IsPointInTab(Tab* tab, |
| const gfx::Point& point_in_tabstrip_coords) { |
| gfx::Point point_in_tab_coords(point_in_tabstrip_coords); |
| View::ConvertPointToView(this, tab, &point_in_tab_coords); |
| return tab->HitTest(point_in_tab_coords); |
| } |
| |