// Copyright 2019 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/app_list/views/assistant/assistant_dialog_plate.h"
#include <string_view>
#include <utility>
#include "ash/ash_element_identifiers.h"
#include "ash/assistant/model/assistant_interaction_model.h"
#include "ash/assistant/model/assistant_ui_model.h"
#include "ash/assistant/ui/assistant_ui_constants.h"
#include "ash/assistant/ui/assistant_view_delegate.h"
#include "ash/assistant/ui/assistant_view_ids.h"
#include "ash/assistant/ui/base/assistant_button.h"
#include "ash/assistant/ui/dialog_plate/mic_view.h"
#include "ash/assistant/util/animation_util.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_interaction_controller.h"
#include "ash/public/cpp/assistant/controller/assistant_ui_controller.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/functional/bind.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ui/vector_icons/vector_icons.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/callback_layer_animation_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animator.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
// Appearance.
constexpr int kIconSizeDip = 24;
constexpr int kButtonSizeDip = 32;
constexpr int kPaddingBottomDip = 8;
constexpr int kPaddingHorizontalDip = 16;
constexpr int kPaddingTopDip = 12;
// Animation.
constexpr base::TimeDelta kAnimationFadeInDelay = base::Milliseconds(83);
constexpr base::TimeDelta kAnimationFadeInDuration = base::Milliseconds(100);
constexpr base::TimeDelta kAnimationFadeOutDuration = base::Milliseconds(83);
constexpr base::TimeDelta kAnimationTransformInDuration =
base::Milliseconds(333);
constexpr int kAnimationTranslationDip = 30;
using keyboard::KeyboardUIController;
// Textfield used for inputting text based Assistant queries.
class AssistantTextfield : public views::Textfield {
METADATA_HEADER(AssistantTextfield, views::Textfield)
public:
AssistantTextfield() { SetID(AssistantViewID::kTextQueryField); }
};
BEGIN_METADATA(AssistantTextfield)
END_METADATA
void ShowKeyboardIfEnabled() {
auto* keyboard_controller = KeyboardUIController::Get();
if (keyboard_controller->IsEnabled())
keyboard_controller->ShowKeyboard(/*lock=*/false);
}
void HideKeyboardIfEnabled() {
auto* keyboard_controller = KeyboardUIController::Get();
if (keyboard_controller->IsEnabled())
keyboard_controller->HideKeyboardImplicitlyByUser();
}
} // namespace
// AssistantDialogPlate --------------------------------------------------------
AssistantDialogPlate::AssistantDialogPlate(AssistantViewDelegate* delegate)
: delegate_(delegate),
animation_observer_(std::make_unique<ui::CallbackLayerAnimationObserver>(
/*start_animation_callback=*/base::BindRepeating(
&AssistantDialogPlate::OnAnimationStarted,
base::Unretained(this)),
/*end_animation_callback=*/base::BindRepeating(
&AssistantDialogPlate::OnAnimationEnded,
base::Unretained(this)))),
query_history_iterator_(AssistantInteractionController::Get()
->GetModel()
->query_history()
.GetIterator()) {
SetID(AssistantViewID::kDialogPlate);
SetProperty(views::kElementIdentifierKey, kAssistantDialogPlateElementId);
InitLayout();
assistant_controller_observation_.Observe(AssistantController::Get());
AssistantInteractionController::Get()->GetModel()->AddObserver(this);
AssistantUiController::Get()->GetModel()->AddObserver(this);
}
AssistantDialogPlate::~AssistantDialogPlate() {
if (AssistantUiController::Get())
AssistantUiController::Get()->GetModel()->RemoveObserver(this);
if (AssistantInteractionController::Get())
AssistantInteractionController::Get()->GetModel()->RemoveObserver(this);
}
gfx::Size AssistantDialogPlate::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
return gfx::Size(
INT_MAX, GetLayoutManager()->GetPreferredHeightForWidth(this, INT_MAX));
}
void AssistantDialogPlate::OnButtonPressed(AssistantButtonId button_id) {
delegate_->OnDialogPlateButtonPressed(button_id);
textfield_->SetText(std::u16string());
}
bool AssistantDialogPlate::HandleKeyEvent(views::Textfield* textfield,
const ui::KeyEvent& key_event) {
if (key_event.type() != ui::EventType::kKeyPressed) {
return false;
}
switch (key_event.key_code()) {
case ui::KeyboardCode::VKEY_RETURN: {
// In tablet mode the virtual keyboard should not be sticky, so we hide it
// when committing a query.
if (delegate_->IsTabletMode())
HideKeyboardIfEnabled();
std::u16string_view trimmed_text = base::TrimWhitespace(
textfield_->GetText(), base::TrimPositions::TRIM_ALL);
// Only non-empty trimmed text is consider a valid contents commit.
// Anything else will simply result in the AssistantDialogPlate being
// cleared.
if (!trimmed_text.empty()) {
delegate_->OnDialogPlateContentsCommitted(
base::UTF16ToUTF8(trimmed_text));
}
textfield_->SetText(std::u16string());
return true;
}
case ui::KeyboardCode::VKEY_UP:
case ui::KeyboardCode::VKEY_DOWN: {
DCHECK(query_history_iterator_);
auto opt_query = key_event.key_code() == ui::KeyboardCode::VKEY_UP
? query_history_iterator_->Prev()
: query_history_iterator_->Next();
textfield_->SetText(base::UTF8ToUTF16(opt_query.value_or("")));
return true;
}
default:
return false;
}
}
void AssistantDialogPlate::OnAssistantControllerDestroying() {
AssistantUiController::Get()->GetModel()->RemoveObserver(this);
AssistantInteractionController::Get()->GetModel()->RemoveObserver(this);
DCHECK(assistant_controller_observation_.IsObservingSource(
AssistantController::Get()));
assistant_controller_observation_.Reset();
}
void AssistantDialogPlate::OnInputModalityChanged(
InputModality input_modality) {
using assistant::util::CreateLayerAnimationSequence;
using assistant::util::CreateOpacityElement;
using assistant::util::CreateTransformElement;
using assistant::util::StartLayerAnimationSequencesTogether;
keyboard_layout_container_->SetVisible(true);
voice_layout_container_->SetVisible(true);
switch (input_modality) {
case InputModality::kKeyboard: {
// Animate voice layout container opacity to 0%.
voice_layout_container_->layer()->GetAnimator()->StartAnimation(
CreateLayerAnimationSequence(
CreateOpacityElement(0.f, kAnimationFadeOutDuration,
gfx::Tween::Type::FAST_OUT_LINEAR_IN)));
// Apply a pre-transformation on the keyboard layout container so that it
// can be animated into place.
gfx::Transform transform;
transform.Translate(-kAnimationTranslationDip, 0);
keyboard_layout_container_->layer()->SetTransform(transform);
// Animate keyboard layout container.
StartLayerAnimationSequencesTogether(
keyboard_layout_container_->layer()->GetAnimator(),
{// Animate transformation.
CreateLayerAnimationSequence(CreateTransformElement(
gfx::Transform(), kAnimationTransformInDuration,
gfx::Tween::Type::FAST_OUT_SLOW_IN_2)),
// Animate opacity to 100% with delay.
CreateLayerAnimationSequence(
ui::LayerAnimationElement::CreatePauseElement(
ui::LayerAnimationElement::AnimatableProperty::OPACITY,
kAnimationFadeInDelay),
CreateOpacityElement(1.f, kAnimationFadeInDuration,
gfx::Tween::Type::FAST_OUT_LINEAR_IN))},
// Observe this animation.
animation_observer_.get());
// Activate the animation observer to receive start/end events.
animation_observer_->SetActive();
break;
}
case InputModality::kVoice: {
// Animate keyboard layout container opacity to 0%.
keyboard_layout_container_->layer()->GetAnimator()->StartAnimation(
CreateLayerAnimationSequence(
CreateOpacityElement(0.f, kAnimationFadeOutDuration,
gfx::Tween::Type::FAST_OUT_LINEAR_IN)));
// Apply a pre-transformation on the voice layout container so that it can
// be animated into place.
gfx::Transform transform;
transform.Translate(kAnimationTranslationDip, 0);
voice_layout_container_->layer()->SetTransform(transform);
// Animate voice layout container.
StartLayerAnimationSequencesTogether(
voice_layout_container_->layer()->GetAnimator(),
{// Animate transformation.
CreateLayerAnimationSequence(CreateTransformElement(
gfx::Transform(), kAnimationTransformInDuration,
gfx::Tween::Type::FAST_OUT_SLOW_IN_2)),
// Animate opacity to 100% with delay.
CreateLayerAnimationSequence(
ui::LayerAnimationElement::CreatePauseElement(
ui::LayerAnimationElement::AnimatableProperty::OPACITY,
kAnimationFadeInDelay),
CreateOpacityElement(1.f, kAnimationFadeInDuration,
gfx::Tween::Type::FAST_OUT_LINEAR_IN))},
// Observe this animation.
animation_observer_.get());
// Activate the animation observer to receive start/end events.
animation_observer_->SetActive();
break;
}
}
}
void AssistantDialogPlate::OnCommittedQueryChanged(
const AssistantQuery& committed_query) {
// Whenever a query is submitted we return the focus to the dialog plate.
RequestFocus();
DCHECK(query_history_iterator_);
query_history_iterator_->ResetToLast();
}
void AssistantDialogPlate::OnUiVisibilityChanged(
AssistantVisibility new_visibility,
AssistantVisibility old_visibility,
std::optional<AssistantEntryPoint> entry_point,
std::optional<AssistantExitPoint> exit_point) {
switch (new_visibility) {
case AssistantVisibility::kVisible:
UpdateModalityVisibility();
UpdateKeyboardVisibility();
break;
case AssistantVisibility::kClosed:
// When the Assistant UI is no longer visible we need to clear the dialog
// plate so that text does not persist across Assistant launches.
textfield_->SetText(std::u16string());
HideKeyboardIfEnabled();
break;
case AssistantVisibility::kClosing:
// No action.
break;
}
}
void AssistantDialogPlate::RequestFocus() {
views::View* view = FindFirstFocusableView();
if (view)
view->RequestFocus();
}
void AssistantDialogPlate::OnThemeChanged() {
views::View::OnThemeChanged();
textfield_->SetTextColor(
GetColorProvider()->GetColor(cros_tokens::kColorPrimary));
textfield_->set_placeholder_text_color(
GetColorProvider()->GetColor(cros_tokens::kColorSecondary));
}
views::View* AssistantDialogPlate::FindFirstFocusableView() {
// The first focusable view depends entirely on current input modality.
switch (input_modality()) {
case InputModality::kKeyboard:
return textfield_;
case InputModality::kVoice:
return animated_voice_input_toggle_;
}
}
void AssistantDialogPlate::InitLayout() {
views::BoxLayout* layout_manager =
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::TLBR(kPaddingTopDip, kPaddingHorizontalDip,
kPaddingBottomDip, kPaddingHorizontalDip)));
layout_manager->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
// Molecule icon.
molecule_icon_ = AddChildView(std::make_unique<views::ImageView>());
molecule_icon_->SetID(AssistantViewID::kModuleIcon);
molecule_icon_->SetPreferredSize(gfx::Size(kIconSizeDip, kIconSizeDip));
molecule_icon_->SetImage(gfx::CreateVectorIcon(
chromeos::kAssistantIcon, kIconSizeDip, gfx::kPlaceholderColor));
// Input modality layout container.
input_modality_layout_container_ =
AddChildView(std::make_unique<views::View>());
input_modality_layout_container_->SetLayoutManager(
std::make_unique<views::FillLayout>());
input_modality_layout_container_->SetPaintToLayer();
input_modality_layout_container_->layer()->SetFillsBoundsOpaquely(false);
input_modality_layout_container_->layer()->SetMasksToBounds(true);
layout_manager->SetFlexForView(input_modality_layout_container_, 1);
InitKeyboardLayoutContainer();
InitVoiceLayoutContainer();
// Set initial state.
UpdateModalityVisibility();
}
void AssistantDialogPlate::InitKeyboardLayoutContainer() {
auto keyboard_layout_container = std::make_unique<views::View>();
keyboard_layout_container->SetPaintToLayer();
keyboard_layout_container->layer()->SetFillsBoundsOpaquely(false);
keyboard_layout_container->layer()->SetOpacity(0.f);
constexpr int kLeftPaddingDip = 16;
views::BoxLayout* layout_manager =
keyboard_layout_container->SetLayoutManager(
std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal,
gfx::Insets::TLBR(0, kLeftPaddingDip, 0, 0)));
layout_manager->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
gfx::FontList font_list =
assistant::ui::GetDefaultFontList().DeriveWithSizeDelta(2);
// Textfield.
auto textfield = std::make_unique<AssistantTextfield>();
textfield->SetBackgroundColor(SK_ColorTRANSPARENT);
textfield->SetBorder(views::NullBorder());
textfield->set_controller(this);
textfield->SetFontList(font_list);
textfield->set_placeholder_font_list(font_list);
auto textfield_hint =
l10n_util::GetStringUTF16(IDS_ASH_ASSISTANT_DIALOG_PLATE_HINT);
textfield->SetPlaceholderText(textfield_hint);
textfield->GetViewAccessibility().SetName(textfield_hint);
textfield_ = keyboard_layout_container->AddChildView(std::move(textfield));
layout_manager->SetFlexForView(textfield_, 1);
// Voice input toggle.
AssistantButton::InitParams params;
params.size_in_dip = kButtonSizeDip;
params.icon_size_in_dip = kIconSizeDip;
params.accessible_name_id = IDS_ASH_ASSISTANT_DIALOG_PLATE_MIC_ACCNAME;
params.tooltip_id = IDS_ASH_ASSISTANT_DIALOG_PLATE_MIC_TOOLTIP;
std::unique_ptr<AssistantButton> voice_input_toggle = AssistantButton::Create(
this, kMicIcon, AssistantButtonId::kVoiceInputToggle, std::move(params));
voice_input_toggle->SetID(AssistantViewID::kVoiceInputToggle);
voice_input_toggle_ =
keyboard_layout_container->AddChildView(std::move(voice_input_toggle));
keyboard_layout_container_ = input_modality_layout_container_->AddChildView(
std::move(keyboard_layout_container));
}
void AssistantDialogPlate::InitVoiceLayoutContainer() {
auto voice_layout_container = std::make_unique<views::View>();
// TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
voice_layout_container->SetLayoutManagerUseConstrainedSpace(false);
voice_layout_container->SetPaintToLayer();
voice_layout_container->layer()->SetFillsBoundsOpaquely(false);
voice_layout_container->layer()->SetOpacity(0.f);
views::BoxLayout* layout_manager = voice_layout_container->SetLayoutManager(
std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kHorizontal));
layout_manager->set_cross_axis_alignment(
views::BoxLayout::CrossAxisAlignment::kCenter);
// Offset.
// To make the |animated_voice_input_toggle_| horizontally centered in the
// dialog plate we need to offset by the difference in width between the
// |molecule_icon_| and the |keyboard_input_toggle_|.
constexpr int difference =
/*keyboard_input_toggle_width=*/kButtonSizeDip -
/*molecule_icon_width=*/kIconSizeDip;
auto offset = std::make_unique<views::View>();
offset->SetPreferredSize(gfx::Size(difference, 1));
voice_layout_container->AddChildView(std::move(offset));
// Spacer.
auto spacer = std::make_unique<views::View>();
layout_manager->SetFlexForView(
voice_layout_container->AddChildView(std::move(spacer)), 1);
// Animated voice input toggle.
auto animated_voice_input_toggle =
std::make_unique<MicView>(this, AssistantButtonId::kVoiceInputToggle);
animated_voice_input_toggle->SetID(AssistantViewID::kMicView);
animated_voice_input_toggle->GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ASH_ASSISTANT_DIALOG_PLATE_MIC_ACCNAME));
animated_voice_input_toggle_ = voice_layout_container->AddChildView(
std::move(animated_voice_input_toggle));
// Spacer.
layout_manager->SetFlexForView(
voice_layout_container->AddChildView(std::make_unique<views::View>()), 1);
// Keyboard input toggle.
AssistantButton::InitParams params;
params.size_in_dip = kButtonSizeDip;
params.icon_size_in_dip = kIconSizeDip;
params.icon_color_type = cros_tokens::kColorPrimary;
params.accessible_name_id = IDS_ASH_ASSISTANT_DIALOG_PLATE_KEYBOARD_ACCNAME;
params.tooltip_id = IDS_ASH_ASSISTANT_DIALOG_PLATE_KEYBOARD_TOOLTIP;
keyboard_input_toggle_ =
voice_layout_container->AddChildView(AssistantButton::Create(
this, vector_icons::kKeyboardIcon,
AssistantButtonId::kKeyboardInputToggle,
std::move(params)));
keyboard_input_toggle_->SetID(AssistantViewID::kKeyboardInputToggle);
voice_layout_container_ = input_modality_layout_container_->AddChildView(
std::move(voice_layout_container));
}
void AssistantDialogPlate::UpdateModalityVisibility() {
// Hide everything.
keyboard_layout_container_->SetVisible(false);
voice_layout_container_->SetVisible(false);
// Reset opacity.
keyboard_layout_container_->layer()->SetOpacity(1);
voice_layout_container_->layer()->SetOpacity(1);
// Show currently selected content.
switch (input_modality()) {
case InputModality::kKeyboard:
keyboard_layout_container_->SetVisible(true);
break;
case InputModality::kVoice:
voice_layout_container_->SetVisible(true);
break;
}
}
void AssistantDialogPlate::UpdateKeyboardVisibility() {
if (!delegate_->IsTabletMode())
return;
bool should_show_keyboard = (input_modality() == InputModality::kKeyboard);
if (should_show_keyboard)
ShowKeyboardIfEnabled();
else
HideKeyboardIfEnabled();
}
void AssistantDialogPlate::OnAnimationStarted(
const ui::CallbackLayerAnimationObserver& observer) {
keyboard_layout_container_->SetCanProcessEventsWithinSubtree(false);
voice_layout_container_->SetCanProcessEventsWithinSubtree(false);
}
bool AssistantDialogPlate::OnAnimationEnded(
const ui::CallbackLayerAnimationObserver& observer) {
keyboard_layout_container_->SetCanProcessEventsWithinSubtree(true);
voice_layout_container_->SetCanProcessEventsWithinSubtree(true);
UpdateModalityVisibility();
RequestFocus();
UpdateKeyboardVisibility();
// We return false so that the animation observer will not destroy itself.
return false;
}
InputModality AssistantDialogPlate::input_modality() const {
return AssistantInteractionController::Get()->GetModel()->input_modality();
}
BEGIN_METADATA(AssistantDialogPlate)
END_METADATA
} // namespace ash