Roll src/third_party/WebKit eac3800:0237a66 (svn 202606:202607)
[chromium-blink-merge.git] / ash / system / cast / tray_cast.cc
blob93c2f6f0fe4cdf2975d56a8f209d34da38518434
1 // Copyright 2015 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/cast/tray_cast.h"
7 #include "ash/session/session_state_delegate.h"
8 #include "ash/shelf/shelf_types.h"
9 #include "ash/shell.h"
10 #include "ash/system/chromeos/screen_security/screen_tray_item.h"
11 #include "ash/system/tray/fixed_sized_image_view.h"
12 #include "ash/system/tray/fixed_sized_scroll_view.h"
13 #include "ash/system/tray/hover_highlight_view.h"
14 #include "ash/system/tray/system_tray.h"
15 #include "ash/system/tray/system_tray_delegate.h"
16 #include "ash/system/tray/system_tray_notifier.h"
17 #include "ash/system/tray/throbber_view.h"
18 #include "ash/system/tray/tray_constants.h"
19 #include "ash/system/tray/tray_details_view.h"
20 #include "ash/system/tray/tray_item_more.h"
21 #include "ash/system/tray/tray_item_view.h"
22 #include "ash/system/tray/tray_popup_label_button.h"
23 #include "base/bind.h"
24 #include "grit/ash_resources.h"
25 #include "grit/ash_strings.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/image/image.h"
29 #include "ui/gfx/text_elider.h"
30 #include "ui/views/controls/button/button.h"
31 #include "ui/views/controls/image_view.h"
32 #include "ui/views/controls/label.h"
33 #include "ui/views/layout/box_layout.h"
34 #include "ui/views/layout/fill_layout.h"
36 namespace ash {
38 namespace {
40 const int kMaximumStatusStringLength = 100;
41 const int kStopButtonRightPadding = 18;
43 // Returns the active CastConfigDelegate instance.
44 ash::CastConfigDelegate* GetCastConfigDelegate() {
45 return ash::Shell::GetInstance()
46 ->system_tray_delegate()
47 ->GetCastConfigDelegate();
50 // Helper method to elide the given string to the maximum length. If a string is
51 // contains user-input and is displayed, we should elide it.
52 // TODO(jdufault): This does not properly trim unicode characters. We should
53 // implement this properly by using views::Label::SetElideBehavior(...). See
54 // crbug.com/532496.
55 base::string16 ElideString(const base::string16& text) {
56 base::string16 elided;
57 gfx::ElideString(text, kMaximumStatusStringLength, &elided);
58 return elided;
61 } // namespace
63 namespace tray {
65 // This view is displayed in the system tray when the cast extension is active.
66 // It asks the user if they want to cast the desktop. If they click on the
67 // chevron, then a detail view will replace this view where the user will
68 // actually pick the cast receiver.
69 class CastSelectDefaultView : public TrayItemMore {
70 public:
71 CastSelectDefaultView(SystemTrayItem* owner,
72 bool show_more);
73 ~CastSelectDefaultView() override;
75 private:
76 DISALLOW_COPY_AND_ASSIGN(CastSelectDefaultView);
79 CastSelectDefaultView::CastSelectDefaultView(SystemTrayItem* owner,
80 bool show_more)
81 : TrayItemMore(owner, show_more) {
82 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
84 // Update the image and label.
85 SetImage(rb.GetImageNamed(IDR_AURA_UBER_TRAY_CAST).ToImageSkia());
86 base::string16 label =
87 rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_DESKTOP);
88 SetLabel(label);
89 SetAccessibleName(label);
92 CastSelectDefaultView::~CastSelectDefaultView() {}
94 // This view is displayed when the screen is actively being casted; it allows
95 // the user to easily stop casting. It fully replaces the
96 // |CastSelectDefaultView| view inside of the |CastDuplexView|.
97 class CastCastView : public views::View, public views::ButtonListener {
98 public:
99 CastCastView();
100 ~CastCastView() override;
102 void StopCasting();
104 // Updates the label for the stop view to include information about the
105 // current device that is being casted.
106 void UpdateLabel(
107 const CastConfigDelegate::ReceiversAndActivites& receivers_activities);
109 private:
110 // Overridden from views::View.
111 int GetHeightForWidth(int width) const override;
112 void Layout() override;
114 // Overridden from views::ButtonListener.
115 void ButtonPressed(views::Button* sender, const ui::Event& event) override;
117 views::ImageView* icon_;
118 views::Label* label_;
119 TrayPopupLabelButton* stop_button_;
120 base::WeakPtrFactory<CastCastView> weak_ptr_factory_;
122 DISALLOW_COPY_AND_ASSIGN(CastCastView);
125 CastCastView::CastCastView() : weak_ptr_factory_(this) {
126 // We will initialize the primary tray view which shows a stop button here.
128 set_background(views::Background::CreateSolidBackground(kBackgroundColor));
129 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
130 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal,
131 kTrayPopupPaddingHorizontal, 0,
132 kTrayPopupPaddingBetweenItems));
133 icon_ = new FixedSizedImageView(0, kTrayPopupItemHeight);
134 icon_->SetImage(
135 bundle.GetImageNamed(IDR_AURA_UBER_TRAY_CAST_ENABLED).ToImageSkia());
136 AddChildView(icon_);
138 // The label which describes both what we are casting (ie, the desktop) and
139 // where we are casting it to.
140 label_ = new views::Label;
141 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
142 label_->SetMultiLine(true);
143 label_->SetText(
144 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN));
145 AddChildView(label_);
147 // Add the stop bottom on the far-right. We customize how this stop button is
148 // displayed inside of |Layout()|.
149 base::string16 stop_button_text =
150 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
151 IDS_ASH_STATUS_TRAY_CAST_STOP);
152 stop_button_ = new TrayPopupLabelButton(this, stop_button_text);
153 AddChildView(stop_button_);
156 CastCastView::~CastCastView() {
159 int CastCastView::GetHeightForWidth(int width) const {
160 // We are reusing the cached label_->bounds() calculation which was
161 // done inside of Layout(). Due to the way this object is initialized,
162 // Layout() will always get initially invoked with the dummy text
163 // (which will compute the proper label width) and then when we know
164 // the cast receiver we will update the label text, which will cause
165 // this method to get invoked.
166 return std::max(views::View::GetHeightForWidth(width),
167 kTrayPopupPaddingBetweenItems * 2 +
168 label_->GetHeightForWidth(label_->bounds().width()));
171 void CastCastView::Layout() {
172 views::View::Layout();
174 // Give the stop button the space it requests.
175 gfx::Size stop_size = stop_button_->GetPreferredSize();
176 gfx::Rect stop_bounds(stop_size);
177 stop_bounds.set_x(width() - stop_size.width() - kStopButtonRightPadding);
178 stop_bounds.set_y((height() - stop_size.height()) / 2);
179 stop_button_->SetBoundsRect(stop_bounds);
181 // Adjust the label's bounds in case it got cut off by |stop_button_|.
182 if (label_->bounds().Intersects(stop_button_->bounds())) {
183 gfx::Rect label_bounds = label_->bounds();
184 label_bounds.set_width(stop_button_->x() - kTrayPopupPaddingBetweenItems -
185 label_->x());
186 label_->SetBoundsRect(label_bounds);
190 void CastCastView::StopCasting() {
191 GetCastConfigDelegate()->StopCasting();
192 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
193 ash::UMA_STATUS_AREA_CAST_STOP_CAST);
196 void CastCastView::UpdateLabel(
197 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
198 for (auto& i : receivers_activities) {
199 const CastConfigDelegate::Receiver& receiver = i.receiver;
200 const CastConfigDelegate::Activity& activity = i.activity;
201 if (!activity.id.empty()) {
202 // We want to display different labels inside of the title depending on
203 // what we are actually casting - either the desktop, a tab, or a fallback
204 // that catches everything else (ie, an extension tab).
205 if (activity.tab_id == CastConfigDelegate::Activity::TabId::DESKTOP) {
206 label_->SetText(ElideString(l10n_util::GetStringFUTF16(
207 IDS_ASH_STATUS_TRAY_CAST_CAST_DESKTOP, receiver.name)));
208 } else if (activity.tab_id >= 0) {
209 label_->SetText(ElideString(l10n_util::GetStringFUTF16(
210 IDS_ASH_STATUS_TRAY_CAST_CAST_TAB, activity.title, receiver.name)));
211 } else {
212 label_->SetText(
213 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN));
216 PreferredSizeChanged();
217 Layout();
218 break;
223 void CastCastView::ButtonPressed(views::Button* sender,
224 const ui::Event& event) {
225 DCHECK(sender == stop_button_);
226 StopCasting();
229 // This view by itself does very little. It acts as a front-end for managing
230 // which of the two child views (|CastSelectDefaultView| and |CastCastView|)
231 // is active.
232 class CastDuplexView : public views::View {
233 public:
234 CastDuplexView(
235 SystemTrayItem* owner,
236 bool show_more,
237 const CastConfigDelegate::ReceiversAndActivites& receivers_activities);
238 ~CastDuplexView() override;
240 // Activate either the casting or select view.
241 void ActivateCastView();
242 void ActivateSelectView();
244 CastSelectDefaultView* select_view() { return select_view_; }
245 CastCastView* cast_view() { return cast_view_; }
247 private:
248 // Overridden from views::View.
249 void ChildPreferredSizeChanged(views::View* child) override;
250 void Layout() override;
252 // Only one of |select_view_| or |cast_view_| will be displayed at any given
253 // time. This will return the view is being displayed.
254 views::View* ActiveChildView();
256 CastSelectDefaultView* select_view_;
257 CastCastView* cast_view_;
259 DISALLOW_COPY_AND_ASSIGN(CastDuplexView);
262 CastDuplexView::CastDuplexView(
263 SystemTrayItem* owner,
264 bool show_more,
265 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
266 select_view_ = new CastSelectDefaultView(owner, show_more);
267 cast_view_ = new CastCastView();
268 cast_view_->UpdateLabel(receivers_activities);
269 SetLayoutManager(new views::FillLayout());
271 ActivateSelectView();
274 CastDuplexView::~CastDuplexView() {
275 RemoveChildView(ActiveChildView());
276 delete select_view_;
277 delete cast_view_;
280 void CastDuplexView::ActivateCastView() {
281 if (ActiveChildView() == cast_view_)
282 return;
284 RemoveChildView(select_view_);
285 AddChildView(cast_view_);
286 InvalidateLayout();
289 void CastDuplexView::ActivateSelectView() {
290 if (ActiveChildView() == select_view_)
291 return;
293 RemoveChildView(cast_view_);
294 AddChildView(select_view_);
295 InvalidateLayout();
298 void CastDuplexView::ChildPreferredSizeChanged(views::View* child) {
299 PreferredSizeChanged();
302 void CastDuplexView::Layout() {
303 views::View::Layout();
305 select_view_->SetBoundsRect(GetContentsBounds());
306 cast_view_->SetBoundsRect(GetContentsBounds());
309 views::View* CastDuplexView::ActiveChildView() {
310 if (cast_view_->parent() == this)
311 return cast_view_;
312 if (select_view_->parent() == this)
313 return select_view_;
314 return nullptr;
317 // Exposes an icon in the tray. |TrayCast| manages the visiblity of this.
318 class CastTrayView : public TrayItemView {
319 public:
320 CastTrayView(SystemTrayItem* tray_item);
321 ~CastTrayView() override;
323 // Called when the tray alignment changes so that the icon can recenter
324 // itself.
325 void UpdateAlignment(ShelfAlignment alignment);
327 private:
328 DISALLOW_COPY_AND_ASSIGN(CastTrayView);
331 CastTrayView::CastTrayView(SystemTrayItem* tray_item)
332 : TrayItemView(tray_item) {
333 CreateImageView();
335 image_view()->SetImage(ui::ResourceBundle::GetSharedInstance()
336 .GetImageNamed(IDR_AURA_UBER_TRAY_SCREENSHARE)
337 .ToImageSkia());
340 CastTrayView::~CastTrayView() {
343 void CastTrayView::UpdateAlignment(ShelfAlignment alignment) {
344 // Center the item dependent on the orientation of the shelf.
345 views::BoxLayout::Orientation layout = views::BoxLayout::kHorizontal;
346 switch (alignment) {
347 case ash::SHELF_ALIGNMENT_BOTTOM:
348 case ash::SHELF_ALIGNMENT_TOP:
349 layout = views::BoxLayout::kHorizontal;
350 break;
351 case ash::SHELF_ALIGNMENT_LEFT:
352 case ash::SHELF_ALIGNMENT_RIGHT:
353 layout = views::BoxLayout::kVertical;
354 break;
356 SetLayoutManager(new views::BoxLayout(layout, 0, 0, 0));
357 Layout();
360 // This view displays a list of cast receivers that can be clicked on and casted
361 // to. It is activated by clicking on the chevron inside of
362 // |CastSelectDefaultView|.
363 class CastDetailedView : public TrayDetailsView, public ViewClickListener {
364 public:
365 CastDetailedView(SystemTrayItem* owner,
366 user::LoginStatus login,
367 const CastConfigDelegate::ReceiversAndActivites&
368 receivers_and_activities);
369 ~CastDetailedView() override;
371 // Makes the detail view think the view associated with the given receiver_id
372 // was clicked. This will start a cast.
373 void SimulateViewClickedForTest(const std::string& receiver_id);
375 // Updates the list of available receivers.
376 void UpdateReceiverList(const CastConfigDelegate::ReceiversAndActivites&
377 new_receivers_and_activities);
379 private:
380 void CreateItems();
382 void UpdateReceiverListFromCachedData();
383 views::View* AddToReceiverList(
384 const CastConfigDelegate::ReceiverAndActivity& receiverActivity);
386 void AppendSettingsEntries();
387 void AppendHeaderEntry();
389 // Overridden from ViewClickListener.
390 void OnViewClicked(views::View* sender) override;
392 user::LoginStatus login_;
393 views::View* options_ = nullptr;
394 // A mapping from the receiver id to the receiver/activity data.
395 std::map<std::string, CastConfigDelegate::ReceiverAndActivity>
396 receivers_and_activities_;
397 // A mapping from the view pointer to the associated activity id.
398 std::map<views::View*, std::string> receiver_activity_map_;
399 base::WeakPtrFactory<CastDetailedView> weak_ptr_factory_;
401 DISALLOW_COPY_AND_ASSIGN(CastDetailedView);
404 CastDetailedView::CastDetailedView(
405 SystemTrayItem* owner,
406 user::LoginStatus login,
407 const CastConfigDelegate::ReceiversAndActivites& receivers_and_activities)
408 : TrayDetailsView(owner), login_(login), weak_ptr_factory_(this) {
409 CreateItems();
410 UpdateReceiverList(receivers_and_activities);
413 CastDetailedView::~CastDetailedView() {
416 void CastDetailedView::SimulateViewClickedForTest(
417 const std::string& receiver_id) {
418 for (auto& it : receiver_activity_map_) {
419 if (it.second == receiver_id) {
420 OnViewClicked(it.first);
421 break;
426 void CastDetailedView::CreateItems() {
427 CreateScrollableList();
428 AppendSettingsEntries();
429 AppendHeaderEntry();
432 void CastDetailedView::UpdateReceiverList(
433 const CastConfigDelegate::ReceiversAndActivites&
434 new_receivers_and_activities) {
435 // Add/update existing.
436 for (auto i = new_receivers_and_activities.begin();
437 i != new_receivers_and_activities.end(); ++i) {
438 receivers_and_activities_[i->receiver.id] = *i;
441 // Remove non-existent receivers. Removing an element invalidates all existing
442 // iterators.
443 auto i = receivers_and_activities_.begin();
444 while (i != receivers_and_activities_.end()) {
445 bool has_receiver = false;
446 for (auto receiver : new_receivers_and_activities) {
447 if (i->first == receiver.receiver.id)
448 has_receiver = true;
451 if (has_receiver)
452 ++i;
453 else
454 i = receivers_and_activities_.erase(i);
457 // Update UI.
458 UpdateReceiverListFromCachedData();
459 Layout();
462 void CastDetailedView::UpdateReceiverListFromCachedData() {
463 // Remove all of the existing views.
464 receiver_activity_map_.clear();
465 scroll_content()->RemoveAllChildViews(true);
467 // Add a view for each receiver.
468 for (auto& it : receivers_and_activities_) {
469 const CastConfigDelegate::ReceiverAndActivity& receiver_activity =
470 it.second;
471 views::View* container = AddToReceiverList(receiver_activity);
472 receiver_activity_map_[container] = it.first;
475 scroll_content()->SizeToPreferredSize();
476 static_cast<views::View*>(scroller())->Layout();
479 views::View* CastDetailedView::AddToReceiverList(
480 const CastConfigDelegate::ReceiverAndActivity& receiverActivity) {
481 HoverHighlightView* container = new HoverHighlightView(this);
483 const gfx::ImageSkia* image =
484 ui::ResourceBundle::GetSharedInstance()
485 .GetImageNamed(IDR_AURA_UBER_TRAY_CAST_DEVICE_ICON)
486 .ToImageSkia();
487 const base::string16& name = receiverActivity.receiver.name;
488 container->AddIndentedIconAndLabel(*image, name, false);
490 scroll_content()->AddChildView(container);
491 return container;
494 void CastDetailedView::AppendSettingsEntries() {
495 // Settings requires a browser window, hide it for non logged in user.
496 const bool userAddingRunning = Shell::GetInstance()
497 ->session_state_delegate()
498 ->IsInSecondaryLoginScreen();
500 if (login_ == user::LOGGED_IN_NONE || login_ == user::LOGGED_IN_LOCKED ||
501 userAddingRunning)
502 return;
504 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
505 HoverHighlightView* container = new HoverHighlightView(this);
506 container->AddLabel(rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_OPTIONS),
507 gfx::ALIGN_LEFT, false /* highlight */);
509 AddChildView(container);
510 options_ = container;
513 void CastDetailedView::AppendHeaderEntry() {
514 CreateSpecialRow(IDS_ASH_STATUS_TRAY_CAST, this);
517 void CastDetailedView::OnViewClicked(views::View* sender) {
518 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
520 if (sender == footer()->content()) {
521 TransitionToDefaultView();
522 } else if (sender == options_) {
523 cast_config_delegate->LaunchCastOptions();
524 } else {
525 // Find the receiver we are going to cast to
526 auto it = receiver_activity_map_.find(sender);
527 if (it != receiver_activity_map_.end()) {
528 cast_config_delegate->CastToReceiver(it->second);
529 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
530 ash::UMA_STATUS_AREA_DETAILED_CAST_VIEW_LAUNCH_CAST);
535 } // namespace tray
537 TrayCast::TrayCast(SystemTray* system_tray)
538 : SystemTrayItem(system_tray),
539 weak_ptr_factory_(this) {
540 Shell::GetInstance()->AddShellObserver(this);
543 TrayCast::~TrayCast() {
544 Shell::GetInstance()->RemoveShellObserver(this);
547 void TrayCast::StartCastForTest(const std::string& receiver_id) {
548 if (detailed_ != nullptr)
549 detailed_->SimulateViewClickedForTest(receiver_id);
552 void TrayCast::StopCastForTest() {
553 default_->cast_view()->StopCasting();
556 const views::View* TrayCast::GetDefaultView() const {
557 return default_;
560 views::View* TrayCast::CreateTrayView(user::LoginStatus status) {
561 CHECK(tray_ == nullptr);
562 tray_ = new tray::CastTrayView(this);
563 tray_->SetVisible(is_casting_);
564 return tray_;
567 views::View* TrayCast::CreateDefaultView(user::LoginStatus status) {
568 CHECK(default_ == nullptr);
570 if (HasCastExtension()) {
571 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
573 // We add the cast listener here instead of in the ctor for two reasons:
574 // - The ctor gets called too early in the initialization cycle (at least
575 // for the tests); the correct profile hasn't been setup yet.
576 // - The listener is only added if there is a cast extension. If the call
577 // below were in the ctor, then the cast tray item would not appear if the
578 // user installed the extension in an existing session.
579 if (!device_update_subscription_) {
580 device_update_subscription_ =
581 cast_config_delegate->RegisterDeviceUpdateObserver(base::Bind(
582 &TrayCast::OnReceiversUpdated, weak_ptr_factory_.GetWeakPtr()));
585 // The extension updates its view model whenever the popup is opened, so we
586 // probably should as well.
587 cast_config_delegate->RequestDeviceRefresh();
590 default_ = new tray::CastDuplexView(this, status != user::LOGGED_IN_LOCKED,
591 receivers_and_activities_);
592 default_->set_id(TRAY_VIEW);
593 default_->select_view()->set_id(SELECT_VIEW);
594 default_->cast_view()->set_id(CAST_VIEW);
596 UpdatePrimaryView();
597 return default_;
600 views::View* TrayCast::CreateDetailedView(user::LoginStatus status) {
601 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
602 ash::UMA_STATUS_AREA_DETAILED_CAST_VIEW);
603 CHECK(detailed_ == nullptr);
604 detailed_ =
605 new tray::CastDetailedView(this, status, receivers_and_activities_);
606 return detailed_;
609 void TrayCast::DestroyTrayView() {
610 tray_ = nullptr;
613 void TrayCast::DestroyDefaultView() {
614 default_ = nullptr;
617 void TrayCast::DestroyDetailedView() {
618 detailed_ = nullptr;
621 bool TrayCast::HasCastExtension() {
622 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
623 return cast_config_delegate != nullptr &&
624 cast_config_delegate->HasCastExtension();
627 void TrayCast::OnReceiversUpdated(
628 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
629 receivers_and_activities_ = receivers_activities;
631 if (default_) {
632 bool has_receivers = !receivers_and_activities_.empty();
633 default_->SetVisible(has_receivers);
634 default_->cast_view()->UpdateLabel(receivers_and_activities_);
636 if (detailed_)
637 detailed_->UpdateReceiverList(receivers_and_activities_);
640 void TrayCast::UpdatePrimaryView() {
641 if (HasCastExtension()) {
642 if (default_) {
643 if (is_casting_)
644 default_->ActivateCastView();
645 else
646 default_->ActivateSelectView();
649 if (tray_)
650 tray_->SetVisible(is_casting_);
651 } else {
652 if (default_)
653 default_->SetVisible(false);
654 if (tray_)
655 tray_->SetVisible(false);
659 void TrayCast::OnCastingSessionStartedOrStopped(bool started) {
660 is_casting_ = started;
661 UpdatePrimaryView();
664 void TrayCast::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
665 if (tray_)
666 tray_->UpdateAlignment(alignment);
669 } // namespace ash