| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/ash/input_method/suggestion_window_view.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/i18n/number_formatting.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/ash/input_method/assistive_window_properties.h" |
| #include "chrome/browser/ui/ash/input_method/assistive_delegate.h" |
| #include "chrome/browser/ui/ash/input_method/border_factory.h" |
| #include "chrome/browser/ui/ash/input_method/colors.h" |
| #include "chrome/browser/ui/ash/input_method/completion_suggestion_view.h" |
| #include "chrome/browser/ui/ash/input_method/suggestion_details.h" |
| #include "chrome/grit/generated_resources.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/mojom/dialog_button.mojom.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/color/color_id.h" |
| #include "ui/color/color_provider.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/font.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/insets.h" |
| #include "ui/gfx/paint_vector_icon.h" |
| #include "ui/gfx/text_constants.h" |
| #include "ui/views/background.h" |
| #include "ui/views/border.h" |
| #include "ui/views/bubble/bubble_border.h" |
| #include "ui/views/bubble/bubble_frame_view.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/button/label_button.h" |
| #include "ui/views/controls/link.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/layout_provider.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/window_animations.h" |
| #include "ui/wm/core/window_properties.h" |
| |
| namespace ui { |
| namespace ime { |
| |
| namespace { |
| |
| bool ShouldHighlight(const views::Button& button) { |
| return button.GetState() == views::Button::STATE_HOVERED || |
| button.GetState() == views::Button::STATE_PRESSED; |
| } |
| |
| // TODO(b/1101669): Create abstract HighlightableButton for learn_more button, |
| // setting_link_, suggestion_view and undo_view. |
| void SetHighlighted(views::View& view, bool highlighted) { |
| if (!!view.background() != highlighted) { |
| view.SetBackground(highlighted |
| ? views::CreateRoundedRectBackground( |
| ResolveSemanticColor(kButtonHighlightColor), 2) |
| : nullptr); |
| } |
| } |
| |
| } // namespace |
| |
| // static |
| SuggestionWindowView* SuggestionWindowView::Create(gfx::NativeView parent, |
| AssistiveDelegate* delegate, |
| Orientation orientation) { |
| auto* const view = new SuggestionWindowView(parent, delegate, orientation); |
| views::Widget* const widget = |
| views::BubbleDialogDelegateView::CreateBubble(view); |
| wm::SetWindowVisibilityAnimationTransition(widget->GetNativeView(), |
| wm::ANIMATE_NONE); |
| return view; |
| } |
| |
| std::unique_ptr<views::FrameView> SuggestionWindowView::CreateFrameView( |
| views::Widget* widget) { |
| std::unique_ptr<views::FrameView> frame = |
| views::BubbleDialogDelegateView::CreateFrameView(widget); |
| static_cast<views::BubbleFrameView*>(frame.get()) |
| ->SetBubbleBorder(GetBorderForWindow(WindowBorderType::Suggestion)); |
| return frame; |
| } |
| |
| void SuggestionWindowView::Show(const SuggestionDetails& details) { |
| ResizeCandidateArea({}); |
| Reorient(Orientation::kVertical); |
| completion_view_->SetVisible(true); |
| completion_view_->SetView(details); |
| if (details.show_setting_link) { |
| completion_view_->SetMinWidth( |
| setting_link_ |
| ->GetPreferredSize(views::SizeBounds(setting_link_->width(), {})) |
| .width()); |
| } |
| |
| setting_link_->SetVisible(details.show_setting_link); |
| |
| MakeVisible(); |
| } |
| |
| void SuggestionWindowView::ShowMultipleCandidates( |
| const ash::input_method::AssistiveWindowProperties& properties, |
| Orientation orientation) { |
| const std::vector<std::u16string>& candidates = properties.candidates; |
| completion_view_->SetVisible(false); |
| Reorient(orientation, /*extra_padding_on_right=*/ |
| properties.type != |
| ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion); |
| ResizeCandidateArea( |
| candidates, |
| properties.type == ash::ime::AssistiveWindowType::kEmojiSuggestion); |
| learn_more_button_->SetVisible(properties.show_setting_link); |
| type_ = properties.type; |
| // Ensure colours are correct. |
| OnThemeChanged(); |
| MakeVisible(); |
| } |
| |
| void SuggestionWindowView::SetButtonHighlighted( |
| const AssistiveWindowButton& button, |
| bool highlighted) { |
| if (button.id == ButtonId::kSuggestion) { |
| if (completion_view_->GetVisible()) { |
| completion_view_->SetHighlighted(highlighted); |
| } else { |
| const views::View::Views& candidate_buttons = |
| multiple_candidate_area_->children(); |
| if (button.suggestion_index < candidate_buttons.size()) { |
| SetCandidateHighlighted(static_cast<IndexedSuggestionCandidateButton*>( |
| candidate_buttons[button.suggestion_index]), |
| highlighted); |
| } |
| } |
| } else if (button.id == ButtonId::kSmartInputsSettingLink) { |
| SetHighlighted(*setting_link_, highlighted); |
| } else if (button.id == ButtonId::kLearnMore) { |
| SetHighlighted(*learn_more_button_, highlighted); |
| } |
| } |
| |
| gfx::Rect SuggestionWindowView::GetBubbleBounds() { |
| // The bubble bounds must be shifted to align with the anchor if there is a |
| // completion view. |
| const gfx::Point anchor_origin = completion_view_->GetVisible() |
| ? completion_view_->GetAnchorOrigin() |
| : gfx::Point(0, 0); |
| return BubbleDialogDelegateView::GetBubbleBounds() - |
| anchor_origin.OffsetFromOrigin(); |
| } |
| |
| void SuggestionWindowView::OnThemeChanged() { |
| BubbleDialogDelegateView::OnThemeChanged(); |
| |
| const auto* const color_provider = GetColorProvider(); |
| if (type_ == ash::ime::AssistiveWindowType::kLongpressDiacriticsSuggestion) { |
| const int inset = views::LayoutProvider::Get()->GetDistanceMetric( |
| views::DistanceMetric::DISTANCE_VECTOR_ICON_PADDING); |
| learn_more_button_->SetBorder(views::CreatePaddedBorder( |
| views::CreateSolidSidedBorder( |
| gfx::Insets::TLBR(inset, 0, inset, inset), |
| color_provider->GetColor(ui::kColorButtonBackground)), |
| views::LayoutProvider::Get()->GetInsetsMetric( |
| views::INSETS_VECTOR_IMAGE_BUTTON))); |
| } else { |
| learn_more_button_->SetBorder(views::CreatePaddedBorder( |
| views::CreateSolidSidedBorder( |
| gfx::Insets::TLBR( |
| views::LayoutProvider::Get()->GetShadowElevationMetric( |
| views::Emphasis::kLow), |
| 0, 0, 0), |
| color_provider->GetColor(ui::kColorBubbleFooterBorder)), |
| views::LayoutProvider::Get()->GetInsetsMetric( |
| views::INSETS_VECTOR_IMAGE_BUTTON))); |
| } |
| // TODO(crbug.com/1099044): Update and use cros colors. |
| learn_more_button_->SetImageModel( |
| views::Button::ButtonState::STATE_NORMAL, |
| ui::ImageModel::FromVectorIcon(vector_icons::kSettingsOutlineIcon, |
| ui::kColorIconSecondary)); |
| } |
| |
| SuggestionWindowView::SuggestionWindowView(gfx::NativeView parent, |
| AssistiveDelegate* delegate, |
| Orientation orientation) |
| : delegate_(delegate) { |
| DCHECK(parent); |
| // AccessibleRole determines whether the content is announced on pop-up. |
| // Inner content should not be announced when the window appears since this |
| // is handled by AssistiveAccessibilityView to announce a custom string. |
| SetAccessibleWindowRole(ax::mojom::Role::kNone); |
| SetButtons(static_cast<int>(ui::mojom::DialogButton::kNone)); |
| SetCanActivate(false); |
| set_parent_window(parent); |
| set_margins(gfx::Insets()); |
| set_adjust_if_offscreen(true); |
| |
| completion_view_ = AddChildView( |
| std::make_unique<CompletionSuggestionView>(base::BindRepeating( |
| &AssistiveDelegate::AssistiveWindowButtonClicked, |
| base::Unretained(delegate_), |
| AssistiveWindowButton{.id = ui::ime::ButtonId::kSuggestion, |
| .suggestion_index = 0}))); |
| completion_view_->SetVisible(false); |
| multiple_candidate_area_ = AddChildView(std::make_unique<views::View>()); |
| multiple_candidate_area_->SetVisible(false); |
| Reorient(orientation); |
| |
| setting_link_ = AddChildView(std::make_unique<views::Link>( |
| l10n_util::GetStringUTF16(IDS_SUGGESTION_LEARN_MORE))); |
| setting_link_->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| // TODO(crbug.com/40138695): Implement proper UI layout using Insets constant. |
| constexpr auto insets = gfx::Insets::TLBR(0, kPadding, kPadding, kPadding); |
| setting_link_->SetBorder(views::CreateEmptyBorder(insets)); |
| constexpr int kSettingLinkFontSize = 11; |
| setting_link_->SetFontList(gfx::FontList({kFontStyle}, gfx::Font::ITALIC, |
| kSettingLinkFontSize, |
| gfx::Font::Weight::NORMAL)); |
| const auto on_setting_link_clicked = [](AssistiveDelegate* delegate) { |
| delegate->AssistiveWindowButtonClicked( |
| {.id = ButtonId::kSmartInputsSettingLink}); |
| }; |
| setting_link_->SetCallback( |
| base::BindRepeating(on_setting_link_clicked, delegate_)); |
| setting_link_->SetVisible(false); |
| |
| learn_more_button_ = |
| AddChildView(std::make_unique<views::ImageButton>(base::BindRepeating( |
| &SuggestionWindowView::LearnMoreClicked, base::Unretained(this)))); |
| learn_more_button_->SetImageHorizontalAlignment( |
| views::ImageButton::ALIGN_CENTER); |
| learn_more_button_->SetImageVerticalAlignment( |
| views::ImageButton::ALIGN_MIDDLE); |
| learn_more_button_->SetTooltipText(l10n_util::GetStringUTF16(IDS_LEARN_MORE)); |
| const auto update_button_highlight = [](views::Button* button) { |
| SetHighlighted(*button, ShouldHighlight(*button)); |
| }; |
| auto subscription = |
| learn_more_button_->AddStateChangedCallback(base::BindRepeating( |
| update_button_highlight, base::Unretained(learn_more_button_))); |
| subscriptions_.insert({learn_more_button_, std::move(subscription)}); |
| learn_more_button_->SetVisible(false); |
| } |
| |
| SuggestionWindowView::~SuggestionWindowView() = default; |
| |
| void SuggestionWindowView::LearnMoreClicked() { |
| delegate_->AssistiveWindowButtonClicked(AssistiveWindowButton{ |
| .id = ui::ime::ButtonId::kLearnMore, .window_type = type_}); |
| } |
| |
| raw_ptr<views::ImageButton> SuggestionWindowView::getLearnMoreButton() { |
| return learn_more_button_; |
| } |
| |
| void SuggestionWindowView::ResizeCandidateArea( |
| const std::vector<std::u16string>& new_candidates, |
| bool use_legacy_candidate) { |
| const views::View::Views& candidates = multiple_candidate_area_->children(); |
| while (candidates.size()) { |
| subscriptions_.erase( |
| multiple_candidate_area_->RemoveChildViewT(candidates.back()).get()); |
| } |
| |
| for (size_t index = 0; index < new_candidates.size(); ++index) { |
| auto* const candidate = multiple_candidate_area_->AddChildView( |
| std::make_unique<IndexedSuggestionCandidateButton>( |
| base::BindRepeating( |
| &AssistiveDelegate::AssistiveWindowButtonClicked, |
| base::Unretained(delegate_), |
| AssistiveWindowButton{.id = ui::ime::ButtonId::kSuggestion, |
| .suggestion_index = index}), |
| /* candidate_text=*/new_candidates[index], |
| // Label indexes start from "1", hence we increment index by one. |
| /* index_text=*/base::FormatNumber(index + 1), |
| use_legacy_candidate)); |
| // TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace. |
| candidate->SetLayoutManagerUseConstrainedSpace(false); |
| |
| auto subscription = candidate->AddStateChangedCallback(base::BindRepeating( |
| [](SuggestionWindowView* window, |
| IndexedSuggestionCandidateButton* button) { |
| window->SetCandidateHighlighted(button, ShouldHighlight(*button)); |
| }, |
| base::Unretained(this), base::Unretained(candidate))); |
| subscriptions_.insert({candidate, std::move(subscription)}); |
| } |
| } |
| |
| void SuggestionWindowView::Reorient(Orientation orientation, |
| bool extra_padding_on_right) { |
| views::BoxLayout::Orientation layout_orientation = |
| views::BoxLayout::Orientation::kVertical; |
| int multiple_candidate_area_padding = 0; |
| switch (orientation) { |
| case Orientation::kVertical: { |
| layout_orientation = views::BoxLayout::Orientation::kVertical; |
| // TODO(jhtin): remove this when emoji uses horizontal layout. |
| multiple_candidate_area_padding = 0; |
| break; |
| } |
| case Orientation::kHorizontal: { |
| layout_orientation = views::BoxLayout::Orientation::kHorizontal; |
| multiple_candidate_area_padding = 4; |
| break; |
| } |
| } |
| |
| SetLayoutManager(std::make_unique<views::BoxLayout>(layout_orientation)); |
| gfx::Insets inset(multiple_candidate_area_padding); |
| if (!extra_padding_on_right) { |
| inset.set_right(0); |
| } |
| multiple_candidate_area_->SetLayoutManager(std::make_unique<views::BoxLayout>( |
| layout_orientation, inset, |
| /* between_child_spacing=*/multiple_candidate_area_padding)); |
| } |
| |
| void SuggestionWindowView::MakeVisible() { |
| multiple_candidate_area_->SetVisible(true); |
| SizeToContents(); |
| // Docs can put the cursor offscreen - force it onscreen. |
| GetWidget()->SetBoundsConstrained(GetBubbleBounds()); |
| } |
| |
| void SuggestionWindowView::SetCandidateHighlighted( |
| IndexedSuggestionCandidateButton* view, |
| bool highlighted) { |
| // Clear all highlights if any exists. |
| for (views::View* candidate_button : multiple_candidate_area_->children()) { |
| static_cast<IndexedSuggestionCandidateButton*>(candidate_button) |
| ->SetHighlight(false); |
| } |
| |
| if (highlighted) { |
| view->SetHighlight(highlighted); |
| } |
| } |
| |
| BEGIN_METADATA(SuggestionWindowView) |
| END_METADATA |
| |
| } // namespace ime |
| } // namespace ui |