Home | History | Annotate | Download | only in speech
      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