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