| // 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 <algorithm> |
| |
| #include "base/strings/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/toolbar/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; |
| |
| // 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 base::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 base::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(int render_process_id, int render_view_id, |
| 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( |
| int render_process_id, int render_view_id, Delegate* delegate, |
| const gfx::Rect& element_rect) |
| : SpeechRecognitionBubbleBase(render_process_id, render_view_id), |
| 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() { |
| WebContents* web_contents = GetWebContents(); |
| if (!web_contents) |
| return; |
| |
| if (!bubble_) { |
| views::View* icon = NULL; |
| |
| // Anchor to the location bar, in case |element_rect| is offscreen. |
| Browser* browser = chrome::FindBrowserWithWebContents(web_contents); |
| if (browser) { |
| BrowserView* browser_view = |
| BrowserView::GetBrowserViewForBrowser(browser); |
| icon = browser_view->GetLocationBarView() ? |
| browser_view->GetLocationBarView()->GetLocationBarAnchor() : 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_ && GetWebContents()) |
| bubble_->UpdateLayout(display_mode(), message_text(), icon_image()); |
| } |
| |
| void SpeechRecognitionBubbleImpl::UpdateImage() { |
| if (bubble_ && GetWebContents()) |
| bubble_->SetImage(icon_image()); |
| } |
| |
| } // namespace |
| |
| SpeechRecognitionBubble* SpeechRecognitionBubble::CreateNativeBubble( |
| int render_process_id, |
| int render_view_id, |
| SpeechRecognitionBubble::Delegate* delegate, |
| const gfx::Rect& element_rect) { |
| return new SpeechRecognitionBubbleImpl(render_process_id, render_view_id, |
| delegate, element_rect); |
| } |