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 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 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 144 void ContentView::SetImage(const SkBitmap& image) { 145 icon_->SetImage(image); 146 } 147 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 159 void ContentView::LinkActivated(views::Link* source, int event_flags) { 160 DCHECK_EQ(source, mic_settings_); 161 AudioManager::GetAudioManager()->ShowAudioInputSettings(); 162 } 163 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 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 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 299 SpeechInputBubbleImpl::~SpeechInputBubbleImpl() { 300 did_invoke_close_ = true; 301 Hide(); 302 } 303 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 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 322 bool SpeechInputBubbleImpl::CloseOnEscape() { 323 return false; 324 } 325 326 bool SpeechInputBubbleImpl::FadeInOnShow() { 327 return false; 328 } 329 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 355 void SpeechInputBubbleImpl::Hide() { 356 if (bubble_) 357 bubble_->Close(); 358 } 359 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 367 void SpeechInputBubbleImpl::UpdateImage() { 368 if (bubble_content_) 369 bubble_content_->SetImage(icon_image()); 370 } 371 372 } // namespace 373 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