Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / app_list / views / search_box_view.cc
blob191dd434e6e8978705b046a7589dc8c6cb779adf
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/app_list/views/search_box_view.h"
7 #include <algorithm>
9 #include "ui/app_list/app_list_constants.h"
10 #include "ui/app_list/app_list_model.h"
11 #include "ui/app_list/app_list_switches.h"
12 #include "ui/app_list/app_list_view_delegate.h"
13 #include "ui/app_list/resources/grit/app_list_resources.h"
14 #include "ui/app_list/search_box_model.h"
15 #include "ui/app_list/speech_ui_model.h"
16 #include "ui/app_list/views/app_list_menu_views.h"
17 #include "ui/app_list/views/contents_view.h"
18 #include "ui/app_list/views/search_box_view_delegate.h"
19 #include "ui/base/ime/text_input_flags.h"
20 #include "ui/base/l10n/l10n_util.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/events/event.h"
23 #include "ui/gfx/canvas.h"
24 #include "ui/gfx/shadow_value.h"
25 #include "ui/strings/grit/ui_strings.h"
26 #include "ui/views/border.h"
27 #include "ui/views/controls/button/image_button.h"
28 #include "ui/views/controls/button/menu_button.h"
29 #include "ui/views/controls/image_view.h"
30 #include "ui/views/controls/textfield/textfield.h"
31 #include "ui/views/layout/box_layout.h"
32 #include "ui/views/layout/fill_layout.h"
33 #include "ui/views/shadow_border.h"
35 namespace app_list {
37 namespace {
39 const int kPadding = 16;
40 const int kInnerPadding = 24;
41 const int kPreferredWidth = 360;
42 const int kPreferredHeight = 48;
44 const SkColor kHintTextColor = SkColorSetRGB(0xA0, 0xA0, 0xA0);
46 // Menu offset relative to the bottom-right corner of the menu button.
47 const int kMenuYOffsetFromButton = -4;
48 const int kMenuXOffsetFromButton = -7;
50 const int kBackgroundBorderCornerRadius = 2;
52 // A background that paints a solid white rounded rect with a thin grey border.
53 class ExperimentalSearchBoxBackground : public views::Background {
54 public:
55 ExperimentalSearchBoxBackground() {}
56 ~ExperimentalSearchBoxBackground() override {}
58 private:
59 // views::Background overrides:
60 void Paint(gfx::Canvas* canvas, views::View* view) const override {
61 gfx::Rect bounds = view->GetContentsBounds();
63 SkPaint paint;
64 paint.setFlags(SkPaint::kAntiAlias_Flag);
65 paint.setColor(kSearchBoxBackground);
66 canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, paint);
69 DISALLOW_COPY_AND_ASSIGN(ExperimentalSearchBoxBackground);
72 } // namespace
74 // To paint grey background on mic and back buttons
75 class SearchBoxImageButton : public views::ImageButton {
76 public:
77 explicit SearchBoxImageButton(views::ButtonListener* listener)
78 : ImageButton(listener), selected_(false) {}
79 ~SearchBoxImageButton() override {}
81 bool selected() { return selected_; }
82 void SetSelected(bool selected) {
83 if (selected_ == selected)
84 return;
86 selected_ = selected;
87 SchedulePaint();
88 if (selected)
89 NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true);
92 bool OnKeyPressed(const ui::KeyEvent& event) override {
93 // Disable space key to press the button. The keyboard events received
94 // by this view are forwarded from a Textfield (SearchBoxView) and key
95 // released events are not forwarded. This leaves the button in pressed
96 // state.
97 if (event.key_code() == ui::VKEY_SPACE)
98 return false;
100 return CustomButton::OnKeyPressed(event);
103 private:
104 // views::View overrides:
105 void OnPaintBackground(gfx::Canvas* canvas) override {
106 if (state_ == STATE_HOVERED || state_ == STATE_PRESSED || selected_)
107 canvas->FillRect(gfx::Rect(size()), kSelectedColor);
110 bool selected_;
112 DISALLOW_COPY_AND_ASSIGN(SearchBoxImageButton);
115 SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate,
116 AppListViewDelegate* view_delegate)
117 : delegate_(delegate),
118 view_delegate_(view_delegate),
119 model_(NULL),
120 content_container_(new views::View),
121 icon_view_(NULL),
122 back_button_(NULL),
123 speech_button_(NULL),
124 menu_button_(NULL),
125 search_box_(new views::Textfield),
126 contents_view_(NULL),
127 focused_view_(FOCUS_SEARCH_BOX) {
128 SetLayoutManager(new views::FillLayout);
129 AddChildView(content_container_);
131 if (switches::IsExperimentalAppListEnabled()) {
132 SetShadow(GetShadowForZHeight(2));
133 back_button_ = new SearchBoxImageButton(this);
134 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
135 back_button_->SetImage(
136 views::ImageButton::STATE_NORMAL,
137 rb.GetImageSkiaNamed(IDR_APP_LIST_FOLDER_BACK_NORMAL));
138 back_button_->SetImageAlignment(views::ImageButton::ALIGN_CENTER,
139 views::ImageButton::ALIGN_MIDDLE);
140 SetBackButtonLabel(false);
141 content_container_->AddChildView(back_button_);
143 content_container_->set_background(new ExperimentalSearchBoxBackground());
144 } else {
145 set_background(
146 views::Background::CreateSolidBackground(kSearchBoxBackground));
147 SetBorder(
148 views::Border::CreateSolidSidedBorder(0, 0, 1, 0, kTopSeparatorColor));
149 icon_view_ = new views::ImageView;
150 content_container_->AddChildView(icon_view_);
153 views::BoxLayout* layout =
154 new views::BoxLayout(views::BoxLayout::kHorizontal, kPadding, 0,
155 kInnerPadding - views::Textfield::kTextPadding);
156 content_container_->SetLayoutManager(layout);
157 layout->set_cross_axis_alignment(
158 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
159 layout->set_minimum_cross_axis_size(kPreferredHeight);
161 search_box_->SetBorder(views::Border::NullBorder());
162 search_box_->SetTextColor(kSearchTextColor);
163 search_box_->SetBackgroundColor(kSearchBoxBackground);
164 search_box_->set_placeholder_text_color(kHintTextColor);
165 search_box_->set_controller(this);
166 search_box_->SetTextInputType(ui::TEXT_INPUT_TYPE_SEARCH);
167 search_box_->SetTextInputFlags(ui::TEXT_INPUT_FLAG_AUTOCORRECT_OFF);
168 content_container_->AddChildView(search_box_);
169 layout->SetFlexForView(search_box_, 1);
171 #if !defined(OS_CHROMEOS)
172 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
173 menu_button_ = new views::MenuButton(NULL, base::string16(), this, false);
174 menu_button_->SetBorder(views::Border::NullBorder());
175 menu_button_->SetImage(views::Button::STATE_NORMAL,
176 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_NORMAL));
177 menu_button_->SetImage(views::Button::STATE_HOVERED,
178 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_HOVER));
179 menu_button_->SetImage(views::Button::STATE_PRESSED,
180 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_PRESSED));
181 content_container_->AddChildView(menu_button_);
182 #endif
184 view_delegate_->GetSpeechUI()->AddObserver(this);
185 ModelChanged();
188 SearchBoxView::~SearchBoxView() {
189 view_delegate_->GetSpeechUI()->RemoveObserver(this);
190 model_->search_box()->RemoveObserver(this);
193 void SearchBoxView::ModelChanged() {
194 if (model_)
195 model_->search_box()->RemoveObserver(this);
197 model_ = view_delegate_->GetModel();
198 DCHECK(model_);
199 model_->search_box()->AddObserver(this);
200 IconChanged();
201 SpeechRecognitionButtonPropChanged();
202 HintTextChanged();
205 bool SearchBoxView::HasSearch() const {
206 return !search_box_->text().empty();
209 void SearchBoxView::ClearSearch() {
210 search_box_->SetText(base::string16());
211 view_delegate_->AutoLaunchCanceled();
212 // Updates model and fires query changed manually because SetText() above
213 // does not generate ContentsChanged() notification.
214 UpdateModel();
215 NotifyQueryChanged();
218 void SearchBoxView::InvalidateMenu() {
219 menu_.reset();
222 void SearchBoxView::SetShadow(const gfx::ShadowValue& shadow) {
223 SetBorder(make_scoped_ptr(new views::ShadowBorder(shadow)));
224 Layout();
227 gfx::Rect SearchBoxView::GetViewBoundsForSearchBoxContentsBounds(
228 const gfx::Rect& rect) const {
229 gfx::Rect view_bounds = rect;
230 view_bounds.Inset(-GetInsets());
231 return view_bounds;
234 views::ImageButton* SearchBoxView::back_button() {
235 return static_cast<views::ImageButton*>(back_button_);
238 // Returns true if set internally, i.e. if focused_view_ != CONTENTS_VIEW.
239 // Note: because we always want to be able to type in the edit box, this is only
240 // a faux-focus so that buttons can respond to the ENTER key.
241 bool SearchBoxView::MoveTabFocus(bool move_backwards) {
242 if (back_button_)
243 back_button_->SetSelected(false);
244 if (speech_button_)
245 speech_button_->SetSelected(false);
247 switch (focused_view_) {
248 case FOCUS_BACK_BUTTON:
249 focused_view_ = move_backwards ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX;
250 break;
251 case FOCUS_SEARCH_BOX:
252 if (move_backwards) {
253 focused_view_ = back_button_ && back_button_->visible()
254 ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX;
255 } else {
256 focused_view_ = speech_button_ && speech_button_->visible()
257 ? FOCUS_MIC_BUTTON : FOCUS_CONTENTS_VIEW;
259 break;
260 case FOCUS_MIC_BUTTON:
261 focused_view_ = move_backwards ? FOCUS_SEARCH_BOX : FOCUS_CONTENTS_VIEW;
262 break;
263 case FOCUS_CONTENTS_VIEW:
264 focused_view_ = move_backwards
265 ? (speech_button_ && speech_button_->visible() ?
266 FOCUS_MIC_BUTTON : FOCUS_SEARCH_BOX)
267 : FOCUS_CONTENTS_VIEW;
268 break;
269 default:
270 DCHECK(false);
273 switch (focused_view_) {
274 case FOCUS_BACK_BUTTON:
275 if (back_button_)
276 back_button_->SetSelected(true);
277 break;
278 case FOCUS_SEARCH_BOX:
279 // Set the ChromeVox focus to the search box. However, DO NOT do this if
280 // we are in the search results state (i.e., if the search box has text in
281 // it), because the focus is about to be shifted to the first search
282 // result and we do not want to read out the name of the search box as
283 // well.
284 if (search_box_->text().empty())
285 search_box_->NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true);
286 break;
287 case FOCUS_MIC_BUTTON:
288 if (speech_button_)
289 speech_button_->SetSelected(true);
290 break;
291 default:
292 break;
295 if (focused_view_ < FOCUS_CONTENTS_VIEW)
296 delegate_->SetSearchResultSelection(focused_view_ == FOCUS_SEARCH_BOX);
298 return (focused_view_ < FOCUS_CONTENTS_VIEW);
301 void SearchBoxView::ResetTabFocus(bool on_contents) {
302 if (back_button_)
303 back_button_->SetSelected(false);
304 if (speech_button_)
305 speech_button_->SetSelected(false);
306 focused_view_ = on_contents ? FOCUS_CONTENTS_VIEW : FOCUS_SEARCH_BOX;
309 void SearchBoxView::SetBackButtonLabel(bool folder) {
310 if (!back_button_)
311 return;
313 base::string16 back_button_label(l10n_util::GetStringUTF16(
314 folder ? IDS_APP_LIST_FOLDER_CLOSE_FOLDER_ACCESSIBILE_NAME
315 : IDS_APP_LIST_BACK));
316 back_button_->SetAccessibleName(back_button_label);
317 back_button_->SetTooltipText(back_button_label);
320 gfx::Size SearchBoxView::GetPreferredSize() const {
321 return gfx::Size(kPreferredWidth, kPreferredHeight);
324 bool SearchBoxView::OnMouseWheel(const ui::MouseWheelEvent& event) {
325 if (contents_view_)
326 return contents_view_->OnMouseWheel(event);
328 return false;
331 void SearchBoxView::OnEnabledChanged() {
332 search_box_->SetEnabled(enabled());
333 if (menu_button_)
334 menu_button_->SetEnabled(enabled());
335 if (speech_button_)
336 speech_button_->SetEnabled(enabled());
339 void SearchBoxView::UpdateModel() {
340 // Temporarily remove from observer to ignore notifications caused by us.
341 model_->search_box()->RemoveObserver(this);
342 model_->search_box()->SetText(search_box_->text());
343 model_->search_box()->SetSelectionModel(search_box_->GetSelectionModel());
344 model_->search_box()->AddObserver(this);
347 void SearchBoxView::NotifyQueryChanged() {
348 DCHECK(delegate_);
349 delegate_->QueryChanged(this);
352 void SearchBoxView::ContentsChanged(views::Textfield* sender,
353 const base::string16& new_contents) {
354 UpdateModel();
355 view_delegate_->AutoLaunchCanceled();
356 NotifyQueryChanged();
359 bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
360 const ui::KeyEvent& key_event) {
361 bool handled = false;
362 if (key_event.key_code() == ui::VKEY_TAB) {
363 if (focused_view_ != FOCUS_CONTENTS_VIEW &&
364 MoveTabFocus(key_event.IsShiftDown()))
365 return true;
368 if (focused_view_ == FOCUS_BACK_BUTTON && back_button_ &&
369 back_button_->OnKeyPressed(key_event))
370 return true;
372 if (focused_view_ == FOCUS_MIC_BUTTON && speech_button_ &&
373 speech_button_->OnKeyPressed(key_event))
374 return true;
376 if (contents_view_ && contents_view_->visible())
377 handled = contents_view_->OnKeyPressed(key_event);
379 // Arrow keys may have selected an item. If they did, move focus off buttons.
380 // If they didn't, we still select the first search item, in case they're
381 // moving the caret through typed search text. The UP arrow never moves
382 // focus from text/buttons to app list/results, so ignore it.
383 if (focused_view_ < FOCUS_CONTENTS_VIEW &&
384 (key_event.key_code() == ui::VKEY_LEFT ||
385 key_event.key_code() == ui::VKEY_RIGHT ||
386 key_event.key_code() == ui::VKEY_DOWN)) {
387 if (!handled)
388 delegate_->SetSearchResultSelection(true);
389 ResetTabFocus(handled);
392 return handled;
395 void SearchBoxView::ButtonPressed(views::Button* sender,
396 const ui::Event& event) {
397 if (back_button_ && sender == back_button_)
398 delegate_->BackButtonPressed();
399 else if (speech_button_ && sender == speech_button_)
400 view_delegate_->ToggleSpeechRecognition();
401 else
402 NOTREACHED();
405 void SearchBoxView::OnMenuButtonClicked(View* source, const gfx::Point& point) {
406 if (!menu_)
407 menu_.reset(new AppListMenuViews(view_delegate_));
409 const gfx::Point menu_location =
410 menu_button_->GetBoundsInScreen().bottom_right() +
411 gfx::Vector2d(kMenuXOffsetFromButton, kMenuYOffsetFromButton);
412 menu_->RunMenuAt(menu_button_, menu_location);
415 void SearchBoxView::IconChanged() {
416 if (icon_view_)
417 icon_view_->SetImage(model_->search_box()->icon());
420 void SearchBoxView::SpeechRecognitionButtonPropChanged() {
421 const SearchBoxModel::SpeechButtonProperty* speech_button_prop =
422 model_->search_box()->speech_button();
423 if (speech_button_prop) {
424 if (!speech_button_) {
425 speech_button_ = new SearchBoxImageButton(this);
426 content_container_->AddChildView(speech_button_);
429 speech_button_->SetAccessibleName(speech_button_prop->accessible_name);
430 if (view_delegate_->GetSpeechUI()->state() ==
431 SPEECH_RECOGNITION_HOTWORD_LISTENING) {
432 speech_button_->SetImage(
433 views::Button::STATE_NORMAL, &speech_button_prop->on_icon);
434 speech_button_->SetTooltipText(speech_button_prop->on_tooltip);
435 } else {
436 speech_button_->SetImage(
437 views::Button::STATE_NORMAL, &speech_button_prop->off_icon);
438 speech_button_->SetTooltipText(speech_button_prop->off_tooltip);
440 } else {
441 if (speech_button_) {
442 // Deleting a view will detach it from its parent.
443 delete speech_button_;
444 speech_button_ = NULL;
447 Layout();
450 void SearchBoxView::HintTextChanged() {
451 const app_list::SearchBoxModel* search_box = model_->search_box();
452 search_box_->set_placeholder_text(search_box->hint_text());
453 search_box_->SetAccessibleName(search_box->accessible_name());
456 void SearchBoxView::SelectionModelChanged() {
457 search_box_->SelectSelectionModel(model_->search_box()->selection_model());
460 void SearchBoxView::TextChanged() {
461 search_box_->SetText(model_->search_box()->text());
462 NotifyQueryChanged();
465 void SearchBoxView::OnSpeechRecognitionStateChanged(
466 SpeechRecognitionState new_state) {
467 SpeechRecognitionButtonPropChanged();
468 SchedulePaint();
471 } // namespace app_list