1 // Copyright 2014 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 "ash/system/audio/volume_view.h" 6 7 #include "ash/ash_constants.h" 8 #include "ash/shell.h" 9 #include "ash/system/audio/tray_audio.h" 10 #include "ash/system/audio/tray_audio_delegate.h" 11 #include "ash/system/tray/system_tray_item.h" 12 #include "ash/system/tray/tray_constants.h" 13 #include "grit/ash_resources.h" 14 #include "grit/ash_strings.h" 15 #include "ui/base/resource/resource_bundle.h" 16 #include "ui/gfx/canvas.h" 17 #include "ui/gfx/image/image_skia_operations.h" 18 #include "ui/views/controls/button/image_button.h" 19 #include "ui/views/controls/image_view.h" 20 #include "ui/views/layout/box_layout.h" 21 22 namespace { 23 const int kVolumeImageWidth = 25; 24 const int kVolumeImageHeight = 25; 25 const int kBarSeparatorWidth = 25; 26 const int kBarSeparatorHeight = 30; 27 const int kSliderRightPaddingToVolumeViewEdge = 17; 28 const int kExtraPaddingBetweenBarAndMore = 10; 29 30 // IDR_AURA_UBER_TRAY_VOLUME_LEVELS contains 5 images, 31 // The one for mute is at the 0 index and the other 32 // four are used for ascending volume levels. 33 const int kVolumeLevels = 4; 34 35 } // namespace 36 37 namespace ash { 38 namespace tray { 39 40 class VolumeButton : public views::ToggleImageButton { 41 public: 42 VolumeButton(views::ButtonListener* listener, 43 system::TrayAudioDelegate* audio_delegate) 44 : views::ToggleImageButton(listener), 45 audio_delegate_(audio_delegate), 46 image_index_(-1) { 47 SetImageAlignment(ALIGN_CENTER, ALIGN_MIDDLE); 48 image_ = ui::ResourceBundle::GetSharedInstance().GetImageNamed( 49 IDR_AURA_UBER_TRAY_VOLUME_LEVELS); 50 SetPreferredSize(gfx::Size(kTrayPopupItemHeight, kTrayPopupItemHeight)); 51 Update(); 52 } 53 54 virtual ~VolumeButton() {} 55 56 void Update() { 57 float level = 58 static_cast<float>(audio_delegate_->GetOutputVolumeLevel()) / 100.0f; 59 int image_index = audio_delegate_->IsOutputAudioMuted() ? 60 0 : (level == 1.0 ? 61 kVolumeLevels : 62 std::max(1, int(std::ceil(level * (kVolumeLevels - 1))))); 63 if (image_index != image_index_) { 64 gfx::Rect region(0, image_index * kVolumeImageHeight, 65 kVolumeImageWidth, kVolumeImageHeight); 66 gfx::ImageSkia image_skia = gfx::ImageSkiaOperations::ExtractSubset( 67 *(image_.ToImageSkia()), region); 68 SetImage(views::CustomButton::STATE_NORMAL, &image_skia); 69 image_index_ = image_index; 70 } 71 SchedulePaint(); 72 } 73 74 private: 75 // Overridden from views::View. 76 virtual gfx::Size GetPreferredSize() const OVERRIDE { 77 gfx::Size size = views::ToggleImageButton::GetPreferredSize(); 78 size.set_height(kTrayPopupItemHeight); 79 return size; 80 } 81 82 system::TrayAudioDelegate* audio_delegate_; 83 gfx::Image image_; 84 int image_index_; 85 86 DISALLOW_COPY_AND_ASSIGN(VolumeButton); 87 }; 88 89 class VolumeSlider : public views::Slider { 90 public: 91 VolumeSlider(views::SliderListener* listener, 92 system::TrayAudioDelegate* audio_delegate) 93 : views::Slider(listener, views::Slider::HORIZONTAL), 94 audio_delegate_(audio_delegate) { 95 set_focus_border_color(kFocusBorderColor); 96 SetValue( 97 static_cast<float>(audio_delegate_->GetOutputVolumeLevel()) / 100.0f); 98 SetAccessibleName( 99 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( 100 IDS_ASH_STATUS_TRAY_VOLUME)); 101 Update(); 102 } 103 virtual ~VolumeSlider() {} 104 105 void Update() { 106 UpdateState(!audio_delegate_->IsOutputAudioMuted()); 107 } 108 109 private: 110 system::TrayAudioDelegate* audio_delegate_; 111 112 DISALLOW_COPY_AND_ASSIGN(VolumeSlider); 113 }; 114 115 // Vertical bar separator that can be placed on the VolumeView. 116 class BarSeparator : public views::View { 117 public: 118 BarSeparator() {} 119 virtual ~BarSeparator() {} 120 121 // Overriden from views::View. 122 virtual gfx::Size GetPreferredSize() const OVERRIDE { 123 return gfx::Size(kBarSeparatorWidth, kBarSeparatorHeight); 124 } 125 126 private: 127 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 128 canvas->FillRect(gfx::Rect(width() / 2, 0, 1, height()), 129 kButtonStrokeColor); 130 } 131 132 DISALLOW_COPY_AND_ASSIGN(BarSeparator); 133 }; 134 135 VolumeView::VolumeView(SystemTrayItem* owner, 136 system::TrayAudioDelegate* audio_delegate, 137 bool is_default_view) 138 : owner_(owner), 139 audio_delegate_(audio_delegate), 140 icon_(NULL), 141 slider_(NULL), 142 bar_(NULL), 143 device_type_(NULL), 144 more_(NULL), 145 is_default_view_(is_default_view) { 146 SetFocusable(false); 147 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal, 148 kTrayPopupPaddingHorizontal, 0, kTrayPopupPaddingBetweenItems)); 149 150 icon_ = new VolumeButton(this, audio_delegate_); 151 AddChildView(icon_); 152 153 slider_ = new VolumeSlider(this, audio_delegate_); 154 AddChildView(slider_); 155 156 bar_ = new BarSeparator; 157 AddChildView(bar_); 158 159 device_type_ = new views::ImageView; 160 AddChildView(device_type_); 161 162 more_ = new views::ImageView; 163 more_->EnableCanvasFlippingForRTLUI(true); 164 more_->SetImage(ui::ResourceBundle::GetSharedInstance().GetImageNamed( 165 IDR_AURA_UBER_TRAY_MORE).ToImageSkia()); 166 AddChildView(more_); 167 168 Update(); 169 } 170 171 VolumeView::~VolumeView() { 172 } 173 174 void VolumeView::Update() { 175 icon_->Update(); 176 slider_->Update(); 177 UpdateDeviceTypeAndMore(); 178 Layout(); 179 } 180 181 void VolumeView::SetVolumeLevel(float percent) { 182 // Slider's value is in finer granularity than audio volume level(0.01), 183 // there will be a small discrepancy between slider's value and volume level 184 // on audio side. To avoid the jittering in slider UI, do not set change 185 // slider value if the change is less than 1%. 186 if (std::abs(percent-slider_->value()) < 0.01) 187 return; 188 // The change in volume will be reflected via accessibility system events, 189 // so we prevent the UI event from being sent here. 190 slider_->set_enable_accessibility_events(false); 191 slider_->SetValue(percent); 192 // It is possible that the volume was (un)muted, but the actual volume level 193 // did not change. In that case, setting the value of the slider won't 194 // trigger an update. So explicitly trigger an update. 195 Update(); 196 slider_->set_enable_accessibility_events(true); 197 } 198 199 void VolumeView::UpdateDeviceTypeAndMore() { 200 if (!TrayAudio::ShowAudioDeviceMenu() || !is_default_view_) { 201 more_->SetVisible(false); 202 bar_->SetVisible(false); 203 device_type_->SetVisible(false); 204 return; 205 } 206 207 bool show_more = audio_delegate_->HasAlternativeSources(); 208 more_->SetVisible(show_more); 209 bar_->SetVisible(show_more); 210 211 // Show output device icon if necessary. 212 int device_icon = audio_delegate_->GetActiveOutputDeviceIconId(); 213 if (device_icon != system::TrayAudioDelegate::kNoAudioDeviceIcon) { 214 device_type_->SetVisible(true); 215 device_type_->SetImage( 216 ui::ResourceBundle::GetSharedInstance().GetImageNamed( 217 device_icon).ToImageSkia()); 218 } else { 219 device_type_->SetVisible(false); 220 } 221 } 222 223 void VolumeView::HandleVolumeUp(float level) { 224 audio_delegate_->SetOutputVolumeLevel(level); 225 if (audio_delegate_->IsOutputAudioMuted() && 226 level > audio_delegate_->GetOutputDefaultVolumeMuteLevel()) { 227 audio_delegate_->SetOutputAudioIsMuted(false); 228 } 229 } 230 231 void VolumeView::HandleVolumeDown(float level) { 232 audio_delegate_->SetOutputVolumeLevel(level); 233 if (!audio_delegate_->IsOutputAudioMuted() && 234 level <= audio_delegate_->GetOutputDefaultVolumeMuteLevel()) { 235 audio_delegate_->SetOutputAudioIsMuted(true); 236 } else if (audio_delegate_->IsOutputAudioMuted() && 237 level > audio_delegate_->GetOutputDefaultVolumeMuteLevel()) { 238 audio_delegate_->SetOutputAudioIsMuted(false); 239 } 240 } 241 242 void VolumeView::Layout() { 243 views::View::Layout(); 244 245 if (!more_->visible()) { 246 int w = width() - slider_->bounds().x() - 247 kSliderRightPaddingToVolumeViewEdge; 248 slider_->SetSize(gfx::Size(w, slider_->height())); 249 return; 250 } 251 252 // Make sure the chevron always has the full size. 253 gfx::Size size = more_->GetPreferredSize(); 254 gfx::Rect bounds(size); 255 bounds.set_x(width() - size.width() - kTrayPopupPaddingBetweenItems); 256 bounds.set_y((height() - size.height()) / 2); 257 more_->SetBoundsRect(bounds); 258 259 // Layout either bar_ or device_type_ at the left of the more_ button. 260 views::View* view_left_to_more; 261 if (device_type_->visible()) 262 view_left_to_more = device_type_; 263 else 264 view_left_to_more = bar_; 265 gfx::Size view_size = view_left_to_more->GetPreferredSize(); 266 gfx::Rect view_bounds(view_size); 267 view_bounds.set_x(more_->bounds().x() - view_size.width() - 268 kExtraPaddingBetweenBarAndMore); 269 view_bounds.set_y((height() - view_size.height()) / 2); 270 view_left_to_more->SetBoundsRect(view_bounds); 271 272 // Layout vertical bar next to view_left_to_more if device_type_ is visible. 273 if (device_type_->visible()) { 274 gfx::Size bar_size = bar_->GetPreferredSize(); 275 gfx::Rect bar_bounds(bar_size); 276 bar_bounds.set_x(view_left_to_more->bounds().x() - bar_size.width()); 277 bar_bounds.set_y((height() - bar_size.height()) / 2); 278 bar_->SetBoundsRect(bar_bounds); 279 } 280 281 // Layout slider, calculate slider width. 282 gfx::Rect slider_bounds = slider_->bounds(); 283 slider_bounds.set_width( 284 bar_->bounds().x() 285 - (device_type_->visible() ? 0 : kTrayPopupPaddingBetweenItems) 286 - slider_bounds.x()); 287 slider_->SetBoundsRect(slider_bounds); 288 } 289 290 void VolumeView::ButtonPressed(views::Button* sender, const ui::Event& event) { 291 CHECK(sender == icon_); 292 bool mute_on = !audio_delegate_->IsOutputAudioMuted(); 293 audio_delegate_->SetOutputAudioIsMuted(mute_on); 294 if (!mute_on) 295 audio_delegate_->AdjustOutputVolumeToAudibleLevel(); 296 icon_->Update(); 297 } 298 299 void VolumeView::SliderValueChanged(views::Slider* sender, 300 float value, 301 float old_value, 302 views::SliderChangeReason reason) { 303 if (reason == views::VALUE_CHANGED_BY_USER) { 304 float new_volume = value * 100.0f; 305 float current_volume = audio_delegate_->GetOutputVolumeLevel(); 306 // Do not call change audio volume if the difference is less than 307 // 1%, which is beyond cras audio api's granularity for output volume. 308 if (std::abs(new_volume - current_volume) < 1.0f) 309 return; 310 Shell::GetInstance()->metrics()->RecordUserMetricsAction( 311 is_default_view_ ? 312 ash::UMA_STATUS_AREA_CHANGED_VOLUME_MENU : 313 ash::UMA_STATUS_AREA_CHANGED_VOLUME_POPUP); 314 if (new_volume > current_volume) 315 HandleVolumeUp(new_volume); 316 else 317 HandleVolumeDown(new_volume); 318 } 319 icon_->Update(); 320 } 321 322 bool VolumeView::PerformAction(const ui::Event& event) { 323 if (!more_->visible()) 324 return false; 325 owner_->TransitionDetailedView(); 326 return true; 327 } 328 329 } // namespace tray 330 } // namespace ash 331