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 19 namespace { 20 21 const int kVerticalTabSpacing = 2; 22 const int kTabStripWidth = 140; 23 const SkColor kBackgroundColor = SkColorSetARGB(255, 209, 220, 248); 24 const SkColor kSeparatorColor = SkColorSetARGB(255, 151, 159, 179); 25 26 // Height of the scroll buttons. 27 const int kScrollButtonHeight = 20; 28 29 // Height of the separator. 30 const int kSeparatorHeight = 1; 31 32 // Padding between tabs and scroll button. 33 const int kScrollButtonVerticalPadding = 2; 34 35 // The new tab button is rendered using a SideTab. 36 class 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 51 SideTabNewTabButton::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 63 bool SideTabNewTabButton::OnMousePressed(const views::MouseEvent& event) { 64 return true; 65 } 66 67 void 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. 73 class 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 92 ScrollButton::ScrollButton(views::ButtonListener* listener, 93 Type type) 94 : views::TextButton(listener, std::wstring()), 95 type_(type) { 96 } 97 98 void 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 118 const int SideTabStrip::kTabStripInset = 3; 119 120 //////////////////////////////////////////////////////////////////////////////// 121 // SideTabStrip, public: 122 123 SideTabStrip::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 143 SideTabStrip::~SideTabStrip() { 144 DestroyDragController(); 145 } 146 147 //////////////////////////////////////////////////////////////////////////////// 148 // SideTabStrip, AbstractTabStripView implementation: 149 150 bool SideTabStrip::IsPositionInWindowCaption(const gfx::Point& point) { 151 return GetEventHandlerForPoint(point) == this; 152 } 153 154 void SideTabStrip::SetBackgroundOffset(const gfx::Point& offset) { 155 } 156 157 //////////////////////////////////////////////////////////////////////////////// 158 // SideTabStrip, BaseTabStrip implementation: 159 160 void SideTabStrip::StartHighlight(int model_index) { 161 } 162 163 void SideTabStrip::StopAllHighlighting() { 164 } 165 166 BaseTab* 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 174 void SideTabStrip::RemoveTabAt(int model_index) { 175 StartRemoveTabAnimation(model_index); 176 } 177 178 void 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 185 void SideTabStrip::TabTitleChangedNotLoading(int model_index) { 186 } 187 188 gfx::Size SideTabStrip::GetPreferredSize() { 189 return gfx::Size(kTabStripWidth, 0); 190 } 191 192 void 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 225 views::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 244 void 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 288 BaseTab* SideTabStrip::CreateTab() { 289 return new SideTab(this); 290 } 291 292 void 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 340 void 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 359 void 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 371 void 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 382 void 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 400 void 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 413 int SideTabStrip::GetSizeNeededForTabs(const std::vector<BaseTab*>& tabs) { 414 return static_cast<int>(tabs.size()) * SideTab::GetPreferredHeight(); 415 } 416 417 void 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 429 void 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 446 int SideTabStrip::GetMaxOffset() const { 447 int available_height = GetMaxTabY() - kTabStripInset; 448 return std::min(0, available_height - ideal_height_); 449 } 450 451 int SideTabStrip::GetMaxTabY() const { 452 return height() - kTabStripInset - kScrollButtonVerticalPadding - 453 kScrollButtonHeight; 454 } 455 456 void 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