1 // Copyright 2013 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 "ui/app_list/views/speech_view.h" 6 7 #include "base/strings/utf_string_conversions.h" 8 #include "grit/ui_resources.h" 9 #include "grit/ui_strings.h" 10 #include "third_party/skia/include/core/SkPath.h" 11 #include "ui/app_list/app_list_model.h" 12 #include "ui/app_list/app_list_view_delegate.h" 13 #include "ui/app_list/speech_ui_model.h" 14 #include "ui/base/l10n/l10n_util.h" 15 #include "ui/base/resource/resource_bundle.h" 16 #include "ui/gfx/canvas.h" 17 #include "ui/gfx/path.h" 18 #include "ui/views/animation/bounds_animator.h" 19 #include "ui/views/background.h" 20 #include "ui/views/controls/button/image_button.h" 21 #include "ui/views/controls/image_view.h" 22 #include "ui/views/controls/label.h" 23 #include "ui/views/layout/fill_layout.h" 24 #include "ui/views/masked_view_targeter.h" 25 #include "ui/views/shadow_border.h" 26 27 namespace app_list { 28 29 namespace { 30 31 const int kShadowOffset = 1; 32 const int kShadowBlur = 4; 33 const int kSpeechViewMaxHeight = 300; 34 const int kMicButtonMargin = 12; 35 const int kTextMargin = 32; 36 const int kLogoMarginLeft = 30; 37 const int kLogoMarginTop = 28; 38 const int kLogoWidth = 104; 39 const int kLogoHeight = 36; 40 const int kIndicatorCenterOffsetY = -1; 41 const int kIndicatorRadiusMinOffset = -3; 42 const int kIndicatorRadiusMax = 100; 43 const int kIndicatorAnimationDuration = 100; 44 const SkColor kShadowColor = SkColorSetARGB(0.3 * 255, 0, 0, 0); 45 const SkColor kHintTextColor = SkColorSetRGB(119, 119, 119); 46 const SkColor kResultTextColor = SkColorSetRGB(178, 178, 178); 47 const SkColor kSoundLevelIndicatorColor = SkColorSetRGB(219, 219, 219); 48 49 class SoundLevelIndicator : public views::View { 50 public: 51 SoundLevelIndicator(); 52 virtual ~SoundLevelIndicator(); 53 54 private: 55 // Overridden from views::View: 56 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 57 58 DISALLOW_COPY_AND_ASSIGN(SoundLevelIndicator); 59 }; 60 61 SoundLevelIndicator::SoundLevelIndicator() {} 62 63 SoundLevelIndicator::~SoundLevelIndicator() {} 64 65 void SoundLevelIndicator::OnPaint(gfx::Canvas* canvas) { 66 SkPaint paint; 67 paint.setStyle(SkPaint::kFill_Style); 68 paint.setColor(kSoundLevelIndicatorColor); 69 paint.setAntiAlias(true); 70 canvas->DrawCircle(bounds().CenterPoint(), width() / 2, paint); 71 } 72 73 // MicButton is an image button with circular hit area. 74 class MicButton : public views::ImageButton { 75 public: 76 explicit MicButton(views::ButtonListener* listener); 77 virtual ~MicButton(); 78 79 private: 80 // Overridden from views::View: 81 virtual bool HasHitTestMask() const OVERRIDE; 82 virtual void GetHitTestMask(views::View::HitTestSource source, 83 gfx::Path* mask) const OVERRIDE; 84 85 DISALLOW_COPY_AND_ASSIGN(MicButton); 86 }; 87 88 MicButton::MicButton(views::ButtonListener* listener) 89 : views::ImageButton(listener) {} 90 91 MicButton::~MicButton() {} 92 93 bool MicButton::HasHitTestMask() const { 94 return true; 95 } 96 97 // TODO(tdanderson): Move the implementation into views::View::HitTestRect() 98 // and delete this function. See crbug.com/377527. 99 void MicButton::GetHitTestMask(views::View::HitTestSource source, 100 gfx::Path* mask) const { 101 const ui::EventTargeter* targeter = GetEventTargeter(); 102 CHECK(targeter); 103 static_cast<const views::MaskedViewTargeter*>(targeter) 104 ->GetHitTestMask(this, mask); 105 } 106 107 // Used to define the circular hit test region of a MicButton for the 108 // purposes of event targeting. 109 class MicButtonTargeter : public views::MaskedViewTargeter { 110 public: 111 explicit MicButtonTargeter(views::View* mic_button) 112 : views::MaskedViewTargeter(mic_button) {} 113 virtual ~MicButtonTargeter() {} 114 115 private: 116 // views::MaskedViewTargeter: 117 virtual bool GetHitTestMask(const views::View* view, 118 gfx::Path* mask) const OVERRIDE { 119 DCHECK(mask); 120 DCHECK_EQ(view, masked_view()); 121 122 // The mic button icon is a circle. 123 gfx::Rect local_bounds = view->GetLocalBounds(); 124 int radius = local_bounds.width() / 2 + kIndicatorRadiusMinOffset; 125 gfx::Point center = local_bounds.CenterPoint(); 126 center.set_y(center.y() + kIndicatorCenterOffsetY); 127 mask->addCircle(SkIntToScalar(center.x()), 128 SkIntToScalar(center.y()), 129 SkIntToScalar(radius)); 130 return true; 131 } 132 133 DISALLOW_COPY_AND_ASSIGN(MicButtonTargeter); 134 }; 135 136 } // namespace 137 138 // static 139 140 SpeechView::SpeechView(AppListViewDelegate* delegate) 141 : delegate_(delegate), 142 logo_(NULL) { 143 SetBorder(scoped_ptr<views::Border>( 144 new views::ShadowBorder(kShadowBlur, 145 kShadowColor, 146 kShadowOffset, // Vertical offset. 147 0))); 148 149 // To keep the painting order of the border and the background, this class 150 // actually has a single child of 'container' which has white background and 151 // contains all components. 152 views::View* container = new views::View(); 153 container->set_background( 154 views::Background::CreateSolidBackground(SK_ColorWHITE)); 155 156 const gfx::ImageSkia& logo_image = delegate_->GetSpeechUI()->logo(); 157 if (!logo_image.isNull()) { 158 logo_ = new views::ImageView(); 159 logo_->SetImage(&logo_image); 160 container->AddChildView(logo_); 161 } 162 163 indicator_ = new SoundLevelIndicator(); 164 indicator_->SetVisible(false); 165 container->AddChildView(indicator_); 166 167 mic_button_ = new MicButton(this); 168 container->AddChildView(mic_button_); 169 mic_button_->SetEventTargeter( 170 scoped_ptr<ui::EventTargeter>(new MicButtonTargeter(mic_button_))); 171 172 // TODO(mukai): use BoundedLabel to cap 2 lines. 173 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 174 speech_result_ = new views::Label( 175 base::string16(), bundle.GetFontList(ui::ResourceBundle::LargeFont)); 176 speech_result_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 177 178 speech_result_->SetMultiLine(true); 179 container->AddChildView(speech_result_); 180 181 AddChildView(container); 182 183 delegate_->GetSpeechUI()->AddObserver(this); 184 indicator_animator_.reset(new views::BoundsAnimator(container)); 185 indicator_animator_->SetAnimationDuration(kIndicatorAnimationDuration); 186 indicator_animator_->set_tween_type(gfx::Tween::LINEAR); 187 188 Reset(); 189 } 190 191 SpeechView::~SpeechView() { 192 delegate_->GetSpeechUI()->RemoveObserver(this); 193 } 194 195 void SpeechView::Reset() { 196 OnSpeechRecognitionStateChanged(delegate_->GetSpeechUI()->state()); 197 } 198 199 int SpeechView::GetIndicatorRadius(uint8 level) { 200 int radius_min = mic_button_->width() / 2 + kIndicatorRadiusMinOffset; 201 int range = kIndicatorRadiusMax - radius_min; 202 return level * range / kuint8max + radius_min; 203 } 204 205 void SpeechView::Layout() { 206 views::View* container = child_at(0); 207 container->SetBoundsRect(GetContentsBounds()); 208 209 // Because container is a pure View, this class should layout its children. 210 const gfx::Rect contents_bounds = container->GetContentsBounds(); 211 if (logo_) 212 logo_->SetBounds(kLogoMarginLeft, kLogoMarginTop, kLogoWidth, kLogoHeight); 213 gfx::Size mic_size = mic_button_->GetPreferredSize(); 214 gfx::Point mic_origin( 215 contents_bounds.right() - kMicButtonMargin - mic_size.width(), 216 contents_bounds.y() + kMicButtonMargin); 217 mic_button_->SetBoundsRect(gfx::Rect(mic_origin, mic_size)); 218 219 int speech_width = contents_bounds.width() - kTextMargin * 2; 220 speech_result_->SizeToFit(speech_width); 221 int speech_height = speech_result_->GetHeightForWidth(speech_width); 222 speech_result_->SetBounds( 223 contents_bounds.x() + kTextMargin, 224 contents_bounds.bottom() - kTextMargin - speech_height, 225 speech_width, 226 speech_height); 227 } 228 229 gfx::Size SpeechView::GetPreferredSize() const { 230 return gfx::Size(0, kSpeechViewMaxHeight); 231 } 232 233 void SpeechView::ButtonPressed(views::Button* sender, const ui::Event& event) { 234 delegate_->ToggleSpeechRecognition(); 235 } 236 237 void SpeechView::OnSpeechSoundLevelChanged(uint8 level) { 238 if (!visible() || 239 delegate_->GetSpeechUI()->state() == SPEECH_RECOGNITION_NETWORK_ERROR) 240 return; 241 242 gfx::Point origin = mic_button_->bounds().CenterPoint(); 243 int radius = GetIndicatorRadius(level); 244 origin.Offset(-radius, -radius + kIndicatorCenterOffsetY); 245 gfx::Rect indicator_bounds = 246 gfx::Rect(origin, gfx::Size(radius * 2, radius * 2)); 247 if (indicator_->visible()) { 248 indicator_animator_->AnimateViewTo(indicator_, indicator_bounds); 249 } else { 250 indicator_->SetVisible(true); 251 indicator_->SetBoundsRect(indicator_bounds); 252 } 253 } 254 255 void SpeechView::OnSpeechResult(const base::string16& result, 256 bool is_final) { 257 speech_result_->SetText(result); 258 speech_result_->SetEnabledColor(kResultTextColor); 259 } 260 261 void SpeechView::OnSpeechRecognitionStateChanged( 262 SpeechRecognitionState new_state) { 263 int resource_id = IDR_APP_LIST_SPEECH_MIC_OFF; 264 if (new_state == SPEECH_RECOGNITION_RECOGNIZING) 265 resource_id = IDR_APP_LIST_SPEECH_MIC_ON; 266 else if (new_state == SPEECH_RECOGNITION_IN_SPEECH) 267 resource_id = IDR_APP_LIST_SPEECH_MIC_RECORDING; 268 269 int text_resource_id = IDS_APP_LIST_SPEECH_HINT_TEXT; 270 271 if (new_state == SPEECH_RECOGNITION_NETWORK_ERROR) { 272 text_resource_id = IDS_APP_LIST_SPEECH_NETWORK_ERROR_HINT_TEXT; 273 indicator_->SetVisible(false); 274 } 275 speech_result_->SetText(l10n_util::GetStringUTF16(text_resource_id)); 276 speech_result_->SetEnabledColor(kHintTextColor); 277 278 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance(); 279 mic_button_->SetImage(views::Button::STATE_NORMAL, 280 bundle.GetImageSkiaNamed(resource_id)); 281 } 282 283 } // namespace app_list 284