chromium/chrome/browser/ui/ash/input_method/suggestion_window_view.cc

// 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::NonClientFrameView>
SuggestionWindowView::CreateNonClientFrameView(views::Widget* widget) {
  std::unique_ptr<views::NonClientFrameView> frame =
      views::BubbleDialogDelegateView::CreateNonClientFrameView(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