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"
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"
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
{
55 ExperimentalSearchBoxBackground() {}
56 ~ExperimentalSearchBoxBackground() override
{}
59 // views::Background overrides:
60 void Paint(gfx::Canvas
* canvas
, views::View
* view
) const override
{
61 gfx::Rect bounds
= view
->GetContentsBounds();
64 paint
.setFlags(SkPaint::kAntiAlias_Flag
);
65 paint
.setColor(kSearchBoxBackground
);
66 canvas
->DrawRoundRect(bounds
, kBackgroundBorderCornerRadius
, paint
);
69 DISALLOW_COPY_AND_ASSIGN(ExperimentalSearchBoxBackground
);
74 // To paint grey background on mic and back buttons
75 class SearchBoxImageButton
: public views::ImageButton
{
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
)
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
97 if (event
.key_code() == ui::VKEY_SPACE
)
100 return CustomButton::OnKeyPressed(event
);
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
);
112 DISALLOW_COPY_AND_ASSIGN(SearchBoxImageButton
);
115 SearchBoxView::SearchBoxView(SearchBoxViewDelegate
* delegate
,
116 AppListViewDelegate
* view_delegate
)
117 : delegate_(delegate
),
118 view_delegate_(view_delegate
),
120 content_container_(new views::View
),
123 speech_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());
146 views::Background::CreateSolidBackground(kSearchBoxBackground
));
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_
);
184 view_delegate_
->GetSpeechUI()->AddObserver(this);
188 SearchBoxView::~SearchBoxView() {
189 view_delegate_
->GetSpeechUI()->RemoveObserver(this);
190 model_
->search_box()->RemoveObserver(this);
193 void SearchBoxView::ModelChanged() {
195 model_
->search_box()->RemoveObserver(this);
197 model_
= view_delegate_
->GetModel();
199 model_
->search_box()->AddObserver(this);
201 SpeechRecognitionButtonPropChanged();
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.
215 NotifyQueryChanged();
218 void SearchBoxView::InvalidateMenu() {
222 void SearchBoxView::SetShadow(const gfx::ShadowValue
& shadow
) {
223 SetBorder(make_scoped_ptr(new views::ShadowBorder(shadow
)));
227 gfx::Rect
SearchBoxView::GetViewBoundsForSearchBoxContentsBounds(
228 const gfx::Rect
& rect
) const {
229 gfx::Rect view_bounds
= rect
;
230 view_bounds
.Inset(-GetInsets());
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
) {
243 back_button_
->SetSelected(false);
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
;
251 case FOCUS_SEARCH_BOX
:
252 if (move_backwards
) {
253 focused_view_
= back_button_
&& back_button_
->visible()
254 ? FOCUS_BACK_BUTTON
: FOCUS_SEARCH_BOX
;
256 focused_view_
= speech_button_
&& speech_button_
->visible()
257 ? FOCUS_MIC_BUTTON
: FOCUS_CONTENTS_VIEW
;
260 case FOCUS_MIC_BUTTON
:
261 focused_view_
= move_backwards
? FOCUS_SEARCH_BOX
: FOCUS_CONTENTS_VIEW
;
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
;
273 switch (focused_view_
) {
274 case FOCUS_BACK_BUTTON
:
276 back_button_
->SetSelected(true);
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
284 if (search_box_
->text().empty())
285 search_box_
->NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS
, true);
287 case FOCUS_MIC_BUTTON
:
289 speech_button_
->SetSelected(true);
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
) {
303 back_button_
->SetSelected(false);
305 speech_button_
->SetSelected(false);
306 focused_view_
= on_contents
? FOCUS_CONTENTS_VIEW
: FOCUS_SEARCH_BOX
;
309 void SearchBoxView::SetBackButtonLabel(bool folder
) {
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
) {
326 return contents_view_
->OnMouseWheel(event
);
331 void SearchBoxView::OnEnabledChanged() {
332 search_box_
->SetEnabled(enabled());
334 menu_button_
->SetEnabled(enabled());
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() {
349 delegate_
->QueryChanged(this);
352 void SearchBoxView::ContentsChanged(views::Textfield
* sender
,
353 const base::string16
& new_contents
) {
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()))
368 if (focused_view_
== FOCUS_BACK_BUTTON
&& back_button_
&&
369 back_button_
->OnKeyPressed(key_event
))
372 if (focused_view_
== FOCUS_MIC_BUTTON
&& speech_button_
&&
373 speech_button_
->OnKeyPressed(key_event
))
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
)) {
388 delegate_
->SetSearchResultSelection(true);
389 ResetTabFocus(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();
405 void SearchBoxView::OnMenuButtonClicked(View
* source
, const gfx::Point
& point
) {
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() {
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
);
436 speech_button_
->SetImage(
437 views::Button::STATE_NORMAL
, &speech_button_prop
->off_icon
);
438 speech_button_
->SetTooltipText(speech_button_prop
->off_tooltip
);
441 if (speech_button_
) {
442 // Deleting a view will detach it from its parent.
443 delete speech_button_
;
444 speech_button_
= NULL
;
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();
471 } // namespace app_list