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/tab_strip.h" 6 7 #include <algorithm> 8 9 #include "base/compiler_specific.h" 10 #include "base/stl_util-inl.h" 11 #include "base/utf_string_conversions.h" 12 #include "chrome/browser/defaults.h" 13 #include "chrome/browser/themes/theme_service.h" 14 #include "chrome/browser/ui/view_ids.h" 15 #include "chrome/browser/ui/views/tabs/tab.h" 16 #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" 17 #include "grit/generated_resources.h" 18 #include "grit/theme_resources.h" 19 #include "ui/base/accessibility/accessible_view_state.h" 20 #include "ui/base/animation/animation_container.h" 21 #include "ui/base/dragdrop/drag_drop_types.h" 22 #include "ui/base/l10n/l10n_util.h" 23 #include "ui/base/resource/resource_bundle.h" 24 #include "ui/gfx/canvas_skia.h" 25 #include "ui/gfx/path.h" 26 #include "ui/gfx/size.h" 27 #include "views/controls/image_view.h" 28 #include "views/widget/default_theme_provider.h" 29 #include "views/window/non_client_view.h" 30 #include "views/window/window.h" 31 32 #if defined(OS_WIN) 33 #include "views/widget/monitor_win.h" 34 #include "views/widget/widget_win.h" 35 #elif defined(OS_LINUX) 36 #include "views/widget/widget_gtk.h" 37 #endif 38 39 #undef min 40 #undef max 41 42 #if defined(COMPILER_GCC) 43 // Squash false positive signed overflow warning in GenerateStartAndEndWidths 44 // when doing 'start_tab_count < end_tab_count'. 45 #pragma GCC diagnostic ignored "-Wstrict-overflow" 46 #endif 47 48 using views::DropTargetEvent; 49 50 static const int kNewTabButtonHOffset = -5; 51 static const int kNewTabButtonVOffset = 5; 52 static const int kSuspendAnimationsTimeMs = 200; 53 static const int kTabHOffset = -16; 54 static const int kTabStripAnimationVSlop = 40; 55 56 // Size of the drop indicator. 57 static int drop_indicator_width; 58 static int drop_indicator_height; 59 60 static inline int Round(double x) { 61 // Why oh why is this not in a standard header? 62 return static_cast<int>(floor(x + 0.5)); 63 } 64 65 namespace { 66 67 /////////////////////////////////////////////////////////////////////////////// 68 // NewTabButton 69 // 70 // A subclass of button that hit-tests to the shape of the new tab button. 71 72 class NewTabButton : public views::ImageButton { 73 public: 74 explicit NewTabButton(views::ButtonListener* listener) 75 : views::ImageButton(listener) { 76 } 77 virtual ~NewTabButton() {} 78 79 protected: 80 // Overridden from views::View: 81 virtual bool HasHitTestMask() const { 82 // When the button is sized to the top of the tab strip we want the user to 83 // be able to click on complete bounds, and so don't return a custom hit 84 // mask. 85 return !browser_defaults::kSizeTabButtonToTopOfTabStrip; 86 } 87 virtual void GetHitTestMask(gfx::Path* path) const { 88 DCHECK(path); 89 90 SkScalar w = SkIntToScalar(width()); 91 92 // These values are defined by the shape of the new tab bitmap. Should that 93 // bitmap ever change, these values will need to be updated. They're so 94 // custom it's not really worth defining constants for. 95 path->moveTo(0, 1); 96 path->lineTo(w - 7, 1); 97 path->lineTo(w - 4, 4); 98 path->lineTo(w, 16); 99 path->lineTo(w - 1, 17); 100 path->lineTo(7, 17); 101 path->lineTo(4, 13); 102 path->lineTo(0, 1); 103 path->close(); 104 } 105 106 private: 107 DISALLOW_COPY_AND_ASSIGN(NewTabButton); 108 }; 109 110 } // namespace 111 112 /////////////////////////////////////////////////////////////////////////////// 113 // TabStrip, public: 114 115 // static 116 const int TabStrip::mini_to_non_mini_gap_ = 3; 117 118 TabStrip::TabStrip(TabStripController* controller) 119 : BaseTabStrip(controller, BaseTabStrip::HORIZONTAL_TAB_STRIP), 120 newtab_button_(NULL), 121 current_unselected_width_(Tab::GetStandardSize().width()), 122 current_selected_width_(Tab::GetStandardSize().width()), 123 available_width_for_tabs_(-1), 124 in_tab_close_(false), 125 animation_container_(new ui::AnimationContainer()) { 126 Init(); 127 } 128 129 TabStrip::~TabStrip() { 130 // The animations may reference the tabs. Shut down the animation before we 131 // delete the tabs. 132 StopAnimating(false); 133 134 DestroyDragController(); 135 136 // Make sure we unhook ourselves as a message loop observer so that we don't 137 // crash in the case where the user closes the window after closing a tab 138 // but before moving the mouse. 139 RemoveMessageLoopObserver(); 140 141 // The children (tabs) may callback to us from their destructor. Delete them 142 // so that if they call back we aren't in a weird state. 143 RemoveAllChildViews(true); 144 } 145 146 void TabStrip::InitTabStripButtons() { 147 newtab_button_ = new NewTabButton(this); 148 if (browser_defaults::kSizeTabButtonToTopOfTabStrip) { 149 newtab_button_->SetImageAlignment(views::ImageButton::ALIGN_LEFT, 150 views::ImageButton::ALIGN_BOTTOM); 151 } 152 LoadNewTabButtonImage(); 153 newtab_button_->SetAccessibleName( 154 l10n_util::GetStringUTF16(IDS_ACCNAME_NEWTAB)); 155 AddChildView(newtab_button_); 156 } 157 158 gfx::Rect TabStrip::GetNewTabButtonBounds() { 159 return newtab_button_->bounds(); 160 } 161 162 void TabStrip::MouseMovedOutOfView() { 163 ResizeLayoutTabs(); 164 } 165 166 //////////////////////////////////////////////////////////////////////////////// 167 // TabStrip, AbstractTabStripView implementation: 168 169 bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) { 170 views::View* v = GetEventHandlerForPoint(point); 171 172 // If there is no control at this location, claim the hit was in the title 173 // bar to get a move action. 174 if (v == this) 175 return true; 176 177 // Check to see if the point is within the non-button parts of the new tab 178 // button. The button has a non-rectangular shape, so if it's not in the 179 // visual portions of the button we treat it as a click to the caption. 180 gfx::Point point_in_newtab_coords(point); 181 View::ConvertPointToView(this, newtab_button_, &point_in_newtab_coords); 182 if (newtab_button_->bounds().Contains(point) && 183 !newtab_button_->HitTest(point_in_newtab_coords)) { 184 return true; 185 } 186 187 // All other regions, including the new Tab button, should be considered part 188 // of the containing Window's client area so that regular events can be 189 // processed for them. 190 return false; 191 } 192 193 void TabStrip::SetBackgroundOffset(const gfx::Point& offset) { 194 for (int i = 0; i < tab_count(); ++i) 195 GetTabAtTabDataIndex(i)->set_background_offset(offset); 196 } 197 198 //////////////////////////////////////////////////////////////////////////////// 199 // TabStrip, BaseTabStrip implementation: 200 201 void TabStrip::PrepareForCloseAt(int model_index) { 202 if (!in_tab_close_ && IsAnimating()) { 203 // Cancel any current animations. We do this as remove uses the current 204 // ideal bounds and we need to know ideal bounds is in a good state. 205 StopAnimating(true); 206 } 207 208 int model_count = GetModelCount(); 209 if (model_index + 1 != model_count && model_count > 1) { 210 // The user is about to close a tab other than the last tab. Set 211 // available_width_for_tabs_ so that if we do a layout we don't position a 212 // tab past the end of the second to last tab. We do this so that as the 213 // user closes tabs with the mouse a tab continues to fall under the mouse. 214 available_width_for_tabs_ = GetAvailableWidthForTabs( 215 GetTabAtModelIndex(model_count - 2)); 216 } 217 218 in_tab_close_ = true; 219 AddMessageLoopObserver(); 220 } 221 222 void TabStrip::RemoveTabAt(int model_index) { 223 if (in_tab_close_ && model_index != GetModelCount()) 224 StartMouseInitiatedRemoveTabAnimation(model_index); 225 else 226 StartRemoveTabAnimation(model_index); 227 } 228 229 void TabStrip::SelectTabAt(int old_model_index, int new_model_index) { 230 // We have "tiny tabs" if the tabs are so tiny that the unselected ones are 231 // a different size to the selected ones. 232 bool tiny_tabs = current_unselected_width_ != current_selected_width_; 233 if (!IsAnimating() && (!in_tab_close_ || tiny_tabs)) { 234 DoLayout(); 235 } else { 236 SchedulePaint(); 237 } 238 239 if (old_model_index >= 0) { 240 GetTabAtTabDataIndex(ModelIndexToTabIndex(old_model_index))-> 241 StopMiniTabTitleAnimation(); 242 } 243 } 244 245 void TabStrip::TabTitleChangedNotLoading(int model_index) { 246 Tab* tab = GetTabAtModelIndex(model_index); 247 if (tab->data().mini && !tab->IsActive()) 248 tab->StartMiniTabTitleAnimation(); 249 } 250 251 void TabStrip::StartHighlight(int model_index) { 252 GetTabAtModelIndex(model_index)->StartPulse(); 253 } 254 255 void TabStrip::StopAllHighlighting() { 256 for (int i = 0; i < tab_count(); ++i) 257 GetTabAtTabDataIndex(i)->StopPulse(); 258 } 259 260 BaseTab* TabStrip::CreateTabForDragging() { 261 Tab* tab = new Tab(NULL); 262 // Make sure the dragged tab shares our theme provider. We need to explicitly 263 // do this as during dragging there isn't a theme provider. 264 tab->set_theme_provider(GetThemeProvider()); 265 return tab; 266 } 267 268 /////////////////////////////////////////////////////////////////////////////// 269 // TabStrip, views::View overrides: 270 271 void TabStrip::PaintChildren(gfx::Canvas* canvas) { 272 // Tabs are painted in reverse order, so they stack to the left. 273 Tab* active_tab = NULL; 274 std::vector<Tab*> tabs_dragging; 275 std::vector<Tab*> selected_tabs; 276 bool is_dragging = false; 277 278 for (int i = tab_count() - 1; i >= 0; --i) { 279 // We must ask the _Tab's_ model, not ourselves, because in some situations 280 // the model will be different to this object, e.g. when a Tab is being 281 // removed after its TabContents has been destroyed. 282 Tab* tab = GetTabAtTabDataIndex(i); 283 if (tab->dragging()) { 284 is_dragging = true; 285 if (tab->IsActive()) 286 active_tab = tab; 287 else 288 tabs_dragging.push_back(tab); 289 } else if (!tab->IsActive()) { 290 if (!tab->IsSelected()) 291 tab->Paint(canvas); 292 else 293 selected_tabs.push_back(tab); 294 } else { 295 active_tab = tab; 296 } 297 } 298 299 if (GetWindow()->non_client_view()->UseNativeFrame()) { 300 bool multiple_tabs_selected = (!selected_tabs.empty() || 301 tabs_dragging.size() > 1); 302 // Make sure non-active tabs are somewhat transparent. 303 SkPaint paint; 304 // If there are multiple tabs selected, fade non-selected tabs more to make 305 // the selected tabs more noticable. 306 paint.setColor(SkColorSetARGB( 307 multiple_tabs_selected ? 150 : 200, 255, 255, 255)); 308 paint.setXfermodeMode(SkXfermode::kDstIn_Mode); 309 paint.setStyle(SkPaint::kFill_Style); 310 canvas->DrawRectInt(0, 0, width(), 311 height() - 2, // Visible region that overlaps the toolbar. 312 paint); 313 } 314 315 // Now selected but not active. We don't want these dimmed if using native 316 // frame, so they're painted after initial pass. 317 for (size_t i = 0; i < selected_tabs.size(); ++i) 318 selected_tabs[i]->Paint(canvas); 319 320 // Next comes the active tab. 321 if (active_tab && !is_dragging) 322 active_tab->Paint(canvas); 323 324 // Paint the New Tab button. 325 newtab_button_->Paint(canvas); 326 327 // And the dragged tabs. 328 for (size_t i = 0; i < tabs_dragging.size(); ++i) 329 tabs_dragging[i]->Paint(canvas); 330 331 // If the active tab is being dragged, it goes last. 332 if (active_tab && is_dragging) 333 active_tab->Paint(canvas); 334 } 335 336 // Overridden to support automation. See automation_proxy_uitest.cc. 337 const views::View* TabStrip::GetViewByID(int view_id) const { 338 if (tab_count() > 0) { 339 if (view_id == VIEW_ID_TAB_LAST) { 340 return GetTabAtTabDataIndex(tab_count() - 1); 341 } else if ((view_id >= VIEW_ID_TAB_0) && (view_id < VIEW_ID_TAB_LAST)) { 342 int index = view_id - VIEW_ID_TAB_0; 343 if (index >= 0 && index < tab_count()) { 344 return GetTabAtTabDataIndex(index); 345 } else { 346 return NULL; 347 } 348 } 349 } 350 351 return View::GetViewByID(view_id); 352 } 353 354 gfx::Size TabStrip::GetPreferredSize() { 355 return gfx::Size(0, Tab::GetMinimumUnselectedSize().height()); 356 } 357 358 void TabStrip::OnDragEntered(const DropTargetEvent& event) { 359 // Force animations to stop, otherwise it makes the index calculation tricky. 360 StopAnimating(true); 361 362 UpdateDropIndex(event); 363 } 364 365 int TabStrip::OnDragUpdated(const DropTargetEvent& event) { 366 UpdateDropIndex(event); 367 return GetDropEffect(event); 368 } 369 370 void TabStrip::OnDragExited() { 371 SetDropIndex(-1, false); 372 } 373 374 int TabStrip::OnPerformDrop(const DropTargetEvent& event) { 375 if (!drop_info_.get()) 376 return ui::DragDropTypes::DRAG_NONE; 377 378 const int drop_index = drop_info_->drop_index; 379 const bool drop_before = drop_info_->drop_before; 380 381 // Hide the drop indicator. 382 SetDropIndex(-1, false); 383 384 GURL url; 385 string16 title; 386 if (!event.data().GetURLAndTitle(&url, &title) || !url.is_valid()) 387 return ui::DragDropTypes::DRAG_NONE; 388 389 controller()->PerformDrop(drop_before, drop_index, url); 390 391 return GetDropEffect(event); 392 } 393 394 void TabStrip::GetAccessibleState(ui::AccessibleViewState* state) { 395 state->role = ui::AccessibilityTypes::ROLE_PAGETABLIST; 396 } 397 398 views::View* TabStrip::GetEventHandlerForPoint(const gfx::Point& point) { 399 // Return any view that isn't a Tab or this TabStrip immediately. We don't 400 // want to interfere. 401 views::View* v = View::GetEventHandlerForPoint(point); 402 if (v && v != this && v->GetClassName() != Tab::kViewClassName) 403 return v; 404 405 // The display order doesn't necessarily match the child list order, so we 406 // walk the display list hit-testing Tabs. Since the active tab always 407 // renders on top of adjacent tabs, it needs to be hit-tested before any 408 // left-adjacent Tab, so we look ahead for it as we walk. 409 for (int i = 0; i < tab_count(); ++i) { 410 Tab* next_tab = i < (tab_count() - 1) ? GetTabAtTabDataIndex(i + 1) : NULL; 411 if (next_tab && next_tab->IsActive() && IsPointInTab(next_tab, point)) 412 return next_tab; 413 Tab* tab = GetTabAtTabDataIndex(i); 414 if (IsPointInTab(tab, point)) 415 return tab; 416 } 417 418 // No need to do any floating view stuff, we don't use them in the TabStrip. 419 return this; 420 } 421 422 void TabStrip::OnThemeChanged() { 423 LoadNewTabButtonImage(); 424 } 425 426 BaseTab* TabStrip::CreateTab() { 427 Tab* tab = new Tab(this); 428 tab->set_animation_container(animation_container_.get()); 429 return tab; 430 } 431 432 void TabStrip::StartInsertTabAnimation(int model_index) { 433 PrepareForAnimation(); 434 435 // The TabStrip can now use its entire width to lay out Tabs. 436 in_tab_close_ = false; 437 available_width_for_tabs_ = -1; 438 439 GenerateIdealBounds(); 440 441 int tab_data_index = ModelIndexToTabIndex(model_index); 442 BaseTab* tab = base_tab_at_tab_index(tab_data_index); 443 if (model_index == 0) { 444 tab->SetBounds(0, ideal_bounds(tab_data_index).y(), 0, 445 ideal_bounds(tab_data_index).height()); 446 } else { 447 BaseTab* last_tab = base_tab_at_tab_index(tab_data_index - 1); 448 tab->SetBounds(last_tab->bounds().right() + kTabHOffset, 449 ideal_bounds(tab_data_index).y(), 0, 450 ideal_bounds(tab_data_index).height()); 451 } 452 453 AnimateToIdealBounds(); 454 } 455 456 void TabStrip::AnimateToIdealBounds() { 457 for (int i = 0; i < tab_count(); ++i) { 458 Tab* tab = GetTabAtTabDataIndex(i); 459 if (!tab->closing() && !tab->dragging()) 460 bounds_animator().AnimateViewTo(tab, ideal_bounds(i)); 461 } 462 463 bounds_animator().AnimateViewTo(newtab_button_, newtab_button_bounds_); 464 } 465 466 bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { 467 return in_tab_close_; 468 } 469 470 void TabStrip::DoLayout() { 471 BaseTabStrip::DoLayout(); 472 473 newtab_button_->SetBoundsRect(newtab_button_bounds_); 474 } 475 476 void TabStrip::LayoutDraggedTabsAt(const std::vector<BaseTab*>& tabs, 477 BaseTab* active_tab, 478 const gfx::Point& location, 479 bool initial_drag) { 480 std::vector<gfx::Rect> bounds; 481 CalculateBoundsForDraggedTabs(tabs, &bounds); 482 DCHECK_EQ(tabs.size(), bounds.size()); 483 int active_tab_model_index = GetModelIndexOfBaseTab(active_tab); 484 int active_tab_index = static_cast<int>( 485 std::find(tabs.begin(), tabs.end(), active_tab) - tabs.begin()); 486 for (size_t i = 0; i < tabs.size(); ++i) { 487 BaseTab* tab = tabs[i]; 488 gfx::Rect new_bounds = bounds[i]; 489 new_bounds.Offset(location.x(), location.y()); 490 int consecutive_index = 491 active_tab_model_index - (active_tab_index - static_cast<int>(i)); 492 // If this is the initial layout during a drag and the tabs aren't 493 // consecutive animate the view into position. Do the same if the tab is 494 // already animating (which means we previously caused it to animate). 495 if ((initial_drag && 496 GetModelIndexOfBaseTab(tabs[i]) != consecutive_index) || 497 bounds_animator().IsAnimating(tabs[i])) { 498 bounds_animator().SetTargetBounds(tabs[i], new_bounds); 499 } else { 500 tab->SetBoundsRect(new_bounds); 501 } 502 } 503 } 504 505 void TabStrip::CalculateBoundsForDraggedTabs(const std::vector<BaseTab*>& tabs, 506 std::vector<gfx::Rect>* bounds) { 507 int x = 0; 508 for (size_t i = 0; i < tabs.size(); ++i) { 509 BaseTab* tab = tabs[i]; 510 if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) 511 x += mini_to_non_mini_gap_; 512 gfx::Rect new_bounds = tab->bounds(); 513 new_bounds.set_origin(gfx::Point(x, 0)); 514 bounds->push_back(new_bounds); 515 x += tab->width() + kTabHOffset; 516 } 517 } 518 519 int TabStrip::GetSizeNeededForTabs(const std::vector<BaseTab*>& tabs) { 520 int width = 0; 521 for (size_t i = 0; i < tabs.size(); ++i) { 522 BaseTab* tab = tabs[i]; 523 width += tab->width(); 524 if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) 525 width += mini_to_non_mini_gap_; 526 } 527 if (tabs.size() > 0) 528 width += kTabHOffset * static_cast<int>(tabs.size() - 1); 529 return width; 530 } 531 532 void TabStrip::ViewHierarchyChanged(bool is_add, 533 views::View* parent, 534 views::View* child) { 535 if (is_add && child == this) 536 InitTabStripButtons(); 537 } 538 539 /////////////////////////////////////////////////////////////////////////////// 540 // TabStrip, views::BaseButton::ButtonListener implementation: 541 542 void TabStrip::ButtonPressed(views::Button* sender, const views::Event& event) { 543 if (sender == newtab_button_) 544 controller()->CreateNewTab(); 545 } 546 547 /////////////////////////////////////////////////////////////////////////////// 548 // TabStrip, private: 549 550 void TabStrip::Init() { 551 SetID(VIEW_ID_TAB_STRIP); 552 newtab_button_bounds_.SetRect(0, 0, kNewTabButtonWidth, kNewTabButtonHeight); 553 if (browser_defaults::kSizeTabButtonToTopOfTabStrip) { 554 newtab_button_bounds_.set_height( 555 kNewTabButtonHeight + kNewTabButtonVOffset); 556 } 557 if (drop_indicator_width == 0) { 558 // Direction doesn't matter, both images are the same size. 559 SkBitmap* drop_image = GetDropArrowImage(true); 560 drop_indicator_width = drop_image->width(); 561 drop_indicator_height = drop_image->height(); 562 } 563 } 564 565 void TabStrip::LoadNewTabButtonImage() { 566 ui::ThemeProvider* tp = GetThemeProvider(); 567 568 // If we don't have a theme provider yet, it means we do not have a 569 // root view, and are therefore in a test. 570 bool in_test = false; 571 if (tp == NULL) { 572 tp = new views::DefaultThemeProvider(); 573 in_test = true; 574 } 575 576 SkBitmap* bitmap = tp->GetBitmapNamed(IDR_NEWTAB_BUTTON); 577 SkColor color = tp->GetColor(ThemeService::COLOR_BUTTON_BACKGROUND); 578 SkBitmap* background = tp->GetBitmapNamed( 579 IDR_THEME_WINDOW_CONTROL_BACKGROUND); 580 581 newtab_button_->SetImage(views::CustomButton::BS_NORMAL, bitmap); 582 newtab_button_->SetImage(views::CustomButton::BS_PUSHED, 583 tp->GetBitmapNamed(IDR_NEWTAB_BUTTON_P)); 584 newtab_button_->SetImage(views::CustomButton::BS_HOT, 585 tp->GetBitmapNamed(IDR_NEWTAB_BUTTON_H)); 586 newtab_button_->SetBackground(color, background, 587 tp->GetBitmapNamed(IDR_NEWTAB_BUTTON_MASK)); 588 if (in_test) 589 delete tp; 590 } 591 592 Tab* TabStrip::GetTabAtTabDataIndex(int tab_data_index) const { 593 return static_cast<Tab*>(base_tab_at_tab_index(tab_data_index)); 594 } 595 596 Tab* TabStrip::GetTabAtModelIndex(int model_index) const { 597 return GetTabAtTabDataIndex(ModelIndexToTabIndex(model_index)); 598 } 599 600 void TabStrip::GetCurrentTabWidths(double* unselected_width, 601 double* selected_width) const { 602 *unselected_width = current_unselected_width_; 603 *selected_width = current_selected_width_; 604 } 605 606 void TabStrip::GetDesiredTabWidths(int tab_count, 607 int mini_tab_count, 608 double* unselected_width, 609 double* selected_width) const { 610 DCHECK(tab_count >= 0 && mini_tab_count >= 0 && mini_tab_count <= tab_count); 611 const double min_unselected_width = Tab::GetMinimumUnselectedSize().width(); 612 const double min_selected_width = Tab::GetMinimumSelectedSize().width(); 613 614 *unselected_width = min_unselected_width; 615 *selected_width = min_selected_width; 616 617 if (tab_count == 0) { 618 // Return immediately to avoid divide-by-zero below. 619 return; 620 } 621 622 // Determine how much space we can actually allocate to tabs. 623 int available_width; 624 if (available_width_for_tabs_ < 0) { 625 available_width = width(); 626 available_width -= (kNewTabButtonHOffset + newtab_button_bounds_.width()); 627 } else { 628 // Interesting corner case: if |available_width_for_tabs_| > the result 629 // of the calculation in the conditional arm above, the strip is in 630 // overflow. We can either use the specified width or the true available 631 // width here; the first preserves the consistent "leave the last tab under 632 // the user's mouse so they can close many tabs" behavior at the cost of 633 // prolonging the glitchy appearance of the overflow state, while the second 634 // gets us out of overflow as soon as possible but forces the user to move 635 // their mouse for a few tabs' worth of closing. We choose visual 636 // imperfection over behavioral imperfection and select the first option. 637 available_width = available_width_for_tabs_; 638 } 639 640 if (mini_tab_count > 0) { 641 available_width -= mini_tab_count * (Tab::GetMiniWidth() + kTabHOffset); 642 tab_count -= mini_tab_count; 643 if (tab_count == 0) { 644 *selected_width = *unselected_width = Tab::GetStandardSize().width(); 645 return; 646 } 647 // Account for gap between the last mini-tab and first non-mini-tab. 648 available_width -= mini_to_non_mini_gap_; 649 } 650 651 // Calculate the desired tab widths by dividing the available space into equal 652 // portions. Don't let tabs get larger than the "standard width" or smaller 653 // than the minimum width for each type, respectively. 654 const int total_offset = kTabHOffset * (tab_count - 1); 655 const double desired_tab_width = std::min((static_cast<double>( 656 available_width - total_offset) / static_cast<double>(tab_count)), 657 static_cast<double>(Tab::GetStandardSize().width())); 658 *unselected_width = std::max(desired_tab_width, min_unselected_width); 659 *selected_width = std::max(desired_tab_width, min_selected_width); 660 661 // When there are multiple tabs, we'll have one selected and some unselected 662 // tabs. If the desired width was between the minimum sizes of these types, 663 // try to shrink the tabs with the smaller minimum. For example, if we have 664 // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5. If 665 // selected tabs have a minimum width of 4 and unselected tabs have a minimum 666 // width of 1, the above code would set *unselected_width = 2.5, 667 // *selected_width = 4, which results in a total width of 11.5. Instead, we 668 // want to set *unselected_width = 2, *selected_width = 4, for a total width 669 // of 10. 670 if (tab_count > 1) { 671 if ((min_unselected_width < min_selected_width) && 672 (desired_tab_width < min_selected_width)) { 673 // Unselected width = (total width - selected width) / (num_tabs - 1) 674 *unselected_width = std::max(static_cast<double>( 675 available_width - total_offset - min_selected_width) / 676 static_cast<double>(tab_count - 1), min_unselected_width); 677 } else if ((min_unselected_width > min_selected_width) && 678 (desired_tab_width < min_unselected_width)) { 679 // Selected width = (total width - (unselected width * (num_tabs - 1))) 680 *selected_width = std::max(available_width - total_offset - 681 (min_unselected_width * (tab_count - 1)), min_selected_width); 682 } 683 } 684 } 685 686 void TabStrip::ResizeLayoutTabs() { 687 // We've been called back after the TabStrip has been emptied out (probably 688 // just prior to the window being destroyed). We need to do nothing here or 689 // else GetTabAt below will crash. 690 if (tab_count() == 0) 691 return; 692 693 // It is critically important that this is unhooked here, otherwise we will 694 // keep spying on messages forever. 695 RemoveMessageLoopObserver(); 696 697 in_tab_close_ = false; 698 available_width_for_tabs_ = -1; 699 int mini_tab_count = GetMiniTabCount(); 700 if (mini_tab_count == tab_count()) { 701 // Only mini-tabs, we know the tab widths won't have changed (all 702 // mini-tabs have the same width), so there is nothing to do. 703 return; 704 } 705 Tab* first_tab = GetTabAtTabDataIndex(mini_tab_count); 706 double unselected, selected; 707 GetDesiredTabWidths(tab_count(), mini_tab_count, &unselected, &selected); 708 // TODO: this is always selected, should it be 'selected : unselected'? 709 int w = Round(first_tab->IsActive() ? selected : selected); 710 711 // We only want to run the animation if we're not already at the desired 712 // size. 713 if (abs(first_tab->width() - w) > 1) 714 StartResizeLayoutAnimation(); 715 } 716 717 void TabStrip::AddMessageLoopObserver() { 718 if (!mouse_watcher_.get()) { 719 mouse_watcher_.reset( 720 new views::MouseWatcher(this, this, 721 gfx::Insets(0, 0, kTabStripAnimationVSlop, 0))); 722 } 723 mouse_watcher_->Start(); 724 } 725 726 void TabStrip::RemoveMessageLoopObserver() { 727 mouse_watcher_.reset(NULL); 728 } 729 730 gfx::Rect TabStrip::GetDropBounds(int drop_index, 731 bool drop_before, 732 bool* is_beneath) { 733 DCHECK(drop_index != -1); 734 int center_x; 735 if (drop_index < tab_count()) { 736 Tab* tab = GetTabAtTabDataIndex(drop_index); 737 if (drop_before) 738 center_x = tab->x() - (kTabHOffset / 2); 739 else 740 center_x = tab->x() + (tab->width() / 2); 741 } else { 742 Tab* last_tab = GetTabAtTabDataIndex(drop_index - 1); 743 center_x = last_tab->x() + last_tab->width() + (kTabHOffset / 2); 744 } 745 746 // Mirror the center point if necessary. 747 center_x = GetMirroredXInView(center_x); 748 749 // Determine the screen bounds. 750 gfx::Point drop_loc(center_x - drop_indicator_width / 2, 751 -drop_indicator_height); 752 ConvertPointToScreen(this, &drop_loc); 753 gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), drop_indicator_width, 754 drop_indicator_height); 755 756 // If the rect doesn't fit on the monitor, push the arrow to the bottom. 757 #if defined(OS_WIN) 758 gfx::Rect monitor_bounds = views::GetMonitorBoundsForRect(drop_bounds); 759 *is_beneath = (monitor_bounds.IsEmpty() || 760 !monitor_bounds.Contains(drop_bounds)); 761 #else 762 *is_beneath = false; 763 NOTIMPLEMENTED(); 764 #endif 765 if (*is_beneath) 766 drop_bounds.Offset(0, drop_bounds.height() + height()); 767 768 return drop_bounds; 769 } 770 771 void TabStrip::UpdateDropIndex(const DropTargetEvent& event) { 772 // If the UI layout is right-to-left, we need to mirror the mouse 773 // coordinates since we calculate the drop index based on the 774 // original (and therefore non-mirrored) positions of the tabs. 775 const int x = GetMirroredXInView(event.x()); 776 // We don't allow replacing the urls of mini-tabs. 777 for (int i = GetMiniTabCount(); i < tab_count(); ++i) { 778 Tab* tab = GetTabAtTabDataIndex(i); 779 const int tab_max_x = tab->x() + tab->width(); 780 const int hot_width = tab->width() / 3; 781 if (x < tab_max_x) { 782 if (x < tab->x() + hot_width) 783 SetDropIndex(i, true); 784 else if (x >= tab_max_x - hot_width) 785 SetDropIndex(i + 1, true); 786 else 787 SetDropIndex(i, false); 788 return; 789 } 790 } 791 792 // The drop isn't over a tab, add it to the end. 793 SetDropIndex(tab_count(), true); 794 } 795 796 void TabStrip::SetDropIndex(int tab_data_index, bool drop_before) { 797 if (tab_data_index == -1) { 798 if (drop_info_.get()) 799 drop_info_.reset(NULL); 800 return; 801 } 802 803 if (drop_info_.get() && drop_info_->drop_index == tab_data_index && 804 drop_info_->drop_before == drop_before) { 805 return; 806 } 807 808 bool is_beneath; 809 gfx::Rect drop_bounds = GetDropBounds(tab_data_index, drop_before, 810 &is_beneath); 811 812 if (!drop_info_.get()) { 813 drop_info_.reset(new DropInfo(tab_data_index, drop_before, !is_beneath)); 814 } else { 815 drop_info_->drop_index = tab_data_index; 816 drop_info_->drop_before = drop_before; 817 if (is_beneath == drop_info_->point_down) { 818 drop_info_->point_down = !is_beneath; 819 drop_info_->arrow_view->SetImage( 820 GetDropArrowImage(drop_info_->point_down)); 821 } 822 } 823 824 // Reposition the window. Need to show it too as the window is initially 825 // hidden. 826 drop_info_->arrow_window->SetBounds(drop_bounds); 827 drop_info_->arrow_window->Show(); 828 } 829 830 int TabStrip::GetDropEffect(const views::DropTargetEvent& event) { 831 const int source_ops = event.source_operations(); 832 if (source_ops & ui::DragDropTypes::DRAG_COPY) 833 return ui::DragDropTypes::DRAG_COPY; 834 if (source_ops & ui::DragDropTypes::DRAG_LINK) 835 return ui::DragDropTypes::DRAG_LINK; 836 return ui::DragDropTypes::DRAG_MOVE; 837 } 838 839 // static 840 SkBitmap* TabStrip::GetDropArrowImage(bool is_down) { 841 return ResourceBundle::GetSharedInstance().GetBitmapNamed( 842 is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); 843 } 844 845 // TabStrip::DropInfo ---------------------------------------------------------- 846 847 TabStrip::DropInfo::DropInfo(int drop_index, bool drop_before, bool point_down) 848 : drop_index(drop_index), 849 drop_before(drop_before), 850 point_down(point_down) { 851 arrow_view = new views::ImageView; 852 arrow_view->SetImage(GetDropArrowImage(point_down)); 853 854 views::Widget::CreateParams params(views::Widget::CreateParams::TYPE_POPUP); 855 params.keep_on_top = true; 856 params.transparent = true; 857 params.accept_events = false; 858 params.can_activate = false; 859 arrow_window = views::Widget::CreateWidget(params); 860 arrow_window->Init( 861 NULL, 862 gfx::Rect(0, 0, drop_indicator_width, drop_indicator_height)); 863 arrow_window->SetContentsView(arrow_view); 864 } 865 866 TabStrip::DropInfo::~DropInfo() { 867 // Close eventually deletes the window, which deletes arrow_view too. 868 arrow_window->Close(); 869 } 870 871 /////////////////////////////////////////////////////////////////////////////// 872 873 // Called from: 874 // - BasicLayout 875 // - Tab insertion/removal 876 // - Tab reorder 877 void TabStrip::GenerateIdealBounds() { 878 int non_closing_tab_count = 0; 879 int mini_tab_count = 0; 880 for (int i = 0; i < tab_count(); ++i) { 881 BaseTab* tab = base_tab_at_tab_index(i); 882 if (!tab->closing()) { 883 ++non_closing_tab_count; 884 if (tab->data().mini) 885 mini_tab_count++; 886 } 887 } 888 889 double unselected, selected; 890 GetDesiredTabWidths(non_closing_tab_count, mini_tab_count, &unselected, 891 &selected); 892 893 current_unselected_width_ = unselected; 894 current_selected_width_ = selected; 895 896 // NOTE: This currently assumes a tab's height doesn't differ based on 897 // selected state or the number of tabs in the strip! 898 int tab_height = Tab::GetStandardSize().height(); 899 double tab_x = 0; 900 bool last_was_mini = false; 901 for (int i = 0; i < tab_count(); ++i) { 902 Tab* tab = GetTabAtTabDataIndex(i); 903 if (!tab->closing()) { 904 double tab_width = unselected; 905 if (tab->data().mini) { 906 tab_width = Tab::GetMiniWidth(); 907 } else { 908 if (last_was_mini) { 909 // Give a bigger gap between mini and non-mini tabs. 910 tab_x += mini_to_non_mini_gap_; 911 } 912 if (tab->IsActive()) 913 tab_width = selected; 914 } 915 double end_of_tab = tab_x + tab_width; 916 int rounded_tab_x = Round(tab_x); 917 set_ideal_bounds(i, 918 gfx::Rect(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, 919 tab_height)); 920 tab_x = end_of_tab + kTabHOffset; 921 last_was_mini = tab->data().mini; 922 } 923 } 924 925 // Update bounds of new tab button. 926 int new_tab_x; 927 int new_tab_y = browser_defaults::kSizeTabButtonToTopOfTabStrip ? 928 0 : kNewTabButtonVOffset; 929 if (abs(Round(unselected) - Tab::GetStandardSize().width()) > 1 && 930 !in_tab_close_) { 931 // We're shrinking tabs, so we need to anchor the New Tab button to the 932 // right edge of the TabStrip's bounds, rather than the right edge of the 933 // right-most Tab, otherwise it'll bounce when animating. 934 new_tab_x = width() - newtab_button_bounds_.width(); 935 } else { 936 new_tab_x = Round(tab_x - kTabHOffset) + kNewTabButtonHOffset; 937 } 938 newtab_button_bounds_.set_origin(gfx::Point(new_tab_x, new_tab_y)); 939 } 940 941 void TabStrip::StartResizeLayoutAnimation() { 942 PrepareForAnimation(); 943 GenerateIdealBounds(); 944 AnimateToIdealBounds(); 945 } 946 947 void TabStrip::StartMiniTabAnimation() { 948 in_tab_close_ = false; 949 available_width_for_tabs_ = -1; 950 951 PrepareForAnimation(); 952 953 GenerateIdealBounds(); 954 AnimateToIdealBounds(); 955 } 956 957 void TabStrip::StartMouseInitiatedRemoveTabAnimation(int model_index) { 958 // The user initiated the close. We want to persist the bounds of all the 959 // existing tabs, so we manually shift ideal_bounds then animate. 960 int tab_data_index = ModelIndexToTabIndex(model_index); 961 DCHECK(tab_data_index != tab_count()); 962 BaseTab* tab_closing = base_tab_at_tab_index(tab_data_index); 963 int delta = tab_closing->width() + kTabHOffset; 964 if (tab_closing->data().mini && model_index + 1 < GetModelCount() && 965 !GetBaseTabAtModelIndex(model_index + 1)->data().mini) { 966 delta += mini_to_non_mini_gap_; 967 } 968 969 for (int i = tab_data_index + 1; i < tab_count(); ++i) { 970 BaseTab* tab = base_tab_at_tab_index(i); 971 if (!tab->closing()) { 972 gfx::Rect bounds = ideal_bounds(i); 973 bounds.set_x(bounds.x() - delta); 974 set_ideal_bounds(i, bounds); 975 } 976 } 977 978 newtab_button_bounds_.set_x(newtab_button_bounds_.x() - delta); 979 980 PrepareForAnimation(); 981 982 // Mark the tab as closing. 983 tab_closing->set_closing(true); 984 985 AnimateToIdealBounds(); 986 987 gfx::Rect tab_bounds = tab_closing->bounds(); 988 if (type() == HORIZONTAL_TAB_STRIP) 989 tab_bounds.set_width(0); 990 else 991 tab_bounds.set_height(0); 992 bounds_animator().AnimateViewTo(tab_closing, tab_bounds); 993 994 // Register delegate to do cleanup when done, BoundsAnimator takes 995 // ownership of RemoveTabDelegate. 996 bounds_animator().SetAnimationDelegate(tab_closing, 997 CreateRemoveTabDelegate(tab_closing), 998 true); 999 } 1000 1001 int TabStrip::GetMiniTabCount() const { 1002 int mini_count = 0; 1003 for (int i = 0; i < tab_count(); ++i) { 1004 if (base_tab_at_tab_index(i)->data().mini) 1005 mini_count++; 1006 else 1007 return mini_count; 1008 } 1009 return mini_count; 1010 } 1011 1012 int TabStrip::GetAvailableWidthForTabs(Tab* last_tab) const { 1013 return last_tab->x() + last_tab->width(); 1014 } 1015 1016 bool TabStrip::IsPointInTab(Tab* tab, 1017 const gfx::Point& point_in_tabstrip_coords) { 1018 gfx::Point point_in_tab_coords(point_in_tabstrip_coords); 1019 View::ConvertPointToView(this, tab, &point_in_tab_coords); 1020 return tab->HitTest(point_in_tab_coords); 1021 } 1022