| // Copyright 2017 The Chromium Authors |
| // 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 <array> |
| #include <iterator> |
| #include <limits> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <unordered_set> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.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/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/raw_ptr.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/numerics/safe_conversions.h" |
| #include "base/observer_list.h" |
| #include "base/scoped_observation.h" |
| #include "base/stl_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "base/types/to_address.h" |
| #include "build/build_config.h" |
| #include "chrome/browser/defaults.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/themes/theme_service_factory.h" |
| #include "chrome/browser/ui/browser_element_identifiers.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/alert/tab_alert.h" |
| #include "chrome/browser/ui/tabs/features.h" |
| #include "chrome/browser/ui/tabs/new_tab_grouping_user_data.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_strip_prefs.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/compound_tab_container.h" |
| #include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| #include "chrome/browser/ui/views/tabs/tab_container_impl.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_controller.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_strip_types.h" |
| #include "chrome/browser/ui/views/tabs/z_orderable_tab_container_element.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "components/crash/core/common/crash_key.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 "components/tabs/public/split_tab_id.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" |
| #include "ui/base/interaction/element_identifier.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/base/mojom/menu_source_type.mojom-forward.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/display/display.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/geometry/skia_conversions.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/cascading_property.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/interaction/element_tracker_views.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/view_observer.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/frame_view.h" |
| |
| #if BUILDFLAG(IS_WIN) |
| #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 { |
| |
| ui::mojom::DragEventSource EventSourceFromEvent(const ui::LocatedEvent& event) { |
| return event.IsGestureEvent() ? ui::mojom::DragEventSource::kTouch |
| : ui::mojom::DragEventSource::kMouse; |
| } |
| |
| std::unique_ptr<TabContainer> MakeTabContainer( |
| TabStrip* tab_strip, |
| TabHoverCardController* hover_card_controller, |
| TabDragContext* drag_context) { |
| if (base::FeatureList::IsEnabled(tabs::kSplitTabStrip)) { |
| return std::make_unique<CompoundTabContainer>( |
| *tab_strip, hover_card_controller, drag_context, *tab_strip, tab_strip); |
| } |
| return std::make_unique<TabContainerImpl>( |
| *tab_strip, hover_card_controller, drag_context, *tab_strip, tab_strip); |
| } |
| |
| void UpdateDragEventSourceCrashKey( |
| std::optional<ui::mojom::DragEventSource> event_source) { |
| static crash_reporter::CrashKeyString<8> key("tabdrag-event-source"); |
| if (!event_source.has_value()) { |
| key.Clear(); |
| return; |
| } |
| key.Set(*event_source == ui::mojom::DragEventSource::kTouch ? "touch" |
| : "mouse"); |
| } |
| |
| } // namespace |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip::TabDragContextImpl |
| // |
| class TabStrip::TabDragContextImpl : public TabDragContext, |
| public views::BoundsAnimatorObserver { |
| METADATA_HEADER(TabDragContextImpl, TabDragContext) |
| |
| public: |
| explicit TabDragContextImpl(TabStrip* tab_strip) |
| : tab_strip_(tab_strip), bounds_animator_(this) { |
| SetCanProcessEventsWithinSubtree(false); |
| |
| bounds_animator_.AddObserver(this); |
| } |
| // If a window is closed during a drag session, all our tabs will be taken |
| // from us before our destructor is even called. |
| ~TabDragContextImpl() override = default; |
| |
| gfx::Size CalculatePreferredSize( |
| const views::SizeBounds& available_size) const override { |
| int max_child_x = 0; |
| for (views::View* child : children()) { |
| if (!views::IsViewClass<TabSlotView>(child)) { |
| continue; |
| } |
| max_child_x = std::max(max_child_x, child->bounds().right()); |
| } |
| |
| return gfx::Size(max_child_x, GetLayoutConstant(TAB_HEIGHT)); |
| } |
| |
| bool OnMouseDragged(const ui::MouseEvent& event) override { |
| (void)ContinueDrag(this, event); |
| return true; |
| } |
| |
| void OnMouseReleased(const ui::MouseEvent& event) override { |
| // When using a system DnD session for tab dragging, we might receive the |
| // mouse release event that signals we should end the drag, even though |
| // a different tab strip owns `TabDragController`. `EndDrag()` exits early |
| // if `drag_controller_` is null, so we use this dedicated method to notify |
| // `TabDragController`. |
| if (TabDragController::IsSystemDnDSessionRunning()) { |
| TabDragController::OnSystemDnDEnded(); |
| } else { |
| EndDrag(EndDragReason::kComplete); |
| } |
| } |
| |
| void OnMouseCaptureLost() override { EndDrag(EndDragReason::kCaptureLost); } |
| |
| void OnGestureEvent(ui::GestureEvent* event) override { |
| Liveness tabstrip_alive = Liveness::kAlive; |
| switch (event->type()) { |
| case ui::EventType::kGestureScrollEnd: |
| case ui::EventType::kScrollFlingStart: |
| case ui::EventType::kGestureEnd: |
| EndDrag(EndDragReason::kComplete); |
| break; |
| |
| case ui::EventType::kGestureLongTap: { |
| EndDrag(EndDragReason::kCancel); |
| break; |
| } |
| |
| case ui::EventType::kGestureScrollUpdate: |
| // N.B. !! ContinueDrag may enter a nested run loop !! |
| tabstrip_alive = ContinueDrag(this, *event); |
| break; |
| |
| case ui::EventType::kGestureTapDown: |
| EndDrag(EndDragReason::kCancel); |
| break; |
| |
| default: |
| break; |
| } |
| event->SetHandled(); |
| |
| // If tabstrip was destroyed (during ContinueDrag above), return early to |
| // avoid UAF below. |
| if (tabstrip_alive == Liveness::kDeleted) { |
| return; |
| } |
| |
| // TabDragContext gets event capture as soon as a drag session begins, which |
| // precludes TabStrip from ever getting events like tap or long tap. Forward |
| // this on to TabStrip so it can respond to those events. |
| tab_strip_->OnGestureEvent(event); |
| } |
| |
| bool IsDragStarted() const { |
| return drag_controller_ && drag_controller_->started_drag(); |
| } |
| |
| void TabWasAdded() { |
| if (drag_controller_) { |
| drag_controller_->TabWasAdded(); |
| } |
| } |
| |
| void OnTabWillBeRemoved(content::WebContents* contents) { |
| if (drag_controller_) { |
| drag_controller_->OnTabWillBeRemoved(contents); |
| } |
| } |
| |
| bool CanRemoveTabIfDragging(content::WebContents* contents) const { |
| return drag_controller_ ? drag_controller_->CanRemoveTabDuringDrag(contents) |
| : true; |
| } |
| |
| 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 { |
| // Any groups where all the tabs are selected should get dragged. |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> fully_selected_groups = |
| GetFullySelectedTabGroups(); |
| |
| // TODO(crbug.com/425933884): Look into using just the selected tabs. |
| for (int i = 0; i < GetTabCount(); ++i) { |
| Tab* other_tab = GetTabAt(i); |
| if (tab_strip_->IsTabSelected(other_tab)) { |
| if (other_tab->group().has_value()) { |
| const tab_groups::TabGroupId group = other_tab->group().value(); |
| if (fully_selected_groups.contains(group)) { |
| dragging_views.push_back(fully_selected_groups[group]); |
| fully_selected_groups.erase(group); |
| } |
| } |
| |
| 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).value())) { |
| selection_model = original_selection; |
| } |
| } |
| |
| CHECK(!dragging_views.empty()) |
| << "Dragging views cannot be empty during drag initialization."; |
| CHECK(base::Contains(dragging_views, source)) |
| << "Source must be part of dragging views."; |
| |
| // 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(); |
| |
| CHECK((event.type() == ui::EventType::kMousePressed || |
| event.type() == ui::EventType::kGestureTapDown || |
| event.type() == ui::EventType::kGestureScrollBegin)) |
| << "Event type must be suitable for starting a drag."; |
| |
| drag_controller_ = std::make_unique<TabDragController>(); |
| |
| // !!! Init may delete `drag_controller_` on some platforms. !!! |
| // Init takes capture, which on some platforms may reenter Chrome, the |
| // TabStrip, and the TabDragController, and may end the drag and destroy |
| // `tab_drag_controller_`. If Init returns DELETED, then `drag_controller_` |
| // is nullptr or it points to a *different instance*. |
| if (drag_controller_->Init(this, source, dragging_views, gfx::Point(x, y), |
| event.x(), std::move(selection_model), |
| EventSourceFromEvent(event)) == |
| TabDragController::Liveness::DELETED) { |
| return; |
| } |
| |
| UpdateDragEventSourceCrashKey(drag_controller_->event_source()); |
| |
| if (drag_controller_set_callback_) { |
| std::move(drag_controller_set_callback_).Run(drag_controller_.get()); |
| } |
| } |
| |
| [[nodiscard]] Liveness ContinueDrag(views::View* view, |
| const ui::LocatedEvent& event) { |
| if (!drag_controller_.get() || |
| drag_controller_->event_source() != EventSourceFromEvent(event)) { |
| return Liveness::kAlive; |
| } |
| |
| gfx::Point screen_location(event.location()); |
| views::View::ConvertPointToScreen(view, &screen_location); |
| |
| // Note: `tab_strip_` can be destroyed during drag, also destroying `this`. |
| const TabDragController::Liveness drag_controller_alive = |
| drag_controller_->Drag(screen_location); |
| |
| return drag_controller_alive == TabDragController::Liveness::ALIVE |
| ? Liveness::kAlive |
| : Liveness::kDeleted; |
| } |
| |
| bool EndDrag(EndDragReason reason) { |
| if (!drag_controller_.get()) { |
| return false; |
| } |
| bool started_drag = drag_controller_->started_drag(); |
| drag_controller_->EndDrag(reason); |
| return started_drag; |
| } |
| |
| bool IsTabStripCloseable() const { |
| // Allow the close in two scenarios: |
| // . The user is not actively dragging the tabstrip. |
| // . In the process of remove the last tab in a drag (so that it can be |
| // inserted back into another tabstrip). |
| return !IsDragSessionActive() || drag_controller_->IsMovingLastTab(); |
| } |
| |
| // TabDragContext: |
| Tab* GetTabAt(int i) const override { return tab_strip_->tab_at(i); } |
| |
| std::optional<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_->GetModelPinnedTabCount(); |
| } |
| |
| 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(); |
| } |
| |
| TabDragController* GetDragController() override { |
| return drag_controller_.get(); |
| } |
| |
| void OwnDragController( |
| std::unique_ptr<TabDragController> controller) override { |
| CHECK(controller) |
| << "The provided TabDragController is null, which is not expected."; |
| |
| CHECK(!drag_controller_) |
| << "Attempting to own a new drag controller while one already exists."; |
| |
| drag_controller_ = std::move(controller); |
| if (drag_controller_set_callback_) { |
| std::move(drag_controller_set_callback_).Run(drag_controller_.get()); |
| } |
| } |
| |
| void DestroyDragController() override { drag_controller_.reset(); } |
| |
| std::unique_ptr<TabDragController> ReleaseDragController() override { |
| return std::move(drag_controller_); |
| } |
| |
| void SetDragControllerCallbackForTesting( |
| base::OnceCallback<void(TabDragController*)> callback) override { |
| drag_controller_set_callback_ = std::move(callback); |
| } |
| |
| void UpdateAnimationTarget(TabSlotView* tab_slot_view, |
| const gfx::Rect& target_bounds) override { |
| if (bounds_animator_.IsAnimating(tab_slot_view)) { |
| bounds_animator_.SetTargetBounds(tab_slot_view, target_bounds); |
| } |
| } |
| |
| bool IsDragSessionActive() const override { |
| return drag_controller_ != nullptr; |
| } |
| |
| bool IsAnimatingDragEnd() const override { |
| // The drag is ending if we're animating tabs back to the TabContainer, or |
| // if the TabDragController is in the kStopped state. |
| return (drag_controller_ == nullptr && bounds_animator_.IsAnimating()) || |
| (drag_controller_ && !drag_controller_->active()); |
| } |
| |
| void CompleteEndDragAnimations() override { |
| // Finishing animations will return tabs to the TabContainer via |
| // ResetDraggingStateDelegate::AnimationEnded. |
| bounds_animator_.Complete(); |
| } |
| |
| bool IsActiveDropTarget() const override { |
| for (int i = 0; i < GetTabCount(); ++i) { |
| const Tab* const tab = GetTabAt(i); |
| if (tab->dragging()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| 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_->tab_container_->GetAvailableWidthForTabContainer(), |
| tab_strip_->width()); |
| } |
| |
| int TabDragAreaBeginX() const override { |
| return tab_strip_->GetMirroredXWithWidthInView(0, GetTabDragAreaWidth()); |
| } |
| |
| int TabDragAreaEndX() const override { |
| return TabDragAreaBeginX() + GetTabDragAreaWidth(); |
| } |
| |
| int GetInsertionIndexForDraggedBounds(const gfx::Rect& dragged_bounds, |
| std::vector<TabSlotView*> dragged_views, |
| int num_dragged_tabs) const override { |
| // If the strip has no tabs, the only position to insert at is 0. |
| if (!GetTabCount()) { |
| return 0; |
| } |
| |
| std::optional<int> first_dragged_tab_model_index = std::nullopt; |
| bool can_insert_into_groups = true; |
| for (TabSlotView* tab_slot_view : dragged_views) { |
| const bool is_tab = |
| tab_slot_view->GetTabSlotViewType() == TabSlotView::ViewType::kTab; |
| if (first_dragged_tab_model_index == std::nullopt && is_tab) { |
| first_dragged_tab_model_index = |
| tab_strip_->GetModelIndexOf(tab_slot_view).value(); |
| } |
| can_insert_into_groups &= is_tab; |
| } |
| CHECK(first_dragged_tab_model_index.has_value()); |
| |
| const int index = CalculateInsertionIndex( |
| dragged_bounds, first_dragged_tab_model_index.value(), num_dragged_tabs, |
| can_insert_into_groups); |
| |
| const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab(); |
| int last_insertion_point = |
| last_visible_tab ? (GetIndexOf(last_visible_tab).value() + 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); |
| } |
| |
| std::vector<gfx::Rect> CalculateBoundsForDraggedViews( |
| const std::vector<TabSlotView*>& views) override { |
| CHECK(!views.empty()) << "The views vector must not be empty."; |
| |
| std::vector<gfx::Rect> bounds; |
| const int overlap = TabStyle::Get()->GetTabOverlap(); |
| int x = 0; |
| for (const TabSlotView* view : views) { |
| const int width = view->width(); |
| bounds.emplace_back(x, height() - view->height(), width, view->height()); |
| x += width - overlap; |
| } |
| |
| return bounds; |
| } |
| |
| void SetBoundsForDrag(const std::vector<TabSlotView*>& views, |
| const std::vector<gfx::Rect>& bounds) override { |
| CHECK(!views.empty() || !bounds.empty()) |
| << "Views and bounds cannot both be empty."; |
| tab_strip_->tab_container_->CancelAnimation(); |
| CHECK_EQ(views.size(), bounds.size()) |
| << "The sizes of views and bounds must match."; |
| 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. |
| 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_->tab_container_->InvalidateIdealBounds(); |
| for (auto* view : views) { |
| if (view->group().has_value()) { |
| tab_strip_->tab_container_->UpdateTabGroupVisuals( |
| view->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())); |
| |
| // Complete animations to ensure that the previous drag session fully ends |
| // before we start the next one. In particular this reparents the dragged |
| // tabs back to the TabContainer from the TabDragContext. N.B. this can |
| // happen either when starting a new drag in this tabstrip or when dragging |
| // tabs into this tabstrip from elsewhere. |
| tab_strip_->tab_container_->CompleteAnimationAndLayout(); |
| |
| // No tabs should be dragging at this point. |
| for (int i = 0; i < GetTabCount(); ++i) { |
| CHECK(!GetTabAt(i)->dragging()) |
| << "A tab is still marked as dragging when starting a new drag."; |
| } |
| |
| for (TabSlotView* dragged_view : views) { |
| CHECK_NE(dragged_view->parent(), this); |
| AddChildViewRaw(dragged_view); |
| dragged_view->set_dragging(true); |
| if (TabGroupHeader* header = |
| views::AsViewClass<TabGroupHeader>(dragged_view)) { |
| tab_strip_->tab_container_->GetGroupViews(header->group().value()) |
| ->highlight() |
| ->SetVisible(true); |
| // Make sure the bounds of the group views are up to date right now |
| // instead of waiting for subsequent drag events - if we are dragging a |
| // window by a group header, we won't get any more events. See |
| // https://crbug.com/1344774. |
| tab_strip_->tab_container_->UpdateTabGroupVisuals( |
| header->group().value()); |
| } |
| } |
| |
| tab_strip_->tab_container_->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() override { |
| // Let the controller know that the user stopped dragging tabs. |
| tab_strip_->controller_->OnStoppedDragging(); |
| UpdateDragEventSourceCrashKey({}); |
| |
| // Animate the dragged views to their ideal positions. We'll hand them back |
| // to TabContainer when the animation ends. |
| for (views::View* child : children()) { |
| gfx::Rect ideal_bounds; |
| TabSlotView* const slot_view = views::AsViewClass<TabSlotView>(child); |
| |
| if (!slot_view) { |
| continue; |
| } |
| |
| const TabGroupHeader* const header = |
| views::AsViewClass<TabGroupHeader>(slot_view); |
| if (header) { |
| // Disable the group highlight now that the drag is ended. |
| tab_strip_->tab_container_->GetGroupViews(header->group().value()) |
| ->highlight() |
| ->SetVisible(false); |
| ideal_bounds = |
| tab_strip_->tab_container_->GetIdealBounds(header->group().value()); |
| } else { |
| ideal_bounds = tab_strip_->tab_container_->GetIdealBounds( |
| tab_strip_->GetModelIndexOf(slot_view).value()); |
| } |
| |
| bounds_animator_.AnimateViewTo( |
| slot_view, ideal_bounds, |
| std::make_unique<ResetDraggingStateDelegate>( |
| *tab_strip_->tab_container_, *slot_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); |
| CHECK_EQ(views.size(), bounds.size()) |
| << "The sizes of views and bounds must match in LayoutDraggedViewsAt."; |
| |
| // The index of `source_view` in the TabStrip's viewmodel. |
| std::optional<int> source_view_model_index = GetIndexOf(source_view); |
| // The index of `source_view` as a child of this TabDragContext. |
| int source_view_index = |
| static_cast<int>(std::ranges::find(views, source_view) - views.begin()); |
| |
| const auto should_animate_tab = [&](size_t index_in_views) -> bool { |
| // If the tab at `index_in_views` is already animating, don't interrupt |
| // it. |
| if (bounds_animator_.IsAnimating(views[index_in_views])) { |
| return true; |
| } |
| |
| // If `source_view_model_index` is nullopt, we are dragging by a header, |
| // so the tabs are guaranteed to be consecutive already. |
| if (!source_view_model_index.has_value()) { |
| return false; |
| } |
| |
| // If the source of the drag is not a group header but a header is present |
| // in the dragging views, this result will be same as for the first tab in |
| // the group. |
| if (views[index_in_views]->GetTabSlotViewType() == |
| TabSlotView::ViewType::kTabGroupHeader) { |
| index_in_views += 1; |
| } |
| |
| // If the tab isn't at the right model index relative to `source_view`, |
| // animate it into position. |
| const int consecutive_model_index = |
| source_view_model_index.value() - |
| (source_view_index - static_cast<int>(index_in_views)); |
| return initial_drag && |
| GetIndexOf(views[index_in_views]) != consecutive_model_index; |
| }; |
| |
| 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()); |
| if (should_animate_tab(i)) { |
| bounds_animator_.SetTargetBounds(views[i], new_bounds); |
| } else { |
| view->SetBoundsRect(new_bounds); |
| } |
| } |
| tab_strip_->tab_container_->SetTabSlotVisibility(); |
| // The rightmost dragged tab may have moved, which would change our |
| // preferred width. |
| PreferredSizeChanged(); |
| |
| // If any of the dragged tabs are in a group, we need to update the bounds |
| // of the corresponding underlines and headers. |
| std::unordered_set<tab_groups::TabGroupId, tab_groups::TabGroupIdHash> |
| updated_groups; |
| for (TabSlotView* view : views) { |
| if (view->group().has_value() && |
| !updated_groups.contains(view->group().value())) { |
| tab_strip_->tab_container_->UpdateTabGroupVisuals( |
| view->group().value()); |
| updated_groups.insert(view->group().value()); |
| } |
| } |
| |
| // When multiple tabs are dragged out of a tab group at the right edge of |
| // the browser, some visual artifacts appear. Because the preferred size |
| // isn't changing and tab indices aren't changed, tabs aren't painted by |
| // default. When the dragged tabs aren't in a group, they are immediately |
| // to the right of a group, and they are the last tabs in the tab strip: |
| // ensure the tabs get repainted. |
| // TODO(crbug.com/423965262): Investigate if we can listen to `TabStrip` |
| // model changed events to call `SchedulePaint`. |
| const std::optional<int> leftmost_tab_in_drag = GetIndexOf(views[0]); |
| const std::optional<int> rightmost_tab_in_drag = |
| GetIndexOf(views[views.size() - 1]); |
| if (leftmost_tab_in_drag.has_value() && rightmost_tab_in_drag.has_value() && |
| tab_strip_->IsValidModelIndex(leftmost_tab_in_drag.value() - 1)) { |
| TabSlotView* left_of_dragged_tabs = |
| tab_strip_->tab_at(leftmost_tab_in_drag.value() - 1); |
| if (updated_groups.empty() && left_of_dragged_tabs->group().has_value() && |
| rightmost_tab_in_drag.value() == tab_strip_->GetTabCount() - 1) { |
| // Schedule paint for the dragged tabs. |
| for (TabSlotView* view : views) { |
| view->SchedulePaint(); |
| } |
| } |
| } |
| } |
| |
| // Forces the entire tabstrip to lay out. |
| void ForceLayout() override { |
| tab_strip_->InvalidateLayout(); |
| tab_strip_->tab_container_->CompleteAnimationAndLayout(); |
| } |
| |
| void PaintChildren(const views::PaintInfo& paint_info) override { |
| std::vector<ZOrderableTabContainerElement> orderable_children; |
| for (views::View* child : children()) { |
| orderable_children.emplace_back(child); |
| } |
| |
| // Sort in ascending order by z-value. Stable sort breaks ties by child |
| // index. |
| std::stable_sort(orderable_children.begin(), orderable_children.end()); |
| |
| for (const ZOrderableTabContainerElement& child : orderable_children) { |
| child.view()->Paint(paint_info); |
| } |
| } |
| |
| void OnBoundsAnimatorProgressed(views::BoundsAnimator* animator) override {} |
| void OnBoundsAnimatorDone(views::BoundsAnimator* animator) override { |
| // Send the Container a message to simulate a mouse moved event at the |
| // current mouse position. This tickles the Tab the mouse is currently over |
| // to show the "hot" state of the close button, or to show the hover card, |
| // etc. Note that this is not required (and indeed may crash!) during a |
| // drag session. |
| if (!IsDragSessionActive()) { |
| // The widget can apparently be null during shutdown. |
| views::Widget* widget = GetWidget(); |
| if (widget) { |
| widget->SynthesizeMouseMoveEvent(); |
| } |
| } |
| } |
| |
| views::ScrollView* GetScrollView() override { |
| return views::ScrollView::GetScrollViewForContents(tab_strip_); |
| } |
| |
| private: |
| // Animates tabs after a drag has ended, then hands them back to |
| // `tab_container_`. |
| class ResetDraggingStateDelegate : public gfx::AnimationDelegate { |
| public: |
| ResetDraggingStateDelegate(TabContainer& tab_container, |
| TabSlotView& slot_view) |
| : tab_container_(tab_container), slot_view_(slot_view) { |
| slot_view_->set_animating(true); |
| } |
| ResetDraggingStateDelegate(const ResetDraggingStateDelegate&) = delete; |
| ResetDraggingStateDelegate& operator=(const ResetDraggingStateDelegate&) = |
| delete; |
| ~ResetDraggingStateDelegate() override = default; |
| |
| void AnimationProgressed(const gfx::Animation* animation) override { |
| tab_container_->OnTabSlotAnimationProgressed( |
| base::to_address(slot_view_)); |
| } |
| |
| void AnimationEnded(const gfx::Animation* animation) override { |
| AnimationProgressed(animation); |
| slot_view_->set_animating(false); |
| slot_view_->set_dragging(false); |
| tab_container_->ReturnTabSlotView(base::to_address(slot_view_)); |
| } |
| |
| void AnimationCanceled(const gfx::Animation* animation) override { |
| AnimationEnded(animation); |
| } |
| |
| private: |
| const raw_ref<TabContainer> tab_container_; |
| const raw_ref<TabSlotView, DanglingUntriaged> slot_view_; |
| }; |
| |
| // Returns a map of all the tabgroups and the headers that are fully selected |
| // for drag. |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> |
| GetFullySelectedTabGroups() { |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> fully_selected_groups = |
| tab_strip_->GetGroupHeaders(); |
| std::erase_if(fully_selected_groups, [this](const auto& entry) { |
| const gfx::Range tabs_in_group = tab_strip_->ListTabsInGroup(entry.first); |
| for (size_t index = tabs_in_group.start(); index < tabs_in_group.end(); |
| index++) { |
| if (!GetTabAt(index)->IsSelected()) { |
| return true; |
| } |
| } |
| return false; |
| }); |
| |
| return fully_selected_groups; |
| } |
| |
| // 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, |
| bool can_insert_into_groups) 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, can_insert_into_groups)) { |
| 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 + |
| (can_insert_into_groups |
| ? CalculateIdealXAdjustmentIfAddedToGroup(candidate_index) |
| : 0); |
| 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; |
| } |
| } |
| |
| CHECK_NE(min_distance_index, -1) |
| << "No valid insertion index found for the dragged tab, which " |
| "indicates a potential logic error in tab placement calculations."; |
| |
| // 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, |
| bool can_insert_into_groups) 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; |
| } |
| |
| Tab* const left_tab = GetTabAt(candidate_index - 1); |
| Tab* const right_tab = tab_strip_->IsValidModelIndex(candidate_index) |
| ? GetTabAt(candidate_index) |
| : nullptr; |
| |
| // This might be in the middle of a group, which may or may not be fine. |
| std::optional<tab_groups::TabGroupId> left_group = left_tab->group(); |
| std::optional<tab_groups::TabGroupId> right_group = |
| right_tab ? right_tab->group() : std::nullopt; |
| if (left_group.has_value() && left_group == right_group) { |
| if (!can_insert_into_groups) { |
| return false; |
| } |
| // Can't drag a tab into a collapsed group. |
| if (tab_strip_->IsGroupCollapsed(left_group.value())) { |
| return false; |
| } |
| } |
| |
| // Prevent a tab from being inserted in between two tabs that are within the |
| // same split view. |
| if (right_tab) { |
| std::optional<split_tabs::SplitTabId> left_split_id = left_tab->split(); |
| if (left_split_id.has_value() && (left_split_id == right_tab->split())) { |
| 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::Get()->GetTabOverlap(); |
| |
| // We'll insert just right of the tab at `candidate_index` - 1. |
| int ideal_x = |
| tab_strip_->tab_container_->GetIdealBounds(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) const { |
| if (!tab_strip_->IsValidModelIndex(candidate_index)) { |
| return 0; |
| } |
| // 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. |
| std::optional<tab_groups::TabGroupId> left_group = |
| tab_strip_->IsValidModelIndex(candidate_index - 1) |
| ? GetTabAt(candidate_index - 1)->group() |
| : std::nullopt; |
| std::optional<tab_groups::TabGroupId> right_group = |
| GetTabAt(candidate_index)->group(); |
| if (!right_group.has_value() || left_group == right_group || |
| tab_strip_->IsGroupCollapsed(right_group.value())) { |
| return 0; |
| } |
| |
| const int header_width = GetTabGroupHeader(*right_group)->bounds().width() - |
| TabStyle::Get()->GetTabOverlap(); |
| return header_width; |
| } |
| |
| const raw_ptr<TabStrip, DanglingUntriaged> tab_strip_; |
| |
| // Responsible for animating tabs during drag sessions. |
| views::BoundsAnimator bounds_animator_; |
| |
| // The controller for a drag initiated from a Tab. Valid for the lifetime of |
| // the drag session. |
| std::unique_ptr<TabDragController> drag_controller_; |
| |
| // Only used in tests. |
| base::OnceCallback<void(TabDragController*)> drag_controller_set_callback_; |
| |
| base::WeakPtrFactory<TabDragContext> weak_factory_{this}; |
| }; |
| |
| BEGIN_METADATA(TabStrip, TabDragContextImpl); |
| END_METADATA |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, public: |
| |
| TabStrip::TabStrip(std::unique_ptr<TabStripController> controller) |
| : controller_(std::move(controller)), |
| hover_card_controller_(std::make_unique<TabHoverCardController>(this)), |
| drag_context_(*AddChildView(std::make_unique<TabDragContextImpl>(this))), |
| tab_container_( |
| *AddChildViewAt(MakeTabContainer(this, |
| hover_card_controller_.get(), |
| base::to_address(drag_context_)), |
| 0)), |
| style_(TabStyle::Get()) { |
| // TODO(pbos): This is probably incorrect, the background of individual tabs |
| // depend on their selected state. This should probably be pushed down into |
| // tabs. |
| views::SetCascadingColorProviderColor(this, views::kCascadingBackgroundColor, |
| kColorToolbar); |
| Init(); |
| |
| SetProperty(views::kElementIdentifierKey, kTabStripElementId); |
| } |
| |
| TabStrip::~TabStrip() { |
| // Eliminate the hover card first to avoid order-of-operation issues. |
| hover_card_controller_.reset(); |
| |
| // 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. |
| // End any ongoing drag session. |
| drag_context_->DestroyDragController(); |
| // Immediately clean up that drag session instead of allowing things to |
| // animate back into place over time. |
| drag_context_->CompleteEndDragAnimations(); |
| |
| // `tab_container_`'s tabs may call back to us or to `drag_context_` from |
| // their destructors. Delete them first so that if they call back we aren't in |
| // a weird state. |
| RemoveChildViewT(base::to_address(tab_container_)); |
| RemoveChildViewT(base::to_address(drag_context_)); |
| |
| CHECK(!IsInObserverList()) |
| << "TabStrip should not be in any observer lists at destruction."; |
| } |
| |
| void TabStrip::SetAvailableWidthCallback( |
| base::RepeatingCallback<int()> available_width_callback) { |
| tab_container_->SetAvailableWidthCallback(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::Get()->GetTabOverlap() * (views.size() - 1); |
| } |
| return width; |
| } |
| |
| void TabStrip::SetTabStripObserver(TabStripObserver* observer) { |
| // Overwriting a non-null delegate with another delegate is likely a logic |
| // bug. |
| CHECK_NE(!!observer_, !!observer); |
| observer_ = observer; |
| } |
| |
| bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { |
| // `rect` is in the window caption if it doesn't hit any content area. |
| return !tab_container_->IsRectInContentArea(rect); |
| } |
| |
| bool TabStrip::IsTabStripCloseable() const { |
| return drag_context_->IsTabStripCloseable(); |
| } |
| |
| bool TabStrip::IsTabStripEditable() const { |
| return !tab_strip_not_editable_for_testing_ && |
| !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; |
| } |
| |
| std::optional<tabs::TabAlert> 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::AddTabsAt( |
| std::vector<std::pair<int, TabRendererData>> tabs_datas) { |
| std::vector<TabContainer::TabInsertionParams> tabs_params; |
| |
| for (const auto& tab_data : tabs_datas) { |
| const int model_index = tab_data.first; |
| CHECK(IsValidModelIndex(model_index)) |
| << "Attempted to add a tab with an invalid model index."; |
| TabContainer::TabInsertionParams param( |
| std::make_unique<Tab>(this), tab_data.first, |
| tab_data.second.pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| tabs_params.push_back(std::move(param)); |
| } |
| |
| std::vector<Tab*> tabs = tab_container_->AddTabs(std::move(tabs_params)); |
| |
| for (int index = 0; index < static_cast<int>(tabs_datas.size()); index++) { |
| Tab* tab = tabs[index]; |
| int model_index = tabs_datas[index].first; |
| TabRendererData renderer_data = tabs_datas[index].second; |
| tab->set_context_menu_controller(&context_menu_controller_); |
| tab->AddObserver(this); |
| 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(renderer_data)); |
| |
| if (observer_) { |
| observer_->OnTabAdded(model_index); |
| } |
| |
| // At the start of AddTabAt() the model and tabs are out of 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. |
| drag_context_->TabWasAdded(); |
| } |
| |
| Profile* profile = controller_->GetProfile(); |
| if (profile) { |
| if (profile->IsGuestSession()) { |
| base::UmaHistogramCounts100("Tab.Count.Guest", GetTabCount()); |
| } else if (profile->IsIncognitoProfile()) { |
| base::UmaHistogramCounts100("Tab.Count.Incognito", GetTabCount()); |
| } |
| } |
| |
| if (new_tab_button_pressed_start_time_.has_value()) { |
| base::UmaHistogramTimes( |
| "TabStrip.TimeToCreateNewTabFromPress", |
| base::TimeTicks::Now() - new_tab_button_pressed_start_time_.value()); |
| new_tab_button_pressed_start_time_.reset(); |
| } |
| } |
| |
| void TabStrip::MoveTab(int from_model_index, |
| int to_model_index, |
| TabRendererData data) { |
| CHECK_GT(GetTabCount(), 0) |
| << "The tab strip must contain at least one tab to perform a move " |
| "operation."; |
| |
| Tab* moving_tab = tab_at(from_model_index); |
| moving_tab->SetData(std::move(data)); |
| |
| tab_container_->MoveTab(from_model_index, to_model_index); |
| |
| selected_tabs_.Move(from_model_index, to_model_index, /*length=*/1); |
| |
| if (observer_) { |
| observer_->OnTabMoved(from_model_index, to_model_index); |
| } |
| } |
| |
| void TabStrip::RemoveTabAt(content::WebContents* contents, |
| int model_index, |
| bool was_active) { |
| // OnTabWillBeRemoved should have ended any ongoing drags containing |
| // `contents` already - unless the call is coming from inside the house! (i.e. |
| // the TabDragController is doing the removing as part of reverting a drag) |
| CHECK(drag_context_->CanRemoveTabIfDragging(contents)) |
| << "Attempted to remove a tab that could not be removed during drag."; |
| |
| tab_container_->RemoveTab(model_index, was_active); |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); |
| |
| selected_tabs_.DecrementFrom(model_index); |
| |
| if (observer_) { |
| observer_->OnTabRemoved(model_index); |
| } |
| } |
| |
| void TabStrip::OnTabWillBeRemoved(content::WebContents* contents, |
| int model_index) { |
| drag_context_->OnTabWillBeRemoved(contents); |
| } |
| |
| void TabStrip::MaybeUpdateGroupOnTabChanged(int model_index) { |
| Tab* tab = tab_at(model_index); |
| if (tab->group().has_value()) { |
| if (ListTabsInGroup(tab->group().value()).length() > 0) { |
| // Since tab group naming can be based on the name of the first tab in the |
| // group, update the tab group name if this tab is the first in the group. |
| std::optional<int> tab_model_index = GetModelIndexOf(tab); |
| std::optional<int> group_first_tab = |
| GetFirstTabInGroup(tab->group().value()); |
| if (tab_model_index.has_value() && group_first_tab.has_value() && |
| tab_model_index.value() == group_first_tab.value()) { |
| OnGroupContentsChanged(tab->group().value()); |
| } |
| } |
| } |
| } |
| |
| 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; |
| const bool tab_title_changed = tab->data().title != data.title; |
| tab->SetData(std::move(data)); |
| |
| if (HoverCardIsShowingForTab(tab)) { |
| UpdateHoverCard(tab, HoverCardUpdateType::kTabDataChanged); |
| } |
| |
| if (pinned_state_changed) { |
| tab_container_->SetTabPinned( |
| model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| } |
| |
| if (tab_title_changed) { |
| MaybeUpdateGroupOnTabChanged(model_index); |
| } |
| } |
| |
| void TabStrip::AddTabToGroup(std::optional<tab_groups::TabGroupId> group, |
| int model_index) { |
| tab_at(model_index)->SetGroup(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 (static_cast<size_t>(model_index) == selected_tabs_.active() && |
| group.has_value() && IsGroupCollapsed(group.value())) { |
| ToggleTabGroupCollapsedState( |
| group.value(), ToggleTabGroupCollapsedStateOrigin::kMenuAction); |
| } |
| |
| if (group.has_value()) { |
| tab_container_->ExitTabClosingMode(); |
| } |
| } |
| |
| void TabStrip::OnGroupCreated(const tab_groups::TabGroupId& group) { |
| tab_container_->OnGroupCreated(group); |
| } |
| |
| void TabStrip::OnGroupEditorOpened(const tab_groups::TabGroupId& group) { |
| tab_container_->OnGroupEditorOpened(group); |
| } |
| |
| void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) { |
| tab_container_->OnGroupContentsChanged(group); |
| } |
| |
| void TabStrip::OnGroupVisualsChanged( |
| const tab_groups::TabGroupId& group, |
| const tab_groups::TabGroupVisualData* old_visuals, |
| const tab_groups::TabGroupVisualData* new_visuals) { |
| tab_container_->OnGroupVisualsChanged(group, old_visuals, new_visuals); |
| } |
| |
| void TabStrip::ToggleTabGroup(const tab_groups::TabGroupId& group, |
| bool is_collapsing, |
| ToggleTabGroupCollapsedStateOrigin origin) { |
| tab_container_->ToggleTabGroup(group, is_collapsing, origin); |
| } |
| |
| void TabStrip::OnGroupMoved(const tab_groups::TabGroupId& group) { |
| tab_container_->OnGroupMoved(group); |
| } |
| |
| void TabStrip::OnGroupClosed(const tab_groups::TabGroupId& group) { |
| for (int tab_view_model_index = 0; |
| tab_view_model_index < tab_container_->GetTabCount(); |
| tab_view_model_index++) { |
| Tab* tab = tab_at(tab_view_model_index); |
| if (tab->group() == group) { |
| tab->SetGroup(std::nullopt); |
| } |
| } |
| tab_container_->OnGroupClosed(group); |
| } |
| |
| void TabStrip::OnSplitCreated(const std::vector<int>& split_indices, |
| split_tabs::SplitTabId split_id) { |
| for (const int split_index : split_indices) { |
| tab_at(split_index)->SetSplit(split_id); |
| } |
| |
| tab_container_->OnSplitCreated(split_indices); |
| } |
| |
| void TabStrip::OnSplitRemoved(const std::vector<int>& split_indices) { |
| for (const int split_index : split_indices) { |
| tab_at(split_index)->SetSplit(std::nullopt); |
| } |
| |
| tab_container_->OnSplitRemoved(split_indices); |
| } |
| |
| void TabStrip::OnSplitContentsChanged(const std::vector<int>& split_indices) { |
| for (const int split_index : split_indices) { |
| tab_at(split_index)->UpdateAccessibleName(); |
| } |
| |
| tab_container_->OnSplitContentsChanged(split_indices); |
| } |
| |
| bool TabStrip::ShouldDrawStrokes() const { |
| #if BUILDFLAG(IS_CHROMEOS) |
| return false; |
| #else // BUILDFLAG(IS_CHROMEOS) |
| |
| // If the controller says we can't draw strokes, don't. |
| if (!controller_->CanDrawStrokes()) { |
| return false; |
| } |
| |
| bool using_system_theme = false; |
| if (auto* profile = controller_->GetProfile()) { |
| auto* theme_service = ThemeServiceFactory::GetForProfile(profile); |
| using_system_theme = |
| theme_service->IsSystemThemeDistinctFromDefaultTheme() && |
| theme_service->UsingSystemTheme(); |
| } |
| |
| // The Tabstrip in the refreshed style does not meet the contrast ratio |
| // requirements listed below but does not have strokes for Tabs or the bottom |
| // border. |
| if (!using_system_theme) { |
| 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 = TabStyle::Get()->GetTabBackgroundColor( |
| TabStyle::TabSelectionState::kActive, /*hovered=*/false, |
| /*frame_active=*/true, *GetColorProvider()); |
| const SkColor frame_color = |
| controller_->GetFrameColor(BrowserFrameActiveState::kActive); |
| const float contrast_ratio = |
| color_utils::GetContrastRatio(background_color, frame_color); |
| return contrast_ratio < kMinimumContrastRatioForOutlines; |
| #endif // BUILDFLAG(IS_CHROMEOS) |
| } |
| |
| void TabStrip::SetSelection(const ui::ListSelectionModel& new_selection) { |
| // This CHECK ensures there is always an active tab to maintain UI |
| // consistency. |
| CHECK(new_selection.active().has_value()) |
| << "We should never transition to a state where no tab is active."; |
| |
| Tab* const new_active_tab = tab_at(new_selection.active().value()); |
| Tab* const old_active_tab = selected_tabs_.active().has_value() |
| ? tab_at(selected_tabs_.active().value()) |
| : nullptr; |
| |
| if (new_active_tab != old_active_tab) { |
| if (old_active_tab) { |
| if (old_active_tab->split().has_value()) { |
| for (Tab* split_tab : GetTabsInSplit(old_active_tab)) { |
| split_tab->ActiveStateChanged(); |
| } |
| } else { |
| old_active_tab->ActiveStateChanged(); |
| } |
| } |
| |
| if (new_active_tab->split().has_value()) { |
| for (Tab* split_tab : GetTabsInSplit(new_active_tab)) { |
| split_tab->ActiveStateChanged(); |
| } |
| } else { |
| new_active_tab->ActiveStateChanged(); |
| } |
| |
| tab_container_->SetActiveTab(selected_tabs_.active(), |
| new_selection.active()); |
| } |
| |
| for (int selection : new_selection.selected_indices()) { |
| Tab* const selected_tab = tab_at(selection); |
| if (selected_tab->group().has_value()) { |
| const tab_groups::TabGroupId new_group = selected_tab->group().value(); |
| // If the tab that is about to be selected is in a collapsed group, |
| // automatically expand the group. |
| if (IsGroupCollapsed(new_group)) { |
| ToggleTabGroupCollapsedState( |
| new_group, ToggleTabGroupCollapsedStateOrigin::kTabsSelected); |
| } |
| } |
| } |
| |
| // 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()); |
| |
| selected_tabs_ = new_selection; |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kSelectionChanged); |
| |
| // 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::ScrollTowardsTrailingTabs(int offset) { |
| tab_container_->ScrollTabContainerByOffset(offset); |
| } |
| |
| void TabStrip::ScrollTowardsLeadingTabs(int offset) { |
| tab_container_->ScrollTabContainerByOffset(-offset); |
| } |
| |
| void TabStrip::OnWidgetActivationChanged(views::Widget* widget, bool active) { |
| if (active && selected_tabs_.active().has_value()) { |
| // When the browser window is activated, set the accessible selection and |
| // fire a selection event on the currently active tab, to help enable |
| // per-tab modes in assistive technologies. |
| tab_at(selected_tabs_.active().value()) |
| ->GetViewAccessibility() |
| .SetIsSelected(true); |
| |
| // When the browser window is activated, fire a selection event on the |
| // currently active tab, to help enable per-tab modes in assistive |
| // technologies. |
| // We need to make sure we fire the event manually here, because even |
| // though we set the tab to selected above, there are cases where the |
| // event will not be fired since the selected state was already set |
| // on the tab. Nevertheless, JAWS needs the event to be fired regardless, |
| // as per https://crbug.com/41450089. |
| tab_at(selected_tabs_.active().value()) |
| ->NotifyAccessibilityEventDeprecated(ax::mojom::Event::kSelection, |
| true); |
| } |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kEvent); |
| } |
| |
| void TabStrip::SetTabNeedsAttention(int model_index, bool attention) { |
| tab_at(model_index)->SetTabNeedsAttention(attention); |
| } |
| |
| void TabStrip::SetTabGroupNeedsAttention(const tab_groups::TabGroupId& id, |
| bool attention) { |
| group_header(id)->SetTabGroupNeedsAttention(attention); |
| } |
| |
| TabGroup* TabStrip::GetTabGroup(const tab_groups::TabGroupId& id) const { |
| return controller_->GetTabGroup(id); |
| } |
| |
| std::optional<int> TabStrip::GetModelIndexOf(const TabSlotView* view) const { |
| const std::optional<int> viewmodel_index = |
| tab_container_->GetModelIndexOf(view); |
| |
| // TODO(crbug.com/40880410): The viewmodel (as accessed by |
| // `tab_container_->GetModelIndexOf(Tab*)`) can be out of sync with the actual |
| // TabStripModel when multiple tabs are closed at once. We can check |
| // IsValidModelIndex to avoid crashes or out of bounds issues, but we can't |
| // avoid returning incorrect indices from this method in that context. |
| if (viewmodel_index.has_value() && |
| !IsValidModelIndex(viewmodel_index.value())) { |
| return std::nullopt; |
| } |
| return viewmodel_index; |
| } |
| |
| int TabStrip::GetTabCount() const { |
| return tab_container_->GetTabCount(); |
| } |
| |
| int TabStrip::GetModelCount() const { |
| return controller_->GetCount(); |
| } |
| |
| int TabStrip::GetModelPinnedTabCount() const { |
| for (size_t i = 0; i < static_cast<size_t>(controller_->GetCount()); ++i) { |
| if (!controller_->IsTabPinned(static_cast<int>(i))) { |
| return static_cast<int>(i); |
| } |
| } |
| |
| // All tabs are pinned. |
| return controller_->GetCount(); |
| } |
| |
| TabDragContext* TabStrip::GetDragContext() { |
| return base::to_address(drag_context_); |
| } |
| |
| void TabStrip::StopAnimating() { |
| tab_container_->CompleteAnimationAndLayout(); |
| } |
| |
| views::View* TabStrip::GetTabViewForPromoAnchor(int index_hint) { |
| return tab_at(std::clamp(index_hint, 0, GetTabCount() - 1)); |
| } |
| |
| views::View* TabStrip::GetDefaultFocusableChild() { |
| const std::optional<int> active = GetActiveIndex(); |
| return active.has_value() ? tab_at(active.value()) : nullptr; |
| } |
| |
| BrowserWindowInterface* TabStrip::GetBrowserWindowInterface() { |
| return controller_->GetBrowserWindowInterface(); |
| } |
| |
| bool TabStrip::IsValidModelIndex(int index) const { |
| return controller_->IsValidIndex(index); |
| } |
| |
| std::optional<int> TabStrip::GetActiveIndex() const { |
| return controller_->GetActiveIndex(); |
| } |
| |
| int TabStrip::NumPinnedTabsInModel() const { |
| for (size_t i = 0; i < static_cast<size_t>(controller_->GetCount()); ++i) { |
| if (!controller_->IsTabPinned(static_cast<int>(i))) { |
| return static_cast<int>(i); |
| } |
| } |
| |
| // All tabs are pinned. |
| return controller_->GetCount(); |
| } |
| |
| void TabStrip::OnDropIndexUpdate(const std::optional<int> index, |
| const bool drop_before) { |
| controller_->OnDropIndexUpdate(index, drop_before); |
| } |
| |
| bool TabStrip::IsBrowserClosing() const { |
| return controller_->IsBrowserClosing(); |
| } |
| |
| std::optional<int> TabStrip::GetFirstTabInGroup( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetFirstTabInGroup(group); |
| } |
| |
| gfx::Range TabStrip::ListTabsInGroup( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->ListTabsInGroup(group); |
| } |
| |
| bool TabStrip::CanExtendDragHandle() const { |
| return !controller_->IsFrameCondensed() && |
| !controller_->EverHasVisibleBackgroundTabShapes(); |
| } |
| |
| const views::View* TabStrip::GetTabClosingModeMouseWatcherHostView() const { |
| return this; |
| } |
| |
| bool TabStrip::IsAnimatingInTabStrip() const { |
| return tab_container_->IsAnimating() || drag_context_->IsAnimatingDragEnd(); |
| } |
| |
| void TabStrip::UpdateAnimationTarget(TabSlotView* tab_slot_view, |
| gfx::Rect target_bounds) { |
| // TODO(crbug.com/40711732): This may need to do coordinate space |
| // transformations if the view hierarchy changes so `tab_container_` and |
| // `drag_context_` don't share spaces. |
| drag_context_->UpdateAnimationTarget(tab_slot_view, target_bounds); |
| } |
| |
| bool TabStrip::IsGroupCollapsed(const tab_groups::TabGroupId& group) const { |
| return controller_->IsGroupCollapsed(group); |
| } |
| |
| const ui::ListSelectionModel& TabStrip::GetSelectionModel() const { |
| return controller_->GetSelectionModel(); |
| } |
| |
| Tab* TabStrip::tab_at(int index) const { |
| return tab_container_->GetTabAtModelIndex(index); |
| } |
| |
| void TabStrip::SelectTab(Tab* tab, const ui::Event& event) { |
| const std::optional<int> maybe_model_index = GetModelIndexOf(tab); |
| if (!maybe_model_index.has_value()) { |
| return; |
| } |
| |
| const int model_index = maybe_model_index.value(); |
| |
| if (!tab->IsActive()) { |
| controller_->RecordMetricsOnTabSelectionChange(tab->group()); |
| } |
| |
| controller_->SelectTab(model_index, event); |
| } |
| |
| void TabStrip::ExtendSelectionTo(Tab* tab) { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| if (model_index.has_value()) { |
| controller_->ExtendSelectionTo(model_index.value()); |
| } |
| } |
| |
| void TabStrip::ToggleSelected(Tab* tab) { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| if (model_index.has_value()) { |
| controller_->ToggleSelected(model_index.value()); |
| } |
| } |
| |
| void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| if (model_index.has_value()) { |
| controller_->AddSelectionFromAnchorTo(model_index.value()); |
| } |
| } |
| |
| void TabStrip::CloseTab(Tab* tab, CloseTabSource source) { |
| const std::optional<int> index_to_close = |
| tab_container_->GetModelIndexOfFirstNonClosingTab(tab); |
| if (index_to_close.has_value() && IsValidModelIndex(index_to_close.value())) { |
| auto callback = |
| base::BindOnce(&TabStrip::CloseTabInternal, base::Unretained(this), |
| index_to_close.value()); |
| controller_->OnCloseTab(index_to_close.value(), source, |
| std::move(callback)); |
| } |
| } |
| |
| void TabStrip::ToggleTabAudioMute(Tab* tab) { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| if (model_index.has_value()) { |
| controller_->ToggleTabAudioMute(model_index.value()); |
| } |
| } |
| |
| 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 std::optional<int> start_index = GetModelIndexOf(tab); |
| if (!start_index.has_value()) { |
| return; |
| } |
| |
| int target_index = 0; |
| if (!controller_->IsTabPinned(start_index.value())) { |
| 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.value(), 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 std::optional<int> maybe_start_index = GetModelIndexOf(tab); |
| if (!maybe_start_index.has_value()) { |
| return; |
| } |
| |
| const int start_index = maybe_start_index.value(); |
| |
| 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::ToggleTabGroupCollapsedState( |
| const tab_groups::TabGroupId group, |
| ToggleTabGroupCollapsedStateOrigin origin) { |
| int tab_count = GetTabCount(); |
| controller_->ToggleTabGroupCollapsedState(group, origin); |
| // If tab count changed, all tab groups are collapsed and we have |
| // created a new tab. We need to exit closing mode to resize the new |
| // tab immediately. |
| // TODO(crbug.com/40878307): This should be captured along with the |
| // ToggleTabGroup logic, so other callers to |
| // TabStripController::ToggleTabGroupCollapsedState see the same |
| // behavior. |
| if (tab_count != GetTabCount()) { |
| tab_container_->ExitTabClosingMode(); |
| } |
| } |
| |
| void TabStrip::NotifyTabstripBubbleOpened() { |
| tab_container_->NotifyTabstripBubbleOpened(); |
| } |
| |
| void TabStrip::NotifyTabstripBubbleClosed() { |
| tab_container_->NotifyTabstripBubbleClosed(); |
| } |
| |
| void TabStrip::ShowContextMenuForTab(Tab* tab, |
| const gfx::Point& p, |
| ui::mojom::MenuSourceType source_type) { |
| controller_->ShowContextMenuForTab(tab, p, source_type); |
| } |
| |
| bool TabStrip::IsActiveTab(const Tab* tab) const { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| return model_index.has_value() && |
| controller_->IsActiveTab(model_index.value()); |
| } |
| |
| bool TabStrip::IsTabSelected(const Tab* tab) const { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| return model_index.has_value() && |
| controller_->IsTabSelected(model_index.value()); |
| } |
| |
| bool TabStrip::IsTabPinned(const Tab* tab) const { |
| std::optional<int> model_index = GetModelIndexOf(tab); |
| return model_index.has_value() && |
| controller_->IsTabPinned(model_index.value()); |
| } |
| |
| bool TabStrip::IsTabFirst(const Tab* tab) const { |
| return GetModelIndexOf(tab) == 0; |
| } |
| |
| bool TabStrip::IsFocusInTabs() const { |
| return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView()); |
| } |
| |
| bool TabStrip::ShouldCompactLeadingEdge() const { |
| return !features::HasTabSearchToolbarButton() && |
| !controller_->GetBrowser() |
| ->window() |
| ->AsBrowserView() |
| ->browser_widget() |
| ->GetFrameView() |
| ->CaptionButtonsOnLeadingEdge() && |
| tabs::GetTabSearchTrailingTabstrip(controller_->GetProfile()); |
| } |
| |
| 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 (IsAnimatingInTabStrip() || controller_->HasAvailableDragActions() == 0) { |
| return; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| // Block drag operation if the web app is locked for OnTask. This prevents the |
| // window from moving along with the tab when in locked fullsceeen mode. Only |
| // relevant for non-web browser scenarios. |
| if (IsLockedForOnTask()) { |
| return; |
| } |
| #endif |
| |
| // Check that the source is either a valid tab or a tab group header, which |
| // are the only valid drag targets. |
| CHECK(GetModelIndexOf(source).has_value() || |
| source->GetTabSlotViewType() == TabSlotView::ViewType::kTabGroupHeader) |
| << "Drag source must be a valid tab or a tab group header."; |
| |
| drag_context_->MaybeStartDrag(source, event, original_selection); |
| has_reported_tab_drag_metrics_ = false; |
| } |
| |
| TabSlotController::Liveness TabStrip::ContinueDrag( |
| views::View* view, |
| const ui::LocatedEvent& event) { |
| // We enter here when dragging really happens. |
| // Note that `MaybeStartDrag()` is invoked as soon as mouse pressed. |
| if (!has_reported_tab_drag_metrics_) { |
| base::TimeTicks drag_time = base::TimeTicks::Now(); |
| if (mouse_entered_tabstrip_time_.has_value()) { |
| UmaHistogramMediumTimes("TabStrip.Dragging.TimeFromMouseEntered", |
| drag_time - mouse_entered_tabstrip_time_.value()); |
| } |
| |
| tab_drag_count_30min_++; |
| tab_drag_count_5min_++; |
| |
| if (last_tab_drag_time_.has_value()) { |
| UmaHistogramLongTimes("TabStrip.Dragging.TimeFromLastDrag", |
| drag_time - last_tab_drag_time_.value()); |
| } |
| last_tab_drag_time_ = drag_time; |
| |
| has_reported_tab_drag_metrics_ = true; |
| } |
| return 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; |
| } |
| |
| Tab* TabStrip::GetAdjacentTab(const Tab* tab, int offset) { |
| const std::optional<int> tab_index = GetModelIndexOf(tab); |
| if (!tab_index.has_value()) { |
| return nullptr; |
| } |
| const int adjacent_index = tab_index.value() + offset; |
| return IsValidModelIndex(adjacent_index) ? tab_at(adjacent_index) : nullptr; |
| } |
| |
| std::vector<Tab*> TabStrip::GetTabsInSplit(const Tab* tab) { |
| if (!tab->split().has_value()) { |
| return {}; |
| } |
| |
| Tab* current_tab = tab->controller()->GetAdjacentTab(tab, 0); |
| // Note that this only supports having two tabs in a split. |
| Tab* start_tab = tab->controller()->GetAdjacentTab(tab, -1); |
| if (start_tab && start_tab->split().has_value() && |
| start_tab->split().value() == current_tab->split().value()) { |
| return {start_tab, current_tab}; |
| } |
| Tab* const end_tab = tab->controller()->GetAdjacentTab(tab, 1); |
| if (end_tab && end_tab->split().has_value() && |
| end_tab->split().value() == current_tab->split().value()) { |
| return {current_tab, end_tab}; |
| } |
| |
| return {}; |
| } |
| |
| 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() && |
| !has_reported_time_mouse_entered_to_switch_ && |
| event.type() == ui::EventType::kMousePressed && |
| views::IsViewClass<Tab>(source)) { |
| DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES( |
| "TabStrip.TimeToSwitch", |
| base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value()); |
| has_reported_time_mouse_entered_to_switch_ = true; |
| } |
| } |
| |
| void TabStrip::UpdateHoverCard(Tab* tab, HoverCardUpdateType update_type) { |
| tab_container_->UpdateHoverCard(tab, update_type); |
| } |
| |
| bool TabStrip::HoverCardIsShowingForTab(Tab* tab) { |
| return hover_card_controller_ && |
| hover_card_controller_->IsHoverCardShowingForTab(tab); |
| } |
| |
| void TabStrip::ShowHover(Tab* tab, TabStyle::ShowHoverStyle style) { |
| if (tab->split().has_value()) { |
| for (Tab* split_tab : GetTabsInSplit(tab)) { |
| split_tab->ShowHover(style); |
| } |
| } else { |
| tab->ShowHover(style); |
| } |
| } |
| |
| void TabStrip::HideHover(Tab* tab, TabStyle::HideHoverStyle style) { |
| if (tab->split().has_value()) { |
| for (Tab* split_tab : GetTabsInSplit(tab)) { |
| split_tab->HideHover(style); |
| } |
| } else { |
| tab->HideHover(style); |
| } |
| } |
| |
| int TabStrip::GetStrokeThickness() const { |
| return ShouldDrawStrokes() ? 1 : 0; |
| } |
| |
| bool TabStrip::CanPaintThrobberToLayer() const { |
| // Disable layer-painting of throbbers if dragging or if any tab animation is |
| // in progress. 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 && !dragging && !IsAnimatingInTabStrip() && |
| !widget->IsFullscreen(); |
| } |
| |
| bool TabStrip::HasVisibleBackgroundTabShapes() const { |
| return controller_->HasVisibleBackgroundTabShapes(); |
| } |
| |
| SkColor TabStrip::GetTabSeparatorColor() const { |
| return separator_color_; |
| } |
| |
| SkColor TabStrip::GetTabForegroundColor(TabActive active) const { |
| const ui::ColorProvider* cp = GetColorProvider(); |
| if (!cp) { |
| return gfx::kPlaceholderColor; |
| } |
| |
| static constexpr std::array<std::array<ChromeColorIds, 2>, 2> kColorIds = { |
| {{kColorTabForegroundInactiveFrameInactive, |
| kColorTabForegroundInactiveFrameActive}, |
| {kColorTabForegroundActiveFrameInactive, |
| kColorTabForegroundActiveFrameActive}}}; |
| |
| const bool tab_active = active == TabActive::kActive; |
| const bool frame_active = GetWidget()->ShouldPaintAsActive(); |
| return cp->GetColor(kColorIds[tab_active][frame_active]); |
| } |
| |
| // Returns the accessible tab name for the tab. |
| std::u16string TabStrip::GetAccessibleTabName(const Tab* tab) const { |
| return GetModelIndexOf(tab).has_value() |
| ? controller_->GetAccessibleTabName(tab) |
| : std::u16string(); |
| } |
| |
| std::optional<int> TabStrip::GetCustomBackgroundId( |
| BrowserFrameActiveState active_state) const { |
| return controller_->GetCustomBackgroundId(active_state); |
| } |
| |
| 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_; |
| } |
| |
| std::u16string TabStrip::GetGroupTitle( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupTitle(group); |
| } |
| |
| std::u16string TabStrip::GetGroupContentString( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupContentString(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 GetColorProvider()->GetColor( |
| GetTabGroupTabStripColorId(color_id, GetWidget()->ShouldPaintAsActive())); |
| } |
| |
| void TabStrip::ShiftGroupLeft(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, -1); |
| } |
| |
| void TabStrip::ShiftGroupRight(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, 1); |
| } |
| |
| Browser* TabStrip::GetBrowser() { |
| return controller_->GetBrowser(); |
| } |
| |
| bool TabStrip::IsFrameCondensed() const { |
| return controller_->IsFrameCondensed(); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| bool TabStrip::IsLockedForOnTask() { |
| return controller_->IsLockedForOnTask(); |
| } |
| #endif |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, views::View overrides: |
| |
| views::SizeBounds TabStrip::GetAvailableSize(const views::View* child) const { |
| CHECK(child == base::to_address(tab_container_)) |
| << "The child view does not match the expected tab_container_ address."; |
| return parent()->GetAvailableSize(this); |
| } |
| |
| gfx::Size TabStrip::GetMinimumSize() const { |
| // `tab_container_` and `drag_context_` overlap (both share TabStrip's |
| // origin), so we need to be able to cover the union of their bounds. |
| gfx::Size min_size = tab_container_->GetMinimumSize(); |
| min_size.SetToMax(drag_context_->GetMinimumSize()); |
| |
| return min_size; |
| } |
| |
| gfx::Size TabStrip::CalculatePreferredSize( |
| const views::SizeBounds& available_size) const { |
| // `tab_container_` and `drag_context_` overlap (both share TabStrip's |
| // origin), so we need to be able to cover the union of their bounds. |
| gfx::Size preferred_size = tab_container_->GetPreferredSize(available_size); |
| preferred_size.SetToMax(drag_context_->GetPreferredSize(available_size)); |
| |
| return preferred_size; |
| } |
| |
| void TabStrip::Layout(PassKey) { |
| if (base::FeatureList::IsEnabled(tabs::kScrollableTabStrip)) { |
| // With tab scrolling, the TabStrip is the contents view of a ScrollView and |
| // as such is expected to set its own bounds during layout. |
| // (With great sizing power comes great sizing responsibility). |
| |
| // We should never be larger than our preferred width. |
| const int max_width = GetPreferredSize().width(); |
| // We should never be smaller than our minimum width. |
| const int min_width = GetMinimumSize().width(); |
| // If we can, we should fit within the tab strip region to avoid scrolling. |
| const int available_width = |
| tab_container_->GetAvailableWidthForTabContainer(); |
| // 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_STRIP_HEIGHT)); |
| } |
| |
| if (tab_container_->bounds() != GetLocalBounds()) { |
| UpdateHoverCard(nullptr, |
| TabSlotController::HoverCardUpdateType::kAnimating); |
| tab_container_->SetBoundsRect(GetLocalBounds()); |
| } else { |
| // We still need to layout in this case, as the available width may have |
| // changed, which can change layout outcomes (e.g. affecting tab |
| // visibility). See https://crbug.com/1370459. |
| // TODO(crbug.com/40870361): TabContainer should observe available width |
| // changes and invalidate its layout when needed. |
| tab_container_->DeprecatedLayoutImmediately(); |
| } |
| drag_context_->SetBoundsRect(GetLocalBounds()); |
| } |
| |
| void TabStrip::ChildPreferredSizeChanged(views::View* child) { |
| PreferredSizeChanged(); |
| } |
| |
| std::optional<BrowserRootView::DropIndex> TabStrip::GetDropIndex( |
| const ui::DropTargetEvent& event) { |
| // BrowserView should talk directly to `tab_container_` instead of asking us. |
| NOTREACHED(); |
| } |
| |
| BrowserRootView::DropTarget* TabStrip::GetDropTarget( |
| gfx::Point loc_in_local_coords) { |
| return tab_container_->GetDropTarget(loc_in_local_coords); |
| } |
| |
| views::View* TabStrip::GetViewForDrop() { |
| // BrowserView should talk directly to `tab_container_` instead of asking us. |
| NOTREACHED(); |
| } |
| |
| void TabStrip::SetTabStripNotEditableForTesting() { |
| tab_strip_not_editable_for_testing_ = true; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, private: |
| |
| void TabStrip::Init() { |
| SetID(VIEW_ID_TAB_STRIP); |
| // So we only get enter/exit messages when the mouse enters/exits the whole |
| // tabstrip, even if it is entering/exiting a specific Tab, too. |
| SetNotifyEnterExitOnChild(true); |
| |
| tab_drag_count_timer_5min_ = std::make_unique<base::RepeatingTimer>( |
| FROM_HERE, base::Minutes(5), |
| base::BindRepeating( |
| [](TabStrip* tab_strip) { |
| base::UmaHistogramCounts100("TabStrip.Dragging.Count5Min", |
| tab_strip->tab_drag_count_5min_); |
| tab_strip->tab_drag_count_5min_ = 0; |
| }, |
| base::Unretained(this))); |
| tab_drag_count_timer_30min_ = std::make_unique<base::RepeatingTimer>( |
| FROM_HERE, base::Minutes(30), |
| base::BindRepeating( |
| [](TabStrip* tab_strip) { |
| base::UmaHistogramCounts100("TabStrip.Dragging.Count30Min", |
| tab_strip->tab_drag_count_30min_); |
| tab_strip->tab_drag_count_5min_ = 0; |
| }, |
| base::Unretained(this))); |
| } |
| |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> TabStrip::GetGroupHeaders() { |
| return tab_container_->GetGroupHeaders(); |
| } |
| |
| void TabStrip::NewTabButtonPressed(const ui::Event& event) { |
| new_tab_button_pressed_start_time_ = base::TimeTicks::Now(); |
| |
| base::RecordAction(base::UserMetricsAction("NewTab_Button")); |
| GetBrowser()->profile()->SetUserData( |
| NewTabGroupingUserData::kNewTabGroupingUserDataKey, |
| std::make_unique<NewTabGroupingUserData>( |
| GetBrowser()->tab_strip_model()->GetActiveTabGroupId())); |
| if (event.IsMouseEvent()) { |
| // Prevent the hover card from popping back in immediately. This forces a |
| // normal fade-in. |
| if (hover_card_controller_) { |
| hover_card_controller_->PreventImmediateReshow(); |
| } |
| } |
| controller_->CreateNewTab(NewTabTypes::kNewTabButton); |
| } |
| |
| bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { |
| return tab_container_->InTabClose(); |
| } |
| |
| bool TabStrip::TitlebarBackgroundIsTransparent() const { |
| #if BUILDFLAG(IS_WIN) |
| return false; |
| #else |
| return GetWidget()->ShouldWindowContentsBeTransparent(); |
| #endif // BUILDFLAG(IS_WIN) |
| } |
| |
| const Tab* TabStrip::GetLastVisibleTab() const { |
| for (int i = GetTabCount() - 1; i >= 0; --i) { |
| const Tab* tab = tab_at(i); |
| |
| // The tab is marked not visible in a collapsed group, but is "visible" in |
| // the tabstrip if the header is visible. |
| if (tab->GetVisible() || |
| (tab->group().has_value() && |
| group_header(tab->group().value())->GetVisible())) { |
| return tab; |
| } |
| } |
| // While in normal use the tabstrip should always be wide enough to have at |
| // least one visible tab, it can be zero-width in tests, meaning we get here. |
| return nullptr; |
| } |
| |
| void TabStrip::CloseTabInternal(int model_index, CloseTabSource source) { |
| if (!tab_container_->InTabClose() && IsAnimatingInTabStrip()) { |
| // Cancel any current animations. We do this as remove uses the current |
| // ideal bounds and we need to know ideal bounds is in a good state. |
| tab_container_->CompleteAnimationAndLayout(); |
| } |
| |
| if (GetWidget()) { |
| // Enter tab closing mode now, but wait to calculate the width constraint |
| // until RemoveTabAt() is called, since there are code paths that go through |
| // RemoveTabAt() but not this method that must also set that constraint. |
| tab_container_->EnterTabClosingMode(std::nullopt, source); |
| } |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); |
| if (tab_at(model_index)->group().has_value()) { |
| base::RecordAction(base::UserMetricsAction("CloseGroupedTab")); |
| |
| if (controller_->GetCount() == 1) { |
| // Prevent the browser from closing when the last grouped tab is closed |
| // from the browser by adding a new tab. |
| controller_->CreateNewTab(NewTabTypes::kNoUserAction); |
| // In some situations the new tab is assigned a group. So if it is in a |
| // group, we remove it from the group so that after closing the tab at |
| // `model_index`, the browser shows a tab without a group. |
| controller_->RemoveTabFromGroup(1); |
| } |
| } |
| controller_->CloseTab(model_index); |
| } |
| |
| void TabStrip::UpdateContrastRatioValues() { |
| // There may be no controller in unit tests, and the call to |
| // GetTabBackgroundColor() below requires one, so bail early if it is absent. |
| if (!controller_) { |
| return; |
| } |
| |
| const SkColor inactive_bg = TabStyle::Get()->GetTabBackgroundColor( |
| TabStyle::TabSelectionState::kInactive, |
| /*hovered=*/false, GetWidget()->ShouldPaintAsActive(), |
| *GetColorProvider()); |
| const auto get_blend = [inactive_bg](SkColor target, float contrast) { |
| return color_utils::BlendForMinContrast(inactive_bg, inactive_bg, target, |
| contrast); |
| }; |
| |
| const SkColor active_bg = TabStyle::Get()->GetTabBackgroundColor( |
| TabStyle::TabSelectionState::kActive, /*hovered=*/false, |
| GetWidget()->ShouldPaintAsActive(), *GetColorProvider()); |
| const auto get_hover_opacity = [active_bg, &get_blend](float contrast) { |
| return get_blend(active_bg, contrast).alpha / 255.0f; |
| }; |
| |
| // The contrast ratio for the hover effect on standard-width tabs. |
| // In the default color scheme, this corresponds to a hover opacity of 0.4. |
| constexpr float kStandardWidthContrast = 1.11f; |
| hover_opacity_min_ = get_hover_opacity(kStandardWidthContrast); |
| |
| // The contrast ratio for the hover effect on min-width tabs. |
| // In the default color scheme, this corresponds to a hover opacity of 0.65. |
| constexpr float kMinWidthContrast = 1.19f; |
| hover_opacity_max_ = get_hover_opacity(kMinWidthContrast); |
| |
| // The contrast ratio for the radial gradient effect on hovered tabs. |
| // In the default color scheme, this corresponds to a hover opacity of 0.45. |
| constexpr float kRadialGradientContrast = 1.13728f; |
| radial_highlight_opacity_ = get_hover_opacity(kRadialGradientContrast); |
| |
| const SkColor inactive_fg = GetTabForegroundColor(TabActive::kInactive); |
| // The contrast ratio for the separator between inactive tabs. |
| constexpr float kTabSeparatorContrast = 2.5f; |
| separator_color_ = get_blend(inactive_fg, kTabSeparatorContrast).color; |
| |
| SchedulePaint(); |
| } |
| |
| void TabStrip::ShiftTabRelative(Tab* tab, int offset) { |
| CHECK_EQ(1, std::abs(offset)) |
| << "Offset must be 1 or -1 to shift tab left or right."; |
| const std::optional<int> maybe_start_index = GetModelIndexOf(tab); |
| if (!maybe_start_index.has_value()) { |
| return; |
| } |
| |
| const int start_index = maybe_start_index.value(); |
| int target_index = start_index + offset; |
| |
| if (tab->closing()) { |
| return; |
| } |
| |
| const auto old_group = tab->group(); |
| if (!IsValidModelIndex(target_index) || |
| controller_->IsTabPinned(start_index) != |
| controller_->IsTabPinned(target_index)) { |
| // Even if we've reached the boundary of where the tab could go, it may |
| // still be able to "move" out of its current group. |
| if (old_group.has_value()) { |
| AnnounceTabRemovedFromGroup(old_group.value()); |
| controller_->RemoveTabFromGroup(start_index); |
| } |
| return; |
| } |
| |
| // If the tab is at a group boundary and the group is expanded, instead of |
| // actually moving the tab just change its group membership. |
| std::optional<tab_groups::TabGroupId> target_group = |
| tab_at(target_index)->group(); |
| if (old_group != target_group) { |
| if (old_group.has_value()) { |
| AnnounceTabRemovedFromGroup(old_group.value()); |
| controller_->RemoveTabFromGroup(start_index); |
| return; |
| } else if (target_group.has_value()) { |
| // If the tab is at a group boundary and the group is collapsed, treat the |
| // collapsed group as a tab and find the next available slot for the tab |
| // to move to. |
| if (IsGroupCollapsed(target_group.value())) { |
| int candidate_index = target_index + offset; |
| while (IsValidModelIndex(candidate_index) && |
| tab_at(candidate_index)->group() == target_group) { |
| candidate_index += offset; |
| } |
| if (IsValidModelIndex(candidate_index)) { |
| target_index = candidate_index - offset; |
| } else { |
| target_index = offset < 0 ? 0 : GetModelCount() - 1; |
| } |
| } else { |
| // Read before adding the tab to the group so that the group description |
| // isn't the tab we just added. |
| AnnounceTabAddedToGroup(target_group.value()); |
| controller_->AddTabToGroup(start_index, target_group.value()); |
| views::ElementTrackerViews::GetInstance()->NotifyCustomEvent( |
| kTabGroupedCustomEventId, tab); |
| return; |
| } |
| } |
| } |
| |
| controller_->MoveTab(start_index, target_index); |
| GetViewAccessibility().AnnounceText(l10n_util::GetStringUTF16( |
| ((offset > 0) ^ base::i18n::IsRTL()) ? IDS_TAB_AX_ANNOUNCE_MOVED_RIGHT |
| : IDS_TAB_AX_ANNOUNCE_MOVED_LEFT)); |
| } |
| |
| void TabStrip::ShiftGroupRelative(const tab_groups::TabGroupId& group, |
| int offset) { |
| CHECK_EQ(1, std::abs(offset)) |
| << "Offset must be 1 or -1 to shift the group left or right."; |
| gfx::Range tabs_in_group = controller_->ListTabsInGroup(group); |
| |
| const int start_index = tabs_in_group.start(); |
| const int index_of_skipped_over_tab = |
| offset == 1 ? start_index + tabs_in_group.length() : start_index - 1; |
| |
| if (!IsValidModelIndex(start_index) || |
| !IsValidModelIndex(index_of_skipped_over_tab)) { |
| return; |
| } |
| |
| if (controller_->IsTabPinned(index_of_skipped_over_tab)) { |
| return; |
| } |
| |
| // Avoid moving into the middle of another group by accounting for its size. |
| std::optional<tab_groups::TabGroupId> target_group = |
| tab_at(index_of_skipped_over_tab)->group(); |
| if (target_group.has_value()) { |
| CHECK_NE(target_group.value(), group) |
| << "The target group must be different from the current group to move."; |
| } |
| |
| const int num_skipped_tabs = |
| target_group.has_value() |
| ? controller_->ListTabsInGroup(target_group.value()).length() |
| : 1; |
| |
| const int target_index = start_index + offset * num_skipped_tabs; |
| controller_->MoveGroup(group, target_index); |
| } |
| |
| // TabStrip:TabContextMenuController: |
| // ---------------------------------------------------------- |
| |
| TabStrip::TabContextMenuController::TabContextMenuController(TabStrip* parent) |
| : parent_(parent) {} |
| |
| void TabStrip::TabContextMenuController::ShowContextMenuForViewImpl( |
| views::View* source, |
| const gfx::Point& point, |
| ui::mojom::MenuSourceType source_type) { |
| // We are only intended to be installed as a context-menu handler for tabs, so |
| // this cast should be safe. |
| CHECK(views::IsViewClass<Tab>(source)) << "The source must be a Tab class."; |
| Tab* const tab = static_cast<Tab*>(source); |
| if (tab->closing()) { |
| return; |
| } |
| parent_->ShowContextMenuForTab(tab, point, source_type); |
| } |
| |
| void TabStrip::OnMouseEntered(const ui::MouseEvent& event) { |
| mouse_entered_tabstrip_time_ = base::TimeTicks::Now(); |
| has_reported_time_mouse_entered_to_switch_ = false; |
| } |
| |
| void TabStrip::OnMouseExited(const ui::MouseEvent& event) { |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kHover); |
| } |
| |
| void TabStrip::AddedToWidget() { |
| GetWidget()->AddObserver(this); |
| paint_as_active_subscription_ = |
| GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating( |
| &TabStrip::UpdateContrastRatioValues, base::Unretained(this))); |
| } |
| |
| void TabStrip::RemovedFromWidget() { |
| GetWidget()->RemoveObserver(this); |
| paint_as_active_subscription_ = {}; |
| } |
| |
| void TabStrip::OnThemeChanged() { |
| views::View::OnThemeChanged(); |
| UpdateContrastRatioValues(); |
| } |
| |
| void TabStrip::OnGestureEvent(ui::GestureEvent* event) { |
| switch (event->type()) { |
| case ui::EventType::kGestureLongTap: { |
| tab_container_->HandleLongTap(event); |
| break; |
| } |
| |
| default: |
| break; |
| } |
| event->SetHandled(); |
| } |
| |
| void TabStrip::OnViewFocused(views::View* observed_view) { |
| TabSlotView* slot_view = views::AsViewClass<TabSlotView>(observed_view); |
| if (!slot_view) { |
| return; |
| } |
| |
| std::optional<int> index = GetModelIndexOf(slot_view); |
| if (index.has_value()) { |
| controller_->OnKeyboardFocusedTabChanged(index); |
| } |
| } |
| |
| void TabStrip::OnViewBlurred(views::View* observed_view) { |
| controller_->OnKeyboardFocusedTabChanged(std::nullopt); |
| } |
| |
| void TabStrip::OnTouchUiChanged() { |
| tab_container_->CompleteAnimationAndLayout(); |
| PreferredSizeChanged(); |
| } |
| |
| void TabStrip::AnnounceTabAddedToGroup(tab_groups::TabGroupId group_id) { |
| const std::u16string group_title = GetGroupTitle(group_id); |
| const std::u16string contents_string = GetGroupContentString(group_id); |
| GetViewAccessibility().AnnounceText( |
| group_title.empty() |
| ? l10n_util::GetStringFUTF16( |
| IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_UNNAMED_GROUP, contents_string) |
| : l10n_util::GetStringFUTF16( |
| IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_NAMED_GROUP, group_title, |
| contents_string)); |
| } |
| |
| void TabStrip::AnnounceTabRemovedFromGroup(tab_groups::TabGroupId group_id) { |
| const std::u16string group_title = GetGroupTitle(group_id); |
| const std::u16string contents_string = GetGroupContentString(group_id); |
| GetViewAccessibility().AnnounceText( |
| group_title.empty() |
| ? l10n_util::GetStringFUTF16( |
| IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_UNNAMED_GROUP, |
| contents_string) |
| : l10n_util::GetStringFUTF16( |
| IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_NAMED_GROUP, group_title, |
| contents_string)); |
| } |
| |
| BEGIN_METADATA(TabStrip) |
| ADD_READONLY_PROPERTY_METADATA(int, TabCount) |
| ADD_READONLY_PROPERTY_METADATA(int, ModelCount) |
| ADD_READONLY_PROPERTY_METADATA(int, ModelPinnedTabCount) |
| ADD_READONLY_PROPERTY_METADATA(int, StrokeThickness) |
| ADD_READONLY_PROPERTY_METADATA(SkColor, |
| TabSeparatorColor, |
| ui::metadata::SkColorConverter) |
| ADD_READONLY_PROPERTY_METADATA(float, HoverOpacityForRadialHighlight) |
| END_METADATA |