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.
5 #include "ash/system/audio/volume_view.h"
7 #include "ash/ash_constants.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 "ash/system/tray/tray_popup_item_container.h"
14 #include "grit/ash_resources.h"
15 #include "grit/ash_strings.h"
16 #include "ui/base/resource/resource_bundle.h"
17 #include "ui/gfx/canvas.h"
18 #include "ui/gfx/image/image_skia_operations.h"
19 #include "ui/views/background.h"
20 #include "ui/views/border.h"
21 #include "ui/views/controls/button/image_button.h"
22 #include "ui/views/controls/image_view.h"
23 #include "ui/views/controls/separator.h"
24 #include "ui/views/layout/box_layout.h"
27 const int kVolumeImageWidth
= 25;
28 const int kVolumeImageHeight
= 25;
29 const int kSeparatorSize
= 3;
30 const int kSeparatorVerticalInset
= 8;
31 const int kSliderRightPaddingToVolumeViewEdge
= 17;
32 const int kExtraPaddingBetweenBarAndMore
= 10;
33 const int kExtraPaddingBetweenIconAndSlider
= 8;
34 const int kBoxLayoutPadding
= 2;
36 // IDR_AURA_UBER_TRAY_VOLUME_LEVELS contains 5 images,
37 // The one for mute is at the 0 index and the other
38 // four are used for ascending volume levels.
39 const int kVolumeLevels
= 4;
46 class VolumeButton
: public views::ToggleImageButton
{
48 VolumeButton(views::ButtonListener
* listener
,
49 system::TrayAudioDelegate
* audio_delegate
)
50 : views::ToggleImageButton(listener
),
51 audio_delegate_(audio_delegate
),
53 SetImageAlignment(ALIGN_CENTER
, ALIGN_MIDDLE
);
54 image_
= ui::ResourceBundle::GetSharedInstance().GetImageNamed(
55 IDR_AURA_UBER_TRAY_VOLUME_LEVELS
);
59 ~VolumeButton() override
{}
63 static_cast<float>(audio_delegate_
->GetOutputVolumeLevel()) / 100.0f
;
64 int image_index
= audio_delegate_
->IsOutputAudioMuted() ?
67 std::max(1, int(std::ceil(level
* (kVolumeLevels
- 1)))));
68 if (image_index
!= image_index_
) {
69 gfx::Rect
region(0, image_index
* kVolumeImageHeight
,
70 kVolumeImageWidth
, kVolumeImageHeight
);
71 gfx::ImageSkia image_skia
= gfx::ImageSkiaOperations::ExtractSubset(
72 *(image_
.ToImageSkia()), region
);
73 SetImage(views::CustomButton::STATE_NORMAL
, &image_skia
);
74 image_index_
= image_index
;
80 gfx::Size
GetPreferredSize() const override
{
81 gfx::Size size
= views::ToggleImageButton::GetPreferredSize();
82 size
.set_height(kTrayPopupItemHeight
);
86 // views::CustomButton:
87 void StateChanged() override
{
88 if (state() == STATE_HOVERED
|| state() == STATE_PRESSED
) {
90 views::Background::CreateSolidBackground(kHoverBackgroundColor
));
92 set_background(nullptr);
96 system::TrayAudioDelegate
* audio_delegate_
;
100 DISALLOW_COPY_AND_ASSIGN(VolumeButton
);
103 VolumeView::VolumeView(SystemTrayItem
* owner
,
104 system::TrayAudioDelegate
* audio_delegate
,
105 bool is_default_view
)
107 audio_delegate_(audio_delegate
),
112 is_default_view_(is_default_view
) {
114 views::BoxLayout
* box_layout
= new views::BoxLayout(
115 views::BoxLayout::kHorizontal
, 0, 0, kBoxLayoutPadding
);
116 box_layout
->SetDefaultFlex(0);
117 SetLayoutManager(box_layout
);
119 icon_
= new VolumeButton(this, audio_delegate_
);
120 icon_
->SetBorder(views::Border::CreateEmptyBorder(
121 0, kTrayPopupPaddingHorizontal
, 0, kExtraPaddingBetweenIconAndSlider
));
124 slider_
= new views::Slider(this, views::Slider::HORIZONTAL
);
125 slider_
->set_focus_border_color(kFocusBorderColor
);
127 static_cast<float>(audio_delegate_
->GetOutputVolumeLevel()) / 100.0f
);
128 slider_
->SetAccessibleName(
129 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
130 IDS_ASH_STATUS_TRAY_VOLUME
));
132 views::Border::CreateEmptyBorder(0, 0, 0, kTrayPopupPaddingBetweenItems
));
133 AddChildView(slider_
);
134 box_layout
->SetFlexForView(slider_
, 1);
136 separator_
= new views::Separator(views::Separator::VERTICAL
);
137 separator_
->SetColor(kButtonStrokeColor
);
138 separator_
->SetPreferredSize(kSeparatorSize
);
139 separator_
->SetBorder(views::Border::CreateEmptyBorder(
140 kSeparatorVerticalInset
, 0, kSeparatorVerticalInset
, kBoxLayoutPadding
));
142 more_region_
= new TrayPopupItemContainer(separator_
, true, false);
143 more_region_
->SetBorder(
144 views::Border::CreateEmptyBorder(0, 0, 0, kTrayPopupPaddingBetweenItems
));
145 AddChildView(more_region_
);
147 device_type_
= new views::ImageView
;
148 more_region_
->AddChildView(device_type_
);
150 more_
= new views::ImageView
;
151 more_
->EnableCanvasFlippingForRTLUI(true);
152 more_
->SetImage(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
153 IDR_AURA_UBER_TRAY_MORE
).ToImageSkia());
154 more_region_
->AddChildView(more_
);
156 set_background(views::Background::CreateSolidBackground(kBackgroundColor
));
161 VolumeView::~VolumeView() {
164 void VolumeView::Update() {
166 slider_
->UpdateState(!audio_delegate_
->IsOutputAudioMuted());
167 UpdateDeviceTypeAndMore();
171 void VolumeView::SetVolumeLevel(float percent
) {
172 // Slider's value is in finer granularity than audio volume level(0.01),
173 // there will be a small discrepancy between slider's value and volume level
174 // on audio side. To avoid the jittering in slider UI, do not set change
175 // slider value if the change is less than 1%.
176 if (std::abs(percent
-slider_
->value()) < 0.01)
178 // The change in volume will be reflected via accessibility system events,
179 // so we prevent the UI event from being sent here.
180 slider_
->set_enable_accessibility_events(false);
181 slider_
->SetValue(percent
);
182 // It is possible that the volume was (un)muted, but the actual volume level
183 // did not change. In that case, setting the value of the slider won't
184 // trigger an update. So explicitly trigger an update.
186 slider_
->set_enable_accessibility_events(true);
189 void VolumeView::UpdateDeviceTypeAndMore() {
190 bool show_more
= is_default_view_
&& TrayAudio::ShowAudioDeviceMenu() &&
191 audio_delegate_
->HasAlternativeSources();
192 slider_
->SetBorder(views::Border::CreateEmptyBorder(
193 0, 0, 0, show_more
? kTrayPopupPaddingBetweenItems
194 : kSliderRightPaddingToVolumeViewEdge
));
197 more_region_
->SetVisible(false);
201 // Show output device icon if necessary.
202 int device_icon
= audio_delegate_
->GetActiveOutputDeviceIconId();
203 if (device_icon
!= system::TrayAudioDelegate::kNoAudioDeviceIcon
) {
204 device_type_
->SetVisible(true);
205 device_type_
->SetImage(
206 ui::ResourceBundle::GetSharedInstance().GetImageNamed(
207 device_icon
).ToImageSkia());
208 more_region_
->SetLayoutManager(new views::BoxLayout(
209 views::BoxLayout::kHorizontal
, 0, 0, kTrayPopupPaddingBetweenItems
));
211 device_type_
->SetVisible(false);
212 more_region_
->SetLayoutManager(new views::BoxLayout(
213 views::BoxLayout::kHorizontal
, 0, 0,
214 kTrayPopupPaddingBetweenItems
+ kExtraPaddingBetweenBarAndMore
));
216 more_region_
->SetVisible(true);
219 void VolumeView::HandleVolumeUp(float level
) {
220 audio_delegate_
->SetOutputVolumeLevel(level
);
221 if (audio_delegate_
->IsOutputAudioMuted() &&
222 level
> audio_delegate_
->GetOutputDefaultVolumeMuteLevel()) {
223 audio_delegate_
->SetOutputAudioIsMuted(false);
227 void VolumeView::HandleVolumeDown(float level
) {
228 audio_delegate_
->SetOutputVolumeLevel(level
);
229 if (!audio_delegate_
->IsOutputAudioMuted() &&
230 level
<= audio_delegate_
->GetOutputDefaultVolumeMuteLevel()) {
231 audio_delegate_
->SetOutputAudioIsMuted(true);
232 } else if (audio_delegate_
->IsOutputAudioMuted() &&
233 level
> audio_delegate_
->GetOutputDefaultVolumeMuteLevel()) {
234 audio_delegate_
->SetOutputAudioIsMuted(false);
238 void VolumeView::ButtonPressed(views::Button
* sender
, const ui::Event
& event
) {
239 CHECK(sender
== icon_
);
240 bool mute_on
= !audio_delegate_
->IsOutputAudioMuted();
241 audio_delegate_
->SetOutputAudioIsMuted(mute_on
);
243 audio_delegate_
->AdjustOutputVolumeToAudibleLevel();
247 void VolumeView::SliderValueChanged(views::Slider
* sender
,
250 views::SliderChangeReason reason
) {
251 if (reason
== views::VALUE_CHANGED_BY_USER
) {
252 float new_volume
= value
* 100.0f
;
253 float current_volume
= audio_delegate_
->GetOutputVolumeLevel();
254 // Do not call change audio volume if the difference is less than
255 // 1%, which is beyond cras audio api's granularity for output volume.
256 if (std::abs(new_volume
- current_volume
) < 1.0f
)
258 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
260 ash::UMA_STATUS_AREA_CHANGED_VOLUME_MENU
:
261 ash::UMA_STATUS_AREA_CHANGED_VOLUME_POPUP
);
262 if (new_volume
> current_volume
)
263 HandleVolumeUp(new_volume
);
265 HandleVolumeDown(new_volume
);
270 bool VolumeView::PerformAction(const ui::Event
& event
) {
271 if (!more_region_
->visible())
273 owner_
->TransitionDetailedView();
277 void VolumeView::OnBoundsChanged(const gfx::Rect
& previous_bounds
) {
278 // Separator's prefered size is based on set bounds. When an empty bounds is
279 // set on first layout this causes BoxLayout to ignore the separator. Reset
280 // its height on each bounds change so that it is laid out properly.
281 separator_
->SetSize(gfx::Size(kSeparatorSize
, bounds().height()));