| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/quick_insert/views/quick_insert_search_field_view.h" |
| |
| #include <string> |
| #include <string_view> |
| |
| #include "ash/ash_element_identifiers.h" |
| #include "ash/quick_insert/metrics/quick_insert_performance_metrics.h" |
| #include "ash/quick_insert/views/quick_insert_focus_indicator.h" |
| #include "ash/quick_insert/views/quick_insert_key_event_handler.h" |
| #include "ash/quick_insert/views/quick_insert_pseudo_focus.h" |
| #include "ash/quick_insert/views/quick_insert_search_bar_textfield.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/ash_color_id.h" |
| #include "ash/style/icon_button.h" |
| #include "ash/style/style_util.h" |
| #include "ash/style/typography.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/rtl.h" |
| #include "base/time/time.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "components/vector_icons/vector_icons.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/chromeos/styles/cros_tokens_color_mappings.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/compositor/compositor.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/ink_drop.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/highlight_path_generator.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| #include "ui/views/layout/layout_manager.h" |
| #include "ui/views/metadata/view_factory.h" |
| #include "ui/views/vector_icons.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace ash { |
| namespace { |
| |
| constexpr auto kSearchFieldVerticalPadding = gfx::Insets::VH(6, 0); |
| constexpr auto kButtonHorizontalMargin = gfx::Insets::VH(0, 8); |
| // The default horizontal margin for the textfield when surrounding icon buttons |
| // are not visible. |
| constexpr int kDefaultTextfieldHorizontalMargin = 16; |
| // Margins around the textfield focus indicator bar. |
| constexpr auto kTextfieldFocusIndicatorMargins = gfx::Insets::VH(6, 0); |
| |
| } // namespace |
| |
| QuickInsertSearchFieldView::QuickInsertSearchFieldView( |
| SearchCallback search_callback, |
| BackCallback back_callback, |
| QuickInsertKeyEventHandler* key_event_handler, |
| QuickInsertPerformanceMetrics* performance_metrics) |
| : search_callback_(std::move(search_callback)), |
| key_event_handler_(key_event_handler), |
| performance_metrics_(performance_metrics) { |
| views::Builder<QuickInsertSearchFieldView>(this) |
| .SetOrientation(views::LayoutOrientation::kHorizontal) |
| .SetCrossAxisAlignment(views::LayoutAlignment::kCenter) |
| .SetProperty(views::kMarginsKey, kSearchFieldVerticalPadding) |
| .AddChildren( |
| views::Builder<views::ImageButton>( |
| std::make_unique<IconButton>( |
| std::move(back_callback), IconButton::Type::kSmallFloating, |
| &(::features::IsRoundedIconsEnabled() |
| ? vector_icons::kArrowBackIcon |
| : vector_icons::kArrowBackOldIcon), |
| IDS_ACCNAME_BACK)) |
| .CopyAddressTo(&back_button_) |
| .SetProperty(views::kMarginsKey, kButtonHorizontalMargin) |
| .SetVisible(false), |
| views::Builder<QuickInsertSearchBarTextfield>( |
| std::make_unique<QuickInsertSearchBarTextfield>(this)) |
| .CopyAddressTo(&textfield_) |
| .SetProperty(views::kElementIdentifierKey, |
| kQuickInsertSearchFieldTextfieldElementId) |
| .SetController(this) |
| .SetBackgroundColor(SK_ColorTRANSPARENT) |
| .SetFontList(TypographyProvider::Get()->ResolveTypographyToken( |
| TypographyToken::kCrosBody2)) |
| .SetProperty(views::kBoxLayoutFlexKey, |
| views::BoxLayoutFlexSpecification().WithWeight(1))) |
| .AddChild(views::Builder<views::ImageButton>( |
| std::make_unique<IconButton>( |
| // `base::Unretained` is safe here since the search |
| // field is owned by this class. |
| base::BindRepeating( |
| &QuickInsertSearchFieldView::ClearButtonPressed, |
| base::Unretained(this)), |
| IconButton::Type::kSmallFloating, |
| &(::features::IsRoundedIconsEnabled() |
| ? views::kCloseIcon |
| : views::kIcCloseOldIcon), |
| IDS_APP_LIST_CLEAR_SEARCHBOX)) |
| .CopyAddressTo(&clear_button_) |
| .SetProperty(views::kMarginsKey, kButtonHorizontalMargin) |
| .SetVisible(false)) |
| .BuildChildren(); |
| |
| StyleUtil::SetUpInkDropForButton(back_button_, gfx::Insets(), |
| /*highlight_on_hover=*/true, |
| /*highlight_on_focus=*/true); |
| StyleUtil::SetUpInkDropForButton(clear_button_, gfx::Insets(), |
| /*highlight_on_hover=*/true, |
| /*highlight_on_focus=*/true); |
| |
| UpdateTextfieldBorder(); |
| } |
| |
| QuickInsertSearchFieldView::~QuickInsertSearchFieldView() = default; |
| |
| void QuickInsertSearchFieldView::RequestFocus() { |
| textfield_->RequestFocus(); |
| } |
| |
| void QuickInsertSearchFieldView::AddedToWidget() { |
| GetFocusManager()->AddFocusChangeListener(this); |
| } |
| |
| void QuickInsertSearchFieldView::RemovedFromWidget() { |
| GetFocusManager()->RemoveFocusChangeListener(this); |
| } |
| |
| void QuickInsertSearchFieldView::OnPaint(gfx::Canvas* canvas) { |
| views::View::OnPaint(canvas); |
| |
| if (should_show_focus_indicator_) { |
| PaintQuickInsertFocusIndicator( |
| canvas, gfx::Point(0, kTextfieldFocusIndicatorMargins.top()), |
| height() - kTextfieldFocusIndicatorMargins.height(), |
| GetColorProvider()->GetColor(cros_tokens::kCrosSysFocusRing)); |
| } |
| } |
| |
| void QuickInsertSearchFieldView::ContentsChanged( |
| views::Textfield* sender, |
| const std::u16string& new_contents) { |
| ContentsChangedInternal(new_contents); |
| |
| search_callback_.Run(new_contents); |
| } |
| |
| void QuickInsertSearchFieldView::ContentsChangedInternal( |
| std::u16string_view new_contents) { |
| performance_metrics_->MarkContentsChanged(); |
| |
| // Show the clear button only when the query is not empty. |
| clear_button_->SetVisible(!new_contents.empty()); |
| UpdateTextfieldBorder(); |
| |
| ScheduleNotifyInitialActiveDescendantForA11y(); |
| } |
| |
| bool QuickInsertSearchFieldView::HandleKeyEvent(views::Textfield* sender, |
| const ui::KeyEvent& key_event) { |
| return key_event_handler_->HandleKeyEvent(key_event); |
| } |
| |
| void QuickInsertSearchFieldView::OnDidChangeFocus(View* focused_before, |
| View* focused_now) { |
| if (focused_now == textfield_) { |
| performance_metrics_->MarkInputFocus(); |
| } |
| |
| ScheduleNotifyInitialActiveDescendantForA11y(); |
| } |
| |
| std::u16string_view QuickInsertSearchFieldView::GetPlaceholderText() const { |
| return textfield_->GetPlaceholderText(); |
| } |
| |
| void QuickInsertSearchFieldView::SetPlaceholderText( |
| std::u16string_view new_placeholder_text) { |
| textfield_->SetPlaceholderText(new_placeholder_text); |
| textfield_->GetViewAccessibility().SetName( |
| std::u16string(new_placeholder_text)); |
| } |
| |
| void QuickInsertSearchFieldView::SetTextfieldActiveDescendant( |
| views::View* view) { |
| // If the initial active descendant has not been announced yet, then track |
| // this descendant so it can be announced when the timer fires. |
| if (!textfield_->HasFocus() || |
| notify_initial_active_descendant_timer_.IsRunning()) { |
| active_descendant_tracker_.SetView(view); |
| return; |
| } |
| |
| // The initial active descendant has been announced, so announce this |
| // descendant immediately. |
| if (view) { |
| textfield_->GetViewAccessibility().SetActiveDescendant(*view); |
| } else { |
| textfield_->GetViewAccessibility().ClearActiveDescendant(); |
| } |
| |
| active_descendant_tracker_.SetView(nullptr); |
| } |
| |
| std::u16string_view QuickInsertSearchFieldView::GetQueryText() const { |
| return textfield_->GetText(); |
| } |
| |
| void QuickInsertSearchFieldView::SetQueryText(std::u16string text) { |
| if (text != GetQueryText()) { |
| textfield_->SetText(std::move(text)); |
| ContentsChangedInternal(GetQueryText()); |
| } |
| } |
| |
| void QuickInsertSearchFieldView::SetBackButtonVisible(bool visible) { |
| back_button_->SetVisible(visible); |
| UpdateTextfieldBorder(); |
| } |
| |
| void QuickInsertSearchFieldView::SetShouldShowFocusIndicator( |
| bool should_show_focus_indicator) { |
| if (should_show_focus_indicator_ == should_show_focus_indicator) { |
| return; |
| } |
| should_show_focus_indicator_ = should_show_focus_indicator; |
| SchedulePaint(); |
| } |
| |
| views::View* QuickInsertSearchFieldView::GetViewLeftOf(views::View* view) { |
| if (!Contains(view)) { |
| return nullptr; |
| } |
| views::View* left_view = GetNextQuickInsertPseudoFocusableView( |
| view, QuickInsertPseudoFocusDirection::kBackward, /*should_loop=*/false); |
| return Contains(left_view) ? left_view : nullptr; |
| } |
| |
| views::View* QuickInsertSearchFieldView::GetViewRightOf(views::View* view) { |
| if (!Contains(view)) { |
| return nullptr; |
| } |
| views::View* right_view = GetNextQuickInsertPseudoFocusableView( |
| view, QuickInsertPseudoFocusDirection::kForward, /*should_loop=*/false); |
| return Contains(right_view) ? right_view : nullptr; |
| } |
| |
| bool QuickInsertSearchFieldView::LeftEventShouldMoveCursor( |
| views::View* pseudo_focused_view) { |
| if (pseudo_focused_view == textfield_ && |
| textfield_->GetCursorPosition() != GetQueryStartIndexForTraversal()) { |
| return true; |
| } |
| return GetViewLeftOf(pseudo_focused_view) == nullptr; |
| } |
| |
| bool QuickInsertSearchFieldView::RightEventShouldMoveCursor( |
| views::View* pseudo_focused_view) { |
| if (pseudo_focused_view == textfield_ && |
| textfield_->GetCursorPosition() != GetQueryEndIndexForTraversal()) { |
| return true; |
| } |
| return GetViewRightOf(pseudo_focused_view) == nullptr; |
| } |
| |
| void QuickInsertSearchFieldView::OnGainedPseudoFocusFromLeftEvent( |
| views::View* pseudo_focused_view) { |
| if (pseudo_focused_view == textfield_) { |
| textfield_->SetSelectedRange(gfx::Range(GetQueryEndIndexForTraversal())); |
| } |
| } |
| |
| void QuickInsertSearchFieldView::OnGainedPseudoFocusFromRightEvent( |
| views::View* pseudo_focused_view) { |
| if (pseudo_focused_view == textfield_) { |
| textfield_->SetSelectedRange(gfx::Range(GetQueryStartIndexForTraversal())); |
| } |
| } |
| |
| void QuickInsertSearchFieldView::ClearButtonPressed() { |
| textfield_->SetText(u""); |
| ContentsChanged(textfield_, u""); |
| } |
| |
| void QuickInsertSearchFieldView::UpdateTextfieldBorder() { |
| textfield_->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR( |
| 0, back_button_->GetVisible() ? 0 : kDefaultTextfieldHorizontalMargin, 0, |
| clear_button_->GetVisible() ? 0 : kDefaultTextfieldHorizontalMargin))); |
| } |
| |
| void QuickInsertSearchFieldView:: |
| ScheduleNotifyInitialActiveDescendantForA11y() { |
| // Delay the active descendant change so that: |
| // (1) There's no jarring transition of the screen reader's focus rectangle. |
| // (2) There's time for the screen reader to read out the change to input |
| // field contents. |
| notify_initial_active_descendant_timer_.Start( |
| FROM_HERE, kNotifyInitialActiveDescendantA11yDelay, |
| base::BindOnce( |
| &QuickInsertSearchFieldView::NotifyInitialActiveDescendantForA11y, |
| base::Unretained(this))); |
| } |
| |
| void QuickInsertSearchFieldView::NotifyInitialActiveDescendantForA11y() { |
| if (active_descendant_tracker_) { |
| SetTextfieldActiveDescendant(active_descendant_tracker_.view()); |
| } |
| } |
| |
| size_t QuickInsertSearchFieldView::GetQueryStartIndexForTraversal() { |
| // The query start index should actually be the same regardless of text |
| // direction, but we reverse it here since left / right key events are swapped |
| // when traversing Quick Insert UI in RTL. |
| return base::i18n::IsRTL() ? GetQueryText().length() : 0; |
| } |
| |
| size_t QuickInsertSearchFieldView::GetQueryEndIndexForTraversal() { |
| // The query end index should actually be the same regardless of text |
| // direction, but we reverse it here since left / right key events are swapped |
| // when traversing Quick Insert UI in RTL. |
| return base::i18n::IsRTL() ? 0 : GetQueryText().length(); |
| } |
| |
| BEGIN_METADATA(QuickInsertSearchFieldView) |
| END_METADATA |
| |
| } // namespace ash |