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
29 namespace {
30
31 const int kBubbleHorizMargin = 6;
32 const int kBubbleVertMargin = 4;
33 const int kBubbleHeadingVertMargin = 6;
34
35 // This is the content view which is placed inside a SpeechInputBubble.
36 class 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
ContentView(SpeechInputBubbleDelegate * delegate)72 ContentView::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
UpdateLayout(SpeechInputBubbleBase::DisplayMode mode,const string16 & message_text,const SkBitmap & image)116 void 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
SetImage(const SkBitmap & image)144 void ContentView::SetImage(const SkBitmap& image) {
145 icon_->SetImage(image);
146 }
147
ButtonPressed(views::Button * source,const views::Event & event)148 void 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
LinkActivated(views::Link * source,int event_flags)159 void ContentView::LinkActivated(views::Link* source, int event_flags) {
160 DCHECK_EQ(source, mic_settings_);
161 AudioManager::GetAudioManager()->ShowAudioInputSettings();
162 }
163
GetPreferredSize()164 gfx::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
Layout()197 void 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.
249 class 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
SpeechInputBubbleImpl(TabContents * tab_contents,Delegate * delegate,const gfx::Rect & element_rect)288 SpeechInputBubbleImpl::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
~SpeechInputBubbleImpl()299 SpeechInputBubbleImpl::~SpeechInputBubbleImpl() {
300 did_invoke_close_ = true;
301 Hide();
302 }
303
GetInfoBubbleTarget(const gfx::Rect & element_rect)304 gfx::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
BubbleClosing(Bubble * bubble,bool closed_by_escape)314 void 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
CloseOnEscape()322 bool SpeechInputBubbleImpl::CloseOnEscape() {
323 return false;
324 }
325
FadeInOnShow()326 bool SpeechInputBubbleImpl::FadeInOnShow() {
327 return false;
328 }
329
Show()330 void 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
Hide()355 void SpeechInputBubbleImpl::Hide() {
356 if (bubble_)
357 bubble_->Close();
358 }
359
UpdateLayout()360 void 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
UpdateImage()367 void SpeechInputBubbleImpl::UpdateImage() {
368 if (bubble_content_)
369 bubble_content_->SetImage(icon_image());
370 }
371
372 } // namespace
373
CreateNativeBubble(TabContents * tab_contents,SpeechInputBubble::Delegate * delegate,const gfx::Rect & element_rect)374 SpeechInputBubble* 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