1// Copyright (c) 2011 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/ui/views/tabs/side_tab_strip.h"
6
7#include "chrome/browser/ui/view_ids.h"
8#include "chrome/browser/ui/views/tabs/side_tab.h"
9#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
10#include "grit/generated_resources.h"
11#include "grit/theme_resources.h"
12#include "ui/base/l10n/l10n_util.h"
13#include "ui/base/resource/resource_bundle.h"
14#include "ui/gfx/canvas.h"
15#include "views/background.h"
16#include "views/controls/button/image_button.h"
17#include "views/controls/button/text_button.h"
18
19namespace {
20
21const int kVerticalTabSpacing = 2;
22const int kTabStripWidth = 140;
23const SkColor kBackgroundColor = SkColorSetARGB(255, 209, 220, 248);
24const SkColor kSeparatorColor = SkColorSetARGB(255, 151, 159, 179);
25
26// Height of the scroll buttons.
27const int kScrollButtonHeight = 20;
28
29// Height of the separator.
30const int kSeparatorHeight = 1;
31
32// Padding between tabs and scroll button.
33const int kScrollButtonVerticalPadding = 2;
34
35// The new tab button is rendered using a SideTab.
36class SideTabNewTabButton : public SideTab {
37 public:
38  explicit SideTabNewTabButton(TabStripController* controller);
39
40  virtual bool ShouldPaintHighlight() const OVERRIDE { return false; }
41  virtual bool IsSelected() const OVERRIDE { return false; }
42  virtual bool OnMousePressed(const views::MouseEvent& event) OVERRIDE;
43  virtual void OnMouseReleased(const views::MouseEvent& event) OVERRIDE;
44
45 private:
46  TabStripController* controller_;
47
48  DISALLOW_COPY_AND_ASSIGN(SideTabNewTabButton);
49};
50
51SideTabNewTabButton::SideTabNewTabButton(TabStripController* controller)
52    : SideTab(NULL),
53      controller_(controller) {
54  // Never show a close button for the new tab button.
55  close_button()->SetVisible(false);
56  TabRendererData data;
57  data.favicon = *ResourceBundle::GetSharedInstance().GetBitmapNamed(
58      IDR_SIDETABS_NEW_TAB);
59  data.title = l10n_util::GetStringUTF16(IDS_NEW_TAB_TITLE);
60  SetData(data);
61}
62
63bool SideTabNewTabButton::OnMousePressed(const views::MouseEvent& event) {
64  return true;
65}
66
67void SideTabNewTabButton::OnMouseReleased(const views::MouseEvent& event) {
68  if (event.IsOnlyLeftMouseButton() && HitTest(event.location()))
69    controller_->CreateNewTab();
70}
71
72// Button class used for the scroll buttons.
73class ScrollButton : public views::TextButton {
74 public:
75  enum Type {
76    UP,
77    DOWN
78  };
79
80  explicit ScrollButton(views::ButtonListener* listener, Type type);
81
82 protected:
83  // views::View overrides.
84  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
85
86 private:
87  const Type type_;
88
89  DISALLOW_COPY_AND_ASSIGN(ScrollButton);
90};
91
92ScrollButton::ScrollButton(views::ButtonListener* listener,
93                           Type type)
94    : views::TextButton(listener, std::wstring()),
95      type_(type) {
96}
97
98void ScrollButton::OnPaint(gfx::Canvas* canvas) {
99  TextButton::OnPaint(canvas);
100
101  // Draw the arrow.
102  SkColor arrow_color = IsEnabled() ? SK_ColorBLACK : SK_ColorGRAY;
103  int arrow_height = 5;
104  int x = width() / 2;
105  int y = (height() - arrow_height) / 2;
106  int delta_y = 1;
107  if (type_ == DOWN) {
108    delta_y = -1;
109    y += arrow_height;
110  }
111  for (int i = 0; i < arrow_height; ++i, --x, y += delta_y)
112    canvas->FillRectInt(arrow_color, x, y, (i * 2) + 1, 1);
113}
114
115}  // namespace
116
117// static
118const int SideTabStrip::kTabStripInset = 3;
119
120////////////////////////////////////////////////////////////////////////////////
121// SideTabStrip, public:
122
123SideTabStrip::SideTabStrip(TabStripController* controller)
124    : BaseTabStrip(controller, BaseTabStrip::VERTICAL_TAB_STRIP),
125      newtab_button_(new SideTabNewTabButton(controller)),
126      scroll_up_button_(NULL),
127      scroll_down_button_(NULL),
128      separator_(new views::View()),
129      first_tab_y_offset_(0),
130      ideal_height_(0) {
131  SetID(VIEW_ID_TAB_STRIP);
132  set_background(views::Background::CreateSolidBackground(kBackgroundColor));
133  AddChildView(newtab_button_);
134  separator_->set_background(
135      views::Background::CreateSolidBackground(kSeparatorColor));
136  AddChildView(separator_);
137  scroll_up_button_ = new ScrollButton(this, ScrollButton::UP);
138  AddChildView(scroll_up_button_);
139  scroll_down_button_ = new ScrollButton(this, ScrollButton::DOWN);
140  AddChildView(scroll_down_button_);
141}
142
143SideTabStrip::~SideTabStrip() {
144  DestroyDragController();
145}
146
147////////////////////////////////////////////////////////////////////////////////
148// SideTabStrip, AbstractTabStripView implementation:
149
150bool SideTabStrip::IsPositionInWindowCaption(const gfx::Point& point) {
151  return GetEventHandlerForPoint(point) == this;
152}
153
154void SideTabStrip::SetBackgroundOffset(const gfx::Point& offset) {
155}
156
157////////////////////////////////////////////////////////////////////////////////
158// SideTabStrip, BaseTabStrip implementation:
159
160void SideTabStrip::StartHighlight(int model_index) {
161}
162
163void SideTabStrip::StopAllHighlighting() {
164}
165
166BaseTab* SideTabStrip::CreateTabForDragging() {
167  SideTab* tab = new SideTab(NULL);
168  // Make sure the dragged tab shares our theme provider. We need to explicitly
169  // do this as during dragging there isn't a theme provider.
170  tab->set_theme_provider(GetThemeProvider());
171  return tab;
172}
173
174void SideTabStrip::RemoveTabAt(int model_index) {
175  StartRemoveTabAnimation(model_index);
176}
177
178void SideTabStrip::SelectTabAt(int old_model_index, int new_model_index) {
179  GetBaseTabAtModelIndex(new_model_index)->SchedulePaint();
180
181  if (controller()->IsActiveTab(new_model_index))
182    MakeTabVisible(ModelIndexToTabIndex(new_model_index));
183}
184
185void SideTabStrip::TabTitleChangedNotLoading(int model_index) {
186}
187
188gfx::Size SideTabStrip::GetPreferredSize() {
189  return gfx::Size(kTabStripWidth, 0);
190}
191
192void SideTabStrip::PaintChildren(gfx::Canvas* canvas) {
193  // Make sure any tabs being dragged appear on top of all others by painting
194  // them last.
195  std::vector<BaseTab*> dragging_tabs;
196
197  // Make sure nothing draws on top of the scroll buttons.
198  canvas->Save();
199  canvas->ClipRectInt(kTabStripInset, kTabStripInset,
200                      width() - kTabStripInset - kTabStripInset,
201                      GetMaxTabY() - kTabStripInset);
202
203  // Paint the new tab and separator first so that any tabs animating appear on
204  // top.
205  separator_->Paint(canvas);
206  newtab_button_->Paint(canvas);
207
208  for (int i = tab_count() - 1; i >= 0; --i) {
209    BaseTab* tab = base_tab_at_tab_index(i);
210    if (tab->dragging())
211      dragging_tabs.push_back(tab);
212    else
213      tab->Paint(canvas);
214  }
215
216  for (size_t i = 0; i < dragging_tabs.size(); ++i)
217    dragging_tabs[i]->Paint(canvas);
218
219  canvas->Restore();
220
221  scroll_down_button_->Paint(canvas);
222  scroll_up_button_->Paint(canvas);
223}
224
225views::View* SideTabStrip::GetEventHandlerForPoint(const gfx::Point& point) {
226  // Check the scroll buttons first as they visually appear on top of everything
227  // else.
228  if (scroll_down_button_->IsVisible()) {
229    gfx::Point local_point(point);
230    View::ConvertPointToView(this, scroll_down_button_, &local_point);
231    if (scroll_down_button_->HitTest(local_point))
232      return scroll_down_button_->GetEventHandlerForPoint(local_point);
233  }
234
235  if (scroll_up_button_->IsVisible()) {
236    gfx::Point local_point(point);
237    View::ConvertPointToView(this, scroll_up_button_, &local_point);
238    if (scroll_up_button_->HitTest(local_point))
239      return scroll_up_button_->GetEventHandlerForPoint(local_point);
240  }
241  return views::View::GetEventHandlerForPoint(point);
242}
243
244void SideTabStrip::ButtonPressed(views::Button* sender,
245                                 const views::Event& event) {
246  int max_offset = GetMaxOffset();
247  if (max_offset == 0) {
248    // All the tabs fit, no need to scroll.
249    return;
250  }
251
252  // Determine the index of the first visible tab.
253  int initial_y = kTabStripInset;
254  int first_vis_index = -1;
255  for (int i = 0; i < tab_count(); ++i) {
256    if (ideal_bounds(i).bottom() > initial_y) {
257      first_vis_index = i;
258      break;
259    }
260  }
261  if (first_vis_index == -1)
262    return;
263
264  int delta = 0;
265  if (sender == scroll_up_button_) {
266    delta = initial_y - ideal_bounds(first_vis_index).y();
267    if (delta <= 0) {
268      if (first_vis_index == 0) {
269        delta = -first_tab_y_offset_;
270      } else {
271        delta = initial_y - ideal_bounds(first_vis_index - 1).y();
272        DCHECK_NE(0, delta);  // Not fatal, but indicates we aren't scrolling.
273      }
274    }
275  } else {
276    DCHECK_EQ(sender, scroll_down_button_);
277    if (ideal_bounds(first_vis_index).y() > initial_y) {
278      delta = initial_y - ideal_bounds(first_vis_index).y();
279    } else if (first_vis_index + 1 == tab_count()) {
280      delta = -first_tab_y_offset_;
281    } else {
282      delta = initial_y - ideal_bounds(first_vis_index + 1).y();
283    }
284  }
285  SetFirstTabYOffset(first_tab_y_offset_ + delta);
286}
287
288BaseTab* SideTabStrip::CreateTab() {
289  return new SideTab(this);
290}
291
292void SideTabStrip::GenerateIdealBounds() {
293  gfx::Rect layout_rect = GetContentsBounds();
294  layout_rect.Inset(kTabStripInset, kTabStripInset);
295
296  int y = layout_rect.y() + first_tab_y_offset_;
297  bool last_was_mini = true;
298  bool has_non_closing_tab = false;
299  separator_bounds_.SetRect(0, -kSeparatorHeight, width(), kSeparatorHeight);
300  for (int i = 0; i < tab_count(); ++i) {
301    BaseTab* tab = base_tab_at_tab_index(i);
302    if (!tab->closing()) {
303      if (last_was_mini != tab->data().mini) {
304        if (has_non_closing_tab) {
305          separator_bounds_.SetRect(0, y, width(), kSeparatorHeight);
306          y += kSeparatorHeight + kVerticalTabSpacing;
307        }
308        newtab_button_bounds_.SetRect(
309            layout_rect.x(), y, layout_rect.width(),
310            newtab_button_->GetPreferredSize().height());
311        y = newtab_button_bounds_.bottom() + kVerticalTabSpacing;
312        last_was_mini = tab->data().mini;
313      }
314      gfx::Rect bounds = gfx::Rect(layout_rect.x(), y, layout_rect.width(),
315                                   tab->GetPreferredSize().height());
316      set_ideal_bounds(i, bounds);
317      y = bounds.bottom() + kVerticalTabSpacing;
318      has_non_closing_tab = true;
319    }
320  }
321
322  if (last_was_mini) {
323    if (has_non_closing_tab) {
324      separator_bounds_.SetRect(0, y, width(), kSeparatorHeight);
325      y += kSeparatorHeight + kVerticalTabSpacing;
326    }
327    newtab_button_bounds_ =
328        gfx::Rect(layout_rect.x(), y, layout_rect.width(),
329                  newtab_button_->GetPreferredSize().height());
330    y += newtab_button_->GetPreferredSize().height();
331  }
332
333  ideal_height_ = y - layout_rect.y() - first_tab_y_offset_;
334
335  scroll_up_button_->SetEnabled(first_tab_y_offset_ != 0);
336  scroll_down_button_->SetEnabled(GetMaxOffset() != 0 &&
337                                  first_tab_y_offset_ != GetMaxOffset());
338}
339
340void SideTabStrip::StartInsertTabAnimation(int model_index) {
341  PrepareForAnimation();
342
343  GenerateIdealBounds();
344
345  int tab_data_index = ModelIndexToTabIndex(model_index);
346  BaseTab* tab = base_tab_at_tab_index(tab_data_index);
347  if (model_index == 0) {
348    tab->SetBounds(ideal_bounds(tab_data_index).x(), 0,
349                   ideal_bounds(tab_data_index).width(), 0);
350  } else {
351    BaseTab* last_tab = base_tab_at_tab_index(tab_data_index - 1);
352    tab->SetBounds(last_tab->x(), last_tab->bounds().bottom(),
353                   ideal_bounds(tab_data_index).width(), 0);
354  }
355
356  AnimateToIdealBounds();
357}
358
359void SideTabStrip::AnimateToIdealBounds() {
360  for (int i = 0; i < tab_count(); ++i) {
361    BaseTab* tab = base_tab_at_tab_index(i);
362    if (!tab->closing() && !tab->dragging())
363      bounds_animator().AnimateViewTo(tab, ideal_bounds(i));
364  }
365
366  bounds_animator().AnimateViewTo(newtab_button_, newtab_button_bounds_);
367
368  bounds_animator().AnimateViewTo(separator_, separator_bounds_);
369}
370
371void SideTabStrip::DoLayout() {
372  BaseTabStrip::DoLayout();
373  newtab_button_->SetBoundsRect(newtab_button_bounds_);
374  separator_->SetBoundsRect(separator_bounds_);
375  int scroll_button_y = height() - kScrollButtonHeight;
376  scroll_up_button_->SetBounds(0, scroll_button_y, width() / 2,
377                               kScrollButtonHeight);
378  scroll_down_button_->SetBounds(width() / 2, scroll_button_y, width() / 2,
379                                 kScrollButtonHeight);
380}
381
382void SideTabStrip::LayoutDraggedTabsAt(const std::vector<BaseTab*>& tabs,
383                                       BaseTab* active_tab,
384                                       const gfx::Point& location,
385                                       bool initial_drag) {
386  // TODO: add support for initial_drag (see TabStrip's implementation).
387  gfx::Rect layout_rect = GetContentsBounds();
388  layout_rect.Inset(kTabStripInset, kTabStripInset);
389  int y = location.y();
390  for (size_t i = 0; i < tabs.size(); ++i) {
391    BaseTab* tab = tabs[i];
392    tab->SchedulePaint();
393    tab->SetBounds(layout_rect.x(), y, layout_rect.width(),
394                   tab->GetPreferredSize().height());
395    tab->SchedulePaint();
396    y += tab->height() + kVerticalTabSpacing;
397  }
398}
399
400void SideTabStrip::CalculateBoundsForDraggedTabs(
401    const std::vector<BaseTab*>& tabs,
402    std::vector<gfx::Rect>* bounds) {
403  int y = 0;
404  for (size_t i = 0; i < tabs.size(); ++i) {
405    BaseTab* tab = tabs[i];
406    gfx::Rect tab_bounds(tab->bounds());
407    tab_bounds.set_y(y);
408    y += tab->height() + kVerticalTabSpacing;
409    bounds->push_back(tab_bounds);
410  }
411}
412
413int SideTabStrip::GetSizeNeededForTabs(const std::vector<BaseTab*>& tabs) {
414  return static_cast<int>(tabs.size()) * SideTab::GetPreferredHeight();
415}
416
417void SideTabStrip::OnBoundsChanged(const gfx::Rect& previous_bounds) {
418  // When our height changes we may be able to show more.
419  first_tab_y_offset_ = std::max(GetMaxOffset(),
420                                 std::min(0, first_tab_y_offset_));
421  for (int i = 0; i < controller()->GetCount(); ++i) {
422    if (controller()->IsActiveTab(i)) {
423      MakeTabVisible(ModelIndexToTabIndex(i));
424      break;
425    }
426  }
427}
428
429void SideTabStrip::SetFirstTabYOffset(int new_offset) {
430  int max_offset = GetMaxOffset();
431  if (max_offset == 0) {
432    // All the tabs fit, no need to scroll.
433    return;
434  }
435  new_offset = std::max(max_offset, std::min(0, new_offset));
436  if (new_offset == first_tab_y_offset_)
437    return;
438
439  StopAnimating(false);
440  first_tab_y_offset_ = new_offset;
441  GenerateIdealBounds();
442  DoLayout();
443
444}
445
446int SideTabStrip::GetMaxOffset() const {
447  int available_height = GetMaxTabY() - kTabStripInset;
448  return std::min(0, available_height - ideal_height_);
449}
450
451int SideTabStrip::GetMaxTabY() const {
452  return height() - kTabStripInset - kScrollButtonVerticalPadding -
453      kScrollButtonHeight;
454}
455
456void SideTabStrip::MakeTabVisible(int tab_index) {
457  if (height() == 0)
458    return;
459
460  if (ideal_bounds(tab_index).y() < kTabStripInset) {
461    SetFirstTabYOffset(first_tab_y_offset_ - ideal_bounds(tab_index).y() +
462                       kTabStripInset);
463  } else if (ideal_bounds(tab_index).bottom() > GetMaxTabY()) {
464    SetFirstTabYOffset(GetMaxTabY() - (ideal_bounds(tab_index).bottom() -
465                                       first_tab_y_offset_));
466  }
467}
468