| // Copyright (c) 2012 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 "ui/app_list/views/search_box_view.h" |
| |
| #include <algorithm> |
| |
| #include "base/macros.h" |
| #include "base/memory/ptr_util.h" |
| #include "build/build_config.h" |
| #include "ui/app_list/app_list_constants.h" |
| #include "ui/app_list/app_list_model.h" |
| #include "ui/app_list/app_list_switches.h" |
| #include "ui/app_list/app_list_view_delegate.h" |
| #include "ui/app_list/resources/grit/app_list_resources.h" |
| #include "ui/app_list/search_box_model.h" |
| #include "ui/app_list/speech_ui_model.h" |
| #include "ui/app_list/views/contents_view.h" |
| #include "ui/app_list/views/search_box_view_delegate.h" |
| #include "ui/base/ime/text_input_flags.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/shadow_value.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/background.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/shadow_border.h" |
| |
| namespace app_list { |
| |
| namespace { |
| |
| const int kPadding = 16; |
| const int kInnerPadding = 24; |
| const int kPreferredWidth = 360; |
| const int kPreferredHeight = 48; |
| |
| const SkColor kHintTextColor = SkColorSetRGB(0xA0, 0xA0, 0xA0); |
| |
| const int kBackgroundBorderCornerRadius = 2; |
| |
| // A background that paints a solid white rounded rect with a thin grey border. |
| class SearchBoxBackground : public views::Background { |
| public: |
| SearchBoxBackground() {} |
| ~SearchBoxBackground() override {} |
| |
| private: |
| // views::Background overrides: |
| void Paint(gfx::Canvas* canvas, views::View* view) const override { |
| gfx::Rect bounds = view->GetContentsBounds(); |
| |
| cc::PaintFlags flags; |
| flags.setAntiAlias(true); |
| flags.setColor(kSearchBoxBackground); |
| canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, flags); |
| } |
| |
| DISALLOW_COPY_AND_ASSIGN(SearchBoxBackground); |
| }; |
| |
| } // namespace |
| |
| // To paint grey background on mic and back buttons |
| class SearchBoxImageButton : public views::ImageButton { |
| public: |
| explicit SearchBoxImageButton(views::ButtonListener* listener) |
| : ImageButton(listener), selected_(false) {} |
| ~SearchBoxImageButton() override {} |
| |
| bool selected() { return selected_; } |
| void SetSelected(bool selected) { |
| if (selected_ == selected) |
| return; |
| |
| selected_ = selected; |
| SchedulePaint(); |
| if (selected) |
| NotifyAccessibilityEvent(ui::AX_EVENT_SELECTION, true); |
| } |
| |
| bool OnKeyPressed(const ui::KeyEvent& event) override { |
| // Disable space key to press the button. The keyboard events received |
| // by this view are forwarded from a Textfield (SearchBoxView) and key |
| // released events are not forwarded. This leaves the button in pressed |
| // state. |
| if (event.key_code() == ui::VKEY_SPACE) |
| return false; |
| |
| return CustomButton::OnKeyPressed(event); |
| } |
| |
| private: |
| // views::View overrides: |
| void OnPaintBackground(gfx::Canvas* canvas) override { |
| if (state() == STATE_HOVERED || state() == STATE_PRESSED || selected_) |
| canvas->FillRect(gfx::Rect(size()), kSelectedColor); |
| } |
| |
| bool selected_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SearchBoxImageButton); |
| }; |
| |
| SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate, |
| AppListViewDelegate* view_delegate) |
| : delegate_(delegate), |
| view_delegate_(view_delegate), |
| model_(NULL), |
| content_container_(new views::View), |
| back_button_(NULL), |
| speech_button_(NULL), |
| search_box_(new views::Textfield), |
| contents_view_(NULL), |
| focused_view_(FOCUS_SEARCH_BOX) { |
| SetLayoutManager(new views::FillLayout); |
| AddChildView(content_container_); |
| |
| SetShadow(GetShadowForZHeight(2)); |
| back_button_ = new SearchBoxImageButton(this); |
| ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); |
| back_button_->SetImage(views::ImageButton::STATE_NORMAL, |
| rb.GetImageSkiaNamed(IDR_APP_LIST_FOLDER_BACK_NORMAL)); |
| back_button_->SetImageAlignment(views::ImageButton::ALIGN_CENTER, |
| views::ImageButton::ALIGN_MIDDLE); |
| SetBackButtonLabel(false); |
| content_container_->AddChildView(back_button_); |
| content_container_->set_background(new SearchBoxBackground()); |
| |
| views::BoxLayout* layout = |
| new views::BoxLayout(views::BoxLayout::kHorizontal, kPadding, 0, |
| kInnerPadding - views::Textfield::kTextPadding); |
| content_container_->SetLayoutManager(layout); |
| layout->set_cross_axis_alignment( |
| views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER); |
| layout->set_minimum_cross_axis_size(kPreferredHeight); |
| |
| search_box_->SetBorder(views::NullBorder()); |
| search_box_->SetTextColor(kSearchTextColor); |
| search_box_->SetBackgroundColor(kSearchBoxBackground); |
| search_box_->set_placeholder_text_color(kHintTextColor); |
| search_box_->set_controller(this); |
| search_box_->SetTextInputType(ui::TEXT_INPUT_TYPE_SEARCH); |
| search_box_->SetTextInputFlags(ui::TEXT_INPUT_FLAG_AUTOCORRECT_OFF); |
| content_container_->AddChildView(search_box_); |
| layout->SetFlexForView(search_box_, 1); |
| |
| view_delegate_->GetSpeechUI()->AddObserver(this); |
| ModelChanged(); |
| } |
| |
| SearchBoxView::~SearchBoxView() { |
| view_delegate_->GetSpeechUI()->RemoveObserver(this); |
| model_->search_box()->RemoveObserver(this); |
| } |
| |
| void SearchBoxView::ModelChanged() { |
| if (model_) |
| model_->search_box()->RemoveObserver(this); |
| |
| model_ = view_delegate_->GetModel(); |
| DCHECK(model_); |
| model_->search_box()->AddObserver(this); |
| SpeechRecognitionButtonPropChanged(); |
| HintTextChanged(); |
| } |
| |
| bool SearchBoxView::HasSearch() const { |
| return !search_box_->text().empty(); |
| } |
| |
| void SearchBoxView::ClearSearch() { |
| search_box_->SetText(base::string16()); |
| view_delegate_->AutoLaunchCanceled(); |
| // Updates model and fires query changed manually because SetText() above |
| // does not generate ContentsChanged() notification. |
| UpdateModel(); |
| NotifyQueryChanged(); |
| } |
| |
| void SearchBoxView::SetShadow(const gfx::ShadowValue& shadow) { |
| SetBorder(base::MakeUnique<views::ShadowBorder>(shadow)); |
| Layout(); |
| } |
| |
| gfx::Rect SearchBoxView::GetViewBoundsForSearchBoxContentsBounds( |
| const gfx::Rect& rect) const { |
| gfx::Rect view_bounds = rect; |
| view_bounds.Inset(-GetInsets()); |
| return view_bounds; |
| } |
| |
| views::ImageButton* SearchBoxView::back_button() { |
| return static_cast<views::ImageButton*>(back_button_); |
| } |
| |
| // Returns true if set internally, i.e. if focused_view_ != CONTENTS_VIEW. |
| // Note: because we always want to be able to type in the edit box, this is only |
| // a faux-focus so that buttons can respond to the ENTER key. |
| bool SearchBoxView::MoveTabFocus(bool move_backwards) { |
| if (back_button_) |
| back_button_->SetSelected(false); |
| if (speech_button_) |
| speech_button_->SetSelected(false); |
| |
| switch (focused_view_) { |
| case FOCUS_BACK_BUTTON: |
| focused_view_ = move_backwards ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX; |
| break; |
| case FOCUS_SEARCH_BOX: |
| if (move_backwards) { |
| focused_view_ = back_button_ && back_button_->visible() |
| ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX; |
| } else { |
| focused_view_ = speech_button_ && speech_button_->visible() |
| ? FOCUS_MIC_BUTTON : FOCUS_CONTENTS_VIEW; |
| } |
| break; |
| case FOCUS_MIC_BUTTON: |
| focused_view_ = move_backwards ? FOCUS_SEARCH_BOX : FOCUS_CONTENTS_VIEW; |
| break; |
| case FOCUS_CONTENTS_VIEW: |
| focused_view_ = move_backwards |
| ? (speech_button_ && speech_button_->visible() ? |
| FOCUS_MIC_BUTTON : FOCUS_SEARCH_BOX) |
| : FOCUS_CONTENTS_VIEW; |
| break; |
| default: |
| DCHECK(false); |
| } |
| |
| switch (focused_view_) { |
| case FOCUS_BACK_BUTTON: |
| if (back_button_) |
| back_button_->SetSelected(true); |
| break; |
| case FOCUS_SEARCH_BOX: |
| // Set the ChromeVox focus to the search box. However, DO NOT do this if |
| // we are in the search results state (i.e., if the search box has text in |
| // it), because the focus is about to be shifted to the first search |
| // result and we do not want to read out the name of the search box as |
| // well. |
| if (search_box_->text().empty()) |
| search_box_->NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true); |
| break; |
| case FOCUS_MIC_BUTTON: |
| if (speech_button_) |
| speech_button_->SetSelected(true); |
| break; |
| default: |
| break; |
| } |
| |
| if (focused_view_ < FOCUS_CONTENTS_VIEW) |
| delegate_->SetSearchResultSelection(focused_view_ == FOCUS_SEARCH_BOX); |
| |
| return (focused_view_ < FOCUS_CONTENTS_VIEW); |
| } |
| |
| void SearchBoxView::ResetTabFocus(bool on_contents) { |
| if (back_button_) |
| back_button_->SetSelected(false); |
| if (speech_button_) |
| speech_button_->SetSelected(false); |
| focused_view_ = on_contents ? FOCUS_CONTENTS_VIEW : FOCUS_SEARCH_BOX; |
| } |
| |
| void SearchBoxView::SetBackButtonLabel(bool folder) { |
| if (!back_button_) |
| return; |
| |
| base::string16 back_button_label(l10n_util::GetStringUTF16( |
| folder ? IDS_APP_LIST_FOLDER_CLOSE_FOLDER_ACCESSIBILE_NAME |
| : IDS_APP_LIST_BACK)); |
| back_button_->SetAccessibleName(back_button_label); |
| back_button_->SetTooltipText(back_button_label); |
| } |
| |
| gfx::Size SearchBoxView::GetPreferredSize() const { |
| return gfx::Size(kPreferredWidth, kPreferredHeight); |
| } |
| |
| bool SearchBoxView::OnMouseWheel(const ui::MouseWheelEvent& event) { |
| if (contents_view_) |
| return contents_view_->OnMouseWheel(event); |
| |
| return false; |
| } |
| |
| void SearchBoxView::OnEnabledChanged() { |
| search_box_->SetEnabled(enabled()); |
| if (speech_button_) |
| speech_button_->SetEnabled(enabled()); |
| } |
| |
| void SearchBoxView::UpdateModel() { |
| // Temporarily remove from observer to ignore notifications caused by us. |
| model_->search_box()->RemoveObserver(this); |
| model_->search_box()->SetText(search_box_->text()); |
| model_->search_box()->SetSelectionModel(search_box_->GetSelectionModel()); |
| model_->search_box()->AddObserver(this); |
| } |
| |
| void SearchBoxView::NotifyQueryChanged() { |
| DCHECK(delegate_); |
| delegate_->QueryChanged(this); |
| } |
| |
| void SearchBoxView::ContentsChanged(views::Textfield* sender, |
| const base::string16& new_contents) { |
| UpdateModel(); |
| view_delegate_->AutoLaunchCanceled(); |
| NotifyQueryChanged(); |
| } |
| |
| bool SearchBoxView::HandleKeyEvent(views::Textfield* sender, |
| const ui::KeyEvent& key_event) { |
| if (key_event.type() == ui::ET_KEY_PRESSED) { |
| if (key_event.key_code() == ui::VKEY_TAB && |
| focused_view_ != FOCUS_CONTENTS_VIEW && |
| MoveTabFocus(key_event.IsShiftDown())) |
| return true; |
| |
| if (focused_view_ == FOCUS_BACK_BUTTON && back_button_ && |
| back_button_->OnKeyPressed(key_event)) |
| return true; |
| |
| if (focused_view_ == FOCUS_MIC_BUTTON && speech_button_ && |
| speech_button_->OnKeyPressed(key_event)) |
| return true; |
| |
| const bool handled = contents_view_ && contents_view_->visible() && |
| contents_view_->OnKeyPressed(key_event); |
| |
| // Arrow keys may have selected an item. If they did, move focus off |
| // buttons. |
| // If they didn't, we still select the first search item, in case they're |
| // moving the caret through typed search text. The UP arrow never moves |
| // focus from text/buttons to app list/results, so ignore it. |
| if (focused_view_ < FOCUS_CONTENTS_VIEW && |
| (key_event.key_code() == ui::VKEY_LEFT || |
| key_event.key_code() == ui::VKEY_RIGHT || |
| key_event.key_code() == ui::VKEY_DOWN)) { |
| if (!handled) |
| delegate_->SetSearchResultSelection(true); |
| ResetTabFocus(handled); |
| } |
| return handled; |
| } |
| |
| if (key_event.type() == ui::ET_KEY_RELEASED) { |
| if (focused_view_ == FOCUS_BACK_BUTTON && back_button_ && |
| back_button_->OnKeyReleased(key_event)) |
| return true; |
| |
| if (focused_view_ == FOCUS_MIC_BUTTON && speech_button_ && |
| speech_button_->OnKeyReleased(key_event)) |
| return true; |
| |
| return contents_view_ && contents_view_->visible() && |
| contents_view_->OnKeyReleased(key_event); |
| } |
| |
| return false; |
| } |
| |
| void SearchBoxView::ButtonPressed(views::Button* sender, |
| const ui::Event& event) { |
| if (back_button_ && sender == back_button_) |
| delegate_->BackButtonPressed(); |
| else if (speech_button_ && sender == speech_button_) |
| view_delegate_->StartSpeechRecognition(); |
| else |
| NOTREACHED(); |
| } |
| |
| void SearchBoxView::SpeechRecognitionButtonPropChanged() { |
| const SearchBoxModel::SpeechButtonProperty* speech_button_prop = |
| model_->search_box()->speech_button(); |
| if (speech_button_prop) { |
| if (!speech_button_) { |
| speech_button_ = new SearchBoxImageButton(this); |
| content_container_->AddChildView(speech_button_); |
| } |
| |
| speech_button_->SetAccessibleName(speech_button_prop->accessible_name); |
| if (view_delegate_->GetSpeechUI()->state() == |
| SPEECH_RECOGNITION_HOTWORD_LISTENING) { |
| speech_button_->SetImage( |
| views::Button::STATE_NORMAL, &speech_button_prop->on_icon); |
| speech_button_->SetTooltipText(speech_button_prop->on_tooltip); |
| } else { |
| speech_button_->SetImage( |
| views::Button::STATE_NORMAL, &speech_button_prop->off_icon); |
| speech_button_->SetTooltipText(speech_button_prop->off_tooltip); |
| } |
| } else { |
| if (speech_button_) { |
| // Deleting a view will detach it from its parent. |
| delete speech_button_; |
| speech_button_ = NULL; |
| } |
| } |
| Layout(); |
| } |
| |
| void SearchBoxView::HintTextChanged() { |
| const app_list::SearchBoxModel* search_box = model_->search_box(); |
| search_box_->set_placeholder_text(search_box->hint_text()); |
| search_box_->SetAccessibleName(search_box->accessible_name()); |
| } |
| |
| void SearchBoxView::SelectionModelChanged() { |
| search_box_->SelectSelectionModel(model_->search_box()->selection_model()); |
| } |
| |
| void SearchBoxView::TextChanged() { |
| search_box_->SetText(model_->search_box()->text()); |
| NotifyQueryChanged(); |
| } |
| |
| void SearchBoxView::OnSpeechRecognitionStateChanged( |
| SpeechRecognitionState new_state) { |
| SpeechRecognitionButtonPropChanged(); |
| SchedulePaint(); |
| } |
| |
| } // namespace app_list |