ozone: evdev: Sync caps lock LED state to evdev
[chromium-blink-merge.git] / ui / app_list / views / search_box_view.cc
blob14763823b2f50795df0ea5f57a797ece52c5bcce
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/search_box_model.h"
14 #include "ui/app_list/speech_ui_model.h"
15 #include "ui/app_list/views/app_list_menu_views.h"
16 #include "ui/app_list/views/contents_view.h"
17 #include "ui/app_list/views/search_box_view_delegate.h"
18 #include "ui/base/l10n/l10n_util.h"
19 #include "ui/base/resource/resource_bundle.h"
20 #include "ui/events/event.h"
21 #include "ui/gfx/canvas.h"
22 #include "ui/gfx/shadow_value.h"
23 #include "ui/resources/grit/ui_resources.h"
24 #include "ui/strings/grit/ui_strings.h"
25 #include "ui/views/border.h"
26 #include "ui/views/controls/button/image_button.h"
27 #include "ui/views/controls/button/menu_button.h"
28 #include "ui/views/controls/image_view.h"
29 #include "ui/views/controls/textfield/textfield.h"
30 #include "ui/views/layout/box_layout.h"
31 #include "ui/views/layout/fill_layout.h"
32 #include "ui/views/shadow_border.h"
34 namespace app_list {
36 namespace {
38 const int kPadding = 16;
39 const int kInnerPadding = 24;
40 const int kPreferredWidth = 360;
41 const int kPreferredHeight = 48;
43 const SkColor kHintTextColor = SkColorSetRGB(0xA0, 0xA0, 0xA0);
45 // Menu offset relative to the bottom-right corner of the menu button.
46 const int kMenuYOffsetFromButton = -4;
47 const int kMenuXOffsetFromButton = -7;
49 const int kBackgroundBorderCornerRadius = 2;
51 // A background that paints a solid white rounded rect with a thin grey border.
52 class ExperimentalSearchBoxBackground : public views::Background {
53 public:
54 ExperimentalSearchBoxBackground() {}
55 ~ExperimentalSearchBoxBackground() override {}
57 private:
58 // views::Background overrides:
59 void Paint(gfx::Canvas* canvas, views::View* view) const override {
60 gfx::Rect bounds = view->GetContentsBounds();
62 SkPaint paint;
63 paint.setFlags(SkPaint::kAntiAlias_Flag);
64 paint.setColor(kSearchBoxBackground);
65 canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, paint);
68 DISALLOW_COPY_AND_ASSIGN(ExperimentalSearchBoxBackground);
71 } // namespace
73 // To paint grey background on mic and back buttons
74 class SearchBoxImageButton : public views::ImageButton {
75 public:
76 explicit SearchBoxImageButton(views::ButtonListener* listener)
77 : ImageButton(listener), selected_(false) {}
78 ~SearchBoxImageButton() override {}
80 bool selected() { return selected_; }
81 void SetSelected(bool selected) {
82 if (selected_ == selected)
83 return;
85 selected_ = selected;
86 SchedulePaint();
87 if (selected)
88 NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true);
91 bool OnKeyPressed(const ui::KeyEvent& event) override {
92 // Disable space key to press the button. The keyboard events received
93 // by this view are forwarded from a Textfield (SearchBoxView) and key
94 // released events are not forwarded. This leaves the button in pressed
95 // state.
96 if (event.key_code() == ui::VKEY_SPACE)
97 return false;
99 return CustomButton::OnKeyPressed(event);
102 private:
103 // views::View overrides:
104 void OnPaintBackground(gfx::Canvas* canvas) override {
105 if (state_ == STATE_HOVERED || state_ == STATE_PRESSED || selected_)
106 canvas->FillRect(gfx::Rect(size()), kSelectedColor);
109 bool selected_;
111 DISALLOW_COPY_AND_ASSIGN(SearchBoxImageButton);
114 SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate,
115 AppListViewDelegate* view_delegate)
116 : delegate_(delegate),
117 view_delegate_(view_delegate),
118 model_(NULL),
119 content_container_(new views::View),
120 icon_view_(NULL),
121 back_button_(NULL),
122 speech_button_(NULL),
123 menu_button_(NULL),
124 search_box_(new views::Textfield),
125 contents_view_(NULL),
126 focused_view_(FOCUS_SEARCH_BOX) {
127 SetLayoutManager(new views::FillLayout);
128 AddChildView(content_container_);
130 if (switches::IsExperimentalAppListEnabled()) {
131 SetShadow(GetShadowForZHeight(2));
132 back_button_ = new SearchBoxImageButton(this);
133 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
134 back_button_->SetImage(
135 views::ImageButton::STATE_NORMAL,
136 rb.GetImageSkiaNamed(IDR_APP_LIST_FOLDER_BACK_NORMAL));
137 back_button_->SetImageAlignment(views::ImageButton::ALIGN_CENTER,
138 views::ImageButton::ALIGN_MIDDLE);
139 base::string16 back_title(l10n_util::GetStringUTF16(IDS_APP_LIST_BACK));
140 back_button_->SetAccessibleName(back_title);
141 back_button_->SetTooltipText(back_title);
142 content_container_->AddChildView(back_button_);
144 content_container_->set_background(new ExperimentalSearchBoxBackground());
145 } else {
146 set_background(
147 views::Background::CreateSolidBackground(kSearchBoxBackground));
148 SetBorder(
149 views::Border::CreateSolidSidedBorder(0, 0, 1, 0, kTopSeparatorColor));
150 icon_view_ = new views::ImageView;
151 content_container_->AddChildView(icon_view_);
154 views::BoxLayout* layout =
155 new views::BoxLayout(views::BoxLayout::kHorizontal, kPadding, 0,
156 kInnerPadding - views::Textfield::kTextPadding);
157 content_container_->SetLayoutManager(layout);
158 layout->set_cross_axis_alignment(
159 views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
160 layout->set_minimum_cross_axis_size(kPreferredHeight);
162 search_box_->SetBorder(views::Border::NullBorder());
163 search_box_->SetTextColor(kSearchTextColor);
164 search_box_->set_placeholder_text_color(kHintTextColor);
165 search_box_->set_controller(this);
166 content_container_->AddChildView(search_box_);
167 layout->SetFlexForView(search_box_, 1);
169 #if !defined(OS_CHROMEOS)
170 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
171 menu_button_ = new views::MenuButton(NULL, base::string16(), this, false);
172 menu_button_->SetBorder(views::Border::NullBorder());
173 menu_button_->SetImage(views::Button::STATE_NORMAL,
174 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_NORMAL));
175 menu_button_->SetImage(views::Button::STATE_HOVERED,
176 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_HOVER));
177 menu_button_->SetImage(views::Button::STATE_PRESSED,
178 *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_PRESSED));
179 content_container_->AddChildView(menu_button_);
180 #endif
182 view_delegate_->GetSpeechUI()->AddObserver(this);
183 ModelChanged();
186 SearchBoxView::~SearchBoxView() {
187 view_delegate_->GetSpeechUI()->RemoveObserver(this);
188 model_->search_box()->RemoveObserver(this);
191 void SearchBoxView::ModelChanged() {
192 if (model_)
193 model_->search_box()->RemoveObserver(this);
195 model_ = view_delegate_->GetModel();
196 DCHECK(model_);
197 model_->search_box()->AddObserver(this);
198 IconChanged();
199 SpeechRecognitionButtonPropChanged();
200 HintTextChanged();
203 bool SearchBoxView::HasSearch() const {
204 return !search_box_->text().empty();
207 void SearchBoxView::ClearSearch() {
208 search_box_->SetText(base::string16());
209 view_delegate_->AutoLaunchCanceled();
210 // Updates model and fires query changed manually because SetText() above
211 // does not generate ContentsChanged() notification.
212 UpdateModel();
213 NotifyQueryChanged();
216 void SearchBoxView::InvalidateMenu() {
217 menu_.reset();
220 void SearchBoxView::SetShadow(const gfx::ShadowValue& shadow) {
221 SetBorder(make_scoped_ptr(new views::ShadowBorder(shadow)));
222 Layout();
225 gfx::Rect SearchBoxView::GetViewBoundsForSearchBoxContentsBounds(
226 const gfx::Rect& rect) const {
227 gfx::Rect view_bounds = rect;
228 view_bounds.Inset(-GetInsets());
229 return view_bounds;
232 views::ImageButton* SearchBoxView::back_button() {
233 return static_cast<views::ImageButton*>(back_button_);
236 // Returns true if set internally, i.e. if focused_view_ != CONTENTS_VIEW.
237 // Note: because we always want to be able to type in the edit box, this is only
238 // a faux-focus so that buttons can respond to the ENTER key.
239 bool SearchBoxView::MoveTabFocus(bool move_backwards) {
240 if (back_button_)
241 back_button_->SetSelected(false);
242 if (speech_button_)
243 speech_button_->SetSelected(false);
245 switch (focused_view_) {
246 case FOCUS_BACK_BUTTON:
247 focused_view_ = move_backwards ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX;
248 break;
249 case FOCUS_SEARCH_BOX:
250 if (move_backwards) {
251 focused_view_ = back_button_ && back_button_->visible()
252 ? FOCUS_BACK_BUTTON : FOCUS_SEARCH_BOX;
253 } else {
254 focused_view_ = speech_button_ && speech_button_->visible()
255 ? FOCUS_MIC_BUTTON : FOCUS_CONTENTS_VIEW;
257 break;
258 case FOCUS_MIC_BUTTON:
259 focused_view_ = move_backwards ? FOCUS_SEARCH_BOX : FOCUS_CONTENTS_VIEW;
260 break;
261 case FOCUS_CONTENTS_VIEW:
262 focused_view_ = move_backwards
263 ? (speech_button_ && speech_button_->visible() ?
264 FOCUS_MIC_BUTTON : FOCUS_SEARCH_BOX)
265 : FOCUS_CONTENTS_VIEW;
266 break;
267 default:
268 DCHECK(false);
271 switch (focused_view_) {
272 case FOCUS_BACK_BUTTON:
273 if (back_button_)
274 back_button_->SetSelected(true);
275 break;
276 case FOCUS_SEARCH_BOX:
277 // Set the ChromeVox focus to the search box. However, DO NOT do this if
278 // we are in the search results state (i.e., if the search box has text in
279 // it), because the focus is about to be shifted to the first search
280 // result and we do not want to read out the name of the search box as
281 // well.
282 if (search_box_->text().empty())
283 search_box_->NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true);
284 break;
285 case FOCUS_MIC_BUTTON:
286 if (speech_button_)
287 speech_button_->SetSelected(true);
288 break;
289 default:
290 break;
293 if (focused_view_ < FOCUS_CONTENTS_VIEW)
294 delegate_->SetSearchResultSelection(focused_view_ == FOCUS_SEARCH_BOX);
296 return (focused_view_ < FOCUS_CONTENTS_VIEW);
299 void SearchBoxView::ResetTabFocus(bool on_contents) {
300 if (back_button_)
301 back_button_->SetSelected(false);
302 if (speech_button_)
303 speech_button_->SetSelected(false);
304 focused_view_ = on_contents ? FOCUS_CONTENTS_VIEW : FOCUS_SEARCH_BOX;
307 gfx::Size SearchBoxView::GetPreferredSize() const {
308 return gfx::Size(kPreferredWidth, kPreferredHeight);
311 bool SearchBoxView::OnMouseWheel(const ui::MouseWheelEvent& event) {
312 if (contents_view_)
313 return contents_view_->OnMouseWheel(event);
315 return false;
318 void SearchBoxView::OnEnabledChanged() {
319 search_box_->SetEnabled(enabled());
320 if (menu_button_)
321 menu_button_->SetEnabled(enabled());
322 if (speech_button_)
323 speech_button_->SetEnabled(enabled());
326 void SearchBoxView::UpdateModel() {
327 // Temporarily remove from observer to ignore notifications caused by us.
328 model_->search_box()->RemoveObserver(this);
329 model_->search_box()->SetText(search_box_->text());
330 model_->search_box()->SetSelectionModel(search_box_->GetSelectionModel());
331 model_->search_box()->AddObserver(this);
334 void SearchBoxView::NotifyQueryChanged() {
335 DCHECK(delegate_);
336 delegate_->QueryChanged(this);
339 void SearchBoxView::ContentsChanged(views::Textfield* sender,
340 const base::string16& new_contents) {
341 UpdateModel();
342 view_delegate_->AutoLaunchCanceled();
343 NotifyQueryChanged();
346 bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
347 const ui::KeyEvent& key_event) {
348 bool handled = false;
349 if (key_event.key_code() == ui::VKEY_TAB) {
350 if (focused_view_ != FOCUS_CONTENTS_VIEW &&
351 MoveTabFocus(key_event.IsShiftDown()))
352 return true;
355 if (focused_view_ == FOCUS_BACK_BUTTON && back_button_ &&
356 back_button_->OnKeyPressed(key_event))
357 return true;
359 if (focused_view_ == FOCUS_MIC_BUTTON && speech_button_ &&
360 speech_button_->OnKeyPressed(key_event))
361 return true;
363 if (contents_view_ && contents_view_->visible())
364 handled = contents_view_->OnKeyPressed(key_event);
366 // Arrow keys may have selected an item. If they did, move focus off buttons.
367 // If they didn't, we still select the first search item, in case they're
368 // moving the caret through typed search text. The UP arrow never moves
369 // focus from text/buttons to app list/results, so ignore it.
370 if (focused_view_ < FOCUS_CONTENTS_VIEW &&
371 (key_event.key_code() == ui::VKEY_LEFT ||
372 key_event.key_code() == ui::VKEY_RIGHT ||
373 key_event.key_code() == ui::VKEY_DOWN)) {
374 if (!handled)
375 delegate_->SetSearchResultSelection(true);
376 ResetTabFocus(handled);
379 return handled;
382 void SearchBoxView::ButtonPressed(views::Button* sender,
383 const ui::Event& event) {
384 if (back_button_ && sender == back_button_)
385 delegate_->BackButtonPressed();
386 else if (speech_button_ && sender == speech_button_)
387 view_delegate_->ToggleSpeechRecognition();
388 else
389 NOTREACHED();
392 void SearchBoxView::OnMenuButtonClicked(View* source, const gfx::Point& point) {
393 if (!menu_)
394 menu_.reset(new AppListMenuViews(view_delegate_));
396 const gfx::Point menu_location =
397 menu_button_->GetBoundsInScreen().bottom_right() +
398 gfx::Vector2d(kMenuXOffsetFromButton, kMenuYOffsetFromButton);
399 menu_->RunMenuAt(menu_button_, menu_location);
402 void SearchBoxView::IconChanged() {
403 if (icon_view_)
404 icon_view_->SetImage(model_->search_box()->icon());
407 void SearchBoxView::SpeechRecognitionButtonPropChanged() {
408 const SearchBoxModel::SpeechButtonProperty* speech_button_prop =
409 model_->search_box()->speech_button();
410 if (speech_button_prop) {
411 if (!speech_button_) {
412 speech_button_ = new SearchBoxImageButton(this);
413 content_container_->AddChildView(speech_button_);
416 speech_button_->SetAccessibleName(speech_button_prop->accessible_name);
417 if (view_delegate_->GetSpeechUI()->state() ==
418 SPEECH_RECOGNITION_HOTWORD_LISTENING) {
419 speech_button_->SetImage(
420 views::Button::STATE_NORMAL, &speech_button_prop->on_icon);
421 speech_button_->SetTooltipText(speech_button_prop->on_tooltip);
422 } else {
423 speech_button_->SetImage(
424 views::Button::STATE_NORMAL, &speech_button_prop->off_icon);
425 speech_button_->SetTooltipText(speech_button_prop->off_tooltip);
427 } else {
428 if (speech_button_) {
429 // Deleting a view will detach it from its parent.
430 delete speech_button_;
431 speech_button_ = NULL;
434 Layout();
437 void SearchBoxView::HintTextChanged() {
438 const app_list::SearchBoxModel* search_box = model_->search_box();
439 search_box_->set_placeholder_text(search_box->hint_text());
440 search_box_->SetAccessibleName(search_box->accessible_name());
443 void SearchBoxView::SelectionModelChanged() {
444 search_box_->SelectSelectionModel(model_->search_box()->selection_model());
447 void SearchBoxView::TextChanged() {
448 search_box_->SetText(model_->search_box()->text());
449 NotifyQueryChanged();
452 void SearchBoxView::OnSpeechRecognitionStateChanged(
453 SpeechRecognitionState new_state) {
454 SpeechRecognitionButtonPropChanged();
455 SchedulePaint();
458 } // namespace app_list