1// Copyright (c) 2011 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 "chrome/browser/speech/speech_input_bubble.h"
6
7#include <algorithm>
8
9#include "base/message_loop.h"
10#include "base/utf_string_conversions.h"
11#include "chrome/browser/ui/browser_window.h"
12#include "chrome/browser/ui/views/bubble/bubble.h"
13#include "content/browser/tab_contents/tab_contents.h"
14#include "content/browser/tab_contents/tab_contents_view.h"
15#include "grit/generated_resources.h"
16#include "grit/theme_resources.h"
17#include "media/audio/audio_manager.h"
18#include "ui/base/l10n/l10n_util.h"
19#include "ui/base/resource/resource_bundle.h"
20#include "ui/gfx/canvas.h"
21#include "views/border.h"
22#include "views/controls/button/native_button.h"
23#include "views/controls/image_view.h"
24#include "views/controls/label.h"
25#include "views/controls/link.h"
26#include "views/layout/layout_constants.h"
27#include "views/view.h"
28
29namespace {
30
31const int kBubbleHorizMargin = 6;
32const int kBubbleVertMargin = 4;
33const int kBubbleHeadingVertMargin = 6;
34
35// This is the content view which is placed inside a SpeechInputBubble.
36class ContentView
37    : public views::View,
38      public views::ButtonListener,
39      public views::LinkController {
40 public:
41  explicit ContentView(SpeechInputBubbleDelegate* delegate);
42
43  void UpdateLayout(SpeechInputBubbleBase::DisplayMode mode,
44                    const string16& message_text,
45                    const SkBitmap& image);
46  void SetImage(const SkBitmap& image);
47
48  // views::ButtonListener methods.
49  virtual void ButtonPressed(views::Button* source, const views::Event& event);
50
51  // views::LinkController methods.
52  virtual void LinkActivated(views::Link* source, int event_flags);
53
54  // views::View overrides.
55  virtual gfx::Size GetPreferredSize();
56  virtual void Layout();
57
58 private:
59  SpeechInputBubbleDelegate* delegate_;
60  views::ImageView* icon_;
61  views::Label* heading_;
62  views::Label* message_;
63  views::NativeButton* try_again_;
64  views::NativeButton* cancel_;
65  views::Link* mic_settings_;
66  SpeechInputBubbleBase::DisplayMode display_mode_;
67  const int kIconLayoutMinWidth;
68
69  DISALLOW_COPY_AND_ASSIGN(ContentView);
70};
71
72ContentView::ContentView(SpeechInputBubbleDelegate* delegate)
73     : delegate_(delegate),
74       display_mode_(SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP),
75       kIconLayoutMinWidth(ResourceBundle::GetSharedInstance().GetBitmapNamed(
76                           IDR_SPEECH_INPUT_MIC_EMPTY)->width()) {
77  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
78  const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont);
79
80  heading_ = new views::Label(
81      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)));
82  heading_->set_border(views::Border::CreateEmptyBorder(
83      kBubbleHeadingVertMargin, 0, kBubbleHeadingVertMargin, 0));
84  heading_->SetFont(font);
85  heading_->SetHorizontalAlignment(views::Label::ALIGN_CENTER);
86  heading_->SetText(UTF16ToWide(
87      l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)));
88  AddChildView(heading_);
89
90  message_ = new views::Label();
91  message_->SetFont(font);
92  message_->SetHorizontalAlignment(views::Label::ALIGN_CENTER);
93  message_->SetMultiLine(true);
94  AddChildView(message_);
95
96  icon_ = new views::ImageView();
97  icon_->SetHorizontalAlignment(views::ImageView::CENTER);
98  AddChildView(icon_);
99
100  cancel_ = new views::NativeButton(
101      this,
102      UTF16ToWide(l10n_util::GetStringUTF16(IDS_CANCEL)));
103  AddChildView(cancel_);
104
105  try_again_ = new views::NativeButton(
106      this,
107      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_TRY_AGAIN)));
108  AddChildView(try_again_);
109
110  mic_settings_ = new views::Link(
111      UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_MIC_SETTINGS)));
112  mic_settings_->SetController(this);
113  AddChildView(mic_settings_);
114}
115
116void ContentView::UpdateLayout(SpeechInputBubbleBase::DisplayMode mode,
117                               const string16& message_text,
118                               const SkBitmap& image) {
119  display_mode_ = mode;
120  bool is_message = (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE);
121  icon_->SetVisible(!is_message);
122  message_->SetVisible(is_message);
123  mic_settings_->SetVisible(is_message);
124  try_again_->SetVisible(is_message);
125  cancel_->SetVisible(mode != SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP);
126  heading_->SetVisible(mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING);
127
128  if (is_message) {
129    message_->SetText(UTF16ToWideHack(message_text));
130  } else {
131    SetImage(image);
132  }
133
134  if (icon_->IsVisible())
135    icon_->ResetImageSize();
136
137  // When moving from warming up to recording state, the size of the content
138  // stays the same. So we wouldn't get a resize/layout call from the view
139  // system and we do it ourselves.
140  if (GetPreferredSize() == size())  // |size()| here is the current size.
141    Layout();
142}
143
144void ContentView::SetImage(const SkBitmap& image) {
145  icon_->SetImage(image);
146}
147
148void ContentView::ButtonPressed(views::Button* source,
149                                const views::Event& event) {
150  if (source == cancel_) {
151    delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL);
152  } else if (source == try_again_) {
153    delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN);
154  } else {
155    NOTREACHED() << "Unknown button";
156  }
157}
158
159void ContentView::LinkActivated(views::Link* source, int event_flags) {
160  DCHECK_EQ(source, mic_settings_);
161  AudioManager::GetAudioManager()->ShowAudioInputSettings();
162}
163
164gfx::Size ContentView::GetPreferredSize() {
165  int width = heading_->GetPreferredSize().width();
166  int control_width = cancel_->GetPreferredSize().width();
167  if (try_again_->IsVisible()) {
168    control_width += try_again_->GetPreferredSize().width() +
169                     views::kRelatedButtonHSpacing;
170  }
171  width = std::max(width, control_width);
172  control_width = std::max(icon_->GetPreferredSize().width(),
173                           kIconLayoutMinWidth);
174  width = std::max(width, control_width);
175  if (mic_settings_->IsVisible()) {
176    control_width = mic_settings_->GetPreferredSize().width();
177    width = std::max(width, control_width);
178  }
179
180  int height = cancel_->GetPreferredSize().height();
181  if (message_->IsVisible()) {
182    height += message_->GetHeightForWidth(width) +
183              views::kLabelToControlVerticalSpacing;
184  }
185  if (heading_->IsVisible())
186    height += heading_->GetPreferredSize().height();
187  if (icon_->IsVisible())
188    height += icon_->GetImage().height();
189  if (mic_settings_->IsVisible())
190    height += mic_settings_->GetPreferredSize().height();
191  width += kBubbleHorizMargin * 2;
192  height += kBubbleVertMargin * 2;
193
194  return gfx::Size(width, height);
195}
196
197void ContentView::Layout() {
198  int x = kBubbleHorizMargin;
199  int y = kBubbleVertMargin;
200  int available_width = width() - kBubbleHorizMargin * 2;
201  int available_height = height() - kBubbleVertMargin * 2;
202
203  if (message_->IsVisible()) {
204    DCHECK(try_again_->IsVisible());
205
206    int control_height = try_again_->GetPreferredSize().height();
207    int try_again_width = try_again_->GetPreferredSize().width();
208    int cancel_width = cancel_->GetPreferredSize().width();
209    y += available_height - control_height;
210    x += (available_width - cancel_width - try_again_width -
211          views::kRelatedButtonHSpacing) / 2;
212    try_again_->SetBounds(x, y, try_again_width, control_height);
213    cancel_->SetBounds(x + try_again_width + views::kRelatedButtonHSpacing, y,
214                       cancel_width, control_height);
215
216    control_height = message_->GetHeightForWidth(available_width);
217    message_->SetBounds(kBubbleHorizMargin, kBubbleVertMargin,
218                        available_width, control_height);
219    y = kBubbleVertMargin + control_height;
220
221    control_height = mic_settings_->GetPreferredSize().height();
222    mic_settings_->SetBounds(kBubbleHorizMargin, y, available_width,
223                             control_height);
224  } else {
225    DCHECK(icon_->IsVisible());
226
227    int control_height = icon_->GetImage().height();
228    if (display_mode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP)
229      y = (available_height - control_height) / 2;
230    icon_->SetBounds(x, y, available_width, control_height);
231    y += control_height;
232
233    if (heading_->IsVisible()) {
234      control_height = heading_->GetPreferredSize().height();
235      heading_->SetBounds(x, y, available_width, control_height);
236      y += control_height;
237    }
238
239    if (cancel_->IsVisible()) {
240      control_height = cancel_->GetPreferredSize().height();
241      int width = cancel_->GetPreferredSize().width();
242      cancel_->SetBounds(x + (available_width - width) / 2, y, width,
243                         control_height);
244    }
245  }
246}
247
248// Implementation of SpeechInputBubble.
249class SpeechInputBubbleImpl
250    : public SpeechInputBubbleBase,
251      public BubbleDelegate {
252 public:
253  SpeechInputBubbleImpl(TabContents* tab_contents,
254                        Delegate* delegate,
255                        const gfx::Rect& element_rect);
256  virtual ~SpeechInputBubbleImpl();
257
258  // SpeechInputBubble methods.
259  virtual void Show();
260  virtual void Hide();
261
262  // SpeechInputBubbleBase methods.
263  virtual void UpdateLayout();
264  virtual void UpdateImage();
265
266  // Returns the screen rectangle to use as the info bubble's target.
267  // |element_rect| is the html element's bounds in page coordinates.
268  gfx::Rect GetInfoBubbleTarget(const gfx::Rect& element_rect);
269
270  // BubbleDelegate
271  virtual void BubbleClosing(Bubble* bubble, bool closed_by_escape);
272  virtual bool CloseOnEscape();
273  virtual bool FadeInOnShow();
274
275 private:
276  Delegate* delegate_;
277  Bubble* bubble_;
278  ContentView* bubble_content_;
279  gfx::Rect element_rect_;
280
281  // Set to true if the object is being destroyed normally instead of the
282  // user clicking outside the window causing it to close automatically.
283  bool did_invoke_close_;
284
285  DISALLOW_COPY_AND_ASSIGN(SpeechInputBubbleImpl);
286};
287
288SpeechInputBubbleImpl::SpeechInputBubbleImpl(TabContents* tab_contents,
289                                             Delegate* delegate,
290                                             const gfx::Rect& element_rect)
291    : SpeechInputBubbleBase(tab_contents),
292      delegate_(delegate),
293      bubble_(NULL),
294      bubble_content_(NULL),
295      element_rect_(element_rect),
296      did_invoke_close_(false) {
297}
298
299SpeechInputBubbleImpl::~SpeechInputBubbleImpl() {
300  did_invoke_close_ = true;
301  Hide();
302}
303
304gfx::Rect SpeechInputBubbleImpl::GetInfoBubbleTarget(
305    const gfx::Rect& element_rect) {
306  gfx::Rect container_rect;
307  tab_contents()->GetContainerBounds(&container_rect);
308  return gfx::Rect(
309      container_rect.x() + element_rect.x() + element_rect.width() -
310          kBubbleTargetOffsetX,
311      container_rect.y() + element_rect.y() + element_rect.height(), 1, 1);
312}
313
314void SpeechInputBubbleImpl::BubbleClosing(Bubble* bubble,
315                                          bool closed_by_escape) {
316  bubble_ = NULL;
317  bubble_content_ = NULL;
318  if (!did_invoke_close_)
319    delegate_->InfoBubbleFocusChanged();
320}
321
322bool SpeechInputBubbleImpl::CloseOnEscape() {
323  return false;
324}
325
326bool SpeechInputBubbleImpl::FadeInOnShow() {
327  return false;
328}
329
330void SpeechInputBubbleImpl::Show() {
331  if (bubble_)
332    return;  // nothing to do, already visible.
333
334  bubble_content_ = new ContentView(delegate_);
335  UpdateLayout();
336
337  views::NativeWidget* toplevel_widget =
338      views::NativeWidget::GetTopLevelNativeWidget(
339          tab_contents()->view()->GetNativeView());
340  if (toplevel_widget) {
341    bubble_ = Bubble::Show(toplevel_widget->GetWidget(),
342                           GetInfoBubbleTarget(element_rect_),
343                           BubbleBorder::TOP_LEFT, bubble_content_,
344                           this);
345
346    // We don't want fade outs when closing because it makes speech recognition
347    // appear slower than it is. Also setting it to false allows |Close| to
348    // destroy the bubble immediately instead of waiting for the fade animation
349    // to end so the caller can manage this object's life cycle like a normal
350    // stack based or member variable object.
351    bubble_->set_fade_away_on_close(false);
352  }
353}
354
355void SpeechInputBubbleImpl::Hide() {
356  if (bubble_)
357    bubble_->Close();
358}
359
360void SpeechInputBubbleImpl::UpdateLayout() {
361  if (bubble_content_)
362    bubble_content_->UpdateLayout(display_mode(), message_text(), icon_image());
363  if (bubble_)  // Will be null on first call.
364    bubble_->SizeToContents();
365}
366
367void SpeechInputBubbleImpl::UpdateImage() {
368  if (bubble_content_)
369    bubble_content_->SetImage(icon_image());
370}
371
372}  // namespace
373
374SpeechInputBubble* SpeechInputBubble::CreateNativeBubble(
375    TabContents* tab_contents,
376    SpeechInputBubble::Delegate* delegate,
377    const gfx::Rect& element_rect) {
378  return new SpeechInputBubbleImpl(tab_contents, delegate, element_rect);
379}
380