1 // Copyright 2020 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/media/unified_media_controls_view.h"
6
7 #include "ash/resources/vector_icons/vector_icons.h"
8 #include "ash/strings/grit/ash_strings.h"
9 #include "ash/style/ash_color_provider.h"
10 #include "ash/system/media/unified_media_controls_controller.h"
11 #include "ash/system/tray/tray_constants.h"
12 #include "ash/system/tray/tray_popup_utils.h"
13 #include "components/media_message_center/media_notification_util.h"
14 #include "components/vector_icons/vector_icons.h"
15 #include "ui/base/l10n/l10n_util.h"
16 #include "ui/gfx/paint_vector_icon.h"
17 #include "ui/views/animation/flood_fill_ink_drop_ripple.h"
18 #include "ui/views/animation/ink_drop_highlight.h"
19 #include "ui/views/animation/ink_drop_impl.h"
20 #include "ui/views/background.h"
21 #include "ui/views/border.h"
22 #include "ui/views/controls/highlight_path_generator.h"
23 #include "ui/views/controls/image_view.h"
24 #include "ui/views/controls/label.h"
25 #include "ui/views/layout/box_layout.h"
26
27 namespace ash {
28
29 using media_session::mojom::MediaSessionAction;
30
31 namespace {
32
33 constexpr int kMediaControlsCornerRadius = 8;
34 constexpr int kMediaControlsViewPadding = 8;
35 constexpr int kMediaButtonsPadding = 8;
36 constexpr int kMediaButtonIconSize = 20;
37 constexpr int kArtworkCornerRadius = 4;
38 constexpr int kTitleRowHeight = 20;
39 constexpr int kTrackTitleFontSizeIncrease = 1;
40
41 constexpr gfx::Insets kTrackColumnInsets = gfx::Insets(1, 8, 1, 8);
42 constexpr gfx::Insets kMediaControlsViewInsets = gfx::Insets(8, 8, 8, 12);
43
44 constexpr gfx::Size kEmptyArtworkIconSize = gfx::Size(20, 20);
45 constexpr gfx::Size kArtworkSize = gfx::Size(40, 40);
46 constexpr gfx::Size kMediaButtonSize = gfx::Size(32, 32);
47
ScaleSizeToFitView(const gfx::Size & size,const gfx::Size & view_size)48 gfx::Size ScaleSizeToFitView(const gfx::Size& size,
49 const gfx::Size& view_size) {
50 // If |size| is too small in either dimension or too big in both
51 // dimensions, scale it appropriately.
52 if ((size.width() > view_size.width() &&
53 size.height() > view_size.height()) ||
54 (size.width() < view_size.width() ||
55 size.height() < view_size.height())) {
56 const float scale =
57 std::max(view_size.width() / static_cast<float>(size.width()),
58 view_size.height() / static_cast<float>(size.height()));
59 return gfx::ScaleToFlooredSize(size, scale);
60 }
61
62 return size;
63 }
64
GetVectorIconForMediaAction(MediaSessionAction action)65 const gfx::VectorIcon& GetVectorIconForMediaAction(MediaSessionAction action) {
66 switch (action) {
67 case MediaSessionAction::kPreviousTrack:
68 return vector_icons::kMediaPreviousTrackIcon;
69 case MediaSessionAction::kPause:
70 return vector_icons::kPauseIcon;
71 case MediaSessionAction::kNextTrack:
72 return vector_icons::kMediaNextTrackIcon;
73 case MediaSessionAction::kPlay:
74 return vector_icons::kPlayArrowIcon;
75
76 // Actions that are not supported.
77 case MediaSessionAction::kSeekBackward:
78 case MediaSessionAction::kSeekForward:
79 case MediaSessionAction::kStop:
80 case MediaSessionAction::kSkipAd:
81 case MediaSessionAction::kSeekTo:
82 case MediaSessionAction::kScrubTo:
83 case MediaSessionAction::kEnterPictureInPicture:
84 case MediaSessionAction::kExitPictureInPicture:
85 case MediaSessionAction::kSwitchAudioDevice:
86 NOTREACHED();
87 break;
88 }
89
90 NOTREACHED();
91 return gfx::kNoneIcon;
92 }
93
GetBackgroundColor()94 SkColor GetBackgroundColor() {
95 return AshColorProvider::Get()->GetControlsLayerColor(
96 AshColorProvider::ControlsLayerType::kControlBackgroundColorInactive);
97 }
98
99 } // namespace
100
MediaActionButton(UnifiedMediaControlsController * controller,MediaSessionAction action,const base::string16 & accessible_name)101 UnifiedMediaControlsView::MediaActionButton::MediaActionButton(
102 UnifiedMediaControlsController* controller,
103 MediaSessionAction action,
104 const base::string16& accessible_name)
105 : views::ImageButton(base::BindRepeating(
106 // Handle dynamically-updated button tags without rebinding.
107 [](UnifiedMediaControlsController* controller,
108 MediaActionButton* button) {
109 controller->PerformAction(
110 media_message_center::GetActionFromButtonTag(*button));
111 },
112 controller,
113 this)),
114 action_(action) {
115 SetImageHorizontalAlignment(views::ImageButton::ALIGN_CENTER);
116 SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
117 SetPreferredSize(kMediaButtonSize);
118 SetAction(action, accessible_name);
119
120 TrayPopupUtils::ConfigureTrayPopupButton(this);
121 views::InstallCircleHighlightPathGenerator(this);
122 }
123
SetAction(MediaSessionAction action,const base::string16 & accessible_name)124 void UnifiedMediaControlsView::MediaActionButton::SetAction(
125 MediaSessionAction action,
126 const base::string16& accessible_name) {
127 action_ = action;
128 set_tag(static_cast<int>(action));
129 SetTooltipText(accessible_name);
130 UpdateVectorIcon();
131 }
132
133 std::unique_ptr<views::InkDrop>
CreateInkDrop()134 UnifiedMediaControlsView::MediaActionButton::CreateInkDrop() {
135 auto ink_drop = TrayPopupUtils::CreateInkDrop(this);
136 ink_drop->SetShowHighlightOnHover(true);
137 return ink_drop;
138 }
139
140 std::unique_ptr<views::InkDropHighlight>
CreateInkDropHighlight() const141 UnifiedMediaControlsView::MediaActionButton::CreateInkDropHighlight() const {
142 return TrayPopupUtils::CreateInkDropHighlight(this);
143 }
144
145 std::unique_ptr<views::InkDropRipple>
CreateInkDropRipple() const146 UnifiedMediaControlsView::MediaActionButton::CreateInkDropRipple() const {
147 return TrayPopupUtils::CreateInkDropRipple(
148 TrayPopupInkDropStyle::FILL_BOUNDS, this,
149 GetInkDropCenterBasedOnLastEvent());
150 }
151
OnThemeChanged()152 void UnifiedMediaControlsView::MediaActionButton::OnThemeChanged() {
153 views::ImageButton::OnThemeChanged();
154 UpdateVectorIcon();
155 focus_ring()->SetColor(AshColorProvider::Get()->GetControlsLayerColor(
156 AshColorProvider::ControlsLayerType::kFocusRingColor));
157 }
158
UpdateVectorIcon()159 void UnifiedMediaControlsView::MediaActionButton::UpdateVectorIcon() {
160 AshColorProvider::Get()->DecorateIconButton(
161 this, GetVectorIconForMediaAction(action_), /*toggled=*/false,
162 kMediaButtonIconSize);
163 }
164
UnifiedMediaControlsView(UnifiedMediaControlsController * controller)165 UnifiedMediaControlsView::UnifiedMediaControlsView(
166 UnifiedMediaControlsController* controller)
167 : views::Button(base::BindRepeating(
168 [](UnifiedMediaControlsView* view) {
169 if (!view->is_in_empty_state_)
170 view->controller_->OnMediaControlsViewClicked();
171 },
172 this)),
173 controller_(controller) {
174 SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
175 SetBackground(views::CreateRoundedRectBackground(GetBackgroundColor(),
176 kMediaControlsCornerRadius));
177 auto* box_layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
178 views::BoxLayout::Orientation::kHorizontal, kMediaControlsViewInsets,
179 kMediaControlsViewPadding));
180 box_layout->set_cross_axis_alignment(
181 views::BoxLayout::CrossAxisAlignment::kCenter);
182
183 auto artwork_view = std::make_unique<views::ImageView>();
184 artwork_view->SetPreferredSize(kArtworkSize);
185 artwork_view_ = AddChildView(std::move(artwork_view));
186 artwork_view_->SetVisible(false);
187
188 auto track_column = std::make_unique<views::View>();
189 auto* track_column_layout =
190 track_column->SetLayoutManager(std::make_unique<views::BoxLayout>(
191 views::BoxLayout::Orientation::kVertical, kTrackColumnInsets));
192 track_column_layout->set_cross_axis_alignment(
193 views::BoxLayout::CrossAxisAlignment::kStart);
194
195 auto title_row = std::make_unique<views::View>();
196 auto* title_row_layout =
197 title_row->SetLayoutManager(std::make_unique<views::BoxLayout>(
198 views::BoxLayout::Orientation::kHorizontal, gfx::Insets()));
199 title_row_layout->set_minimum_cross_axis_size(kTitleRowHeight);
200
__anonc8111bb70402(views::Label* label) 201 auto config_label = [](views::Label* label) {
202 label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
203 label->SetAutoColorReadabilityEnabled(false);
204 label->SetSubpixelRenderingEnabled(false);
205 };
206
207 title_label_ = title_row->AddChildView(std::make_unique<views::Label>());
208 config_label(title_label_);
209 title_label_->SetFontList(
210 views::Label::GetDefaultFontList().DeriveWithSizeDelta(
211 kTrackTitleFontSizeIncrease));
212
213 drop_down_icon_ =
214 title_row->AddChildView(std::make_unique<views::ImageView>());
215 drop_down_icon_->SetPreferredSize(
216 gfx::Size(kTitleRowHeight, kTitleRowHeight));
217
218 title_row_layout->SetFlexForView(title_label_, 1);
219 track_column->AddChildView(std::move(title_row));
220
221 artist_label_ = track_column->AddChildView(std::make_unique<views::Label>());
222 config_label(artist_label_);
223
224 box_layout->SetFlexForView(AddChildView(std::move(track_column)), 1);
225
226 auto button_row = std::make_unique<views::View>();
227 button_row->SetLayoutManager(std::make_unique<views::BoxLayout>(
228 views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
229 kMediaButtonsPadding));
230
231 button_row->AddChildView(std::make_unique<MediaActionButton>(
232 controller_, MediaSessionAction::kPreviousTrack,
233 l10n_util::GetStringUTF16(
234 IDS_ASH_MEDIA_NOTIFICATION_ACTION_PREVIOUS_TRACK)));
235
236 play_pause_button_ =
237 button_row->AddChildView(std::make_unique<MediaActionButton>(
238 controller_, MediaSessionAction::kPause,
239 l10n_util::GetStringUTF16(IDS_ASH_MEDIA_NOTIFICATION_ACTION_PAUSE)));
240
241 button_row->AddChildView(std::make_unique<MediaActionButton>(
242 controller_, MediaSessionAction::kNextTrack,
243 l10n_util::GetStringUTF16(IDS_ASH_MEDIA_NOTIFICATION_ACTION_NEXT_TRACK)));
244
245 button_row_ = AddChildView(std::move(button_row));
246 }
247
SetIsPlaying(bool playing)248 void UnifiedMediaControlsView::SetIsPlaying(bool playing) {
249 if (playing) {
250 play_pause_button_->SetAction(
251 MediaSessionAction::kPause,
252 l10n_util::GetStringUTF16(IDS_ASH_MEDIA_NOTIFICATION_ACTION_PAUSE));
253 } else {
254 play_pause_button_->SetAction(
255 MediaSessionAction::kPlay,
256 l10n_util::GetStringUTF16(IDS_ASH_MEDIA_NOTIFICATION_ACTION_PLAY));
257 }
258 }
259
SetArtwork(base::Optional<gfx::ImageSkia> artwork)260 void UnifiedMediaControlsView::SetArtwork(
261 base::Optional<gfx::ImageSkia> artwork) {
262 if (!artwork.has_value()) {
263 artwork_view_->SetImage(nullptr);
264 artwork_view_->SetVisible(false);
265 artwork_view_->InvalidateLayout();
266 return;
267 }
268
269 artwork_view_->SetVisible(true);
270 gfx::Size image_size = ScaleSizeToFitView(artwork->size(), kArtworkSize);
271 artwork_view_->SetImageSize(image_size);
272 artwork_view_->SetImage(*artwork);
273
274 Layout();
275 artwork_view_->SetClipPath(GetArtworkClipPath());
276 }
277
SetTitle(const base::string16 & title)278 void UnifiedMediaControlsView::SetTitle(const base::string16& title) {
279 if (title_label_->GetText() == title)
280 return;
281
282 title_label_->SetText(title);
283 SetAccessibleName(l10n_util::GetStringFUTF16(
284 IDS_ASH_QUICK_SETTINGS_BUBBLE_MEDIA_CONTROLS_ACCESSIBLE_DESCRIPTION,
285 title));
286 }
287
SetArtist(const base::string16 & artist)288 void UnifiedMediaControlsView::SetArtist(const base::string16& artist) {
289 artist_label_->SetText(artist);
290
291 if (artist_label_->GetVisible() != artist.empty())
292 return;
293
294 artist_label_->SetVisible(!artist.empty());
295 InvalidateLayout();
296 }
297
UpdateActionButtonAvailability(const base::flat_set<MediaSessionAction> & enabled_actions)298 void UnifiedMediaControlsView::UpdateActionButtonAvailability(
299 const base::flat_set<MediaSessionAction>& enabled_actions) {
300 bool should_invalidate = false;
301 for (views::View* child : button_row_->children()) {
302 views::Button* button = static_cast<views::Button*>(child);
303 bool should_show = base::Contains(
304 enabled_actions, media_message_center::GetActionFromButtonTag(*button));
305
306 should_invalidate |= should_show != button->GetVisible();
307 button->SetVisible(should_show);
308 }
309
310 if (should_invalidate)
311 button_row_->InvalidateLayout();
312 }
313
OnThemeChanged()314 void UnifiedMediaControlsView::OnThemeChanged() {
315 views::Button::OnThemeChanged();
316 auto* color_provider = AshColorProvider::Get();
317 focus_ring()->SetColor(color_provider->GetControlsLayerColor(
318 AshColorProvider::ControlsLayerType::kFocusRingColor));
319 background()->SetNativeControlColor(GetBackgroundColor());
320 title_label_->SetEnabledColor(color_provider->GetContentLayerColor(
321 AshColorProvider::ContentLayerType::kTextColorPrimary));
322 drop_down_icon_->SetImage(CreateVectorIcon(
323 kUnifiedMenuMoreIcon,
324 color_provider->GetContentLayerColor(
325 AshColorProvider::ContentLayerType::kIconColorPrimary)));
326 artist_label_->SetEnabledColor(color_provider->GetContentLayerColor(
327 AshColorProvider::ContentLayerType::kTextColorSecondary));
328 }
329
ShowEmptyState()330 void UnifiedMediaControlsView::ShowEmptyState() {
331 if (is_in_empty_state_)
332 return;
333
334 is_in_empty_state_ = true;
335
336 title_label_->SetText(
337 l10n_util::GetStringUTF16(IDS_ASH_GLOBAL_MEDIA_CONTROLS_NO_MEDIA_TEXT));
338 title_label_->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
339 AshColorProvider::ContentLayerType::kTextColorSecondary));
340 artist_label_->SetVisible(false);
341 drop_down_icon_->SetVisible(false);
342
343 for (views::View* button : button_row_->children())
344 button->SetEnabled(false);
345 InvalidateLayout();
346
347 if (!artwork_view_->GetVisible())
348 return;
349
350 artwork_view_->SetBackground(views::CreateSolidBackground(
351 AshColorProvider::Get()->GetControlsLayerColor(
352 AshColorProvider::ControlsLayerType::
353 kControlBackgroundColorInactive)));
354 artwork_view_->SetImageSize(kEmptyArtworkIconSize);
355 artwork_view_->SetImage(CreateVectorIcon(
356 kMusicNoteIcon, kEmptyArtworkIconSize.width(),
357 AshColorProvider::Get()->GetContentLayerColor(
358 AshColorProvider::ContentLayerType::kIconColorSecondary)));
359
360 artwork_view_->SetClipPath(GetArtworkClipPath());
361 }
362
OnNewMediaSession()363 void UnifiedMediaControlsView::OnNewMediaSession() {
364 if (!is_in_empty_state_)
365 return;
366
367 is_in_empty_state_ = false;
368 title_label_->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
369 AshColorProvider::ContentLayerType::kTextColorPrimary));
370 drop_down_icon_->SetVisible(true);
371
372 for (views::View* button : button_row_->children())
373 button->SetEnabled(true);
374 InvalidateLayout();
375
376 if (!artwork_view_->GetVisible())
377 return;
378 artwork_view_->SetBackground(nullptr);
379 }
380
GetArtworkClipPath()381 SkPath UnifiedMediaControlsView::GetArtworkClipPath() {
382 SkPath path;
383 path.addRoundRect(gfx::RectToSkRect(gfx::Rect(0, 0, kArtworkSize.width(),
384 kArtworkSize.height())),
385 kArtworkCornerRadius, kArtworkCornerRadius);
386 return path;
387 }
388
389 } // namespace ash
390