// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/speech/speech_input_bubble.h" #include <algorithm> #include "base/message_loop.h" #include "base/utf_string_conversions.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/views/bubble/bubble.h" #include "content/browser/tab_contents/tab_contents.h" #include "content/browser/tab_contents/tab_contents_view.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "media/audio/audio_manager.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "views/border.h" #include "views/controls/button/native_button.h" #include "views/controls/image_view.h" #include "views/controls/label.h" #include "views/controls/link.h" #include "views/layout/layout_constants.h" #include "views/view.h" namespace { const int kBubbleHorizMargin = 6; const int kBubbleVertMargin = 4; const int kBubbleHeadingVertMargin = 6; // This is the content view which is placed inside a SpeechInputBubble. class ContentView : public views::View, public views::ButtonListener, public views::LinkController { public: explicit ContentView(SpeechInputBubbleDelegate* delegate); void UpdateLayout(SpeechInputBubbleBase::DisplayMode mode, const string16& message_text, const SkBitmap& image); void SetImage(const SkBitmap& image); // views::ButtonListener methods. virtual void ButtonPressed(views::Button* source, const views::Event& event); // views::LinkController methods. virtual void LinkActivated(views::Link* source, int event_flags); // views::View overrides. virtual gfx::Size GetPreferredSize(); virtual void Layout(); private: SpeechInputBubbleDelegate* delegate_; views::ImageView* icon_; views::Label* heading_; views::Label* message_; views::NativeButton* try_again_; views::NativeButton* cancel_; views::Link* mic_settings_; SpeechInputBubbleBase::DisplayMode display_mode_; const int kIconLayoutMinWidth; DISALLOW_COPY_AND_ASSIGN(ContentView); }; ContentView::ContentView(SpeechInputBubbleDelegate* delegate) : delegate_(delegate), display_mode_(SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP), kIconLayoutMinWidth(ResourceBundle::GetSharedInstance().GetBitmapNamed( IDR_SPEECH_INPUT_MIC_EMPTY)->width()) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont); heading_ = new views::Label( UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING))); heading_->set_border(views::Border::CreateEmptyBorder( kBubbleHeadingVertMargin, 0, kBubbleHeadingVertMargin, 0)); heading_->SetFont(font); heading_->SetHorizontalAlignment(views::Label::ALIGN_CENTER); heading_->SetText(UTF16ToWide( l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING))); AddChildView(heading_); message_ = new views::Label(); message_->SetFont(font); message_->SetHorizontalAlignment(views::Label::ALIGN_CENTER); message_->SetMultiLine(true); AddChildView(message_); icon_ = new views::ImageView(); icon_->SetHorizontalAlignment(views::ImageView::CENTER); AddChildView(icon_); cancel_ = new views::NativeButton( this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_CANCEL))); AddChildView(cancel_); try_again_ = new views::NativeButton( this, UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_TRY_AGAIN))); AddChildView(try_again_); mic_settings_ = new views::Link( UTF16ToWide(l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_MIC_SETTINGS))); mic_settings_->SetController(this); AddChildView(mic_settings_); } void ContentView::UpdateLayout(SpeechInputBubbleBase::DisplayMode mode, const string16& message_text, const SkBitmap& image) { display_mode_ = mode; bool is_message = (mode == SpeechInputBubbleBase::DISPLAY_MODE_MESSAGE); icon_->SetVisible(!is_message); message_->SetVisible(is_message); mic_settings_->SetVisible(is_message); try_again_->SetVisible(is_message); cancel_->SetVisible(mode != SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP); heading_->SetVisible(mode == SpeechInputBubbleBase::DISPLAY_MODE_RECORDING); if (is_message) { message_->SetText(UTF16ToWideHack(message_text)); } else { SetImage(image); } if (icon_->IsVisible()) icon_->ResetImageSize(); // When moving from warming up to recording state, the size of the content // stays the same. So we wouldn't get a resize/layout call from the view // system and we do it ourselves. if (GetPreferredSize() == size()) // |size()| here is the current size. Layout(); } void ContentView::SetImage(const SkBitmap& image) { icon_->SetImage(image); } void ContentView::ButtonPressed(views::Button* source, const views::Event& event) { if (source == cancel_) { delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_CANCEL); } else if (source == try_again_) { delegate_->InfoBubbleButtonClicked(SpeechInputBubble::BUTTON_TRY_AGAIN); } else { NOTREACHED() << "Unknown button"; } } void ContentView::LinkActivated(views::Link* source, int event_flags) { DCHECK_EQ(source, mic_settings_); AudioManager::GetAudioManager()->ShowAudioInputSettings(); } gfx::Size ContentView::GetPreferredSize() { int width = heading_->GetPreferredSize().width(); int control_width = cancel_->GetPreferredSize().width(); if (try_again_->IsVisible()) { control_width += try_again_->GetPreferredSize().width() + views::kRelatedButtonHSpacing; } width = std::max(width, control_width); control_width = std::max(icon_->GetPreferredSize().width(), kIconLayoutMinWidth); width = std::max(width, control_width); if (mic_settings_->IsVisible()) { control_width = mic_settings_->GetPreferredSize().width(); width = std::max(width, control_width); } int height = cancel_->GetPreferredSize().height(); if (message_->IsVisible()) { height += message_->GetHeightForWidth(width) + views::kLabelToControlVerticalSpacing; } if (heading_->IsVisible()) height += heading_->GetPreferredSize().height(); if (icon_->IsVisible()) height += icon_->GetImage().height(); if (mic_settings_->IsVisible()) height += mic_settings_->GetPreferredSize().height(); width += kBubbleHorizMargin * 2; height += kBubbleVertMargin * 2; return gfx::Size(width, height); } void ContentView::Layout() { int x = kBubbleHorizMargin; int y = kBubbleVertMargin; int available_width = width() - kBubbleHorizMargin * 2; int available_height = height() - kBubbleVertMargin * 2; if (message_->IsVisible()) { DCHECK(try_again_->IsVisible()); int control_height = try_again_->GetPreferredSize().height(); int try_again_width = try_again_->GetPreferredSize().width(); int cancel_width = cancel_->GetPreferredSize().width(); y += available_height - control_height; x += (available_width - cancel_width - try_again_width - views::kRelatedButtonHSpacing) / 2; try_again_->SetBounds(x, y, try_again_width, control_height); cancel_->SetBounds(x + try_again_width + views::kRelatedButtonHSpacing, y, cancel_width, control_height); control_height = message_->GetHeightForWidth(available_width); message_->SetBounds(kBubbleHorizMargin, kBubbleVertMargin, available_width, control_height); y = kBubbleVertMargin + control_height; control_height = mic_settings_->GetPreferredSize().height(); mic_settings_->SetBounds(kBubbleHorizMargin, y, available_width, control_height); } else { DCHECK(icon_->IsVisible()); int control_height = icon_->GetImage().height(); if (display_mode_ == SpeechInputBubbleBase::DISPLAY_MODE_WARM_UP) y = (available_height - control_height) / 2; icon_->SetBounds(x, y, available_width, control_height); y += control_height; if (heading_->IsVisible()) { control_height = heading_->GetPreferredSize().height(); heading_->SetBounds(x, y, available_width, control_height); y += control_height; } if (cancel_->IsVisible()) { control_height = cancel_->GetPreferredSize().height(); int width = cancel_->GetPreferredSize().width(); cancel_->SetBounds(x + (available_width - width) / 2, y, width, control_height); } } } // Implementation of SpeechInputBubble. class SpeechInputBubbleImpl : public SpeechInputBubbleBase, public BubbleDelegate { public: SpeechInputBubbleImpl(TabContents* tab_contents, Delegate* delegate, const gfx::Rect& element_rect); virtual ~SpeechInputBubbleImpl(); // SpeechInputBubble methods. virtual void Show(); virtual void Hide(); // SpeechInputBubbleBase methods. virtual void UpdateLayout(); virtual void UpdateImage(); // Returns the screen rectangle to use as the info bubble's target. // |element_rect| is the html element's bounds in page coordinates. gfx::Rect GetInfoBubbleTarget(const gfx::Rect& element_rect); // BubbleDelegate virtual void BubbleClosing(Bubble* bubble, bool closed_by_escape); virtual bool CloseOnEscape(); virtual bool FadeInOnShow(); private: Delegate* delegate_; Bubble* bubble_; ContentView* bubble_content_; gfx::Rect element_rect_; // Set to true if the object is being destroyed normally instead of the // user clicking outside the window causing it to close automatically. bool did_invoke_close_; DISALLOW_COPY_AND_ASSIGN(SpeechInputBubbleImpl); }; SpeechInputBubbleImpl::SpeechInputBubbleImpl(TabContents* tab_contents, Delegate* delegate, const gfx::Rect& element_rect) : SpeechInputBubbleBase(tab_contents), delegate_(delegate), bubble_(NULL), bubble_content_(NULL), element_rect_(element_rect), did_invoke_close_(false) { } SpeechInputBubbleImpl::~SpeechInputBubbleImpl() { did_invoke_close_ = true; Hide(); } gfx::Rect SpeechInputBubbleImpl::GetInfoBubbleTarget( const gfx::Rect& element_rect) { gfx::Rect container_rect; tab_contents()->GetContainerBounds(&container_rect); return gfx::Rect( container_rect.x() + element_rect.x() + element_rect.width() - kBubbleTargetOffsetX, container_rect.y() + element_rect.y() + element_rect.height(), 1, 1); } void SpeechInputBubbleImpl::BubbleClosing(Bubble* bubble, bool closed_by_escape) { bubble_ = NULL; bubble_content_ = NULL; if (!did_invoke_close_) delegate_->InfoBubbleFocusChanged(); } bool SpeechInputBubbleImpl::CloseOnEscape() { return false; } bool SpeechInputBubbleImpl::FadeInOnShow() { return false; } void SpeechInputBubbleImpl::Show() { if (bubble_) return; // nothing to do, already visible. bubble_content_ = new ContentView(delegate_); UpdateLayout(); views::NativeWidget* toplevel_widget = views::NativeWidget::GetTopLevelNativeWidget( tab_contents()->view()->GetNativeView()); if (toplevel_widget) { bubble_ = Bubble::Show(toplevel_widget->GetWidget(), GetInfoBubbleTarget(element_rect_), BubbleBorder::TOP_LEFT, bubble_content_, this); // We don't want fade outs when closing because it makes speech recognition // appear slower than it is. Also setting it to false allows |Close| to // destroy the bubble immediately instead of waiting for the fade animation // to end so the caller can manage this object's life cycle like a normal // stack based or member variable object. bubble_->set_fade_away_on_close(false); } } void SpeechInputBubbleImpl::Hide() { if (bubble_) bubble_->Close(); } void SpeechInputBubbleImpl::UpdateLayout() { if (bubble_content_) bubble_content_->UpdateLayout(display_mode(), message_text(), icon_image()); if (bubble_) // Will be null on first call. bubble_->SizeToContents(); } void SpeechInputBubbleImpl::UpdateImage() { if (bubble_content_) bubble_content_->SetImage(icon_image()); } } // namespace SpeechInputBubble* SpeechInputBubble::CreateNativeBubble( TabContents* tab_contents, SpeechInputBubble::Delegate* delegate, const gfx::Rect& element_rect) { return new SpeechInputBubbleImpl(tab_contents, delegate, element_rect); }