| // 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/dialog_plate.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/base/assistant_button.h" |
| #include "ash/assistant/ui/dialog_plate/dialog_plate.h" |
| #include "ash/assistant/ui/dialog_plate/mic_view.h" |
| #include "ash/assistant/ui/logo_view/logo_view.h" |
| #include "ash/assistant/util/animation_util.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 "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/textfield/textfield.h" |
| #include "ui/views/focus/focus_manager.h" |
| #include "ui/views/layout/box_layout.h" |
| #include "ui/views/layout/fill_layout.h" |
| |
| namespace app_list { |
| |
| 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; |
| |
| } // namespace |
| |
| // DialogPlate ----------------------------------------------------------------- |
| |
| DialogPlate::DialogPlate(ash::AssistantViewDelegate* delegate) |
| : delegate_(delegate), |
| animation_observer_(std::make_unique<ui::CallbackLayerAnimationObserver>( |
| /*start_animation_callback=*/base::BindRepeating( |
| &DialogPlate::OnAnimationStarted, |
| base::Unretained(this)), |
| /*end_animation_callback=*/base::BindRepeating( |
| &DialogPlate::OnAnimationEnded, |
| base::Unretained(this)))), |
| query_history_iterator_( |
| delegate_->GetInteractionModel()->query_history().GetIterator()) { |
| InitLayout(); |
| |
| // The AssistantViewDelegate should outlive DialogPlate. |
| delegate_->AddInteractionModelObserver(this); |
| } |
| |
| DialogPlate::~DialogPlate() { |
| delegate_->RemoveInteractionModelObserver(this); |
| } |
| |
| const char* DialogPlate::GetClassName() const { |
| return "DialogPlate"; |
| } |
| |
| gfx::Size DialogPlate::CalculatePreferredSize() const { |
| return gfx::Size(INT_MAX, GetHeightForWidth(INT_MAX)); |
| } |
| |
| void DialogPlate::ButtonPressed(views::Button* sender, const ui::Event& event) { |
| OnButtonPressed(static_cast<ash::AssistantButtonId>(sender->id())); |
| } |
| |
| bool DialogPlate::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()) |
| textfield_->GetFocusManager()->ClearFocus(); |
| |
| const base::StringPiece16& trimmed_text = base::TrimWhitespace( |
| textfield_->text(), base::TrimPositions::TRIM_ALL); |
| |
| // Only non-empty trimmed text is consider a valid contents commit. |
| // Anything else will simply result in the DialogPlate being cleared. |
| if (!trimmed_text.empty()) { |
| delegate_->OnDialogPlateContentsCommitted( |
| base::UTF16ToUTF8(trimmed_text)); |
| } |
| |
| textfield_->SetText(base::string16()); |
| |
| 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 DialogPlate::OnInputModalityChanged(ash::InputModality input_modality) { |
| using ash::assistant::util::CreateLayerAnimationSequence; |
| using ash::assistant::util::CreateOpacityElement; |
| using ash::assistant::util::CreateTransformElement; |
| using ash::assistant::util::StartLayerAnimationSequencesTogether; |
| |
| keyboard_layout_container_->SetVisible(true); |
| voice_layout_container_->SetVisible(true); |
| |
| switch (input_modality) { |
| case ash::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 ash::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; |
| } |
| case ash::InputModality::kStylus: |
| // No action necessary. |
| break; |
| } |
| } |
| |
| void DialogPlate::OnCommittedQueryChanged( |
| const ash::AssistantQuery& committed_query) { |
| DCHECK(query_history_iterator_); |
| query_history_iterator_->ResetToLast(); |
| } |
| |
| void DialogPlate::RequestFocus() { |
| SetFocus(delegate_->GetInteractionModel()->input_modality()); |
| } |
| |
| views::View* DialogPlate::FindFirstFocusableView() { |
| ash::InputModality input_modality = |
| delegate_->GetInteractionModel()->input_modality(); |
| |
| // The first focusable view depends entirely on current input modality. |
| switch (input_modality) { |
| case ash::InputModality::kKeyboard: |
| return textfield_; |
| case ash::InputModality::kVoice: |
| return animated_voice_input_toggle_; |
| case ash::InputModality::kStylus: |
| // Default views::FocusSearch behavior is acceptable. |
| return nullptr; |
| } |
| } |
| |
| void DialogPlate::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::CROSS_AXIS_ALIGNMENT_CENTER); |
| |
| // Molecule icon. |
| molecule_icon_ = ash::LogoView::Create(); |
| molecule_icon_->SetPreferredSize(gfx::Size(kIconSizeDip, kIconSizeDip)); |
| molecule_icon_->SetState(ash::LogoView::State::kMoleculeWavy, |
| /*animate=*/false); |
| AddChildView(molecule_icon_); |
| |
| // Input modality layout container. |
| input_modality_layout_container_ = new 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); |
| AddChildView(input_modality_layout_container_); |
| |
| layout_manager->SetFlexForView(input_modality_layout_container_, 1); |
| |
| InitKeyboardLayoutContainer(); |
| InitVoiceLayoutContainer(); |
| |
| // Artificially trigger event to set initial state. |
| OnInputModalityChanged(delegate_->GetInteractionModel()->input_modality()); |
| } |
| |
| void DialogPlate::InitKeyboardLayoutContainer() { |
| keyboard_layout_container_ = new 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::CROSS_AXIS_ALIGNMENT_CENTER); |
| |
| gfx::FontList font_list = |
| ash::assistant::ui::GetDefaultFontList().DeriveWithSizeDelta(2); |
| |
| // Textfield. |
| textfield_ = new views::Textfield(); |
| 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_->set_placeholder_text(textfield_hint); |
| textfield_->SetAccessibleName(textfield_hint); |
| textfield_->set_placeholder_text_color(ash::kTextColorSecondary); |
| textfield_->SetTextColor(ash::kTextColorPrimary); |
| keyboard_layout_container_->AddChildView(textfield_); |
| |
| layout_manager->SetFlexForView(textfield_, 1); |
| |
| // Voice input toggle. |
| voice_input_toggle_ = ash::AssistantButton::Create( |
| this, ash::kMicIcon, kButtonSizeDip, kIconSizeDip, |
| IDS_ASH_ASSISTANT_DIALOG_PLATE_MIC_ACCNAME, |
| ash::AssistantButtonId::kVoiceInputToggle); |
| keyboard_layout_container_->AddChildView(voice_input_toggle_); |
| |
| input_modality_layout_container_->AddChildView(keyboard_layout_container_); |
| } |
| |
| void DialogPlate::InitVoiceLayoutContainer() { |
| voice_layout_container_ = new 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::CROSS_AXIS_ALIGNMENT_CENTER); |
| |
| // 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; |
| views::View* offset = new views::View(); |
| offset->SetPreferredSize(gfx::Size(difference, 1)); |
| voice_layout_container_->AddChildView(offset); |
| |
| // Spacer. |
| views::View* spacer = new views::View(); |
| voice_layout_container_->AddChildView(spacer); |
| layout_manager->SetFlexForView(spacer, 1); |
| |
| // Animated voice input toggle. |
| animated_voice_input_toggle_ = new ash::MicView( |
| this, delegate_, ash::AssistantButtonId::kVoiceInputToggle); |
| animated_voice_input_toggle_->SetAccessibleName( |
| l10n_util::GetStringUTF16(IDS_ASH_ASSISTANT_DIALOG_PLATE_MIC_ACCNAME)); |
| voice_layout_container_->AddChildView(animated_voice_input_toggle_); |
| |
| // Spacer. |
| spacer = new views::View(); |
| voice_layout_container_->AddChildView(spacer); |
| layout_manager->SetFlexForView(spacer, 1); |
| |
| // Keyboard input toggle. |
| keyboard_input_toggle_ = ash::AssistantButton::Create( |
| this, ash::kKeyboardIcon, kButtonSizeDip, kIconSizeDip, |
| IDS_ASH_ASSISTANT_DIALOG_PLATE_KEYBOARD_ACCNAME, |
| ash::AssistantButtonId::kKeyboardInputToggle); |
| voice_layout_container_->AddChildView(keyboard_input_toggle_); |
| |
| input_modality_layout_container_->AddChildView(voice_layout_container_); |
| } |
| |
| void DialogPlate::OnButtonPressed(ash::AssistantButtonId id) { |
| delegate_->OnDialogPlateButtonPressed(id); |
| textfield_->SetText(base::string16()); |
| } |
| |
| void DialogPlate::OnAnimationStarted( |
| const ui::CallbackLayerAnimationObserver& observer) { |
| keyboard_layout_container_->set_can_process_events_within_subtree(false); |
| voice_layout_container_->set_can_process_events_within_subtree(false); |
| } |
| |
| bool DialogPlate::OnAnimationEnded( |
| const ui::CallbackLayerAnimationObserver& observer) { |
| ash::InputModality input_modality = |
| delegate_->GetInteractionModel()->input_modality(); |
| |
| switch (input_modality) { |
| case ash::InputModality::kKeyboard: |
| keyboard_layout_container_->set_can_process_events_within_subtree(true); |
| voice_layout_container_->SetVisible(false); |
| break; |
| case ash::InputModality::kVoice: |
| voice_layout_container_->set_can_process_events_within_subtree(true); |
| keyboard_layout_container_->SetVisible(false); |
| break; |
| case ash::InputModality::kStylus: |
| // No action necessary. |
| break; |
| } |
| |
| SetFocus(input_modality); |
| |
| // We return false so that the animation observer will not destroy itself. |
| return false; |
| } |
| |
| void DialogPlate::SetFocus(ash::InputModality input_modality) { |
| switch (input_modality) { |
| case ash::InputModality::kKeyboard: |
| textfield_->RequestFocus(); |
| break; |
| case ash::InputModality::kVoice: |
| animated_voice_input_toggle_->RequestFocus(); |
| break; |
| case ash::InputModality::kStylus: |
| break; |
| } |
| } |
| |
| } // namespace app_list |