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