tab_strip.cc revision cedac228d2dd51db4b79ea1e72c7f249408ee061
1// Copyright (c) 2012 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#if defined(OS_WIN) 8#include <windowsx.h> 9#endif 10 11#include <algorithm> 12#include <iterator> 13#include <string> 14#include <vector> 15 16#include "base/compiler_specific.h" 17#include "base/metrics/histogram.h" 18#include "base/stl_util.h" 19#include "base/strings/utf_string_conversions.h" 20#include "chrome/browser/defaults.h" 21#include "chrome/browser/ui/host_desktop.h" 22#include "chrome/browser/ui/tabs/tab_strip_model.h" 23#include "chrome/browser/ui/view_ids.h" 24#include "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h" 25#include "chrome/browser/ui/views/tabs/tab.h" 26#include "chrome/browser/ui/views/tabs/tab_drag_controller.h" 27#include "chrome/browser/ui/views/tabs/tab_strip_controller.h" 28#include "chrome/browser/ui/views/tabs/tab_strip_observer.h" 29#include "chrome/browser/ui/views/touch_uma/touch_uma.h" 30#include "content/public/browser/user_metrics.h" 31#include "grit/generated_resources.h" 32#include "grit/theme_resources.h" 33#include "ui/accessibility/ax_view_state.h" 34#include "ui/base/default_theme_provider.h" 35#include "ui/base/dragdrop/drag_drop_types.h" 36#include "ui/base/l10n/l10n_util.h" 37#include "ui/base/models/list_selection_model.h" 38#include "ui/base/resource/resource_bundle.h" 39#include "ui/gfx/animation/animation_container.h" 40#include "ui/gfx/animation/throb_animation.h" 41#include "ui/gfx/canvas.h" 42#include "ui/gfx/display.h" 43#include "ui/gfx/image/image_skia.h" 44#include "ui/gfx/image/image_skia_operations.h" 45#include "ui/gfx/path.h" 46#include "ui/gfx/rect_conversions.h" 47#include "ui/gfx/screen.h" 48#include "ui/gfx/size.h" 49#include "ui/views/controls/image_view.h" 50#include "ui/views/mouse_watcher_view_host.h" 51#include "ui/views/rect_based_targeting_utils.h" 52#include "ui/views/view_model_utils.h" 53#include "ui/views/widget/root_view.h" 54#include "ui/views/widget/widget.h" 55#include "ui/views/window/non_client_view.h" 56 57#if defined(OS_WIN) 58#include "ui/gfx/win/hwnd_util.h" 59#include "ui/views/widget/monitor_win.h" 60#include "ui/views/win/hwnd_util.h" 61#endif 62 63using base::UserMetricsAction; 64using ui::DropTargetEvent; 65 66namespace { 67 68static const int kTabStripAnimationVSlop = 40; 69// Inactive tabs in a native frame are slightly transparent. 70static const int kGlassFrameInactiveTabAlpha = 200; 71// If there are multiple tabs selected then make non-selected inactive tabs 72// even more transparent. 73static const int kGlassFrameInactiveTabAlphaMultiSelection = 150; 74 75// Alpha applied to all elements save the selected tabs. 76static const int kInactiveTabAndNewTabButtonAlphaAsh = 230; 77static const int kInactiveTabAndNewTabButtonAlpha = 255; 78 79// Inverse ratio of the width of a tab edge to the width of the tab. When 80// hovering over the left or right edge of a tab, the drop indicator will 81// point between tabs. 82static const int kTabEdgeRatioInverse = 4; 83 84// Size of the drop indicator. 85static int drop_indicator_width; 86static int drop_indicator_height; 87 88static inline int Round(double x) { 89 // Why oh why is this not in a standard header? 90 return static_cast<int>(floor(x + 0.5)); 91} 92 93// Max number of stacked tabs. 94static const int kMaxStackedCount = 4; 95 96// Padding between stacked tabs. 97static const int kStackedPadding = 6; 98 99// See UpdateLayoutTypeFromMouseEvent() for a description of these. 100#if !defined(USE_ASH) 101const int kMouseMoveTimeMS = 200; 102const int kMouseMoveCountBeforeConsiderReal = 3; 103#endif 104 105// Amount of time we delay before resizing after a close from a touch. 106const int kTouchResizeLayoutTimeMS = 2000; 107 108// Horizontal offset for the new tab button to bring it closer to the 109// rightmost tab. 110const int kNewTabButtonHorizontalOffset = -11; 111 112// Vertical offset for the new tab button to bring it closer to the 113// rightmost tab. 114const int kNewTabButtonVerticalOffset = 7; 115 116// Amount the left edge of a tab is offset from the rectangle of the tab's 117// favicon/title/close box. Related to the width of IDR_TAB_ACTIVE_LEFT. 118// Affects the size of the "V" between adjacent tabs. 119const int kTabHorizontalOffset = -26; 120 121// The size of the new tab button must be hardcoded because we need to be 122// able to lay it out before we are able to get its image from the 123// ui::ThemeProvider. It also makes sense to do this, because the size of the 124// new tab button should not need to be calculated dynamically. 125const int kNewTabButtonAssetWidth = 34; 126const int kNewTabButtonAssetHeight = 18; 127 128// Amount to adjust the clip by when the tab is stacked before the active index. 129const int kStackedTabLeftClip = 20; 130 131// Amount to adjust the clip by when the tab is stacked after the active index. 132const int kStackedTabRightClip = 20; 133 134base::string16 GetClipboardText() { 135 if (!ui::Clipboard::IsSupportedClipboardType(ui::CLIPBOARD_TYPE_SELECTION)) 136 return base::string16(); 137 ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); 138 CHECK(clipboard); 139 base::string16 clipboard_text; 140 clipboard->ReadText(ui::CLIPBOARD_TYPE_SELECTION, &clipboard_text); 141 return clipboard_text; 142} 143 144// Animation delegate used when a dragged tab is released. When done sets the 145// dragging state to false. 146class ResetDraggingStateDelegate 147 : public views::BoundsAnimator::OwnedAnimationDelegate { 148 public: 149 explicit ResetDraggingStateDelegate(Tab* tab) : tab_(tab) { 150 } 151 152 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE { 153 tab_->set_dragging(false); 154 } 155 156 virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE { 157 tab_->set_dragging(false); 158 } 159 160 private: 161 Tab* tab_; 162 163 DISALLOW_COPY_AND_ASSIGN(ResetDraggingStateDelegate); 164}; 165 166// If |dest| contains the point |point_in_source| the event handler from |dest| 167// is returned. Otherwise NULL is returned. 168views::View* ConvertPointToViewAndGetEventHandler( 169 views::View* source, 170 views::View* dest, 171 const gfx::Point& point_in_source) { 172 gfx::Point dest_point(point_in_source); 173 views::View::ConvertPointToTarget(source, dest, &dest_point); 174 return dest->HitTestPoint(dest_point) ? 175 dest->GetEventHandlerForPoint(dest_point) : NULL; 176} 177 178// Gets a tooltip handler for |point_in_source| from |dest|. Note that |dest| 179// should return NULL if it does not contain the point. 180views::View* ConvertPointToViewAndGetTooltipHandler( 181 views::View* source, 182 views::View* dest, 183 const gfx::Point& point_in_source) { 184 gfx::Point dest_point(point_in_source); 185 views::View::ConvertPointToTarget(source, dest, &dest_point); 186 return dest->GetTooltipHandlerForPoint(dest_point); 187} 188 189TabDragController::EventSource EventSourceFromEvent( 190 const ui::LocatedEvent& event) { 191 return event.IsGestureEvent() ? TabDragController::EVENT_SOURCE_TOUCH : 192 TabDragController::EVENT_SOURCE_MOUSE; 193} 194 195} // namespace 196 197/////////////////////////////////////////////////////////////////////////////// 198// NewTabButton 199// 200// A subclass of button that hit-tests to the shape of the new tab button and 201// does custom drawing. 202 203class NewTabButton : public views::ImageButton { 204 public: 205 NewTabButton(TabStrip* tab_strip, views::ButtonListener* listener); 206 virtual ~NewTabButton(); 207 208 // Set the background offset used to match the background image to the frame 209 // image. 210 void set_background_offset(const gfx::Point& offset) { 211 background_offset_ = offset; 212 } 213 214 protected: 215 // Overridden from views::View: 216 virtual bool HasHitTestMask() const OVERRIDE; 217 virtual void GetHitTestMask(HitTestSource source, 218 gfx::Path* path) const OVERRIDE; 219#if defined(OS_WIN) 220 virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE; 221#endif 222 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 223 224 // Overridden from ui::EventHandler: 225 virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE; 226 227 private: 228 bool ShouldWindowContentsBeTransparent() const; 229 gfx::ImageSkia GetBackgroundImage(views::CustomButton::ButtonState state, 230 float scale) const; 231 gfx::ImageSkia GetImageForState(views::CustomButton::ButtonState state, 232 float scale) const; 233 gfx::ImageSkia GetImageForScale(float scale) const; 234 235 // Tab strip that contains this button. 236 TabStrip* tab_strip_; 237 238 // The offset used to paint the background image. 239 gfx::Point background_offset_; 240 241 // were we destroyed? 242 bool* destroyed_; 243 244 DISALLOW_COPY_AND_ASSIGN(NewTabButton); 245}; 246 247NewTabButton::NewTabButton(TabStrip* tab_strip, views::ButtonListener* listener) 248 : views::ImageButton(listener), 249 tab_strip_(tab_strip), 250 destroyed_(NULL) { 251#if defined(OS_LINUX) && !defined(OS_CHROMEOS) 252 set_triggerable_event_flags(triggerable_event_flags() | 253 ui::EF_MIDDLE_MOUSE_BUTTON); 254#endif 255} 256 257NewTabButton::~NewTabButton() { 258 if (destroyed_) 259 *destroyed_ = true; 260} 261 262bool NewTabButton::HasHitTestMask() const { 263 // When the button is sized to the top of the tab strip we want the user to 264 // be able to click on complete bounds, and so don't return a custom hit 265 // mask. 266 return !tab_strip_->SizeTabButtonToTopOfTabStrip(); 267} 268 269void NewTabButton::GetHitTestMask(HitTestSource source, gfx::Path* path) const { 270 DCHECK(path); 271 272 SkScalar w = SkIntToScalar(width()); 273 SkScalar v_offset = SkIntToScalar(kNewTabButtonVerticalOffset); 274 275 // These values are defined by the shape of the new tab image. Should that 276 // image ever change, these values will need to be updated. They're so 277 // custom it's not really worth defining constants for. 278 // These values are correct for regular and USE_ASH versions of the image. 279 path->moveTo(0, v_offset + 1); 280 path->lineTo(w - 7, v_offset + 1); 281 path->lineTo(w - 4, v_offset + 4); 282 path->lineTo(w, v_offset + 16); 283 path->lineTo(w - 1, v_offset + 17); 284 path->lineTo(7, v_offset + 17); 285 path->lineTo(4, v_offset + 13); 286 path->lineTo(0, v_offset + 1); 287 path->close(); 288} 289 290#if defined(OS_WIN) 291void NewTabButton::OnMouseReleased(const ui::MouseEvent& event) { 292 if (event.IsOnlyRightMouseButton()) { 293 gfx::Point point = event.location(); 294 views::View::ConvertPointToScreen(this, &point); 295 bool destroyed = false; 296 destroyed_ = &destroyed; 297 gfx::ShowSystemMenuAtPoint(views::HWNDForView(this), point); 298 if (destroyed) 299 return; 300 301 destroyed_ = NULL; 302 SetState(views::CustomButton::STATE_NORMAL); 303 return; 304 } 305 views::ImageButton::OnMouseReleased(event); 306} 307#endif 308 309void NewTabButton::OnPaint(gfx::Canvas* canvas) { 310 gfx::ImageSkia image = GetImageForScale(canvas->image_scale()); 311 canvas->DrawImageInt(image, 0, height() - image.height()); 312} 313 314void NewTabButton::OnGestureEvent(ui::GestureEvent* event) { 315 // Consume all gesture events here so that the parent (Tab) does not 316 // start consuming gestures. 317 views::ImageButton::OnGestureEvent(event); 318 event->SetHandled(); 319} 320 321bool NewTabButton::ShouldWindowContentsBeTransparent() const { 322 return GetWidget() && 323 GetWidget()->GetTopLevelWidget()->ShouldWindowContentsBeTransparent(); 324} 325 326gfx::ImageSkia NewTabButton::GetBackgroundImage( 327 views::CustomButton::ButtonState state, 328 float scale) const { 329 int background_id = 0; 330 if (ShouldWindowContentsBeTransparent()) { 331 background_id = IDR_THEME_TAB_BACKGROUND_V; 332 } else if (tab_strip_->controller()->IsIncognito()) { 333 background_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO; 334 } else { 335 background_id = IDR_THEME_TAB_BACKGROUND; 336 } 337 338 int alpha = 0; 339 switch (state) { 340 case views::CustomButton::STATE_NORMAL: 341 case views::CustomButton::STATE_HOVERED: 342 alpha = ShouldWindowContentsBeTransparent() ? kGlassFrameInactiveTabAlpha 343 : 255; 344 break; 345 case views::CustomButton::STATE_PRESSED: 346 alpha = 145; 347 break; 348 default: 349 NOTREACHED(); 350 break; 351 } 352 353 gfx::ImageSkia* mask = 354 GetThemeProvider()->GetImageSkiaNamed(IDR_NEWTAB_BUTTON_MASK); 355 int height = mask->height(); 356 int width = mask->width(); 357 // The canvas and mask has to use the same scale factor. 358 if (!mask->HasRepresentation(scale)) 359 scale = ui::GetScaleForScaleFactor(ui::SCALE_FACTOR_100P); 360 361 gfx::Canvas canvas(gfx::Size(width, height), scale, false); 362 363 // For custom images the background starts at the top of the tab strip. 364 // Otherwise the background starts at the top of the frame. 365 gfx::ImageSkia* background = 366 GetThemeProvider()->GetImageSkiaNamed(background_id); 367 int offset_y = GetThemeProvider()->HasCustomImage(background_id) ? 368 0 : background_offset_.y(); 369 370 // The new tab background is mirrored in RTL mode, but the theme background 371 // should never be mirrored. Mirror it here to compensate. 372 float x_scale = 1.0f; 373 int x = GetMirroredX() + background_offset_.x(); 374 if (base::i18n::IsRTL()) { 375 x_scale = -1.0f; 376 // Offset by |width| such that the same region is painted as if there was no 377 // flip. 378 x += width; 379 } 380 canvas.TileImageInt(*background, x, kNewTabButtonVerticalOffset + offset_y, 381 x_scale, 1.0f, 0, 0, width, height); 382 383 if (alpha != 255) { 384 SkPaint paint; 385 paint.setColor(SkColorSetARGB(alpha, 255, 255, 255)); 386 paint.setXfermodeMode(SkXfermode::kDstIn_Mode); 387 paint.setStyle(SkPaint::kFill_Style); 388 canvas.DrawRect(gfx::Rect(0, 0, width, height), paint); 389 } 390 391 // White highlight on hover. 392 if (state == views::CustomButton::STATE_HOVERED) 393 canvas.FillRect(GetLocalBounds(), SkColorSetARGB(64, 255, 255, 255)); 394 395 return gfx::ImageSkiaOperations::CreateMaskedImage( 396 gfx::ImageSkia(canvas.ExtractImageRep()), *mask); 397} 398 399gfx::ImageSkia NewTabButton::GetImageForState( 400 views::CustomButton::ButtonState state, 401 float scale) const { 402 const int overlay_id = state == views::CustomButton::STATE_PRESSED ? 403 IDR_NEWTAB_BUTTON_P : IDR_NEWTAB_BUTTON; 404 gfx::ImageSkia* overlay = GetThemeProvider()->GetImageSkiaNamed(overlay_id); 405 406 gfx::Canvas canvas( 407 gfx::Size(overlay->width(), overlay->height()), 408 scale, 409 false); 410 canvas.DrawImageInt(GetBackgroundImage(state, scale), 0, 0); 411 412 // Draw the button border with a slight alpha. 413 const int kGlassFrameOverlayAlpha = 178; 414 const int kOpaqueFrameOverlayAlpha = 230; 415 uint8 alpha = ShouldWindowContentsBeTransparent() ? 416 kGlassFrameOverlayAlpha : kOpaqueFrameOverlayAlpha; 417 canvas.DrawImageInt(*overlay, 0, 0, alpha); 418 419 return gfx::ImageSkia(canvas.ExtractImageRep()); 420} 421 422gfx::ImageSkia NewTabButton::GetImageForScale(float scale) const { 423 if (!hover_animation_->is_animating()) 424 return GetImageForState(state(), scale); 425 return gfx::ImageSkiaOperations::CreateBlendedImage( 426 GetImageForState(views::CustomButton::STATE_NORMAL, scale), 427 GetImageForState(views::CustomButton::STATE_HOVERED, scale), 428 hover_animation_->GetCurrentValue()); 429} 430 431/////////////////////////////////////////////////////////////////////////////// 432// TabStrip::RemoveTabDelegate 433// 434// AnimationDelegate used when removing a tab. Does the necessary cleanup when 435// done. 436class TabStrip::RemoveTabDelegate 437 : public views::BoundsAnimator::OwnedAnimationDelegate { 438 public: 439 RemoveTabDelegate(TabStrip* tab_strip, Tab* tab); 440 441 virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; 442 virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE; 443 444 private: 445 void CompleteRemove(); 446 447 // When the animation completes, we send the Container a message to simulate 448 // a mouse moved event at the current mouse position. This tickles the Tab 449 // the mouse is currently over to show the "hot" state of the close button. 450 void HighlightCloseButton(); 451 452 TabStrip* tabstrip_; 453 Tab* tab_; 454 455 DISALLOW_COPY_AND_ASSIGN(RemoveTabDelegate); 456}; 457 458TabStrip::RemoveTabDelegate::RemoveTabDelegate(TabStrip* tab_strip, 459 Tab* tab) 460 : tabstrip_(tab_strip), 461 tab_(tab) { 462} 463 464void TabStrip::RemoveTabDelegate::AnimationEnded( 465 const gfx::Animation* animation) { 466 CompleteRemove(); 467} 468 469void TabStrip::RemoveTabDelegate::AnimationCanceled( 470 const gfx::Animation* animation) { 471 CompleteRemove(); 472} 473 474void TabStrip::RemoveTabDelegate::CompleteRemove() { 475 DCHECK(tab_->closing()); 476 tabstrip_->RemoveAndDeleteTab(tab_); 477 HighlightCloseButton(); 478} 479 480void TabStrip::RemoveTabDelegate::HighlightCloseButton() { 481 if (tabstrip_->IsDragSessionActive() || 482 !tabstrip_->ShouldHighlightCloseButtonAfterRemove()) { 483 // This function is not required (and indeed may crash!) for removes 484 // spawned by non-mouse closes and drag-detaches. 485 return; 486 } 487 488 views::Widget* widget = tabstrip_->GetWidget(); 489 // This can be null during shutdown. See http://crbug.com/42737. 490 if (!widget) 491 return; 492 493 widget->SynthesizeMouseMoveEvent(); 494} 495 496/////////////////////////////////////////////////////////////////////////////// 497// TabStrip, public: 498 499// static 500const char TabStrip::kViewClassName[] = "TabStrip"; 501 502// static 503const int TabStrip::kMiniToNonMiniGap = 3; 504 505TabStrip::TabStrip(TabStripController* controller) 506 : controller_(controller), 507 newtab_button_(NULL), 508 current_unselected_width_(Tab::GetStandardSize().width()), 509 current_selected_width_(Tab::GetStandardSize().width()), 510 available_width_for_tabs_(-1), 511 in_tab_close_(false), 512 animation_container_(new gfx::AnimationContainer()), 513 bounds_animator_(this), 514 stacked_layout_(false), 515 adjust_layout_(false), 516 reset_to_shrink_on_exit_(false), 517 mouse_move_count_(0), 518 immersive_style_(false) { 519 Init(); 520} 521 522TabStrip::~TabStrip() { 523 FOR_EACH_OBSERVER(TabStripObserver, observers_, 524 TabStripDeleted(this)); 525 526 // The animations may reference the tabs. Shut down the animation before we 527 // delete the tabs. 528 StopAnimating(false); 529 530 DestroyDragController(); 531 532 // Make sure we unhook ourselves as a message loop observer so that we don't 533 // crash in the case where the user closes the window after closing a tab 534 // but before moving the mouse. 535 RemoveMessageLoopObserver(); 536 537 // The children (tabs) may callback to us from their destructor. Delete them 538 // so that if they call back we aren't in a weird state. 539 RemoveAllChildViews(true); 540} 541 542void TabStrip::AddObserver(TabStripObserver* observer) { 543 observers_.AddObserver(observer); 544} 545 546void TabStrip::RemoveObserver(TabStripObserver* observer) { 547 observers_.RemoveObserver(observer); 548} 549 550void TabStrip::SetStackedLayout(bool stacked_layout) { 551 if (stacked_layout == stacked_layout_) 552 return; 553 554 const int active_index = controller_->GetActiveIndex(); 555 int active_center = 0; 556 if (active_index != -1) { 557 active_center = ideal_bounds(active_index).x() + 558 ideal_bounds(active_index).width() / 2; 559 } 560 stacked_layout_ = stacked_layout; 561 SetResetToShrinkOnExit(false); 562 SwapLayoutIfNecessary(); 563 // When transitioning to stacked try to keep the active tab centered. 564 if (touch_layout_.get() && active_index != -1) { 565 touch_layout_->SetActiveTabLocation( 566 active_center - ideal_bounds(active_index).width() / 2); 567 AnimateToIdealBounds(); 568 } 569} 570 571gfx::Rect TabStrip::GetNewTabButtonBounds() { 572 return newtab_button_->bounds(); 573} 574 575bool TabStrip::SizeTabButtonToTopOfTabStrip() { 576 // Extend the button to the screen edge in maximized and immersive fullscreen. 577 views::Widget* widget = GetWidget(); 578 return browser_defaults::kSizeTabButtonToTopOfTabStrip || 579 (widget && (widget->IsMaximized() || widget->IsFullscreen())); 580} 581 582void TabStrip::StartHighlight(int model_index) { 583 tab_at(model_index)->StartPulse(); 584} 585 586void TabStrip::StopAllHighlighting() { 587 for (int i = 0; i < tab_count(); ++i) 588 tab_at(i)->StopPulse(); 589} 590 591void TabStrip::AddTabAt(int model_index, 592 const TabRendererData& data, 593 bool is_active) { 594 // Stop dragging when a new tab is added and dragging a window. Doing 595 // otherwise results in a confusing state if the user attempts to reattach. We 596 // could allow this and make TabDragController update itself during the add, 597 // but this comes up infrequently enough that it's not work the complexity. 598 if (drag_controller_.get() && !drag_controller_->is_mutating() && 599 drag_controller_->is_dragging_window()) { 600 EndDrag(END_DRAG_COMPLETE); 601 } 602 Tab* tab = CreateTab(); 603 tab->SetData(data); 604 UpdateTabsClosingMap(model_index, 1); 605 tabs_.Add(tab, model_index); 606 AddChildView(tab); 607 608 if (touch_layout_.get()) { 609 GenerateIdealBoundsForMiniTabs(NULL); 610 int add_types = 0; 611 if (data.mini) 612 add_types |= StackedTabStripLayout::kAddTypeMini; 613 if (is_active) 614 add_types |= StackedTabStripLayout::kAddTypeActive; 615 touch_layout_->AddTab(model_index, add_types, GetStartXForNormalTabs()); 616 } 617 618 // Don't animate the first tab, it looks weird, and don't animate anything 619 // if the containing window isn't visible yet. 620 if (tab_count() > 1 && GetWidget() && GetWidget()->IsVisible()) 621 StartInsertTabAnimation(model_index); 622 else 623 DoLayout(); 624 625 SwapLayoutIfNecessary(); 626 627 FOR_EACH_OBSERVER(TabStripObserver, observers_, 628 TabStripAddedTabAt(this, model_index)); 629} 630 631void TabStrip::MoveTab(int from_model_index, 632 int to_model_index, 633 const TabRendererData& data) { 634 DCHECK_GT(tabs_.view_size(), 0); 635 Tab* last_tab = tab_at(tab_count() - 1); 636 tab_at(from_model_index)->SetData(data); 637 if (touch_layout_.get()) { 638 tabs_.MoveViewOnly(from_model_index, to_model_index); 639 int mini_count = 0; 640 GenerateIdealBoundsForMiniTabs(&mini_count); 641 touch_layout_->MoveTab( 642 from_model_index, to_model_index, controller_->GetActiveIndex(), 643 GetStartXForNormalTabs(), mini_count); 644 } else { 645 tabs_.Move(from_model_index, to_model_index); 646 } 647 StartMoveTabAnimation(); 648 if (TabDragController::IsAttachedTo(this) && 649 (last_tab != tab_at(tab_count() - 1) || last_tab->dragging())) { 650 newtab_button_->SetVisible(false); 651 } 652 SwapLayoutIfNecessary(); 653 654 FOR_EACH_OBSERVER(TabStripObserver, observers_, 655 TabStripMovedTab(this, from_model_index, to_model_index)); 656} 657 658void TabStrip::RemoveTabAt(int model_index) { 659 if (touch_layout_.get()) { 660 Tab* tab = tab_at(model_index); 661 tab->set_closing(true); 662 int old_x = tabs_.ideal_bounds(model_index).x(); 663 // We still need to paint the tab until we actually remove it. Put it in 664 // tabs_closing_map_ so we can find it. 665 RemoveTabFromViewModel(model_index); 666 touch_layout_->RemoveTab(model_index, GenerateIdealBoundsForMiniTabs(NULL), 667 old_x); 668 ScheduleRemoveTabAnimation(tab); 669 } else if (in_tab_close_ && model_index != GetModelCount()) { 670 StartMouseInitiatedRemoveTabAnimation(model_index); 671 } else { 672 StartRemoveTabAnimation(model_index); 673 } 674 SwapLayoutIfNecessary(); 675 676 FOR_EACH_OBSERVER(TabStripObserver, observers_, 677 TabStripRemovedTabAt(this, model_index)); 678} 679 680void TabStrip::SetTabData(int model_index, const TabRendererData& data) { 681 Tab* tab = tab_at(model_index); 682 bool mini_state_changed = tab->data().mini != data.mini; 683 tab->SetData(data); 684 685 if (mini_state_changed) { 686 if (touch_layout_.get()) { 687 int mini_tab_count = 0; 688 int start_x = GenerateIdealBoundsForMiniTabs(&mini_tab_count); 689 touch_layout_->SetXAndMiniCount(start_x, mini_tab_count); 690 } 691 if (GetWidget() && GetWidget()->IsVisible()) 692 StartMiniTabAnimation(); 693 else 694 DoLayout(); 695 } 696 SwapLayoutIfNecessary(); 697} 698 699void TabStrip::PrepareForCloseAt(int model_index, CloseTabSource source) { 700 if (!in_tab_close_ && IsAnimating()) { 701 // Cancel any current animations. We do this as remove uses the current 702 // ideal bounds and we need to know ideal bounds is in a good state. 703 StopAnimating(true); 704 } 705 706 if (!GetWidget()) 707 return; 708 709 int model_count = GetModelCount(); 710 if (model_index + 1 != model_count && model_count > 1) { 711 // The user is about to close a tab other than the last tab. Set 712 // available_width_for_tabs_ so that if we do a layout we don't position a 713 // tab past the end of the second to last tab. We do this so that as the 714 // user closes tabs with the mouse a tab continues to fall under the mouse. 715 Tab* last_tab = tab_at(model_count - 1); 716 Tab* tab_being_removed = tab_at(model_index); 717 available_width_for_tabs_ = last_tab->x() + last_tab->width() - 718 tab_being_removed->width() - kTabHorizontalOffset; 719 if (model_index == 0 && tab_being_removed->data().mini && 720 !tab_at(1)->data().mini) { 721 available_width_for_tabs_ -= kMiniToNonMiniGap; 722 } 723 } 724 725 in_tab_close_ = true; 726 resize_layout_timer_.Stop(); 727 if (source == CLOSE_TAB_FROM_TOUCH) { 728 StartResizeLayoutTabsFromTouchTimer(); 729 } else { 730 AddMessageLoopObserver(); 731 } 732} 733 734void TabStrip::SetSelection(const ui::ListSelectionModel& old_selection, 735 const ui::ListSelectionModel& new_selection) { 736 if (touch_layout_.get()) { 737 touch_layout_->SetActiveIndex(new_selection.active()); 738 // Only start an animation if we need to. Otherwise clicking on an 739 // unselected tab and dragging won't work because dragging is only allowed 740 // if not animating. 741 if (!views::ViewModelUtils::IsAtIdealBounds(tabs_)) 742 AnimateToIdealBounds(); 743 SchedulePaint(); 744 } else { 745 // We have "tiny tabs" if the tabs are so tiny that the unselected ones are 746 // a different size to the selected ones. 747 bool tiny_tabs = current_unselected_width_ != current_selected_width_; 748 if (!IsAnimating() && (!in_tab_close_ || tiny_tabs)) { 749 DoLayout(); 750 } else { 751 SchedulePaint(); 752 } 753 } 754 755 ui::ListSelectionModel::SelectedIndices no_longer_selected = 756 base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>( 757 old_selection.selected_indices(), 758 new_selection.selected_indices()); 759 for (size_t i = 0; i < no_longer_selected.size(); ++i) 760 tab_at(no_longer_selected[i])->StopMiniTabTitleAnimation(); 761} 762 763void TabStrip::TabTitleChangedNotLoading(int model_index) { 764 Tab* tab = tab_at(model_index); 765 if (tab->data().mini && !tab->IsActive()) 766 tab->StartMiniTabTitleAnimation(); 767} 768 769Tab* TabStrip::tab_at(int index) const { 770 return static_cast<Tab*>(tabs_.view_at(index)); 771} 772 773int TabStrip::GetModelIndexOfTab(const Tab* tab) const { 774 return tabs_.GetIndexOfView(tab); 775} 776 777int TabStrip::GetModelCount() const { 778 return controller_->GetCount(); 779} 780 781bool TabStrip::IsValidModelIndex(int model_index) const { 782 return controller_->IsValidIndex(model_index); 783} 784 785bool TabStrip::IsDragSessionActive() const { 786 return drag_controller_.get() != NULL; 787} 788 789bool TabStrip::IsActiveDropTarget() const { 790 for (int i = 0; i < tab_count(); ++i) { 791 Tab* tab = tab_at(i); 792 if (tab->dragging()) 793 return true; 794 } 795 return false; 796} 797 798bool TabStrip::IsTabStripEditable() const { 799 return !IsDragSessionActive() && !IsActiveDropTarget(); 800} 801 802bool TabStrip::IsTabStripCloseable() const { 803 return !IsDragSessionActive(); 804} 805 806void TabStrip::UpdateLoadingAnimations() { 807 controller_->UpdateLoadingAnimations(); 808} 809 810bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) { 811 return IsRectInWindowCaption(gfx::Rect(point, gfx::Size(1, 1))); 812} 813 814bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { 815 views::View* v = GetEventHandlerForRect(rect); 816 817 // If there is no control at this location, claim the hit was in the title 818 // bar to get a move action. 819 if (v == this) 820 return true; 821 822 // Check to see if the rect intersects the non-button parts of the new tab 823 // button. The button has a non-rectangular shape, so if it's not in the 824 // visual portions of the button we treat it as a click to the caption. 825 gfx::RectF rect_in_newtab_coords_f(rect); 826 View::ConvertRectToTarget(this, newtab_button_, &rect_in_newtab_coords_f); 827 gfx::Rect rect_in_newtab_coords = gfx::ToEnclosingRect( 828 rect_in_newtab_coords_f); 829 if (newtab_button_->GetLocalBounds().Intersects(rect_in_newtab_coords) && 830 !newtab_button_->HitTestRect(rect_in_newtab_coords)) 831 return true; 832 833 // All other regions, including the new Tab button, should be considered part 834 // of the containing Window's client area so that regular events can be 835 // processed for them. 836 return false; 837} 838 839void TabStrip::SetBackgroundOffset(const gfx::Point& offset) { 840 for (int i = 0; i < tab_count(); ++i) 841 tab_at(i)->set_background_offset(offset); 842 newtab_button_->set_background_offset(offset); 843} 844 845views::View* TabStrip::newtab_button() { 846 return newtab_button_; 847} 848 849void TabStrip::SetImmersiveStyle(bool enable) { 850 if (immersive_style_ == enable) 851 return; 852 immersive_style_ = enable; 853} 854 855bool TabStrip::IsAnimating() const { 856 return bounds_animator_.IsAnimating(); 857} 858 859void TabStrip::StopAnimating(bool layout) { 860 if (!IsAnimating()) 861 return; 862 863 bounds_animator_.Cancel(); 864 865 if (layout) 866 DoLayout(); 867} 868 869void TabStrip::FileSupported(const GURL& url, bool supported) { 870 if (drop_info_.get() && drop_info_->url == url) 871 drop_info_->file_supported = supported; 872} 873 874const ui::ListSelectionModel& TabStrip::GetSelectionModel() { 875 return controller_->GetSelectionModel(); 876} 877 878bool TabStrip::SupportsMultipleSelection() { 879 // TODO: currently only allow single selection in touch layout mode. 880 return touch_layout_.get() == NULL; 881} 882 883void TabStrip::SelectTab(Tab* tab) { 884 int model_index = GetModelIndexOfTab(tab); 885 if (IsValidModelIndex(model_index)) 886 controller_->SelectTab(model_index); 887} 888 889void TabStrip::ExtendSelectionTo(Tab* tab) { 890 int model_index = GetModelIndexOfTab(tab); 891 if (IsValidModelIndex(model_index)) 892 controller_->ExtendSelectionTo(model_index); 893} 894 895void TabStrip::ToggleSelected(Tab* tab) { 896 int model_index = GetModelIndexOfTab(tab); 897 if (IsValidModelIndex(model_index)) 898 controller_->ToggleSelected(model_index); 899} 900 901void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { 902 int model_index = GetModelIndexOfTab(tab); 903 if (IsValidModelIndex(model_index)) 904 controller_->AddSelectionFromAnchorTo(model_index); 905} 906 907void TabStrip::CloseTab(Tab* tab, CloseTabSource source) { 908 if (tab->closing()) { 909 // If the tab is already closing, close the next tab. We do this so that the 910 // user can rapidly close tabs by clicking the close button and not have 911 // the animations interfere with that. 912 for (TabsClosingMap::const_iterator i(tabs_closing_map_.begin()); 913 i != tabs_closing_map_.end(); ++i) { 914 std::vector<Tab*>::const_iterator j = 915 std::find(i->second.begin(), i->second.end(), tab); 916 if (j != i->second.end()) { 917 if (i->first + 1 < GetModelCount()) 918 controller_->CloseTab(i->first + 1, source); 919 return; 920 } 921 } 922 // If we get here, it means a tab has been marked as closing but isn't in 923 // the set of known closing tabs. 924 NOTREACHED(); 925 return; 926 } 927 int model_index = GetModelIndexOfTab(tab); 928 if (IsValidModelIndex(model_index)) 929 controller_->CloseTab(model_index, source); 930} 931 932void TabStrip::ShowContextMenuForTab(Tab* tab, 933 const gfx::Point& p, 934 ui::MenuSourceType source_type) { 935 controller_->ShowContextMenuForTab(tab, p, source_type); 936} 937 938bool TabStrip::IsActiveTab(const Tab* tab) const { 939 int model_index = GetModelIndexOfTab(tab); 940 return IsValidModelIndex(model_index) && 941 controller_->IsActiveTab(model_index); 942} 943 944bool TabStrip::IsTabSelected(const Tab* tab) const { 945 int model_index = GetModelIndexOfTab(tab); 946 return IsValidModelIndex(model_index) && 947 controller_->IsTabSelected(model_index); 948} 949 950bool TabStrip::IsTabPinned(const Tab* tab) const { 951 if (tab->closing()) 952 return false; 953 954 int model_index = GetModelIndexOfTab(tab); 955 return IsValidModelIndex(model_index) && 956 controller_->IsTabPinned(model_index); 957} 958 959void TabStrip::MaybeStartDrag( 960 Tab* tab, 961 const ui::LocatedEvent& event, 962 const ui::ListSelectionModel& original_selection) { 963 // Don't accidentally start any drag operations during animations if the 964 // mouse is down... during an animation tabs are being resized automatically, 965 // so the View system can misinterpret this easily if the mouse is down that 966 // the user is dragging. 967 if (IsAnimating() || tab->closing() || 968 controller_->HasAvailableDragActions() == 0) { 969 return; 970 } 971 972 // Do not do any dragging of tabs when using the super short immersive style. 973 if (IsImmersiveStyle()) 974 return; 975 976 int model_index = GetModelIndexOfTab(tab); 977 if (!IsValidModelIndex(model_index)) { 978 CHECK(false); 979 return; 980 } 981 std::vector<Tab*> tabs; 982 int size_to_selected = 0; 983 int x = tab->GetMirroredXInView(event.x()); 984 int y = event.y(); 985 // Build the set of selected tabs to drag and calculate the offset from the 986 // first selected tab. 987 for (int i = 0; i < tab_count(); ++i) { 988 Tab* other_tab = tab_at(i); 989 if (IsTabSelected(other_tab)) { 990 tabs.push_back(other_tab); 991 if (other_tab == tab) { 992 size_to_selected = GetSizeNeededForTabs(tabs); 993 x = size_to_selected - tab->width() + x; 994 } 995 } 996 } 997 DCHECK(!tabs.empty()); 998 DCHECK(std::find(tabs.begin(), tabs.end(), tab) != tabs.end()); 999 ui::ListSelectionModel selection_model; 1000 if (!original_selection.IsSelected(model_index)) 1001 selection_model.Copy(original_selection); 1002 // Delete the existing DragController before creating a new one. We do this as 1003 // creating the DragController remembers the WebContents delegates and we need 1004 // to make sure the existing DragController isn't still a delegate. 1005 drag_controller_.reset(); 1006 TabDragController::DetachBehavior detach_behavior = 1007 TabDragController::DETACHABLE; 1008 TabDragController::MoveBehavior move_behavior = 1009 TabDragController::REORDER; 1010 // Use MOVE_VISIBILE_TABS in the following conditions: 1011 // . Mouse event generated from touch and the left button is down (the right 1012 // button corresponds to a long press, which we want to reorder). 1013 // . Gesture begin and control key isn't down. 1014 // . Real mouse event and control is down. This is mostly for testing. 1015 DCHECK(event.type() == ui::ET_MOUSE_PRESSED || 1016 event.type() == ui::ET_GESTURE_BEGIN); 1017 if (touch_layout_.get() && 1018 ((event.type() == ui::ET_MOUSE_PRESSED && 1019 (((event.flags() & ui::EF_FROM_TOUCH) && 1020 static_cast<const ui::MouseEvent&>(event).IsLeftMouseButton()) || 1021 (!(event.flags() & ui::EF_FROM_TOUCH) && 1022 static_cast<const ui::MouseEvent&>(event).IsControlDown()))) || 1023 (event.type() == ui::ET_GESTURE_BEGIN && !event.IsControlDown()))) { 1024 move_behavior = TabDragController::MOVE_VISIBILE_TABS; 1025 } 1026 1027 drag_controller_.reset(new TabDragController); 1028 drag_controller_->Init( 1029 this, tab, tabs, gfx::Point(x, y), event.x(), selection_model, 1030 detach_behavior, move_behavior, EventSourceFromEvent(event)); 1031} 1032 1033void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) { 1034 if (drag_controller_.get() && 1035 drag_controller_->event_source() == EventSourceFromEvent(event)) { 1036 gfx::Point screen_location(event.location()); 1037 views::View::ConvertPointToScreen(view, &screen_location); 1038 drag_controller_->Drag(screen_location); 1039 } 1040} 1041 1042bool TabStrip::EndDrag(EndDragReason reason) { 1043 if (!drag_controller_.get()) 1044 return false; 1045 bool started_drag = drag_controller_->started_drag(); 1046 drag_controller_->EndDrag(reason); 1047 return started_drag; 1048} 1049 1050Tab* TabStrip::GetTabAt(Tab* tab, const gfx::Point& tab_in_tab_coordinates) { 1051 gfx::Point local_point = tab_in_tab_coordinates; 1052 ConvertPointToTarget(tab, this, &local_point); 1053 1054 views::View* view = GetEventHandlerForPoint(local_point); 1055 if (!view) 1056 return NULL; // No tab contains the point. 1057 1058 // Walk up the view hierarchy until we find a tab, or the TabStrip. 1059 while (view && view != this && view->id() != VIEW_ID_TAB) 1060 view = view->parent(); 1061 1062 return view && view->id() == VIEW_ID_TAB ? static_cast<Tab*>(view) : NULL; 1063} 1064 1065void TabStrip::OnMouseEventInTab(views::View* source, 1066 const ui::MouseEvent& event) { 1067 UpdateStackedLayoutFromMouseEvent(source, event); 1068} 1069 1070bool TabStrip::ShouldPaintTab(const Tab* tab, gfx::Rect* clip) { 1071 // Only touch layout needs to restrict the clip. 1072 if (!(touch_layout_.get() || IsStackingDraggedTabs())) 1073 return true; 1074 1075 int index = GetModelIndexOfTab(tab); 1076 if (index == -1) 1077 return true; // Tab is closing, paint it all. 1078 1079 int active_index = IsStackingDraggedTabs() ? 1080 controller_->GetActiveIndex() : touch_layout_->active_index(); 1081 if (active_index == tab_count()) 1082 active_index--; 1083 1084 if (index < active_index) { 1085 if (tab_at(index)->x() == tab_at(index + 1)->x()) 1086 return false; 1087 1088 if (tab_at(index)->x() > tab_at(index + 1)->x()) 1089 return true; // Can happen during dragging. 1090 1091 clip->SetRect(0, 0, tab_at(index + 1)->x() - tab_at(index)->x() + 1092 kStackedTabLeftClip, 1093 tab_at(index)->height()); 1094 } else if (index > active_index && index > 0) { 1095 const gfx::Rect& tab_bounds(tab_at(index)->bounds()); 1096 const gfx::Rect& previous_tab_bounds(tab_at(index - 1)->bounds()); 1097 if (tab_bounds.x() == previous_tab_bounds.x()) 1098 return false; 1099 1100 if (tab_bounds.x() < previous_tab_bounds.x()) 1101 return true; // Can happen during dragging. 1102 1103 if (previous_tab_bounds.right() + kTabHorizontalOffset != tab_bounds.x()) { 1104 int x = previous_tab_bounds.right() - tab_bounds.x() - 1105 kStackedTabRightClip; 1106 clip->SetRect(x, 0, tab_bounds.width() - x, tab_bounds.height()); 1107 } 1108 } 1109 return true; 1110} 1111 1112bool TabStrip::IsImmersiveStyle() const { 1113 return immersive_style_; 1114} 1115 1116void TabStrip::MouseMovedOutOfHost() { 1117 ResizeLayoutTabs(); 1118 if (reset_to_shrink_on_exit_) { 1119 reset_to_shrink_on_exit_ = false; 1120 SetStackedLayout(false); 1121 controller_->StackedLayoutMaybeChanged(); 1122 } 1123} 1124 1125/////////////////////////////////////////////////////////////////////////////// 1126// TabStrip, views::View overrides: 1127 1128void TabStrip::Layout() { 1129 // Only do a layout if our size changed. 1130 if (last_layout_size_ == size()) 1131 return; 1132 if (IsDragSessionActive()) 1133 return; 1134 DoLayout(); 1135} 1136 1137void TabStrip::PaintChildren(gfx::Canvas* canvas, 1138 const views::CullSet& cull_set) { 1139 // The view order doesn't match the paint order (tabs_ contains the tab 1140 // ordering). Additionally we need to paint the tabs that are closing in 1141 // |tabs_closing_map_|. 1142 Tab* active_tab = NULL; 1143 std::vector<Tab*> tabs_dragging; 1144 std::vector<Tab*> selected_tabs; 1145 int selected_tab_count = 0; 1146 bool is_dragging = false; 1147 int active_tab_index = -1; 1148 // Since |touch_layout_| is created based on number of tabs and width we use 1149 // the ideal state to determine if we should paint stacked. This minimizes 1150 // painting changes as we switch between the two. 1151 const bool stacking = stacked_layout_ || IsStackingDraggedTabs(); 1152 1153 const chrome::HostDesktopType host_desktop_type = 1154 chrome::GetHostDesktopTypeForNativeView(GetWidget()->GetNativeView()); 1155 const int inactive_tab_alpha = 1156 host_desktop_type == chrome::HOST_DESKTOP_TYPE_ASH ? 1157 kInactiveTabAndNewTabButtonAlphaAsh : 1158 kInactiveTabAndNewTabButtonAlpha; 1159 1160 if (inactive_tab_alpha < 255) 1161 canvas->SaveLayerAlpha(inactive_tab_alpha); 1162 1163 PaintClosingTabs(canvas, tab_count(), cull_set); 1164 1165 for (int i = tab_count() - 1; i >= 0; --i) { 1166 Tab* tab = tab_at(i); 1167 if (tab->IsSelected()) 1168 selected_tab_count++; 1169 if (tab->dragging() && !stacking) { 1170 is_dragging = true; 1171 if (tab->IsActive()) { 1172 active_tab = tab; 1173 active_tab_index = i; 1174 } else { 1175 tabs_dragging.push_back(tab); 1176 } 1177 } else if (!tab->IsActive()) { 1178 if (!tab->IsSelected()) { 1179 if (!stacking) 1180 tab->Paint(canvas, cull_set); 1181 } else { 1182 selected_tabs.push_back(tab); 1183 } 1184 } else { 1185 active_tab = tab; 1186 active_tab_index = i; 1187 } 1188 PaintClosingTabs(canvas, i, cull_set); 1189 } 1190 1191 // Draw from the left and then the right if we're in touch mode. 1192 if (stacking && active_tab_index >= 0) { 1193 for (int i = 0; i < active_tab_index; ++i) { 1194 Tab* tab = tab_at(i); 1195 tab->Paint(canvas, cull_set); 1196 } 1197 1198 for (int i = tab_count() - 1; i > active_tab_index; --i) { 1199 Tab* tab = tab_at(i); 1200 tab->Paint(canvas, cull_set); 1201 } 1202 } 1203 if (inactive_tab_alpha < 255) 1204 canvas->Restore(); 1205 1206 if (GetWidget()->ShouldWindowContentsBeTransparent()) { 1207 // Make sure non-active tabs are somewhat transparent. 1208 SkPaint paint; 1209 // If there are multiple tabs selected, fade non-selected tabs more to make 1210 // the selected tabs more noticable. 1211 int alpha = selected_tab_count > 1 ? 1212 kGlassFrameInactiveTabAlphaMultiSelection : 1213 kGlassFrameInactiveTabAlpha; 1214 paint.setColor(SkColorSetARGB(alpha, 255, 255, 255)); 1215 paint.setXfermodeMode(SkXfermode::kDstIn_Mode); 1216 paint.setStyle(SkPaint::kFill_Style); 1217 // The tabstrip area overlaps the toolbar area by 2 px. 1218 canvas->DrawRect(gfx::Rect(0, 0, width(), height() - 2), paint); 1219 } 1220 1221 // Now selected but not active. We don't want these dimmed if using native 1222 // frame, so they're painted after initial pass. 1223 for (size_t i = 0; i < selected_tabs.size(); ++i) 1224 selected_tabs[i]->Paint(canvas, cull_set); 1225 1226 // Next comes the active tab. 1227 if (active_tab && !is_dragging) 1228 active_tab->Paint(canvas, cull_set); 1229 1230 // Paint the New Tab button. 1231 if (inactive_tab_alpha < 255) 1232 canvas->SaveLayerAlpha(inactive_tab_alpha); 1233 newtab_button_->Paint(canvas, cull_set); 1234 if (inactive_tab_alpha < 255) 1235 canvas->Restore(); 1236 1237 // And the dragged tabs. 1238 for (size_t i = 0; i < tabs_dragging.size(); ++i) 1239 tabs_dragging[i]->Paint(canvas, cull_set); 1240 1241 // If the active tab is being dragged, it goes last. 1242 if (active_tab && is_dragging) 1243 active_tab->Paint(canvas, cull_set); 1244} 1245 1246const char* TabStrip::GetClassName() const { 1247 return kViewClassName; 1248} 1249 1250gfx::Size TabStrip::GetPreferredSize() const { 1251 // For stacked tabs the minimum size is calculated as the size needed to 1252 // handle showing any number of tabs. Otherwise report the minimum width as 1253 // the size required for a single selected tab plus the new tab button. Don't 1254 // base it on the actual number of tabs because it's undesirable to have the 1255 // minimum window size change when a new tab is opened. 1256 int needed_width; 1257 if (touch_layout_.get() || adjust_layout_) { 1258 needed_width = Tab::GetTouchWidth() + 1259 (2 * kStackedPadding * kMaxStackedCount); 1260 } else { 1261 needed_width = Tab::GetMinimumSelectedSize().width(); 1262 } 1263 needed_width += new_tab_button_width(); 1264 if (immersive_style_) 1265 return gfx::Size(needed_width, Tab::GetImmersiveHeight()); 1266 return gfx::Size(needed_width, Tab::GetMinimumUnselectedSize().height()); 1267} 1268 1269void TabStrip::OnDragEntered(const DropTargetEvent& event) { 1270 // Force animations to stop, otherwise it makes the index calculation tricky. 1271 StopAnimating(true); 1272 1273 UpdateDropIndex(event); 1274 1275 GURL url; 1276 base::string16 title; 1277 1278 // Check whether the event data includes supported drop data. 1279 if (event.data().GetURLAndTitle( 1280 ui::OSExchangeData::CONVERT_FILENAMES, &url, &title) && 1281 url.is_valid()) { 1282 drop_info_->url = url; 1283 1284 // For file:// URLs, kick off a MIME type request in case they're dropped. 1285 if (url.SchemeIsFile()) 1286 controller()->CheckFileSupported(url); 1287 } 1288} 1289 1290int TabStrip::OnDragUpdated(const DropTargetEvent& event) { 1291 // Update the drop index even if the file is unsupported, to allow 1292 // dragging a file to the contents of another tab. 1293 UpdateDropIndex(event); 1294 1295 if (!drop_info_->file_supported) 1296 return ui::DragDropTypes::DRAG_NONE; 1297 1298 return GetDropEffect(event); 1299} 1300 1301void TabStrip::OnDragExited() { 1302 SetDropIndex(-1, false); 1303} 1304 1305int TabStrip::OnPerformDrop(const DropTargetEvent& event) { 1306 if (!drop_info_.get()) 1307 return ui::DragDropTypes::DRAG_NONE; 1308 1309 const int drop_index = drop_info_->drop_index; 1310 const bool drop_before = drop_info_->drop_before; 1311 const bool file_supported = drop_info_->file_supported; 1312 1313 // Hide the drop indicator. 1314 SetDropIndex(-1, false); 1315 1316 // Do nothing if the file was unsupported or the URL is invalid. The URL may 1317 // have been changed after |drop_info_| was created. 1318 GURL url; 1319 base::string16 title; 1320 if (!file_supported || 1321 !event.data().GetURLAndTitle( 1322 ui::OSExchangeData::CONVERT_FILENAMES, &url, &title) || 1323 !url.is_valid()) 1324 return ui::DragDropTypes::DRAG_NONE; 1325 1326 controller()->PerformDrop(drop_before, drop_index, url); 1327 1328 return GetDropEffect(event); 1329} 1330 1331void TabStrip::GetAccessibleState(ui::AXViewState* state) { 1332 state->role = ui::AX_ROLE_TAB_LIST; 1333} 1334 1335views::View* TabStrip::GetEventHandlerForRect(const gfx::Rect& rect) { 1336 if (!views::UsePointBasedTargeting(rect)) 1337 return View::GetEventHandlerForRect(rect); 1338 const gfx::Point point(rect.CenterPoint()); 1339 1340 if (!touch_layout_.get()) { 1341 // Return any view that isn't a Tab or this TabStrip immediately. We don't 1342 // want to interfere. 1343 views::View* v = View::GetEventHandlerForRect(rect); 1344 if (v && v != this && strcmp(v->GetClassName(), Tab::kViewClassName)) 1345 return v; 1346 1347 views::View* tab = FindTabHitByPoint(point); 1348 if (tab) 1349 return tab; 1350 } else { 1351 if (newtab_button_->visible()) { 1352 views::View* view = 1353 ConvertPointToViewAndGetEventHandler(this, newtab_button_, point); 1354 if (view) 1355 return view; 1356 } 1357 Tab* tab = FindTabForEvent(point); 1358 if (tab) 1359 return ConvertPointToViewAndGetEventHandler(this, tab, point); 1360 } 1361 return this; 1362} 1363 1364views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) { 1365 if (!HitTestPoint(point)) 1366 return NULL; 1367 1368 if (!touch_layout_.get()) { 1369 // Return any view that isn't a Tab or this TabStrip immediately. We don't 1370 // want to interfere. 1371 views::View* v = View::GetTooltipHandlerForPoint(point); 1372 if (v && v != this && strcmp(v->GetClassName(), Tab::kViewClassName)) 1373 return v; 1374 1375 views::View* tab = FindTabHitByPoint(point); 1376 if (tab) 1377 return tab; 1378 } else { 1379 if (newtab_button_->visible()) { 1380 views::View* view = 1381 ConvertPointToViewAndGetTooltipHandler(this, newtab_button_, point); 1382 if (view) 1383 return view; 1384 } 1385 Tab* tab = FindTabForEvent(point); 1386 if (tab) 1387 return ConvertPointToViewAndGetTooltipHandler(this, tab, point); 1388 } 1389 return this; 1390} 1391 1392// static 1393int TabStrip::GetImmersiveHeight() { 1394 return Tab::GetImmersiveHeight(); 1395} 1396 1397int TabStrip::GetMiniTabCount() const { 1398 int mini_count = 0; 1399 while (mini_count < tab_count() && tab_at(mini_count)->data().mini) 1400 mini_count++; 1401 return mini_count; 1402} 1403 1404/////////////////////////////////////////////////////////////////////////////// 1405// TabStrip, views::ButtonListener implementation: 1406 1407void TabStrip::ButtonPressed(views::Button* sender, const ui::Event& event) { 1408 if (sender == newtab_button_) { 1409 content::RecordAction(UserMetricsAction("NewTab_Button")); 1410 UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON, 1411 TabStripModel::NEW_TAB_ENUM_COUNT); 1412 if (event.IsMouseEvent()) { 1413 const ui::MouseEvent& mouse = static_cast<const ui::MouseEvent&>(event); 1414 if (mouse.IsOnlyMiddleMouseButton()) { 1415 base::string16 clipboard_text = GetClipboardText(); 1416 if (!clipboard_text.empty()) 1417 controller()->CreateNewTabWithLocation(clipboard_text); 1418 return; 1419 } 1420 } 1421 1422 controller()->CreateNewTab(); 1423 if (event.type() == ui::ET_GESTURE_TAP) 1424 TouchUMA::RecordGestureAction(TouchUMA::GESTURE_NEWTAB_TAP); 1425 } 1426} 1427 1428/////////////////////////////////////////////////////////////////////////////// 1429// TabStrip, protected: 1430 1431// Overridden to support automation. See automation_proxy_uitest.cc. 1432const views::View* TabStrip::GetViewByID(int view_id) const { 1433 if (tab_count() > 0) { 1434 if (view_id == VIEW_ID_TAB_LAST) { 1435 return tab_at(tab_count() - 1); 1436 } else if ((view_id >= VIEW_ID_TAB_0) && (view_id < VIEW_ID_TAB_LAST)) { 1437 int index = view_id - VIEW_ID_TAB_0; 1438 if (index >= 0 && index < tab_count()) { 1439 return tab_at(index); 1440 } else { 1441 return NULL; 1442 } 1443 } 1444 } 1445 1446 return View::GetViewByID(view_id); 1447} 1448 1449bool TabStrip::OnMousePressed(const ui::MouseEvent& event) { 1450 UpdateStackedLayoutFromMouseEvent(this, event); 1451 // We can't return true here, else clicking in an empty area won't drag the 1452 // window. 1453 return false; 1454} 1455 1456bool TabStrip::OnMouseDragged(const ui::MouseEvent& event) { 1457 ContinueDrag(this, event); 1458 return true; 1459} 1460 1461void TabStrip::OnMouseReleased(const ui::MouseEvent& event) { 1462 EndDrag(END_DRAG_COMPLETE); 1463 UpdateStackedLayoutFromMouseEvent(this, event); 1464} 1465 1466void TabStrip::OnMouseCaptureLost() { 1467 EndDrag(END_DRAG_CAPTURE_LOST); 1468} 1469 1470void TabStrip::OnMouseMoved(const ui::MouseEvent& event) { 1471 UpdateStackedLayoutFromMouseEvent(this, event); 1472} 1473 1474void TabStrip::OnMouseEntered(const ui::MouseEvent& event) { 1475 SetResetToShrinkOnExit(true); 1476} 1477 1478void TabStrip::OnGestureEvent(ui::GestureEvent* event) { 1479 SetResetToShrinkOnExit(false); 1480 switch (event->type()) { 1481 case ui::ET_GESTURE_SCROLL_END: 1482 case ui::ET_SCROLL_FLING_START: 1483 case ui::ET_GESTURE_END: 1484 EndDrag(END_DRAG_COMPLETE); 1485 if (adjust_layout_) { 1486 SetStackedLayout(true); 1487 controller_->StackedLayoutMaybeChanged(); 1488 } 1489 break; 1490 1491 case ui::ET_GESTURE_LONG_PRESS: 1492 if (drag_controller_.get()) 1493 drag_controller_->SetMoveBehavior(TabDragController::REORDER); 1494 break; 1495 1496 case ui::ET_GESTURE_LONG_TAP: { 1497 EndDrag(END_DRAG_CANCEL); 1498 gfx::Point local_point = event->location(); 1499 Tab* tab = FindTabForEvent(local_point); 1500 if (tab) { 1501 ConvertPointToScreen(this, &local_point); 1502 ShowContextMenuForTab(tab, local_point, ui::MENU_SOURCE_TOUCH); 1503 } 1504 break; 1505 } 1506 1507 case ui::ET_GESTURE_SCROLL_UPDATE: 1508 ContinueDrag(this, *event); 1509 break; 1510 1511 case ui::ET_GESTURE_BEGIN: 1512 EndDrag(END_DRAG_CANCEL); 1513 break; 1514 1515 case ui::ET_GESTURE_TAP: { 1516 const int active_index = controller_->GetActiveIndex(); 1517 DCHECK_NE(-1, active_index); 1518 Tab* active_tab = tab_at(active_index); 1519 TouchUMA::GestureActionType action = TouchUMA::GESTURE_TABNOSWITCH_TAP; 1520 if (active_tab->tab_activated_with_last_gesture_begin()) 1521 action = TouchUMA::GESTURE_TABSWITCH_TAP; 1522 TouchUMA::RecordGestureAction(action); 1523 break; 1524 } 1525 1526 default: 1527 break; 1528 } 1529 event->SetHandled(); 1530} 1531 1532/////////////////////////////////////////////////////////////////////////////// 1533// TabStrip, private: 1534 1535void TabStrip::Init() { 1536 set_id(VIEW_ID_TAB_STRIP); 1537 // So we get enter/exit on children to switch stacked layout on and off. 1538 set_notify_enter_exit_on_child(true); 1539 newtab_button_bounds_.SetRect(0, 1540 0, 1541 kNewTabButtonAssetWidth, 1542 kNewTabButtonAssetHeight + 1543 kNewTabButtonVerticalOffset); 1544 newtab_button_ = new NewTabButton(this, this); 1545 newtab_button_->SetTooltipText( 1546 l10n_util::GetStringUTF16(IDS_TOOLTIP_NEW_TAB)); 1547 newtab_button_->SetAccessibleName( 1548 l10n_util::GetStringUTF16(IDS_ACCNAME_NEWTAB)); 1549 newtab_button_->SetImageAlignment(views::ImageButton::ALIGN_LEFT, 1550 views::ImageButton::ALIGN_BOTTOM); 1551 AddChildView(newtab_button_); 1552 if (drop_indicator_width == 0) { 1553 // Direction doesn't matter, both images are the same size. 1554 gfx::ImageSkia* drop_image = GetDropArrowImage(true); 1555 drop_indicator_width = drop_image->width(); 1556 drop_indicator_height = drop_image->height(); 1557 } 1558} 1559 1560Tab* TabStrip::CreateTab() { 1561 Tab* tab = new Tab(this); 1562 tab->set_animation_container(animation_container_.get()); 1563 return tab; 1564} 1565 1566void TabStrip::StartInsertTabAnimation(int model_index) { 1567 PrepareForAnimation(); 1568 1569 // The TabStrip can now use its entire width to lay out Tabs. 1570 in_tab_close_ = false; 1571 available_width_for_tabs_ = -1; 1572 1573 GenerateIdealBounds(); 1574 1575 Tab* tab = tab_at(model_index); 1576 if (model_index == 0) { 1577 tab->SetBounds(0, ideal_bounds(model_index).y(), 0, 1578 ideal_bounds(model_index).height()); 1579 } else { 1580 Tab* last_tab = tab_at(model_index - 1); 1581 tab->SetBounds(last_tab->bounds().right() + kTabHorizontalOffset, 1582 ideal_bounds(model_index).y(), 0, 1583 ideal_bounds(model_index).height()); 1584 } 1585 1586 AnimateToIdealBounds(); 1587} 1588 1589void TabStrip::StartMoveTabAnimation() { 1590 PrepareForAnimation(); 1591 GenerateIdealBounds(); 1592 AnimateToIdealBounds(); 1593} 1594 1595void TabStrip::StartRemoveTabAnimation(int model_index) { 1596 PrepareForAnimation(); 1597 1598 // Mark the tab as closing. 1599 Tab* tab = tab_at(model_index); 1600 tab->set_closing(true); 1601 1602 RemoveTabFromViewModel(model_index); 1603 1604 ScheduleRemoveTabAnimation(tab); 1605} 1606 1607void TabStrip::ScheduleRemoveTabAnimation(Tab* tab) { 1608 // Start an animation for the tabs. 1609 GenerateIdealBounds(); 1610 AnimateToIdealBounds(); 1611 1612 // Animate the tab being closed to 0x0. 1613 gfx::Rect tab_bounds = tab->bounds(); 1614 tab_bounds.set_width(0); 1615 bounds_animator_.AnimateViewTo(tab, tab_bounds); 1616 1617 // Register delegate to do cleanup when done, BoundsAnimator takes 1618 // ownership of RemoveTabDelegate. 1619 bounds_animator_.SetAnimationDelegate(tab, new RemoveTabDelegate(this, tab), 1620 true); 1621 1622 // Don't animate the new tab button when dragging tabs. Otherwise it looks 1623 // like the new tab button magically appears from beyond the end of the tab 1624 // strip. 1625 if (TabDragController::IsAttachedTo(this)) { 1626 bounds_animator_.StopAnimatingView(newtab_button_); 1627 newtab_button_->SetBoundsRect(newtab_button_bounds_); 1628 } 1629} 1630 1631void TabStrip::AnimateToIdealBounds() { 1632 for (int i = 0; i < tab_count(); ++i) { 1633 Tab* tab = tab_at(i); 1634 if (!tab->dragging()) 1635 bounds_animator_.AnimateViewTo(tab, ideal_bounds(i)); 1636 } 1637 1638 bounds_animator_.AnimateViewTo(newtab_button_, newtab_button_bounds_); 1639} 1640 1641bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { 1642 return in_tab_close_; 1643} 1644 1645void TabStrip::DoLayout() { 1646 last_layout_size_ = size(); 1647 1648 StopAnimating(false); 1649 1650 SwapLayoutIfNecessary(); 1651 1652 if (touch_layout_.get()) 1653 touch_layout_->SetWidth(size().width() - new_tab_button_width()); 1654 1655 GenerateIdealBounds(); 1656 1657 views::ViewModelUtils::SetViewBoundsToIdealBounds(tabs_); 1658 1659 SchedulePaint(); 1660 1661 bounds_animator_.StopAnimatingView(newtab_button_); 1662 newtab_button_->SetBoundsRect(newtab_button_bounds_); 1663} 1664 1665void TabStrip::DragActiveTab(const std::vector<int>& initial_positions, 1666 int delta) { 1667 DCHECK_EQ(tab_count(), static_cast<int>(initial_positions.size())); 1668 if (!touch_layout_.get()) { 1669 StackDraggedTabs(delta); 1670 return; 1671 } 1672 SetIdealBoundsFromPositions(initial_positions); 1673 touch_layout_->DragActiveTab(delta); 1674 DoLayout(); 1675} 1676 1677void TabStrip::SetIdealBoundsFromPositions(const std::vector<int>& positions) { 1678 if (static_cast<size_t>(tab_count()) != positions.size()) 1679 return; 1680 1681 for (int i = 0; i < tab_count(); ++i) { 1682 gfx::Rect bounds(ideal_bounds(i)); 1683 bounds.set_x(positions[i]); 1684 set_ideal_bounds(i, bounds); 1685 } 1686} 1687 1688void TabStrip::StackDraggedTabs(int delta) { 1689 DCHECK(!touch_layout_.get()); 1690 GenerateIdealBounds(); 1691 const int active_index = controller_->GetActiveIndex(); 1692 DCHECK_NE(-1, active_index); 1693 if (delta < 0) { 1694 // Drag the tabs to the left, stacking tabs before the active tab. 1695 const int adjusted_delta = 1696 std::min(ideal_bounds(active_index).x() - 1697 kStackedPadding * std::min(active_index, kMaxStackedCount), 1698 -delta); 1699 for (int i = 0; i <= active_index; ++i) { 1700 const int min_x = std::min(i, kMaxStackedCount) * kStackedPadding; 1701 gfx::Rect new_bounds(ideal_bounds(i)); 1702 new_bounds.set_x(std::max(min_x, new_bounds.x() - adjusted_delta)); 1703 set_ideal_bounds(i, new_bounds); 1704 } 1705 const bool is_active_mini = tab_at(active_index)->data().mini; 1706 const int active_width = ideal_bounds(active_index).width(); 1707 for (int i = active_index + 1; i < tab_count(); ++i) { 1708 const int max_x = ideal_bounds(active_index).x() + 1709 (kStackedPadding * std::min(i - active_index, kMaxStackedCount)); 1710 gfx::Rect new_bounds(ideal_bounds(i)); 1711 int new_x = std::max(new_bounds.x() + delta, max_x); 1712 if (new_x == max_x && !tab_at(i)->data().mini && !is_active_mini && 1713 new_bounds.width() != active_width) 1714 new_x += (active_width - new_bounds.width()); 1715 new_bounds.set_x(new_x); 1716 set_ideal_bounds(i, new_bounds); 1717 } 1718 } else { 1719 // Drag the tabs to the right, stacking tabs after the active tab. 1720 const int last_tab_width = ideal_bounds(tab_count() - 1).width(); 1721 const int last_tab_x = width() - new_tab_button_width() - last_tab_width; 1722 if (active_index == tab_count() - 1 && 1723 ideal_bounds(tab_count() - 1).x() == last_tab_x) 1724 return; 1725 const int adjusted_delta = 1726 std::min(last_tab_x - 1727 kStackedPadding * std::min(tab_count() - active_index - 1, 1728 kMaxStackedCount) - 1729 ideal_bounds(active_index).x(), 1730 delta); 1731 for (int last_index = tab_count() - 1, i = last_index; i >= active_index; 1732 --i) { 1733 const int max_x = last_tab_x - 1734 std::min(tab_count() - i - 1, kMaxStackedCount) * kStackedPadding; 1735 gfx::Rect new_bounds(ideal_bounds(i)); 1736 int new_x = std::min(max_x, new_bounds.x() + adjusted_delta); 1737 // Because of rounding not all tabs are the same width. Adjust the 1738 // position to accommodate this, otherwise the stacking is off. 1739 if (new_x == max_x && !tab_at(i)->data().mini && 1740 new_bounds.width() != last_tab_width) 1741 new_x += (last_tab_width - new_bounds.width()); 1742 new_bounds.set_x(new_x); 1743 set_ideal_bounds(i, new_bounds); 1744 } 1745 for (int i = active_index - 1; i >= 0; --i) { 1746 const int min_x = ideal_bounds(active_index).x() - 1747 std::min(active_index - i, kMaxStackedCount) * kStackedPadding; 1748 gfx::Rect new_bounds(ideal_bounds(i)); 1749 new_bounds.set_x(std::min(min_x, new_bounds.x() + delta)); 1750 set_ideal_bounds(i, new_bounds); 1751 } 1752 if (ideal_bounds(tab_count() - 1).right() >= newtab_button_->x()) 1753 newtab_button_->SetVisible(false); 1754 } 1755 views::ViewModelUtils::SetViewBoundsToIdealBounds(tabs_); 1756 SchedulePaint(); 1757} 1758 1759bool TabStrip::IsStackingDraggedTabs() const { 1760 return drag_controller_.get() && drag_controller_->started_drag() && 1761 (drag_controller_->move_behavior() == 1762 TabDragController::MOVE_VISIBILE_TABS); 1763} 1764 1765void TabStrip::LayoutDraggedTabsAt(const std::vector<Tab*>& tabs, 1766 Tab* active_tab, 1767 const gfx::Point& location, 1768 bool initial_drag) { 1769 // Immediately hide the new tab button if the last tab is being dragged. 1770 if (tab_at(tab_count() - 1)->dragging()) 1771 newtab_button_->SetVisible(false); 1772 std::vector<gfx::Rect> bounds; 1773 CalculateBoundsForDraggedTabs(tabs, &bounds); 1774 DCHECK_EQ(tabs.size(), bounds.size()); 1775 int active_tab_model_index = GetModelIndexOfTab(active_tab); 1776 int active_tab_index = static_cast<int>( 1777 std::find(tabs.begin(), tabs.end(), active_tab) - tabs.begin()); 1778 for (size_t i = 0; i < tabs.size(); ++i) { 1779 Tab* tab = tabs[i]; 1780 gfx::Rect new_bounds = bounds[i]; 1781 new_bounds.Offset(location.x(), location.y()); 1782 int consecutive_index = 1783 active_tab_model_index - (active_tab_index - static_cast<int>(i)); 1784 // If this is the initial layout during a drag and the tabs aren't 1785 // consecutive animate the view into position. Do the same if the tab is 1786 // already animating (which means we previously caused it to animate). 1787 if ((initial_drag && 1788 GetModelIndexOfTab(tabs[i]) != consecutive_index) || 1789 bounds_animator_.IsAnimating(tabs[i])) { 1790 bounds_animator_.SetTargetBounds(tabs[i], new_bounds); 1791 } else { 1792 tab->SetBoundsRect(new_bounds); 1793 } 1794 } 1795} 1796 1797void TabStrip::CalculateBoundsForDraggedTabs(const std::vector<Tab*>& tabs, 1798 std::vector<gfx::Rect>* bounds) { 1799 int x = 0; 1800 for (size_t i = 0; i < tabs.size(); ++i) { 1801 Tab* tab = tabs[i]; 1802 if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) 1803 x += kMiniToNonMiniGap; 1804 gfx::Rect new_bounds = tab->bounds(); 1805 new_bounds.set_origin(gfx::Point(x, 0)); 1806 bounds->push_back(new_bounds); 1807 x += tab->width() + kTabHorizontalOffset; 1808 } 1809} 1810 1811int TabStrip::GetSizeNeededForTabs(const std::vector<Tab*>& tabs) { 1812 int width = 0; 1813 for (size_t i = 0; i < tabs.size(); ++i) { 1814 Tab* tab = tabs[i]; 1815 width += tab->width(); 1816 if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) 1817 width += kMiniToNonMiniGap; 1818 } 1819 if (tabs.size() > 0) 1820 width += kTabHorizontalOffset * static_cast<int>(tabs.size() - 1); 1821 return width; 1822} 1823 1824void TabStrip::RemoveTabFromViewModel(int index) { 1825 // We still need to paint the tab until we actually remove it. Put it 1826 // in tabs_closing_map_ so we can find it. 1827 tabs_closing_map_[index].push_back(tab_at(index)); 1828 UpdateTabsClosingMap(index + 1, -1); 1829 tabs_.Remove(index); 1830} 1831 1832void TabStrip::RemoveAndDeleteTab(Tab* tab) { 1833 scoped_ptr<Tab> deleter(tab); 1834 for (TabsClosingMap::iterator i(tabs_closing_map_.begin()); 1835 i != tabs_closing_map_.end(); ++i) { 1836 std::vector<Tab*>::iterator j = 1837 std::find(i->second.begin(), i->second.end(), tab); 1838 if (j != i->second.end()) { 1839 i->second.erase(j); 1840 if (i->second.empty()) 1841 tabs_closing_map_.erase(i); 1842 return; 1843 } 1844 } 1845 NOTREACHED(); 1846} 1847 1848void TabStrip::UpdateTabsClosingMap(int index, int delta) { 1849 if (tabs_closing_map_.empty()) 1850 return; 1851 1852 if (delta == -1 && 1853 tabs_closing_map_.find(index - 1) != tabs_closing_map_.end() && 1854 tabs_closing_map_.find(index) != tabs_closing_map_.end()) { 1855 const std::vector<Tab*>& tabs(tabs_closing_map_[index]); 1856 tabs_closing_map_[index - 1].insert( 1857 tabs_closing_map_[index - 1].end(), tabs.begin(), tabs.end()); 1858 } 1859 TabsClosingMap updated_map; 1860 for (TabsClosingMap::iterator i(tabs_closing_map_.begin()); 1861 i != tabs_closing_map_.end(); ++i) { 1862 if (i->first > index) 1863 updated_map[i->first + delta] = i->second; 1864 else if (i->first < index) 1865 updated_map[i->first] = i->second; 1866 } 1867 if (delta > 0 && tabs_closing_map_.find(index) != tabs_closing_map_.end()) 1868 updated_map[index + delta] = tabs_closing_map_[index]; 1869 tabs_closing_map_.swap(updated_map); 1870} 1871 1872void TabStrip::StartedDraggingTabs(const std::vector<Tab*>& tabs) { 1873 // Let the controller know that the user started dragging tabs. 1874 controller()->OnStartedDraggingTabs(); 1875 1876 // Hide the new tab button immediately if we didn't originate the drag. 1877 if (!drag_controller_.get()) 1878 newtab_button_->SetVisible(false); 1879 1880 PrepareForAnimation(); 1881 1882 // Reset dragging state of existing tabs. 1883 for (int i = 0; i < tab_count(); ++i) 1884 tab_at(i)->set_dragging(false); 1885 1886 for (size_t i = 0; i < tabs.size(); ++i) { 1887 tabs[i]->set_dragging(true); 1888 bounds_animator_.StopAnimatingView(tabs[i]); 1889 } 1890 1891 // Move the dragged tabs to their ideal bounds. 1892 GenerateIdealBounds(); 1893 1894 // Sets the bounds of the dragged tabs. 1895 for (size_t i = 0; i < tabs.size(); ++i) { 1896 int tab_data_index = GetModelIndexOfTab(tabs[i]); 1897 DCHECK_NE(-1, tab_data_index); 1898 tabs[i]->SetBoundsRect(ideal_bounds(tab_data_index)); 1899 } 1900 SchedulePaint(); 1901} 1902 1903void TabStrip::DraggedTabsDetached() { 1904 // Let the controller know that the user is not dragging this tabstrip's tabs 1905 // anymore. 1906 controller()->OnStoppedDraggingTabs(); 1907 newtab_button_->SetVisible(true); 1908} 1909 1910void TabStrip::StoppedDraggingTabs(const std::vector<Tab*>& tabs, 1911 const std::vector<int>& initial_positions, 1912 bool move_only, 1913 bool completed) { 1914 // Let the controller know that the user stopped dragging tabs. 1915 controller()->OnStoppedDraggingTabs(); 1916 1917 newtab_button_->SetVisible(true); 1918 if (move_only && touch_layout_.get()) { 1919 if (completed) { 1920 touch_layout_->SizeToFit(); 1921 } else { 1922 SetIdealBoundsFromPositions(initial_positions); 1923 } 1924 } 1925 bool is_first_tab = true; 1926 for (size_t i = 0; i < tabs.size(); ++i) 1927 StoppedDraggingTab(tabs[i], &is_first_tab); 1928} 1929 1930void TabStrip::StoppedDraggingTab(Tab* tab, bool* is_first_tab) { 1931 int tab_data_index = GetModelIndexOfTab(tab); 1932 if (tab_data_index == -1) { 1933 // The tab was removed before the drag completed. Don't do anything. 1934 return; 1935 } 1936 1937 if (*is_first_tab) { 1938 *is_first_tab = false; 1939 PrepareForAnimation(); 1940 1941 // Animate the view back to its correct position. 1942 GenerateIdealBounds(); 1943 AnimateToIdealBounds(); 1944 } 1945 bounds_animator_.AnimateViewTo(tab, ideal_bounds(tab_data_index)); 1946 // Install a delegate to reset the dragging state when done. We have to leave 1947 // dragging true for the tab otherwise it'll draw beneath the new tab button. 1948 bounds_animator_.SetAnimationDelegate( 1949 tab, new ResetDraggingStateDelegate(tab), true); 1950} 1951 1952void TabStrip::OwnDragController(TabDragController* controller) { 1953 // Typically, ReleaseDragController() and OwnDragController() calls are paired 1954 // via corresponding calls to TabDragController::Detach() and 1955 // TabDragController::Attach(). There is one exception to that rule: when a 1956 // drag might start, we create a TabDragController that is owned by the 1957 // potential source tabstrip in MaybeStartDrag(). If a drag actually starts, 1958 // we then call Attach() on the source tabstrip, but since the source tabstrip 1959 // already owns the TabDragController, so we don't need to do anything. 1960 if (controller != drag_controller_.get()) 1961 drag_controller_.reset(controller); 1962} 1963 1964void TabStrip::DestroyDragController() { 1965 newtab_button_->SetVisible(true); 1966 drag_controller_.reset(); 1967} 1968 1969TabDragController* TabStrip::ReleaseDragController() { 1970 return drag_controller_.release(); 1971} 1972 1973void TabStrip::PaintClosingTabs(gfx::Canvas* canvas, 1974 int index, 1975 const views::CullSet& cull_set) { 1976 if (tabs_closing_map_.find(index) == tabs_closing_map_.end()) 1977 return; 1978 1979 const std::vector<Tab*>& tabs = tabs_closing_map_[index]; 1980 for (std::vector<Tab*>::const_reverse_iterator i(tabs.rbegin()); 1981 i != tabs.rend(); ++i) { 1982 (*i)->Paint(canvas, cull_set); 1983 } 1984} 1985 1986void TabStrip::UpdateStackedLayoutFromMouseEvent(views::View* source, 1987 const ui::MouseEvent& event) { 1988 if (!adjust_layout_) 1989 return; 1990 1991 // The following code attempts to switch to shrink (not stacked) layout when 1992 // the mouse exits the tabstrip (or the mouse is pressed on a stacked tab) and 1993 // to stacked layout when a touch device is used. This is made problematic by 1994 // windows generating mouse move events that do not clearly indicate the move 1995 // is the result of a touch device. This assumes a real mouse is used if 1996 // |kMouseMoveCountBeforeConsiderReal| mouse move events are received within 1997 // the time window |kMouseMoveTimeMS|. At the time we get a mouse press we 1998 // know whether its from a touch device or not, but we don't layout then else 1999 // everything shifts. Instead we wait for the release. 2000 // 2001 // TODO(sky): revisit this when touch events are really plumbed through. 2002 2003 switch (event.type()) { 2004 case ui::ET_MOUSE_PRESSED: 2005 mouse_move_count_ = 0; 2006 last_mouse_move_time_ = base::TimeTicks(); 2007 SetResetToShrinkOnExit((event.flags() & ui::EF_FROM_TOUCH) == 0); 2008 if (reset_to_shrink_on_exit_ && touch_layout_.get()) { 2009 gfx::Point tab_strip_point(event.location()); 2010 views::View::ConvertPointToTarget(source, this, &tab_strip_point); 2011 Tab* tab = FindTabForEvent(tab_strip_point); 2012 if (tab && touch_layout_->IsStacked(GetModelIndexOfTab(tab))) { 2013 SetStackedLayout(false); 2014 controller_->StackedLayoutMaybeChanged(); 2015 } 2016 } 2017 break; 2018 2019 case ui::ET_MOUSE_MOVED: { 2020#if defined(USE_ASH) 2021 // Ash does not synthesize mouse events from touch events. 2022 SetResetToShrinkOnExit(true); 2023#else 2024 gfx::Point location(event.location()); 2025 ConvertPointToTarget(source, this, &location); 2026 if (location == last_mouse_move_location_) 2027 return; // Ignore spurious moves. 2028 last_mouse_move_location_ = location; 2029 if ((event.flags() & ui::EF_FROM_TOUCH) == 0 && 2030 (event.flags() & ui::EF_IS_SYNTHESIZED) == 0) { 2031 if ((base::TimeTicks::Now() - last_mouse_move_time_).InMilliseconds() < 2032 kMouseMoveTimeMS) { 2033 if (mouse_move_count_++ == kMouseMoveCountBeforeConsiderReal) 2034 SetResetToShrinkOnExit(true); 2035 } else { 2036 mouse_move_count_ = 1; 2037 last_mouse_move_time_ = base::TimeTicks::Now(); 2038 } 2039 } else { 2040 last_mouse_move_time_ = base::TimeTicks(); 2041 } 2042#endif 2043 break; 2044 } 2045 2046 case ui::ET_MOUSE_RELEASED: { 2047 gfx::Point location(event.location()); 2048 ConvertPointToTarget(source, this, &location); 2049 last_mouse_move_location_ = location; 2050 mouse_move_count_ = 0; 2051 last_mouse_move_time_ = base::TimeTicks(); 2052 if ((event.flags() & ui::EF_FROM_TOUCH) == ui::EF_FROM_TOUCH) { 2053 SetStackedLayout(true); 2054 controller_->StackedLayoutMaybeChanged(); 2055 } 2056 break; 2057 } 2058 2059 default: 2060 break; 2061 } 2062} 2063 2064void TabStrip::GetCurrentTabWidths(double* unselected_width, 2065 double* selected_width) const { 2066 *unselected_width = current_unselected_width_; 2067 *selected_width = current_selected_width_; 2068} 2069 2070void TabStrip::GetDesiredTabWidths(int tab_count, 2071 int mini_tab_count, 2072 double* unselected_width, 2073 double* selected_width) const { 2074 DCHECK(tab_count >= 0 && mini_tab_count >= 0 && mini_tab_count <= tab_count); 2075 const double min_unselected_width = Tab::GetMinimumUnselectedSize().width(); 2076 const double min_selected_width = Tab::GetMinimumSelectedSize().width(); 2077 2078 *unselected_width = min_unselected_width; 2079 *selected_width = min_selected_width; 2080 2081 if (tab_count == 0) { 2082 // Return immediately to avoid divide-by-zero below. 2083 return; 2084 } 2085 2086 // Determine how much space we can actually allocate to tabs. 2087 int available_width; 2088 if (available_width_for_tabs_ < 0) { 2089 available_width = width() - new_tab_button_width(); 2090 } else { 2091 // Interesting corner case: if |available_width_for_tabs_| > the result 2092 // of the calculation in the conditional arm above, the strip is in 2093 // overflow. We can either use the specified width or the true available 2094 // width here; the first preserves the consistent "leave the last tab under 2095 // the user's mouse so they can close many tabs" behavior at the cost of 2096 // prolonging the glitchy appearance of the overflow state, while the second 2097 // gets us out of overflow as soon as possible but forces the user to move 2098 // their mouse for a few tabs' worth of closing. We choose visual 2099 // imperfection over behavioral imperfection and select the first option. 2100 available_width = available_width_for_tabs_; 2101 } 2102 2103 if (mini_tab_count > 0) { 2104 available_width -= 2105 mini_tab_count * (Tab::GetMiniWidth() + kTabHorizontalOffset); 2106 tab_count -= mini_tab_count; 2107 if (tab_count == 0) { 2108 *selected_width = *unselected_width = Tab::GetStandardSize().width(); 2109 return; 2110 } 2111 // Account for gap between the last mini-tab and first non-mini-tab. 2112 available_width -= kMiniToNonMiniGap; 2113 } 2114 2115 // Calculate the desired tab widths by dividing the available space into equal 2116 // portions. Don't let tabs get larger than the "standard width" or smaller 2117 // than the minimum width for each type, respectively. 2118 const int total_offset = kTabHorizontalOffset * (tab_count - 1); 2119 const double desired_tab_width = std::min((static_cast<double>( 2120 available_width - total_offset) / static_cast<double>(tab_count)), 2121 static_cast<double>(Tab::GetStandardSize().width())); 2122 *unselected_width = std::max(desired_tab_width, min_unselected_width); 2123 *selected_width = std::max(desired_tab_width, min_selected_width); 2124 2125 // When there are multiple tabs, we'll have one selected and some unselected 2126 // tabs. If the desired width was between the minimum sizes of these types, 2127 // try to shrink the tabs with the smaller minimum. For example, if we have 2128 // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5. If 2129 // selected tabs have a minimum width of 4 and unselected tabs have a minimum 2130 // width of 1, the above code would set *unselected_width = 2.5, 2131 // *selected_width = 4, which results in a total width of 11.5. Instead, we 2132 // want to set *unselected_width = 2, *selected_width = 4, for a total width 2133 // of 10. 2134 if (tab_count > 1) { 2135 if ((min_unselected_width < min_selected_width) && 2136 (desired_tab_width < min_selected_width)) { 2137 // Unselected width = (total width - selected width) / (num_tabs - 1) 2138 *unselected_width = std::max(static_cast<double>( 2139 available_width - total_offset - min_selected_width) / 2140 static_cast<double>(tab_count - 1), min_unselected_width); 2141 } else if ((min_unselected_width > min_selected_width) && 2142 (desired_tab_width < min_unselected_width)) { 2143 // Selected width = (total width - (unselected width * (num_tabs - 1))) 2144 *selected_width = std::max(available_width - total_offset - 2145 (min_unselected_width * (tab_count - 1)), min_selected_width); 2146 } 2147 } 2148} 2149 2150void TabStrip::ResizeLayoutTabs() { 2151 // We've been called back after the TabStrip has been emptied out (probably 2152 // just prior to the window being destroyed). We need to do nothing here or 2153 // else GetTabAt below will crash. 2154 if (tab_count() == 0) 2155 return; 2156 2157 // It is critically important that this is unhooked here, otherwise we will 2158 // keep spying on messages forever. 2159 RemoveMessageLoopObserver(); 2160 2161 in_tab_close_ = false; 2162 available_width_for_tabs_ = -1; 2163 int mini_tab_count = GetMiniTabCount(); 2164 if (mini_tab_count == tab_count()) { 2165 // Only mini-tabs, we know the tab widths won't have changed (all 2166 // mini-tabs have the same width), so there is nothing to do. 2167 return; 2168 } 2169 // Don't try and avoid layout based on tab sizes. If tabs are small enough 2170 // then the width of the active tab may not change, but other widths may 2171 // have. This is particularly important if we've overflowed (all tabs are at 2172 // the min). 2173 StartResizeLayoutAnimation(); 2174} 2175 2176void TabStrip::ResizeLayoutTabsFromTouch() { 2177 // Don't resize if the user is interacting with the tabstrip. 2178 if (!drag_controller_.get()) 2179 ResizeLayoutTabs(); 2180 else 2181 StartResizeLayoutTabsFromTouchTimer(); 2182} 2183 2184void TabStrip::StartResizeLayoutTabsFromTouchTimer() { 2185 resize_layout_timer_.Stop(); 2186 resize_layout_timer_.Start( 2187 FROM_HERE, base::TimeDelta::FromMilliseconds(kTouchResizeLayoutTimeMS), 2188 this, &TabStrip::ResizeLayoutTabsFromTouch); 2189} 2190 2191void TabStrip::SetTabBoundsForDrag(const std::vector<gfx::Rect>& tab_bounds) { 2192 StopAnimating(false); 2193 DCHECK_EQ(tab_count(), static_cast<int>(tab_bounds.size())); 2194 for (int i = 0; i < tab_count(); ++i) 2195 tab_at(i)->SetBoundsRect(tab_bounds[i]); 2196 // Reset the layout size as we've effectively layed out a different size. 2197 // This ensures a layout happens after the drag is done. 2198 last_layout_size_ = gfx::Size(); 2199} 2200 2201void TabStrip::AddMessageLoopObserver() { 2202 if (!mouse_watcher_.get()) { 2203 mouse_watcher_.reset( 2204 new views::MouseWatcher( 2205 new views::MouseWatcherViewHost( 2206 this, gfx::Insets(0, 0, kTabStripAnimationVSlop, 0)), 2207 this)); 2208 } 2209 mouse_watcher_->Start(); 2210} 2211 2212void TabStrip::RemoveMessageLoopObserver() { 2213 mouse_watcher_.reset(NULL); 2214} 2215 2216gfx::Rect TabStrip::GetDropBounds(int drop_index, 2217 bool drop_before, 2218 bool* is_beneath) { 2219 DCHECK_NE(drop_index, -1); 2220 int center_x; 2221 if (drop_index < tab_count()) { 2222 Tab* tab = tab_at(drop_index); 2223 if (drop_before) 2224 center_x = tab->x() - (kTabHorizontalOffset / 2); 2225 else 2226 center_x = tab->x() + (tab->width() / 2); 2227 } else { 2228 Tab* last_tab = tab_at(drop_index - 1); 2229 center_x = last_tab->x() + last_tab->width() + (kTabHorizontalOffset / 2); 2230 } 2231 2232 // Mirror the center point if necessary. 2233 center_x = GetMirroredXInView(center_x); 2234 2235 // Determine the screen bounds. 2236 gfx::Point drop_loc(center_x - drop_indicator_width / 2, 2237 -drop_indicator_height); 2238 ConvertPointToScreen(this, &drop_loc); 2239 gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), drop_indicator_width, 2240 drop_indicator_height); 2241 2242 // If the rect doesn't fit on the monitor, push the arrow to the bottom. 2243 gfx::Screen* screen = gfx::Screen::GetScreenFor(GetWidget()->GetNativeView()); 2244 gfx::Display display = screen->GetDisplayMatching(drop_bounds); 2245 *is_beneath = !display.bounds().Contains(drop_bounds); 2246 if (*is_beneath) 2247 drop_bounds.Offset(0, drop_bounds.height() + height()); 2248 2249 return drop_bounds; 2250} 2251 2252void TabStrip::UpdateDropIndex(const DropTargetEvent& event) { 2253 // If the UI layout is right-to-left, we need to mirror the mouse 2254 // coordinates since we calculate the drop index based on the 2255 // original (and therefore non-mirrored) positions of the tabs. 2256 const int x = GetMirroredXInView(event.x()); 2257 // We don't allow replacing the urls of mini-tabs. 2258 for (int i = GetMiniTabCount(); i < tab_count(); ++i) { 2259 Tab* tab = tab_at(i); 2260 const int tab_max_x = tab->x() + tab->width(); 2261 const int hot_width = tab->width() / kTabEdgeRatioInverse; 2262 if (x < tab_max_x) { 2263 if (x < tab->x() + hot_width) 2264 SetDropIndex(i, true); 2265 else if (x >= tab_max_x - hot_width) 2266 SetDropIndex(i + 1, true); 2267 else 2268 SetDropIndex(i, false); 2269 return; 2270 } 2271 } 2272 2273 // The drop isn't over a tab, add it to the end. 2274 SetDropIndex(tab_count(), true); 2275} 2276 2277void TabStrip::SetDropIndex(int tab_data_index, bool drop_before) { 2278 // Let the controller know of the index update. 2279 controller()->OnDropIndexUpdate(tab_data_index, drop_before); 2280 2281 if (tab_data_index == -1) { 2282 if (drop_info_.get()) 2283 drop_info_.reset(NULL); 2284 return; 2285 } 2286 2287 if (drop_info_.get() && drop_info_->drop_index == tab_data_index && 2288 drop_info_->drop_before == drop_before) { 2289 return; 2290 } 2291 2292 bool is_beneath; 2293 gfx::Rect drop_bounds = GetDropBounds(tab_data_index, drop_before, 2294 &is_beneath); 2295 2296 if (!drop_info_.get()) { 2297 drop_info_.reset( 2298 new DropInfo(tab_data_index, drop_before, !is_beneath, GetWidget())); 2299 } else { 2300 drop_info_->drop_index = tab_data_index; 2301 drop_info_->drop_before = drop_before; 2302 if (is_beneath == drop_info_->point_down) { 2303 drop_info_->point_down = !is_beneath; 2304 drop_info_->arrow_view->SetImage( 2305 GetDropArrowImage(drop_info_->point_down)); 2306 } 2307 } 2308 2309 // Reposition the window. Need to show it too as the window is initially 2310 // hidden. 2311 drop_info_->arrow_window->SetBounds(drop_bounds); 2312 drop_info_->arrow_window->Show(); 2313} 2314 2315int TabStrip::GetDropEffect(const ui::DropTargetEvent& event) { 2316 const int source_ops = event.source_operations(); 2317 if (source_ops & ui::DragDropTypes::DRAG_COPY) 2318 return ui::DragDropTypes::DRAG_COPY; 2319 if (source_ops & ui::DragDropTypes::DRAG_LINK) 2320 return ui::DragDropTypes::DRAG_LINK; 2321 return ui::DragDropTypes::DRAG_MOVE; 2322} 2323 2324// static 2325gfx::ImageSkia* TabStrip::GetDropArrowImage(bool is_down) { 2326 return ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 2327 is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); 2328} 2329 2330// TabStrip::DropInfo ---------------------------------------------------------- 2331 2332TabStrip::DropInfo::DropInfo(int drop_index, 2333 bool drop_before, 2334 bool point_down, 2335 views::Widget* context) 2336 : drop_index(drop_index), 2337 drop_before(drop_before), 2338 point_down(point_down), 2339 file_supported(true) { 2340 arrow_view = new views::ImageView; 2341 arrow_view->SetImage(GetDropArrowImage(point_down)); 2342 2343 arrow_window = new views::Widget; 2344 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); 2345 params.keep_on_top = true; 2346 params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; 2347 params.accept_events = false; 2348 params.bounds = gfx::Rect(drop_indicator_width, drop_indicator_height); 2349 params.context = context->GetNativeView(); 2350 arrow_window->Init(params); 2351 arrow_window->SetContentsView(arrow_view); 2352} 2353 2354TabStrip::DropInfo::~DropInfo() { 2355 // Close eventually deletes the window, which deletes arrow_view too. 2356 arrow_window->Close(); 2357} 2358 2359/////////////////////////////////////////////////////////////////////////////// 2360 2361void TabStrip::PrepareForAnimation() { 2362 if (!IsDragSessionActive() && !TabDragController::IsAttachedTo(this)) { 2363 for (int i = 0; i < tab_count(); ++i) 2364 tab_at(i)->set_dragging(false); 2365 } 2366} 2367 2368void TabStrip::GenerateIdealBounds() { 2369 int new_tab_y = 0; 2370 2371 if (touch_layout_.get()) { 2372 if (tabs_.view_size() == 0) 2373 return; 2374 2375 int new_tab_x = tabs_.ideal_bounds(tabs_.view_size() - 1).right() + 2376 kNewTabButtonHorizontalOffset; 2377 newtab_button_bounds_.set_origin(gfx::Point(new_tab_x, new_tab_y)); 2378 return; 2379 } 2380 2381 double unselected, selected; 2382 GetDesiredTabWidths(tab_count(), GetMiniTabCount(), &unselected, &selected); 2383 current_unselected_width_ = unselected; 2384 current_selected_width_ = selected; 2385 2386 // NOTE: This currently assumes a tab's height doesn't differ based on 2387 // selected state or the number of tabs in the strip! 2388 int tab_height = Tab::GetStandardSize().height(); 2389 int first_non_mini_index = 0; 2390 double tab_x = GenerateIdealBoundsForMiniTabs(&first_non_mini_index); 2391 for (int i = first_non_mini_index; i < tab_count(); ++i) { 2392 Tab* tab = tab_at(i); 2393 DCHECK(!tab->data().mini); 2394 double tab_width = tab->IsActive() ? selected : unselected; 2395 double end_of_tab = tab_x + tab_width; 2396 int rounded_tab_x = Round(tab_x); 2397 set_ideal_bounds( 2398 i, 2399 gfx::Rect(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, 2400 tab_height)); 2401 tab_x = end_of_tab + kTabHorizontalOffset; 2402 } 2403 2404 // Update bounds of new tab button. 2405 int new_tab_x; 2406 if (abs(Round(unselected) - Tab::GetStandardSize().width()) > 1 && 2407 !in_tab_close_) { 2408 // We're shrinking tabs, so we need to anchor the New Tab button to the 2409 // right edge of the TabStrip's bounds, rather than the right edge of the 2410 // right-most Tab, otherwise it'll bounce when animating. 2411 new_tab_x = width() - newtab_button_bounds_.width(); 2412 } else { 2413 new_tab_x = Round(tab_x - kTabHorizontalOffset) + 2414 kNewTabButtonHorizontalOffset; 2415 } 2416 newtab_button_bounds_.set_origin(gfx::Point(new_tab_x, new_tab_y)); 2417} 2418 2419int TabStrip::GenerateIdealBoundsForMiniTabs(int* first_non_mini_index) { 2420 int next_x = 0; 2421 int mini_width = Tab::GetMiniWidth(); 2422 int tab_height = Tab::GetStandardSize().height(); 2423 int index = 0; 2424 for (; index < tab_count() && tab_at(index)->data().mini; ++index) { 2425 set_ideal_bounds(index, 2426 gfx::Rect(next_x, 0, mini_width, tab_height)); 2427 next_x += mini_width + kTabHorizontalOffset; 2428 } 2429 if (index > 0 && index < tab_count()) 2430 next_x += kMiniToNonMiniGap; 2431 if (first_non_mini_index) 2432 *first_non_mini_index = index; 2433 return next_x; 2434} 2435 2436// static 2437int TabStrip::new_tab_button_width() { 2438 return kNewTabButtonAssetWidth + kNewTabButtonHorizontalOffset; 2439} 2440 2441// static 2442int TabStrip::button_v_offset() { 2443 return kNewTabButtonVerticalOffset; 2444} 2445 2446int TabStrip::tab_area_width() const { 2447 return width() - new_tab_button_width(); 2448} 2449 2450void TabStrip::StartResizeLayoutAnimation() { 2451 PrepareForAnimation(); 2452 GenerateIdealBounds(); 2453 AnimateToIdealBounds(); 2454} 2455 2456void TabStrip::StartMiniTabAnimation() { 2457 in_tab_close_ = false; 2458 available_width_for_tabs_ = -1; 2459 2460 PrepareForAnimation(); 2461 2462 GenerateIdealBounds(); 2463 AnimateToIdealBounds(); 2464} 2465 2466void TabStrip::StartMouseInitiatedRemoveTabAnimation(int model_index) { 2467 // The user initiated the close. We want to persist the bounds of all the 2468 // existing tabs, so we manually shift ideal_bounds then animate. 2469 Tab* tab_closing = tab_at(model_index); 2470 int delta = tab_closing->width() + kTabHorizontalOffset; 2471 // If the tab being closed is a mini-tab next to a non-mini-tab, be sure to 2472 // add the extra padding. 2473 DCHECK_NE(model_index + 1, tab_count()); 2474 if (tab_closing->data().mini && model_index + 1 < tab_count() && 2475 !tab_at(model_index + 1)->data().mini) { 2476 delta += kMiniToNonMiniGap; 2477 } 2478 2479 for (int i = model_index + 1; i < tab_count(); ++i) { 2480 gfx::Rect bounds = ideal_bounds(i); 2481 bounds.set_x(bounds.x() - delta); 2482 set_ideal_bounds(i, bounds); 2483 } 2484 2485 newtab_button_bounds_.set_x(newtab_button_bounds_.x() - delta); 2486 2487 PrepareForAnimation(); 2488 2489 tab_closing->set_closing(true); 2490 2491 // We still need to paint the tab until we actually remove it. Put it in 2492 // tabs_closing_map_ so we can find it. 2493 RemoveTabFromViewModel(model_index); 2494 2495 AnimateToIdealBounds(); 2496 2497 gfx::Rect tab_bounds = tab_closing->bounds(); 2498 tab_bounds.set_width(0); 2499 bounds_animator_.AnimateViewTo(tab_closing, tab_bounds); 2500 2501 // Register delegate to do cleanup when done, BoundsAnimator takes 2502 // ownership of RemoveTabDelegate. 2503 bounds_animator_.SetAnimationDelegate( 2504 tab_closing, 2505 new RemoveTabDelegate(this, tab_closing), 2506 true); 2507} 2508 2509bool TabStrip::IsPointInTab(Tab* tab, 2510 const gfx::Point& point_in_tabstrip_coords) { 2511 gfx::Point point_in_tab_coords(point_in_tabstrip_coords); 2512 View::ConvertPointToTarget(this, tab, &point_in_tab_coords); 2513 return tab->HitTestPoint(point_in_tab_coords); 2514} 2515 2516int TabStrip::GetStartXForNormalTabs() const { 2517 int mini_tab_count = GetMiniTabCount(); 2518 if (mini_tab_count == 0) 2519 return 0; 2520 return mini_tab_count * (Tab::GetMiniWidth() + kTabHorizontalOffset) + 2521 kMiniToNonMiniGap; 2522} 2523 2524Tab* TabStrip::FindTabForEvent(const gfx::Point& point) { 2525 if (touch_layout_.get()) { 2526 int active_tab_index = touch_layout_->active_index(); 2527 if (active_tab_index != -1) { 2528 Tab* tab = FindTabForEventFrom(point, active_tab_index, -1); 2529 if (!tab) 2530 tab = FindTabForEventFrom(point, active_tab_index + 1, 1); 2531 return tab; 2532 } else if (tab_count()) { 2533 return FindTabForEventFrom(point, 0, 1); 2534 } 2535 } else { 2536 for (int i = 0; i < tab_count(); ++i) { 2537 if (IsPointInTab(tab_at(i), point)) 2538 return tab_at(i); 2539 } 2540 } 2541 return NULL; 2542} 2543 2544Tab* TabStrip::FindTabForEventFrom(const gfx::Point& point, 2545 int start, 2546 int delta) { 2547 // |start| equals tab_count() when there are only pinned tabs. 2548 if (start == tab_count()) 2549 start += delta; 2550 for (int i = start; i >= 0 && i < tab_count(); i += delta) { 2551 if (IsPointInTab(tab_at(i), point)) 2552 return tab_at(i); 2553 } 2554 return NULL; 2555} 2556 2557views::View* TabStrip::FindTabHitByPoint(const gfx::Point& point) { 2558 // The display order doesn't necessarily match the child list order, so we 2559 // walk the display list hit-testing Tabs. Since the active tab always 2560 // renders on top of adjacent tabs, it needs to be hit-tested before any 2561 // left-adjacent Tab, so we look ahead for it as we walk. 2562 for (int i = 0; i < tab_count(); ++i) { 2563 Tab* next_tab = i < (tab_count() - 1) ? tab_at(i + 1) : NULL; 2564 if (next_tab && next_tab->IsActive() && IsPointInTab(next_tab, point)) 2565 return next_tab; 2566 if (IsPointInTab(tab_at(i), point)) 2567 return tab_at(i); 2568 } 2569 2570 return NULL; 2571} 2572 2573std::vector<int> TabStrip::GetTabXCoordinates() { 2574 std::vector<int> results; 2575 for (int i = 0; i < tab_count(); ++i) 2576 results.push_back(ideal_bounds(i).x()); 2577 return results; 2578} 2579 2580void TabStrip::SwapLayoutIfNecessary() { 2581 bool needs_touch = NeedsTouchLayout(); 2582 bool using_touch = touch_layout_.get() != NULL; 2583 if (needs_touch == using_touch) 2584 return; 2585 2586 if (needs_touch) { 2587 gfx::Size tab_size(Tab::GetMinimumSelectedSize()); 2588 tab_size.set_width(Tab::GetTouchWidth()); 2589 touch_layout_.reset(new StackedTabStripLayout( 2590 tab_size, 2591 kTabHorizontalOffset, 2592 kStackedPadding, 2593 kMaxStackedCount, 2594 &tabs_)); 2595 touch_layout_->SetWidth(width() - new_tab_button_width()); 2596 // This has to be after SetWidth() as SetWidth() is going to reset the 2597 // bounds of the mini-tabs (since StackedTabStripLayout doesn't yet know how 2598 // many mini-tabs there are). 2599 GenerateIdealBoundsForMiniTabs(NULL); 2600 touch_layout_->SetXAndMiniCount(GetStartXForNormalTabs(), 2601 GetMiniTabCount()); 2602 touch_layout_->SetActiveIndex(controller_->GetActiveIndex()); 2603 } else { 2604 touch_layout_.reset(); 2605 } 2606 PrepareForAnimation(); 2607 GenerateIdealBounds(); 2608 AnimateToIdealBounds(); 2609} 2610 2611bool TabStrip::NeedsTouchLayout() const { 2612 if (!stacked_layout_) 2613 return false; 2614 2615 int mini_tab_count = GetMiniTabCount(); 2616 int normal_count = tab_count() - mini_tab_count; 2617 if (normal_count <= 1 || normal_count == mini_tab_count) 2618 return false; 2619 int x = GetStartXForNormalTabs(); 2620 int available_width = width() - x - new_tab_button_width(); 2621 return (Tab::GetTouchWidth() * normal_count + 2622 kTabHorizontalOffset * (normal_count - 1)) > available_width; 2623} 2624 2625void TabStrip::SetResetToShrinkOnExit(bool value) { 2626 if (!adjust_layout_) 2627 return; 2628 2629 if (value && !stacked_layout_) 2630 value = false; // We're already using shrink (not stacked) layout. 2631 2632 if (value == reset_to_shrink_on_exit_) 2633 return; 2634 2635 reset_to_shrink_on_exit_ = value; 2636 // Add an observer so we know when the mouse moves out of the tabstrip. 2637 if (reset_to_shrink_on_exit_) 2638 AddMessageLoopObserver(); 2639 else 2640 RemoveMessageLoopObserver(); 2641} 2642