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.
4
5#include "ui/app_list/views/search_box_view.h"
6
7#include <algorithm>
8
9#include "ui/app_list/app_list_model.h"
10#include "ui/app_list/app_list_switches.h"
11#include "ui/app_list/app_list_view_delegate.h"
12#include "ui/app_list/search_box_model.h"
13#include "ui/app_list/speech_ui_model.h"
14#include "ui/app_list/views/app_list_menu_views.h"
15#include "ui/app_list/views/search_box_view_delegate.h"
16#include "ui/base/resource/resource_bundle.h"
17#include "ui/events/event.h"
18#include "ui/gfx/canvas.h"
19#include "ui/resources/grit/ui_resources.h"
20#include "ui/views/border.h"
21#include "ui/views/controls/button/image_button.h"
22#include "ui/views/controls/button/menu_button.h"
23#include "ui/views/controls/image_view.h"
24#include "ui/views/controls/textfield/textfield.h"
25#include "ui/views/layout/box_layout.h"
26
27namespace app_list {
28
29namespace {
30
31const int kPadding = 14;
32const int kPreferredWidth = 360;
33const int kPreferredHeight = 48;
34
35const SkColor kHintTextColor = SkColorSetRGB(0xA0, 0xA0, 0xA0);
36
37// Menu offset relative to the bottom-right corner of the menu button.
38const int kMenuYOffsetFromButton = -4;
39const int kMenuXOffsetFromButton = -7;
40
41const int kBackgroundBorderWidth = 1;
42const int kBackgroundBorderBottomWidth = 1;
43const int kBackgroundBorderCornerRadius = 2;
44const SkColor kBackgroundBorderColor = SkColorSetRGB(0xEE, 0xEE, 0xEE);
45const SkColor kBackgroundBorderBottomColor = SkColorSetRGB(0xCC, 0xCC, 0xCC);
46
47// A background that paints a solid white rounded rect with a thin grey border.
48class SearchBoxBackground : public views::Background {
49 public:
50  SearchBoxBackground() {}
51  virtual ~SearchBoxBackground() {}
52
53 private:
54  // views::Background overrides:
55  virtual void Paint(gfx::Canvas* canvas, views::View* view) const OVERRIDE {
56    gfx::Rect bounds = view->GetContentsBounds();
57
58    SkPaint paint;
59    paint.setFlags(SkPaint::kAntiAlias_Flag);
60    paint.setColor(kBackgroundBorderColor);
61    canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, paint);
62    bounds.Inset(kBackgroundBorderWidth,
63                 kBackgroundBorderWidth,
64                 kBackgroundBorderWidth,
65                 0);
66    paint.setColor(kBackgroundBorderBottomColor);
67    canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, paint);
68    bounds.Inset(0, 0, 0, kBackgroundBorderBottomWidth);
69    paint.setColor(SK_ColorWHITE);
70    canvas->DrawRoundRect(bounds, kBackgroundBorderCornerRadius, paint);
71  }
72
73  DISALLOW_COPY_AND_ASSIGN(SearchBoxBackground);
74};
75
76}  // namespace
77
78SearchBoxView::SearchBoxView(SearchBoxViewDelegate* delegate,
79                             AppListViewDelegate* view_delegate)
80    : delegate_(delegate),
81      view_delegate_(view_delegate),
82      model_(NULL),
83      icon_view_(NULL),
84      speech_button_(NULL),
85      search_box_(new views::Textfield),
86      contents_view_(NULL) {
87  if (switches::IsExperimentalAppListEnabled()) {
88    set_background(new SearchBoxBackground());
89  } else {
90    icon_view_ = new views::ImageView;
91    AddChildView(icon_view_);
92  }
93
94  views::BoxLayout* layout =
95      new views::BoxLayout(views::BoxLayout::kHorizontal,
96                           kPadding,
97                           0,
98                           kPadding - views::Textfield::kTextPadding);
99  SetLayoutManager(layout);
100  layout->set_cross_axis_alignment(
101      views::BoxLayout::CROSS_AXIS_ALIGNMENT_CENTER);
102  layout->set_minimum_cross_axis_size(kPreferredHeight);
103
104  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
105
106  search_box_->SetBorder(views::Border::NullBorder());
107  search_box_->SetFontList(rb.GetFontList(ui::ResourceBundle::MediumFont));
108  search_box_->set_placeholder_text_color(kHintTextColor);
109  search_box_->set_controller(this);
110  AddChildView(search_box_);
111  layout->SetFlexForView(search_box_, 1);
112
113#if !defined(OS_CHROMEOS)
114  menu_button_ = new views::MenuButton(NULL, base::string16(), this, false);
115  menu_button_->SetBorder(views::Border::NullBorder());
116  menu_button_->SetImage(views::Button::STATE_NORMAL,
117                         *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_NORMAL));
118  menu_button_->SetImage(views::Button::STATE_HOVERED,
119                         *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_HOVER));
120  menu_button_->SetImage(views::Button::STATE_PRESSED,
121                         *rb.GetImageSkiaNamed(IDR_APP_LIST_TOOLS_PRESSED));
122  AddChildView(menu_button_);
123#endif
124
125  view_delegate_->GetSpeechUI()->AddObserver(this);
126  ModelChanged();
127}
128
129SearchBoxView::~SearchBoxView() {
130  view_delegate_->GetSpeechUI()->RemoveObserver(this);
131  model_->search_box()->RemoveObserver(this);
132}
133
134void SearchBoxView::ModelChanged() {
135  if (model_)
136    model_->search_box()->RemoveObserver(this);
137
138  model_ = view_delegate_->GetModel();
139  DCHECK(model_);
140  model_->search_box()->AddObserver(this);
141  IconChanged();
142  SpeechRecognitionButtonPropChanged();
143  HintTextChanged();
144}
145
146bool SearchBoxView::HasSearch() const {
147  return !search_box_->text().empty();
148}
149
150void SearchBoxView::ClearSearch() {
151  search_box_->SetText(base::string16());
152  view_delegate_->AutoLaunchCanceled();
153  // Updates model and fires query changed manually because SetText() above
154  // does not generate ContentsChanged() notification.
155  UpdateModel();
156  NotifyQueryChanged();
157}
158
159void SearchBoxView::InvalidateMenu() {
160  menu_.reset();
161}
162
163gfx::Size SearchBoxView::GetPreferredSize() const {
164  return gfx::Size(kPreferredWidth, kPreferredHeight);
165}
166
167bool SearchBoxView::OnMouseWheel(const ui::MouseWheelEvent& event) {
168  if (contents_view_)
169    return contents_view_->OnMouseWheel(event);
170
171  return false;
172}
173
174void SearchBoxView::UpdateModel() {
175  // Temporarily remove from observer to ignore notifications caused by us.
176  model_->search_box()->RemoveObserver(this);
177  model_->search_box()->SetText(search_box_->text());
178  model_->search_box()->SetSelectionModel(search_box_->GetSelectionModel());
179  model_->search_box()->AddObserver(this);
180}
181
182void SearchBoxView::NotifyQueryChanged() {
183  DCHECK(delegate_);
184  delegate_->QueryChanged(this);
185}
186
187void SearchBoxView::ContentsChanged(views::Textfield* sender,
188                                    const base::string16& new_contents) {
189  UpdateModel();
190  view_delegate_->AutoLaunchCanceled();
191  NotifyQueryChanged();
192}
193
194bool SearchBoxView::HandleKeyEvent(views::Textfield* sender,
195                                   const ui::KeyEvent& key_event) {
196  bool handled = false;
197  if (contents_view_ && contents_view_->visible())
198    handled = contents_view_->OnKeyPressed(key_event);
199
200  return handled;
201}
202
203void SearchBoxView::ButtonPressed(views::Button* sender,
204                                  const ui::Event& event) {
205  DCHECK(speech_button_ && sender == speech_button_);
206  view_delegate_->ToggleSpeechRecognition();
207}
208
209void SearchBoxView::OnMenuButtonClicked(View* source, const gfx::Point& point) {
210  if (!menu_)
211    menu_.reset(new AppListMenuViews(view_delegate_));
212
213  const gfx::Point menu_location =
214      menu_button_->GetBoundsInScreen().bottom_right() +
215      gfx::Vector2d(kMenuXOffsetFromButton, kMenuYOffsetFromButton);
216  menu_->RunMenuAt(menu_button_, menu_location);
217}
218
219void SearchBoxView::IconChanged() {
220  if (icon_view_)
221    icon_view_->SetImage(model_->search_box()->icon());
222}
223
224void SearchBoxView::SpeechRecognitionButtonPropChanged() {
225  const SearchBoxModel::SpeechButtonProperty* speech_button_prop =
226      model_->search_box()->speech_button();
227  if (speech_button_prop) {
228    if (!speech_button_) {
229      speech_button_ = new views::ImageButton(this);
230      AddChildView(speech_button_);
231    }
232
233    if (view_delegate_->GetSpeechUI()->state() ==
234        SPEECH_RECOGNITION_HOTWORD_LISTENING) {
235      speech_button_->SetImage(
236          views::Button::STATE_NORMAL, &speech_button_prop->on_icon);
237      speech_button_->SetTooltipText(speech_button_prop->on_tooltip);
238    } else {
239      speech_button_->SetImage(
240          views::Button::STATE_NORMAL, &speech_button_prop->off_icon);
241      speech_button_->SetTooltipText(speech_button_prop->off_tooltip);
242    }
243  } else {
244    if (speech_button_) {
245      // Deleting a view will detach it from its parent.
246      delete speech_button_;
247      speech_button_ = NULL;
248    }
249  }
250}
251
252void SearchBoxView::HintTextChanged() {
253  search_box_->set_placeholder_text(model_->search_box()->hint_text());
254}
255
256void SearchBoxView::SelectionModelChanged() {
257  search_box_->SelectSelectionModel(model_->search_box()->selection_model());
258}
259
260void SearchBoxView::TextChanged() {
261  search_box_->SetText(model_->search_box()->text());
262  NotifyQueryChanged();
263}
264
265void SearchBoxView::OnSpeechRecognitionStateChanged(
266    SpeechRecognitionState new_state) {
267  SpeechRecognitionButtonPropChanged();
268  SchedulePaint();
269}
270
271}  // namespace app_list
272