// Copyright (c) 2012 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_recognition_bubble.h" #include #include "base/utf_string_conversions.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_finder.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/views/location_bar/location_icon_view.h" #include "chrome/browser/ui/views/toolbar_view.h" #include "content/public/browser/resource_context.h" #include "content/public/browser/speech_recognition_manager.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_view.h" #include "grit/generated_resources.h" #include "grit/theme_resources.h" #include "ui/base/l10n/l10n_util.h" #include "ui/base/resource/resource_bundle.h" #include "ui/views/bubble/bubble_delegate.h" #include "ui/views/controls/button/label_button.h" #include "ui/views/controls/image_view.h" #include "ui/views/controls/label.h" #include "ui/views/controls/link.h" #include "ui/views/controls/link_listener.h" #include "ui/views/layout/layout_constants.h" using content::WebContents; namespace { const int kBubbleHorizMargin = 6; const int kBubbleVertMargin = 4; const int kBubbleHeadingVertMargin = 6; const int kIconHorizontalOffset = 27; const int kIconVerticalOffset = -7; // This is the SpeechRecognitionBubble content and views bubble delegate. class SpeechRecognitionBubbleView : public views::BubbleDelegateView, public views::ButtonListener, public views::LinkListener { public: SpeechRecognitionBubbleView(SpeechRecognitionBubbleDelegate* delegate, views::View* anchor_view, const gfx::Rect& element_rect, WebContents* web_contents); void UpdateLayout(SpeechRecognitionBubbleBase::DisplayMode mode, const string16& message_text, const gfx::ImageSkia& image); void SetImage(const gfx::ImageSkia& image); // views::BubbleDelegateView methods. virtual void OnWidgetActivationChanged(views::Widget* widget, bool active) OVERRIDE; virtual gfx::Rect GetAnchorRect() OVERRIDE; virtual void Init() OVERRIDE; // views::ButtonListener methods. virtual void ButtonPressed(views::Button* source, const ui::Event& event) OVERRIDE; // views::LinkListener methods. virtual void LinkClicked(views::Link* source, int event_flags) OVERRIDE; // views::View overrides. virtual gfx::Size GetPreferredSize() OVERRIDE; virtual void Layout() OVERRIDE; void set_notify_delegate_on_activation_change(bool notify) { notify_delegate_on_activation_change_ = notify; } private: SpeechRecognitionBubbleDelegate* delegate_; gfx::Rect element_rect_; WebContents* web_contents_; bool notify_delegate_on_activation_change_; views::ImageView* icon_; views::Label* heading_; views::Label* message_; views::LabelButton* try_again_; views::LabelButton* cancel_; views::Link* mic_settings_; SpeechRecognitionBubbleBase::DisplayMode display_mode_; const int kIconLayoutMinWidth; DISALLOW_COPY_AND_ASSIGN(SpeechRecognitionBubbleView); }; SpeechRecognitionBubbleView::SpeechRecognitionBubbleView( SpeechRecognitionBubbleDelegate* delegate, views::View* anchor_view, const gfx::Rect& element_rect, WebContents* web_contents) : BubbleDelegateView(anchor_view, views::BubbleBorder::TOP_LEFT), delegate_(delegate), element_rect_(element_rect), web_contents_(web_contents), notify_delegate_on_activation_change_(true), icon_(NULL), heading_(NULL), message_(NULL), try_again_(NULL), cancel_(NULL), mic_settings_(NULL), display_mode_(SpeechRecognitionBubbleBase::DISPLAY_MODE_WARM_UP), kIconLayoutMinWidth(ResourceBundle::GetSharedInstance().GetImageSkiaNamed( IDR_SPEECH_INPUT_MIC_EMPTY)->width()) { // The bubble lifetime is managed by its controller; closing on escape or // explicitly closing on deactivation will cause unexpected behavior. set_close_on_esc(false); set_close_on_deactivate(false); } void SpeechRecognitionBubbleView::OnWidgetActivationChanged( views::Widget* widget, bool active) { if (widget == GetWidget() && !active && notify_delegate_on_activation_change_) delegate_->InfoBubbleFocusChanged(); BubbleDelegateView::OnWidgetActivationChanged(widget, active); } gfx::Rect SpeechRecognitionBubbleView::GetAnchorRect() { gfx::Rect container_rect; web_contents_->GetView()->GetContainerBounds(&container_rect); gfx::Rect anchor(element_rect_); anchor.Offset(container_rect.OffsetFromOrigin()); if (!container_rect.Intersects(anchor)) return BubbleDelegateView::GetAnchorRect(); return anchor; } void SpeechRecognitionBubbleView::Init() { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); const gfx::Font& font = rb.GetFont(ResourceBundle::MediumFont); heading_ = new views::Label( l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)); heading_->set_border(views::Border::CreateEmptyBorder( kBubbleHeadingVertMargin, 0, kBubbleHeadingVertMargin, 0)); heading_->SetFont(font); heading_->SetHorizontalAlignment(gfx::ALIGN_CENTER); heading_->SetText( l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_BUBBLE_HEADING)); AddChildView(heading_); message_ = new views::Label(); message_->SetFont(font); message_->SetMultiLine(true); AddChildView(message_); icon_ = new views::ImageView(); icon_->SetHorizontalAlignment(views::ImageView::CENTER); AddChildView(icon_); cancel_ = new views::LabelButton(this, l10n_util::GetStringUTF16(IDS_CANCEL)); cancel_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON); AddChildView(cancel_); try_again_ = new views::LabelButton( this, l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_TRY_AGAIN)); try_again_->SetStyle(views::Button::STYLE_NATIVE_TEXTBUTTON); AddChildView(try_again_); mic_settings_ = new views::Link( l10n_util::GetStringUTF16(IDS_SPEECH_INPUT_MIC_SETTINGS)); mic_settings_->set_listener(this); AddChildView(mic_settings_); } void SpeechRecognitionBubbleView::UpdateLayout( SpeechRecognitionBubbleBase::DisplayMode mode, const string16& message_text, const gfx::ImageSkia& image) { display_mode_ = mode; bool is_message = (mode == SpeechRecognitionBubbleBase::DISPLAY_MODE_MESSAGE); icon_->SetVisible(!is_message); message_->SetVisible(is_message); mic_settings_->SetVisible(is_message); try_again_->SetVisible(is_message); cancel_->SetVisible( mode != SpeechRecognitionBubbleBase::DISPLAY_MODE_WARM_UP); heading_->SetVisible( mode == SpeechRecognitionBubbleBase::DISPLAY_MODE_RECORDING); // Clickable elements should be enabled if and only if they are visible. mic_settings_->SetEnabled(mic_settings_->visible()); try_again_->SetEnabled(try_again_->visible()); cancel_->SetEnabled(cancel_->visible()); if (is_message) { message_->SetText(message_text); } else { SetImage(image); } if (icon_->visible()) 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(); SizeToContents(); } void SpeechRecognitionBubbleView::SetImage(const gfx::ImageSkia& image) { icon_->SetImage(image); } void SpeechRecognitionBubbleView::ButtonPressed(views::Button* source, const ui::Event& event) { if (source == cancel_) { delegate_->InfoBubbleButtonClicked(SpeechRecognitionBubble::BUTTON_CANCEL); } else if (source == try_again_) { delegate_->InfoBubbleButtonClicked( SpeechRecognitionBubble::BUTTON_TRY_AGAIN); } else { NOTREACHED() << "Unknown button"; } } void SpeechRecognitionBubbleView::LinkClicked(views::Link* source, int event_flags) { DCHECK_EQ(mic_settings_, source); content::SpeechRecognitionManager::GetInstance()->ShowAudioInputSettings(); } gfx::Size SpeechRecognitionBubbleView::GetPreferredSize() { int width = heading_->GetPreferredSize().width(); int control_width = cancel_->GetPreferredSize().width(); if (try_again_->visible()) { 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_->visible()) { control_width = mic_settings_->GetPreferredSize().width(); width = std::max(width, control_width); } int height = cancel_->GetPreferredSize().height(); if (message_->visible()) { height += message_->GetHeightForWidth(width) + views::kLabelToControlVerticalSpacing; } if (heading_->visible()) height += heading_->GetPreferredSize().height(); if (icon_->visible()) height += icon_->GetImage().height(); if (mic_settings_->visible()) height += mic_settings_->GetPreferredSize().height(); width += kBubbleHorizMargin * 2; height += kBubbleVertMargin * 2; return gfx::Size(width, height); } void SpeechRecognitionBubbleView::Layout() { int x = kBubbleHorizMargin; int y = kBubbleVertMargin; int available_width = width() - kBubbleHorizMargin * 2; int available_height = height() - kBubbleVertMargin * 2; if (message_->visible()) { DCHECK(try_again_->visible()); 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_->visible()); int control_height = icon_->GetImage().height(); if (display_mode_ == SpeechRecognitionBubbleBase::DISPLAY_MODE_WARM_UP) y = (available_height - control_height) / 2; icon_->SetBounds(x, y, available_width, control_height); y += control_height; if (heading_->visible()) { control_height = heading_->GetPreferredSize().height(); heading_->SetBounds(x, y, available_width, control_height); y += control_height; } if (cancel_->visible()) { control_height = cancel_->GetPreferredSize().height(); int width = cancel_->GetPreferredSize().width(); cancel_->SetBounds(x + (available_width - width) / 2, y, width, control_height); } } } // Implementation of SpeechRecognitionBubble. class SpeechRecognitionBubbleImpl : public SpeechRecognitionBubbleBase { public: SpeechRecognitionBubbleImpl(WebContents* web_contents, Delegate* delegate, const gfx::Rect& element_rect); virtual ~SpeechRecognitionBubbleImpl(); // SpeechRecognitionBubble methods. virtual void Show() OVERRIDE; virtual void Hide() OVERRIDE; // SpeechRecognitionBubbleBase methods. virtual void UpdateLayout() OVERRIDE; virtual void UpdateImage() OVERRIDE; private: Delegate* delegate_; SpeechRecognitionBubbleView* bubble_; gfx::Rect element_rect_; DISALLOW_COPY_AND_ASSIGN(SpeechRecognitionBubbleImpl); }; SpeechRecognitionBubbleImpl::SpeechRecognitionBubbleImpl( WebContents* web_contents, Delegate* delegate, const gfx::Rect& element_rect) : SpeechRecognitionBubbleBase(web_contents), delegate_(delegate), bubble_(NULL), element_rect_(element_rect) { } SpeechRecognitionBubbleImpl::~SpeechRecognitionBubbleImpl() { if (bubble_) { bubble_->set_notify_delegate_on_activation_change(false); bubble_->GetWidget()->Close(); } } void SpeechRecognitionBubbleImpl::Show() { if (!bubble_) { views::View* icon = NULL; // Anchor to the location icon view, in case |element_rect| is offscreen. WebContents* web_contents = GetWebContents(); Browser* browser = chrome::FindBrowserWithWebContents(web_contents); if (browser) { BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser); icon = browser_view->GetLocationBarView() ? browser_view->GetLocationBarView()->location_icon_view() : NULL; } bubble_ = new SpeechRecognitionBubbleView(delegate_, icon, element_rect_, web_contents); if (!icon) { // We dont't have an icon to attach to. Manually specify the web contents // window as the parent. bubble_->set_parent_window( web_contents->GetView()->GetTopLevelNativeWindow()); } views::BubbleDelegateView::CreateBubble(bubble_); UpdateLayout(); } bubble_->GetWidget()->Show(); } void SpeechRecognitionBubbleImpl::Hide() { if (bubble_) bubble_->GetWidget()->Hide(); } void SpeechRecognitionBubbleImpl::UpdateLayout() { if (bubble_) bubble_->UpdateLayout(display_mode(), message_text(), icon_image()); } void SpeechRecognitionBubbleImpl::UpdateImage() { if (bubble_) bubble_->SetImage(icon_image()); } } // namespace SpeechRecognitionBubble* SpeechRecognitionBubble::CreateNativeBubble( WebContents* web_contents, SpeechRecognitionBubble::Delegate* delegate, const gfx::Rect& element_rect) { return new SpeechRecognitionBubbleImpl(web_contents, delegate, element_rect); }