// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/views/tabs/tab_strip.h" #if defined(OS_WIN) #include #endif #include #include #include #include #include "base/compiler_specific.h" #include "base/metrics/histogram.h" #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/defaults.h" #include "chrome/browser/ui/host_desktop.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "chrome/browser/ui/view_ids.h" #include "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h" #include "chrome/browser/ui/views/tabs/tab.h" #include "chrome/browser/ui/views/tabs/tab_drag_controller.h" #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" #include "chrome/browser/ui/views/tabs/tab_strip_observer.h" #include "chrome/browser/ui/views/touch_uma/touch_uma.h" #include "content/public/browser/user_metrics.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "ui/accessibility/ax_view_state.h" #include "ui/base/default_theme_provider.h" #include "ui/base/dragdrop/drag_drop_types.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/models/list_selection_model.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/animation/animation_container.h" #include "ui/gfx/animation/throb_animation.h" #include "ui/gfx/canvas.h" #include "ui/gfx/display.h" #include "ui/gfx/image/image_skia.h" #include "ui/gfx/image/image_skia_operations.h" #include "ui/gfx/path.h" #include "ui/gfx/rect_conversions.h" #include "ui/gfx/screen.h" #include "ui/gfx/size.h" #include "ui/gfx/skia_util.h" #include "ui/views/controls/image_view.h" #include "ui/views/masked_targeter_delegate.h" #include "ui/views/mouse_watcher_view_host.h" #include "ui/views/rect_based_targeting_utils.h" #include "ui/views/view_model_utils.h" #include "ui/views/view_targeter.h" #include "ui/views/widget/root_view.h" #include "ui/views/widget/widget.h" #include "ui/views/window/non_client_view.h" #if defined(OS_WIN) #include "ui/gfx/win/hwnd_util.h" #include "ui/views/widget/monitor_win.h" #include "ui/views/win/hwnd_util.h" #endif using base::UserMetricsAction; using ui::DropTargetEvent; namespace { static const int kTabStripAnimationVSlop = 40; // Inactive tabs in a native frame are slightly transparent. static const int kGlassFrameInactiveTabAlpha = 200; // If there are multiple tabs selected then make non-selected inactive tabs // even more transparent. static const int kGlassFrameInactiveTabAlphaMultiSelection = 150; // Alpha applied to all elements save the selected tabs. static const int kInactiveTabAndNewTabButtonAlphaAsh = 230; static const int kInactiveTabAndNewTabButtonAlpha = 255; // Inverse ratio of the width of a tab edge to the width of the tab. When // hovering over the left or right edge of a tab, the drop indicator will // point between tabs. static const int kTabEdgeRatioInverse = 4; // Size of the drop indicator. static int drop_indicator_width; static int drop_indicator_height; static inline int Round(double x) { // Why oh why is this not in a standard header? return static_cast(floor(x + 0.5)); } // Max number of stacked tabs. static const int kMaxStackedCount = 4; // Padding between stacked tabs. static const int kStackedPadding = 6; // See UpdateLayoutTypeFromMouseEvent() for a description of these. #if !defined(USE_ASH) const int kMouseMoveTimeMS = 200; const int kMouseMoveCountBeforeConsiderReal = 3; #endif // Amount of time we delay before resizing after a close from a touch. const int kTouchResizeLayoutTimeMS = 2000; // Amount the left edge of a tab is offset from the rectangle of the tab's // favicon/title/close box. Related to the width of IDR_TAB_ACTIVE_LEFT. // Affects the size of the "V" between adjacent tabs. const int kTabHorizontalOffset = -26; // Amount to adjust the clip by when the tab is stacked before the active index. const int kStackedTabLeftClip = 20; // Amount to adjust the clip by when the tab is stacked after the active index. const int kStackedTabRightClip = 20; base::string16 GetClipboardText() { if (!ui::Clipboard::IsSupportedClipboardType(ui::CLIPBOARD_TYPE_SELECTION)) return base::string16(); ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); CHECK(clipboard); base::string16 clipboard_text; clipboard->ReadText(ui::CLIPBOARD_TYPE_SELECTION, &clipboard_text); return clipboard_text; } // Animation delegate used for any automatic tab movement. Hides the tab if it // is not fully visible within the tabstrip area, to prevent overflow clipping. class TabAnimationDelegate : public gfx::AnimationDelegate { public: TabAnimationDelegate(TabStrip* tab_strip, Tab* tab); virtual ~TabAnimationDelegate(); virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE; protected: TabStrip* tab_strip() { return tab_strip_; } Tab* tab() { return tab_; } private: TabStrip* const tab_strip_; Tab* const tab_; DISALLOW_COPY_AND_ASSIGN(TabAnimationDelegate); }; TabAnimationDelegate::TabAnimationDelegate(TabStrip* tab_strip, Tab* tab) : tab_strip_(tab_strip), tab_(tab) { } TabAnimationDelegate::~TabAnimationDelegate() { } void TabAnimationDelegate::AnimationProgressed( const gfx::Animation* animation) { tab_->SetVisible(tab_strip_->ShouldTabBeVisible(tab_)); } // Animation delegate used when a dragged tab is released. When done sets the // dragging state to false. class ResetDraggingStateDelegate : public TabAnimationDelegate { public: ResetDraggingStateDelegate(TabStrip* tab_strip, Tab* tab); virtual ~ResetDraggingStateDelegate(); virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE; private: DISALLOW_COPY_AND_ASSIGN(ResetDraggingStateDelegate); }; ResetDraggingStateDelegate::ResetDraggingStateDelegate(TabStrip* tab_strip, Tab* tab) : TabAnimationDelegate(tab_strip, tab) { } ResetDraggingStateDelegate::~ResetDraggingStateDelegate() { } void ResetDraggingStateDelegate::AnimationEnded( const gfx::Animation* animation) { tab()->set_dragging(false); AnimationProgressed(animation); // Forces tab visibility to update. } void ResetDraggingStateDelegate::AnimationCanceled( const gfx::Animation* animation) { AnimationEnded(animation); } // If |dest| contains the point |point_in_source| the event handler from |dest| // is returned. Otherwise NULL is returned. views::View* ConvertPointToViewAndGetEventHandler( views::View* source, views::View* dest, const gfx::Point& point_in_source) { gfx::Point dest_point(point_in_source); views::View::ConvertPointToTarget(source, dest, &dest_point); return dest->HitTestPoint(dest_point) ? dest->GetEventHandlerForPoint(dest_point) : NULL; } // Gets a tooltip handler for |point_in_source| from |dest|. Note that |dest| // should return NULL if it does not contain the point. views::View* ConvertPointToViewAndGetTooltipHandler( views::View* source, views::View* dest, const gfx::Point& point_in_source) { gfx::Point dest_point(point_in_source); views::View::ConvertPointToTarget(source, dest, &dest_point); return dest->GetTooltipHandlerForPoint(dest_point); } TabDragController::EventSource EventSourceFromEvent( const ui::LocatedEvent& event) { return event.IsGestureEvent() ? TabDragController::EVENT_SOURCE_TOUCH : TabDragController::EVENT_SOURCE_MOUSE; } } // namespace /////////////////////////////////////////////////////////////////////////////// // NewTabButton // // A subclass of button that hit-tests to the shape of the new tab button and // does custom drawing. class NewTabButton : public views::ImageButton, public views::MaskedTargeterDelegate { public: NewTabButton(TabStrip* tab_strip, views::ButtonListener* listener); virtual ~NewTabButton(); // Set the background offset used to match the background image to the frame // image. void set_background_offset(const gfx::Point& offset) { background_offset_ = offset; } protected: // views::View: #if defined(OS_WIN) virtual void OnMouseReleased(const ui::MouseEvent& event) OVERRIDE; #endif virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; // ui::EventHandler: virtual void OnGestureEvent(ui::GestureEvent* event) OVERRIDE; private: // views::MaskedTargeterDelegate: virtual bool GetHitTestMask(gfx::Path* mask) const OVERRIDE; bool ShouldWindowContentsBeTransparent() const; gfx::ImageSkia GetBackgroundImage(views::CustomButton::ButtonState state, float scale) const; gfx::ImageSkia GetImageForState(views::CustomButton::ButtonState state, float scale) const; gfx::ImageSkia GetImageForScale(float scale) const; // Tab strip that contains this button. TabStrip* tab_strip_; // The offset used to paint the background image. gfx::Point background_offset_; // were we destroyed? bool* destroyed_; DISALLOW_COPY_AND_ASSIGN(NewTabButton); }; NewTabButton::NewTabButton(TabStrip* tab_strip, views::ButtonListener* listener) : views::ImageButton(listener), tab_strip_(tab_strip), destroyed_(NULL) { #if defined(OS_LINUX) && !defined(OS_CHROMEOS) set_triggerable_event_flags(triggerable_event_flags() | ui::EF_MIDDLE_MOUSE_BUTTON); #endif } NewTabButton::~NewTabButton() { if (destroyed_) *destroyed_ = true; } #if defined(OS_WIN) void NewTabButton::OnMouseReleased(const ui::MouseEvent& event) { if (event.IsOnlyRightMouseButton()) { gfx::Point point = event.location(); views::View::ConvertPointToScreen(this, &point); bool destroyed = false; destroyed_ = &destroyed; gfx::ShowSystemMenuAtPoint(views::HWNDForView(this), point); if (destroyed) return; destroyed_ = NULL; SetState(views::CustomButton::STATE_NORMAL); return; } views::ImageButton::OnMouseReleased(event); } #endif void NewTabButton::OnPaint(gfx::Canvas* canvas) { gfx::ImageSkia image = GetImageForScale(canvas->image_scale()); canvas->DrawImageInt(image, 0, height() - image.height()); } void NewTabButton::OnGestureEvent(ui::GestureEvent* event) { // Consume all gesture events here so that the parent (Tab) does not // start consuming gestures. views::ImageButton::OnGestureEvent(event); event->SetHandled(); } bool NewTabButton::GetHitTestMask(gfx::Path* mask) const { DCHECK(mask); // When the button is sized to the top of the tab strip, we want the hit // test mask to be defined as the complete (rectangular) bounds of the // button. if (tab_strip_->SizeTabButtonToTopOfTabStrip()) { gfx::Rect button_bounds(GetContentsBounds()); button_bounds.set_x(GetMirroredXForRect(button_bounds)); mask->addRect(RectToSkRect(button_bounds)); return true; } SkScalar w = SkIntToScalar(width()); SkScalar v_offset = SkIntToScalar(TabStrip::kNewTabButtonVerticalOffset); // These values are defined by the shape of the new tab image. Should that // image ever change, these values will need to be updated. They're so // custom it's not really worth defining constants for. // These values are correct for regular and USE_ASH versions of the image. mask->moveTo(0, v_offset + 1); mask->lineTo(w - 7, v_offset + 1); mask->lineTo(w - 4, v_offset + 4); mask->lineTo(w, v_offset + 16); mask->lineTo(w - 1, v_offset + 17); mask->lineTo(7, v_offset + 17); mask->lineTo(4, v_offset + 13); mask->lineTo(0, v_offset + 1); mask->close(); return true; } bool NewTabButton::ShouldWindowContentsBeTransparent() const { return GetWidget() && GetWidget()->GetTopLevelWidget()->ShouldWindowContentsBeTransparent(); } gfx::ImageSkia NewTabButton::GetBackgroundImage( views::CustomButton::ButtonState state, float scale) const { int background_id = 0; if (ShouldWindowContentsBeTransparent()) { background_id = IDR_THEME_TAB_BACKGROUND_V; } else if (tab_strip_->controller()->IsIncognito()) { background_id = IDR_THEME_TAB_BACKGROUND_INCOGNITO; } else { background_id = IDR_THEME_TAB_BACKGROUND; } int alpha = 0; switch (state) { case views::CustomButton::STATE_NORMAL: case views::CustomButton::STATE_HOVERED: alpha = ShouldWindowContentsBeTransparent() ? kGlassFrameInactiveTabAlpha : 255; break; case views::CustomButton::STATE_PRESSED: alpha = 145; break; default: NOTREACHED(); break; } gfx::ImageSkia* mask = GetThemeProvider()->GetImageSkiaNamed(IDR_NEWTAB_BUTTON_MASK); int height = mask->height(); int width = mask->width(); // The canvas and mask has to use the same scale factor. if (!mask->HasRepresentation(scale)) scale = ui::GetScaleForScaleFactor(ui::SCALE_FACTOR_100P); gfx::Canvas canvas(gfx::Size(width, height), scale, false); // For custom images the background starts at the top of the tab strip. // Otherwise the background starts at the top of the frame. gfx::ImageSkia* background = GetThemeProvider()->GetImageSkiaNamed(background_id); int offset_y = GetThemeProvider()->HasCustomImage(background_id) ? 0 : background_offset_.y(); // The new tab background is mirrored in RTL mode, but the theme background // should never be mirrored. Mirror it here to compensate. float x_scale = 1.0f; int x = GetMirroredX() + background_offset_.x(); if (base::i18n::IsRTL()) { x_scale = -1.0f; // Offset by |width| such that the same region is painted as if there was no // flip. x += width; } canvas.TileImageInt(*background, x, TabStrip::kNewTabButtonVerticalOffset + offset_y, x_scale, 1.0f, 0, 0, width, height); if (alpha != 255) { SkPaint paint; paint.setColor(SkColorSetARGB(alpha, 255, 255, 255)); paint.setXfermodeMode(SkXfermode::kDstIn_Mode); paint.setStyle(SkPaint::kFill_Style); canvas.DrawRect(gfx::Rect(0, 0, width, height), paint); } // White highlight on hover. if (state == views::CustomButton::STATE_HOVERED) canvas.FillRect(GetLocalBounds(), SkColorSetARGB(64, 255, 255, 255)); return gfx::ImageSkiaOperations::CreateMaskedImage( gfx::ImageSkia(canvas.ExtractImageRep()), *mask); } gfx::ImageSkia NewTabButton::GetImageForState( views::CustomButton::ButtonState state, float scale) const { const int overlay_id = state == views::CustomButton::STATE_PRESSED ? IDR_NEWTAB_BUTTON_P : IDR_NEWTAB_BUTTON; gfx::ImageSkia* overlay = GetThemeProvider()->GetImageSkiaNamed(overlay_id); gfx::Canvas canvas( gfx::Size(overlay->width(), overlay->height()), scale, false); canvas.DrawImageInt(GetBackgroundImage(state, scale), 0, 0); // Draw the button border with a slight alpha. const int kGlassFrameOverlayAlpha = 178; const int kOpaqueFrameOverlayAlpha = 230; uint8 alpha = ShouldWindowContentsBeTransparent() ? kGlassFrameOverlayAlpha : kOpaqueFrameOverlayAlpha; canvas.DrawImageInt(*overlay, 0, 0, alpha); return gfx::ImageSkia(canvas.ExtractImageRep()); } gfx::ImageSkia NewTabButton::GetImageForScale(float scale) const { if (!hover_animation_->is_animating()) return GetImageForState(state(), scale); return gfx::ImageSkiaOperations::CreateBlendedImage( GetImageForState(views::CustomButton::STATE_NORMAL, scale), GetImageForState(views::CustomButton::STATE_HOVERED, scale), hover_animation_->GetCurrentValue()); } /////////////////////////////////////////////////////////////////////////////// // TabStrip::RemoveTabDelegate // // AnimationDelegate used when removing a tab. Does the necessary cleanup when // done. class TabStrip::RemoveTabDelegate : public TabAnimationDelegate { public: RemoveTabDelegate(TabStrip* tab_strip, Tab* tab); virtual void AnimationEnded(const gfx::Animation* animation) OVERRIDE; virtual void AnimationCanceled(const gfx::Animation* animation) OVERRIDE; private: DISALLOW_COPY_AND_ASSIGN(RemoveTabDelegate); }; TabStrip::RemoveTabDelegate::RemoveTabDelegate(TabStrip* tab_strip, Tab* tab) : TabAnimationDelegate(tab_strip, tab) { } void TabStrip::RemoveTabDelegate::AnimationEnded( const gfx::Animation* animation) { DCHECK(tab()->closing()); tab_strip()->RemoveAndDeleteTab(tab()); // Send the Container a message to simulate a mouse moved event at the current // mouse position. This tickles the Tab the mouse is currently over to show // the "hot" state of the close button. Note that this is not required (and // indeed may crash!) for removes spawned by non-mouse closes and // drag-detaches. if (!tab_strip()->IsDragSessionActive() && tab_strip()->ShouldHighlightCloseButtonAfterRemove()) { // The widget can apparently be null during shutdown. views::Widget* widget = tab_strip()->GetWidget(); if (widget) widget->SynthesizeMouseMoveEvent(); } } void TabStrip::RemoveTabDelegate::AnimationCanceled( const gfx::Animation* animation) { AnimationEnded(animation); } /////////////////////////////////////////////////////////////////////////////// // TabStrip, public: // static const char TabStrip::kViewClassName[] = "TabStrip"; const int TabStrip::kNewTabButtonHorizontalOffset = -11; const int TabStrip::kNewTabButtonVerticalOffset = 7; const int TabStrip::kMiniToNonMiniGap = 3; const int TabStrip::kNewTabButtonAssetWidth = 34; const int TabStrip::kNewTabButtonAssetHeight = 18; TabStrip::TabStrip(TabStripController* controller) : controller_(controller), newtab_button_(NULL), current_unselected_width_(Tab::GetStandardSize().width()), current_selected_width_(Tab::GetStandardSize().width()), available_width_for_tabs_(-1), in_tab_close_(false), animation_container_(new gfx::AnimationContainer()), bounds_animator_(this), stacked_layout_(false), adjust_layout_(false), reset_to_shrink_on_exit_(false), mouse_move_count_(0), immersive_style_(false) { Init(); SetEventTargeter( scoped_ptr(new views::ViewTargeter(this))); } TabStrip::~TabStrip() { FOR_EACH_OBSERVER(TabStripObserver, observers_, TabStripDeleted(this)); // The animations may reference the tabs. Shut down the animation before we // delete the tabs. StopAnimating(false); DestroyDragController(); // Make sure we unhook ourselves as a message loop observer so that we don't // crash in the case where the user closes the window after closing a tab // but before moving the mouse. RemoveMessageLoopObserver(); // The children (tabs) may callback to us from their destructor. Delete them // so that if they call back we aren't in a weird state. RemoveAllChildViews(true); } void TabStrip::AddObserver(TabStripObserver* observer) { observers_.AddObserver(observer); } void TabStrip::RemoveObserver(TabStripObserver* observer) { observers_.RemoveObserver(observer); } void TabStrip::SetStackedLayout(bool stacked_layout) { if (stacked_layout == stacked_layout_) return; const int active_index = controller_->GetActiveIndex(); int active_center = 0; if (active_index != -1) { active_center = ideal_bounds(active_index).x() + ideal_bounds(active_index).width() / 2; } stacked_layout_ = stacked_layout; SetResetToShrinkOnExit(false); SwapLayoutIfNecessary(); // When transitioning to stacked try to keep the active tab centered. if (touch_layout_ && active_index != -1) { touch_layout_->SetActiveTabLocation( active_center - ideal_bounds(active_index).width() / 2); AnimateToIdealBounds(); } } gfx::Rect TabStrip::GetNewTabButtonBounds() { return newtab_button_->bounds(); } bool TabStrip::SizeTabButtonToTopOfTabStrip() { // Extend the button to the screen edge in maximized and immersive fullscreen. views::Widget* widget = GetWidget(); return browser_defaults::kSizeTabButtonToTopOfTabStrip || (widget && (widget->IsMaximized() || widget->IsFullscreen())); } void TabStrip::StartHighlight(int model_index) { tab_at(model_index)->StartPulse(); } void TabStrip::StopAllHighlighting() { for (int i = 0; i < tab_count(); ++i) tab_at(i)->StopPulse(); } void TabStrip::AddTabAt(int model_index, const TabRendererData& data, bool is_active) { // Stop dragging when a new tab is added and dragging a window. Doing // otherwise results in a confusing state if the user attempts to reattach. We // could allow this and make TabDragController update itself during the add, // but this comes up infrequently enough that it's not work the complexity. if (drag_controller_.get() && !drag_controller_->is_mutating() && drag_controller_->is_dragging_window()) { EndDrag(END_DRAG_COMPLETE); } Tab* tab = CreateTab(); tab->SetData(data); UpdateTabsClosingMap(model_index, 1); tabs_.Add(tab, model_index); AddChildView(tab); if (touch_layout_) { GenerateIdealBoundsForMiniTabs(NULL); int add_types = 0; if (data.mini) add_types |= StackedTabStripLayout::kAddTypeMini; if (is_active) add_types |= StackedTabStripLayout::kAddTypeActive; touch_layout_->AddTab(model_index, add_types, GetStartXForNormalTabs()); } // Don't animate the first tab, it looks weird, and don't animate anything // if the containing window isn't visible yet. if (tab_count() > 1 && GetWidget() && GetWidget()->IsVisible()) StartInsertTabAnimation(model_index); else DoLayout(); SwapLayoutIfNecessary(); FOR_EACH_OBSERVER(TabStripObserver, observers_, TabStripAddedTabAt(this, model_index)); } void TabStrip::MoveTab(int from_model_index, int to_model_index, const TabRendererData& data) { DCHECK_GT(tabs_.view_size(), 0); const Tab* last_tab = GetLastVisibleTab(); tab_at(from_model_index)->SetData(data); if (touch_layout_) { tabs_.MoveViewOnly(from_model_index, to_model_index); int mini_count = 0; GenerateIdealBoundsForMiniTabs(&mini_count); touch_layout_->MoveTab( from_model_index, to_model_index, controller_->GetActiveIndex(), GetStartXForNormalTabs(), mini_count); } else { tabs_.Move(from_model_index, to_model_index); } StartMoveTabAnimation(); if (TabDragController::IsAttachedTo(this) && (last_tab != GetLastVisibleTab() || last_tab->dragging())) { newtab_button_->SetVisible(false); } SwapLayoutIfNecessary(); FOR_EACH_OBSERVER(TabStripObserver, observers_, TabStripMovedTab(this, from_model_index, to_model_index)); } void TabStrip::RemoveTabAt(int model_index) { if (touch_layout_) { Tab* tab = tab_at(model_index); tab->set_closing(true); int old_x = tabs_.ideal_bounds(model_index).x(); // We still need to paint the tab until we actually remove it. Put it in // tabs_closing_map_ so we can find it. RemoveTabFromViewModel(model_index); touch_layout_->RemoveTab(model_index, GenerateIdealBoundsForMiniTabs(NULL), old_x); ScheduleRemoveTabAnimation(tab); } else if (in_tab_close_ && model_index != GetModelCount()) { StartMouseInitiatedRemoveTabAnimation(model_index); } else { StartRemoveTabAnimation(model_index); } SwapLayoutIfNecessary(); FOR_EACH_OBSERVER(TabStripObserver, observers_, TabStripRemovedTabAt(this, model_index)); } void TabStrip::SetTabData(int model_index, const TabRendererData& data) { Tab* tab = tab_at(model_index); bool mini_state_changed = tab->data().mini != data.mini; tab->SetData(data); if (mini_state_changed) { if (touch_layout_) { int mini_tab_count = 0; int start_x = GenerateIdealBoundsForMiniTabs(&mini_tab_count); touch_layout_->SetXAndMiniCount(start_x, mini_tab_count); } if (GetWidget() && GetWidget()->IsVisible()) StartMiniTabAnimation(); else DoLayout(); } SwapLayoutIfNecessary(); } bool TabStrip::ShouldTabBeVisible(const Tab* tab) const { // Detached tabs should always be invisible (as they close). if (tab->detached()) return false; // When stacking tabs, all tabs should always be visible. if (stacked_layout_) return true; // If the tab is currently clipped, it shouldn't be visible. Note that we // allow dragged tabs to draw over the "New Tab button" region as well, // because either the New Tab button will be hidden, or the dragged tabs will // be animating back to their normal positions and we don't want to hide them // in the New Tab button region in case they re-appear after leaving it. // (This prevents flickeriness.) We never draw non-dragged tabs in New Tab // button area, even when the button is invisible, so that they don't appear // to "pop in" when the button disappears. // TODO: Probably doesn't work for RTL int right_edge = tab->bounds().right(); const int visible_width = tab->dragging() ? width() : tab_area_width(); if (right_edge > visible_width) return false; // Non-clipped dragging tabs should always be visible. if (tab->dragging()) return true; // Let all non-clipped closing tabs be visible. These will probably finish // closing before the user changes the active tab, so there's little reason to // try and make the more complex logic below apply. if (tab->closing()) return true; // Now we need to check whether the tab isn't currently clipped, but could // become clipped if we changed the active tab, widening either this tab or // the tabstrip portion before it. // Mini tabs don't change size when activated, so any tab in the mini tab // region is safe. if (tab->data().mini) return true; // If the active tab is on or before this tab, we're safe. if (controller_->GetActiveIndex() <= GetModelIndexOfTab(tab)) return true; // We need to check what would happen if the active tab were to move to this // tab or before. return (right_edge + current_selected_width_ - current_unselected_width_) <= tab_area_width(); } void TabStrip::PrepareForCloseAt(int model_index, CloseTabSource source) { if (!in_tab_close_ && IsAnimating()) { // Cancel any current animations. We do this as remove uses the current // ideal bounds and we need to know ideal bounds is in a good state. StopAnimating(true); } if (!GetWidget()) return; int model_count = GetModelCount(); if (model_count > 1 && model_index != model_count - 1) { // The user is about to close a tab other than the last tab. Set // available_width_for_tabs_ so that if we do a layout we don't position a // tab past the end of the second to last tab. We do this so that as the // user closes tabs with the mouse a tab continues to fall under the mouse. Tab* last_tab = tab_at(model_count - 1); Tab* tab_being_removed = tab_at(model_index); available_width_for_tabs_ = last_tab->x() + last_tab->width() - tab_being_removed->width() - kTabHorizontalOffset; if (model_index == 0 && tab_being_removed->data().mini && !tab_at(1)->data().mini) { available_width_for_tabs_ -= kMiniToNonMiniGap; } } in_tab_close_ = true; resize_layout_timer_.Stop(); if (source == CLOSE_TAB_FROM_TOUCH) { StartResizeLayoutTabsFromTouchTimer(); } else { AddMessageLoopObserver(); } } void TabStrip::SetSelection(const ui::ListSelectionModel& old_selection, const ui::ListSelectionModel& new_selection) { if (touch_layout_) { touch_layout_->SetActiveIndex(new_selection.active()); // Only start an animation if we need to. Otherwise clicking on an // unselected tab and dragging won't work because dragging is only allowed // if not animating. if (!views::ViewModelUtils::IsAtIdealBounds(tabs_)) AnimateToIdealBounds(); SchedulePaint(); } else { // We have "tiny tabs" if the tabs are so tiny that the unselected ones are // a different size to the selected ones. bool tiny_tabs = current_unselected_width_ != current_selected_width_; if (!IsAnimating() && (!in_tab_close_ || tiny_tabs)) { DoLayout(); } else { SchedulePaint(); } } // Use STLSetDifference to get the indices of elements newly selected // and no longer selected, since selected_indices() is always sorted. ui::ListSelectionModel::SelectedIndices no_longer_selected = base::STLSetDifference( old_selection.selected_indices(), new_selection.selected_indices()); ui::ListSelectionModel::SelectedIndices newly_selected = base::STLSetDifference( new_selection.selected_indices(), old_selection.selected_indices()); // Fire accessibility events that reflect the changes to selection, and // stop the mini tab title animation on tabs no longer selected. for (size_t i = 0; i < no_longer_selected.size(); ++i) { tab_at(no_longer_selected[i])->StopMiniTabTitleAnimation(); tab_at(no_longer_selected[i])->NotifyAccessibilityEvent( ui::AX_EVENT_SELECTION_REMOVE, true); } for (size_t i = 0; i < newly_selected.size(); ++i) { tab_at(newly_selected[i])->NotifyAccessibilityEvent( ui::AX_EVENT_SELECTION_ADD, true); } tab_at(new_selection.active())->NotifyAccessibilityEvent( ui::AX_EVENT_SELECTION, true); } void TabStrip::TabTitleChangedNotLoading(int model_index) { Tab* tab = tab_at(model_index); if (tab->data().mini && !tab->IsActive()) tab->StartMiniTabTitleAnimation(); } int TabStrip::GetModelIndexOfTab(const Tab* tab) const { return tabs_.GetIndexOfView(tab); } int TabStrip::GetModelCount() const { return controller_->GetCount(); } bool TabStrip::IsValidModelIndex(int model_index) const { return controller_->IsValidIndex(model_index); } bool TabStrip::IsDragSessionActive() const { return drag_controller_.get() != NULL; } bool TabStrip::IsActiveDropTarget() const { for (int i = 0; i < tab_count(); ++i) { Tab* tab = tab_at(i); if (tab->dragging()) return true; } return false; } bool TabStrip::IsTabStripEditable() const { return !IsDragSessionActive() && !IsActiveDropTarget(); } bool TabStrip::IsTabStripCloseable() const { return !IsDragSessionActive(); } void TabStrip::UpdateLoadingAnimations() { controller_->UpdateLoadingAnimations(); } bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) { return IsRectInWindowCaption(gfx::Rect(point, gfx::Size(1, 1))); } bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { views::View* v = GetEventHandlerForRect(rect); // If there is no control at this location, claim the hit was in the title // bar to get a move action. if (v == this) return true; // Check to see if the rect intersects the non-button parts of the new tab // button. The button has a non-rectangular shape, so if it's not in the // visual portions of the button we treat it as a click to the caption. gfx::RectF rect_in_newtab_coords_f(rect); View::ConvertRectToTarget(this, newtab_button_, &rect_in_newtab_coords_f); gfx::Rect rect_in_newtab_coords = gfx::ToEnclosingRect( rect_in_newtab_coords_f); if (newtab_button_->GetLocalBounds().Intersects(rect_in_newtab_coords) && !newtab_button_->HitTestRect(rect_in_newtab_coords)) return true; // All other regions, including the new Tab button, should be considered part // of the containing Window's client area so that regular events can be // processed for them. return false; } void TabStrip::SetBackgroundOffset(const gfx::Point& offset) { for (int i = 0; i < tab_count(); ++i) tab_at(i)->set_background_offset(offset); newtab_button_->set_background_offset(offset); } void TabStrip::SetImmersiveStyle(bool enable) { if (immersive_style_ == enable) return; immersive_style_ = enable; } bool TabStrip::IsAnimating() const { return bounds_animator_.IsAnimating(); } void TabStrip::StopAnimating(bool layout) { if (!IsAnimating()) return; bounds_animator_.Cancel(); if (layout) DoLayout(); } void TabStrip::FileSupported(const GURL& url, bool supported) { if (drop_info_.get() && drop_info_->url == url) drop_info_->file_supported = supported; } const ui::ListSelectionModel& TabStrip::GetSelectionModel() { return controller_->GetSelectionModel(); } bool TabStrip::SupportsMultipleSelection() { // TODO: currently only allow single selection in touch layout mode. return touch_layout_ == NULL; } void TabStrip::SelectTab(Tab* tab) { int model_index = GetModelIndexOfTab(tab); if (IsValidModelIndex(model_index)) controller_->SelectTab(model_index); } void TabStrip::ExtendSelectionTo(Tab* tab) { int model_index = GetModelIndexOfTab(tab); if (IsValidModelIndex(model_index)) controller_->ExtendSelectionTo(model_index); } void TabStrip::ToggleSelected(Tab* tab) { int model_index = GetModelIndexOfTab(tab); if (IsValidModelIndex(model_index)) controller_->ToggleSelected(model_index); } void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { int model_index = GetModelIndexOfTab(tab); if (IsValidModelIndex(model_index)) controller_->AddSelectionFromAnchorTo(model_index); } void TabStrip::CloseTab(Tab* tab, CloseTabSource source) { if (tab->closing()) { // If the tab is already closing, close the next tab. We do this so that the // user can rapidly close tabs by clicking the close button and not have // the animations interfere with that. const int closed_tab_index = FindClosingTab(tab).first->first; if (closed_tab_index < GetModelCount()) controller_->CloseTab(closed_tab_index, source); return; } int model_index = GetModelIndexOfTab(tab); if (IsValidModelIndex(model_index)) controller_->CloseTab(model_index, source); } void TabStrip::ShowContextMenuForTab(Tab* tab, const gfx::Point& p, ui::MenuSourceType source_type) { controller_->ShowContextMenuForTab(tab, p, source_type); } bool TabStrip::IsActiveTab(const Tab* tab) const { int model_index = GetModelIndexOfTab(tab); return IsValidModelIndex(model_index) && controller_->IsActiveTab(model_index); } bool TabStrip::IsTabSelected(const Tab* tab) const { int model_index = GetModelIndexOfTab(tab); return IsValidModelIndex(model_index) && controller_->IsTabSelected(model_index); } bool TabStrip::IsTabPinned(const Tab* tab) const { if (tab->closing()) return false; int model_index = GetModelIndexOfTab(tab); return IsValidModelIndex(model_index) && controller_->IsTabPinned(model_index); } void TabStrip::MaybeStartDrag( Tab* tab, const ui::LocatedEvent& event, const ui::ListSelectionModel& original_selection) { // Don't accidentally start any drag operations during animations if the // mouse is down... during an animation tabs are being resized automatically, // so the View system can misinterpret this easily if the mouse is down that // the user is dragging. if (IsAnimating() || tab->closing() || controller_->HasAvailableDragActions() == 0) { return; } // Do not do any dragging of tabs when using the super short immersive style. if (IsImmersiveStyle()) return; int model_index = GetModelIndexOfTab(tab); if (!IsValidModelIndex(model_index)) { CHECK(false); return; } Tabs tabs; int size_to_selected = 0; int x = tab->GetMirroredXInView(event.x()); int y = event.y(); // Build the set of selected tabs to drag and calculate the offset from the // first selected tab. for (int i = 0; i < tab_count(); ++i) { Tab* other_tab = tab_at(i); if (IsTabSelected(other_tab)) { tabs.push_back(other_tab); if (other_tab == tab) { size_to_selected = GetSizeNeededForTabs(tabs); x = size_to_selected - tab->width() + x; } } } DCHECK(!tabs.empty()); DCHECK(std::find(tabs.begin(), tabs.end(), tab) != tabs.end()); ui::ListSelectionModel selection_model; if (!original_selection.IsSelected(model_index)) selection_model.Copy(original_selection); // Delete the existing DragController before creating a new one. We do this as // creating the DragController remembers the WebContents delegates and we need // to make sure the existing DragController isn't still a delegate. drag_controller_.reset(); TabDragController::MoveBehavior move_behavior = TabDragController::REORDER; // Use MOVE_VISIBILE_TABS in the following conditions: // . Mouse event generated from touch and the left button is down (the right // button corresponds to a long press, which we want to reorder). // . Gesture tap down and control key isn't down. // . Real mouse event and control is down. This is mostly for testing. DCHECK(event.type() == ui::ET_MOUSE_PRESSED || event.type() == ui::ET_GESTURE_TAP_DOWN); if (touch_layout_ && ((event.type() == ui::ET_MOUSE_PRESSED && (((event.flags() & ui::EF_FROM_TOUCH) && static_cast(event).IsLeftMouseButton()) || (!(event.flags() & ui::EF_FROM_TOUCH) && static_cast(event).IsControlDown()))) || (event.type() == ui::ET_GESTURE_TAP_DOWN && !event.IsControlDown()))) { move_behavior = TabDragController::MOVE_VISIBILE_TABS; } drag_controller_.reset(new TabDragController); drag_controller_->Init( this, tab, tabs, gfx::Point(x, y), event.x(), selection_model, move_behavior, EventSourceFromEvent(event)); } void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) { if (drag_controller_.get() && drag_controller_->event_source() == EventSourceFromEvent(event)) { gfx::Point screen_location(event.location()); views::View::ConvertPointToScreen(view, &screen_location); drag_controller_->Drag(screen_location); } } bool TabStrip::EndDrag(EndDragReason reason) { if (!drag_controller_.get()) return false; bool started_drag = drag_controller_->started_drag(); drag_controller_->EndDrag(reason); return started_drag; } Tab* TabStrip::GetTabAt(Tab* tab, const gfx::Point& tab_in_tab_coordinates) { gfx::Point local_point = tab_in_tab_coordinates; ConvertPointToTarget(tab, this, &local_point); views::View* view = GetEventHandlerForPoint(local_point); if (!view) return NULL; // No tab contains the point. // Walk up the view hierarchy until we find a tab, or the TabStrip. while (view && view != this && view->id() != VIEW_ID_TAB) view = view->parent(); return view && view->id() == VIEW_ID_TAB ? static_cast(view) : NULL; } void TabStrip::OnMouseEventInTab(views::View* source, const ui::MouseEvent& event) { UpdateStackedLayoutFromMouseEvent(source, event); } bool TabStrip::ShouldPaintTab(const Tab* tab, gfx::Rect* clip) { // Only touch layout needs to restrict the clip. if (!touch_layout_ && !IsStackingDraggedTabs()) return true; int index = GetModelIndexOfTab(tab); if (index == -1) return true; // Tab is closing, paint it all. int active_index = IsStackingDraggedTabs() ? controller_->GetActiveIndex() : touch_layout_->active_index(); if (active_index == tab_count()) active_index--; if (index < active_index) { if (tab_at(index)->x() == tab_at(index + 1)->x()) return false; if (tab_at(index)->x() > tab_at(index + 1)->x()) return true; // Can happen during dragging. clip->SetRect( 0, 0, tab_at(index + 1)->x() - tab_at(index)->x() + kStackedTabLeftClip, tab_at(index)->height()); } else if (index > active_index && index > 0) { const gfx::Rect& tab_bounds(tab_at(index)->bounds()); const gfx::Rect& previous_tab_bounds(tab_at(index - 1)->bounds()); if (tab_bounds.x() == previous_tab_bounds.x()) return false; if (tab_bounds.x() < previous_tab_bounds.x()) return true; // Can happen during dragging. if (previous_tab_bounds.right() + kTabHorizontalOffset != tab_bounds.x()) { int x = previous_tab_bounds.right() - tab_bounds.x() - kStackedTabRightClip; clip->SetRect(x, 0, tab_bounds.width() - x, tab_bounds.height()); } } return true; } bool TabStrip::IsImmersiveStyle() const { return immersive_style_; } void TabStrip::UpdateTabAccessibilityState(const Tab* tab, ui::AXViewState* state) { state->count = tab_count(); state->index = GetModelIndexOfTab(tab); } void TabStrip::MouseMovedOutOfHost() { ResizeLayoutTabs(); if (reset_to_shrink_on_exit_) { reset_to_shrink_on_exit_ = false; SetStackedLayout(false); controller_->StackedLayoutMaybeChanged(); } } /////////////////////////////////////////////////////////////////////////////// // TabStrip, views::View overrides: void TabStrip::Layout() { // Only do a layout if our size changed. if (last_layout_size_ == size()) return; if (IsDragSessionActive()) return; DoLayout(); } void TabStrip::PaintChildren(gfx::Canvas* canvas, const views::CullSet& cull_set) { // The view order doesn't match the paint order (tabs_ contains the tab // ordering). Additionally we need to paint the tabs that are closing in // |tabs_closing_map_|. Tab* active_tab = NULL; Tabs tabs_dragging; Tabs selected_tabs; int selected_tab_count = 0; bool is_dragging = false; int active_tab_index = -1; const chrome::HostDesktopType host_desktop_type = chrome::GetHostDesktopTypeForNativeView(GetWidget()->GetNativeView()); const int inactive_tab_alpha = (host_desktop_type == chrome::HOST_DESKTOP_TYPE_ASH) ? kInactiveTabAndNewTabButtonAlphaAsh : kInactiveTabAndNewTabButtonAlpha; if (inactive_tab_alpha < 255) canvas->SaveLayerAlpha(inactive_tab_alpha); PaintClosingTabs(canvas, tab_count(), cull_set); for (int i = tab_count() - 1; i >= 0; --i) { Tab* tab = tab_at(i); if (tab->IsSelected()) selected_tab_count++; if (tab->dragging() && !stacked_layout_) { is_dragging = true; if (tab->IsActive()) { active_tab = tab; active_tab_index = i; } else { tabs_dragging.push_back(tab); } } else if (!tab->IsActive()) { if (!tab->IsSelected()) { if (!stacked_layout_) tab->Paint(canvas, cull_set); } else { selected_tabs.push_back(tab); } } else { active_tab = tab; active_tab_index = i; } PaintClosingTabs(canvas, i, cull_set); } // Draw from the left and then the right if we're in touch mode. if (stacked_layout_ && active_tab_index >= 0) { for (int i = 0; i < active_tab_index; ++i) { Tab* tab = tab_at(i); tab->Paint(canvas, cull_set); } for (int i = tab_count() - 1; i > active_tab_index; --i) { Tab* tab = tab_at(i); tab->Paint(canvas, cull_set); } } if (inactive_tab_alpha < 255) canvas->Restore(); if (GetWidget()->ShouldWindowContentsBeTransparent()) { // Make sure non-active tabs are somewhat transparent. SkPaint paint; // If there are multiple tabs selected, fade non-selected tabs more to make // the selected tabs more noticable. int alpha = selected_tab_count > 1 ? kGlassFrameInactiveTabAlphaMultiSelection : kGlassFrameInactiveTabAlpha; paint.setColor(SkColorSetARGB(alpha, 255, 255, 255)); paint.setXfermodeMode(SkXfermode::kDstIn_Mode); paint.setStyle(SkPaint::kFill_Style); // The tabstrip area overlaps the toolbar area by 2 px. canvas->DrawRect(gfx::Rect(0, 0, width(), height() - 2), paint); } // Now selected but not active. We don't want these dimmed if using native // frame, so they're painted after initial pass. for (size_t i = 0; i < selected_tabs.size(); ++i) selected_tabs[i]->Paint(canvas, cull_set); // Next comes the active tab. if (active_tab && !is_dragging) active_tab->Paint(canvas, cull_set); // Paint the New Tab button. if (inactive_tab_alpha < 255) canvas->SaveLayerAlpha(inactive_tab_alpha); newtab_button_->Paint(canvas, cull_set); if (inactive_tab_alpha < 255) canvas->Restore(); // And the dragged tabs. for (size_t i = 0; i < tabs_dragging.size(); ++i) tabs_dragging[i]->Paint(canvas, cull_set); // If the active tab is being dragged, it goes last. if (active_tab && is_dragging) active_tab->Paint(canvas, cull_set); } const char* TabStrip::GetClassName() const { return kViewClassName; } gfx::Size TabStrip::GetPreferredSize() const { int needed_tab_width; if (touch_layout_ || adjust_layout_) { // For stacked tabs the minimum size is calculated as the size needed to // handle showing any number of tabs. needed_tab_width = Tab::GetTouchWidth() + (2 * kStackedPadding * kMaxStackedCount); } else { // Otherwise the minimum width is based on the actual number of tabs. const int mini_tab_count = GetMiniTabCount(); needed_tab_width = mini_tab_count * Tab::GetMiniWidth(); const int remaining_tab_count = tab_count() - mini_tab_count; const int min_selected_width = Tab::GetMinimumSelectedSize().width(); const int min_unselected_width = Tab::GetMinimumUnselectedSize().width(); if (remaining_tab_count > 0) { needed_tab_width += kMiniToNonMiniGap + min_selected_width + ((remaining_tab_count - 1) * min_unselected_width); } if (tab_count() > 1) needed_tab_width += (tab_count() - 1) * kTabHorizontalOffset; // Don't let the tabstrip shrink smaller than is necessary to show one tab, // and don't force it to be larger than is necessary to show 20 tabs. const int largest_min_tab_width = min_selected_width + 19 * (min_unselected_width + kTabHorizontalOffset); needed_tab_width = std::min( std::max(needed_tab_width, min_selected_width), largest_min_tab_width); } return gfx::Size( needed_tab_width + new_tab_button_width(), immersive_style_ ? Tab::GetImmersiveHeight() : Tab::GetMinimumUnselectedSize().height()); } void TabStrip::OnDragEntered(const DropTargetEvent& event) { // Force animations to stop, otherwise it makes the index calculation tricky. StopAnimating(true); UpdateDropIndex(event); GURL url; base::string16 title; // Check whether the event data includes supported drop data. if (event.data().GetURLAndTitle( ui::OSExchangeData::CONVERT_FILENAMES, &url, &title) && url.is_valid()) { drop_info_->url = url; // For file:// URLs, kick off a MIME type request in case they're dropped. if (url.SchemeIsFile()) controller()->CheckFileSupported(url); } } int TabStrip::OnDragUpdated(const DropTargetEvent& event) { // Update the drop index even if the file is unsupported, to allow // dragging a file to the contents of another tab. UpdateDropIndex(event); if (!drop_info_->file_supported) return ui::DragDropTypes::DRAG_NONE; return GetDropEffect(event); } void TabStrip::OnDragExited() { SetDropIndex(-1, false); } int TabStrip::OnPerformDrop(const DropTargetEvent& event) { if (!drop_info_.get()) return ui::DragDropTypes::DRAG_NONE; const int drop_index = drop_info_->drop_index; const bool drop_before = drop_info_->drop_before; const bool file_supported = drop_info_->file_supported; // Hide the drop indicator. SetDropIndex(-1, false); // Do nothing if the file was unsupported or the URL is invalid. The URL may // have been changed after |drop_info_| was created. GURL url; base::string16 title; if (!file_supported || !event.data().GetURLAndTitle( ui::OSExchangeData::CONVERT_FILENAMES, &url, &title) || !url.is_valid()) return ui::DragDropTypes::DRAG_NONE; controller()->PerformDrop(drop_before, drop_index, url); return GetDropEffect(event); } void TabStrip::GetAccessibleState(ui::AXViewState* state) { state->role = ui::AX_ROLE_TAB_LIST; } views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) { if (!HitTestPoint(point)) return NULL; if (!touch_layout_) { // Return any view that isn't a Tab or this TabStrip immediately. We don't // want to interfere. views::View* v = View::GetTooltipHandlerForPoint(point); if (v && v != this && strcmp(v->GetClassName(), Tab::kViewClassName)) return v; views::View* tab = FindTabHitByPoint(point); if (tab) return tab; } else { if (newtab_button_->visible()) { views::View* view = ConvertPointToViewAndGetTooltipHandler(this, newtab_button_, point); if (view) return view; } Tab* tab = FindTabForEvent(point); if (tab) return ConvertPointToViewAndGetTooltipHandler(this, tab, point); } return this; } // static int TabStrip::GetImmersiveHeight() { return Tab::GetImmersiveHeight(); } /////////////////////////////////////////////////////////////////////////////// // TabStrip, private: void TabStrip::Init() { set_id(VIEW_ID_TAB_STRIP); // So we get enter/exit on children to switch stacked layout on and off. set_notify_enter_exit_on_child(true); newtab_button_bounds_.SetRect(0, 0, kNewTabButtonAssetWidth, kNewTabButtonAssetHeight + kNewTabButtonVerticalOffset); newtab_button_ = new NewTabButton(this, this); newtab_button_->SetTooltipText( l10n_util::GetStringUTF16(IDS_TOOLTIP_NEW_TAB)); newtab_button_->SetAccessibleName( l10n_util::GetStringUTF16(IDS_ACCNAME_NEWTAB)); newtab_button_->SetImageAlignment(views::ImageButton::ALIGN_LEFT, views::ImageButton::ALIGN_BOTTOM); newtab_button_->SetEventTargeter( scoped_ptr(new views::ViewTargeter(newtab_button_))); AddChildView(newtab_button_); if (drop_indicator_width == 0) { // Direction doesn't matter, both images are the same size. gfx::ImageSkia* drop_image = GetDropArrowImage(true); drop_indicator_width = drop_image->width(); drop_indicator_height = drop_image->height(); } } Tab* TabStrip::CreateTab() { Tab* tab = new Tab(this); tab->set_animation_container(animation_container_.get()); return tab; } void TabStrip::StartInsertTabAnimation(int model_index) { PrepareForAnimation(); // The TabStrip can now use its entire width to lay out Tabs. in_tab_close_ = false; available_width_for_tabs_ = -1; GenerateIdealBounds(); Tab* tab = tab_at(model_index); if (model_index == 0) { tab->SetBounds(0, ideal_bounds(model_index).y(), 0, ideal_bounds(model_index).height()); } else { Tab* last_tab = tab_at(model_index - 1); tab->SetBounds(last_tab->bounds().right() + kTabHorizontalOffset, ideal_bounds(model_index).y(), 0, ideal_bounds(model_index).height()); } AnimateToIdealBounds(); } void TabStrip::StartMoveTabAnimation() { PrepareForAnimation(); GenerateIdealBounds(); AnimateToIdealBounds(); } void TabStrip::StartRemoveTabAnimation(int model_index) { PrepareForAnimation(); // Mark the tab as closing. Tab* tab = tab_at(model_index); tab->set_closing(true); RemoveTabFromViewModel(model_index); ScheduleRemoveTabAnimation(tab); } void TabStrip::ScheduleRemoveTabAnimation(Tab* tab) { // Start an animation for the tabs. GenerateIdealBounds(); AnimateToIdealBounds(); // Animate the tab being closed to zero width. gfx::Rect tab_bounds = tab->bounds(); tab_bounds.set_width(0); bounds_animator_.AnimateViewTo(tab, tab_bounds); bounds_animator_.SetAnimationDelegate( tab, scoped_ptr(new RemoveTabDelegate(this, tab))); // Don't animate the new tab button when dragging tabs. Otherwise it looks // like the new tab button magically appears from beyond the end of the tab // strip. if (TabDragController::IsAttachedTo(this)) { bounds_animator_.StopAnimatingView(newtab_button_); newtab_button_->SetBoundsRect(newtab_button_bounds_); } } void TabStrip::AnimateToIdealBounds() { for (int i = 0; i < tab_count(); ++i) { Tab* tab = tab_at(i); if (!tab->dragging()) { bounds_animator_.AnimateViewTo(tab, ideal_bounds(i)); bounds_animator_.SetAnimationDelegate( tab, scoped_ptr( new TabAnimationDelegate(this, tab))); } } bounds_animator_.AnimateViewTo(newtab_button_, newtab_button_bounds_); } bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { return in_tab_close_; } void TabStrip::DoLayout() { last_layout_size_ = size(); StopAnimating(false); SwapLayoutIfNecessary(); if (touch_layout_) touch_layout_->SetWidth(tab_area_width()); GenerateIdealBounds(); views::ViewModelUtils::SetViewBoundsToIdealBounds(tabs_); SetTabVisibility(); SchedulePaint(); bounds_animator_.StopAnimatingView(newtab_button_); newtab_button_->SetBoundsRect(newtab_button_bounds_); } void TabStrip::SetTabVisibility() { // We could probably be more efficient here by making use of the fact that the // tabstrip will always have any visible tabs, and then any invisible tabs, so // we could e.g. binary-search for the changeover point. But since we have to // iterate through all the tabs to call SetVisible() anyway, it doesn't seem // worth it. for (int i = 0; i < tab_count(); ++i) { Tab* tab = tab_at(i); tab->SetVisible(ShouldTabBeVisible(tab)); } for (TabsClosingMap::const_iterator i(tabs_closing_map_.begin()); i != tabs_closing_map_.end(); ++i) { for (Tabs::const_iterator j(i->second.begin()); j != i->second.end(); ++j) { Tab* tab = *j; tab->SetVisible(ShouldTabBeVisible(tab)); } } } void TabStrip::DragActiveTab(const std::vector& initial_positions, int delta) { DCHECK_EQ(tab_count(), static_cast(initial_positions.size())); if (!touch_layout_) { StackDraggedTabs(delta); return; } SetIdealBoundsFromPositions(initial_positions); touch_layout_->DragActiveTab(delta); DoLayout(); } void TabStrip::SetIdealBoundsFromPositions(const std::vector& positions) { if (static_cast(tab_count()) != positions.size()) return; for (int i = 0; i < tab_count(); ++i) { gfx::Rect bounds(ideal_bounds(i)); bounds.set_x(positions[i]); tabs_.set_ideal_bounds(i, bounds); } } void TabStrip::StackDraggedTabs(int delta) { DCHECK(!touch_layout_); GenerateIdealBounds(); const int active_index = controller_->GetActiveIndex(); DCHECK_NE(-1, active_index); if (delta < 0) { // Drag the tabs to the left, stacking tabs before the active tab. const int adjusted_delta = std::min(ideal_bounds(active_index).x() - kStackedPadding * std::min(active_index, kMaxStackedCount), -delta); for (int i = 0; i <= active_index; ++i) { const int min_x = std::min(i, kMaxStackedCount) * kStackedPadding; gfx::Rect new_bounds(ideal_bounds(i)); new_bounds.set_x(std::max(min_x, new_bounds.x() - adjusted_delta)); tabs_.set_ideal_bounds(i, new_bounds); } const bool is_active_mini = tab_at(active_index)->data().mini; const int active_width = ideal_bounds(active_index).width(); for (int i = active_index + 1; i < tab_count(); ++i) { const int max_x = ideal_bounds(active_index).x() + (kStackedPadding * std::min(i - active_index, kMaxStackedCount)); gfx::Rect new_bounds(ideal_bounds(i)); int new_x = std::max(new_bounds.x() + delta, max_x); if (new_x == max_x && !tab_at(i)->data().mini && !is_active_mini && new_bounds.width() != active_width) new_x += (active_width - new_bounds.width()); new_bounds.set_x(new_x); tabs_.set_ideal_bounds(i, new_bounds); } } else { // Drag the tabs to the right, stacking tabs after the active tab. const int last_tab_width = ideal_bounds(tab_count() - 1).width(); const int last_tab_x = tab_area_width() - last_tab_width; if (active_index == tab_count() - 1 && ideal_bounds(tab_count() - 1).x() == last_tab_x) return; const int adjusted_delta = std::min(last_tab_x - kStackedPadding * std::min(tab_count() - active_index - 1, kMaxStackedCount) - ideal_bounds(active_index).x(), delta); for (int last_index = tab_count() - 1, i = last_index; i >= active_index; --i) { const int max_x = last_tab_x - std::min(tab_count() - i - 1, kMaxStackedCount) * kStackedPadding; gfx::Rect new_bounds(ideal_bounds(i)); int new_x = std::min(max_x, new_bounds.x() + adjusted_delta); // Because of rounding not all tabs are the same width. Adjust the // position to accommodate this, otherwise the stacking is off. if (new_x == max_x && !tab_at(i)->data().mini && new_bounds.width() != last_tab_width) new_x += (last_tab_width - new_bounds.width()); new_bounds.set_x(new_x); tabs_.set_ideal_bounds(i, new_bounds); } for (int i = active_index - 1; i >= 0; --i) { const int min_x = ideal_bounds(active_index).x() - std::min(active_index - i, kMaxStackedCount) * kStackedPadding; gfx::Rect new_bounds(ideal_bounds(i)); new_bounds.set_x(std::min(min_x, new_bounds.x() + delta)); tabs_.set_ideal_bounds(i, new_bounds); } if (ideal_bounds(tab_count() - 1).right() >= newtab_button_->x()) newtab_button_->SetVisible(false); } views::ViewModelUtils::SetViewBoundsToIdealBounds(tabs_); SchedulePaint(); } bool TabStrip::IsStackingDraggedTabs() const { return drag_controller_.get() && drag_controller_->started_drag() && (drag_controller_->move_behavior() == TabDragController::MOVE_VISIBILE_TABS); } void TabStrip::LayoutDraggedTabsAt(const Tabs& tabs, Tab* active_tab, const gfx::Point& location, bool initial_drag) { // Immediately hide the new tab button if the last tab is being dragged. const Tab* last_visible_tab = GetLastVisibleTab(); if (last_visible_tab && last_visible_tab->dragging()) newtab_button_->SetVisible(false); std::vector bounds; CalculateBoundsForDraggedTabs(tabs, &bounds); DCHECK_EQ(tabs.size(), bounds.size()); int active_tab_model_index = GetModelIndexOfTab(active_tab); int active_tab_index = static_cast( std::find(tabs.begin(), tabs.end(), active_tab) - tabs.begin()); for (size_t i = 0; i < tabs.size(); ++i) { Tab* tab = tabs[i]; gfx::Rect new_bounds = bounds[i]; new_bounds.Offset(location.x(), location.y()); int consecutive_index = active_tab_model_index - (active_tab_index - static_cast(i)); // If this is the initial layout during a drag and the tabs aren't // consecutive animate the view into position. Do the same if the tab is // already animating (which means we previously caused it to animate). if ((initial_drag && GetModelIndexOfTab(tabs[i]) != consecutive_index) || bounds_animator_.IsAnimating(tabs[i])) { bounds_animator_.SetTargetBounds(tabs[i], new_bounds); } else { tab->SetBoundsRect(new_bounds); } } SetTabVisibility(); } void TabStrip::CalculateBoundsForDraggedTabs(const Tabs& tabs, std::vector* bounds) { int x = 0; for (size_t i = 0; i < tabs.size(); ++i) { Tab* tab = tabs[i]; if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) x += kMiniToNonMiniGap; gfx::Rect new_bounds = tab->bounds(); new_bounds.set_origin(gfx::Point(x, 0)); bounds->push_back(new_bounds); x += tab->width() + kTabHorizontalOffset; } } int TabStrip::GetSizeNeededForTabs(const Tabs& tabs) { int width = 0; for (size_t i = 0; i < tabs.size(); ++i) { Tab* tab = tabs[i]; width += tab->width(); if (i > 0 && tab->data().mini != tabs[i - 1]->data().mini) width += kMiniToNonMiniGap; } if (tabs.size() > 0) width += kTabHorizontalOffset * static_cast(tabs.size() - 1); return width; } int TabStrip::GetMiniTabCount() const { int mini_count = 0; while (mini_count < tab_count() && tab_at(mini_count)->data().mini) mini_count++; return mini_count; } const Tab* TabStrip::GetLastVisibleTab() const { for (int i = tab_count() - 1; i >= 0; --i) { const Tab* tab = tab_at(i); if (tab->visible()) return tab; } // While in normal use the tabstrip should always be wide enough to have at // least one visible tab, it can be zero-width in tests, meaning we get here. return NULL; } void TabStrip::RemoveTabFromViewModel(int index) { // We still need to paint the tab until we actually remove it. Put it // in tabs_closing_map_ so we can find it. tabs_closing_map_[index].push_back(tab_at(index)); UpdateTabsClosingMap(index + 1, -1); tabs_.Remove(index); } void TabStrip::RemoveAndDeleteTab(Tab* tab) { scoped_ptr deleter(tab); FindClosingTabResult res(FindClosingTab(tab)); res.first->second.erase(res.second); if (res.first->second.empty()) tabs_closing_map_.erase(res.first); } void TabStrip::UpdateTabsClosingMap(int index, int delta) { if (tabs_closing_map_.empty()) return; if (delta == -1 && tabs_closing_map_.find(index - 1) != tabs_closing_map_.end() && tabs_closing_map_.find(index) != tabs_closing_map_.end()) { const Tabs& tabs(tabs_closing_map_[index]); tabs_closing_map_[index - 1].insert( tabs_closing_map_[index - 1].end(), tabs.begin(), tabs.end()); } TabsClosingMap updated_map; for (TabsClosingMap::iterator i(tabs_closing_map_.begin()); i != tabs_closing_map_.end(); ++i) { if (i->first > index) updated_map[i->first + delta] = i->second; else if (i->first < index) updated_map[i->first] = i->second; } if (delta > 0 && tabs_closing_map_.find(index) != tabs_closing_map_.end()) updated_map[index + delta] = tabs_closing_map_[index]; tabs_closing_map_.swap(updated_map); } void TabStrip::StartedDraggingTabs(const Tabs& tabs) { // Let the controller know that the user started dragging tabs. controller()->OnStartedDraggingTabs(); // Hide the new tab button immediately if we didn't originate the drag. if (!drag_controller_.get()) newtab_button_->SetVisible(false); PrepareForAnimation(); // Reset dragging state of existing tabs. for (int i = 0; i < tab_count(); ++i) tab_at(i)->set_dragging(false); for (size_t i = 0; i < tabs.size(); ++i) { tabs[i]->set_dragging(true); bounds_animator_.StopAnimatingView(tabs[i]); } // Move the dragged tabs to their ideal bounds. GenerateIdealBounds(); // Sets the bounds of the dragged tabs. for (size_t i = 0; i < tabs.size(); ++i) { int tab_data_index = GetModelIndexOfTab(tabs[i]); DCHECK_NE(-1, tab_data_index); tabs[i]->SetBoundsRect(ideal_bounds(tab_data_index)); } SetTabVisibility(); SchedulePaint(); } void TabStrip::DraggedTabsDetached() { // Let the controller know that the user is not dragging this tabstrip's tabs // anymore. controller()->OnStoppedDraggingTabs(); newtab_button_->SetVisible(true); } void TabStrip::StoppedDraggingTabs(const Tabs& tabs, const std::vector& initial_positions, bool move_only, bool completed) { // Let the controller know that the user stopped dragging tabs. controller()->OnStoppedDraggingTabs(); newtab_button_->SetVisible(true); if (move_only && touch_layout_) { if (completed) touch_layout_->SizeToFit(); else SetIdealBoundsFromPositions(initial_positions); } bool is_first_tab = true; for (size_t i = 0; i < tabs.size(); ++i) StoppedDraggingTab(tabs[i], &is_first_tab); } void TabStrip::StoppedDraggingTab(Tab* tab, bool* is_first_tab) { int tab_data_index = GetModelIndexOfTab(tab); if (tab_data_index == -1) { // The tab was removed before the drag completed. Don't do anything. return; } if (*is_first_tab) { *is_first_tab = false; PrepareForAnimation(); // Animate the view back to its correct position. GenerateIdealBounds(); AnimateToIdealBounds(); } bounds_animator_.AnimateViewTo(tab, ideal_bounds(tab_data_index)); // Install a delegate to reset the dragging state when done. We have to leave // dragging true for the tab otherwise it'll draw beneath the new tab button. bounds_animator_.SetAnimationDelegate( tab, scoped_ptr( new ResetDraggingStateDelegate(this, tab))); } void TabStrip::OwnDragController(TabDragController* controller) { // Typically, ReleaseDragController() and OwnDragController() calls are paired // via corresponding calls to TabDragController::Detach() and // TabDragController::Attach(). There is one exception to that rule: when a // drag might start, we create a TabDragController that is owned by the // potential source tabstrip in MaybeStartDrag(). If a drag actually starts, // we then call Attach() on the source tabstrip, but since the source tabstrip // already owns the TabDragController, so we don't need to do anything. if (controller != drag_controller_.get()) drag_controller_.reset(controller); } void TabStrip::DestroyDragController() { newtab_button_->SetVisible(true); drag_controller_.reset(); } TabDragController* TabStrip::ReleaseDragController() { return drag_controller_.release(); } TabStrip::FindClosingTabResult TabStrip::FindClosingTab(const Tab* tab) { DCHECK(tab->closing()); for (TabsClosingMap::iterator i(tabs_closing_map_.begin()); i != tabs_closing_map_.end(); ++i) { Tabs::iterator j = std::find(i->second.begin(), i->second.end(), tab); if (j != i->second.end()) return FindClosingTabResult(i, j); } NOTREACHED(); return FindClosingTabResult(tabs_closing_map_.end(), Tabs::iterator()); } void TabStrip::PaintClosingTabs(gfx::Canvas* canvas, int index, const views::CullSet& cull_set) { if (tabs_closing_map_.find(index) == tabs_closing_map_.end()) return; const Tabs& tabs = tabs_closing_map_[index]; for (Tabs::const_reverse_iterator i(tabs.rbegin()); i != tabs.rend(); ++i) (*i)->Paint(canvas, cull_set); } void TabStrip::UpdateStackedLayoutFromMouseEvent(views::View* source, const ui::MouseEvent& event) { if (!adjust_layout_) return; // The following code attempts to switch to shrink (not stacked) layout when // the mouse exits the tabstrip (or the mouse is pressed on a stacked tab) and // to stacked layout when a touch device is used. This is made problematic by // windows generating mouse move events that do not clearly indicate the move // is the result of a touch device. This assumes a real mouse is used if // |kMouseMoveCountBeforeConsiderReal| mouse move events are received within // the time window |kMouseMoveTimeMS|. At the time we get a mouse press we // know whether its from a touch device or not, but we don't layout then else // everything shifts. Instead we wait for the release. // // TODO(sky): revisit this when touch events are really plumbed through. switch (event.type()) { case ui::ET_MOUSE_PRESSED: mouse_move_count_ = 0; last_mouse_move_time_ = base::TimeTicks(); SetResetToShrinkOnExit((event.flags() & ui::EF_FROM_TOUCH) == 0); if (reset_to_shrink_on_exit_ && touch_layout_) { gfx::Point tab_strip_point(event.location()); views::View::ConvertPointToTarget(source, this, &tab_strip_point); Tab* tab = FindTabForEvent(tab_strip_point); if (tab && touch_layout_->IsStacked(GetModelIndexOfTab(tab))) { SetStackedLayout(false); controller_->StackedLayoutMaybeChanged(); } } break; case ui::ET_MOUSE_MOVED: { #if defined(USE_ASH) // Ash does not synthesize mouse events from touch events. SetResetToShrinkOnExit(true); #else gfx::Point location(event.location()); ConvertPointToTarget(source, this, &location); if (location == last_mouse_move_location_) return; // Ignore spurious moves. last_mouse_move_location_ = location; if ((event.flags() & ui::EF_FROM_TOUCH) == 0 && (event.flags() & ui::EF_IS_SYNTHESIZED) == 0) { if ((base::TimeTicks::Now() - last_mouse_move_time_).InMilliseconds() < kMouseMoveTimeMS) { if (mouse_move_count_++ == kMouseMoveCountBeforeConsiderReal) SetResetToShrinkOnExit(true); } else { mouse_move_count_ = 1; last_mouse_move_time_ = base::TimeTicks::Now(); } } else { last_mouse_move_time_ = base::TimeTicks(); } #endif break; } case ui::ET_MOUSE_RELEASED: { gfx::Point location(event.location()); ConvertPointToTarget(source, this, &location); last_mouse_move_location_ = location; mouse_move_count_ = 0; last_mouse_move_time_ = base::TimeTicks(); if ((event.flags() & ui::EF_FROM_TOUCH) == ui::EF_FROM_TOUCH) { SetStackedLayout(true); controller_->StackedLayoutMaybeChanged(); } break; } default: break; } } void TabStrip::GetCurrentTabWidths(double* unselected_width, double* selected_width) const { *unselected_width = current_unselected_width_; *selected_width = current_selected_width_; } void TabStrip::GetDesiredTabWidths(int tab_count, int mini_tab_count, double* unselected_width, double* selected_width) const { DCHECK(tab_count >= 0 && mini_tab_count >= 0 && mini_tab_count <= tab_count); const double min_unselected_width = Tab::GetMinimumUnselectedSize().width(); const double min_selected_width = Tab::GetMinimumSelectedSize().width(); *unselected_width = min_unselected_width; *selected_width = min_selected_width; if (tab_count == 0) { // Return immediately to avoid divide-by-zero below. return; } // Determine how much space we can actually allocate to tabs. int available_width = (available_width_for_tabs_ < 0) ? tab_area_width() : available_width_for_tabs_; if (mini_tab_count > 0) { available_width -= mini_tab_count * (Tab::GetMiniWidth() + kTabHorizontalOffset); tab_count -= mini_tab_count; if (tab_count == 0) { *selected_width = *unselected_width = Tab::GetStandardSize().width(); return; } // Account for gap between the last mini-tab and first non-mini-tab. available_width -= kMiniToNonMiniGap; } // Calculate the desired tab widths by dividing the available space into equal // portions. Don't let tabs get larger than the "standard width" or smaller // than the minimum width for each type, respectively. const int total_offset = kTabHorizontalOffset * (tab_count - 1); const double desired_tab_width = std::min((static_cast( available_width - total_offset) / static_cast(tab_count)), static_cast(Tab::GetStandardSize().width())); *unselected_width = std::max(desired_tab_width, min_unselected_width); *selected_width = std::max(desired_tab_width, min_selected_width); // When there are multiple tabs, we'll have one selected and some unselected // tabs. If the desired width was between the minimum sizes of these types, // try to shrink the tabs with the smaller minimum. For example, if we have // a strip of width 10 with 4 tabs, the desired width per tab will be 2.5. If // selected tabs have a minimum width of 4 and unselected tabs have a minimum // width of 1, the above code would set *unselected_width = 2.5, // *selected_width = 4, which results in a total width of 11.5. Instead, we // want to set *unselected_width = 2, *selected_width = 4, for a total width // of 10. if (tab_count > 1) { if (desired_tab_width < min_selected_width) { // Unselected width = (total width - selected width) / (num_tabs - 1) *unselected_width = std::max(static_cast( available_width - total_offset - min_selected_width) / static_cast(tab_count - 1), min_unselected_width); } } } void TabStrip::ResizeLayoutTabs() { // We've been called back after the TabStrip has been emptied out (probably // just prior to the window being destroyed). We need to do nothing here or // else GetTabAt below will crash. if (tab_count() == 0) return; // It is critically important that this is unhooked here, otherwise we will // keep spying on messages forever. RemoveMessageLoopObserver(); in_tab_close_ = false; available_width_for_tabs_ = -1; int mini_tab_count = GetMiniTabCount(); if (mini_tab_count == tab_count()) { // Only mini-tabs, we know the tab widths won't have changed (all // mini-tabs have the same width), so there is nothing to do. return; } // Don't try and avoid layout based on tab sizes. If tabs are small enough // then the width of the active tab may not change, but other widths may // have. This is particularly important if we've overflowed (all tabs are at // the min). StartResizeLayoutAnimation(); } void TabStrip::ResizeLayoutTabsFromTouch() { // Don't resize if the user is interacting with the tabstrip. if (!drag_controller_.get()) ResizeLayoutTabs(); else StartResizeLayoutTabsFromTouchTimer(); } void TabStrip::StartResizeLayoutTabsFromTouchTimer() { resize_layout_timer_.Stop(); resize_layout_timer_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds(kTouchResizeLayoutTimeMS), this, &TabStrip::ResizeLayoutTabsFromTouch); } void TabStrip::SetTabBoundsForDrag(const std::vector& tab_bounds) { StopAnimating(false); DCHECK_EQ(tab_count(), static_cast(tab_bounds.size())); for (int i = 0; i < tab_count(); ++i) tab_at(i)->SetBoundsRect(tab_bounds[i]); // Reset the layout size as we've effectively layed out a different size. // This ensures a layout happens after the drag is done. last_layout_size_ = gfx::Size(); } void TabStrip::AddMessageLoopObserver() { if (!mouse_watcher_.get()) { mouse_watcher_.reset( new views::MouseWatcher( new views::MouseWatcherViewHost( this, gfx::Insets(0, 0, kTabStripAnimationVSlop, 0)), this)); } mouse_watcher_->Start(); } void TabStrip::RemoveMessageLoopObserver() { mouse_watcher_.reset(NULL); } gfx::Rect TabStrip::GetDropBounds(int drop_index, bool drop_before, bool* is_beneath) { DCHECK_NE(drop_index, -1); int center_x; if (drop_index < tab_count()) { Tab* tab = tab_at(drop_index); if (drop_before) center_x = tab->x() - (kTabHorizontalOffset / 2); else center_x = tab->x() + (tab->width() / 2); } else { Tab* last_tab = tab_at(drop_index - 1); center_x = last_tab->x() + last_tab->width() + (kTabHorizontalOffset / 2); } // Mirror the center point if necessary. center_x = GetMirroredXInView(center_x); // Determine the screen bounds. gfx::Point drop_loc(center_x - drop_indicator_width / 2, -drop_indicator_height); ConvertPointToScreen(this, &drop_loc); gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), drop_indicator_width, drop_indicator_height); // If the rect doesn't fit on the monitor, push the arrow to the bottom. gfx::Screen* screen = gfx::Screen::GetScreenFor(GetWidget()->GetNativeView()); gfx::Display display = screen->GetDisplayMatching(drop_bounds); *is_beneath = !display.bounds().Contains(drop_bounds); if (*is_beneath) drop_bounds.Offset(0, drop_bounds.height() + height()); return drop_bounds; } void TabStrip::UpdateDropIndex(const DropTargetEvent& event) { // If the UI layout is right-to-left, we need to mirror the mouse // coordinates since we calculate the drop index based on the // original (and therefore non-mirrored) positions of the tabs. const int x = GetMirroredXInView(event.x()); // We don't allow replacing the urls of mini-tabs. for (int i = GetMiniTabCount(); i < tab_count(); ++i) { Tab* tab = tab_at(i); const int tab_max_x = tab->x() + tab->width(); const int hot_width = tab->width() / kTabEdgeRatioInverse; if (x < tab_max_x) { if (x < tab->x() + hot_width) SetDropIndex(i, true); else if (x >= tab_max_x - hot_width) SetDropIndex(i + 1, true); else SetDropIndex(i, false); return; } } // The drop isn't over a tab, add it to the end. SetDropIndex(tab_count(), true); } void TabStrip::SetDropIndex(int tab_data_index, bool drop_before) { // Let the controller know of the index update. controller()->OnDropIndexUpdate(tab_data_index, drop_before); if (tab_data_index == -1) { if (drop_info_.get()) drop_info_.reset(NULL); return; } if (drop_info_.get() && drop_info_->drop_index == tab_data_index && drop_info_->drop_before == drop_before) { return; } bool is_beneath; gfx::Rect drop_bounds = GetDropBounds(tab_data_index, drop_before, &is_beneath); if (!drop_info_.get()) { drop_info_.reset( new DropInfo(tab_data_index, drop_before, !is_beneath, GetWidget())); } else { drop_info_->drop_index = tab_data_index; drop_info_->drop_before = drop_before; if (is_beneath == drop_info_->point_down) { drop_info_->point_down = !is_beneath; drop_info_->arrow_view->SetImage( GetDropArrowImage(drop_info_->point_down)); } } // Reposition the window. Need to show it too as the window is initially // hidden. drop_info_->arrow_window->SetBounds(drop_bounds); drop_info_->arrow_window->Show(); } int TabStrip::GetDropEffect(const ui::DropTargetEvent& event) { const int source_ops = event.source_operations(); if (source_ops & ui::DragDropTypes::DRAG_COPY) return ui::DragDropTypes::DRAG_COPY; if (source_ops & ui::DragDropTypes::DRAG_LINK) return ui::DragDropTypes::DRAG_LINK; return ui::DragDropTypes::DRAG_MOVE; } // static gfx::ImageSkia* TabStrip::GetDropArrowImage(bool is_down) { return ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); } // TabStrip::DropInfo ---------------------------------------------------------- TabStrip::DropInfo::DropInfo(int drop_index, bool drop_before, bool point_down, views::Widget* context) : drop_index(drop_index), drop_before(drop_before), point_down(point_down), file_supported(true) { arrow_view = new views::ImageView; arrow_view->SetImage(GetDropArrowImage(point_down)); arrow_window = new views::Widget; views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); params.keep_on_top = true; params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; params.accept_events = false; params.bounds = gfx::Rect(drop_indicator_width, drop_indicator_height); params.context = context->GetNativeWindow(); arrow_window->Init(params); arrow_window->SetContentsView(arrow_view); } TabStrip::DropInfo::~DropInfo() { // Close eventually deletes the window, which deletes arrow_view too. arrow_window->Close(); } /////////////////////////////////////////////////////////////////////////////// void TabStrip::PrepareForAnimation() { if (!IsDragSessionActive() && !TabDragController::IsAttachedTo(this)) { for (int i = 0; i < tab_count(); ++i) tab_at(i)->set_dragging(false); } } void TabStrip::GenerateIdealBounds() { int new_tab_y = 0; if (touch_layout_) { if (tabs_.view_size() == 0) return; int new_tab_x = tabs_.ideal_bounds(tabs_.view_size() - 1).right() + kNewTabButtonHorizontalOffset; newtab_button_bounds_.set_origin(gfx::Point(new_tab_x, new_tab_y)); return; } GetDesiredTabWidths(tab_count(), GetMiniTabCount(), ¤t_unselected_width_, ¤t_selected_width_); // NOTE: This currently assumes a tab's height doesn't differ based on // selected state or the number of tabs in the strip! int tab_height = Tab::GetStandardSize().height(); int first_non_mini_index = 0; double tab_x = GenerateIdealBoundsForMiniTabs(&first_non_mini_index); for (int i = first_non_mini_index; i < tab_count(); ++i) { Tab* tab = tab_at(i); DCHECK(!tab->data().mini); double tab_width = tab->IsActive() ? current_selected_width_ : current_unselected_width_; double end_of_tab = tab_x + tab_width; int rounded_tab_x = Round(tab_x); tabs_.set_ideal_bounds( i, gfx::Rect(rounded_tab_x, 0, Round(end_of_tab) - rounded_tab_x, tab_height)); tab_x = end_of_tab + kTabHorizontalOffset; } // Update bounds of new tab button. int new_tab_x; if ((Tab::GetStandardSize().width() - Round(current_unselected_width_)) > 1 && !in_tab_close_) { // We're shrinking tabs, so we need to anchor the New Tab button to the // right edge of the TabStrip's bounds, rather than the right edge of the // right-most Tab, otherwise it'll bounce when animating. new_tab_x = width() - newtab_button_bounds_.width(); } else { new_tab_x = Round(tab_x - kTabHorizontalOffset) + kNewTabButtonHorizontalOffset; } newtab_button_bounds_.set_origin(gfx::Point(new_tab_x, new_tab_y)); } int TabStrip::GenerateIdealBoundsForMiniTabs(int* first_non_mini_index) { int next_x = 0; int mini_width = Tab::GetMiniWidth(); int tab_height = Tab::GetStandardSize().height(); int index = 0; for (; index < tab_count() && tab_at(index)->data().mini; ++index) { tabs_.set_ideal_bounds(index, gfx::Rect(next_x, 0, mini_width, tab_height)); next_x += mini_width + kTabHorizontalOffset; } if (index > 0 && index < tab_count()) next_x += kMiniToNonMiniGap; if (first_non_mini_index) *first_non_mini_index = index; return next_x; } void TabStrip::StartResizeLayoutAnimation() { PrepareForAnimation(); GenerateIdealBounds(); AnimateToIdealBounds(); } void TabStrip::StartMiniTabAnimation() { in_tab_close_ = false; available_width_for_tabs_ = -1; PrepareForAnimation(); GenerateIdealBounds(); AnimateToIdealBounds(); } void TabStrip::StartMouseInitiatedRemoveTabAnimation(int model_index) { // The user initiated the close. We want to persist the bounds of all the // existing tabs, so we manually shift ideal_bounds then animate. Tab* tab_closing = tab_at(model_index); int delta = tab_closing->width() + kTabHorizontalOffset; // If the tab being closed is a mini-tab next to a non-mini-tab, be sure to // add the extra padding. DCHECK_LT(model_index, tab_count() - 1); if (tab_closing->data().mini && !tab_at(model_index + 1)->data().mini) delta += kMiniToNonMiniGap; for (int i = model_index + 1; i < tab_count(); ++i) { gfx::Rect bounds = ideal_bounds(i); bounds.set_x(bounds.x() - delta); tabs_.set_ideal_bounds(i, bounds); } // Don't just subtract |delta| from the New Tab x-coordinate, as we might have // overflow tabs that will be able to animate into the strip, in which case // the new tab button should stay where it is. newtab_button_bounds_.set_x(std::min( width() - newtab_button_bounds_.width(), ideal_bounds(tab_count() - 1).right() + kNewTabButtonHorizontalOffset)); PrepareForAnimation(); tab_closing->set_closing(true); // We still need to paint the tab until we actually remove it. Put it in // tabs_closing_map_ so we can find it. RemoveTabFromViewModel(model_index); AnimateToIdealBounds(); gfx::Rect tab_bounds = tab_closing->bounds(); tab_bounds.set_width(0); bounds_animator_.AnimateViewTo(tab_closing, tab_bounds); // Register delegate to do cleanup when done, BoundsAnimator takes // ownership of RemoveTabDelegate. bounds_animator_.SetAnimationDelegate( tab_closing, scoped_ptr( new RemoveTabDelegate(this, tab_closing))); } bool TabStrip::IsPointInTab(Tab* tab, const gfx::Point& point_in_tabstrip_coords) { gfx::Point point_in_tab_coords(point_in_tabstrip_coords); View::ConvertPointToTarget(this, tab, &point_in_tab_coords); return tab->HitTestPoint(point_in_tab_coords); } int TabStrip::GetStartXForNormalTabs() const { int mini_tab_count = GetMiniTabCount(); if (mini_tab_count == 0) return 0; return mini_tab_count * (Tab::GetMiniWidth() + kTabHorizontalOffset) + kMiniToNonMiniGap; } Tab* TabStrip::FindTabForEvent(const gfx::Point& point) { if (touch_layout_) { int active_tab_index = touch_layout_->active_index(); if (active_tab_index != -1) { Tab* tab = FindTabForEventFrom(point, active_tab_index, -1); if (!tab) tab = FindTabForEventFrom(point, active_tab_index + 1, 1); return tab; } if (tab_count()) return FindTabForEventFrom(point, 0, 1); } else { for (int i = 0; i < tab_count(); ++i) { if (IsPointInTab(tab_at(i), point)) return tab_at(i); } } return NULL; } Tab* TabStrip::FindTabForEventFrom(const gfx::Point& point, int start, int delta) { // |start| equals tab_count() when there are only pinned tabs. if (start == tab_count()) start += delta; for (int i = start; i >= 0 && i < tab_count(); i += delta) { if (IsPointInTab(tab_at(i), point)) return tab_at(i); } return NULL; } views::View* TabStrip::FindTabHitByPoint(const gfx::Point& point) { // The display order doesn't necessarily match the child list order, so we // walk the display list hit-testing Tabs. Since the active tab always // renders on top of adjacent tabs, it needs to be hit-tested before any // left-adjacent Tab, so we look ahead for it as we walk. for (int i = 0; i < tab_count(); ++i) { Tab* next_tab = i < (tab_count() - 1) ? tab_at(i + 1) : NULL; if (next_tab && next_tab->IsActive() && IsPointInTab(next_tab, point)) return next_tab; if (IsPointInTab(tab_at(i), point)) return tab_at(i); } return NULL; } std::vector TabStrip::GetTabXCoordinates() { std::vector results; for (int i = 0; i < tab_count(); ++i) results.push_back(ideal_bounds(i).x()); return results; } void TabStrip::SwapLayoutIfNecessary() { bool needs_touch = NeedsTouchLayout(); bool using_touch = touch_layout_ != NULL; if (needs_touch == using_touch) return; if (needs_touch) { gfx::Size tab_size(Tab::GetMinimumSelectedSize()); tab_size.set_width(Tab::GetTouchWidth()); touch_layout_.reset(new StackedTabStripLayout( tab_size, kTabHorizontalOffset, kStackedPadding, kMaxStackedCount, &tabs_)); touch_layout_->SetWidth(tab_area_width()); // This has to be after SetWidth() as SetWidth() is going to reset the // bounds of the mini-tabs (since StackedTabStripLayout doesn't yet know how // many mini-tabs there are). GenerateIdealBoundsForMiniTabs(NULL); touch_layout_->SetXAndMiniCount(GetStartXForNormalTabs(), GetMiniTabCount()); touch_layout_->SetActiveIndex(controller_->GetActiveIndex()); } else { touch_layout_.reset(); } PrepareForAnimation(); GenerateIdealBounds(); SetTabVisibility(); AnimateToIdealBounds(); } bool TabStrip::NeedsTouchLayout() const { if (!stacked_layout_) return false; int mini_tab_count = GetMiniTabCount(); int normal_count = tab_count() - mini_tab_count; if (normal_count <= 1 || normal_count == mini_tab_count) return false; int x = GetStartXForNormalTabs(); int available_width = tab_area_width() - x; return (Tab::GetTouchWidth() * normal_count + kTabHorizontalOffset * (normal_count - 1)) > available_width; } void TabStrip::SetResetToShrinkOnExit(bool value) { if (!adjust_layout_) return; if (value && !stacked_layout_) value = false; // We're already using shrink (not stacked) layout. if (value == reset_to_shrink_on_exit_) return; reset_to_shrink_on_exit_ = value; // Add an observer so we know when the mouse moves out of the tabstrip. if (reset_to_shrink_on_exit_) AddMessageLoopObserver(); else RemoveMessageLoopObserver(); } void TabStrip::ButtonPressed(views::Button* sender, const ui::Event& event) { if (sender == newtab_button_) { content::RecordAction(UserMetricsAction("NewTab_Button")); UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON, TabStripModel::NEW_TAB_ENUM_COUNT); if (event.IsMouseEvent()) { const ui::MouseEvent& mouse = static_cast(event); if (mouse.IsOnlyMiddleMouseButton()) { base::string16 clipboard_text = GetClipboardText(); if (!clipboard_text.empty()) controller()->CreateNewTabWithLocation(clipboard_text); return; } } controller()->CreateNewTab(); if (event.type() == ui::ET_GESTURE_TAP) TouchUMA::RecordGestureAction(TouchUMA::GESTURE_NEWTAB_TAP); } } // Overridden to support automation. See automation_proxy_uitest.cc. const views::View* TabStrip::GetViewByID(int view_id) const { if (tab_count() > 0) { if (view_id == VIEW_ID_TAB_LAST) return tab_at(tab_count() - 1); if ((view_id >= VIEW_ID_TAB_0) && (view_id < VIEW_ID_TAB_LAST)) { int index = view_id - VIEW_ID_TAB_0; return (index >= 0 && index < tab_count()) ? tab_at(index) : NULL; } } return View::GetViewByID(view_id); } bool TabStrip::OnMousePressed(const ui::MouseEvent& event) { UpdateStackedLayoutFromMouseEvent(this, event); // We can't return true here, else clicking in an empty area won't drag the // window. return false; } bool TabStrip::OnMouseDragged(const ui::MouseEvent& event) { ContinueDrag(this, event); return true; } void TabStrip::OnMouseReleased(const ui::MouseEvent& event) { EndDrag(END_DRAG_COMPLETE); UpdateStackedLayoutFromMouseEvent(this, event); } void TabStrip::OnMouseCaptureLost() { EndDrag(END_DRAG_CAPTURE_LOST); } void TabStrip::OnMouseMoved(const ui::MouseEvent& event) { UpdateStackedLayoutFromMouseEvent(this, event); } void TabStrip::OnMouseEntered(const ui::MouseEvent& event) { SetResetToShrinkOnExit(true); } void TabStrip::OnGestureEvent(ui::GestureEvent* event) { SetResetToShrinkOnExit(false); switch (event->type()) { case ui::ET_GESTURE_SCROLL_END: case ui::ET_SCROLL_FLING_START: case ui::ET_GESTURE_END: EndDrag(END_DRAG_COMPLETE); if (adjust_layout_) { SetStackedLayout(true); controller_->StackedLayoutMaybeChanged(); } break; case ui::ET_GESTURE_LONG_PRESS: if (drag_controller_.get()) drag_controller_->SetMoveBehavior(TabDragController::REORDER); break; case ui::ET_GESTURE_LONG_TAP: { EndDrag(END_DRAG_CANCEL); gfx::Point local_point = event->location(); Tab* tab = FindTabForEvent(local_point); if (tab) { ConvertPointToScreen(this, &local_point); ShowContextMenuForTab(tab, local_point, ui::MENU_SOURCE_TOUCH); } break; } case ui::ET_GESTURE_SCROLL_UPDATE: ContinueDrag(this, *event); break; case ui::ET_GESTURE_TAP_DOWN: EndDrag(END_DRAG_CANCEL); break; case ui::ET_GESTURE_TAP: { const int active_index = controller_->GetActiveIndex(); DCHECK_NE(-1, active_index); Tab* active_tab = tab_at(active_index); TouchUMA::GestureActionType action = TouchUMA::GESTURE_TABNOSWITCH_TAP; if (active_tab->tab_activated_with_last_tap_down()) action = TouchUMA::GESTURE_TABSWITCH_TAP; TouchUMA::RecordGestureAction(action); break; } default: break; } event->SetHandled(); } views::View* TabStrip::TargetForRect(views::View* root, const gfx::Rect& rect) { CHECK_EQ(root, this); if (!views::UsePointBasedTargeting(rect)) return views::ViewTargeterDelegate::TargetForRect(root, rect); const gfx::Point point(rect.CenterPoint()); if (!touch_layout_) { // Return any view that isn't a Tab or this TabStrip immediately. We don't // want to interfere. views::View* v = views::ViewTargeterDelegate::TargetForRect(root, rect); if (v && v != this && strcmp(v->GetClassName(), Tab::kViewClassName)) return v; views::View* tab = FindTabHitByPoint(point); if (tab) return tab; } else { if (newtab_button_->visible()) { views::View* view = ConvertPointToViewAndGetEventHandler(this, newtab_button_, point); if (view) return view; } Tab* tab = FindTabForEvent(point); if (tab) return ConvertPointToViewAndGetEventHandler(this, tab, point); } return this; }