Commit cb5878d6 authored by Thomas Lukaszewicz's avatar Thomas Lukaszewicz Committed by Commit Bot

Views: Add experimental Tab Search entry-point in the tab strip

This change adds an experimental entry-point for the tab search
feature in the tab strip.

Currently this tab search button instance has been implemented as
another instance of the NewTabButton class, positioned directly next
to the existing new tab button.

The addition of this feature is gated by both the ENABLE_TAB_SEARCH
build flag and the kTabSearch UI feature flag.

Outside of changes gated by the build and feature flags, this CL
wraps the existing new tab button in a |new_tab_button_wrapper_|.
This wrapper allows the addition of the tab search button without
introducing significant changes to existing logic that deals with the
new tab button (ie animation, hit testing, padding etc). This wrapper
is not expected to change the current behavior of the new tab button.

There is follow up work to fix issues such as visual style and
creating a base class inherited by both the actual new tab button
and tab search button.

Bug: 1099917
Change-Id: I63cd0804603a8d25efd4416e4f3ea4307ff6535c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2333341
Commit-Queue: Thomas Lukaszewicz <tluk@chromium.org>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarRobert Liao <robliao@chromium.org>
Cr-Commit-Position: refs/heads/master@{#795969}
parent d063a6f8
......@@ -5690,6 +5690,9 @@ Keep your key file in a safe place. You will need it to create new versions of y
<message name="IDS_TOOLTIP_FIND" desc="The tooltip for the find button">
Find
</message>
<message name="IDS_TOOLTIP_TAB_SEARCH" desc="The tooltip for the Tab Search bubble.">
Tab search
</message>
<!-- Strings for intent picker -->
<if expr="not is_android">
......@@ -5772,6 +5775,9 @@ the Bookmarks menu.">
<message name="IDS_ACCNAME_ZOOM_SET_DEFAULT" desc="The accessible description of the Reset button in the Zoom bubble.">
Reset to default zoom level
</message>
<message name="IDS_ACCNAME_TAB_SEARCH" desc="The accessible name for the Tab Search bubble.">
Tab search
</message>
<!-- Clipboard permission -->
<message name="IDS_ALLOWED_CLIPBOARD_TITLE" desc="Title of the info bubble shown when a site has been allowed access to read the contents of the system clipboard.">
......
9abc682eb478c03fa60ce766712a08b9b2c6b29b
\ No newline at end of file
dce6b1b6b24f3a9f580a8753a926666e492c68fd
\ No newline at end of file
......@@ -83,12 +83,14 @@
#include "ui/gfx/skia_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/flex_layout.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_targeter_delegate.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/non_client_view.h"
......@@ -104,6 +106,11 @@
#include "ui/aura/window.h"
#endif
#if BUILDFLAG(ENABLE_TAB_SEARCH)
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tab_search/tab_search_bubble_view.h"
#endif
namespace {
// Distance from the next/previous stacked before before we consider the tab
......@@ -291,6 +298,33 @@ int GetStackableTabWidth() {
(ui::TouchUiController::Get()->touch_ui() ? 136 : 102);
}
// |TabControlsContainer| is as a container class for control views that live
// in the tab strip.
class TabControlsContainer : public views::View,
public views::ViewTargeterDelegate {
public:
TabControlsContainer() {
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
SetLayoutManager(std::make_unique<views::FlexLayout>());
}
~TabControlsContainer() override = default;
// views::ViewTargeterDelegate:
bool DoesIntersectRect(const View* target,
const gfx::Rect& rect) const override {
DCHECK_EQ(this, target);
const auto& children = target->children();
const auto hits_child = [target, rect](const views::View* child) {
gfx::RectF child_rect(rect);
views::View::ConvertRectToTarget(target, child, &child_rect);
return child->GetVisible() &&
child->HitTestRect(gfx::ToEnclosingRect(child_rect));
};
return std::any_of(children.cbegin(), children.cend(), hits_child);
}
};
} // namespace
///////////////////////////////////////////////////////////////////////////////
......@@ -506,7 +540,7 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
}
void DestroyDragController() override {
SetNewTabButtonVisible(true);
SetTabControlsVisible(true);
drag_controller_.reset();
}
......@@ -688,7 +722,7 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
// Hide the new tab button immediately if we didn't originate the drag.
if (!drag_controller_)
SetNewTabButtonVisible(false);
SetTabControlsVisible(false);
// Reset dragging state of existing tabs.
for (int i = 0; i < GetTabCount(); ++i)
......@@ -722,7 +756,7 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
// Let the controller know that the user is not dragging this tabstrip's
// tabs anymore.
tab_strip_->controller_->OnStoppedDragging();
SetNewTabButtonVisible(true);
SetTabControlsVisible(true);
}
void StoppedDragging(const std::vector<TabSlotView*>& views,
......@@ -732,7 +766,7 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
// Let the controller know that the user stopped dragging tabs.
tab_strip_->controller_->OnStoppedDragging();
SetNewTabButtonVisible(true);
SetTabControlsVisible(true);
if (move_only && tab_strip_->touch_layout_) {
if (completed)
tab_strip_->touch_layout_->SizeToFit();
......@@ -751,7 +785,7 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
// Immediately hide the new tab button if the last tab is being dragged.
const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab();
if (last_visible_tab && last_visible_tab->dragging())
SetNewTabButtonVisible(false);
SetTabControlsVisible(false);
std::vector<gfx::Rect> bounds = CalculateBoundsForDraggedViews(views);
DCHECK_EQ(views.size(), bounds.size());
......@@ -791,8 +825,8 @@ class TabStrip::TabDragContextImpl : public TabDragContext {
return tab_strip_->ideal_bounds(group);
}
void SetNewTabButtonVisible(bool visible) {
tab_strip_->new_tab_button_->SetVisible(visible);
void SetTabControlsVisible(bool visible) {
tab_strip_->tab_controls_container_->SetVisible(visible);
}
// Used by GetInsertionIndexForDraggedBounds() when the tabstrip is stacked.
......@@ -1017,6 +1051,8 @@ TabStrip::~TabStrip() {
RemoveMessageLoopObserver();
new_tab_button_->RemoveObserver(this);
if (tab_search_button_)
tab_search_button_->RemoveObserver(this);
hover_card_observer_.RemoveAll();
......@@ -1058,6 +1094,8 @@ void TabStrip::FrameColorsChanged() {
for (int i = 0; i < tab_count(); ++i)
tab_at(i)->FrameColorsChanged();
new_tab_button_->FrameColorsChanged();
if (tab_search_button_)
tab_search_button_->FrameColorsChanged();
UpdateContrastRatioValues();
SchedulePaint();
}
......@@ -1096,22 +1134,26 @@ bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) {
tab_drag_handle.Intersects(rect);
}
// Similarly, a hit in the new tab button is considered to be in the caption
// if it's in this thin strip.
gfx::Rect new_tab_button_drag_handle = new_tab_button_->GetMirroredBounds();
new_tab_button_drag_handle.set_height(drag_handle_extension);
if (extend_drag_handle && new_tab_button_drag_handle.Intersects(rect))
// Similarly, a hit in the tab controls container is considered to be in the
// caption if it's in this thin strip.
gfx::Rect tab_controls_drag_handle =
tab_controls_container_->GetMirroredBounds();
tab_controls_drag_handle.set_height(drag_handle_extension);
if (extend_drag_handle && tab_controls_drag_handle.Intersects(rect))
return true;
// Check to see if the rect intersects the non-button parts of the new tab
// button. The button has a non-rectangular shape, so if it's not in the
// visual portions of the button we treat it as a click to the caption.
gfx::RectF rect_in_new_tab_coords_f(rect);
View::ConvertRectToTarget(this, new_tab_button_, &rect_in_new_tab_coords_f);
gfx::Rect rect_in_new_tab_coords =
gfx::ToEnclosingRect(rect_in_new_tab_coords_f);
return new_tab_button_->GetLocalBounds().Intersects(rect_in_new_tab_coords) &&
!new_tab_button_->HitTestRect(rect_in_new_tab_coords);
// Check to see if the rect intersects the non-button parts of the tab
// controls container. The enclosed buttons have a non-rectangular shape, so
// if it's not in the visual portions of the buttons we treat it as a click to
// the caption.
gfx::RectF rect_in_tab_controls_coords_f(rect);
View::ConvertRectToTarget(this, tab_controls_container_,
&rect_in_tab_controls_coords_f);
gfx::Rect rect_in_tab_controls_coords =
gfx::ToEnclosingRect(rect_in_tab_controls_coords_f);
return tab_controls_container_->GetLocalBounds().Intersects(
rect_in_tab_controls_coords) &&
!tab_controls_container_->HitTestRect(rect_in_tab_controls_coords);
}
bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) {
......@@ -1259,7 +1301,7 @@ void TabStrip::MoveTab(int from_model_index,
StartMoveTabAnimation();
if (TabDragController::IsAttachedTo(GetDragContext()) &&
(last_tab != GetLastVisibleTab() || last_tab->dragging())) {
new_tab_button_->SetVisible(false);
tab_controls_container_->SetVisible(false);
}
SwapLayoutIfNecessary();
......@@ -1828,8 +1870,11 @@ bool TabStrip::IsTabFirst(const Tab* tab) const {
}
bool TabStrip::IsFocusInTabs() const {
// Focus is in tabs if the currently focused view is contained in the TabStrip
// and not in the |tab_controls_container_|.
return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView()) &&
GetFocusManager()->GetFocusedView() != new_tab_button_;
!tab_controls_container_->Contains(
GetFocusManager()->GetFocusedView());
}
void TabStrip::MaybeStartDrag(
......@@ -2235,9 +2280,8 @@ void TabStrip::PaintChildren(const views::PaintInfo& paint_info) {
if (active_tab && !is_dragging)
active_tab->Paint(paint_info);
// Paint the New Tab button, unless it's being painted onto its own Layer.
if (!new_tab_button_->layer())
new_tab_button_->Paint(paint_info);
// Paint the tab controls.
tab_controls_container_->Paint(paint_info);
// If dragging a group, paint the group highlight and header above all
// non-dragging tabs and groups.
......@@ -2314,9 +2358,9 @@ views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) {
if (tab)
return tab;
} else {
if (new_tab_button_->GetVisible()) {
views::View* view =
ConvertPointToViewAndGetTooltipHandler(this, new_tab_button_, point);
if (tab_controls_container_->GetVisible()) {
views::View* view = ConvertPointToViewAndGetTooltipHandler(
this, tab_controls_container_, point);
if (view)
return view;
}
......@@ -2388,19 +2432,45 @@ void TabStrip::Init() {
// So we get enter/exit on children to switch stacked layout on and off.
set_notify_enter_exit_on_child(true);
new_tab_button_ = new NewTabButton(this, this);
new_tab_button_->SetTooltipText(
tab_controls_container_ =
AddChildView(std::make_unique<TabControlsContainer>());
auto new_tab_button = std::make_unique<NewTabButton>(this, this);
new_tab_button->SetTooltipText(
l10n_util::GetStringUTF16(IDS_TOOLTIP_NEW_TAB));
new_tab_button_->SetAccessibleName(
new_tab_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_NEWTAB));
new_tab_button_->SetImageVerticalAlignment(views::ImageButton::ALIGN_BOTTOM);
new_tab_button_->SetEventTargeter(
std::make_unique<views::ViewTargeter>(new_tab_button_));
AddChildView(new_tab_button_);
new_tab_button_->AddObserver(this);
new_tab_button->SetImageVerticalAlignment(views::ImageButton::ALIGN_BOTTOM);
new_tab_button->SetEventTargeter(
std::make_unique<views::ViewTargeter>(new_tab_button.get()));
new_tab_button->AddObserver(this);
new_tab_button_ =
tab_controls_container_->AddChildView(std::move(new_tab_button));
#if BUILDFLAG(ENABLE_TAB_SEARCH)
if (base::FeatureList::IsEnabled(features::kTabSearch)) {
// TODO(tluk): Replace the use of the NewTabButton with an appropriate class
// for the tab search button. Current usage is temporary.
auto tab_search_button = std::make_unique<NewTabButton>(this, this);
tab_search_button->SetTooltipText(
l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_SEARCH));
tab_search_button->SetAccessibleName(
l10n_util::GetStringUTF16(IDS_ACCNAME_TAB_SEARCH));
tab_search_button->SetImageVerticalAlignment(
views::ImageButton::ALIGN_BOTTOM);
tab_search_button->SetEventTargeter(
std::make_unique<views::ViewTargeter>(tab_search_button.get()));
tab_search_button->AddObserver(this);
tab_search_button_ =
tab_controls_container_->AddChildView(std::move(tab_search_button));
}
#endif
UpdateNewTabButtonBorder();
new_tab_button_ideal_bounds_.set_size(new_tab_button_->GetPreferredSize());
tab_controls_container_ideal_bounds_.set_size(
tab_controls_container_->GetPreferredSize());
if (g_drop_indicator_width == 0) {
// Direction doesn't matter, both images are the same size.
......@@ -2538,11 +2608,12 @@ void TabStrip::StartRemoveTabAnimation(int model_index, bool was_active) {
// sense to me. Why is each condition justified?
if ((touch_layout_ || !in_tab_close_ || model_index == GetModelCount()) &&
TabDragController::IsAttachedTo(GetDragContext())) {
// Don't animate the new tab button when dragging tabs. Otherwise it looks
// like the new tab button magically appears from beyond the end of the tab
// Don't animate the tab controls container when dragging tabs. Otherwise it
// looks like the controls magically appear from beyond the end of the tab
// strip.
bounds_animator_.StopAnimatingView(new_tab_button_);
new_tab_button_->SetBoundsRect(new_tab_button_ideal_bounds_);
bounds_animator_.StopAnimatingView(tab_controls_container_);
tab_controls_container_->SetBoundsRect(
tab_controls_container_ideal_bounds_);
}
}
......@@ -2598,10 +2669,10 @@ void TabStrip::AnimateToIdealBounds() {
base::Unretained(this))));
}
if (bounds_animator_.GetTargetBounds(new_tab_button_) !=
new_tab_button_ideal_bounds_) {
bounds_animator_.AnimateViewTo(new_tab_button_,
new_tab_button_ideal_bounds_);
if (bounds_animator_.GetTargetBounds(tab_controls_container_) !=
tab_controls_container_ideal_bounds_) {
bounds_animator_.AnimateViewTo(tab_controls_container_,
tab_controls_container_ideal_bounds_);
}
}
......@@ -2615,7 +2686,7 @@ void TabStrip::SnapToIdealBounds() {
header_pair.second->UpdateBounds();
}
new_tab_button_->SetBoundsRect(new_tab_button_ideal_bounds_);
tab_controls_container_->SetBoundsRect(tab_controls_container_ideal_bounds_);
}
void TabStrip::ExitTabClosingMode() {
......@@ -2638,7 +2709,7 @@ int TabStrip::FrameGrabWidth() const {
// The grab area is adjacent to the new tab button. Treat the padding in the
// new tab button as part of the grab area.
constexpr int kApparentWidth = 50;
return kApparentWidth - new_tab_button_->GetInsets().right();
return kApparentWidth - tab_controls_container_->GetInsets().right();
}
bool TabStrip::TitlebarBackgroundIsTransparent() const {
......@@ -2710,8 +2781,8 @@ int TabStrip::GetTabAreaWidth() const {
}
int TabStrip::GetRightSideReservedWidth() const {
return new_tab_button_ideal_bounds_.width() + TabToNewTabButtonSpacing() +
FrameGrabWidth();
return tab_controls_container_ideal_bounds_.width() +
TabToNewTabButtonSpacing() + FrameGrabWidth();
}
const Tab* TabStrip::GetLastVisibleTab() const {
......@@ -3291,7 +3362,7 @@ void TabStrip::UpdateIdealBounds() {
if (touch_layout_) {
const int trailing_x = tabs_.ideal_bounds(tab_count() - 1).right();
new_tab_button_ideal_bounds_.set_origin(
tab_controls_container_ideal_bounds_.set_origin(
gfx::Point(trailing_x + TabToNewTabButtonSpacing(), 0));
} else {
const int available_width_for_tabs = CalculateAvailableWidthForTabs();
......@@ -3303,7 +3374,7 @@ void TabStrip::UpdateIdealBounds() {
? trailing_x
: std::min(available_width_for_tabs, trailing_x);
new_tab_button_ideal_bounds_.set_origin(
tab_controls_container_ideal_bounds_.set_origin(
gfx::Point(ntb_x_offset + TabToNewTabButtonSpacing(), 0));
}
}
......@@ -3471,12 +3542,15 @@ void TabStrip::SetResetToShrinkOnExit(bool value) {
}
void TabStrip::UpdateNewTabButtonBorder() {
// The button is placed vertically exactly in the center of the tabstrip.
// The controls container is placed vertically exactly in the center of the
// tabstrip.
const int extra_vertical_space = GetLayoutConstant(TAB_HEIGHT) -
GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP) -
NewTabButton::kButtonSize.height();
constexpr int kHorizontalInset = 8;
new_tab_button_->SetBorder(views::CreateEmptyBorder(gfx::Insets(
// TODO(tluk): Investigate whether this should apply to the |new_tab_button_|
// or the |tab_controls_container_|.
tab_controls_container_->SetBorder(views::CreateEmptyBorder(gfx::Insets(
extra_vertical_space / 2, kHorizontalInset, 0, kHorizontalInset)));
}
......@@ -3524,6 +3598,12 @@ void TabStrip::ButtonPressed(views::Button* sender, const ui::Event& event) {
if (event.type() == ui::ET_GESTURE_TAP)
TouchUMA::RecordGestureAction(TouchUMA::kGestureNewTabTap);
}
#if BUILDFLAG(ENABLE_TAB_SEARCH)
if (sender == tab_search_button_) {
TabSearchBubbleView::CreateTabSearchBubble(controller()->GetProfile(),
tab_search_button_);
}
#endif
}
// Overridden to support automation. See automation_proxy_uitest.cc.
......@@ -3659,9 +3739,9 @@ views::View* TabStrip::TargetForRect(views::View* root, const gfx::Rect& rect) {
if (tab)
return tab;
} else {
if (new_tab_button_->GetVisible()) {
views::View* view =
ConvertPointToViewAndGetEventHandler(this, new_tab_button_, point);
if (tab_controls_container_->GetVisible()) {
views::View* view = ConvertPointToViewAndGetEventHandler(
this, tab_controls_container_, point);
if (view)
return view;
}
......@@ -3694,8 +3774,9 @@ void TabStrip::OnViewBlurred(views::View* observed_view) {
void TabStrip::OnTouchUiChanged() {
UpdateNewTabButtonBorder();
new_tab_button_ideal_bounds_.set_size(new_tab_button_->GetPreferredSize());
new_tab_button_->SetBoundsRect(new_tab_button_ideal_bounds_);
tab_controls_container_ideal_bounds_.set_size(
tab_controls_container_->GetPreferredSize());
tab_controls_container_->SetBoundsRect(tab_controls_container_ideal_bounds_);
StopAnimating(true);
PreferredSizeChanged();
}
......@@ -147,9 +147,9 @@ class TabStrip : public views::AccessiblePaneView,
// Sets |stacked_layout_| and animates if necessary.
void SetStackedLayout(bool stacked_layout);
// Returns the ideal bounds of the new tab button.
gfx::Rect new_tab_button_ideal_bounds() const {
return new_tab_button_ideal_bounds_;
// Returns the ideal bounds of the tab controls container.
gfx::Rect tab_controls_container_ideal_bounds() const {
return tab_controls_container_ideal_bounds_;
}
// Adds a tab at the specified index.
......@@ -688,11 +688,16 @@ class TabStrip : public views::AccessiblePaneView,
// Responsible for animating tabs in response to model changes.
views::BoundsAnimator bounds_animator_{this};
// The "New Tab" button.
// Container that holds the |new_tab_button_| and the |tab_search_button_|.
views::View* tab_controls_container_ = nullptr;
NewTabButton* new_tab_button_ = nullptr;
// |tab_search_button_| will be null if features::kTabSearch is disabled.
// TODO(tluk): Subclass NewTabButton and set the appropriate icon for
// |tab_search_button_|.
NewTabButton* tab_search_button_ = nullptr;
// Ideal bounds of the new tab button.
gfx::Rect new_tab_button_ideal_bounds_;
// Ideal bounds of container holding the tab controls.
gfx::Rect tab_controls_container_ideal_bounds_;
// If this value is defined, it is used as the width to lay out tabs
// (instead of GetTabAreaWidth()). It is defined when closing tabs with the
......
......@@ -866,7 +866,8 @@ TEST_P(TabStripTest, NewTabButtonStaysVisible) {
CompleteAnimationAndLayout();
EXPECT_LE(tab_strip_->new_tab_button_ideal_bounds().right(), kTabStripWidth);
EXPECT_LE(tab_strip_->tab_controls_container_ideal_bounds().right(),
kTabStripWidth);
}
TEST_P(TabStripTest, NewTabButtonRightOfTabs) {
......@@ -877,7 +878,7 @@ TEST_P(TabStripTest, NewTabButtonRightOfTabs) {
AnimateToIdealBounds();
EXPECT_EQ(tab_strip_->new_tab_button_ideal_bounds().x(),
EXPECT_EQ(tab_strip_->tab_controls_container_ideal_bounds().x(),
tab_strip_->ideal_bounds(0).right() + TabToNewTabButtonSpacing());
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment