| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <limits> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/compiler_specific.h" |
| #include "base/containers/adapters.h" |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_map.h" |
| #include "base/cxx17_backports.h" |
| #include "base/feature_list.h" |
| #include "base/i18n/rtl.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/scoped_observation.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "base/timer/elapsed_timer.h" |
| #include "build/build_config.h" |
| #include "build/chromeos_buildflags.h" |
| #include "chrome/browser/defaults.h" |
| #include "chrome/browser/themes/theme_properties.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/tab_group_theme.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/tabs/tab_types.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/view_ids.h" |
| #include "chrome/browser/ui/views/frame/browser_view.h" |
| #include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h" |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| #include "chrome/browser/ui/views/tabs/tab_drag_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_header.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_highlight.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_underline.h" |
| #include "chrome/browser/ui/views/tabs/tab_group_views.h" |
| #include "chrome/browser/ui/views/tabs/tab_hover_card_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_style_views.h" |
| #include "chrome/browser/ui/views/touch_uma/touch_uma.h" |
| #include "chrome/browser/ui/web_applications/app_browser_controller.h" |
| #include "chrome/grit/generated_resources.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "components/tab_groups/tab_group_color.h" |
| #include "components/tab_groups/tab_group_id.h" |
| #include "components/tab_groups/tab_group_visual_data.h" |
| #include "third_party/skia/include/core/SkColorFilter.h" |
| #include "third_party/skia/include/core/SkPath.h" |
| #include "third_party/skia/include/pathops/SkPathOps.h" |
| #include "ui/base/clipboard/clipboard.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/resource/resource_bundle.h" |
| #include "ui/base/theme_provider.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/gfx/animation/throb_animation.h" |
| #include "ui/gfx/animation/tween.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/gfx/geometry/size.h" |
| #include "ui/gfx/image/image_skia.h" |
| #include "ui/gfx/image/image_skia_operations.h" |
| #include "ui/gfx/native_widget_types.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/gfx/skia_util.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/cascading_property.h" |
| #include "ui/views/controls/image_view.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/masked_targeter_delegate.h" |
| #include "ui/views/mouse_watcher_view_host.h" |
| #include "ui/views/rect_based_targeting_utils.h" |
| #include "ui/views/view_model_utils.h" |
| #include "ui/views/view_observer.h" |
| #include "ui/views/view_targeter.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/views/window/non_client_view.h" |
| |
| #if defined(OS_WIN) |
| #include "base/win/windows_version.h" |
| #include "ui/display/win/screen_win.h" |
| #include "ui/gfx/win/hwnd_util.h" |
| #include "ui/views/win/hwnd_util.h" |
| #endif |
| |
| #if defined(USE_AURA) |
| #include "ui/aura/window.h" |
| #endif |
| |
| namespace { |
| |
| // Distance from the next/previous stacked before before we consider the tab |
| // close enough to trigger moving. |
| const int kStackedDistance = 36; |
| |
| // Given the bounds of a dragged tab, return the X coordinate to use for |
| // computing where in the strip to insert/move the tab. |
| int GetDraggedX(const gfx::Rect& dragged_bounds) { |
| return dragged_bounds.x() + TabStyle::GetTabInternalPadding().left(); |
| } |
| |
| // Max number of stacked tabs. |
| constexpr int kMaxStackedCount = 4; |
| |
| // Padding between stacked tabs. |
| constexpr int kStackedPadding = 6; |
| |
| // Size of the drop indicator. |
| int g_drop_indicator_width = 0; |
| int g_drop_indicator_height = 0; |
| |
| // Provides the ability to monitor when a tab's bounds have been animated. Used |
| // to hook callbacks to adjust things like tabstrip preferred size and tab group |
| // underlines. |
| class TabSlotAnimationDelegate : public gfx::AnimationDelegate { |
| public: |
| using OnAnimationProgressedCallback = |
| base::RepeatingCallback<void(TabSlotView*)>; |
| |
| TabSlotAnimationDelegate( |
| TabStrip* tab_strip, |
| TabSlotView* slot_view, |
| OnAnimationProgressedCallback on_animation_progressed); |
| TabSlotAnimationDelegate(const TabSlotAnimationDelegate&) = delete; |
| TabSlotAnimationDelegate& operator=(const TabSlotAnimationDelegate&) = delete; |
| ~TabSlotAnimationDelegate() override; |
| |
| void AnimationProgressed(const gfx::Animation* animation) override; |
| void AnimationEnded(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| |
| protected: |
| TabStrip* tab_strip() { return tab_strip_; } |
| TabSlotView* slot_view() { return slot_view_; } |
| |
| private: |
| TabStrip* const tab_strip_; |
| TabSlotView* const slot_view_; |
| OnAnimationProgressedCallback on_animation_progressed_; |
| }; |
| |
| TabSlotAnimationDelegate::TabSlotAnimationDelegate( |
| TabStrip* tab_strip, |
| TabSlotView* slot_view, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : tab_strip_(tab_strip), |
| slot_view_(slot_view), |
| on_animation_progressed_(on_animation_progressed) { |
| slot_view_->set_animating(true); |
| } |
| |
| TabSlotAnimationDelegate::~TabSlotAnimationDelegate() = default; |
| |
| void TabSlotAnimationDelegate::AnimationProgressed( |
| const gfx::Animation* animation) { |
| on_animation_progressed_.Run(slot_view()); |
| } |
| |
| void TabSlotAnimationDelegate::AnimationEnded(const gfx::Animation* animation) { |
| slot_view_->set_animating(false); |
| AnimationProgressed(animation); |
| slot_view_->Layout(); |
| } |
| |
| void TabSlotAnimationDelegate::AnimationCanceled( |
| const gfx::Animation* animation) { |
| AnimationEnded(animation); |
| } |
| |
| // Animation delegate used when a dragged tab is released. When done sets the |
| // dragging state to false. |
| class ResetDraggingStateDelegate : public TabSlotAnimationDelegate { |
| public: |
| ResetDraggingStateDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed); |
| ResetDraggingStateDelegate(const ResetDraggingStateDelegate&) = delete; |
| ResetDraggingStateDelegate& operator=(const ResetDraggingStateDelegate&) = |
| delete; |
| ~ResetDraggingStateDelegate() override; |
| |
| void AnimationEnded(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| }; |
| |
| ResetDraggingStateDelegate::ResetDraggingStateDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {} |
| |
| ResetDraggingStateDelegate::~ResetDraggingStateDelegate() = default; |
| |
| void ResetDraggingStateDelegate::AnimationEnded( |
| const gfx::Animation* animation) { |
| static_cast<Tab*>(slot_view())->set_dragging(false); |
| TabSlotAnimationDelegate::AnimationEnded(animation); |
| } |
| |
| void ResetDraggingStateDelegate::AnimationCanceled( |
| const gfx::Animation* animation) { |
| AnimationEnded(animation); |
| } |
| |
| // If |dest| contains the point |point_in_source| the event handler from |dest| |
| // is returned. Otherwise returns null. |
| views::View* ConvertPointToViewAndGetEventHandler( |
| views::View* source, |
| views::View* dest, |
| const gfx::Point& point_in_source) { |
| gfx::Point dest_point(point_in_source); |
| views::View::ConvertPointToTarget(source, dest, &dest_point); |
| return dest->HitTestPoint(dest_point) |
| ? dest->GetEventHandlerForPoint(dest_point) |
| : nullptr; |
| } |
| |
| // Gets a tooltip handler for |point_in_source| from |dest|. Note that |dest| |
| // should return null if it does not contain the point. |
| views::View* ConvertPointToViewAndGetTooltipHandler( |
| views::View* source, |
| views::View* dest, |
| const gfx::Point& point_in_source) { |
| gfx::Point dest_point(point_in_source); |
| views::View::ConvertPointToTarget(source, dest, &dest_point); |
| return dest->GetTooltipHandlerForPoint(dest_point); |
| } |
| |
| TabDragController::EventSource EventSourceFromEvent( |
| const ui::LocatedEvent& event) { |
| return event.IsGestureEvent() ? TabDragController::EVENT_SOURCE_TOUCH |
| : TabDragController::EVENT_SOURCE_MOUSE; |
| } |
| |
| int GetStackableTabWidth() { |
| return TabStyle::GetTabOverlap() + |
| (ui::TouchUiController::Get()->touch_ui() ? 136 : 102); |
| } |
| |
| // Helper class that manages the tab scrolling animation. |
| class TabScrollingAnimation : public gfx::LinearAnimation, |
| public gfx::AnimationDelegate { |
| public: |
| explicit TabScrollingAnimation( |
| TabStrip* tab_strip, |
| gfx::AnimationContainer* bounds_animator_container, |
| base::TimeDelta duration, |
| const gfx::Rect start_visible_rect, |
| const gfx::Rect end_visible_rect) |
| : gfx::LinearAnimation(duration, |
| gfx::LinearAnimation::kDefaultFrameRate, |
| this), |
| tab_strip_(tab_strip), |
| start_visible_rect_(start_visible_rect), |
| end_visible_rect_(end_visible_rect) { |
| SetContainer(bounds_animator_container); |
| } |
| TabScrollingAnimation(const TabScrollingAnimation&) = delete; |
| TabScrollingAnimation& operator=(const TabScrollingAnimation&) = delete; |
| ~TabScrollingAnimation() override = default; |
| |
| void AnimateToState(double state) override { |
| gfx::Rect intermediary_rect( |
| start_visible_rect_.x() + |
| (end_visible_rect_.x() - start_visible_rect_.x()) * state, |
| start_visible_rect_.y(), start_visible_rect_.width(), |
| start_visible_rect_.height()); |
| |
| tab_strip_->ScrollRectToVisible(intermediary_rect); |
| } |
| |
| void AnimationEnded(const gfx::Animation* animation) override { |
| tab_strip_->ScrollRectToVisible(end_visible_rect_); |
| } |
| |
| private: |
| TabStrip* const tab_strip_; |
| const gfx::Rect start_visible_rect_; |
| const gfx::Rect end_visible_rect_; |
| }; |
| |
| } // namespace |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip::RemoveTabDelegate |
| // |
| // AnimationDelegate used when removing a tab. Does the necessary cleanup when |
| // done. |
| class TabStrip::RemoveTabDelegate : public TabSlotAnimationDelegate { |
| public: |
| RemoveTabDelegate(TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed); |
| RemoveTabDelegate(const RemoveTabDelegate&) = delete; |
| RemoveTabDelegate& operator=(const RemoveTabDelegate&) = delete; |
| |
| void AnimationEnded(const gfx::Animation* animation) override; |
| void AnimationCanceled(const gfx::Animation* animation) override; |
| }; |
| |
| TabStrip::RemoveTabDelegate::RemoveTabDelegate( |
| TabStrip* tab_strip, |
| Tab* tab, |
| OnAnimationProgressedCallback on_animation_progressed) |
| : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {} |
| |
| void TabStrip::RemoveTabDelegate::AnimationEnded( |
| const gfx::Animation* animation) { |
| tab_strip()->OnTabCloseAnimationCompleted(static_cast<Tab*>(slot_view())); |
| } |
| |
| void TabStrip::RemoveTabDelegate::AnimationCanceled( |
| const gfx::Animation* animation) { |
| AnimationEnded(animation); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip::TabDragContextImpl |
| // |
| class TabStrip::TabDragContextImpl : public TabDragContext { |
| public: |
| explicit TabDragContextImpl(TabStrip* tab_strip) : tab_strip_(tab_strip) {} |
| |
| bool IsDragStarted() const { |
| return drag_controller_ && drag_controller_->started_drag(); |
| } |
| |
| bool IsMutating() const { |
| return drag_controller_ && drag_controller_->is_mutating(); |
| } |
| |
| bool IsDraggingWindow() const { |
| return drag_controller_ && drag_controller_->is_dragging_window(); |
| } |
| |
| bool IsDraggingTab(content::WebContents* contents) const { |
| return contents && drag_controller_ && |
| drag_controller_->IsDraggingTab(contents); |
| } |
| |
| void SetMoveBehavior(TabDragController::MoveBehavior move_behavior) { |
| if (drag_controller_) |
| drag_controller_->SetMoveBehavior(move_behavior); |
| } |
| |
| void MaybeStartDrag(TabSlotView* source, |
| const ui::LocatedEvent& event, |
| const ui::ListSelectionModel& original_selection) { |
| std::vector<TabSlotView*> dragging_views; |
| int x = source->GetMirroredXInView(event.x()); |
| int y = event.y(); |
| |
| // Build the set of selected tabs to drag and calculate the offset from the |
| // source. |
| ui::ListSelectionModel selection_model; |
| if (source->GetTabSlotViewType() == |
| TabSlotView::ViewType::kTabGroupHeader) { |
| dragging_views.push_back(source); |
| |
| const gfx::Range grouped_tabs = |
| tab_strip_->controller()->ListTabsInGroup(source->group().value()); |
| for (auto index = grouped_tabs.start(); index < grouped_tabs.end(); |
| ++index) { |
| dragging_views.push_back(GetTabAt(index)); |
| // Set |selection_model| if and only if the original selection does not |
| // match the group exactly. See TabDragController::Init() for details |
| // on how |selection_model| is used. |
| if (!original_selection.IsSelected(index)) |
| selection_model = original_selection; |
| } |
| if (grouped_tabs.length() != original_selection.size()) |
| selection_model = original_selection; |
| } else { |
| for (int i = 0; i < GetTabCount(); ++i) { |
| Tab* other_tab = GetTabAt(i); |
| if (tab_strip_->IsTabSelected(other_tab)) { |
| dragging_views.push_back(other_tab); |
| if (other_tab == source) |
| x += GetSizeNeededForViews(dragging_views) - other_tab->width(); |
| } |
| } |
| if (!original_selection.IsSelected(tab_strip_->GetModelIndexOf(source))) |
| selection_model = original_selection; |
| } |
| |
| DCHECK(!dragging_views.empty()); |
| DCHECK(base::Contains(dragging_views, source)); |
| |
| // Delete the existing DragController before creating a new one. We do this |
| // as creating the DragController remembers the WebContents delegates and we |
| // need to make sure the existing DragController isn't still a delegate. |
| drag_controller_.reset(); |
| TabDragController::MoveBehavior move_behavior = TabDragController::REORDER; |
| |
| // Use MOVE_VISIBLE_TABS in the following conditions: |
| // . Mouse event generated from touch and the left button is down (the right |
| // button corresponds to a long press, which we want to reorder). |
| // . Gesture tap down and control key isn't down. |
| // . Real mouse event and control is down. This is mostly for testing. |
| DCHECK(event.type() == ui::ET_MOUSE_PRESSED || |
| event.type() == ui::ET_GESTURE_TAP_DOWN || |
| event.type() == ui::ET_GESTURE_SCROLL_BEGIN); |
| if (tab_strip_->touch_layout_ && |
| ((event.type() == ui::ET_MOUSE_PRESSED && |
| (((event.flags() & ui::EF_FROM_TOUCH) && |
| static_cast<const ui::MouseEvent&>(event).IsLeftMouseButton()) || |
| (!(event.flags() & ui::EF_FROM_TOUCH) && |
| static_cast<const ui::MouseEvent&>(event).IsControlDown()))) || |
| (event.type() == ui::ET_GESTURE_TAP_DOWN && !event.IsControlDown()) || |
| (event.type() == ui::ET_GESTURE_SCROLL_BEGIN && |
| !event.IsControlDown()))) { |
| move_behavior = TabDragController::MOVE_VISIBLE_TABS; |
| } |
| |
| drag_controller_ = std::make_unique<TabDragController>(); |
| drag_controller_->Init(this, source, dragging_views, gfx::Point(x, y), |
| event.x(), std::move(selection_model), move_behavior, |
| EventSourceFromEvent(event)); |
| if (drag_controller_set_callback_) |
| std::move(drag_controller_set_callback_).Run(drag_controller_.get()); |
| } |
| |
| void ContinueDrag(views::View* view, const ui::LocatedEvent& event) { |
| if (drag_controller_.get() && |
| drag_controller_->event_source() == EventSourceFromEvent(event)) { |
| gfx::Point screen_location(event.location()); |
| views::View::ConvertPointToScreen(view, &screen_location); |
| |
| // Note: |tab_strip_| can be destroyed during drag, also destroying |
| // |this|. |
| base::WeakPtr<TabDragContext> weak_ptr(weak_factory_.GetWeakPtr()); |
| drag_controller_->Drag(screen_location); |
| |
| if (!weak_ptr) |
| return; |
| } |
| |
| // Note: |drag_controller| can be set to null during the drag above. |
| if (drag_controller_ && drag_controller_->group()) |
| tab_strip_->UpdateTabGroupVisuals(*drag_controller_->group()); |
| } |
| |
| bool EndDrag(EndDragReason reason) { |
| if (!drag_controller_.get()) |
| return false; |
| bool started_drag = drag_controller_->started_drag(); |
| drag_controller_->EndDrag(reason); |
| return started_drag; |
| } |
| |
| // TabDragContext: |
| views::View* AsView() override { return tab_strip_; } |
| |
| const views::View* AsView() const override { return tab_strip_; } |
| |
| Tab* GetTabAt(int i) const override { return tab_strip_->tab_at(i); } |
| |
| int GetIndexOf(const TabSlotView* view) const override { |
| return tab_strip_->GetModelIndexOf(view); |
| } |
| |
| int GetTabCount() const override { return tab_strip_->GetTabCount(); } |
| |
| bool IsTabPinned(const Tab* tab) const override { |
| return tab_strip_->IsTabPinned(tab); |
| } |
| |
| int GetPinnedTabCount() const override { |
| return tab_strip_->GetPinnedTabCount(); |
| } |
| |
| TabGroupHeader* GetTabGroupHeader( |
| const tab_groups::TabGroupId& group) const override { |
| return tab_strip_->group_header(group); |
| } |
| |
| TabStripModel* GetTabStripModel() override { |
| return static_cast<BrowserTabStripController*>( |
| tab_strip_->controller_.get()) |
| ->model(); |
| } |
| |
| absl::optional<int> GetActiveTouchIndex() const override { |
| if (!tab_strip_->touch_layout_) |
| return absl::nullopt; |
| return tab_strip_->touch_layout_->active_index(); |
| } |
| |
| TabDragController* GetDragController() override { |
| return drag_controller_.get(); |
| } |
| |
| void OwnDragController( |
| std::unique_ptr<TabDragController> controller) override { |
| DCHECK(controller); |
| DCHECK(!drag_controller_); |
| 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); |
| } |
| |
| bool IsDragSessionActive() const override { |
| return drag_controller_ != nullptr; |
| } |
| |
| bool IsActiveDropTarget() const override { |
| for (int i = 0; i < GetTabCount(); ++i) { |
| const Tab* const tab = GetTabAt(i); |
| if (tab->dragging()) |
| return true; |
| } |
| return false; |
| } |
| |
| std::vector<int> GetTabXCoordinates() const override { |
| std::vector<int> results; |
| for (int i = 0; i < GetTabCount(); ++i) |
| results.push_back(ideal_bounds(i).x()); |
| return results; |
| } |
| |
| int GetActiveTabWidth() const override { |
| return tab_strip_->GetActiveTabWidth(); |
| } |
| |
| int GetTabDragAreaWidth() const override { |
| // There are two cases here (with tab scrolling enabled): |
| // 1) If the tab strip is not wider than the tab strip region (and thus |
| // not scrollable), returning the available width for tabs rather than the |
| // actual width for tabs allows tabs to be dragged past the current bounds |
| // of the tabstrip, anywhere along the tab strip region. |
| // N.B. The available width for tabs in this case needs to ignore tab |
| // closing mode. |
| // 2) If the tabstrip is wider than the tab strip region (and thus is |
| // scrollable), returning the tabstrip width allows tabs to be dragged |
| // anywhere within the tabstrip, not just in the leftmost region of it. |
| return std::max(tab_strip_->GetAvailableWidthForTabStrip(), |
| tab_strip_->width()); |
| } |
| |
| int TabDragAreaBeginX() const override { |
| return tab_strip_->GetMirroredXWithWidthInView(0, GetTabDragAreaWidth()); |
| } |
| |
| int TabDragAreaEndX() const override { |
| return TabDragAreaBeginX() + GetTabDragAreaWidth(); |
| } |
| |
| int GetHorizontalDragThreshold() const override { |
| constexpr int kHorizontalMoveThreshold = 16; // DIPs. |
| |
| // Stacked tabs in touch mode don't shrink. |
| if (tab_strip_->touch_layout_) |
| return kHorizontalMoveThreshold; |
| |
| double ratio = static_cast<double>(tab_strip_->GetInactiveTabWidth()) / |
| TabStyle::GetStandardWidth(); |
| return base::ClampRound(ratio * kHorizontalMoveThreshold); |
| } |
| |
| int GetInsertionIndexForDraggedBounds( |
| const gfx::Rect& dragged_bounds, |
| std::vector<TabSlotView*> dragged_views, |
| int num_dragged_tabs, |
| bool mouse_has_ever_moved_left, |
| bool mouse_has_ever_moved_right, |
| absl::optional<tab_groups::TabGroupId> group) const override { |
| // If the strip has no tabs, the only position to insert at is 0. |
| if (!GetTabCount()) |
| return 0; |
| |
| absl::optional<int> index; |
| absl::optional<int> touch_index = GetActiveTouchIndex(); |
| if (touch_index) { |
| index = GetInsertionIndexForDraggedBoundsStacked( |
| dragged_bounds, mouse_has_ever_moved_left, |
| mouse_has_ever_moved_right); |
| if (index) { |
| // Only move the tab to the left/right if the user actually moved the |
| // mouse that way. This is necessary as tabs with stacked tabs |
| // before/after them have multiple drag positions. |
| if ((index < touch_index && !mouse_has_ever_moved_left) || |
| (index > touch_index && !mouse_has_ever_moved_right)) { |
| index = *touch_index; |
| } |
| } |
| } else { |
| // If we're dragging a group by its header, the first element of |
| // |dragged_views| is a group header, and the second one is the first tab |
| // in that group. |
| int first_dragged_tab_index = group.has_value() ? 1 : 0; |
| if (static_cast<size_t>(first_dragged_tab_index) >= |
| dragged_views.size()) { |
| // TODO(tbergquist): This shouldn't happen, but we're getting crashes |
| // that indicate that it might be anyways. This logging might help |
| // narrow down exactly which cases it's happening in. |
| NOTREACHED() |
| << "Calculating a drag insertion index from invalid dependencies: " |
| << "Dragging a group: " << group.has_value() |
| << ", dragged_views.size(): " << dragged_views.size() |
| << ", num_dragged_tabs: " << num_dragged_tabs; |
| } else { |
| int first_dragged_tab_model_index = |
| tab_strip_->GetModelIndexOf(dragged_views[first_dragged_tab_index]); |
| index = CalculateInsertionIndex(dragged_bounds, |
| first_dragged_tab_model_index, |
| num_dragged_tabs, std::move(group)); |
| } |
| } |
| if (!index) { |
| const int last_tab_right = ideal_bounds(GetTabCount() - 1).right(); |
| index = (dragged_bounds.right() > last_tab_right) ? GetTabCount() : 0; |
| } |
| |
| const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab(); |
| int last_insertion_point = |
| last_visible_tab ? (GetIndexOf(last_visible_tab) + 1) : 0; |
| |
| // Clamp the insertion point to keep it within the visible region. |
| last_insertion_point = std::max(0, last_insertion_point - num_dragged_tabs); |
| |
| // Ensure the first dragged tab always stays in the visible index range. |
| return std::min(*index, last_insertion_point); |
| } |
| |
| bool ShouldDragToNextStackedTab( |
| const gfx::Rect& dragged_bounds, |
| int index, |
| bool mouse_has_ever_moved_right) const override { |
| if (index + 1 >= GetTabCount() || |
| !tab_strip_->touch_layout_->IsStacked(index + 1) || |
| !mouse_has_ever_moved_right) |
| return false; |
| |
| int active_x = ideal_bounds(index).x(); |
| int next_x = ideal_bounds(index + 1).x(); |
| int mid_x = |
| std::min(next_x - kStackedDistance, active_x + (next_x - active_x) / 4); |
| return GetDraggedX(dragged_bounds) >= mid_x; |
| } |
| |
| bool ShouldDragToPreviousStackedTab( |
| const gfx::Rect& dragged_bounds, |
| int index, |
| bool mouse_has_ever_moved_left) const override { |
| if (index - 1 < tab_strip_->GetPinnedTabCount() || |
| !tab_strip_->touch_layout_->IsStacked(index - 1) || |
| !mouse_has_ever_moved_left) |
| return false; |
| |
| int active_x = ideal_bounds(index).x(); |
| int previous_x = ideal_bounds(index - 1).x(); |
| int mid_x = std::max(previous_x + kStackedDistance, |
| active_x - (active_x - previous_x) / 4); |
| return GetDraggedX(dragged_bounds) <= mid_x; |
| } |
| |
| void DragActiveTabStacked(const std::vector<int>& initial_positions, |
| int delta) override { |
| DCHECK_EQ(GetTabCount(), static_cast<int>(initial_positions.size())); |
| SetIdealBoundsFromPositions(initial_positions); |
| tab_strip_->touch_layout_->DragActiveTab(delta); |
| tab_strip_->CompleteAnimationAndLayout(); |
| } |
| |
| std::vector<gfx::Rect> CalculateBoundsForDraggedViews( |
| const std::vector<TabSlotView*>& views) override { |
| DCHECK(!views.empty()); |
| |
| std::vector<gfx::Rect> bounds; |
| const int overlap = TabStyle::GetTabOverlap(); |
| int x = 0; |
| for (const TabSlotView* view : views) { |
| const int width = view->width(); |
| bounds.push_back(gfx::Rect(x, 0, width, view->height())); |
| x += width - overlap; |
| } |
| |
| return bounds; |
| } |
| |
| void SetBoundsForDrag(const std::vector<TabSlotView*>& views, |
| const std::vector<gfx::Rect>& bounds) override { |
| tab_strip_->StopAnimating(false); |
| DCHECK_EQ(views.size(), bounds.size()); |
| for (size_t i = 0; i < views.size(); ++i) |
| views[i]->SetBoundsRect(bounds[i]); |
| |
| // Ensure that the tab strip and its parent views are correctly re-laid out |
| // after repositioning dragged tabs. This avoids visual/layout issues such |
| // as https://crbug.com/1151092. |
| tab_strip_->PreferredSizeChanged(); |
| |
| // Reset the layout size as we've effectively laid out a different size. |
| // This ensures a layout happens after the drag is done. |
| tab_strip_->last_layout_size_ = gfx::Size(); |
| if (views.at(0)->group().has_value()) |
| tab_strip_->UpdateTabGroupVisuals(views.at(0)->group().value()); |
| } |
| |
| void StartedDragging(const std::vector<TabSlotView*>& views) override { |
| // Let the controller know that the user started dragging tabs. |
| tab_strip_->controller_->OnStartedDragging( |
| views.size() == static_cast<size_t>(tab_strip_->GetModelCount())); |
| |
| // Reset dragging state of existing tabs. |
| for (int i = 0; i < GetTabCount(); ++i) |
| GetTabAt(i)->set_dragging(false); |
| |
| for (size_t i = 0; i < views.size(); ++i) { |
| views[i]->set_dragging(true); |
| tab_strip_->bounds_animator_.StopAnimatingView(views[i]); |
| } |
| |
| // Move the dragged tabs to their ideal bounds. |
| tab_strip_->UpdateIdealBounds(); |
| |
| // Sets the bounds of the dragged tab slots. |
| for (TabSlotView* view : views) { |
| if (view->GetTabSlotViewType() == |
| TabSlotView::ViewType::kTabGroupHeader) { |
| view->SetBoundsRect(ideal_bounds(view->group().value())); |
| } else { |
| int tab_data_index = GetIndexOf(view); |
| DCHECK_NE(TabStripModel::kNoTab, tab_data_index); |
| view->SetBoundsRect(ideal_bounds(tab_data_index)); |
| } |
| } |
| |
| tab_strip_->SetTabSlotVisibility(); |
| tab_strip_->SchedulePaint(); |
| } |
| |
| void DraggedTabsDetached() override { |
| // Let the controller know that the user is not dragging this tabstrip's |
| // tabs anymore. |
| tab_strip_->controller_->OnStoppedDragging(); |
| } |
| |
| void StoppedDragging(const std::vector<TabSlotView*>& views, |
| const std::vector<int>& initial_positions, |
| bool move_only, |
| bool completed) override { |
| // Let the controller know that the user stopped dragging tabs. |
| tab_strip_->controller_->OnStoppedDragging(); |
| |
| if (move_only && tab_strip_->touch_layout_) { |
| if (completed) |
| tab_strip_->touch_layout_->SizeToFit(); |
| else |
| SetIdealBoundsFromPositions(initial_positions); |
| } |
| bool is_first_view = true; |
| for (size_t i = 0; i < views.size(); ++i) |
| tab_strip_->StoppedDraggingView(views[i], &is_first_view); |
| } |
| |
| void LayoutDraggedViewsAt(const std::vector<TabSlotView*>& views, |
| TabSlotView* source_view, |
| const gfx::Point& location, |
| bool initial_drag) override { |
| std::vector<gfx::Rect> bounds = CalculateBoundsForDraggedViews(views); |
| DCHECK_EQ(views.size(), bounds.size()); |
| |
| int active_tab_model_index = GetIndexOf(source_view); |
| int active_tab_index = static_cast<int>( |
| std::find(views.begin(), views.end(), source_view) - views.begin()); |
| for (size_t i = 0; i < views.size(); ++i) { |
| TabSlotView* view = views[i]; |
| gfx::Rect new_bounds = bounds[i]; |
| new_bounds.Offset(location.x(), location.y()); |
| int consecutive_index = |
| active_tab_model_index - (active_tab_index - static_cast<int>(i)); |
| // If this is the initial layout during a drag and the tabs aren't |
| // consecutive animate the view into position. Do the same if the tab is |
| // already animating (which means we previously caused it to animate). |
| if ((initial_drag && GetIndexOf(views[i]) != consecutive_index) || |
| tab_strip_->bounds_animator_.IsAnimating(views[i])) { |
| tab_strip_->bounds_animator_.SetTargetBounds(views[i], new_bounds); |
| } else { |
| view->SetBoundsRect(new_bounds); |
| } |
| } |
| tab_strip_->SetTabSlotVisibility(); |
| // The rightmost tab may have moved, which would change the tabstrip's |
| // preferred width. |
| tab_strip_->PreferredSizeChanged(); |
| } |
| |
| // Forces the entire tabstrip to lay out. |
| void ForceLayout() override { |
| tab_strip_->InvalidateLayout(); |
| tab_strip_->CompleteAnimationAndLayout(); |
| } |
| |
| private: |
| gfx::Rect ideal_bounds(int i) const { return tab_strip_->ideal_bounds(i); } |
| |
| gfx::Rect ideal_bounds(tab_groups::TabGroupId group) const { |
| return tab_strip_->ideal_bounds(group); |
| } |
| |
| // Determines the index to move the dragged tabs to. The dragged tabs must |
| // already be in the tabstrip. |dragged_bounds| is the union of the bounds |
| // of the dragged tabs and group header, if any. |first_dragged_tab_index| is |
| // the current model index in this tabstrip of the first dragged tab. The |
| // dragged tabs must be in the tabstrip already! |
| int CalculateInsertionIndex( |
| const gfx::Rect& dragged_bounds, |
| int first_dragged_tab_index, |
| int num_dragged_tabs, |
| absl::optional<tab_groups::TabGroupId> dragged_group) const { |
| // This method assumes that the dragged tabs and group are already in the |
| // tabstrip (i.e. it doesn't support attaching a drag to a new tabstrip). |
| // This assumption is critical because it means that tab width won't change |
| // after this method's recommendation is implemented. |
| |
| // For each possible insertion index, determine what the ideal bounds of |
| // the dragged tabs would be at that index. This corresponds to where they |
| // would slide to if the drag session ended now. We want to insert at the |
| // index that minimizes the distance between the corresponding ideal bounds |
| // and the current bounds of the tabs. This is equivalent to minimizing: |
| // - the distance of the aforementioned slide, |
| // - the width of the gaps in the tabstrip, or |
| // - the amount of tab overlap. |
| int min_distance_index = -1; |
| int min_distance = std::numeric_limits<int>::max(); |
| for (int candidate_index = 0; candidate_index <= GetTabCount(); |
| ++candidate_index) { |
| if (!IsValidInsertionIndex(candidate_index, first_dragged_tab_index, |
| num_dragged_tabs, dragged_group)) { |
| continue; |
| } |
| |
| // If there's a group header here, and we're dragging a group, we might |
| // end up on either side of that header. Check both cases to find the |
| // best option. |
| // TODO(tbergquist): Use this approach to determine if a tab should be |
| // added to the group. This is calculated elsewhere and may require some |
| // plumbing and/or duplicated code. |
| const int left_ideal_x = CalculateIdealX( |
| candidate_index, first_dragged_tab_index, dragged_bounds); |
| const int left_distance = std::abs(dragged_bounds.x() - left_ideal_x); |
| |
| const int right_ideal_x = |
| left_ideal_x + CalculateIdealXAdjustmentIfAddedToGroup( |
| candidate_index, dragged_group); |
| const int right_distance = std::abs(dragged_bounds.x() - right_ideal_x); |
| |
| const int distance = std::min(left_distance, right_distance); |
| if (distance < min_distance) { |
| min_distance = distance; |
| min_distance_index = candidate_index; |
| } |
| } |
| |
| if (min_distance_index == -1) { |
| NOTREACHED(); |
| return 0; |
| } |
| |
| // When moving a tab within a tabstrip, the target index is expressed as if |
| // the tabs are not in the tabstrip, i.e. it acts like the tabs are first |
| // removed and then re-inserted at the target index. We need to adjust the |
| // target index to account for this. |
| if (min_distance_index > first_dragged_tab_index) |
| min_distance_index -= num_dragged_tabs; |
| |
| return min_distance_index; |
| } |
| |
| // Dragging can't insert tabs into some indices. |
| bool IsValidInsertionIndex( |
| int candidate_index, |
| int first_dragged_tab_index, |
| int num_dragged_tabs, |
| absl::optional<tab_groups::TabGroupId> dragged_group) const { |
| if (candidate_index == 0) |
| return true; |
| |
| // If |candidate_index| is right after one of the tabs we're dragging, |
| // inserting here would be nonsensical - we can't insert the dragged tabs |
| // into the middle of the dragged tabs. That's just silly. |
| if (candidate_index > first_dragged_tab_index && |
| candidate_index <= first_dragged_tab_index + num_dragged_tabs) { |
| return false; |
| } |
| |
| // This might be in the middle of a group, which may or may not be fine. |
| absl::optional<tab_groups::TabGroupId> left_group = |
| GetTabAt(candidate_index - 1)->group(); |
| absl::optional<tab_groups::TabGroupId> right_group = |
| tab_strip_->IsValidModelIndex(candidate_index) |
| ? GetTabAt(candidate_index)->group() |
| : absl::nullopt; |
| if (left_group.has_value() && left_group == right_group) { |
| // Can't drag a group into another group. |
| if (dragged_group.has_value()) |
| return false; |
| // Can't drag a tab into a collapsed group. |
| if (tab_strip_->controller()->IsGroupCollapsed(left_group.value())) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| // Determines the x position that the dragged tabs would have if they were |
| // inserted at |candidate_index|. If there's a group header at that index, |
| // this assumes the dragged tabs *would not* be inserted into the group, |
| // and would therefore end up to the left of that header. |
| int CalculateIdealX(int candidate_index, |
| int first_dragged_tab_index, |
| gfx::Rect dragged_bounds) const { |
| if (candidate_index == 0) |
| return 0; |
| |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| |
| // We'll insert just right of the tab at |candidate_index| - 1. |
| int ideal_x = ideal_bounds(candidate_index - 1).right(); |
| |
| // If the dragged tabs are currently left of |candidate_index|, moving |
| // them to |candidate_index| would move the tab at |candidate_index| - 1 |
| // to the left by |num_dragged_tabs| slots. This would change the ideal x |
| // for the dragged tabs, as well, by the width of the dragged tabs. |
| if (candidate_index - 1 > first_dragged_tab_index) |
| ideal_x -= dragged_bounds.width() - tab_overlap; |
| |
| return ideal_x - tab_overlap; |
| } |
| |
| // There might be a group starting at |candidate_index|. If there is, |
| // this determines how the ideal x would change if the dragged tabs were |
| // added to that group, thereby moving them to that header's right. |
| int CalculateIdealXAdjustmentIfAddedToGroup( |
| int candidate_index, |
| absl::optional<tab_groups::TabGroupId> dragged_group) const { |
| // If the tab to the right of |candidate_index| is the first tab in a |
| // (non-collapsed) group, we are sharing this model index with a group |
| // header. We might end up on either side of it, so we need to check |
| // both positions. |
| if (!dragged_group.has_value() && |
| tab_strip_->IsValidModelIndex(candidate_index)) { |
| absl::optional<tab_groups::TabGroupId> left_group = |
| tab_strip_->IsValidModelIndex(candidate_index - 1) |
| ? GetTabAt(candidate_index - 1)->group() |
| : absl::nullopt; |
| absl::optional<tab_groups::TabGroupId> right_group = |
| GetTabAt(candidate_index)->group(); |
| if (right_group.has_value() && left_group != right_group) { |
| if (tab_strip_->controller()->IsGroupCollapsed(right_group.value())) |
| return 0; |
| const int header_width = |
| GetTabGroupHeader(*right_group)->bounds().width() - |
| TabStyle::GetTabOverlap(); |
| return header_width; |
| } |
| } |
| |
| return 0; |
| } |
| |
| // Used by GetInsertionIndexForDraggedBounds() when the tabstrip is stacked. |
| absl::optional<int> GetInsertionIndexForDraggedBoundsStacked( |
| const gfx::Rect& dragged_bounds, |
| bool mouse_has_ever_moved_left, |
| bool mouse_has_ever_moved_right) const { |
| int active_index = *GetActiveTouchIndex(); |
| // Search from the active index to the front of the tabstrip. Do this as |
| // tabs overlap each other from the active index. |
| absl::optional<int> index = |
| GetInsertionIndexFromReversedStacked(dragged_bounds, active_index); |
| if (index != active_index) |
| return index; |
| if (!index) |
| return GetInsertionIndexFromStacked(dragged_bounds, active_index + 1); |
| |
| // The position to drag to corresponds to the active tab. If the |
| // next/previous tab is stacked, then shorten the distance used to determine |
| // insertion bounds. We do this as GetInsertionIndexFrom() uses the bounds |
| // of the tabs. When tabs are stacked the next/previous tab is on top of the |
| // tab. |
| if (active_index + 1 < GetTabCount() && |
| tab_strip_->touch_layout_->IsStacked(active_index + 1)) { |
| index = GetInsertionIndexFromStacked(dragged_bounds, active_index + 1); |
| if (!index && ShouldDragToNextStackedTab(dragged_bounds, active_index, |
| mouse_has_ever_moved_right)) |
| index = active_index + 1; |
| else if (index == -1) |
| index = active_index; |
| } else if (ShouldDragToPreviousStackedTab(dragged_bounds, active_index, |
| mouse_has_ever_moved_left)) { |
| index = active_index - 1; |
| } |
| return index; |
| } |
| |
| // Determines the index to insert tabs at. |dragged_bounds| is the bounds of |
| // the tab being dragged and |start| is the index of the tab to start looking |
| // from. The search proceeds to the end of the strip. |
| absl::optional<int> GetInsertionIndexFromStacked( |
| const gfx::Rect& dragged_bounds, |
| int start) const { |
| const int last_tab = GetTabCount() - 1; |
| if (start < 0 || start > last_tab) |
| return absl::nullopt; |
| |
| const int dragged_x = GetDraggedX(dragged_bounds); |
| if (dragged_x < ideal_bounds(start).x() || |
| dragged_x > ideal_bounds(last_tab).right()) { |
| return absl::nullopt; |
| } |
| |
| absl::optional<int> insertion_index; |
| for (int i = start; i <= last_tab; ++i) { |
| const gfx::Rect current_bounds = ideal_bounds(i); |
| int current_center = current_bounds.CenterPoint().x(); |
| |
| if (dragged_bounds.width() > current_bounds.width() && |
| dragged_bounds.x() < current_bounds.x()) { |
| current_center -= (dragged_bounds.width() - current_bounds.width()); |
| } |
| |
| if (dragged_x < current_center) { |
| insertion_index = i; |
| break; |
| } |
| } |
| |
| if (!insertion_index.has_value()) |
| return last_tab + 1; |
| |
| return insertion_index; |
| } |
| |
| // Like GetInsertionIndexFrom(), but searches backwards from |start| to the |
| // beginning of the strip. |
| absl::optional<int> GetInsertionIndexFromReversedStacked( |
| const gfx::Rect& dragged_bounds, |
| int start) const { |
| const int dragged_x = GetDraggedX(dragged_bounds); |
| if (start < 0 || start >= GetTabCount() || |
| dragged_x >= ideal_bounds(start).right() || |
| dragged_x < ideal_bounds(0).x()) |
| return absl::nullopt; |
| |
| for (int i = start; i >= 0; --i) { |
| if (dragged_x >= ideal_bounds(i).CenterPoint().x()) |
| return i + 1; |
| } |
| |
| return 0; |
| } |
| |
| // Sets the ideal bounds x-coordinates to |positions|. |
| void SetIdealBoundsFromPositions(const std::vector<int>& positions) { |
| if (static_cast<size_t>(GetTabCount()) != positions.size()) |
| return; |
| |
| for (int i = 0; i < GetTabCount(); ++i) { |
| gfx::Rect bounds(ideal_bounds(i)); |
| bounds.set_x(positions[i]); |
| tab_strip_->tabs_.set_ideal_bounds(i, bounds); |
| } |
| } |
| |
| TabStrip* const tab_strip_; |
| |
| // The controller for a drag initiated from a Tab. Valid for the lifetime of |
| // the drag session. |
| std::unique_ptr<TabDragController> drag_controller_; |
| |
| // Only used in tests. |
| base::OnceCallback<void(TabDragController*)> drag_controller_set_callback_; |
| |
| base::WeakPtrFactory<TabDragContext> weak_factory_{this}; |
| }; |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, public: |
| |
| TabStrip::TabStrip(std::unique_ptr<TabStripController> controller) |
| : controller_(std::move(controller)), |
| layout_helper_(std::make_unique<TabStripLayoutHelper>( |
| controller_.get(), |
| base::BindRepeating(&TabStrip::tabs_view_model, |
| base::Unretained(this)))), |
| drag_context_(std::make_unique<TabDragContextImpl>(this)) { |
| // 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::SetCascadingThemeProviderColor(this, views::kCascadingBackgroundColor, |
| ThemeProperties::COLOR_TOOLBAR); |
| Init(); |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| } |
| |
| TabStrip::~TabStrip() { |
| // Eliminate the hover card first to avoid order-of-operation issues. |
| hover_card_controller_.reset(); |
| |
| // The animations may reference the tabs. Shut down the animation before we |
| // delete the tabs. |
| StopAnimating(false); |
| |
| // Disengage the drag controller before doing any additional cleanup. This |
| // call can interact with child views so we can't reliably do it during member |
| // destruction. |
| drag_context_->DestroyDragController(); |
| |
| // Make sure we unhook ourselves as a message loop observer so that we don't |
| // crash in the case where the user closes the window after closing a tab |
| // but before moving the mouse. |
| RemoveMessageLoopObserver(); |
| |
| // Since TabGroupViews expects be able to remove the views it creates, clear |
| // |group_views_| before removing the remaining children below. |
| group_views_.clear(); |
| |
| // The child tabs may call back to us from their destructors. Delete them so |
| // that if they call back we aren't in a weird state. |
| RemoveAllChildViews(true); |
| |
| CHECK(!IsInObserverList()); |
| } |
| |
| void TabStrip::SetAvailableWidthCallback( |
| base::RepeatingCallback<int()> available_width_callback) { |
| available_width_callback_ = available_width_callback; |
| } |
| |
| // static |
| int TabStrip::GetSizeNeededForViews(const std::vector<TabSlotView*>& views) { |
| int width = 0; |
| for (const TabSlotView* view : views) |
| width += view->width(); |
| if (!views.empty()) |
| width -= TabStyle::GetTabOverlap() * (views.size() - 1); |
| return width; |
| } |
| |
| void TabStrip::AddObserver(TabStripObserver* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void TabStrip::RemoveObserver(TabStripObserver* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| void TabStrip::FrameColorsChanged() { |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->FrameColorsChanged(); |
| UpdateContrastRatioValues(); |
| SchedulePaint(); |
| } |
| |
| void TabStrip::SetBackgroundOffset(int background_offset) { |
| if (background_offset == background_offset_) |
| return; |
| background_offset_ = background_offset; |
| OnPropertyChanged(&background_offset_, views::kPropertyEffectsPaint); |
| } |
| |
| bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { |
| // If there is no control at this location, the hit is in the caption area. |
| const views::View* v = GetEventHandlerForRect(rect); |
| if (v == this) |
| return true; |
| |
| // When the window has a top drag handle, a thin strip at the top of inactive |
| // tabs and the new tab button is treated as part of the window drag handle, |
| // to increase draggability. This region starts 1 DIP above the top of the |
| // separator. |
| const int drag_handle_extension = TabStyle::GetDragHandleExtension(height()); |
| |
| // Disable drag handle extension when tab shapes are visible. |
| bool extend_drag_handle = !controller_->IsFrameCondensed() && |
| !controller_->EverHasVisibleBackgroundTabShapes(); |
| |
| // A hit on the tab is not in the caption unless it is in the thin strip |
| // mentioned above. |
| const int tab_index = tabs_.GetIndexOfView(v); |
| if (IsValidModelIndex(tab_index)) { |
| Tab* tab = tab_at(tab_index); |
| gfx::Rect tab_drag_handle = tab->GetMirroredBounds(); |
| tab_drag_handle.set_height(drag_handle_extension); |
| return extend_drag_handle && !tab->IsActive() && |
| tab_drag_handle.Intersects(rect); |
| } |
| |
| // |v| is some other view (e.g. a close button in a tab) and therefore |rect| |
| // is in client area. |
| return false; |
| } |
| |
| bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) { |
| return IsRectInWindowCaption(gfx::Rect(point, gfx::Size(1, 1))); |
| } |
| |
| bool TabStrip::IsTabStripCloseable() const { |
| return !drag_context_->IsDragSessionActive(); |
| } |
| |
| bool TabStrip::IsTabStripEditable() const { |
| return !drag_context_->IsDragSessionActive() && |
| !drag_context_->IsActiveDropTarget(); |
| } |
| |
| bool TabStrip::IsTabCrashed(int tab_index) const { |
| return tab_at(tab_index)->data().IsCrashed(); |
| } |
| |
| bool TabStrip::TabHasNetworkError(int tab_index) const { |
| return tab_at(tab_index)->data().network_state == TabNetworkState::kError; |
| } |
| |
| absl::optional<TabAlertState> TabStrip::GetTabAlertState(int tab_index) const { |
| return Tab::GetAlertStateToShow(tab_at(tab_index)->data().alert_state); |
| } |
| |
| void TabStrip::UpdateLoadingAnimations(const base::TimeDelta& elapsed_time) { |
| for (int i = 0; i < GetTabCount(); i++) |
| tab_at(i)->StepLoadingAnimation(elapsed_time); |
| } |
| |
| void TabStrip::SetStackedLayout(bool stacked_layout) { |
| if (stacked_layout == stacked_layout_) |
| return; |
| |
| stacked_layout_ = stacked_layout; |
| SetResetToShrinkOnExit(false); |
| SwapLayoutIfNecessary(); |
| |
| // When transitioning to stacked try to keep the active tab from moving. |
| const int active_index = controller_->GetActiveIndex(); |
| if (touch_layout_ && active_index != -1) { |
| touch_layout_->SetActiveTabLocation(ideal_bounds(active_index).x()); |
| AnimateToIdealBounds(); |
| } |
| |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->Layout(); |
| } |
| |
| void TabStrip::AddTabAt(int model_index, TabRendererData data, bool is_active) { |
| Tab* tab = new Tab(this); |
| tab->set_context_menu_controller(&context_menu_controller_); |
| tab->AddObserver(this); |
| AddChildViewAt(tab, GetViewInsertionIndex(tab, absl::nullopt, model_index)); |
| const bool pinned = data.pinned; |
| tabs_.Add(tab, model_index); |
| selected_tabs_.IncrementFrom(model_index); |
| |
| // Setting data must come after all state from the model has been updated |
| // above for the tab. Accessibility, in particular, reacts to data changed |
| // callbacks. |
| tab->SetData(std::move(data)); |
| |
| if (touch_layout_) { |
| int add_types = 0; |
| if (pinned) |
| add_types |= StackedTabStripLayout::kAddTypePinned; |
| if (is_active) |
| add_types |= StackedTabStripLayout::kAddTypeActive; |
| touch_layout_->AddTab(model_index, add_types, |
| UpdateIdealBoundsForPinnedTabs(nullptr)); |
| } |
| |
| // Don't animate the first tab, it looks weird, and don't animate anything |
| // if the containing window isn't visible yet. |
| if (GetTabCount() > 1 && GetWidget() && GetWidget()->IsVisible()) { |
| StartInsertTabAnimation(model_index, |
| pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| } else { |
| layout_helper_->InsertTabAt( |
| model_index, tab, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| CompleteAnimationAndLayout(); |
| } |
| |
| SwapLayoutIfNecessary(); |
| UpdateAccessibleTabIndices(); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabAdded(model_index); |
| |
| // Stop dragging when a new tab is added and dragging a window. Doing |
| // otherwise results in a confusing state if the user attempts to reattach. We |
| // could allow this and make TabDragController update itself during the add, |
| // but this comes up infrequently enough that it's not worth the complexity. |
| // |
| // At the start of AddTabAt() the model and tabs are out sync. Any queries to |
| // find a tab given a model index can go off the end of |tabs_|. As such, it |
| // is important that we complete the drag *after* adding the tab so that the |
| // model and tabstrip are in sync. |
| if (!drag_context_->IsMutating() && drag_context_->IsDraggingWindow()) |
| EndDrag(END_DRAG_COMPLETE); |
| |
| Profile* profile = controller()->GetProfile(); |
| if (profile) { |
| if (profile->IsGuestSession()) |
| 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) { |
| DCHECK_GT(tabs_.view_size(), 0); |
| |
| Tab* moving_tab = tab_at(from_model_index); |
| const bool pinned = data.pinned; |
| moving_tab->SetData(std::move(data)); |
| |
| ReorderChildView( |
| moving_tab, |
| GetViewInsertionIndex(moving_tab, from_model_index, to_model_index)); |
| |
| if (touch_layout_) { |
| tabs_.MoveViewOnly(from_model_index, to_model_index); |
| int pinned_count = 0; |
| const int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_count); |
| touch_layout_->MoveTab(from_model_index, to_model_index, |
| controller_->GetActiveIndex(), start_x, |
| pinned_count); |
| } else { |
| tabs_.Move(from_model_index, to_model_index); |
| } |
| selected_tabs_.Move(from_model_index, to_model_index, /*length=*/1); |
| |
| layout_helper_->MoveTab(moving_tab->group(), from_model_index, |
| to_model_index); |
| layout_helper_->SetTabPinned( |
| to_model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| StartMoveTabAnimation(); |
| SwapLayoutIfNecessary(); |
| |
| UpdateAccessibleTabIndices(); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabMoved(from_model_index, to_model_index); |
| } |
| |
| void TabStrip::RemoveTabAt(content::WebContents* contents, |
| int model_index, |
| bool was_active) { |
| StartRemoveTabAnimation(model_index, was_active); |
| |
| SwapLayoutIfNecessary(); |
| |
| UpdateAccessibleTabIndices(); |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); |
| |
| for (TabStripObserver& observer : observers_) |
| observer.OnTabRemoved(model_index); |
| |
| // Stop dragging when a new tab is removed and dragging a window. Doing |
| // otherwise results in a confusing state if the user attempts to reattach. We |
| // could allow this and make TabDragController update itself during the |
| // remove operation, but this comes up infrequently enough that it's not worth |
| // the complexity. |
| // |
| // At the start of RemoveTabAt() the model and tabs are out sync. Any queries |
| // to find a tab given a model index can go off the end of |tabs_|. As such, |
| // it is important that we complete the drag *after* removing the tab so that |
| // the model and tabstrip are in sync. |
| if (!drag_context_->IsMutating() && drag_context_->IsDraggingTab(contents)) |
| EndDrag(END_DRAG_COMPLETE); |
| } |
| |
| void TabStrip::ScrollTabToVisible(int model_index) { |
| views::ScrollView* scroll_container = |
| views::ScrollView::GetScrollViewForContents(this); |
| if (!scroll_container) { |
| return; |
| } |
| |
| // If the tab strip won't be scrollable after the current tabstrip animations |
| // complete, scroll animation wouldn't be meaningful. |
| if (ideal_bounds(GetTabCount() - 1).right() <= GetAvailableWidthForTabStrip()) |
| return; |
| |
| if (tab_scrolling_animation_) |
| tab_scrolling_animation_->Stop(); |
| |
| gfx::Rect visible_content_rect = scroll_container->GetVisibleRect(); |
| gfx::Rect active_tab_ideal_bounds = ideal_bounds(model_index); |
| |
| if ((active_tab_ideal_bounds.x() >= visible_content_rect.x()) && |
| (active_tab_ideal_bounds.right() <= visible_content_rect.right())) { |
| return; |
| } |
| |
| bool scroll_left = active_tab_ideal_bounds.x() < visible_content_rect.x(); |
| if (scroll_left) { |
| // Scroll the left edge of |visible_content_rect| to show the left edge of |
| // the tab at |model_index|. We can leave the width entirely up to the |
| // ScrollView. |
| gfx::Rect start_left_edge(visible_content_rect.x(), |
| visible_content_rect.y(), 0, 0); |
| gfx::Rect target_left_edge(active_tab_ideal_bounds.x(), |
| visible_content_rect.y(), 0, 0); |
| tab_scrolling_animation_ = std::make_unique<TabScrollingAnimation>( |
| this, bounds_animator_.container(), |
| bounds_animator_.GetAnimationDuration(), start_left_edge, |
| target_left_edge); |
| tab_scrolling_animation_->Start(); |
| } else { |
| // Scroll the right edge of |visible_content_rect| to show the right edge |
| // of the tab at |model_index|. We can leave the width entirely up to the |
| // ScrollView. |
| gfx::Rect start_right_edge(visible_content_rect.right(), |
| visible_content_rect.y(), 0, 0); |
| gfx::Rect target_right_edge(active_tab_ideal_bounds.right(), |
| visible_content_rect.y(), 0, 0); |
| tab_scrolling_animation_ = std::make_unique<TabScrollingAnimation>( |
| this, bounds_animator_.container(), |
| bounds_animator_.GetAnimationDuration(), start_right_edge, |
| target_right_edge); |
| tab_scrolling_animation_->Start(); |
| } |
| } |
| |
| void TabStrip::SetTabData(int model_index, TabRendererData data) { |
| Tab* tab = tab_at(model_index); |
| const bool pinned = data.pinned; |
| const bool pinned_state_changed = tab->data().pinned != pinned; |
| tab->SetData(std::move(data)); |
| |
| if (HoverCardIsShowingForTab(tab)) |
| UpdateHoverCard(tab, HoverCardUpdateType::kTabDataChanged); |
| |
| if (pinned_state_changed) { |
| if (touch_layout_) { |
| int pinned_tab_count = 0; |
| int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_tab_count); |
| touch_layout_->SetXAndPinnedCount(start_x, pinned_tab_count); |
| } |
| |
| layout_helper_->SetTabPinned( |
| model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| if (GetWidget() && GetWidget()->IsVisible()) |
| StartPinnedTabAnimation(); |
| else |
| CompleteAnimationAndLayout(); |
| } |
| SwapLayoutIfNecessary(); |
| } |
| |
| void TabStrip::AddTabToGroup(absl::optional<tab_groups::TabGroupId> group, |
| int model_index) { |
| tab_at(model_index)->set_group(group); |
| |
| // Expand the group if the tab that is getting grouped is the active tab. This |
| // can result in the group expanding in a series of actions where the final |
| // active tab is not in the group. |
| if (model_index == selected_tabs_.active() && group.has_value() && |
| controller()->IsGroupCollapsed(group.value())) { |
| controller()->ToggleTabGroupCollapsedState( |
| group.value(), ToggleTabGroupCollapsedStateOrigin::kImplicitAction); |
| } |
| |
| if (group.has_value()) |
| ExitTabClosingMode(); |
| } |
| |
| void TabStrip::OnGroupCreated(const tab_groups::TabGroupId& group) { |
| auto group_view = std::make_unique<TabGroupViews>(this, group); |
| layout_helper_->InsertGroupHeader(group, group_view->header()); |
| group_views_[group] = std::move(group_view); |
| SetStackedLayout(false); |
| } |
| |
| void TabStrip::OnGroupEditorOpened(const tab_groups::TabGroupId& group) { |
| // The context menu relies on a Browser object which is not provided in |
| // TabStripTest. |
| if (this->controller()->GetBrowser()) { |
| group_views_[group]->header()->ShowContextMenuForViewImpl( |
| this, gfx::Point(), ui::MENU_SOURCE_NONE); |
| } |
| } |
| |
| void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) { |
| // The group header may be in the wrong place if the tab didn't actually |
| // move in terms of model indices. |
| OnGroupMoved(group); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::OnGroupVisualsChanged( |
| const tab_groups::TabGroupId& group, |
| const tab_groups::TabGroupVisualData* old_visuals, |
| const tab_groups::TabGroupVisualData* new_visuals) { |
| group_views_[group]->OnGroupVisualsChanged(); |
| // The group title may have changed size, so update bounds. |
| // First exit tab closing mode, unless this change was a collapse, in which |
| // case we want to stay in tab closing mode. |
| bool is_collapsing = old_visuals && !old_visuals->is_collapsed() && |
| new_visuals->is_collapsed(); |
| if (!is_collapsing) |
| ExitTabClosingMode(); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::ToggleTabGroup(const tab_groups::TabGroupId& group, |
| bool is_collapsing, |
| ToggleTabGroupCollapsedStateOrigin origin) { |
| if (is_collapsing && GetWidget()) { |
| in_tab_close_ = true; |
| if (origin == ToggleTabGroupCollapsedStateOrigin::kMouse) { |
| AddMessageLoopObserver(); |
| } else if (origin == ToggleTabGroupCollapsedStateOrigin::kGesture) { |
| StartResizeLayoutTabsFromTouchTimer(); |
| } else { |
| return; |
| } |
| |
| // The current group header is expanded which is slightly smaller than the |
| // size when the header is collapsed. Calculate the size of the header once |
| // collapsed for maintaining its position. See |
| // TabGroupHeader::CalculateWidth() for more details. |
| const int empty_group_title_adjustment = |
| GetGroupTitle(group).empty() ? 2 : -2; |
| const int title_chip_width = |
| group_views_[group]->header()->GetTabSizeInfo().standard_width - |
| 2 * TabStyle::GetTabOverlap() - empty_group_title_adjustment; |
| const int collapsed_header_width = |
| title_chip_width + 2 * TabGroupUnderline::GetStrokeInset(); |
| override_available_width_for_tabs_ = |
| ideal_bounds(GetModelCount() - 1).right() - |
| group_views_[group]->GetBounds().width() + collapsed_header_width; |
| } else { |
| ExitTabClosingMode(); |
| } |
| } |
| |
| void TabStrip::OnGroupMoved(const tab_groups::TabGroupId& group) { |
| DCHECK(group_views_[group]); |
| |
| layout_helper_->UpdateGroupHeaderIndex(group); |
| |
| TabGroupHeader* group_header = group_views_[group]->header(); |
| const int first_tab = controller_->GetFirstTabInGroup(group).value(); |
| const int header_index = GetIndexOf(group_header); |
| const int first_tab_index = GetIndexOf(tab_at(first_tab)); |
| |
| // The header should be just before the first tab. If it isn't, reorder the |
| // header such that it is. Note that the index to reorder to is different |
| // depending on whether the header is before or after the tab, since the |
| // header itself occupies an index. |
| if (header_index < first_tab_index - 1) |
| ReorderChildView(group_header, first_tab_index - 1); |
| if (header_index > first_tab_index - 1) |
| ReorderChildView(group_header, first_tab_index); |
| } |
| |
| void TabStrip::OnGroupClosed(const tab_groups::TabGroupId& group) { |
| bounds_animator_.StopAnimatingView(group_header(group)); |
| layout_helper_->RemoveGroupHeader(group); |
| |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| group_views_.erase(group); |
| } |
| |
| void TabStrip::ShiftGroupLeft(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, -1); |
| } |
| |
| void TabStrip::ShiftGroupRight(const tab_groups::TabGroupId& group) { |
| ShiftGroupRelative(group, 1); |
| } |
| |
| bool TabStrip::ShouldTabBeVisible(const Tab* tab) const { |
| // When the tabstrip is scrollable, it can grow to accommodate any number of |
| // tabs, so tabs can never become clipped. |
| // N.B. Tabs can still be not-visible because they're in a collapsed group, |
| // but that's handled elsewhere. |
| // N.B. This is separate from the tab being potentially scrolled offscreen - |
| // this solely determines whether the tab should be clipped for the |
| // pre-scrolling overflow behavior. |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) |
| return true; |
| |
| // Detached tabs should always be invisible (as they close). |
| if (tab->detached()) |
| return false; |
| |
| // When stacking tabs, all tabs should always be visible. |
| if (stacked_layout_) |
| return true; |
| |
| // If the tab would be clipped by the trailing edge of the strip, even if the |
| // tabstrip were resized to its greatest possible width, it shouldn't be |
| // visible. |
| int right_edge = tab->bounds().right(); |
| const int tabstrip_right = tab->dragging() |
| ? drag_context_->GetTabDragAreaWidth() |
| : GetAvailableWidthForTabStrip(); |
| if (right_edge > tabstrip_right) |
| return false; |
| |
| // Non-clipped dragging tabs should always be visible. |
| if (tab->dragging()) |
| return true; |
| |
| // Let all non-clipped closing tabs be visible. These will probably finish |
| // closing before the user changes the active tab, so there's little reason to |
| // try and make the more complex logic below apply. |
| if (tab->closing()) |
| return true; |
| |
| // Now we need to check whether the tab isn't currently clipped, but could |
| // become clipped if we changed the active tab, widening either this tab or |
| // the tabstrip portion before it. |
| |
| // Pinned tabs don't change size when activated, so any tab in the pinned tab |
| // region is safe. |
| if (tab->data().pinned) |
| return true; |
| |
| // If the active tab is on or before this tab, we're safe. |
| if (controller_->GetActiveIndex() <= GetModelIndexOf(tab)) |
| return true; |
| |
| // We need to check what would happen if the active tab were to move to this |
| // tab or before. If animating, we want to use the target bounds in this |
| // calculation. |
| if (IsAnimating()) |
| right_edge = bounds_animator_.GetTargetBounds(tab).right(); |
| return (right_edge + GetActiveTabWidth() - GetInactiveTabWidth()) <= |
| tabstrip_right; |
| } |
| |
| bool TabStrip::ShouldDrawStrokes() const { |
| // If the controller says we can't draw strokes, don't. |
| if (!controller_->CanDrawStrokes()) |
| return false; |
| |
| // The tabstrip normally avoids strokes and relies on the active tab |
| // contrasting sufficiently with the frame background. When there isn't |
| // enough contrast, fall back to a stroke. Always compute the contrast ratio |
| // against the active frame color, to avoid toggling the stroke on and off as |
| // the window activation state changes. |
| constexpr float kMinimumContrastRatioForOutlines = 1.3f; |
| const SkColor background_color = GetTabBackgroundColor( |
| TabActive::kActive, BrowserFrameActiveState::kActive); |
| const SkColor frame_color = |
| controller_->GetFrameColor(BrowserFrameActiveState::kActive); |
| const float contrast_ratio = |
| color_utils::GetContrastRatio(background_color, frame_color); |
| if (contrast_ratio < kMinimumContrastRatioForOutlines) |
| return true; |
| |
| // Don't want to have to run a full feature query every time this function is |
| // called. |
| static const bool tab_outlines_in_low_contrast = |
| base::FeatureList::IsEnabled(features::kTabOutlinesInLowContrastThemes); |
| if (tab_outlines_in_low_contrast) { |
| constexpr float kMinimumAbsoluteContrastForOutlines = 0.2f; |
| const float background_luminance = |
| color_utils::GetRelativeLuminance(background_color); |
| const float frame_luminance = |
| color_utils::GetRelativeLuminance(frame_color); |
| const float contrast_difference = |
| std::fabs(background_luminance - frame_luminance); |
| if (contrast_difference < kMinimumAbsoluteContrastForOutlines) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| void TabStrip::SetSelection(const ui::ListSelectionModel& new_selection) { |
| DCHECK_GE(new_selection.active(), 0) |
| << "We should never transition to a state where no tab is active."; |
| Tab* const new_active_tab = tab_at(new_selection.active()); |
| Tab* const old_active_tab = |
| selected_tabs_.active() >= 0 ? tab_at(selected_tabs_.active()) : nullptr; |
| |
| if (new_active_tab != old_active_tab) { |
| if (old_active_tab) { |
| old_active_tab->ActiveStateChanged(); |
| if (old_active_tab->group().has_value()) |
| UpdateTabGroupVisuals(old_active_tab->group().value()); |
| } |
| if (new_active_tab->group().has_value()) { |
| const tab_groups::TabGroupId new_group = new_active_tab->group().value(); |
| // If the tab that is about to be activated is in a collapsed group, |
| // automatically expand the group. |
| if (controller()->IsGroupCollapsed(new_group)) |
| controller()->ToggleTabGroupCollapsedState( |
| new_group, ToggleTabGroupCollapsedStateOrigin::kImplicitAction); |
| UpdateTabGroupVisuals(new_group); |
| } |
| |
| new_active_tab->ActiveStateChanged(); |
| layout_helper_->SetActiveTab(selected_tabs_.active(), |
| new_selection.active()); |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { |
| ScrollTabToVisible(new_selection.active()); |
| } |
| } |
| |
| if (touch_layout_) { |
| touch_layout_->SetActiveIndex(new_selection.active()); |
| // Only start an animation if we need to. Otherwise clicking on an |
| // unselected tab and dragging won't work because dragging is only allowed |
| // if not animating. |
| if (!views::ViewModelUtils::IsAtIdealBounds(tabs_)) |
| AnimateToIdealBounds(); |
| SchedulePaint(); |
| } else { |
| if (GetActiveTabWidth() == GetInactiveTabWidth()) { |
| // When tabs are wide enough, selecting a new tab cannot change the |
| // ideal bounds, so only a repaint is necessary. |
| SchedulePaint(); |
| } else if (IsAnimating()) { |
| // The selection change will have modified the ideal bounds of the tabs |
| // in |selected_tabs_| and |new_selection|. We need to recompute. |
| // Note: This is safe even if we're in the midst of mouse-based tab |
| // closure--we won't expand the tabstrip back to the full window |
| // width--because PrepareForCloseAt() will have set |
| // |override_available_width_for_tabs_| already. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } else { |
| // As in the animating case above, the selection change will have |
| // affected the desired bounds of the tabs, but since we're not animating |
| // we can just snap to the new bounds. |
| CompleteAnimationAndLayout(); |
| } |
| } |
| |
| // Use STLSetDifference to get the indices of elements newly selected |
| // and no longer selected, since selected_indices() is always sorted. |
| ui::ListSelectionModel::SelectedIndices no_longer_selected = |
| base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>( |
| selected_tabs_.selected_indices(), new_selection.selected_indices()); |
| ui::ListSelectionModel::SelectedIndices newly_selected = |
| base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>( |
| new_selection.selected_indices(), selected_tabs_.selected_indices()); |
| |
| new_active_tab->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); |
| selected_tabs_ = new_selection; |
| |
| UpdateHoverCard(nullptr, 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::OnWidgetActivationChanged(views::Widget* widget, bool active) { |
| if (active && selected_tabs_.active() >= 0) { |
| // When the browser window is activated, fire a selection event on the |
| // currently active tab, to help enable per-tab modes in assistive |
| // technologies. |
| tab_at(selected_tabs_.active()) |
| ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); |
| } |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kEvent); |
| } |
| |
| void TabStrip::OnBoundsAnimatorDone(views::BoundsAnimator* animator) { |
| // 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 (!GetDragContext()->IsDragSessionActive()) { |
| // The widget can apparently be null during shutdown. |
| views::Widget* widget = GetWidget(); |
| if (widget) |
| widget->SynthesizeMouseMoveEvent(); |
| } |
| } |
| |
| void TabStrip::SetTabNeedsAttention(int model_index, bool attention) { |
| tab_at(model_index)->SetTabNeedsAttention(attention); |
| } |
| |
| const gfx::Rect& TabStrip::ideal_bounds(tab_groups::TabGroupId group) const { |
| return layout_helper_->group_header_ideal_bounds().at(group); |
| } |
| |
| int TabStrip::GetModelIndexOf(const TabSlotView* view) const { |
| return tabs_.GetIndexOfView(view); |
| } |
| |
| int TabStrip::GetTabCount() const { |
| return tabs_.view_size(); |
| } |
| |
| int TabStrip::GetModelCount() const { |
| return controller_->GetCount(); |
| } |
| |
| bool TabStrip::IsValidModelIndex(int model_index) const { |
| return controller_->IsValidIndex(model_index); |
| } |
| |
| TabDragContext* TabStrip::GetDragContext() { |
| return drag_context_.get(); |
| } |
| |
| int TabStrip::GetPinnedTabCount() const { |
| return layout_helper_->GetPinnedTabCount(); |
| } |
| |
| bool TabStrip::IsAnimating() const { |
| return bounds_animator_.IsAnimating(); |
| } |
| |
| void TabStrip::StopAnimating(bool layout) { |
| if (!IsAnimating()) |
| return; |
| |
| bounds_animator_.Cancel(); |
| |
| if (layout) |
| CompleteAnimationAndLayout(); |
| } |
| |
| absl::optional<int> TabStrip::GetFocusedTabIndex() const { |
| for (int i = 0; i < tabs_.view_size(); ++i) { |
| if (tabs_.view_at(i)->HasFocus()) |
| return i; |
| } |
| return absl::nullopt; |
| } |
| |
| views::View* TabStrip::GetTabViewForPromoAnchor(int index_hint) { |
| return tab_at(base::clamp(index_hint, 0, GetTabCount() - 1)); |
| } |
| |
| views::View* TabStrip::GetDefaultFocusableChild() { |
| int active = controller_->GetActiveIndex(); |
| return active != TabStripModel::kNoTab ? tab_at(active) : nullptr; |
| } |
| |
| const ui::ListSelectionModel& TabStrip::GetSelectionModel() const { |
| return controller_->GetSelectionModel(); |
| } |
| |
| bool TabStrip::SupportsMultipleSelection() { |
| // Currently we only allow single selection in touch layout mode. |
| return touch_layout_ == nullptr; |
| } |
| |
| bool TabStrip::ShouldHideCloseButtonForTab(Tab* tab) const { |
| if (tab->IsActive()) |
| return false; |
| return !!touch_layout_; |
| } |
| |
| void TabStrip::SelectTab(Tab* tab, const ui::Event& event) { |
| int model_index = GetModelIndexOf(tab); |
| |
| if (IsValidModelIndex(model_index)) { |
| if (!tab->IsActive()) { |
| int current_selection = selected_tabs_.active(); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetOfSwitch", |
| current_selection - model_index); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromLeftOfSwitch", |
| model_index); |
| base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromRightOfSwitch", |
| GetModelCount() - model_index - 1); |
| base::UmaHistogramEnumeration("TabStrip.Tab.Views.ActivationAction", |
| TabStripModel::TabActivationTypes::kTab); |
| |
| if (tab->group().has_value()) { |
| base::RecordAction( |
| base::UserMetricsAction("TabGroups_SwitchGroupedTab")); |
| } |
| } |
| |
| // Selecting a tab via mouse affects what statistics we collect. |
| if (event.type() == ui::ET_MOUSE_PRESSED && !tab->IsActive() && |
| hover_card_controller_) { |
| hover_card_controller_->TabSelectedViaMouse(tab); |
| } |
| |
| controller_->SelectTab(model_index, event); |
| } |
| } |
| |
| void TabStrip::ExtendSelectionTo(Tab* tab) { |
| int model_index = GetModelIndexOf(tab); |
| if (IsValidModelIndex(model_index)) |
| controller_->ExtendSelectionTo(model_index); |
| } |
| |
| void TabStrip::ToggleSelected(Tab* tab) { |
| int model_index = GetModelIndexOf(tab); |
| if (IsValidModelIndex(model_index)) |
| controller_->ToggleSelected(model_index); |
| } |
| |
| void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { |
| int model_index = GetModelIndexOf(tab); |
| if (IsValidModelIndex(model_index)) |
| controller_->AddSelectionFromAnchorTo(model_index); |
| } |
| |
| void TabStrip::CloseTab(Tab* tab, CloseTabSource source) { |
| if (tab->closing()) { |
| // If the tab is already closing, close the next tab. We do this so that the |
| // user can rapidly close tabs by clicking the close button and not have |
| // the animations interfere with that. |
| std::vector<Tab*> all_tabs = layout_helper_->GetTabs(); |
| auto it = std::find(all_tabs.begin(), all_tabs.end(), tab); |
| while (it < all_tabs.end() && (*it)->closing()) { |
| it++; |
| } |
| |
| if (it == all_tabs.end()) |
| return; |
| tab = *it; |
| } |
| |
| CloseTabInternal(GetModelIndexOf(tab), source); |
| } |
| |
| void TabStrip::ShiftTabNext(Tab* tab) { |
| ShiftTabRelative(tab, 1); |
| } |
| |
| void TabStrip::ShiftTabPrevious(Tab* tab) { |
| ShiftTabRelative(tab, -1); |
| } |
| |
| void TabStrip::MoveTabFirst(Tab* tab) { |
| if (tab->closing()) |
| return; |
| |
| const int start_index = GetModelIndexOf(tab); |
| if (!IsValidModelIndex(start_index)) |
| return; |
| |
| int target_index = 0; |
| if (!controller_->IsTabPinned(start_index)) { |
| while (target_index < start_index && controller_->IsTabPinned(target_index)) |
| ++target_index; |
| } |
| |
| if (!IsValidModelIndex(target_index)) |
| return; |
| |
| if (target_index != start_index) |
| controller_->MoveTab(start_index, target_index); |
| |
| // The tab may unintentionally land in the first group in the tab strip, so we |
| // remove the group to ensure consistent behavior. Even if the tab is already |
| // at the front, it should "move" out of its current group. |
| if (tab->group().has_value()) |
| controller_->RemoveTabFromGroup(target_index); |
| |
| GetViewAccessibility().AnnounceText( |
| l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_FIRST)); |
| } |
| |
| void TabStrip::MoveTabLast(Tab* tab) { |
| if (tab->closing()) |
| return; |
| |
| const int start_index = GetModelIndexOf(tab); |
| if (!IsValidModelIndex(start_index)) |
| return; |
| |
| int target_index; |
| if (controller_->IsTabPinned(start_index)) { |
| int temp_index = start_index + 1; |
| while (temp_index < GetTabCount() && controller_->IsTabPinned(temp_index)) |
| ++temp_index; |
| target_index = temp_index - 1; |
| } else { |
| target_index = GetTabCount() - 1; |
| } |
| |
| if (!IsValidModelIndex(target_index)) |
| return; |
| |
| if (target_index != start_index) |
| controller_->MoveTab(start_index, target_index); |
| |
| // The tab may unintentionally land in the last group in the tab strip, so we |
| // remove the group to ensure consistent behavior. Even if the tab is already |
| // at the back, it should "move" out of its current group. |
| if (tab->group().has_value()) |
| controller_->RemoveTabFromGroup(target_index); |
| |
| GetViewAccessibility().AnnounceText( |
| l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_LAST)); |
| } |
| |
| void TabStrip::ShowContextMenuForTab(Tab* tab, |
| const gfx::Point& p, |
| ui::MenuSourceType source_type) { |
| controller_->ShowContextMenuForTab(tab, p, source_type); |
| } |
| |
| bool TabStrip::IsActiveTab(const Tab* tab) const { |
| int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsActiveTab(model_index); |
| } |
| |
| bool TabStrip::IsTabSelected(const Tab* tab) const { |
| int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsTabSelected(model_index); |
| } |
| |
| bool TabStrip::IsTabPinned(const Tab* tab) const { |
| if (tab->closing()) |
| return false; |
| |
| int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) && |
| controller_->IsTabPinned(model_index); |
| } |
| |
| bool TabStrip::IsTabFirst(const Tab* tab) const { |
| return GetModelIndexOf(tab) == 0; |
| } |
| |
| bool TabStrip::IsFocusInTabs() const { |
| return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView()); |
| } |
| |
| void TabStrip::MaybeStartDrag( |
| TabSlotView* source, |
| const ui::LocatedEvent& event, |
| const ui::ListSelectionModel& original_selection) { |
| // Don't accidentally start any drag operations during animations if the |
| // mouse is down... during an animation tabs are being resized automatically, |
| // so the View system can misinterpret this easily if the mouse is down that |
| // the user is dragging. |
| if (IsAnimating() || controller_->HasAvailableDragActions() == 0) |
| return; |
| |
| // Check that the source is either a valid tab or a tab group header, which |
| // are the only valid drag targets. |
| if (!IsValidModelIndex(GetModelIndexOf(source))) { |
| DCHECK_EQ(source->GetTabSlotViewType(), |
| TabSlotView::ViewType::kTabGroupHeader); |
| } |
| |
| drag_context_->MaybeStartDrag(source, event, original_selection); |
| } |
| |
| void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) { |
| drag_context_->ContinueDrag(view, event); |
| } |
| |
| bool TabStrip::EndDrag(EndDragReason reason) { |
| return drag_context_->EndDrag(reason); |
| } |
| |
| Tab* TabStrip::GetTabAt(const gfx::Point& point) { |
| views::View* view = GetEventHandlerForPoint(point); |
| if (!view) |
| return nullptr; // No tab contains the point. |
| |
| // Walk up the view hierarchy until we find a tab, or the TabStrip. |
| while (view && view != this && view->GetID() != VIEW_ID_TAB) |
| view = view->parent(); |
| |
| return view && view->GetID() == VIEW_ID_TAB ? static_cast<Tab*>(view) |
| : nullptr; |
| } |
| |
| const Tab* TabStrip::GetAdjacentTab(const Tab* tab, int offset) { |
| int index = GetModelIndexOf(tab); |
| if (index < 0) |
| return nullptr; |
| index += offset; |
| return IsValidModelIndex(index) ? tab_at(index) : nullptr; |
| } |
| |
| void TabStrip::OnMouseEventInTab(views::View* source, |
| const ui::MouseEvent& event) { |
| // Record time from cursor entering the tabstrip to first tap on a tab to |
| // switch. |
| if (mouse_entered_tabstrip_time_.has_value() && |
| event.type() == ui::ET_MOUSE_PRESSED && views::IsViewClass<Tab>(source)) { |
| UMA_HISTOGRAM_MEDIUM_TIMES( |
| "TabStrip.TimeToSwitch", |
| base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value()); |
| mouse_entered_tabstrip_time_.reset(); |
| } |
| UpdateStackedLayoutFromMouseEvent(source, event); |
| } |
| |
| void TabStrip::UpdateHoverCard(Tab* tab, HoverCardUpdateType update_type) { |
| // Some operations (including e.g. starting a drag) can cause the tab focus |
| // to change at the same time as the tabstrip is starting to animate; the |
| // hover card should not be visible at this time. |
| // See crbug.com/1220840 for an example case. |
| if (bounds_animator_.IsAnimating()) { |
| tab = nullptr; |
| update_type = HoverCardUpdateType::kAnimating; |
| } |
| |
| if (!hover_card_controller_) { |
| if (!tab) |
| return; |
| hover_card_controller_ = std::make_unique<TabHoverCardController>(this); |
| } |
| |
| hover_card_controller_->UpdateHoverCard(tab, update_type); |
| } |
| |
| bool TabStrip::ShowDomainInHoverCards() const { |
| const auto* app_controller = controller_->GetBrowser()->app_controller(); |
| return !app_controller || !app_controller->is_for_system_web_app(); |
| } |
| |
| bool TabStrip::HoverCardIsShowingForTab(Tab* tab) { |
| return hover_card_controller_ && |
| hover_card_controller_->IsHoverCardShowingForTab(tab); |
| } |
| |
| int TabStrip::GetBackgroundOffset() const { |
| return background_offset_; |
| } |
| |
| int TabStrip::GetStrokeThickness() const { |
| return ShouldDrawStrokes() ? 1 : 0; |
| } |
| |
| bool TabStrip::CanPaintThrobberToLayer() const { |
| // Disable layer-painting of throbbers if dragging, if any tab animation is in |
| // progress, or if stacked tabs are enabled. Also disable in fullscreen: when |
| // "immersive" the tab strip could be sliding in or out; for other modes, |
| // there's no tab strip. |
| const bool dragging = drag_context_->IsDragStarted(); |
| const views::Widget* widget = GetWidget(); |
| return widget && !touch_layout_ && !dragging && !IsAnimating() && |
| !widget->IsFullscreen(); |
| } |
| |
| bool TabStrip::HasVisibleBackgroundTabShapes() const { |
| return controller_->HasVisibleBackgroundTabShapes(); |
| } |
| |
| bool TabStrip::ShouldPaintAsActiveFrame() const { |
| return controller_->ShouldPaintAsActiveFrame(); |
| } |
| |
| SkColor TabStrip::GetToolbarTopSeparatorColor() const { |
| return controller_->GetToolbarTopSeparatorColor(); |
| } |
| |
| SkColor TabStrip::GetTabSeparatorColor() const { |
| return separator_color_; |
| } |
| |
| SkColor TabStrip::GetTabBackgroundColor( |
| TabActive active, |
| BrowserFrameActiveState active_state) const { |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| if (!tp) |
| return SK_ColorBLACK; |
| |
| constexpr int kColorIds[2][2] = { |
| {ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_ACTIVE}, |
| {ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_ACTIVE}}; |
| |
| using State = BrowserFrameActiveState; |
| const bool tab_active = active == TabActive::kActive; |
| const bool frame_active = |
| (active_state == State::kActive) || |
| ((active_state == State::kUseCurrent) && ShouldPaintAsActiveFrame()); |
| return tp->GetColor(kColorIds[tab_active][frame_active]); |
| } |
| |
| SkColor TabStrip::GetTabForegroundColor(TabActive active, |
| SkColor background_color) const { |
| const ui::ThemeProvider* tp = GetThemeProvider(); |
| if (!tp) |
| return SK_ColorBLACK; |
| |
| constexpr int kColorIds[2][2] = { |
| {ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE}, |
| {ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_INACTIVE, |
| ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE}}; |
| |
| const bool tab_active = active == TabActive::kActive; |
| const bool frame_active = ShouldPaintAsActiveFrame(); |
| const int color_id = kColorIds[tab_active][frame_active]; |
| |
| SkColor color = tp->GetColor(color_id); |
| if (tp->HasCustomColor(color_id)) |
| return color; |
| if ((color_id == |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE) && |
| tp->HasCustomColor( |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE)) { |
| // If a custom theme sets a background tab text color for active but not |
| // inactive windows, generate the inactive color by blending the active one |
| // at 75% as we do in the default theme. |
| color = tp->GetColor( |
| ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE); |
| } |
| |
| if (!frame_active) |
| color = color_utils::AlphaBlend(color, background_color, 0.75f); |
| |
| // To minimize any readability cost of custom system frame colors, try to make |
| // the text reach the same contrast ratio that it would in the default theme. |
| const SkColor target = color_utils::GetColorWithMaxContrast(background_color); |
| // These contrast ratios should match the actual ratios in the default theme |
| // colors when no system colors are involved, except for the inactive tab/ |
| // inactive frame case, which has been raised from 4.48 to 4.5 to meet |
| // accessibility guidelines. |
| constexpr float kContrast[2][2] = {{4.5f, // Inactive tab, inactive frame |
| 7.98f}, // Inactive tab, active frame |
| {5.0f, // Active tab, inactive frame |
| 10.46f}}; // Active tab, active frame |
| const float contrast = kContrast[tab_active][frame_active]; |
| return color_utils::BlendForMinContrast(color, background_color, target, |
| contrast) |
| .color; |
| } |
| |
| // Returns the accessible tab name for the tab. |
| std::u16string TabStrip::GetAccessibleTabName(const Tab* tab) const { |
| const int model_index = GetModelIndexOf(tab); |
| return IsValidModelIndex(model_index) ? controller_->GetAccessibleTabName(tab) |
| : std::u16string(); |
| } |
| |
| absl::optional<int> TabStrip::GetCustomBackgroundId( |
| BrowserFrameActiveState active_state) const { |
| if (!TitlebarBackgroundIsTransparent()) |
| return controller_->GetCustomBackgroundId(active_state); |
| |
| constexpr int kBackgroundIdGlass = IDR_THEME_TAB_BACKGROUND_V; |
| return GetThemeProvider()->HasCustomImage(kBackgroundIdGlass) |
| ? absl::make_optional(kBackgroundIdGlass) |
| : absl::nullopt; |
| } |
| |
| gfx::Rect TabStrip::GetTabAnimationTargetBounds(const Tab* tab) { |
| return bounds_animator_.GetTargetBounds(tab); |
| } |
| |
| void TabStrip::MouseMovedOutOfHost() { |
| ResizeLayoutTabs(); |
| if (reset_to_shrink_on_exit_) { |
| reset_to_shrink_on_exit_ = false; |
| SetStackedLayout(false); |
| controller_->StackedLayoutMaybeChanged(); |
| } |
| } |
| |
| float TabStrip::GetHoverOpacityForTab(float range_parameter) const { |
| return gfx::Tween::FloatValueBetween(range_parameter, hover_opacity_min_, |
| hover_opacity_max_); |
| } |
| |
| float TabStrip::GetHoverOpacityForRadialHighlight() const { |
| return radial_highlight_opacity_; |
| } |
| |
| std::u16string TabStrip::GetGroupTitle( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupTitle(group); |
| } |
| |
| tab_groups::TabGroupColorId TabStrip::GetGroupColorId( |
| const tab_groups::TabGroupId& group) const { |
| return controller_->GetGroupColorId(group); |
| } |
| |
| SkColor TabStrip::GetPaintedGroupColor( |
| const tab_groups::TabGroupColorId& color_id) const { |
| return GetThemeProvider()->GetColor( |
| GetTabGroupTabStripColorId(color_id, ShouldPaintAsActiveFrame())); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, views::AccessiblePaneView overrides: |
| |
| void TabStrip::Layout() { |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { |
| // With tab scrolling, the tabstrip is solely responsible for its own |
| // width. |
| // It should never be larger than its preferred width. |
| const int max_width = CalculatePreferredSize().width(); |
| // It should never be smaller than its minimum width. |
| const int min_width = GetMinimumSize().width(); |
| // If it can, it should fit within the tab strip region. |
| const int available_width = available_width_callback_.Run(); |
| // It should be as wide as possible subject to the above constraints. |
| const int width = std::min(max_width, std::max(min_width, available_width)); |
| SetBounds(0, 0, width, GetLayoutConstant(TAB_HEIGHT)); |
| SetTabSlotVisibility(); |
| } |
| |
| if (IsAnimating()) { |
| // Hide tabs that have animated at least partially out of the clip region. |
| SetTabSlotVisibility(); |
| return; |
| } |
| |
| // Only do a layout if our size or the available width changed. |
| const int available_width = GetAvailableWidthForTabStrip(); |
| if (last_layout_size_ == size() && last_available_width_ == available_width) |
| return; |
| if (drag_context_->IsDragSessionActive()) |
| return; |
| CompleteAnimationAndLayout(); |
| } |
| |
| void TabStrip::PaintChildren(const views::PaintInfo& paint_info) { |
| // The view order doesn't match the paint order (layout_helper_ contains the |
| // view ordering). |
| bool is_dragging = false; |
| Tab* active_tab = nullptr; |
| std::vector<Tab*> tabs_dragging; |
| std::vector<Tab*> selected_and_hovered_tabs; |
| |
| // When background tab shapes are visible, as for hovered or selected tabs, |
| // the paint order must be handled carefully to avoid Z-order errors, so |
| // this code defers drawing such tabs until later. |
| const auto paint_or_add_to_tabs = [&paint_info, |
| &selected_and_hovered_tabs](Tab* tab) { |
| if (tab->tab_style()->GetZValue() > 0.0) { |
| selected_and_hovered_tabs.push_back(tab); |
| } else { |
| tab->Paint(paint_info); |
| } |
| }; |
| |
| std::vector<Tab*> all_tabs = layout_helper_->GetTabs(); |
| |
| int active_tab_index = -1; |
| for (int i = all_tabs.size() - 1; i >= 0; --i) { |
| Tab* tab = all_tabs[i]; |
| if (tab->dragging() && !stacked_layout_) { |
| is_dragging = true; |
| if (tab->IsActive()) { |
| active_tab = tab; |
| active_tab_index = i; |
| } else { |
| tabs_dragging.push_back(tab); |
| } |
| } else if (tab->IsActive()) { |
| active_tab = tab; |
| active_tab_index = i; |
| } else if (!stacked_layout_) { |
| paint_or_add_to_tabs(tab); |
| } |
| } |
| |
| // Draw from the left and then the right if we're in touch mode. |
| if (stacked_layout_ && active_tab_index >= 0) { |
| for (int i = 0; i < active_tab_index; ++i) { |
| Tab* tab = all_tabs[i]; |
| tab->Paint(paint_info); |
| } |
| |
| for (int i = all_tabs.size() - 1; i > active_tab_index; --i) { |
| Tab* tab = all_tabs[i]; |
| tab->Paint(paint_info); |
| } |
| } |
| |
| std::stable_sort(selected_and_hovered_tabs.begin(), |
| selected_and_hovered_tabs.end(), [](Tab* tab1, Tab* tab2) { |
| return tab1->tab_style()->GetZValue() < |
| tab2->tab_style()->GetZValue(); |
| }); |
| for (Tab* tab : selected_and_hovered_tabs) |
| tab->Paint(paint_info); |
| |
| // Keep track of the dragging group if dragging by the group header, or |
| // the current group if just dragging tabs into a group. At most one of these |
| // will have a value, since a drag is either a group drag or a tab drag. |
| absl::optional<tab_groups::TabGroupId> dragging_group = absl::nullopt; |
| absl::optional<tab_groups::TabGroupId> current_group = absl::nullopt; |
| |
| // Paint group headers and underlines. |
| for (const auto& group_view_pair : group_views_) { |
| if (group_view_pair.second->header()->dragging()) { |
| // If the whole group is dragging, defer painting both the header and the |
| // underline, since they should appear above non-dragging tabs and groups. |
| // Instead, just track the dragging group. |
| dragging_group = group_view_pair.first; |
| } else { |
| group_view_pair.second->header()->Paint(paint_info); |
| |
| if (tabs_dragging.size() > 0 && |
| tabs_dragging[0]->group() == group_view_pair.first) { |
| // If tabs are being dragged into a group, defer painting just the |
| // underline, which should appear above non-active dragging tabs as well |
| // as all non-dragging tabs and groups. Instead, just track the group |
| // that the tabs are being dragged into. |
| current_group = group_view_pair.first; |
| } else { |
| group_view_pair.second->underline()->Paint(paint_info); |
| } |
| } |
| } |
| |
| // Always paint the active tab over all the inactive tabs. |
| if (active_tab && !is_dragging) |
| active_tab->Paint(paint_info); |
| |
| // If dragging a group, paint the group highlight and header above all |
| // non-dragging tabs and groups. |
| if (dragging_group.has_value()) { |
| group_views_[dragging_group.value()]->highlight()->Paint(paint_info); |
| group_views_[dragging_group.value()]->header()->Paint(paint_info); |
| } |
| |
| // Paint the dragged tabs. |
| for (size_t i = 0; i < tabs_dragging.size(); ++i) |
| tabs_dragging[i]->Paint(paint_info); |
| |
| // If dragging a group, or dragging tabs into a group, paint the group |
| // underline above the dragging tabs. Otherwise, any non-active dragging tabs |
| // will not get an underline. |
| if (dragging_group.has_value()) |
| group_views_[dragging_group.value()]->underline()->Paint(paint_info); |
| if (current_group.has_value()) |
| group_views_[current_group.value()]->underline()->Paint(paint_info); |
| |
| // If the active tab is being dragged, it goes last. |
| if (active_tab && is_dragging) |
| active_tab->Paint(paint_info); |
| } |
| |
| gfx::Size TabStrip::GetMinimumSize() const { |
| // If tabs can be stacked, our minimum width is the smallest width of the |
| // stacked tabstrip. |
| const int minimum_width = |
| (touch_layout_ || adjust_layout_) |
| ? GetStackableTabWidth() + (2 * kStackedPadding * kMaxStackedCount) |
| : layout_helper_->CalculateMinimumWidth(); |
| |
| return gfx::Size(minimum_width, GetLayoutConstant(TAB_HEIGHT)); |
| } |
| |
| gfx::Size TabStrip::CalculatePreferredSize() const { |
| int preferred_width; |
| // The tabstrip needs to always exactly fit the bounds of the tabs so that |
| // NTB can be laid out just to the right of the rightmost tab. When the tabs |
| // aren't at their ideal bounds (i.e. during animation or a drag), we need to |
| // size ourselves to exactly fit wherever the tabs *currently* are. |
| if (IsAnimating() || drag_context_->IsDragSessionActive()) { |
| // The visual order of the tabs can be out of sync with the logical order, |
| // so we have to check all of them to find the visually trailing-most one. |
| int max_x = 0; |
| for (auto* tab : layout_helper_->GetTabs()) { |
| max_x = std::max(max_x, tab->bounds().right()); |
| } |
| // The tabs span from 0 to |max_x|, so |max_x| is the current width |
| // occupied by tabs. We report the current width as our preferred width so |
| // that the tab strip is sized to exactly fit the current position of the |
| // tabs. |
| preferred_width = max_x; |
| } else { |
| preferred_width = override_available_width_for_tabs_ |
| ? override_available_width_for_tabs_.value() |
| : layout_helper_->CalculatePreferredWidth(); |
| } |
| |
| return gfx::Size(preferred_width, GetLayoutConstant(TAB_HEIGHT)); |
| } |
| |
| views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) { |
| if (!HitTestPoint(point)) |
| return nullptr; |
| |
| if (!touch_layout_) { |
| // Return any view that isn't a Tab or this TabStrip immediately. We don't |
| // want to interfere. |
| views::View* v = View::GetTooltipHandlerForPoint(point); |
| if (v && v != this && !views::IsViewClass<Tab>(v)) |
| return v; |
| |
| views::View* tab = FindTabHitByPoint(point); |
| if (tab) |
| return tab; |
| } else { |
| Tab* tab = FindTabForEvent(point); |
| if (tab) |
| return ConvertPointToViewAndGetTooltipHandler(this, tab, point); |
| } |
| return this; |
| } |
| |
| BrowserRootView::DropIndex TabStrip::GetDropIndex( |
| const ui::DropTargetEvent& event) { |
| // Force animations to stop, otherwise it makes the index calculation tricky. |
| StopAnimating(true); |
| |
| // If the UI layout is right-to-left, we need to mirror the mouse |
| // coordinates since we calculate the drop index based on the |
| // original (and therefore non-mirrored) positions of the tabs. |
| const int x = GetMirroredXInView(event.x()); |
| |
| std::vector<TabSlotView*> views = layout_helper_->GetTabSlotViews(); |
| |
| // Loop until we find a tab or group header that intersects |event|'s |
| // location. |
| for (TabSlotView* view : views) { |
| const int max_x = view->x() + view->width(); |
| if (x >= max_x) |
| continue; |
| |
| if (view->GetTabSlotViewType() == TabSlotView::ViewType::kTab) { |
| Tab* const tab = static_cast<Tab*>(view); |
| // Closing tabs should be skipped. |
| if (tab->closing()) |
| continue; |
| |
| // GetModelIndexOf is an O(n) operation. Since we will definitely |
| // return from the loop at this point, it is only called once. |
| // Hence the loop is still O(n). Calling this every loop iteration |
| // must be avoided since it will become O(n^2). |
| const int model_index = GetModelIndexOf(tab); |
| const bool first_in_group = |
| tab->group().has_value() && |
| model_index == controller_->GetFirstTabInGroup(tab->group().value()); |
| |
| // When hovering over the left or right quarter of a tab, the drop |
| // indicator will point between tabs. |
| const int hot_width = tab->width() / 4; |
| |
| if (x >= (max_x - hot_width)) |
| return {model_index + 1, true /* drop_before */, |
| false /* drop_in_group */}; |
| else if (x < tab->x() + hot_width) |
| return {model_index, true /* drop_before */, first_in_group}; |
| else |
| return {model_index, false /* drop_before */, |
| false /* drop_in_group */}; |
| } else { |
| TabGroupHeader* const group_header = static_cast<TabGroupHeader*>(view); |
| const int first_tab_index = |
| controller_->GetFirstTabInGroup(group_header->group().value()) |
| .value(); |
| |
| if (x < max_x - group_header->width() / 2) |
| return {first_tab_index, true /* drop_before */, |
| false /* drop_in_group */}; |
| else |
| return {first_tab_index, true /* drop_before */, |
| true /* drop_in_group */}; |
| } |
| } |
| |
| // The drop isn't over a tab, add it to the end. |
| return {GetTabCount(), true, false}; |
| } |
| |
| views::View* TabStrip::GetViewForDrop() { |
| return this; |
| } |
| |
| void TabStrip::HandleDragUpdate( |
| const absl::optional<BrowserRootView::DropIndex>& index) { |
| SetDropArrow(index); |
| } |
| |
| void TabStrip::HandleDragExited() { |
| SetDropArrow({}); |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // TabStrip, private: |
| |
| void TabStrip::Init() { |
| SetID(VIEW_ID_TAB_STRIP); |
| // So we get enter/exit on children to switch stacked layout on and off. |
| SetNotifyEnterExitOnChild(true); |
| |
| if (g_drop_indicator_width == 0) { |
| // Direction doesn't matter, both images are the same size. |
| gfx::ImageSkia* drop_image = GetDropArrowImage(true); |
| g_drop_indicator_width = drop_image->width(); |
| g_drop_indicator_height = drop_image->height(); |
| } |
| |
| UpdateContrastRatioValues(); |
| |
| if (!gfx::Animation::ShouldRenderRichAnimation()) |
| bounds_animator_.SetAnimationDuration(base::TimeDelta()); |
| |
| bounds_animator_.AddObserver(this); |
| } |
| |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> TabStrip::GetGroupHeaders() { |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> group_headers; |
| for (const auto& group_view_pair : group_views_) { |
| group_headers.insert(std::make_pair(group_view_pair.first, |
| group_view_pair.second->header())); |
| } |
| return group_headers; |
| } |
| |
| void TabStrip::NewTabButtonPressed(const ui::Event& event) { |
| new_tab_button_pressed_start_time_ = base::TimeTicks::Now(); |
| |
| base::RecordAction(base::UserMetricsAction("NewTab_Button")); |
| UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON, |
| TabStripModel::NEW_TAB_ENUM_COUNT); |
| if (event.IsMouseEvent()) { |
| // Prevent the hover card from popping back in immediately. This forces a |
| // normal fade-in. |
| if (hover_card_controller_) |
| hover_card_controller_->PreventImmediateReshow(); |
| |
| const ui::MouseEvent& mouse = static_cast<const ui::MouseEvent&>(event); |
| if (mouse.IsOnlyMiddleMouseButton()) { |
| if (ui::Clipboard::IsSupportedClipboardBuffer( |
| ui::ClipboardBuffer::kSelection)) { |
| ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); |
| CHECK(clipboard); |
| std::u16string clipboard_text; |
| clipboard->ReadText(ui::ClipboardBuffer::kSelection, |
| /* data_dst = */ nullptr, &clipboard_text); |
| if (!clipboard_text.empty()) |
| controller_->CreateNewTabWithLocation(clipboard_text); |
| } |
| return; |
| } |
| } |
| |
| controller_->CreateNewTab(); |
| if (event.type() == ui::ET_GESTURE_TAP) |
| TouchUMA::RecordGestureAction(TouchUMA::kGestureNewTabTap); |
| } |
| |
| void TabStrip::StartInsertTabAnimation(int model_index, TabPinned pinned) { |
| layout_helper_->InsertTabAt(model_index, tab_at(model_index), pinned); |
| |
| PrepareForAnimation(); |
| |
| ExitTabClosingMode(); |
| |
| gfx::Rect bounds = tab_at(model_index)->bounds(); |
| bounds.set_height(GetLayoutConstant(TAB_HEIGHT)); |
| |
| // Adjust the starting bounds of the new tab. |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| if (model_index > 0) { |
| // If we have a tab to our left, start at its right edge. |
| bounds.set_x(tab_at(model_index - 1)->bounds().right() - tab_overlap); |
| } else if (model_index + 1 < GetTabCount()) { |
| // Otherwise, if we have a tab to our right, start at its left edge. |
| bounds.set_x(tab_at(model_index + 1)->bounds().x()); |
| } else { |
| NOTREACHED() << "First tab inserted into the tabstrip should not animate."; |
| } |
| |
| // Start at the width of the overlap in order to animate at the same speed |
| // the surrounding tabs are moving, since at this width the subsequent tab |
| // is naturally positioned at the same X coordinate. |
| bounds.set_width(tab_overlap); |
| tab_at(model_index)->SetBoundsRect(bounds); |
| |
| // Animate in to the full width. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::StartRemoveTabAnimation(int model_index, bool was_active) { |
| const int model_count = GetModelCount(); |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| if (in_tab_close_ && model_count > 0 && model_index != model_count) { |
| // The user closed a tab other than the last tab. Set |
| // override_available_width_for_tabs_ so that as the user closes tabs with |
| // the mouse a tab continues to fall under the mouse. |
| int next_active_index = controller_->GetActiveIndex(); |
| DCHECK(IsValidModelIndex(next_active_index)); |
| if (model_index <= next_active_index) { |
| // At this point, model's internal state has already been updated. |
| // |contents| has been detached from model and the active index has been |
| // updated. But the tab for |contents| isn't removed yet. Thus, we need to |
| // fix up next_active_index based on it. |
| next_active_index++; |
| } |
| Tab* next_active_tab = tab_at(next_active_index); |
| Tab* tab_being_removed = tab_at(model_index); |
| |
| int size_delta = tab_being_removed->width(); |
| if (!tab_being_removed->data().pinned && was_active && |
| GetActiveTabWidth() > GetInactiveTabWidth()) { |
| // When removing an active, non-pinned tab, an inactive tab will be made |
| // active and thus given the active width. Thus the width being removed |
| // from the strip is really the current width of whichever inactive tab |
| // will be made active. |
| size_delta = next_active_tab->width(); |
| } |
| |
| override_available_width_for_tabs_ = |
| ideal_bounds(model_count).right() - size_delta + tab_overlap; |
| } |
| |
| if (!touch_layout_) |
| PrepareForAnimation(); |
| |
| Tab* tab = tab_at(model_index); |
| tab->SetClosing(true); |
| |
| int old_x = tabs_.ideal_bounds(model_index).x(); |
| RemoveTabFromViewModel(model_index); |
| |
| if (touch_layout_) { |
| touch_layout_->RemoveTab(model_index, |
| UpdateIdealBoundsForPinnedTabs(nullptr), old_x); |
| } |
| |
| layout_helper_->RemoveTabAt(model_index, tab); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| |
| if (in_tab_close_ && model_count > 0 && |
| override_available_width_for_tabs_ > |
| ideal_bounds(model_count - 1).right()) { |
| // Tab closing mode is no longer constraining tab widths - they're at full |
| // size. Exit tab closing mode so that it doesn't artificially inflate the |
| // tabstrip's bounds. |
| ExitTabClosingMode(); |
| } |
| |
| // TODO(pkasting): When closing multiple tabs, we get repeated RemoveTabAt() |
| // calls, each of which closes a new tab and thus generates different ideal |
| // bounds. We should update the animations of any other tabs that are |
| // currently being closed to reflect the new ideal bounds, or else change from |
| // removing one tab at a time to animating the removal of all tabs at once. |
| |
| // Compute the target bounds for animating this tab closed. The tab's left |
| // edge should stay joined to the right edge of the previous tab, if any. |
| gfx::Rect tab_bounds = tab->bounds(); |
| tab_bounds.set_x((model_index > 0) |
| ? (ideal_bounds(model_index - 1).right() - tab_overlap) |
| : 0); |
| |
| // The tab should animate to the width of the overlap in order to close at the |
| // same speed the surrounding tabs are moving, since at this width the |
| // subsequent tab is naturally positioned at the same X coordinate. |
| tab_bounds.set_width(tab_overlap); |
| |
| // Animate the tab closed. |
| bounds_animator_.AnimateViewTo( |
| tab, tab_bounds, |
| std::make_unique<RemoveTabDelegate>( |
| this, tab, |
| base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed, |
| base::Unretained(this)))); |
| } |
| |
| void TabStrip::StartMoveTabAnimation() { |
| PrepareForAnimation(); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::AnimateToIdealBounds() { |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kAnimating); |
| |
| for (int i = 0; i < GetTabCount(); ++i) { |
| // If the tab is being dragged manually, skip it. |
| Tab* tab = tab_at(i); |
| if (tab->dragging() && !bounds_animator_.IsAnimating(tab)) |
| continue; |
| |
| // Also skip tabs already being animated to the same ideal bounds. Calling |
| // AnimateViewTo() again restarts the animation, which changes the timing of |
| // how the tab animates, leading to hitches. |
| const gfx::Rect& target_bounds = ideal_bounds(i); |
| if (bounds_animator_.GetTargetBounds(tab) == target_bounds) |
| continue; |
| |
| // Set an animation delegate for the tab so it will clip appropriately. |
| // Don't do this if dragging() is true. In this case the tab was |
| // previously being dragged and is now animating back to its ideal |
| // bounds; it already has an associated ResetDraggingStateDelegate that |
| // will reset this dragging state. Replacing this delegate would mean |
| // this code would also need to reset the dragging state immediately, |
| // and that could allow the new tab button to be drawn atop this tab. |
| if (bounds_animator_.IsAnimating(tab) && tab->dragging()) { |
| bounds_animator_.SetTargetBounds(tab, target_bounds); |
| } else { |
| bounds_animator_.AnimateViewTo( |
| tab, target_bounds, |
| std::make_unique<TabSlotAnimationDelegate>( |
| this, tab, |
| base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed, |
| base::Unretained(this)))); |
| } |
| } |
| |
| for (const auto& header_pair : group_views_) { |
| TabGroupHeader* const header = header_pair.second->header(); |
| |
| // If the header is being dragged manually, skip it. |
| if (header->dragging() && !bounds_animator_.IsAnimating(header)) |
| continue; |
| |
| bounds_animator_.AnimateViewTo( |
| header, |
| layout_helper_->group_header_ideal_bounds().at(header_pair.first), |
| std::make_unique<TabSlotAnimationDelegate>( |
| this, header, |
| base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed, |
| base::Unretained(this)))); |
| } |
| |
| // Because the preferred size of the tabstrip depends on the IsAnimating() |
| // condition, but starting an animation doesn't necessarily invalidate the |
| // existing preferred size and layout (which may now be incorrect), we need to |
| // signal this explicitly. |
| PreferredSizeChanged(); |
| } |
| |
| void TabStrip::SnapToIdealBounds() { |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->SetBoundsRect(ideal_bounds(i)); |
| |
| for (const auto& header_pair : group_views_) { |
| header_pair.second->header()->SetBoundsRect( |
| layout_helper_->group_header_ideal_bounds().at(header_pair.first)); |
| header_pair.second->UpdateBounds(); |
| } |
| |
| PreferredSizeChanged(); |
| } |
| |
| void TabStrip::ExitTabClosingMode() { |
| in_tab_close_ = false; |
| override_available_width_for_tabs_.reset(); |
| layout_helper_->ExitTabClosingMode(); |
| } |
| |
| bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { |
| return in_tab_close_; |
| } |
| |
| bool TabStrip::TitlebarBackgroundIsTransparent() const { |
| #if defined(OS_WIN) |
| // Windows 8+ uses transparent window contents (because the titlebar area is |
| // drawn by the system and not Chrome), but the actual titlebar is opaque. |
| if (base::win::GetVersion() >= base::win::Version::WIN8) |
| return false; |
| #endif |
| return GetWidget()->ShouldWindowContentsBeTransparent(); |
| } |
| |
| void TabStrip::CompleteAnimationAndLayout() { |
| last_available_width_ = GetAvailableWidthForTabStrip(); |
| last_layout_size_ = size(); |
| |
| bounds_animator_.Cancel(); |
| if (tab_scrolling_animation_) |
| tab_scrolling_animation_->SetCurrentValue(1); |
| |
| SwapLayoutIfNecessary(); |
| if (touch_layout_) |
| touch_layout_->SetWidth(width()); |
| |
| UpdateIdealBounds(); |
| SnapToIdealBounds(); |
| |
| SetTabSlotVisibility(); |
| SchedulePaint(); |
| } |
| |
| void TabStrip::SetTabSlotVisibility() { |
| bool last_tab_visible = false; |
| absl::optional<tab_groups::TabGroupId> last_tab_group = absl::nullopt; |
| std::vector<Tab*> tabs = layout_helper_->GetTabs(); |
| for (std::vector<Tab*>::reverse_iterator tab = tabs.rbegin(); |
| tab != tabs.rend(); ++tab) { |
| absl::optional<tab_groups::TabGroupId> current_group = (*tab)->group(); |
| if (current_group != last_tab_group && last_tab_group.has_value()) { |
| TabGroupViews* group_view = group_views_.at(last_tab_group.value()).get(); |
| group_view->header()->SetVisible(last_tab_visible); |
| group_view->underline()->SetVisible(last_tab_visible); |
| } |
| last_tab_visible = ShouldTabBeVisible(*tab); |
| last_tab_group = (*tab)->closing() ? absl::nullopt : current_group; |
| |
| // Collapsed tabs disappear once they've reached their minimum size. This |
| // is different than very small non-collapsed tabs, because in that case |
| // the tab (and its favicon) must still be visible. |
| bool is_collapsed = |
| (current_group.has_value() && |
| controller()->IsGroupCollapsed(current_group.value()) && |
| (*tab)->bounds().width() <= TabStyle::GetTabOverlap()); |
| (*tab)->SetVisible(is_collapsed ? false : last_tab_visible); |
| } |
| } |
| |
| void TabStrip::UpdateAccessibleTabIndices() { |
| const int num_tabs = GetTabCount(); |
| for (int i = 0; i < num_tabs; ++i) |
| tab_at(i)->GetViewAccessibility().OverridePosInSet(i + 1, num_tabs); |
| } |
| |
| int TabStrip::GetActiveTabWidth() const { |
| return layout_helper_->active_tab_width(); |
| } |
| |
| int TabStrip::GetInactiveTabWidth() const { |
| return layout_helper_->inactive_tab_width(); |
| } |
| |
| 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; |
| } |
| |
| int TabStrip::GetViewInsertionIndex(Tab* tab, |
| absl::optional<int> from_model_index, |
| int to_model_index) const { |
| // -1 is treated a sentinel value to indicate a tab is newly added to the |
| // beginning of the tab strip. |
| if (to_model_index < 0) |
| return 0; |
| |
| // If to_model_index is beyond the end of the tab strip, then the tab is newly |
| // added to the end of the tab strip. In that case we can just return one |
| // beyond the view index of the last existing tab. |
| if (to_model_index >= GetTabCount()) |
| return (GetTabCount() ? GetIndexOf(tab_at(GetTabCount() - 1)) + 1 : 0); |
| |
| // If there is no from_model_index, then the tab is newly added in the middle |
| // of the tab strip. In that case we treat it as coming from the end of the |
| // tab strip, since new views are ordered at the end by default. |
| if (!from_model_index.has_value()) |
| from_model_index = GetTabCount(); |
| |
| DCHECK_NE(to_model_index, from_model_index.value()); |
| |
| // Since we don't have an absolute mapping from model index to view index, we |
| // anchor on the last known view index at the given to_model_index. |
| Tab* other_tab = tab_at(to_model_index); |
| int other_view_index = GetIndexOf(other_tab); |
| |
| if (other_view_index <= 0) |
| return 0; |
| |
| // When moving to the right, just use the anchor index because the tab will |
| // replace that position in both the model and the view. This happens because |
| // the tab itself occupies a lower index that the other tabs will shift into. |
| if (to_model_index > from_model_index.value()) |
| return other_view_index; |
| |
| // When moving to the left, the tab may end up on either the left or right |
| // side of a group header, depending on if it's in that group. This affects |
| // its view index but not its model index, so we adjust the former only. |
| if (other_tab->group().has_value() && other_tab->group() != tab->group()) |
| return other_view_index - 1; |
| |
| return other_view_index; |
| } |
| |
| void TabStrip::CloseTabInternal(int model_index, CloseTabSource source) { |
| if (!IsValidModelIndex(model_index)) |
| return; |
| |
| // If we're not allowed to close this tab for whatever reason, we should not |
| // proceed. |
| if (!controller_->BeforeCloseTab(model_index, source)) |
| return; |
| |
| if (!in_tab_close_ && IsAnimating()) { |
| // Cancel any current animations. We do this as remove uses the current |
| // ideal bounds and we need to know ideal bounds is in a good state. |
| StopAnimating(true); |
| } |
| |
| if (GetWidget()) { |
| in_tab_close_ = true; |
| resize_layout_timer_.Stop(); |
| if (source == CLOSE_TAB_FROM_TOUCH) |
| StartResizeLayoutTabsFromTouchTimer(); |
| else |
| AddMessageLoopObserver(); |
| } |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); |
| if (tab_at(model_index)->group().has_value()) |
| base::RecordAction(base::UserMetricsAction("CloseGroupedTab")); |
| controller_->CloseTab(model_index); |
| } |
| |
| void TabStrip::RemoveTabFromViewModel(int index) { |
| Tab* closing_tab = tab_at(index); |
| bool closing_tab_was_active = closing_tab->IsActive(); |
| |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); |
| |
| // We still need to keep the tab alive until the remove tab animation |
| // completes. Defer destroying it until then. |
| tabs_.Remove(index); |
| selected_tabs_.DecrementFrom(index); |
| |
| if (closing_tab_was_active) |
| closing_tab->ActiveStateChanged(); |
| } |
| |
| void TabStrip::OnTabCloseAnimationCompleted(Tab* tab) { |
| DCHECK(tab->closing()); |
| |
| std::unique_ptr<Tab> deleter(tab); |
| layout_helper_->OnTabDestroyed(tab); |
| } |
| |
| void TabStrip::StoppedDraggingView(TabSlotView* view, bool* is_first_view) { |
| if (view && |
| view->GetTabSlotViewType() == TabSlotView::ViewType::kTabGroupHeader) { |
| // Ensure all tab group UI is repainted, especially the dragging highlight. |
| view->set_dragging(false); |
| SchedulePaint(); |
| return; |
| } |
| |
| int tab_data_index = GetModelIndexOf(view); |
| if (tab_data_index == -1) { |
| // The tab was removed before the drag completed. Don't do anything. |
| return; |
| } |
| |
| if (*is_first_view) { |
| *is_first_view = false; |
| PrepareForAnimation(); |
| |
| // Animate the view back to its correct position. |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| // Install a delegate to reset the dragging state when done. We have to leave |
| // dragging true for the tab otherwise it'll draw beneath the new tab button. |
| bounds_animator_.AnimateViewTo( |
| view, ideal_bounds(tab_data_index), |
| std::make_unique<ResetDraggingStateDelegate>( |
| this, static_cast<Tab*>(view), |
| base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed, |
| base::Unretained(this)))); |
| } |
| |
| void TabStrip::UpdateStackedLayoutFromMouseEvent(views::View* source, |
| const ui::MouseEvent& event) { |
| if (!adjust_layout_) |
| return; |
| |
| // The following code attempts to switch to shrink (not stacked) layout when |
| // the mouse exits the tabstrip (or the mouse is pressed on a stacked tab) and |
| // to stacked layout when a touch device is used. This is made problematic by |
| // windows generating mouse move events that do not clearly indicate the move |
| // is the result of a touch device. This assumes a real mouse is used if |
| // |kMouseMoveCountBeforeConsiderReal| mouse move events are received within |
| // the time window |kMouseMoveTime|. At the time we get a mouse press we know |
| // whether its from a touch device or not, but we don't layout then else |
| // everything shifts. Instead we wait for the release. |
| // |
| // TODO(sky): revisit this when touch events are really plumbed through. |
| #if !BUILDFLAG(IS_CHROMEOS_ASH) |
| constexpr auto kMouseMoveTime = base::TimeDelta::FromMilliseconds(200); |
| constexpr int kMouseMoveCountBeforeConsiderReal = 3; |
| #endif |
| |
| switch (event.type()) { |
| case ui::ET_MOUSE_PRESSED: |
| mouse_move_count_ = 0; |
| last_mouse_move_time_ = base::TimeTicks(); |
| SetResetToShrinkOnExit((event.flags() & ui::EF_FROM_TOUCH) == 0); |
| if (reset_to_shrink_on_exit_ && touch_layout_) { |
| gfx::Point tab_strip_point(event.location()); |
| views::View::ConvertPointToTarget(source, this, &tab_strip_point); |
| Tab* tab = FindTabForEvent(tab_strip_point); |
| if (tab && touch_layout_->IsStacked(GetModelIndexOf(tab))) { |
| SetStackedLayout(false); |
| controller_->StackedLayoutMaybeChanged(); |
| } |
| } |
| break; |
| |
| case ui::ET_MOUSE_MOVED: { |
| #if BUILDFLAG(IS_CHROMEOS_ASH) |
| // Ash does not synthesize mouse events from touch events. |
| SetResetToShrinkOnExit(true); |
| #else |
| gfx::Point location(event.location()); |
| ConvertPointToTarget(source, this, &location); |
| if (location == last_mouse_move_location_) |
| return; // Ignore spurious moves. |
| last_mouse_move_location_ = location; |
| if ((event.flags() & ui::EF_FROM_TOUCH) || |
| (event.flags() & ui::EF_IS_SYNTHESIZED)) { |
| last_mouse_move_time_ = base::TimeTicks(); |
| } else if ((base::TimeTicks::Now() - last_mouse_move_time_) >= |
| kMouseMoveTime) { |
| mouse_move_count_ = 1; |
| last_mouse_move_time_ = base::TimeTicks::Now(); |
| } else if (mouse_move_count_ < kMouseMoveCountBeforeConsiderReal) { |
| ++mouse_move_count_; |
| } else { |
| SetResetToShrinkOnExit(true); |
| } |
| #endif |
| break; |
| } |
| |
| case ui::ET_MOUSE_RELEASED: { |
| gfx::Point location(event.location()); |
| ConvertPointToTarget(source, this, &location); |
| last_mouse_move_location_ = location; |
| mouse_move_count_ = 0; |
| last_mouse_move_time_ = base::TimeTicks(); |
| if ((event.flags() & ui::EF_FROM_TOUCH) == ui::EF_FROM_TOUCH) { |
| SetStackedLayout(true); |
| controller_->StackedLayoutMaybeChanged(); |
| } |
| break; |
| } |
| |
| default: |
| break; |
| } |
| } |
| |
| void TabStrip::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 = GetTabBackgroundColor( |
| TabActive::kInactive, BrowserFrameActiveState::kUseCurrent); |
| const auto get_blend = [inactive_bg](SkColor target, float contrast) { |
| return color_utils::BlendForMinContrast(inactive_bg, inactive_bg, target, |
| contrast); |
| }; |
| |
| const SkColor active_bg = GetTabBackgroundColor( |
| TabActive::kActive, BrowserFrameActiveState::kUseCurrent); |
| 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, inactive_bg); |
| // The contrast ratio for the separator between inactive tabs. |
| constexpr float kTabSeparatorContrast = 2.5f; |
| separator_color_ = get_blend(inactive_fg, kTabSeparatorContrast).color; |
| } |
| |
| void TabStrip::ShiftTabRelative(Tab* tab, int offset) { |
| DCHECK_EQ(1, std::abs(offset)); |
| const int start_index = GetModelIndexOf(tab); |
| int target_index = start_index + offset; |
| |
| if (!IsValidModelIndex(start_index)) |
| return; |
| |
| 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. |
| absl::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 (controller_->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()); |
| 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) { |
| DCHECK_EQ(1, std::abs(offset)); |
| gfx::Range tabs_in_group = controller_->ListTabsInGroup(group); |
| |
| const int start_index = tabs_in_group.start(); |
| int target_index = start_index + offset; |
| |
| if (offset > 0) |
| target_index += tabs_in_group.length() - 1; |
| |
| if (!IsValidModelIndex(start_index) || !IsValidModelIndex(target_index)) |
| return; |
| |
| // Avoid moving into the middle of another group by accounting for its size. |
| absl::optional<tab_groups::TabGroupId> target_group = |
| tab_at(target_index)->group(); |
| if (target_group.has_value()) { |
| target_index += |
| offset * |
| (controller_->ListTabsInGroup(target_group.value()).length() - 1); |
| } |
| |
| if (!IsValidModelIndex(target_index)) |
| return; |
| |
| if (controller_->IsTabPinned(start_index) != |
| controller_->IsTabPinned(target_index)) |
| return; |
| |
| controller_->MoveGroup(group, target_index); |
| } |
| |
| void TabStrip::ResizeLayoutTabs() { |
| // We've been called back after the TabStrip has been emptied out (probably |
| // just prior to the window being destroyed). We need to do nothing here or |
| // else GetTabAt below will crash. |
| if (GetTabCount() == 0) |
| return; |
| |
| // It is critically important that this is unhooked here, otherwise we will |
| // keep spying on messages forever. |
| RemoveMessageLoopObserver(); |
| |
| ExitTabClosingMode(); |
| int pinned_tab_count = GetPinnedTabCount(); |
| if (pinned_tab_count == GetTabCount()) { |
| // Only pinned tabs, we know the tab widths won't have changed (all |
| // pinned tabs have the same width), so there is nothing to do. |
| return; |
| } |
| // Don't try and avoid layout based on tab sizes. If tabs are small enough |
| // then the width of the active tab may not change, but other widths may |
| // have. This is particularly important if we've overflowed (all tabs are at |
| // the min). |
| StartResizeLayoutAnimation(); |
| } |
| |
| void TabStrip::ResizeLayoutTabsFromTouch() { |
| // Don't resize if the user is interacting with the tabstrip. |
| if (!drag_context_->IsDragSessionActive()) |
| ResizeLayoutTabs(); |
| else |
| StartResizeLayoutTabsFromTouchTimer(); |
| } |
| |
| void TabStrip::StartResizeLayoutTabsFromTouchTimer() { |
| // Amount of time we delay before resizing after a close from a touch. |
| constexpr auto kTouchResizeLayoutTime = base::TimeDelta::FromSeconds(2); |
| |
| resize_layout_timer_.Stop(); |
| resize_layout_timer_.Start(FROM_HERE, kTouchResizeLayoutTime, this, |
| &TabStrip::ResizeLayoutTabsFromTouch); |
| } |
| |
| void TabStrip::AddMessageLoopObserver() { |
| if (!mouse_watcher_) { |
| // Expand the watched region downwards below the bottom of the tabstrip. |
| // This allows users to move the cursor horizontally, to another tab, |
| // without accidentally exiting closing mode if they drift verticaally |
| // slightly out of the tabstrip. |
| constexpr int kTabStripAnimationVSlop = 40; |
| // Expand the watched region to the right to cover the NTB. This prevents |
| // the scenario where the user goes to click on the NTB while they're in |
| // closing mode, and closing mode exits just as they reach the NTB. |
| constexpr int kTabStripAnimationHSlop = 60; |
| mouse_watcher_ = std::make_unique<views::MouseWatcher>( |
| std::make_unique<views::MouseWatcherViewHost>( |
| this, |
| gfx::Insets(0, base::i18n::IsRTL() ? kTabStripAnimationHSlop : 0, |
| kTabStripAnimationVSlop, |
| base::i18n::IsRTL() ? 0 : kTabStripAnimationHSlop)), |
| this); |
| } |
| mouse_watcher_->Start(GetWidget()->GetNativeWindow()); |
| } |
| |
| void TabStrip::RemoveMessageLoopObserver() { |
| mouse_watcher_ = nullptr; |
| } |
| |
| gfx::Rect TabStrip::GetDropBounds(int drop_index, |
| bool drop_before, |
| bool drop_in_group, |
| bool* is_beneath) { |
| DCHECK_NE(drop_index, -1); |
| |
| // The X location the indicator points to. |
| int center_x = -1; |
| |
| if (GetTabCount() == 0) { |
| // If the tabstrip is empty, it doesn't matter where the drop arrow goes. |
| // The tabstrip can only be transiently empty, e.g. during shutdown. |
| return gfx::Rect(); |
| } |
| |
| Tab* tab = tab_at(std::min(drop_index, GetTabCount() - 1)); |
| const bool first_in_group = |
| drop_index < GetTabCount() && tab->group().has_value() && |
| GetModelIndexOf(tab) == |
| controller_->GetFirstTabInGroup(tab->group().value()); |
| |
| const int overlap = TabStyle::GetTabOverlap(); |
| if (!drop_before || !first_in_group || drop_in_group) { |
| // Dropping between tabs, or between a group header and the group's first |
| // tab. |
| center_x = tab->x(); |
| const int width = tab->width(); |
| if (drop_index < GetTabCount()) |
| center_x += drop_before ? (overlap / 2) : (width / 2); |
| else |
| center_x += width - (overlap / 2); |
| } else { |
| // Dropping before a group header. |
| TabGroupHeader* const header = group_header(tab->group().value()); |
| center_x = header->x() + overlap / 2; |
| } |
| |
| // Mirror the center point if necessary. |
| center_x = GetMirroredXInView(center_x); |
| |
| // Determine the screen bounds. |
| gfx::Point drop_loc(center_x - g_drop_indicator_width / 2, |
| -g_drop_indicator_height); |
| ConvertPointToScreen(this, &drop_loc); |
| gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), g_drop_indicator_width, |
| g_drop_indicator_height); |
| |
| // If the rect doesn't fit on the monitor, push the arrow to the bottom. |
| display::Screen* screen = display::Screen::GetScreen(); |
| display::Display display = screen->GetDisplayMatching(drop_bounds); |
| *is_beneath = !display.bounds().Contains(drop_bounds); |
| if (*is_beneath) |
| drop_bounds.Offset(0, drop_bounds.height() + height()); |
| |
| return drop_bounds; |
| } |
| |
| void TabStrip::SetDropArrow( |
| const absl::optional<BrowserRootView::DropIndex>& index) { |
| if (!index) { |
| controller_->OnDropIndexUpdate(-1, false); |
| drop_arrow_.reset(); |
| return; |
| } |
| |
| // Let the controller know of the index update. |
| controller_->OnDropIndexUpdate(index->value, index->drop_before); |
| |
| if (drop_arrow_ && (index == drop_arrow_->index())) |
| return; |
| |
| bool is_beneath; |
| gfx::Rect drop_bounds = GetDropBounds(index->value, index->drop_before, |
| index->drop_in_group, &is_beneath); |
| |
| if (!drop_arrow_) { |
| drop_arrow_ = std::make_unique<DropArrow>(*index, !is_beneath, GetWidget()); |
| } else { |
| drop_arrow_->set_index(*index); |
| drop_arrow_->SetPointDown(!is_beneath); |
| } |
| |
| // Reposition the window. |
| drop_arrow_->SetWindowBounds(drop_bounds); |
| } |
| |
| // static |
| gfx::ImageSkia* TabStrip::GetDropArrowImage(bool is_down) { |
| return ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed( |
| is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP); |
| } |
| |
| // TabStrip:TabContextMenuController: |
| // ---------------------------------------------------------- |
| |
| TabStrip::TabContextMenuController::TabContextMenuController(TabStrip* parent) |
| : parent_(parent) {} |
| |
| void TabStrip::TabContextMenuController::ShowContextMenuForViewImpl( |
| views::View* source, |
| const gfx::Point& point, |
| ui::MenuSourceType source_type) { |
| // We are only intended to be installed as a context-menu handler for tabs, so |
| // this cast should be safe. |
| DCHECK(views::IsViewClass<Tab>(source)); |
| Tab* const tab = static_cast<Tab*>(source); |
| if (tab->closing()) |
| return; |
| parent_->controller()->ShowContextMenuForTab(tab, point, source_type); |
| } |
| |
| // TabStrip:DropArrow: |
| // ---------------------------------------------------------- |
| |
| TabStrip::DropArrow::DropArrow(const BrowserRootView::DropIndex& index, |
| bool point_down, |
| views::Widget* context) |
| : index_(index), point_down_(point_down) { |
| arrow_window_ = new views::Widget; |
| views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| params.z_order = ui::ZOrderLevel::kFloatingUIElement; |
| params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; |
| params.accept_events = false; |
| params.bounds = gfx::Rect(g_drop_indicator_width, g_drop_indicator_height); |
| params.context = context->GetNativeWindow(); |
| arrow_window_->Init(std::move(params)); |
| arrow_view_ = |
| arrow_window_->SetContentsView(std::make_unique<views::ImageView>()); |
| arrow_view_->SetImage(GetDropArrowImage(point_down_)); |
| scoped_observation_.Observe(arrow_window_); |
| |
| arrow_window_->Show(); |
| } |
| |
| TabStrip::DropArrow::~DropArrow() { |
| // Close eventually deletes the window, which deletes arrow_view too. |
| if (arrow_window_) |
| arrow_window_->Close(); |
| } |
| |
| void TabStrip::DropArrow::SetPointDown(bool down) { |
| if (point_down_ == down) |
| return; |
| |
| point_down_ = down; |
| arrow_view_->SetImage(GetDropArrowImage(point_down_)); |
| } |
| |
| void TabStrip::DropArrow::SetWindowBounds(const gfx::Rect& bounds) { |
| arrow_window_->SetBounds(bounds); |
| } |
| |
| void TabStrip::DropArrow::OnWidgetDestroying(views::Widget* widget) { |
| DCHECK(scoped_observation_.IsObservingSource(arrow_window_)); |
| scoped_observation_.Reset(); |
| arrow_window_ = nullptr; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| void TabStrip::PrepareForAnimation() { |
| if (!drag_context_->IsDragSessionActive() && |
| !TabDragController::IsAttachedTo(GetDragContext())) { |
| for (int i = 0; i < GetTabCount(); ++i) |
| tab_at(i)->set_dragging(false); |
| } |
| } |
| |
| void TabStrip::UpdateIdealBounds() { |
| if (GetTabCount() == 0) |
| return; // Should only happen during creation/destruction, ignore. |
| |
| // Update |last_available_width_| in case there is a different amount of |
| // available width than there was in the last layout (e.g. if the tabstrip |
| // is currently hidden). |
| last_available_width_ = GetAvailableWidthForTabStrip(); |
| |
| if (!touch_layout_) { |
| const int available_width_for_tabs = CalculateAvailableWidthForTabs(); |
| layout_helper_->UpdateIdealBounds(available_width_for_tabs); |
| } |
| } |
| |
| int TabStrip::UpdateIdealBoundsForPinnedTabs(int* first_non_pinned_index) { |
| layout_helper_->UpdateIdealBoundsForPinnedTabs(); |
| if (first_non_pinned_index) |
| *first_non_pinned_index = layout_helper_->first_non_pinned_tab_index(); |
| return layout_helper_->first_non_pinned_tab_x(); |
| } |
| |
| int TabStrip::CalculateAvailableWidthForTabs() const { |
| return override_available_width_for_tabs_.value_or( |
| GetAvailableWidthForTabStrip()); |
| } |
| |
| int TabStrip::GetAvailableWidthForTabStrip() const { |
| return available_width_callback_ |
| ? available_width_callback_.Run() |
| : parent()->GetAvailableSize(this).width().value(); |
| } |
| |
| void TabStrip::StartResizeLayoutAnimation() { |
| PrepareForAnimation(); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| void TabStrip::StartPinnedTabAnimation() { |
| ExitTabClosingMode(); |
| |
| PrepareForAnimation(); |
| |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| } |
| |
| bool TabStrip::IsPointInTab(Tab* tab, |
| const gfx::Point& point_in_tabstrip_coords) { |
| if (!tab->GetVisible()) |
| return false; |
| gfx::Point point_in_tab_coords(point_in_tabstrip_coords); |
| View::ConvertPointToTarget(this, tab, &point_in_tab_coords); |
| return tab->HitTestPoint(point_in_tab_coords); |
| } |
| |
| Tab* TabStrip::FindTabForEvent(const gfx::Point& point) { |
| DCHECK(touch_layout_); |
| int active_tab_index = touch_layout_->active_index(); |
| Tab* tab = FindTabForEventFrom(point, active_tab_index, -1); |
| return tab ? tab : FindTabForEventFrom(point, active_tab_index + 1, 1); |
| } |
| |
| Tab* TabStrip::FindTabForEventFrom(const gfx::Point& point, |
| int start, |
| int delta) { |
| // |start| equals GetTabCount() when there are only pinned tabs. |
| if (start == GetTabCount()) |
| start += delta; |
| for (int i = start; i >= 0 && i < GetTabCount(); i += delta) { |
| if (IsPointInTab(tab_at(i), point)) |
| return tab_at(i); |
| } |
| return nullptr; |
| } |
| |
| Tab* TabStrip::FindTabHitByPoint(const gfx::Point& point) { |
| // Check all tabs, even closing tabs. Mouse events need to reach closing tabs |
| // for users to be able to rapidly middle-click close several tabs. |
| std::vector<Tab*> all_tabs = layout_helper_->GetTabs(); |
| |
| // The display order doesn't necessarily match the child order, so we iterate |
| // in display order. |
| for (size_t i = 0; i < all_tabs.size(); ++i) { |
| // If we don't first exclude points outside the current tab, the code below |
| // will return the wrong tab if the next tab is selected, the following tab |
| // is active, and |point| is in the overlap region between the two. |
| Tab* tab = all_tabs[i]; |
| if (!IsPointInTab(tab, point)) |
| continue; |
| |
| // Selected tabs render atop unselected ones, and active tabs render atop |
| // everything. Check whether the next tab renders atop this one and |point| |
| // is in the overlap region. |
| Tab* next_tab = i < (all_tabs.size() - 1) ? all_tabs[i + 1] : nullptr; |
| if (next_tab && |
| (next_tab->IsActive() || |
| (next_tab->IsSelected() && !tab->IsSelected())) && |
| IsPointInTab(next_tab, point)) |
| return next_tab; |
| |
| // This is the topmost tab for this point. |
| return tab; |
| } |
| |
| return nullptr; |
| } |
| |
| void TabStrip::SwapLayoutIfNecessary() { |
| bool needs_touch = NeedsTouchLayout(); |
| bool using_touch = touch_layout_ != nullptr; |
| if (needs_touch == using_touch) |
| return; |
| |
| if (needs_touch) { |
| const int overlap = TabStyle::GetTabOverlap(); |
| touch_layout_ = std::make_unique<StackedTabStripLayout>( |
| gfx::Size(GetStackableTabWidth(), GetLayoutConstant(TAB_HEIGHT)), |
| overlap, kStackedPadding, kMaxStackedCount, &tabs_); |
| touch_layout_->SetWidth(width()); |
| // This has to be after SetWidth() as SetWidth() is going to reset the |
| // bounds of the pinned tabs (since StackedTabStripLayout doesn't yet know |
| // how many pinned tabs there are). |
| touch_layout_->SetXAndPinnedCount(UpdateIdealBoundsForPinnedTabs(nullptr), |
| GetPinnedTabCount()); |
| touch_layout_->SetActiveIndex(controller_->GetActiveIndex()); |
| |
| base::RecordAction( |
| base::UserMetricsAction("StackedTab_EnteredStackedLayout")); |
| } else { |
| touch_layout_.reset(); |
| } |
| PrepareForAnimation(); |
| UpdateIdealBounds(); |
| AnimateToIdealBounds(); |
| SetTabSlotVisibility(); |
| } |
| |
| bool TabStrip::NeedsTouchLayout() const { |
| if (!stacked_layout_) |
| return false; |
| |
| // If a group is active in the tabstrip, the layout will not be swapped to |
| // stacked mode due to incompatibility of the UI. |
| // As an alternative, Tab Groups do interoperate with the WebUI Tab Strip, |
| // which is enabled in situations when stacked tabs are not. |
| if (!group_views_.empty()) |
| return false; |
| |
| // If tab scrolling is on, the layout will not be swapped; tab scrolling is |
| // a replacement to stacked tabs providing similar functionality across both |
| // touch and non-touch platforms. |
| if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) |
| return false; |
| |
| const int pinned_tab_count = GetPinnedTabCount(); |
| const int normal_count = GetTabCount() - pinned_tab_count; |
| if (normal_count <= 1) |
| return false; |
| |
| const int tab_overlap = TabStyle::GetTabOverlap(); |
| const int normal_width = |
| (GetStackableTabWidth() - tab_overlap) * normal_count + tab_overlap; |
| const int pinned_width = |
| std::max(0, pinned_tab_count * TabStyle::GetPinnedWidth() - tab_overlap); |
| return normal_width > (width() - pinned_width); |
| } |
| |
| void TabStrip::SetResetToShrinkOnExit(bool value) { |
| if (!adjust_layout_) |
| return; |
| |
| // We have to be using stacked layout to reset out of it. |
| value &= stacked_layout_; |
| |
| if (value == reset_to_shrink_on_exit_) |
| return; |
| |
| reset_to_shrink_on_exit_ = value; |
| // Add an observer so we know when the mouse moves out of the tabstrip. |
| if (reset_to_shrink_on_exit_) |
| AddMessageLoopObserver(); |
| else |
| RemoveMessageLoopObserver(); |
| } |
| |
| void TabStrip::OnTabSlotAnimationProgressed(TabSlotView* view) { |
| // The rightmost tab moving might have changed the tabstrip's preferred width. |
| PreferredSizeChanged(); |
| if (view->group()) |
| UpdateTabGroupVisuals(view->group().value()); |
| } |
| |
| void TabStrip::UpdateTabGroupVisuals(tab_groups::TabGroupId group_id) { |
| const auto group_views = group_views_.find(group_id); |
| if (group_views != group_views_.end()) |
| group_views->second->UpdateBounds(); |
| } |
| |
| bool TabStrip::OnMousePressed(const ui::MouseEvent& event) { |
| UpdateStackedLayoutFromMouseEvent(this, event); |
| // We can't return true here, else clicking in an empty area won't drag the |
| // window. |
| return false; |
| } |
| |
| bool TabStrip::OnMouseDragged(const ui::MouseEvent& event) { |
| ContinueDrag(this, event); |
| return true; |
| } |
| |
| void TabStrip::OnMouseReleased(const ui::MouseEvent& event) { |
| EndDrag(END_DRAG_COMPLETE); |
| UpdateStackedLayoutFromMouseEvent(this, event); |
| } |
| |
| void TabStrip::OnMouseCaptureLost() { |
| EndDrag(END_DRAG_CAPTURE_LOST); |
| } |
| |
| void TabStrip::OnMouseMoved(const ui::MouseEvent& event) { |
| UpdateStackedLayoutFromMouseEvent(this, event); |
| } |
| |
| void TabStrip::OnMouseEntered(const ui::MouseEvent& event) { |
| mouse_entered_tabstrip_time_ = base::TimeTicks::Now(); |
| SetResetToShrinkOnExit(true); |
| } |
| |
| void TabStrip::OnMouseExited(const ui::MouseEvent& event) { |
| UpdateHoverCard(nullptr, HoverCardUpdateType::kHover); |
| } |
| |
| void TabStrip::AddedToWidget() { |
| GetWidget()->AddObserver(this); |
| } |
| |
| void TabStrip::RemovedFromWidget() { |
| GetWidget()->RemoveObserver(this); |
| } |
| |
| void TabStrip::OnGestureEvent(ui::GestureEvent* event) { |
| SetResetToShrinkOnExit(false); |
| switch (event->type()) { |
| case ui::ET_GESTURE_SCROLL_END: |
| case ui::ET_SCROLL_FLING_START: |
| case ui::ET_GESTURE_END: |
| EndDrag(END_DRAG_COMPLETE); |
| if (adjust_layout_) { |
| SetStackedLayout(true); |
| controller_->StackedLayoutMaybeChanged(); |
| } |
| break; |
| |
| case ui::ET_GESTURE_LONG_PRESS: |
| drag_context_->SetMoveBehavior(TabDragController::REORDER); |
| break; |
| |
| case ui::ET_GESTURE_LONG_TAP: { |
| EndDrag(END_DRAG_CANCEL); |
| gfx::Point local_point = event->location(); |
| Tab* tab = touch_layout_ ? FindTabForEvent(local_point) |
| : FindTabHitByPoint(local_point); |
| if (tab) { |
| ConvertPointToScreen(this, &local_point); |
| controller_->ShowContextMenuForTab(tab, local_point, |
| ui::MENU_SOURCE_TOUCH); |
| } |
| break; |
| } |
| |
| case ui::ET_GESTURE_SCROLL_UPDATE: |
| ContinueDrag(this, *event); |
| break; |
| |
| case ui::ET_GESTURE_TAP_DOWN: |
| EndDrag(END_DRAG_CANCEL); |
| break; |
| |
| case ui::ET_GESTURE_TAP: { |
| const int active_index = controller_->GetActiveIndex(); |
| DCHECK_NE(-1, active_index); |
| Tab* active_tab = tab_at(active_index); |
| TouchUMA::GestureActionType action = TouchUMA::kGestureTabNoSwitchTap; |
| if (active_tab->tab_activated_with_last_tap_down()) |
| action = TouchUMA::kGestureTabSwitchTap; |
| TouchUMA::RecordGestureAction(action); |
| break; |
| } |
| |
| default: |
| break; |
| } |
| event->SetHandled(); |
| } |
| |
| views::View* TabStrip::TargetForRect(views::View* root, const gfx::Rect& rect) { |
| CHECK_EQ(root, this); |
| |
| if (!views::UsePointBasedTargeting(rect)) |
| return views::ViewTargeterDelegate::TargetForRect(root, rect); |
| const gfx::Point point(rect.CenterPoint()); |
| |
| if (!touch_layout_) { |
| // Return any view that isn't a Tab or this TabStrip immediately. We don't |
| // want to interfere. |
| views::View* v = views::ViewTargeterDelegate::TargetForRect(root, rect); |
| if (v && v != this && !views::IsViewClass<Tab>(v)) |
| return v; |
| |
| views::View* tab = FindTabHitByPoint(point); |
| if (tab) |
| return tab; |
| } else { |
| Tab* tab = FindTabForEvent(point); |
| if (tab) |
| return ConvertPointToViewAndGetEventHandler(this, tab, point); |
| } |
| return this; |
| } |
| |
| void TabStrip::OnViewFocused(views::View* observed_view) { |
| int index = tabs_.GetIndexOfView(observed_view); |
| if (index != -1) |
| controller_->OnKeyboardFocusedTabChanged(index); |
| } |
| |
| void TabStrip::OnViewBlurred(views::View* observed_view) { |
| controller_->OnKeyboardFocusedTabChanged(absl::nullopt); |
| } |
| |
| void TabStrip::OnTouchUiChanged() { |
| StopAnimating(true); |
| PreferredSizeChanged(); |
| } |
| |
| void TabStrip::AnnounceTabAddedToGroup(tab_groups::TabGroupId group_id) { |
| const std::u16string group_title = controller()->GetGroupTitle(group_id); |
| const std::u16string contents_string = |
| controller()->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 = controller()->GetGroupTitle(group_id); |
| const std::u16string contents_string = |
| controller()->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, views::View) |
| ADD_PROPERTY_METADATA(int, BackgroundOffset) |
| ADD_READONLY_PROPERTY_METADATA(int, TabCount) |
| ADD_READONLY_PROPERTY_METADATA(int, ModelCount) |
| ADD_READONLY_PROPERTY_METADATA(int, PinnedTabCount) |
| ADD_READONLY_PROPERTY_METADATA(absl::optional<int>, FocusedTabIndex) |
| ADD_READONLY_PROPERTY_METADATA(int, StrokeThickness) |
| ADD_READONLY_PROPERTY_METADATA(SkColor, |
| ToolbarTopSeparatorColor, |
| ui::metadata::SkColorConverter) |
| ADD_READONLY_PROPERTY_METADATA(SkColor, |
| TabSeparatorColor, |
| ui::metadata::SkColorConverter) |
| ADD_READONLY_PROPERTY_METADATA(float, HoverOpacityForRadialHighlight) |
| ADD_READONLY_PROPERTY_METADATA(int, ActiveTabWidth) |
| ADD_READONLY_PROPERTY_METADATA(int, InactiveTabWidth) |
| ADD_READONLY_PROPERTY_METADATA(int, AvailableWidthForTabStrip) |
| END_METADATA |