| // Copyright 2017 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" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <limits> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/adapters.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/feature_list.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/ranges.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/defaults.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/tab_group_theme.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/tab_types.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.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_group_header.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_highlight.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_underline.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_views.h" |
| #include "chrome/browser/ui/views/tabs/tab_hover_card_bubble_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_layout_helper.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_observer.h" |
| #include "chrome/browser/ui/views/tabs/tab_style_views.h" |
| #include "chrome/browser/ui/views/touch_uma/touch_uma.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "components/tab_groups/tab_group_color.h" |
| #include "components/tab_groups/tab_group_id.h" |
| #include "components/tab_groups/tab_group_visual_data.h" |
| #include "third_party/skia/include/core/SkColorFilter.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/effects/SkLayerDrawLooper.h" |
| #include "third_party/skia/include/pathops/SkPathOps.h" |
| #include "ui/base/clipboard/clipboard.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/base/theme_provider.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/animation/throb_animation.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/native_widget_types.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/masked_targeter_delegate.h" |
| #include "ui/views/metadata/metadata_impl_macros.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_observer.h" |
| #include "ui/views/view_targeter.h" |
| #include "ui/views/view_utils.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 "base/win/windows_version.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/gfx/win/hwnd_util.h" |
| #include "ui/views/win/hwnd_util.h" |
| #endif |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/window.h" |
| #endif |
| |
| namespace { |
| |
| // Distance from the next/previous stacked before before we consider the tab |
| // close enough to trigger moving. |
| const int kStackedDistance = 36; |
| |
| // Given the bounds of a dragged tab, return the X coordinate to use for |
| // computing where in the strip to insert/move the tab. |
| int GetDraggedX(const gfx::Rect& dragged_bounds) { |
| return dragged_bounds.x() + TabStyle::GetTabInternalPadding().left(); |
| } |
| |
| // Max number of stacked tabs. |
| constexpr int kMaxStackedCount = 4; |
| |
| // Padding between stacked tabs. |
| constexpr int kStackedPadding = 6; |
| |
| // Size of the drop indicator. |
| int g_drop_indicator_width = 0; |
| int g_drop_indicator_height = 0; |
| |
| // Listens in on the browser event stream (as a pre target event handler) and |
| // hides an associated hover card on any keypress. |
| class TabHoverCardEventSniffer : public ui::EventHandler { |
| // On Mac, events should be added to the root view. |
| #if defined(OS_MAC) |
| using OwnerView = views::View*; |
| #else // defined(OS_MAC) |
| using OwnerView = gfx::NativeWindow; |
| #endif // defined(OS_MAC) |
| |
| public: |
| TabHoverCardEventSniffer(TabHoverCardBubbleView* hover_card, |
| TabStrip* tab_strip) |
| : hover_card_(hover_card), |
| tab_strip_(tab_strip), |
| #if defined(OS_MAC) |
| owner_view_(tab_strip->GetWidget()->GetRootView()) { |
| #else // defined(OS_MAC) |
| owner_view_(tab_strip->GetWidget()->GetNativeWindow()) { |
| #endif // defined(OS_MAC) |
| AddPreTargetHandler(); |
| } |
| |
| ~TabHoverCardEventSniffer() override { |
| RemovePreTargetHandler(); |
| } |
| |
| protected: |
| void AddPreTargetHandler() { |
| if (owner_view_) |
| owner_view_->AddPreTargetHandler(this); |
| } |
| |
| void RemovePreTargetHandler() { |
| if (owner_view_) |
| owner_view_->RemovePreTargetHandler(this); |
| } |
| |
| // ui::EventTarget: |
| void OnKeyEvent(ui::KeyEvent* event) override { |
| if (!tab_strip_->IsFocusInTabs()) |
| tab_strip_->UpdateHoverCard(nullptr); |
| } |
| |
| void OnMouseEvent(ui::MouseEvent* event) override { |
| if (event->IsAnyButton()) |
| hover_card_->FadeOutToHide(); |
| } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| hover_card_->FadeOutToHide(); |
| } |
| |
| base::StringPiece GetLogContext() const override { |
| return "TabHoverCardEventSniffer"; |
| } |
| |
| private: |
| TabHoverCardBubbleView* const hover_card_; |
| TabStrip* tab_strip_; |
| const OwnerView owner_view_; |
| }; |
| |
| // Provides the ability to monitor when a tab's bounds have been animated. Used |
| // to hook callbacks to adjust things like tabstrip preferred size and tab group |
| // underlines. |
| class TabSlotAnimationDelegate : public gfx::AnimationDelegate { |
| public: |
| using OnAnimationProgressedCallback = |
| base::RepeatingCallback<void(TabSlotView*)>; |
| |
| TabSlotAnimationDelegate( |
| TabStrip* tab_strip, |
| TabSlotView* slot_view, |
| OnAnimationProgressedCallback on_animation_progressed); |
| TabSlotAnimationDelegate(const TabSlotAnimationDelegate&) = delete; |
| TabSlotAnimationDelegate& operator=(const TabSlotAnimationDelegate&) = delete; |
| ~TabSlotAnimationDelegate() override; |
| |
| void AnimationProgressed(const gfx::Animation* animation) override; |
| |
| protected: |
| TabStrip* tab_strip() { return tab_strip_; } |
| TabSlotView* slot_view() { return slot_view_; } |
| |
| private: |
| TabStrip* const tab_strip_; |
| TabSlotView* const slot_view_; |
| OnAnimationProgressedCallback on_animation_progressed_; |
| }; |
| |
| TabSlotAnimationDelegate::TabSlotAnimationDelegate( |
| TabStrip* tab_strip, |
| TabSlotView* slot_view, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : tab_strip_(tab_strip), |
| slot_view_(slot_view), |
| on_animation_progressed_(on_animation_progressed) {} |
| |
| TabSlotAnimationDelegate::~TabSlotAnimationDelegate() = default; |
| |
| void TabSlotAnimationDelegate::AnimationProgressed( |
| const gfx::Animation* animation) { |
| on_animation_progressed_.Run(slot_view()); |
| } |
| |
| // Animation delegate used when a dragged tab is released. When done sets the |
| // dragging state to false. |
| class ResetDraggingStateDelegate : public TabSlotAnimationDelegate { |
| public: |
| ResetDraggingStateDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed); |
| ResetDraggingStateDelegate(const ResetDraggingStateDelegate&) = delete; |
| ResetDraggingStateDelegate& operator=(const ResetDraggingStateDelegate&) = |
| delete; |
| ~ResetDraggingStateDelegate() override; |
| |
| void AnimationEnded(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| }; |
| |
| ResetDraggingStateDelegate::ResetDraggingStateDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {} |
| |
| ResetDraggingStateDelegate::~ResetDraggingStateDelegate() = default; |
| |
| void ResetDraggingStateDelegate::AnimationEnded( |
| const gfx::Animation* animation) { |
| static_cast<Tab*>(slot_view())->set_dragging(false); |
| AnimationProgressed(animation); |
| } |
| |
| 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 returns null. |
| 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) |
| : nullptr; |
| } |
| |
| // 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; |
| } |
| |
| int GetStackableTabWidth() { |
| return TabStyle::GetTabOverlap() + |
| (ui::TouchUiController::Get()->touch_ui() ? 136 : 102); |
| } |
| |
| } // namespace |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip::RemoveTabDelegate |
| // |
| // AnimationDelegate used when removing a tab. Does the necessary cleanup when |
| // done. |
| class TabStrip::RemoveTabDelegate : public TabSlotAnimationDelegate { |
| public: |
| RemoveTabDelegate(TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed); |
| RemoveTabDelegate(const RemoveTabDelegate&) = delete; |
| RemoveTabDelegate& operator=(const RemoveTabDelegate&) = delete; |
| |
| void AnimationEnded(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| }; |
| |
| TabStrip::RemoveTabDelegate::RemoveTabDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {} |
| |
| void TabStrip::RemoveTabDelegate::AnimationEnded( |
| const gfx::Animation* animation) { |
| tab_strip()->OnTabCloseAnimationCompleted(static_cast<Tab*>(slot_view())); |
| } |
| |
| void TabStrip::RemoveTabDelegate::AnimationCanceled( |
| const gfx::Animation* animation) { |
| AnimationEnded(animation); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip::TabDragContextImpl |
| // |
| class TabStrip::TabDragContextImpl : public TabDragContext { |
| public: |
| explicit TabDragContextImpl(TabStrip* tab_strip) : tab_strip_(tab_strip) {} |
| |
| bool IsDragStarted() const { |
| return drag_controller_ && drag_controller_->started_drag(); |
| } |
| |
| bool IsMutating() const { |
| return drag_controller_ && drag_controller_->is_mutating(); |
| } |
| |
| bool IsDraggingWindow() const { |
| return drag_controller_ && drag_controller_->is_dragging_window(); |
| } |
| |
| bool IsDraggingTab(content::WebContents* contents) const { |
| return contents && drag_controller_ && |
| drag_controller_->IsDraggingTab(contents); |
| } |
| |
| void SetMoveBehavior(TabDragController::MoveBehavior move_behavior) { |
| if (drag_controller_) |
| drag_controller_->SetMoveBehavior(move_behavior); |
| } |
| |
| void MaybeStartDrag(TabSlotView* source, |
| const ui::LocatedEvent& event, |
| const ui::ListSelectionModel& original_selection) { |
| std::vector<TabSlotView*> dragging_views; |
| int x = source->GetMirroredXInView(event.x()); |
| int y = event.y(); |
| |
| // Build the set of selected tabs to drag and calculate the offset from the |
| // source. |
| ui::ListSelectionModel selection_model; |
| if (source->GetTabSlotViewType() == |
| TabSlotView::ViewType::kTabGroupHeader) { |
| dragging_views.push_back(source); |
| |
| const gfx::Range grouped_tabs = |
| tab_strip_->controller()->ListTabsInGroup(source->group().value()); |
| for (auto index = grouped_tabs.start(); index < grouped_tabs.end(); |
| ++index) { |
| dragging_views.push_back(GetTabAt(index)); |
| // Set |selection_model| if and only if the original selection does not |
| // match the group exactly. See TabDragController::Init() for details |
| // on how |selection_model| is used. |
| if (!original_selection.IsSelected(index)) |
| selection_model = original_selection; |
| } |
| if (grouped_tabs.length() != original_selection.size()) |
| selection_model = original_selection; |
| } else { |
| for (int i = 0; i < GetTabCount(); ++i) { |
| Tab* other_tab = GetTabAt(i); |
| if (tab_strip_->IsTabSelected(other_tab)) { |
| dragging_views.push_back(other_tab); |
| if (other_tab == source) |
| x += GetSizeNeededForViews(dragging_views) - other_tab->width(); |
| } |
| } |
| if (!original_selection.IsSelected(tab_strip_->GetModelIndexOf(source))) |
| selection_model = original_selection; |
| } |
| |
| DCHECK(!dragging_views.empty()); |
| DCHECK(base::Contains(dragging_views, source)); |
| |
| // 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_VISIBLE_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 || |
| event.type() == ui::ET_GESTURE_SCROLL_BEGIN); |
| if (tab_strip_->touch_layout_ && |
| ((event.type() == ui::ET_MOUSE_PRESSED && |
| (((event.flags() & ui::EF_FROM_TOUCH) && |
| static_cast<const ui::MouseEvent&>(event).IsLeftMouseButton()) || |
| (!(event.flags() & ui::EF_FROM_TOUCH) && |
| static_cast<const ui::MouseEvent&>(event).IsControlDown()))) || |
| (event.type() == ui::ET_GESTURE_TAP_DOWN && !event.IsControlDown()) || |
| (event.type() == ui::ET_GESTURE_SCROLL_BEGIN && |
| !event.IsControlDown()))) { |
| move_behavior = TabDragController::MOVE_VISIBLE_TABS; |
| } |
| |
| drag_controller_ = std::make_unique<TabDragController>(); |
| drag_controller_->Init(this, source, dragging_views, gfx::Point(x, y), |
| event.x(), std::move(selection_model), move_behavior, |
| EventSourceFromEvent(event)); |
| } |
| |
| void 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); |
| |
| // Note: |tab_strip_| can be destroyed during drag, also destroying |
| // |this|. |
| base::WeakPtr<TabDragContext> weak_ptr(weak_factory_.GetWeakPtr()); |
| drag_controller_->Drag(screen_location); |
| |
| if (!weak_ptr) |
| return; |
| } |
| |
| // Note: |drag_controller| can be set to null during the drag above. |
| if (drag_controller_ && drag_controller_->group()) |
| tab_strip_->UpdateTabGroupVisuals(*drag_controller_->group()); |
| } |
| |
| bool EndDrag(EndDragReason reason) { |
| if (!drag_controller_.get()) |
| return false; |
| bool started_drag = drag_controller_->started_drag(); |
| drag_controller_->EndDrag(reason); |
| return started_drag; |
| } |
| |
| // TabDragContext: |
| views::View* AsView() override { return tab_strip_; } |
| |
| const views::View* AsView() const override { return tab_strip_; } |
| |
| Tab* GetTabAt(int i) const override { return tab_strip_->tab_at(i); } |
| |
| int GetIndexOf(const TabSlotView* view) const override { |
| return tab_strip_->GetModelIndexOf(view); |
| } |
| |
| int GetTabCount() const override { return tab_strip_->GetTabCount(); } |
| |
| bool IsTabPinned(const Tab* tab) const override { |
| return tab_strip_->IsTabPinned(tab); |
| } |
| |
| int GetPinnedTabCount() const override { |
| return tab_strip_->GetPinnedTabCount(); |
| } |
| |
| TabGroupHeader* GetTabGroupHeader( |
| const tab_groups::TabGroupId& group) const override { |
| return tab_strip_->group_header(group); |
| } |
| |
| TabStripModel* GetTabStripModel() override { |
| return static_cast<BrowserTabStripController*>( |
| tab_strip_->controller_.get()) |
| ->model(); |
| } |
| |
| base::Optional<int> GetActiveTouchIndex() const override { |
| if (!tab_strip_->touch_layout_) |
| return base::nullopt; |
| return tab_strip_->touch_layout_->active_index(); |
| } |
| |
| TabDragController* GetDragController() override { |
| return drag_controller_.get(); |
| } |
| |
| void OwnDragController(TabDragController* controller) override { |
| // 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 (drag_controller_.get() != controller) |
| drag_controller_.reset(controller); |
| } |
| |
| void DestroyDragController() override { |
| drag_controller_.reset(); |
| } |
| |
| TabDragController* ReleaseDragController() override { |
| return drag_controller_.release(); |
| } |
| |
| bool IsDragSessionActive() const override { |
| return drag_controller_ != nullptr; |
| } |
| |
| bool IsActiveDropTarget() const override { |
| for (int i = 0; i < GetTabCount(); ++i) { |
| const Tab* const tab = GetTabAt(i); |
| if (tab->dragging()) |
| return true; |
| } |
| return false; |
| } |
| |
| std::vector<int> GetTabXCoordinates() const override { |
| std::vector<int> results; |
| for (int i = 0; i < GetTabCount(); ++i) |
| results.push_back(ideal_bounds(i).x()); |
| return results; |
| } |
| |
| int GetActiveTabWidth() const override { |
| return tab_strip_->GetActiveTabWidth(); |
| } |
| |
| int GetTabDragAreaWidth() const override { |
| // There are two cases here (with tab scrolling enabled): |
| // 1) If the tab strip is not wider than the tab strip region (and thus |
| // not scrollable), returning the available width for tabs rather than the |
| // actual width for tabs allows tabs to be dragged past the current bounds |
| // of the tabstrip, anywhere along the tab strip region. |
| // N.B. The available width for tabs in this case needs to ignore tab |
| // closing mode. |
| // 2) If the tabstrip is wider than the tab strip region (and thus is |
| // scrollable), returning the tabstrip width allows tabs to be dragged |
| // anywhere within the tabstrip, not just in the leftmost region of it. |
| return std::max(tab_strip_->GetAvailableWidthForTabStrip(), |
| tab_strip_->width()); |
| } |
| |
| int TabDragAreaBeginX() const override { |
| return tab_strip_->GetMirroredXWithWidthInView(0, GetTabDragAreaWidth()); |
| } |
| |
| int TabDragAreaEndX() const override { |
| return TabDragAreaBeginX() + GetTabDragAreaWidth(); |
| } |
| |
| int GetHorizontalDragThreshold() const override { |
| constexpr int kHorizontalMoveThreshold = 16; // DIPs. |
| |
| // Stacked tabs in touch mode don't shrink. |
| if (tab_strip_->touch_layout_) |
| return kHorizontalMoveThreshold; |
| |
| double ratio = double{tab_strip_->GetInactiveTabWidth()} / |
| TabStyle::GetStandardWidth(); |
| return base::ClampRound(ratio * kHorizontalMoveThreshold); |
| } |
| |
| int GetInsertionIndexForDraggedBounds( |
| const gfx::Rect& dragged_bounds, |
| std::vector<TabSlotView*> dragged_views, |
| int num_dragged_tabs, |
| bool mouse_has_ever_moved_left, |
| bool mouse_has_ever_moved_right, |
| base::Optional<tab_groups::TabGroupId> group) const override { |
| // If the strip has no tabs, the only position to insert at is 0. |
| if (!GetTabCount()) |
| return 0; |
| |
| base::Optional<int> index; |
| base::Optional<int> touch_index = GetActiveTouchIndex(); |
| if (touch_index) { |
| index = GetInsertionIndexForDraggedBoundsStacked( |
| dragged_bounds, mouse_has_ever_moved_left, |
| mouse_has_ever_moved_right); |
| if (index) { |
| // Only move the tab to the left/right if the user actually moved the |
| // mouse that way. This is necessary as tabs with stacked tabs |
| // before/after them have multiple drag positions. |
| if ((index < touch_index && !mouse_has_ever_moved_left) || |
| (index > touch_index && !mouse_has_ever_moved_right)) { |
| index = *touch_index; |
| } |
| } |
| } else { |
| // If we're dragging a group by its header, the first element of |
| // |dragged_views| is a group header, and the second one is the first tab |
| // in that group. |
| int first_dragged_tab_index = |
| tab_strip_->GetModelIndexOf(dragged_views[group.has_value() ? 1 : 0]); |
| index = CalculateInsertionIndex(dragged_bounds, first_dragged_tab_index, |
| num_dragged_tabs, std::move(group)); |
| } |
| if (!index) { |
| const int last_tab_right = ideal_bounds(GetTabCount() - 1).right(); |
| index = (dragged_bounds.right() > last_tab_right) ? GetTabCount() : 0; |
| } |
| |
| const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab(); |
| int last_insertion_point = |
| last_visible_tab ? (GetIndexOf(last_visible_tab) + 1) : 0; |
| |
| // Clamp the insertion point to keep it within the visible region. |
| last_insertion_point = std::max(0, last_insertion_point - num_dragged_tabs); |
| |
| // Ensure the first dragged tab always stays in the visible index range. |
| return std::min(*index, last_insertion_point); |
| } |
| |
| bool ShouldDragToNextStackedTab( |
| const gfx::Rect& dragged_bounds, |
| int index, |
| bool mouse_has_ever_moved_right) const override { |
| if (index + 1 >= GetTabCount() || |
| !tab_strip_->touch_layout_->IsStacked(index + 1) || |
| !mouse_has_ever_moved_right) |
| return false; |
| |
| int active_x = ideal_bounds(index).x(); |
| int next_x = ideal_bounds(index + 1).x(); |
| int mid_x = |
| std::min(next_x - kStackedDistance, active_x + (next_x - active_x) / 4); |
| return GetDraggedX(dragged_bounds) >= mid_x; |
| } |
| |
| bool ShouldDragToPreviousStackedTab( |
| const gfx::Rect& dragged_bounds, |
| int index, |
| bool mouse_has_ever_moved_left) const override { |
| if (index - 1 < tab_strip_->GetPinnedTabCount() || |
| !tab_strip_->touch_layout_->IsStacked(index - 1) || |
| !mouse_has_ever_moved_left) |
| return false; |
| |
| int active_x = ideal_bounds(index).x(); |
| int previous_x = ideal_bounds(index - 1).x(); |
| int mid_x = std::max(previous_x + kStackedDistance, |
| active_x - (active_x - previous_x) / 4); |
| return GetDraggedX(dragged_bounds) <= mid_x; |
| } |
| |
| void DragActiveTabStacked(const std::vector<int>& initial_positions, |
| int delta) override { |
| DCHECK_EQ(GetTabCount(), int{initial_positions.size()}); |
| SetIdealBoundsFromPositions(initial_positions); |
| tab_strip_->touch_layout_->DragActiveTab(delta); |
| tab_strip_->CompleteAnimationAndLayout(); |
| } |
| |
| std::vector<gfx::Rect> CalculateBoundsForDraggedViews( |
| const std::vector<TabSlotView*>& views) override { |
| DCHECK(!views.empty()); |
| |
| std::vector<gfx::Rect> bounds; |
| const int overlap = TabStyle::GetTabOverlap(); |
| int x = 0; |
| for (const TabSlotView* view : views) { |
| const int width = view->width(); |
| bounds.push_back(gfx::Rect(x, 0, width, view->height())); |
| x += width - overlap; |
| } |
| |
| return bounds; |
| } |
| |
| void SetBoundsForDrag(const std::vector<TabSlotView*>& views, |
| const std::vector<gfx::Rect>& bounds) override { |
| tab_strip_->StopAnimating(false); |
| DCHECK_EQ(views.size(), bounds.size()); |
| for (size_t i = 0; i < views.size(); ++i) |
| views[i]->SetBoundsRect(bounds[i]); |
| |
| // Ensure that the tab strip and its parent views are correctly re-laid out |
| // after repositioning dragged tabs. This avoids visual/layout issues such |
| // as https://crbug.com/1151092. |
| tab_strip_->PreferredSizeChanged(); |
| |
| // Reset the layout size as we've effectively laid out a different size. |
| // This ensures a layout happens after the drag is done. |
| tab_strip_->last_layout_size_ = gfx::Size(); |
| if (views.at(0)->group().has_value()) |
| tab_strip_->UpdateTabGroupVisuals(views.at(0)->group().value()); |
| } |
| |
| void StartedDragging(const std::vector<TabSlotView*>& views) override { |
| // Let the controller know that the user started dragging tabs. |
| tab_strip_->controller_->OnStartedDragging( |
| views.size() == static_cast<size_t>(tab_strip_->GetModelCount())); |
| |
| // Reset dragging state of existing tabs. |
| for (int i = 0; i < GetTabCount(); ++i) |
| GetTabAt(i)->set_dragging(false); |
| |
| for (size_t i = 0; i < views.size(); ++i) { |
| views[i]->set_dragging(true); |
| tab_strip_->bounds_animator_.StopAnimatingView(views[i]); |
| } |
| |
| // Move the dragged tabs to their ideal bounds. |
| tab_strip_->UpdateIdealBounds(); |
| |
| // Sets the bounds of the dragged tab slots. |
| for (TabSlotView* view : views) { |
| if (view->GetTabSlotViewType() == |
| TabSlotView::ViewType::kTabGroupHeader) { |
| view->SetBoundsRect(ideal_bounds(view->group().value())); |
| } else { |
| int tab_data_index = GetIndexOf(view); |
| DCHECK_NE(TabStripModel::kNoTab, tab_data_index); |
| view->SetBoundsRect(ideal_bounds(tab_data_index)); |
| } |
| } |
| |
| tab_strip_->SetTabSlotVisibility(); |
| tab_strip_->SchedulePaint(); |
| } |
| |
| void DraggedTabsDetached() override { |
| // Let the controller know that the user is not dragging this tabstrip's |
| // tabs anymore. |
| tab_strip_->controller_->OnStoppedDragging(); |
| } |
| |
| void StoppedDragging(const std::vector<TabSlotView*>& views, |
| const std::vector<int>& initial_positions, |
| bool move_only, |
| bool completed) override { |
| // Let the controller know that the user stopped dragging tabs. |
| tab_strip_->controller_->OnStoppedDragging(); |
| |
| if (move_only && tab_strip_->touch_layout_) { |
| if (completed) |
| tab_strip_->touch_layout_->SizeToFit(); |
| else |
| SetIdealBoundsFromPositions(initial_positions); |
| } |
| bool is_first_view = true; |
| for (size_t i = 0; i < views.size(); ++i) |
| tab_strip_->StoppedDraggingView(views[i], &is_first_view); |
| } |
| |
| void LayoutDraggedViewsAt(const std::vector<TabSlotView*>& views, |
| TabSlotView* source_view, |
| const gfx::Point& location, |
| bool initial_drag) override { |
| std::vector<gfx::Rect> bounds = CalculateBoundsForDraggedViews(views); |
| DCHECK_EQ(views.size(), bounds.size()); |
| |
| int active_tab_model_index = GetIndexOf(source_view); |
| int active_tab_index = static_cast<int>( |
| std::find(views.begin(), views.end(), source_view) - views.begin()); |
| for (size_t i = 0; i < views.size(); ++i) { |
| TabSlotView* view = views[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<int>(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 && GetIndexOf(views[i]) != consecutive_index) || |
| tab_strip_->bounds_animator_.IsAnimating(views[i])) { |
| tab_strip_->bounds_animator_.SetTargetBounds(views[i], new_bounds); |
| } else { |
| view->SetBoundsRect(new_bounds); |
| } |
| } |
| tab_strip_->SetTabSlotVisibility(); |
| // The rightmost tab may have moved, which would change the tabstrip's |
| // preferred width. |
| tab_strip_->PreferredSizeChanged(); |
| } |
| |
| // Forces the entire tabstrip to lay out. |
| void ForceLayout() override { |
| tab_strip_->InvalidateLayout(); |
| tab_strip_->CompleteAnimationAndLayout(); |
| } |
| |
| private: |
| gfx::Rect ideal_bounds(int i) const { return tab_strip_->ideal_bounds(i); } |
| |
| gfx::Rect ideal_bounds(tab_groups::TabGroupId group) const { |
| return tab_strip_->ideal_bounds(group); |
| } |
| |
| // Determines the index to move the dragged tabs to. The dragged tabs must |
| // already be in the tabstrip. |dragged_bounds| is the union of the bounds |
| // of the dragged tabs and group header, if any. |first_dragged_tab_index| is |
| // the current model index in this tabstrip of the first dragged tab. The |
| // dragged tabs must be in the tabstrip already! |
| int CalculateInsertionIndex( |
| const gfx::Rect& dragged_bounds, |
| int first_dragged_tab_index, |
| int num_dragged_tabs, |
| base::Optional<tab_groups::TabGroupId> dragged_group) const { |
| // This method assumes that the dragged tabs and group are already in the |
| // tabstrip (i.e. it doesn't support attaching a drag to a new tabstrip). |
| // This assumption is critical because it means that tab width won't change |
| // after this method's recommendation is implemented. |
| |
| // For each possible insertion index, determine what the ideal bounds of |
| // the dragged tabs would be at that index. This corresponds to where they |
| // would slide to if the drag session ended now. We want to insert at the |
| // index that minimizes the distance between the corresponding ideal bounds |
| // and the current bounds of the tabs. This is equivalent to minimizing: |
| // - the distance of the aforementioned slide, |
| // - the width of the gaps in the tabstrip, or |
| // - the amount of tab overlap. |
| int min_distance_index = -1; |
| int min_distance = std::numeric_limits<int>::max(); |
| for (int candidate_index = 0; candidate_index <= GetTabCount(); |
| ++candidate_index) { |
| if (!IsValidInsertionIndex(candidate_index, first_dragged_tab_index, |
| num_dragged_tabs, dragged_group)) { |
| continue; |
| } |
| |
| // If there's a group header here, and we're dragging a group, we might |
| // end up on either side of that header. Check both cases to find the |
| // best option. |
| // TODO(tbergquist): Use this approach to determine if a tab should be |
| // added to the group. This is calculated elsewhere and may require some |
| // plumbing and/or duplicated code. |
| const int left_ideal_x = CalculateIdealX( |
| candidate_index, first_dragged_tab_index, dragged_bounds); |
| const int left_distance = std::abs(dragged_bounds.x() - left_ideal_x); |
| |
| const int right_ideal_x = |
| left_ideal_x + CalculateIdealXAdjustmentIfAddedToGroup( |
| candidate_index, dragged_group); |
| const int right_distance = std::abs(dragged_bounds.x() - right_ideal_x); |
| |
| const int distance = std::min(left_distance, right_distance); |
| if (distance < min_distance) { |
| min_distance = distance; |
| min_distance_index = candidate_index; |
| } |
| } |
| |
| if (min_distance_index == -1) { |
| NOTREACHED(); |
| return 0; |
| } |
| |
| // When moving a tab within a tabstrip, the target index is expressed as if |
| // the tabs are not in the tabstrip, i.e. it acts like the tabs are first |
| // removed and then re-inserted at the target index. We need to adjust the |
| // target index to account for this. |
| if (min_distance_index > first_dragged_tab_index) |
| min_distance_index -= num_dragged_tabs; |
| |
| return min_distance_index; |
| } |
| |
| // Dragging can't insert tabs into some indices. |
| bool IsValidInsertionIndex( |
| int candidate_index, |
| int first_dragged_tab_index, |
| int num_dragged_tabs, |
| base::Optional<tab_groups::TabGroupId> dragged_group) const { |
| if (candidate_index == 0) |
| return true; |
| |
| // If |candidate_index| is right after one of the tabs we're dragging, |
| // inserting here would be nonsensical - we can't insert the dragged tabs |
| // into the middle of the dragged tabs. That's just silly. |
| if (candidate_index > first_dragged_tab_index && |
| candidate_index <= first_dragged_tab_index + num_dragged_tabs) { |
| return false; |
| } |
| |
| // This might be in the middle of a group, which may or may not be fine. |
| base::Optional<tab_groups::TabGroupId> left_group = |
| GetTabAt(candidate_index - 1)->group(); |
| base::Optional<tab_groups::TabGroupId> right_group = |
| tab_strip_->IsValidModelIndex(candidate_index) |
| ? GetTabAt(candidate_index)->group() |
| : base::nullopt; |
| if (left_group.has_value() && left_group == right_group) { |
| // Can't drag a group into another group. |
| if (dragged_group.has_value()) |
| return false; |
| // Can't drag a tab into a collapsed group. |
| if (tab_strip_->controller()->IsGroupCollapsed(left_group.value())) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Determines the x position that the dragged tabs would have if they were |
| // inserted at |candidate_index|. If there's a group header at that index, |
| // this assumes the dragged tabs *would not* be inserted into the group, |
| // and would therefore end up to the left of that header. |
| int CalculateIdealX(int candidate_index, |
| int first_dragged_tab_index, |
| gfx::Rect dragged_bounds) const { |
| if (candidate_index == 0) |
| return 0; |
| |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| |
| // We'll insert just right of the tab at |candidate_index| - 1. |
| int ideal_x = ideal_bounds(candidate_index - 1).right(); |
| |
| // If the dragged tabs are currently left of |candidate_index|, moving |
| // them to |candidate_index| would move the tab at |candidate_index| - 1 |
| // to the left by |num_dragged_tabs| slots. This would change the ideal x |
| // for the dragged tabs, as well, by the width of the dragged tabs. |
| if (candidate_index - 1 > first_dragged_tab_index) |
| ideal_x -= dragged_bounds.width() - tab_overlap; |
| |
| return ideal_x - tab_overlap; |
| } |
| |
| // There might be a group starting at |candidate_index|. If there is, |
| // this determines how the ideal x would change if the dragged tabs were |
| // added to that group, thereby moving them to that header's right. |
| int CalculateIdealXAdjustmentIfAddedToGroup( |
| int candidate_index, |
| base::Optional<tab_groups::TabGroupId> dragged_group) const { |
| // If the tab to the right of |candidate_index| is the first tab in a |
| // (non-collapsed) group, we are sharing this model index with a group |
| // header. We might end up on either side of it, so we need to check |
| // both positions. |
| if (!dragged_group.has_value() && |
| tab_strip_->IsValidModelIndex(candidate_index)) { |
| base::Optional<tab_groups::TabGroupId> left_group = |
| tab_strip_->IsValidModelIndex(candidate_index - 1) |
| ? GetTabAt(candidate_index - 1)->group() |
| : base::nullopt; |
| base::Optional<tab_groups::TabGroupId> right_group = |
| GetTabAt(candidate_index)->group(); |
| if (right_group.has_value() && left_group != right_group) { |
| if (tab_strip_->controller()->IsGroupCollapsed(right_group.value())) |
| return 0; |
| const int header_width = |
| GetTabGroupHeader(*right_group)->bounds().width() - |
| TabStyle::GetTabOverlap(); |
| return header_width; |
| } |
| } |
| |
| return 0; |
| } |
| |
| // Used by GetInsertionIndexForDraggedBounds() when the tabstrip is stacked. |
| base::Optional<int> GetInsertionIndexForDraggedBoundsStacked( |
| const gfx::Rect& dragged_bounds, |
| bool mouse_has_ever_moved_left, |
| bool mouse_has_ever_moved_right) const { |
| int active_index = *GetActiveTouchIndex(); |
| // Search from the active index to the front of the tabstrip. Do this as |
| // tabs overlap each other from the active index. |
| base::Optional<int> index = |
| GetInsertionIndexFromReversedStacked(dragged_bounds, active_index); |
| if (index != active_index) |
| return index; |
| if (!index) |
| return GetInsertionIndexFromStacked(dragged_bounds, active_index + 1); |
| |
| // The position to drag to corresponds to the active tab. If the |
| // next/previous tab is stacked, then shorten the distance used to determine |
| // insertion bounds. We do this as GetInsertionIndexFrom() uses the bounds |
| // of the tabs. When tabs are stacked the next/previous tab is on top of the |
| // tab. |
| if (active_index + 1 < GetTabCount() && |
| tab_strip_->touch_layout_->IsStacked(active_index + 1)) { |
| index = GetInsertionIndexFromStacked(dragged_bounds, active_index + 1); |
| if (!index && ShouldDragToNextStackedTab(dragged_bounds, active_index, |
| mouse_has_ever_moved_right)) |
| index = active_index + 1; |
| else if (index == -1) |
| index = active_index; |
| } else if (ShouldDragToPreviousStackedTab(dragged_bounds, active_index, |
| mouse_has_ever_moved_left)) { |
| index = active_index - 1; |
| } |
| return index; |
| } |
| |
| // Determines the index to insert tabs at. |dragged_bounds| is the bounds of |
| // the tab being dragged and |start| is the index of the tab to start looking |
| // from. The search proceeds to the end of the strip. |
| base::Optional<int> GetInsertionIndexFromStacked( |
| const gfx::Rect& dragged_bounds, |
| int start) const { |
| const int last_tab = GetTabCount() - 1; |
| if (start < 0 || start > last_tab) |
| return base::nullopt; |
| |
| const int dragged_x = GetDraggedX(dragged_bounds); |
| if (dragged_x < ideal_bounds(start).x() || |
| dragged_x > ideal_bounds(last_tab).right()) { |
| return base::nullopt; |
| } |
| |
| base::Optional<int> insertion_index; |
| for (int i = start; i <= last_tab; ++i) { |
| const gfx::Rect current_bounds = ideal_bounds(i); |
| int current_center = current_bounds.CenterPoint().x(); |
| |
| if (dragged_bounds.width() > current_bounds.width() && |
| dragged_bounds.x() < current_bounds.x()) { |
| current_center -= (dragged_bounds.width() - current_bounds.width()); |
| } |
| |
| if (dragged_x < current_center) { |
| insertion_index = i; |
| break; |
| } |
| } |
| |
| if (!insertion_index.has_value()) |
| return last_tab + 1; |
| |
| return insertion_index; |
| } |
| |
| // Like GetInsertionIndexFrom(), but searches backwards from |start| to the |
| // beginning of the strip. |
| base::Optional<int> GetInsertionIndexFromReversedStacked( |
| const gfx::Rect& dragged_bounds, |
| int start) const { |
| const int dragged_x = GetDraggedX(dragged_bounds); |
| if (start < 0 || start >= GetTabCount() || |
| dragged_x >= ideal_bounds(start).right() || |
| dragged_x < ideal_bounds(0).x()) |
| return base::nullopt; |
| |
| for (int i = start; i >= 0; --i) { |
| if (dragged_x >= ideal_bounds(i).CenterPoint().x()) |
| return i + 1; |
| } |
| |
| return 0; |
| } |
| |
| // Sets the ideal bounds x-coordinates to |positions|. |
| void SetIdealBoundsFromPositions(const std::vector<int>& positions) { |
| if (static_cast<size_t>(GetTabCount()) != positions.size()) |
| return; |
| |
| for (int i = 0; i < GetTabCount(); ++i) { |
| gfx::Rect bounds(ideal_bounds(i)); |
| bounds.set_x(positions[i]); |
| tab_strip_->tabs_.set_ideal_bounds(i, bounds); |
| } |
| } |
| |
| TabStrip* const tab_strip_; |
| |
| // The controller for a drag initiated from a Tab. Valid for the lifetime of |
| // the drag session. |
| std::unique_ptr<TabDragController> drag_controller_; |
| |
| base::WeakPtrFactory<TabDragContext> weak_factory_{this}; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, public: |
| |
| TabStrip::TabStrip(std::unique_ptr<TabStripController> controller) |
| : controller_(std::move(controller)), |
| layout_helper_(std::make_unique<TabStripLayoutHelper>( |
| controller_.get(), |
| base::BindRepeating(&TabStrip::tabs_view_model, |
| base::Unretained(this)))), |
| drag_context_(std::make_unique<TabDragContextImpl>(this)) { |
| Init(); |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| } |
| |
| TabStrip::~TabStrip() { |
| // The animations may reference the tabs. Shut down the animation before we |
| // delete the tabs. |
| StopAnimating(false); |
| |
| // Disengage the drag controller before doing any additional cleanup. This |
| // call can interact with child views so we can't reliably do it during member |
| // destruction. |
| drag_context_->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(); |
| |
| hover_card_observation_.Reset(); |
| |
| // Since TabGroupViews expects be able to remove the views it creates, clear |
| // |group_views_| before removing the remaining children below. |
| group_views_.clear(); |
| |
| // The child tabs may call back to us from their destructors. Delete them so |
| // that if they call back we aren't in a weird state. |
| RemoveAllChildViews(true); |
| |
| CHECK(!IsInObserverList()); |
| } |
| |
| void TabStrip::SetAvailableWidthCallback( |
| base::RepeatingCallback<int()> available_width_callback) { |
| available_width_callback_ = available_width_callback; |
| } |
| |
| // static |
| int TabStrip::GetSizeNeededForViews(const std::vector<TabSlotView*>& views) { |
| int width = 0; |
| for (const TabSlotView* view : views) |
| width += view->width(); |
| if (!views.empty()) |
| width -= TabStyle::GetTabOverlap() * (views.size() - 1); |
| return width; |
| } |
| |
| void TabStrip::AddObserver(TabStripObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void TabStrip::RemoveObserver(TabStripObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void TabStrip::FrameColorsChanged() { |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->FrameColorsChanged(); |
| UpdateContrastRatioValues(); |
| SchedulePaint(); |
| } |
| |
| void TabStrip::SetBackgroundOffset(int background_offset) { |
| if (background_offset == background_offset_) |
| return; |
| background_offset_ = background_offset; |
| OnPropertyChanged(&background_offset_, views::kPropertyEffectsPaint); |
| } |
| |
| bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { |
| // If there is no control at this location, the hit is in the caption area. |
| const views::View* v = GetEventHandlerForRect(rect); |
| if (v == this) |
| return true; |
| |
| // When the window has a top drag handle, a thin strip at the top of inactive |
| // tabs and the new tab button is treated as part of the window drag handle, |
| // to increase draggability. This region starts 1 DIP above the top of the |
| // separator. |
| const int drag_handle_extension = TabStyle::GetDragHandleExtension(height()); |
| |
| // Disable drag handle extension when tab shapes are visible. |
| bool extend_drag_handle = !controller_->IsFrameCondensed() && |
| !controller_->EverHasVisibleBackgroundTabShapes(); |
| |
| // A hit on the tab is not in the caption unless it is in the thin strip |
| // mentioned above. |
| const int tab_index = tabs_.GetIndexOfView(v); |
| if (IsValidModelIndex(tab_index)) { |
| Tab* tab = tab_at(tab_index); |
| gfx::Rect tab_drag_handle = tab->GetMirroredBounds(); |
| tab_drag_handle.set_height(drag_handle_extension); |
| return extend_drag_handle && !tab->IsActive() && |
| tab_drag_handle.Intersects(rect); |
| } |
| |
| // |v| is some other view (e.g. a close button in a tab) and therefore |rect| |
| // is in client area. |
| return false; |
| } |
| |
| bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) { |
| return IsRectInWindowCaption(gfx::Rect(point, gfx::Size(1, 1))); |
| } |
| |
| bool TabStrip::IsTabStripCloseable() const { |
| return !drag_context_->IsDragSessionActive(); |
| } |
| |
| bool TabStrip::IsTabStripEditable() const { |
| return !drag_context_->IsDragSessionActive() && |
| !drag_context_->IsActiveDropTarget(); |
| } |
| |
| bool TabStrip::IsTabCrashed(int tab_index) const { |
| return tab_at(tab_index)->data().IsCrashed(); |
| } |
| |
| bool TabStrip::TabHasNetworkError(int tab_index) const { |
| return tab_at(tab_index)->data().network_state == TabNetworkState::kError; |
| } |
| |
| base::Optional<TabAlertState> TabStrip::GetTabAlertState(int tab_index) const { |
| return Tab::GetAlertStateToShow(tab_at(tab_index)->data().alert_state); |
| } |
| |
| void TabStrip::UpdateLoadingAnimations(const base::TimeDelta& elapsed_time) { |
| for (int i = 0; i < GetTabCount(); i++) |
| tab_at(i)->StepLoadingAnimation(elapsed_time); |
| } |
| |
| void TabStrip::SetStackedLayout(bool stacked_layout) { |
| if (stacked_layout == stacked_layout_) |
| return; |
| |
| stacked_layout_ = stacked_layout; |
| SetResetToShrinkOnExit(false); |
| SwapLayoutIfNecessary(); |
| |
| // When transitioning to stacked try to keep the active tab from moving. |
| const int active_index = controller_->GetActiveIndex(); |
| if (touch_layout_ && active_index != -1) { |
| touch_layout_->SetActiveTabLocation(ideal_bounds(active_index).x()); |
| AnimateToIdealBounds(); |
| } |
| |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->Layout(); |
| } |
| |
| void TabStrip::AddTabAt(int model_index, TabRendererData data, bool is_active) { |
| Tab* tab = new Tab(this); |
| tab->set_context_menu_controller(&context_menu_controller_); |
| tab->AddObserver(this); |
| AddChildViewAt(tab, GetViewInsertionIndex(tab, base::nullopt, model_index)); |
| const bool pinned = data.pinned; |
| tabs_.Add(tab, model_index); |
| selected_tabs_.IncrementFrom(model_index); |
| |
| // Setting data must come after all state from the model has been updated |
| // above for the tab. Accessibility, in particular, reacts to data changed |
| // callbacks. |
| tab->SetData(std::move(data)); |
| |
| if (touch_layout_) { |
| int add_types = 0; |
| if (pinned) |
| add_types |= StackedTabStripLayout::kAddTypePinned; |
| if (is_active) |
| add_types |= StackedTabStripLayout::kAddTypeActive; |
| touch_layout_->AddTab(model_index, add_types, |
| UpdateIdealBoundsForPinnedTabs(nullptr)); |
| } |
| |
| // Don't animate the first tab, it looks weird, and don't animate anything |
| // if the containing window isn't visible yet. |
| if (GetTabCount() > 1 && GetWidget() && GetWidget()->IsVisible()) { |
| StartInsertTabAnimation(model_index, |
| pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| } else { |
| layout_helper_->InsertTabAt( |
| model_index, tab, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| CompleteAnimationAndLayout(); |
| } |
| |
| SwapLayoutIfNecessary(); |
| UpdateAccessibleTabIndices(); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabAdded(model_index); |
| |
| // 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 worth the complexity. |
| // |
| // At the start of AddTabAt() the model and tabs are out sync. Any queries to |
| // find a tab given a model index can go off the end of |tabs_|. As such, it |
| // is important that we complete the drag *after* adding the tab so that the |
| // model and tabstrip are in sync. |
| if (!drag_context_->IsMutating() && drag_context_->IsDraggingWindow()) |
| EndDrag(END_DRAG_COMPLETE); |
| |
| Profile* profile = controller()->GetProfile(); |
| if (profile) { |
| if (profile->IsGuestSession() || profile->IsEphemeralGuestProfile()) |
| base::UmaHistogramCounts100("Tab.Count.Guest", GetTabCount()); |
| else if (profile->IsIncognitoProfile()) |
| base::UmaHistogramCounts100("Tab.Count.Incognito", GetTabCount()); |
| } |
| } |
| |
| void TabStrip::MoveTab(int from_model_index, |
| int to_model_index, |
| TabRendererData data) { |
| DCHECK_GT(tabs_.view_size(), 0); |
| |
| Tab* moving_tab = tab_at(from_model_index); |
| const bool pinned = data.pinned; |
| moving_tab->SetData(std::move(data)); |
| |
| ReorderChildView( |
| moving_tab, |
| GetViewInsertionIndex(moving_tab, from_model_index, to_model_index)); |
| |
| if (touch_layout_) { |
| tabs_.MoveViewOnly(from_model_index, to_model_index); |
| int pinned_count = 0; |
| const int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_count); |
| touch_layout_->MoveTab(from_model_index, to_model_index, |
| controller_->GetActiveIndex(), start_x, |
| pinned_count); |
| } else { |
| tabs_.Move(from_model_index, to_model_index); |
| } |
| selected_tabs_.Move(from_model_index, to_model_index, /*length=*/1); |
| |
| layout_helper_->MoveTab(moving_tab->group(), from_model_index, |
| to_model_index); |
| layout_helper_->SetTabPinned( |
| to_model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| StartMoveTabAnimation(); |
| SwapLayoutIfNecessary(); |
| |
| UpdateAccessibleTabIndices(); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabMoved(from_model_index, to_model_index); |
| } |
| |
| void TabStrip::RemoveTabAt(content::WebContents* contents, |
| int model_index, |
| bool was_active) { |
| StartRemoveTabAnimation(model_index, was_active); |
| |
| SwapLayoutIfNecessary(); |
| |
| UpdateAccessibleTabIndices(); |
| |
| UpdateHoverCard(nullptr); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabRemoved(model_index); |
| |
| // Stop dragging when a new tab is removed 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 |
| // remove operation, but this comes up infrequently enough that it's not worth |
| // the complexity. |
| // |
| // At the start of RemoveTabAt() the model and tabs are out sync. Any queries |
| // to find a tab given a model index can go off the end of |tabs_|. As such, |
| // it is important that we complete the drag *after* removing the tab so that |
| // the model and tabstrip are in sync. |
| if (!drag_context_->IsMutating() && drag_context_->IsDraggingTab(contents)) |
| EndDrag(END_DRAG_COMPLETE); |
| } |
| |
| void TabStrip::SetTabData(int model_index, TabRendererData data) { |
| Tab* tab = tab_at(model_index); |
| const bool pinned = data.pinned; |
| const bool pinned_state_changed = tab->data().pinned != pinned; |
| tab->SetData(std::move(data)); |
| |
| if (HoverCardIsShowingForTab(tab)) |
| UpdateHoverCard(tab); |
| |
| if (pinned_state_changed) { |
| if (touch_layout_) { |
| int pinned_tab_count = 0; |
| int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_tab_count); |
| touch_layout_->SetXAndPinnedCount(start_x, pinned_tab_count); |
| } |
| |
| layout_helper_->SetTabPinned( |
| model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| if (GetWidget() && GetWidget()->IsVisible()) |
| StartPinnedTabAnimation(); |
| else |
| CompleteAnimationAndLayout(); |
| } |
| SwapLayoutIfNecessary(); |
| } |
| |
| void TabStrip::AddTabToGroup(base::Optional<tab_groups::TabGroupId> group, |
| int model_index) { |
| tab_at(model_index)->set_group(group); |
| |
| // Expand the group if the tab that is getting grouped is the active tab. This |
| // can result in the group expanding in a series of actions where the final |
| // active tab is not in the group. |
| if (model_index == selected_tabs_.active() && group.has_value() && |
| controller()->IsGroupCollapsed(group.value())) { |
| controller()->ToggleTabGroupCollapsedState( |
| group.value(), ToggleTabGroupCollapsedStateOrigin::kImplicitAction); |
| } |
| |
| if (group.has_value()) |
| ExitTabClosingMode(); |
| } |
| |
| void TabStrip::OnGroupCreated(const tab_groups::TabGroupId& group) { |
| auto group_view = std::make_unique<TabGroupViews>(this, group); |
| layout_helper_->InsertGroupHeader(group, group_view->header()); |
| group_views_[group] = std::move(group_view); |
| SetStackedLayout(false); |
| } |
| |
| void TabStrip::OnGroupEditorOpened(const tab_groups::TabGroupId& group) { |
| // The context menu relies on a Browser object which is not provided in |
| // TabStripTest. |
| if (this->controller()->GetBrowser()) { |
| group_views_[group]->header()->ShowContextMenuForViewImpl( |
| this, gfx::Point(), ui::MENU_SOURCE_NONE); |
| } |
| } |
| |
| void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) { |
| // The group header may be in the wrong place if the tab didn't actually |
| // move in terms of model indices. |
| OnGroupMoved(group); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::OnGroupVisualsChanged(const tab_groups::TabGroupId& group) { |
| group_views_[group]->OnGroupVisualsChanged(); |
| // The group title may have changed size, so update bounds. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::ToggleTabGroup(const tab_groups::TabGroupId& group, |
| bool is_collapsing, |
| ToggleTabGroupCollapsedStateOrigin origin) { |
| if (is_collapsing && GetWidget()) { |
| in_tab_close_ = true; |
| if (origin == ToggleTabGroupCollapsedStateOrigin::kMouse) { |
| AddMessageLoopObserver(); |
| } else if (origin == ToggleTabGroupCollapsedStateOrigin::kGesture) { |
| StartResizeLayoutTabsFromTouchTimer(); |
| } else { |
| return; |
| } |
| |
| // The current group header is expanded which is slightly smaller than the |
| // size when the header is collapsed. Calculate the size of the header once |
| // collapsed for maintaining its position. See |
| // TabGroupHeader::CalculateWidth() for more details. |
| const int empty_group_title_adjustment = |
| GetGroupTitle(group).empty() ? 2 : -2; |
| const int title_chip_width = |
| group_views_[group]->header()->GetTabSizeInfo().standard_width - |
| 2 * TabStyle::GetTabOverlap() - empty_group_title_adjustment; |
| const int collapsed_header_width = |
| title_chip_width + 2 * TabGroupUnderline::GetStrokeInset(); |
| override_available_width_for_tabs_ = |
| ideal_bounds(GetModelCount() - 1).right() - |
| group_views_[group]->GetBounds().width() + collapsed_header_width; |
| } else { |
| ExitTabClosingMode(); |
| } |
| } |
| |
| void TabStrip::OnGroupMoved(const tab_groups::TabGroupId& group) { |
| DCHECK(group_views_[group]); |
| |
| layout_helper_->UpdateGroupHeaderIndex(group); |
| |
| TabGroupHeader* group_header = group_views_[group]->header(); |
| const int first_tab = controller_->GetFirstTabInGroup(group).value(); |
| const int header_index = GetIndexOf(group_header); |
| const int first_tab_index = GetIndexOf(tab_at(first_tab)); |
| |
| // The header should be just before the first tab. If it isn't, reorder the |
| // header such that it is. Note that the index to reorder to is different |
| // depending on whether the header is before or after the tab, since the |
| // header itself occupies an index. |
| if (header_index < first_tab_index - 1) |
| ReorderChildView(group_header, first_tab_index - 1); |
| if (header_index > first_tab_index - 1) |
| ReorderChildView(group_header, first_tab_index); |
| } |
| |
| void TabStrip::OnGroupClosed(const tab_groups::TabGroupId& group) { |
| bounds_animator_.StopAnimatingView(group_header(group)); |
| layout_helper_->RemoveGroupHeader(group); |
| |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| group_views_.erase(group); |
| } |
| |
| void TabStrip::ShiftGroupLeft(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, -1); |
| } |
| |
| void TabStrip::ShiftGroupRight(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, 1); |
| } |
| |
| bool TabStrip::ShouldTabBeVisible(const Tab* tab) const { |
| // When the tabstrip is scrollable, it can grow to accommodate any number of |
| // tabs, so tabs can never become clipped. |
| // N.B. Tabs can still be not-visible because they're in a collapsed group, |
| // but that's handled elsewhere. |
| // N.B. This is separate from the tab being potentially scrolled offscreen - |
| // this solely determines whether the tab should be clipped for the |
| // pre-scrolling overflow behavior. |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) |
| return true; |
| |
| // 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 would be clipped by the trailing edge of the strip, even if the |
| // tabstrip were resized to its greatest possible width, it shouldn't be |
| // visible. |
| const int right_edge = tab->bounds().right(); |
| const int tabstrip_right = CalculateAvailableWidthForTabs(); |
| if (right_edge > tabstrip_right) |
| 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. |
| |
| // Pinned tabs don't change size when activated, so any tab in the pinned tab |
| // region is safe. |
| if (tab->data().pinned) |
| return true; |
| |
| // If the active tab is on or before this tab, we're safe. |
| if (controller_->GetActiveIndex() <= GetModelIndexOf(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 + GetActiveTabWidth() - GetInactiveTabWidth()) <= |
| tabstrip_right; |
| } |
| |
| bool TabStrip::ShouldDrawStrokes() const { |
| // If the controller says we can't draw strokes, don't. |
| if (!controller_->CanDrawStrokes()) |
| return false; |
| |
| // The tabstrip normally avoids strokes and relies on the active tab |
| // contrasting sufficiently with the frame background. When there isn't |
| // enough contrast, fall back to a stroke. Always compute the contrast ratio |
| // against the active frame color, to avoid toggling the stroke on and off as |
| // the window activation state changes. |
| constexpr float kMinimumContrastRatioForOutlines = 1.3f; |
| const SkColor background_color = GetTabBackgroundColor( |
| TabActive::kActive, BrowserFrameActiveState::kActive); |
| const SkColor frame_color = |
| controller_->GetFrameColor(BrowserFrameActiveState::kActive); |
| const float contrast_ratio = |
| color_utils::GetContrastRatio(background_color, frame_color); |
| if (contrast_ratio < kMinimumContrastRatioForOutlines) |
| return true; |
| |
| // Don't want to have to run a full feature query every time this function is |
| // called. |
| static const bool tab_outlines_in_low_contrast = |
| base::FeatureList::IsEnabled(features::kTabOutlinesInLowContrastThemes); |
| if (tab_outlines_in_low_contrast) { |
| constexpr float kMinimumAbsoluteContrastForOutlines = 0.2f; |
| const float background_luminance = |
| color_utils::GetRelativeLuminance(background_color); |
| const float frame_luminance = |
| color_utils::GetRelativeLuminance(frame_color); |
| const float contrast_difference = |
| std::fabs(background_luminance - frame_luminance); |
| if (contrast_difference < kMinimumAbsoluteContrastForOutlines) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void TabStrip::SetSelection(const ui::ListSelectionModel& new_selection) { |
| DCHECK_GE(new_selection.active(), 0) |
| << "We should never transition to a state where no tab is active."; |
| Tab* const new_active_tab = tab_at(new_selection.active()); |
| Tab* const old_active_tab = |
| selected_tabs_.active() >= 0 ? tab_at(selected_tabs_.active()) : nullptr; |
| |
| if (new_active_tab != old_active_tab) { |
| if (old_active_tab) { |
| old_active_tab->ActiveStateChanged(); |
| if (old_active_tab->group().has_value()) |
| UpdateTabGroupVisuals(old_active_tab->group().value()); |
| } |
| if (new_active_tab->group().has_value()) { |
| const tab_groups::TabGroupId new_group = new_active_tab->group().value(); |
| // If the tab that is about to be activated is in a collapsed group, |
| // automatically expand the group. |
| if (controller()->IsGroupCollapsed(new_group)) |
| controller()->ToggleTabGroupCollapsedState( |
| new_group, ToggleTabGroupCollapsedStateOrigin::kImplicitAction); |
| UpdateTabGroupVisuals(new_group); |
| } |
| |
| new_active_tab->ActiveStateChanged(); |
| layout_helper_->SetActiveTab(selected_tabs_.active(), |
| new_selection.active()); |
| } |
| |
| 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 { |
| if (GetActiveTabWidth() == GetInactiveTabWidth()) { |
| // When tabs are wide enough, selecting a new tab cannot change the |
| // ideal bounds, so only a repaint is necessary. |
| SchedulePaint(); |
| } else if (IsAnimating()) { |
| // The selection change will have modified the ideal bounds of the tabs |
| // in |selected_tabs_| and |new_selection|. We need to recompute. |
| // Note: This is safe even if we're in the midst of mouse-based tab |
| // closure--we won't expand the tabstrip back to the full window |
| // width--because PrepareForCloseAt() will have set |
| // |override_available_width_for_tabs_| already. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } else { |
| // As in the animating case above, the selection change will have |
| // affected the desired bounds of the tabs, but since we're not animating |
| // we can just snap to the new bounds. |
| CompleteAnimationAndLayout(); |
| } |
| } |
| |
| // 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<ui::ListSelectionModel::SelectedIndices>( |
| selected_tabs_.selected_indices(), new_selection.selected_indices()); |
| ui::ListSelectionModel::SelectedIndices newly_selected = |
| base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>( |
| new_selection.selected_indices(), selected_tabs_.selected_indices()); |
| |
| new_active_tab->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); |
| selected_tabs_ = new_selection; |
| |
| UpdateHoverCard(nullptr); |
| // The hover cards seen count is reset when the active tab is changed by any |
| // event. Note TabStrip::SelectTab does not capture tab changes triggered by |
| // the keyboard. |
| if (base::FeatureList::IsEnabled(features::kTabHoverCards) && hover_card_) |
| hover_card_->reset_hover_cards_seen_count(); |
| |
| // Notify all tabs whose selected state changed. |
| for (auto tab_index : |
| base::STLSetUnion<ui::ListSelectionModel::SelectedIndices>( |
| no_longer_selected, newly_selected)) { |
| tab_at(tab_index)->SelectedStateChanged(); |
| } |
| } |
| |
| void TabStrip::OnWidgetActivationChanged(views::Widget* widget, bool active) { |
| if (active && selected_tabs_.active() >= 0) { |
| // When the browser window is activated, fire a selection event on the |
| // currently active tab, to help enable per-tab modes in assistive |
| // technologies. |
| tab_at(selected_tabs_.active()) |
| ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); |
| } |
| } |
| |
| void TabStrip::SetTabNeedsAttention(int model_index, bool attention) { |
| tab_at(model_index)->SetTabNeedsAttention(attention); |
| } |
| |
| const gfx::Rect& TabStrip::ideal_bounds(tab_groups::TabGroupId group) const { |
| return layout_helper_->group_header_ideal_bounds().at(group); |
| } |
| |
| int TabStrip::GetModelIndexOf(const TabSlotView* view) const { |
| return tabs_.GetIndexOfView(view); |
| } |
| |
| int TabStrip::GetTabCount() const { |
| return tabs_.view_size(); |
| } |
| |
| int TabStrip::GetModelCount() const { |
| return controller_->GetCount(); |
| } |
| |
| bool TabStrip::IsValidModelIndex(int model_index) const { |
| return controller_->IsValidIndex(model_index); |
| } |
| |
| TabDragContext* TabStrip::GetDragContext() { |
| return drag_context_.get(); |
| } |
| |
| int TabStrip::GetPinnedTabCount() const { |
| return layout_helper_->GetPinnedTabCount(); |
| } |
| |
| bool TabStrip::IsAnimating() const { |
| return bounds_animator_.IsAnimating(); |
| } |
| |
| void TabStrip::StopAnimating(bool layout) { |
| if (!IsAnimating()) |
| return; |
| |
| bounds_animator_.Cancel(); |
| |
| if (layout) |
| CompleteAnimationAndLayout(); |
| } |
| |
| base::Optional<int> TabStrip::GetFocusedTabIndex() const { |
| for (int i = 0; i < tabs_.view_size(); ++i) { |
| if (tabs_.view_at(i)->HasFocus()) |
| return i; |
| } |
| return base::nullopt; |
| } |
| |
| views::View* TabStrip::GetTabViewForPromoAnchor(int index_hint) { |
| return tab_at(base::ClampToRange(index_hint, 0, GetTabCount() - 1)); |
| } |
| |
| views::View* TabStrip::GetDefaultFocusableChild() { |
| int active = controller_->GetActiveIndex(); |
| return active != TabStripModel::kNoTab ? tab_at(active) : nullptr; |
| } |
| |
| const ui::ListSelectionModel& TabStrip::GetSelectionModel() const { |
| return controller_->GetSelectionModel(); |
| } |
| |
| bool TabStrip::SupportsMultipleSelection() { |
| // Currently we only allow single selection in touch layout mode. |
| return touch_layout_ == nullptr; |
| } |
| |
| bool TabStrip::ShouldHideCloseButtonForTab(Tab* tab) const { |
| if (tab->IsActive()) |
| return false; |
| return !!touch_layout_; |
| } |
| |
| void TabStrip::SelectTab(Tab* tab, const ui::Event& event) { |
| int model_index = GetModelIndexOf(tab); |
| |
| if (IsValidModelIndex(model_index)) { |
| if (!tab->IsActive()) { |
| int current_selection = selected_tabs_.active(); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetOfSwitch", |
| current_selection - model_index); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromLeftOfSwitch", |
| model_index); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromRightOfSwitch", |
| GetModelCount() - model_index - 1); |
| |
| if (tab->group().has_value()) { |
| base::RecordAction( |
| base::UserMetricsAction("TabGroups_SwitchGroupedTab")); |
| } |
| } |
| |
| // Report histogram metrics for the number of tab hover cards seen before |
| // a tab is selected by mouse press. |
| if (base::FeatureList::IsEnabled(features::kTabHoverCards) && hover_card_ && |
| event.type() == ui::ET_MOUSE_PRESSED && !tab->IsActive()) { |
| hover_card_->RecordHoverCardsSeenRatioMetric(); |
| } |
| |
| controller_->SelectTab(model_index, event); |
| } |
| } |
| |
| void TabStrip::ExtendSelectionTo(Tab* tab) { |
| int model_index = GetModelIndexOf(tab); |
| if (IsValidModelIndex(model_index)) |
| controller_->ExtendSelectionTo(model_index); |
| } |
| |
| void TabStrip::ToggleSelected(Tab* tab) { |
| int model_index = GetModelIndexOf(tab); |
| if (IsValidModelIndex(model_index)) |
| controller_->ToggleSelected(model_index); |
| } |
| |
| void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { |
| int model_index = GetModelIndexOf(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. |
| std::vector<Tab*> all_tabs = layout_helper_->GetTabs(); |
| auto it = std::find(all_tabs.begin(), all_tabs.end(), tab); |
| while (it < all_tabs.end() && (*it)->closing()) { |
| it++; |
| } |
| |
| if (it == all_tabs.end()) |
| return; |
| tab = *it; |
| } |
| |
| CloseTabInternal(GetModelIndexOf(tab), source); |
| } |
| |
| void TabStrip::ShiftTabNext(Tab* tab) { |
| ShiftTabRelative(tab, 1); |
| } |
| |
| void TabStrip::ShiftTabPrevious(Tab* tab) { |
| ShiftTabRelative(tab, -1); |
| } |
| |
| void TabStrip::MoveTabFirst(Tab* tab) { |
| if (tab->closing()) |
| return; |
| |
| const int start_index = GetModelIndexOf(tab); |
| if (!IsValidModelIndex(start_index)) |
| return; |
| |
| int target_index = 0; |
| if (!controller_->IsTabPinned(start_index)) { |
| while (target_index < start_index && controller_->IsTabPinned(target_index)) |
| ++target_index; |
| } |
| |
| if (!IsValidModelIndex(target_index)) |
| return; |
| |
| if (target_index != start_index) |
| controller_->MoveTab(start_index, target_index); |
| |
| // The tab may unintentionally land in the first group in the tab strip, so we |
| // remove the group to ensure consistent behavior. Even if the tab is already |
| // at the front, it should "move" out of its current group. |
| if (tab->group().has_value()) |
| controller_->RemoveTabFromGroup(target_index); |
| |
| GetViewAccessibility().AnnounceText( |
| l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_FIRST)); |
| } |
| |
| void TabStrip::MoveTabLast(Tab* tab) { |
| if (tab->closing()) |
| return; |
| |
| const int start_index = GetModelIndexOf(tab); |
| if (!IsValidModelIndex(start_index)) |
| return; |
| |
| int target_index; |
| if (controller_->IsTabPinned(start_index)) { |
| int temp_index = start_index + 1; |
| while (temp_index < GetTabCount() && controller_->IsTabPinned(temp_index)) |
| ++temp_index; |
| target_index = temp_index - 1; |
| } else { |
| target_index = GetTabCount() - 1; |
| } |
| |
| if (!IsValidModelIndex(target_index)) |
| return; |
| |
| if (target_index != start_index) |
| controller_->MoveTab(start_index, target_index); |
| |
| // The tab may unintentionally land in the last group in the tab strip, so we |
| // remove the group to ensure consistent behavior. Even if the tab is already |
| // at the back, it should "move" out of its current group. |
| if (tab->group().has_value()) |
| controller_->RemoveTabFromGroup(target_index); |
| |
| GetViewAccessibility().AnnounceText( |
| l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_LAST)); |
| } |
| |
| 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 = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsActiveTab(model_index); |
| } |
| |
| bool TabStrip::IsTabSelected(const Tab* tab) const { |
| int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsTabSelected(model_index); |
| } |
| |
| bool TabStrip::IsTabPinned(const Tab* tab) const { |
| if (tab->closing()) |
| return false; |
| |
| int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsTabPinned(model_index); |
| } |
| |
| bool TabStrip::IsTabFirst(const Tab* tab) const { |
| return GetModelIndexOf(tab) == 0; |
| } |
| |
| bool TabStrip::IsFocusInTabs() const { |
| return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView()); |
| } |
| |
| void TabStrip::MaybeStartDrag( |
| TabSlotView* source, |
| 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() || controller_->HasAvailableDragActions() == 0) |
| return; |
| |
| // Check that the source is either a valid tab or a tab group header, which |
| // are the only valid drag targets. |
| if (!IsValidModelIndex(GetModelIndexOf(source))) { |
| DCHECK_EQ(source->GetTabSlotViewType(), |
| TabSlotView::ViewType::kTabGroupHeader); |
| } |
| |
| drag_context_->MaybeStartDrag(source, event, original_selection); |
| } |
| |
| void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) { |
| drag_context_->ContinueDrag(view, event); |
| } |
| |
| bool TabStrip::EndDrag(EndDragReason reason) { |
| return drag_context_->EndDrag(reason); |
| } |
| |
| Tab* TabStrip::GetTabAt(const gfx::Point& point) { |
| views::View* view = GetEventHandlerForPoint(point); |
| if (!view) |
| return nullptr; // No tab contains the point. |
| |
| // Walk up the view hierarchy until we find a tab, or the TabStrip. |
| while (view && view != this && view->GetID() != VIEW_ID_TAB) |
| view = view->parent(); |
| |
| return view && view->GetID() == VIEW_ID_TAB ? static_cast<Tab*>(view) |
| : nullptr; |
| } |
| |
| const Tab* TabStrip::GetAdjacentTab(const Tab* tab, int offset) { |
| int index = GetModelIndexOf(tab); |
| if (index < 0) |
| return nullptr; |
| index += offset; |
| return IsValidModelIndex(index) ? tab_at(index) : nullptr; |
| } |
| |
| void TabStrip::OnMouseEventInTab(views::View* source, |
| const ui::MouseEvent& event) { |
| // Record time from cursor entering the tabstrip to first tap on a tab to |
| // switch. |
| if (mouse_entered_tabstrip_time_.has_value() && |
| event.type() == ui::ET_MOUSE_PRESSED && views::IsViewClass<Tab>(source)) { |
| UMA_HISTOGRAM_MEDIUM_TIMES( |
| "TabStrip.TimeToSwitch", |
| base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value()); |
| mouse_entered_tabstrip_time_.reset(); |
| } |
| UpdateStackedLayoutFromMouseEvent(source, event); |
| } |
| |
| void TabStrip::UpdateHoverCard(Tab* tab) { |
| if (!base::FeatureList::IsEnabled(features::kTabHoverCards)) |
| return; |
| // We don't want to show a hover card while the tabstrip is animating. |
| if (bounds_animator_.IsAnimating()) { |
| return; |
| } |
| |
| if (!hover_card_) { |
| // There is nothing to be done if the hover card doesn't exist and we are |
| // not trying to show it. |
| if (!tab) |
| return; |
| hover_card_ = new TabHoverCardBubbleView(tab); |
| hover_card_observation_.Observe(hover_card_); |
| if (GetWidget()) { |
| hover_card_event_sniffer_ = |
| std::make_unique<TabHoverCardEventSniffer>(hover_card_, this); |
| } |
| } |
| if (tab) |
| hover_card_->UpdateAndShow(tab); |
| else |
| hover_card_->FadeOutToHide(); |
| } |
| |
| bool TabStrip::ShowDomainInHoverCards() const { |
| const auto* app_controller = controller_->GetBrowser()->app_controller(); |
| return !app_controller || !app_controller->is_for_system_web_app(); |
| } |
| |
| bool TabStrip::HoverCardIsShowingForTab(Tab* tab) { |
| if (!base::FeatureList::IsEnabled(features::kTabHoverCards)) |
| return false; |
| |
| return hover_card_ && hover_card_->GetWidget()->IsVisible() && |
| !hover_card_->IsFadingOut() && |
| hover_card_->GetDesiredAnchorView() == tab; |
| } |
| |
| int TabStrip::GetBackgroundOffset() const { |
| return background_offset_; |
| } |
| |
| int TabStrip::GetStrokeThickness() const { |
| return ShouldDrawStrokes() ? 1 : 0; |
| } |
| |
| bool TabStrip::CanPaintThrobberToLayer() const { |
| // Disable layer-painting of throbbers if dragging, if any tab animation is in |
| // progress, or if stacked tabs are enabled. Also disable in fullscreen: when |
| // "immersive" the tab strip could be sliding in or out; for other modes, |
| // there's no tab strip. |
| const bool dragging = drag_context_->IsDragStarted(); |
| const views::Widget* widget = GetWidget(); |
| return widget && !touch_layout_ && !dragging && !IsAnimating() && |
| !widget->IsFullscreen(); |
| } |
| |
| bool TabStrip::HasVisibleBackgroundTabShapes() const { |
| return controller_->HasVisibleBackgroundTabShapes(); |
| } |
| |
| bool TabStrip::ShouldPaintAsActiveFrame() const { |
| return controller_->ShouldPaintAsActiveFrame(); |
| } |
| |
| SkColor TabStrip::GetToolbarTopSeparatorColor() const { |
| return controller_->GetToolbarTopSeparatorColor(); |
| } |
| |
| SkColor TabStrip::GetTabSeparatorColor() const { |
| return separator_color_; |
| } |
| |
| SkColor TabStrip::GetTabBackgroundColor( |
| TabActive active, |
| BrowserFrameActiveState active_state) const { |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| if (!tp) |
| return SK_ColorBLACK; |
| |
| constexpr int kColorIds[2][2] = { |
| {ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_ACTIVE}, |
| {ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_ACTIVE}}; |
| |
| using State = BrowserFrameActiveState; |
| const bool tab_active = active == TabActive::kActive; |
| const bool frame_active = |
| (active_state == State::kActive) || |
| ((active_state == State::kUseCurrent) && ShouldPaintAsActiveFrame()); |
| return tp->GetColor(kColorIds[tab_active][frame_active]); |
| } |
| |
| SkColor TabStrip::GetTabForegroundColor(TabActive active, |
| SkColor background_color) const { |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| if (!tp) |
| return SK_ColorBLACK; |
| |
| constexpr int kColorIds[2][2] = { |
| {ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE}, |
| {ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE}}; |
| |
| const bool tab_active = active == TabActive::kActive; |
| const bool frame_active = ShouldPaintAsActiveFrame(); |
| const int color_id = kColorIds[tab_active][frame_active]; |
| |
| SkColor color = tp->GetColor(color_id); |
| if (tp->HasCustomColor(color_id)) |
| return color; |
| if ((color_id == |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE) && |
| tp->HasCustomColor( |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE)) { |
| // If a custom theme sets a background tab text color for active but not |
| // inactive windows, generate the inactive color by blending the active one |
| // at 75% as we do in the default theme. |
| color = tp->GetColor( |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE); |
| } |
| |
| if (!frame_active) |
| color = color_utils::AlphaBlend(color, background_color, 0.75f); |
| |
| // To minimize any readability cost of custom system frame colors, try to make |
| // the text reach the same contrast ratio that it would in the default theme. |
| const SkColor target = color_utils::GetColorWithMaxContrast(background_color); |
| // These contrast ratios should match the actual ratios in the default theme |
| // colors when no system colors are involved, except for the inactive tab/ |
| // inactive frame case, which has been raised from 4.48 to 4.5 to meet |
| // accessibility guidelines. |
| constexpr float kContrast[2][2] = {{4.5f, // Inactive tab, inactive frame |
| 7.98f}, // Inactive tab, active frame |
| {5.0f, // Active tab, inactive frame |
| 10.46f}}; // Active tab, active frame |
| const float contrast = kContrast[tab_active][frame_active]; |
| return color_utils::BlendForMinContrast(color, background_color, target, |
| contrast) |
| .color; |
| } |
| |
| // Returns the accessible tab name for the tab. |
| base::string16 TabStrip::GetAccessibleTabName(const Tab* tab) const { |
| const int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) ? controller_->GetAccessibleTabName(tab) |
| : base::string16(); |
| } |
| |
| base::Optional<int> TabStrip::GetCustomBackgroundId( |
| BrowserFrameActiveState active_state) const { |
| if (!TitlebarBackgroundIsTransparent()) |
| return controller_->GetCustomBackgroundId(active_state); |
| |
| constexpr int kBackgroundIdGlass = IDR_THEME_TAB_BACKGROUND_V; |
| return GetThemeProvider()->HasCustomImage(kBackgroundIdGlass) |
| ? base::make_optional(kBackgroundIdGlass) |
| : base::nullopt; |
| } |
| |
| gfx::Rect TabStrip::GetTabAnimationTargetBounds(const Tab* tab) { |
| return bounds_animator_.GetTargetBounds(tab); |
| } |
| |
| void TabStrip::MouseMovedOutOfHost() { |
| ResizeLayoutTabs(); |
| if (reset_to_shrink_on_exit_) { |
| reset_to_shrink_on_exit_ = false; |
| SetStackedLayout(false); |
| controller_->StackedLayoutMaybeChanged(); |
| } |
| } |
| |
| float TabStrip::GetHoverOpacityForTab(float range_parameter) const { |
| return gfx::Tween::FloatValueBetween(range_parameter, hover_opacity_min_, |
| hover_opacity_max_); |
| } |
| |
| float TabStrip::GetHoverOpacityForRadialHighlight() const { |
| return radial_highlight_opacity_; |
| } |
| |
| base::string16 TabStrip::GetGroupTitle( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupTitle(group); |
| } |
| |
| tab_groups::TabGroupColorId TabStrip::GetGroupColorId( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupColorId(group); |
| } |
| |
| SkColor TabStrip::GetPaintedGroupColor( |
| const tab_groups::TabGroupColorId& color_id) const { |
| return GetThemeProvider()->GetColor( |
| GetTabGroupTabStripColorId(color_id, ShouldPaintAsActiveFrame())); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, views::AccessiblePaneView overrides: |
| |
| void TabStrip::Layout() { |
| if (IsAnimating()) { |
| // Hide tabs that have animated at least partially out of the clip region. |
| SetTabSlotVisibility(); |
| return; |
| } |
| |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { |
| // With tab scrolling, the tabstrip is solely responsible for its own |
| // width. |
| // It should never be larger than its preferred width. |
| const int max_width = CalculatePreferredSize().width(); |
| // It should never be smaller than its minimum width. |
| const int min_width = GetMinimumSize().width(); |
| // If it can, it should fit within the tab strip region. |
| const int available_width = available_width_callback_.Run(); |
| // It should be as wide as possible subject to the above constraints. |
| const int width = std::min(max_width, std::max(min_width, available_width)); |
| SetBounds(0, 0, width, GetLayoutConstant(TAB_HEIGHT)); |
| SetTabSlotVisibility(); |
| } |
| |
| // Only do a layout if our size or the available width changed. |
| const int available_width = GetAvailableWidthForTabStrip(); |
| if (last_layout_size_ == size() && last_available_width_ == available_width) |
| return; |
| if (drag_context_->IsDragSessionActive()) |
| return; |
| CompleteAnimationAndLayout(); |
| } |
| |
| void TabStrip::PaintChildren(const views::PaintInfo& paint_info) { |
| // This is used to log to UMA. NO EARLY RETURNS! |
| base::ElapsedTimer paint_timer; |
| |
| // The view order doesn't match the paint order (layout_helper_ contains the |
| // view ordering). |
| bool is_dragging = false; |
| Tab* active_tab = nullptr; |
| std::vector<Tab*> tabs_dragging; |
| std::vector<Tab*> selected_and_hovered_tabs; |
| |
| // When background tab shapes are visible, as for hovered or selected tabs, |
| // the paint order must be handled carefully to avoid Z-order errors, so |
| // this code defers drawing such tabs until later. |
| const auto paint_or_add_to_tabs = [&paint_info, |
| &selected_and_hovered_tabs](Tab* tab) { |
| if (tab->tab_style()->GetZValue() > 0.0) { |
| selected_and_hovered_tabs.push_back(tab); |
| } else { |
| tab->Paint(paint_info); |
| } |
| }; |
| |
| std::vector<Tab*> all_tabs = layout_helper_->GetTabs(); |
| |
| int active_tab_index = -1; |
| for (int i = all_tabs.size() - 1; i >= 0; --i) { |
| Tab* tab = all_tabs[i]; |
| 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()) { |
| active_tab = tab; |
| active_tab_index = i; |
| } else if (!stacked_layout_) { |
| paint_or_add_to_tabs(tab); |
| } |
| } |
| |
| // 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 = all_tabs[i]; |
| tab->Paint(paint_info); |
| } |
| |
| for (int i = all_tabs.size() - 1; i > active_tab_index; --i) { |
| Tab* tab = all_tabs[i]; |
| tab->Paint(paint_info); |
| } |
| } |
| |
| std::stable_sort(selected_and_hovered_tabs.begin(), |
| selected_and_hovered_tabs.end(), [](Tab* tab1, Tab* tab2) { |
| return tab1->tab_style()->GetZValue() < |
| tab2->tab_style()->GetZValue(); |
| }); |
| for (Tab* tab : selected_and_hovered_tabs) |
| tab->Paint(paint_info); |
| |
| // Keep track of the dragging group if dragging by the group header, or |
| // the current group if just dragging tabs into a group. At most one of these |
| // will have a value, since a drag is either a group drag or a tab drag. |
| base::Optional<tab_groups::TabGroupId> dragging_group = base::nullopt; |
| base::Optional<tab_groups::TabGroupId> current_group = base::nullopt; |
| |
| // Paint group headers and underlines. |
| for (const auto& group_view_pair : group_views_) { |
| if (group_view_pair.second->header()->dragging()) { |
| // If the whole group is dragging, defer painting both the header and the |
| // underline, since they should appear above non-dragging tabs and groups. |
| // Instead, just track the dragging group. |
| dragging_group = group_view_pair.first; |
| } else { |
| group_view_pair.second->header()->Paint(paint_info); |
| |
| if (tabs_dragging.size() > 0 && |
| tabs_dragging[0]->group() == group_view_pair.first) { |
| // If tabs are being dragged into a group, defer painting just the |
| // underline, which should appear above non-active dragging tabs as well |
| // as all non-dragging tabs and groups. Instead, just track the group |
| // that the tabs are being dragged into. |
| current_group = group_view_pair.first; |
| } else { |
| group_view_pair.second->underline()->Paint(paint_info); |
| } |
| } |
| } |
| |
| // Always paint the active tab over all the inactive tabs. |
| if (active_tab && !is_dragging) |
| active_tab->Paint(paint_info); |
| |
| // If dragging a group, paint the group highlight and header above all |
| // non-dragging tabs and groups. |
| if (dragging_group.has_value()) { |
| group_views_[dragging_group.value()]->highlight()->Paint(paint_info); |
| group_views_[dragging_group.value()]->header()->Paint(paint_info); |
| } |
| |
| // Paint the dragged tabs. |
| for (size_t i = 0; i < tabs_dragging.size(); ++i) |
| tabs_dragging[i]->Paint(paint_info); |
| |
| // If dragging a group, or dragging tabs into a group, paint the group |
| // underline above the dragging tabs. Otherwise, any non-active dragging tabs |
| // will not get an underline. |
| if (dragging_group.has_value()) |
| group_views_[dragging_group.value()]->underline()->Paint(paint_info); |
| if (current_group.has_value()) |
| group_views_[current_group.value()]->underline()->Paint(paint_info); |
| |
| // If the active tab is being dragged, it goes last. |
| if (active_tab && is_dragging) |
| active_tab->Paint(paint_info); |
| |
| UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES( |
| "TabStrip.PaintChildrenDuration", paint_timer.Elapsed(), |
| base::TimeDelta::FromMicroseconds(1), |
| base::TimeDelta::FromMicroseconds(10000), 50); |
| } |
| |
| gfx::Size TabStrip::GetMinimumSize() const { |
| // If tabs can be stacked, our minimum width is the smallest width of the |
| // stacked tabstrip. |
| const int minimum_width = |
| (touch_layout_ || adjust_layout_) |
| ? GetStackableTabWidth() + (2 * kStackedPadding * kMaxStackedCount) |
| : layout_helper_->CalculateMinimumWidth(); |
| |
| return gfx::Size(minimum_width, GetLayoutConstant(TAB_HEIGHT)); |
| } |
| |
| gfx::Size TabStrip::CalculatePreferredSize() const { |
| int preferred_width; |
| // The tabstrip needs to always exactly fit the bounds of the tabs so that |
| // NTB can be laid out just to the right of the rightmost tab. When the tabs |
| // aren't at their ideal bounds (i.e. during animation or a drag), we need to |
| // size ourselves to exactly fit wherever the tabs *currently* are. |
| if (IsAnimating() || drag_context_->IsDragSessionActive()) { |
| // The visual order of the tabs can be out of sync with the logical order, |
| // so we have to check all of them to find the visually trailing-most one. |
| int max_x = 0; |
| for (auto* tab : layout_helper_->GetTabs()) { |
| max_x = std::max(max_x, tab->bounds().right()); |
| } |
| // The tabs span from 0 to |max_x|, so |max_x| is the current width |
| // occupied by tabs. We report the current width as our preferred width so |
| // that the tab strip is sized to exactly fit the current position of the |
| // tabs. |
| preferred_width = max_x; |
| } else { |
| preferred_width = override_available_width_for_tabs_ |
| ? override_available_width_for_tabs_.value() |
| : layout_helper_->CalculatePreferredWidth(); |
| } |
| |
| return gfx::Size(preferred_width, GetLayoutConstant(TAB_HEIGHT)); |
| } |
| |
| views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) { |
| if (!HitTestPoint(point)) |
| return nullptr; |
| |
| 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 && !views::IsViewClass<Tab>(v)) |
| return v; |
| |
| views::View* tab = FindTabHitByPoint(point); |
| if (tab) |
| return tab; |
| } else { |
| Tab* tab = FindTabForEvent(point); |
| if (tab) |
| return ConvertPointToViewAndGetTooltipHandler(this, tab, point); |
| } |
| return this; |
| } |
| |
| BrowserRootView::DropIndex TabStrip::GetDropIndex( |
| const ui::DropTargetEvent& event) { |
| // Force animations to stop, otherwise it makes the index calculation tricky. |
| StopAnimating(true); |
| |
| // 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()); |
| for (int i = 0; i < GetTabCount(); ++i) { |
| Tab* tab = tab_at(i); |
| const int tab_max_x = tab->x() + tab->width(); |
| |
| // When hovering over the left or right quarter of a tab, the drop indicator |
| // will point between tabs. |
| const int hot_width = tab->width() / 4; |
| |
| if (x < tab_max_x) { |
| if (x >= (tab_max_x - hot_width)) |
| return {i + 1, true}; |
| return {i, x < tab->x() + hot_width}; |
| } |
| } |
| |
| // The drop isn't over a tab, add it to the end. |
| return {GetTabCount(), true}; |
| } |
| |
| views::View* TabStrip::GetViewForDrop() { |
| return this; |
| } |
| |
| void TabStrip::HandleDragUpdate( |
| const base::Optional<BrowserRootView::DropIndex>& index) { |
| SetDropArrow(index); |
| } |
| |
| void TabStrip::HandleDragExited() { |
| SetDropArrow({}); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, private: |
| |
| void TabStrip::Init() { |
| SetID(VIEW_ID_TAB_STRIP); |
| // So we get enter/exit on children to switch stacked layout on and off. |
| SetNotifyEnterExitOnChild(true); |
| |
| if (g_drop_indicator_width == 0) { |
| // Direction doesn't matter, both images are the same size. |
| gfx::ImageSkia* drop_image = GetDropArrowImage(true); |
| g_drop_indicator_width = drop_image->width(); |
| g_drop_indicator_height = drop_image->height(); |
| } |
| |
| UpdateContrastRatioValues(); |
| |
| if (!gfx::Animation::ShouldRenderRichAnimation()) |
| bounds_animator_.SetAnimationDuration(base::TimeDelta()); |
| } |
| |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> TabStrip::GetGroupHeaders() { |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> group_headers; |
| for (const auto& group_view_pair : group_views_) { |
| group_headers.insert(std::make_pair(group_view_pair.first, |
| group_view_pair.second->header())); |
| } |
| return group_headers; |
| } |
| |
| void TabStrip::NewTabButtonPressed(const ui::Event& event) { |
| base::RecordAction(base::UserMetricsAction("NewTab_Button")); |
| UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON, |
| TabStripModel::NEW_TAB_ENUM_COUNT); |
| if (event.IsMouseEvent()) { |
| // Prevent the hover card from popping back in immediately. This forces a |
| // normal fade-in. |
| if (hover_card_) |
| hover_card_->set_last_mouse_exit_timestamp(base::TimeTicks()); |
| |
| const ui::MouseEvent& mouse = static_cast<const ui::MouseEvent&>(event); |
| if (mouse.IsOnlyMiddleMouseButton()) { |
| if (ui::Clipboard::IsSupportedClipboardBuffer( |
| ui::ClipboardBuffer::kSelection)) { |
| ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); |
| CHECK(clipboard); |
| base::string16 clipboard_text; |
| clipboard->ReadText(ui::ClipboardBuffer::kSelection, |
| /* data_dst = */ nullptr, &clipboard_text); |
| if (!clipboard_text.empty()) |
| controller_->CreateNewTabWithLocation(clipboard_text); |
| } |
| return; |
| } |
| } |
| |
| controller_->CreateNewTab(); |
| if (event.type() == ui::ET_GESTURE_TAP) |
| TouchUMA::RecordGestureAction(TouchUMA::kGestureNewTabTap); |
| } |
| |
| void TabStrip::StartInsertTabAnimation(int model_index, TabPinned pinned) { |
| layout_helper_->InsertTabAt(model_index, tab_at(model_index), pinned); |
| |
| PrepareForAnimation(); |
| |
| ExitTabClosingMode(); |
| |
| gfx::Rect bounds = tab_at(model_index)->bounds(); |
| bounds.set_height(GetLayoutConstant(TAB_HEIGHT)); |
| |
| // Adjust the starting bounds of the new tab. |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| if (model_index > 0) { |
| // If we have a tab to our left, start at its right edge. |
| bounds.set_x(tab_at(model_index - 1)->bounds().right() - tab_overlap); |
| } else if (model_index + 1 < GetTabCount()) { |
| // Otherwise, if we have a tab to our right, start at its left edge. |
| bounds.set_x(tab_at(model_index + 1)->bounds().x()); |
| } else { |
| NOTREACHED() << "First tab inserted into the tabstrip should not animate."; |
| } |
| |
| // Start at the width of the overlap in order to animate at the same speed |
| // the surrounding tabs are moving, since at this width the subsequent tab |
| // is naturally positioned at the same X coordinate. |
| bounds.set_width(tab_overlap); |
| tab_at(model_index)->SetBoundsRect(bounds); |
| |
| // Animate in to the full width. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::StartRemoveTabAnimation(int model_index, bool was_active) { |
| const int model_count = GetModelCount(); |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| if (in_tab_close_ && model_count > 0 && model_index != model_count) { |
| // The user closed a tab other than the last tab. Set |
| // override_available_width_for_tabs_ so that as the user closes tabs with |
| // the mouse a tab continues to fall under the mouse. |
| int next_active_index = controller_->GetActiveIndex(); |
| DCHECK(IsValidModelIndex(next_active_index)); |
| if (model_index <= next_active_index) { |
| // At this point, model's internal state has already been updated. |
| // |contents| has been detached from model and the active index has been |
| // updated. But the tab for |contents| isn't removed yet. Thus, we need to |
| // fix up next_active_index based on it. |
| next_active_index++; |
| } |
| Tab* next_active_tab = tab_at(next_active_index); |
|