| // Copyright 2019 The Chromium Authors. All rights reserved. |
| // 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 <utility> |
| |
| #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/bind.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chromeos/ui/vector_icons/vector_icons.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/compositor/callback_layer_animation_observer.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/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" |
| |
| 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::TimeDelta::FromMilliseconds(83); |
| constexpr base::TimeDelta kAnimationFadeInDuration = |
| base::TimeDelta::FromMilliseconds(100); |
| constexpr base::TimeDelta kAnimationFadeOutDuration = |
| base::TimeDelta::FromMilliseconds(83); |
| constexpr base::TimeDelta kAnimationTransformInDuration = |
| base::TimeDelta::FromMilliseconds(333); |
| constexpr int kAnimationTranslationDip = 30; |
| |
| using keyboard::KeyboardUIController; |
| |
| // Textfield used for inputting text based Assistant queries. |
| class AssistantTextfield : public views::Textfield { |
| public: |
| AssistantTextfield() : views::Textfield() { |
| SetID(AssistantViewID::kTextQueryField); |
| } |
| |
| // views::Textfield overrides: |
| const char* GetClassName() const override { return "AssistantTextfield"; } |
| }; |
| |
| 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); |
| 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); |
| } |
| |
| const char* AssistantDialogPlate::GetClassName() const { |
| return "AssistantDialogPlate"; |
| } |
| |
| gfx::Size AssistantDialogPlate::CalculatePreferredSize() const { |
| return gfx::Size(INT_MAX, GetHeightForWidth(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::ET_KEY_PRESSED) |
| 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(); |
| |
| const base::StringPiece16& 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, |
| base::Optional<AssistantEntryPoint> entry_point, |
| base::Optional<AssistantExitPoint> exit_point) { |
| if (new_visibility == AssistantVisibility::kVisible) { |
| UpdateModalityVisibility(); |
| UpdateKeyboardVisibility(); |
| } else { |
| // 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(); |
| } |
| } |
| |
| void AssistantDialogPlate::RequestFocus() { |
| views::View* view = FindFirstFocusableView(); |
| if (view) |
| view->RequestFocus(); |
| } |
| |
| 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 voice_layout_container_; |
| } |
| } |
| |
| void AssistantDialogPlate::InitLayout() { |
| views::BoxLayout* layout_manager = |
| SetLayoutManager(std::make_unique<views::BoxLayout>( |
| views::BoxLayout::Orientation::kHorizontal, |
| gfx::Insets(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(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->SetAccessibleName(textfield_hint); |
| textfield->set_placeholder_text_color(kTextColorSecondary); |
| textfield->SetTextColor(kTextColorPrimary); |
| 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>(); |
| 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->SetAccessibleName( |
| 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.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, 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(); |
| } |
| |
| } // namespace ash |