Switch global error menu icon to vectorized MD asset
[chromium-blink-merge.git] / ash / system / cast / tray_cast.cc
blob5d9a60770eb9c164696abcf127f30d7c7290fbd5
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/views/controls/button/button.h"
30 #include "ui/views/controls/image_view.h"
31 #include "ui/views/controls/label.h"
32 #include "ui/views/layout/box_layout.h"
33 #include "ui/views/layout/fill_layout.h"
35 namespace ash {
37 namespace {
38 const int kStopButtonRightPadding = 18;
40 // Returns the active CastConfigDelegate instance.
41 ash::CastConfigDelegate* GetCastConfigDelegate() {
42 return ash::Shell::GetInstance()
43 ->system_tray_delegate()
44 ->GetCastConfigDelegate();
46 } // namespace
48 namespace tray {
50 // This view is displayed in the system tray when the cast extension is active.
51 // It asks the user if they want to cast the desktop. If they click on the
52 // chevron, then a detail view will replace this view where the user will
53 // actually pick the cast receiver.
54 class CastSelectDefaultView : public TrayItemMore {
55 public:
56 CastSelectDefaultView(SystemTrayItem* owner,
57 bool show_more);
58 ~CastSelectDefaultView() override;
60 private:
61 DISALLOW_COPY_AND_ASSIGN(CastSelectDefaultView);
64 CastSelectDefaultView::CastSelectDefaultView(SystemTrayItem* owner,
65 bool show_more)
66 : TrayItemMore(owner, show_more) {
67 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
69 // Update the image and label.
70 SetImage(rb.GetImageNamed(IDR_AURA_UBER_TRAY_CAST).ToImageSkia());
71 base::string16 label =
72 rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_DESKTOP);
73 SetLabel(label);
74 SetAccessibleName(label);
77 CastSelectDefaultView::~CastSelectDefaultView() {}
79 // This view is displayed when the screen is actively being casted; it allows
80 // the user to easily stop casting. It fully replaces the
81 // |CastSelectDefaultView| view inside of the |CastDuplexView|.
82 class CastCastView : public views::View, public views::ButtonListener {
83 public:
84 CastCastView();
85 ~CastCastView() override;
87 void StopCasting();
89 // Updates the label for the stop view to include information about the
90 // current device that is being casted.
91 void UpdateLabel(
92 const CastConfigDelegate::ReceiversAndActivites& receivers_activities);
94 private:
95 // Overridden from views::View.
96 int GetHeightForWidth(int width) const override;
97 void Layout() override;
99 // Overridden from views::ButtonListener.
100 void ButtonPressed(views::Button* sender, const ui::Event& event) override;
102 views::ImageView* icon_;
103 views::Label* label_;
104 TrayPopupLabelButton* stop_button_;
105 base::WeakPtrFactory<CastCastView> weak_ptr_factory_;
107 DISALLOW_COPY_AND_ASSIGN(CastCastView);
110 CastCastView::CastCastView() : weak_ptr_factory_(this) {
111 // We will initialize the primary tray view which shows a stop button here.
113 set_background(views::Background::CreateSolidBackground(kBackgroundColor));
114 ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
115 SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal,
116 kTrayPopupPaddingHorizontal, 0,
117 kTrayPopupPaddingBetweenItems));
118 icon_ = new FixedSizedImageView(0, kTrayPopupItemHeight);
119 icon_->SetImage(
120 bundle.GetImageNamed(IDR_AURA_UBER_TRAY_CAST_ENABLED).ToImageSkia());
121 AddChildView(icon_);
123 // The label which describes both what we are casting (ie, the desktop) and
124 // where we are casting it to.
125 label_ = new views::Label;
126 label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
127 label_->SetMultiLine(true);
128 label_->SetText(
129 bundle.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN));
130 AddChildView(label_);
132 // Add the stop bottom on the far-right. We customize how this stop button is
133 // displayed inside of |Layout()|.
134 base::string16 stop_button_text =
135 ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
136 IDS_ASH_STATUS_TRAY_CAST_STOP);
137 stop_button_ = new TrayPopupLabelButton(this, stop_button_text);
138 AddChildView(stop_button_);
141 CastCastView::~CastCastView() {
144 int CastCastView::GetHeightForWidth(int width) const {
145 // We are reusing the cached label_->bounds() calculation which was
146 // done inside of Layout(). Due to the way this object is initialized,
147 // Layout() will always get initially invoked with the dummy text
148 // (which will compute the proper label width) and then when we know
149 // the cast receiver we will update the label text, which will cause
150 // this method to get invoked.
151 return std::max(views::View::GetHeightForWidth(width),
152 GetInsets().height() +
153 label_->GetHeightForWidth(label_->bounds().width()));
156 void CastCastView::Layout() {
157 views::View::Layout();
159 // Give the stop button the space it requests.
160 gfx::Size stop_size = stop_button_->GetPreferredSize();
161 gfx::Rect stop_bounds(stop_size);
162 stop_bounds.set_x(width() - stop_size.width() - kStopButtonRightPadding);
163 stop_bounds.set_y((height() - stop_size.height()) / 2);
164 stop_button_->SetBoundsRect(stop_bounds);
166 // Adjust the label's bounds in case it got cut off by |stop_button_|.
167 if (label_->bounds().Intersects(stop_button_->bounds())) {
168 gfx::Rect label_bounds = label_->bounds();
169 label_bounds.set_width(stop_button_->x() - kTrayPopupPaddingBetweenItems -
170 label_->x());
171 label_->SetBoundsRect(label_bounds);
175 void CastCastView::StopCasting() {
176 GetCastConfigDelegate()->StopCasting();
177 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
178 ash::UMA_STATUS_AREA_CAST_STOP_CAST);
181 void CastCastView::UpdateLabel(
182 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
183 for (auto& i : receivers_activities) {
184 const CastConfigDelegate::Receiver& receiver = i.receiver;
185 const CastConfigDelegate::Activity& activity = i.activity;
186 if (!activity.id.empty()) {
187 // We want to display different labels inside of the title depending on
188 // what we are actually casting - either the desktop, a tab, or a fallback
189 // that catches everything else (ie, an extension tab).
190 if (activity.tab_id == CastConfigDelegate::Activity::TabId::DESKTOP) {
191 label_->SetText(l10n_util::GetStringFUTF16(
192 IDS_ASH_STATUS_TRAY_CAST_CAST_DESKTOP, receiver.name));
193 } else if (activity.tab_id >= 0) {
194 label_->SetText(l10n_util::GetStringFUTF16(
195 IDS_ASH_STATUS_TRAY_CAST_CAST_TAB, activity.title, receiver.name));
196 } else {
197 label_->SetText(
198 l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_CAST_UNKNOWN));
201 PreferredSizeChanged();
202 Layout();
203 break;
208 void CastCastView::ButtonPressed(views::Button* sender,
209 const ui::Event& event) {
210 DCHECK(sender == stop_button_);
211 StopCasting();
214 // This view by itself does very little. It acts as a front-end for managing
215 // which of the two child views (|CastSelectDefaultView| and |CastCastView|)
216 // is active.
217 class CastDuplexView : public views::View {
218 public:
219 CastDuplexView(
220 SystemTrayItem* owner,
221 bool show_more,
222 const CastConfigDelegate::ReceiversAndActivites& receivers_activities);
223 ~CastDuplexView() override;
225 // Activate either the casting or select view.
226 void ActivateCastView();
227 void ActivateSelectView();
229 CastSelectDefaultView* select_view() { return select_view_; }
230 CastCastView* cast_view() { return cast_view_; }
232 private:
233 // Overridden from views::View.
234 void ChildPreferredSizeChanged(views::View* child) override;
235 void Layout() override;
237 // Only one of |select_view_| or |cast_view_| will be displayed at any given
238 // time. This will return the view is being displayed.
239 views::View* ActiveChildView();
241 CastSelectDefaultView* select_view_;
242 CastCastView* cast_view_;
244 DISALLOW_COPY_AND_ASSIGN(CastDuplexView);
247 CastDuplexView::CastDuplexView(
248 SystemTrayItem* owner,
249 bool show_more,
250 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
251 select_view_ = new CastSelectDefaultView(owner, show_more);
252 cast_view_ = new CastCastView();
253 cast_view_->UpdateLabel(receivers_activities);
254 SetLayoutManager(new views::FillLayout());
256 ActivateSelectView();
259 CastDuplexView::~CastDuplexView() {
260 RemoveChildView(ActiveChildView());
261 delete select_view_;
262 delete cast_view_;
265 void CastDuplexView::ActivateCastView() {
266 if (ActiveChildView() == cast_view_)
267 return;
269 RemoveChildView(select_view_);
270 AddChildView(cast_view_);
271 InvalidateLayout();
274 void CastDuplexView::ActivateSelectView() {
275 if (ActiveChildView() == select_view_)
276 return;
278 RemoveChildView(cast_view_);
279 AddChildView(select_view_);
280 InvalidateLayout();
283 void CastDuplexView::ChildPreferredSizeChanged(views::View* child) {
284 PreferredSizeChanged();
287 void CastDuplexView::Layout() {
288 views::View::Layout();
290 select_view_->SetBoundsRect(GetContentsBounds());
291 cast_view_->SetBoundsRect(GetContentsBounds());
294 views::View* CastDuplexView::ActiveChildView() {
295 if (cast_view_->parent() == this)
296 return cast_view_;
297 if (select_view_->parent() == this)
298 return select_view_;
299 return nullptr;
302 // Exposes an icon in the tray. |TrayCast| manages the visiblity of this.
303 class CastTrayView : public TrayItemView {
304 public:
305 CastTrayView(SystemTrayItem* tray_item);
306 ~CastTrayView() override;
308 // Called when the tray alignment changes so that the icon can recenter
309 // itself.
310 void UpdateAlignment(ShelfAlignment alignment);
312 private:
313 DISALLOW_COPY_AND_ASSIGN(CastTrayView);
316 CastTrayView::CastTrayView(SystemTrayItem* tray_item)
317 : TrayItemView(tray_item) {
318 CreateImageView();
320 image_view()->SetImage(ui::ResourceBundle::GetSharedInstance()
321 .GetImageNamed(IDR_AURA_UBER_TRAY_SCREENSHARE)
322 .ToImageSkia());
325 CastTrayView::~CastTrayView() {
328 void CastTrayView::UpdateAlignment(ShelfAlignment alignment) {
329 // Center the item dependent on the orientation of the shelf.
330 views::BoxLayout::Orientation layout = views::BoxLayout::kHorizontal;
331 switch (alignment) {
332 case ash::SHELF_ALIGNMENT_BOTTOM:
333 case ash::SHELF_ALIGNMENT_TOP:
334 layout = views::BoxLayout::kHorizontal;
335 break;
336 case ash::SHELF_ALIGNMENT_LEFT:
337 case ash::SHELF_ALIGNMENT_RIGHT:
338 layout = views::BoxLayout::kVertical;
339 break;
341 SetLayoutManager(new views::BoxLayout(layout, 0, 0, 0));
342 Layout();
345 // This view displays a list of cast receivers that can be clicked on and casted
346 // to. It is activated by clicking on the chevron inside of
347 // |CastSelectDefaultView|.
348 class CastDetailedView : public TrayDetailsView, public ViewClickListener {
349 public:
350 CastDetailedView(SystemTrayItem* owner,
351 user::LoginStatus login,
352 const CastConfigDelegate::ReceiversAndActivites&
353 receivers_and_activities);
354 ~CastDetailedView() override;
356 // Makes the detail view think the view associated with the given receiver_id
357 // was clicked. This will start a cast.
358 void SimulateViewClickedForTest(const std::string& receiver_id);
360 // Updates the list of available receivers.
361 void UpdateReceiverList(const CastConfigDelegate::ReceiversAndActivites&
362 new_receivers_and_activities);
364 private:
365 void CreateItems();
367 void UpdateReceiverListFromCachedData();
368 views::View* AddToReceiverList(
369 const CastConfigDelegate::ReceiverAndActivity& receiverActivity);
371 void AppendSettingsEntries();
372 void AppendHeaderEntry();
374 // Overridden from ViewClickListener.
375 void OnViewClicked(views::View* sender) override;
377 user::LoginStatus login_;
378 views::View* options_ = nullptr;
379 // A mapping from the receiver id to the receiver/activity data.
380 std::map<std::string, CastConfigDelegate::ReceiverAndActivity>
381 receivers_and_activities_;
382 // A mapping from the view pointer to the associated activity id.
383 std::map<views::View*, std::string> receiver_activity_map_;
384 base::WeakPtrFactory<CastDetailedView> weak_ptr_factory_;
386 DISALLOW_COPY_AND_ASSIGN(CastDetailedView);
389 CastDetailedView::CastDetailedView(
390 SystemTrayItem* owner,
391 user::LoginStatus login,
392 const CastConfigDelegate::ReceiversAndActivites& receivers_and_activities)
393 : TrayDetailsView(owner), login_(login), weak_ptr_factory_(this) {
394 CreateItems();
395 UpdateReceiverList(receivers_and_activities);
398 CastDetailedView::~CastDetailedView() {
401 void CastDetailedView::SimulateViewClickedForTest(
402 const std::string& receiver_id) {
403 for (auto& it : receiver_activity_map_) {
404 if (it.second == receiver_id) {
405 OnViewClicked(it.first);
406 break;
411 void CastDetailedView::CreateItems() {
412 CreateScrollableList();
413 AppendSettingsEntries();
414 AppendHeaderEntry();
417 void CastDetailedView::UpdateReceiverList(
418 const CastConfigDelegate::ReceiversAndActivites&
419 new_receivers_and_activities) {
420 // Add/update existing.
421 for (auto i = new_receivers_and_activities.begin();
422 i != new_receivers_and_activities.end(); ++i) {
423 receivers_and_activities_[i->receiver.id] = *i;
426 // Remove non-existent receivers. Removing an element invalidates all existing
427 // iterators.
428 auto i = receivers_and_activities_.begin();
429 while (i != receivers_and_activities_.end()) {
430 bool has_receiver = false;
431 for (auto receiver : new_receivers_and_activities) {
432 if (i->first == receiver.receiver.id)
433 has_receiver = true;
436 if (has_receiver)
437 ++i;
438 else
439 i = receivers_and_activities_.erase(i);
442 // Update UI.
443 UpdateReceiverListFromCachedData();
444 Layout();
447 void CastDetailedView::UpdateReceiverListFromCachedData() {
448 // Remove all of the existing views.
449 receiver_activity_map_.clear();
450 scroll_content()->RemoveAllChildViews(true);
452 // Add a view for each receiver.
453 for (auto& it : receivers_and_activities_) {
454 const CastConfigDelegate::ReceiverAndActivity& receiver_activity =
455 it.second;
456 views::View* container = AddToReceiverList(receiver_activity);
457 receiver_activity_map_[container] = it.first;
460 scroll_content()->SizeToPreferredSize();
461 static_cast<views::View*>(scroller())->Layout();
464 views::View* CastDetailedView::AddToReceiverList(
465 const CastConfigDelegate::ReceiverAndActivity& receiverActivity) {
466 HoverHighlightView* container = new HoverHighlightView(this);
468 const gfx::ImageSkia* image =
469 ui::ResourceBundle::GetSharedInstance()
470 .GetImageNamed(IDR_AURA_UBER_TRAY_CAST_DEVICE_ICON)
471 .ToImageSkia();
472 const base::string16& name = receiverActivity.receiver.name;
473 container->AddIndentedIconAndLabel(*image, name, false);
475 scroll_content()->AddChildView(container);
476 return container;
479 void CastDetailedView::AppendSettingsEntries() {
480 // Settings requires a browser window, hide it for non logged in user.
481 const bool userAddingRunning = Shell::GetInstance()
482 ->session_state_delegate()
483 ->IsInSecondaryLoginScreen();
485 if (login_ == user::LOGGED_IN_NONE || login_ == user::LOGGED_IN_LOCKED ||
486 userAddingRunning)
487 return;
489 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
490 HoverHighlightView* container = new HoverHighlightView(this);
491 container->AddLabel(rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_CAST_OPTIONS),
492 gfx::ALIGN_LEFT, false /* highlight */);
494 AddChildView(container);
495 options_ = container;
498 void CastDetailedView::AppendHeaderEntry() {
499 CreateSpecialRow(IDS_ASH_STATUS_TRAY_CAST, this);
502 void CastDetailedView::OnViewClicked(views::View* sender) {
503 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
505 if (sender == footer()->content()) {
506 TransitionToDefaultView();
507 } else if (sender == options_) {
508 cast_config_delegate->LaunchCastOptions();
509 } else {
510 // Find the receiver we are going to cast to
511 auto it = receiver_activity_map_.find(sender);
512 if (it != receiver_activity_map_.end()) {
513 cast_config_delegate->CastToReceiver(it->second);
514 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
515 ash::UMA_STATUS_AREA_DETAILED_CAST_VIEW_LAUNCH_CAST);
520 } // namespace tray
522 TrayCast::TrayCast(SystemTray* system_tray)
523 : SystemTrayItem(system_tray),
524 weak_ptr_factory_(this) {
525 Shell::GetInstance()->AddShellObserver(this);
528 TrayCast::~TrayCast() {
529 Shell::GetInstance()->RemoveShellObserver(this);
532 void TrayCast::StartCastForTest(const std::string& receiver_id) {
533 if (detailed_ != nullptr)
534 detailed_->SimulateViewClickedForTest(receiver_id);
537 void TrayCast::StopCastForTest() {
538 default_->cast_view()->StopCasting();
541 const views::View* TrayCast::GetDefaultView() const {
542 return default_;
545 views::View* TrayCast::CreateTrayView(user::LoginStatus status) {
546 CHECK(tray_ == nullptr);
547 tray_ = new tray::CastTrayView(this);
548 tray_->SetVisible(is_casting_);
549 return tray_;
552 views::View* TrayCast::CreateDefaultView(user::LoginStatus status) {
553 CHECK(default_ == nullptr);
555 if (HasCastExtension()) {
556 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
558 // We add the cast listener here instead of in the ctor for two reasons:
559 // - The ctor gets called too early in the initialization cycle (at least
560 // for the tests); the correct profile hasn't been setup yet.
561 // - The listener is only added if there is a cast extension. If the call
562 // below were in the ctor, then the cast tray item would not appear if the
563 // user installed the extension in an existing session.
564 if (!device_update_subscription_) {
565 device_update_subscription_ =
566 cast_config_delegate->RegisterDeviceUpdateObserver(base::Bind(
567 &TrayCast::OnReceiversUpdated, weak_ptr_factory_.GetWeakPtr()));
570 // The extension updates its view model whenever the popup is opened, so we
571 // probably should as well.
572 cast_config_delegate->RequestDeviceRefresh();
575 default_ = new tray::CastDuplexView(this, status != user::LOGGED_IN_LOCKED,
576 receivers_and_activities_);
577 default_->set_id(TRAY_VIEW);
578 default_->select_view()->set_id(SELECT_VIEW);
579 default_->cast_view()->set_id(CAST_VIEW);
581 UpdatePrimaryView();
582 return default_;
585 views::View* TrayCast::CreateDetailedView(user::LoginStatus status) {
586 Shell::GetInstance()->metrics()->RecordUserMetricsAction(
587 ash::UMA_STATUS_AREA_DETAILED_CAST_VIEW);
588 CHECK(detailed_ == nullptr);
589 detailed_ =
590 new tray::CastDetailedView(this, status, receivers_and_activities_);
591 return detailed_;
594 void TrayCast::DestroyTrayView() {
595 tray_ = nullptr;
598 void TrayCast::DestroyDefaultView() {
599 default_ = nullptr;
602 void TrayCast::DestroyDetailedView() {
603 detailed_ = nullptr;
606 bool TrayCast::HasCastExtension() {
607 ash::CastConfigDelegate* cast_config_delegate = GetCastConfigDelegate();
608 return cast_config_delegate != nullptr &&
609 cast_config_delegate->HasCastExtension();
612 void TrayCast::OnReceiversUpdated(
613 const CastConfigDelegate::ReceiversAndActivites& receivers_activities) {
614 receivers_and_activities_ = receivers_activities;
616 if (default_) {
617 bool has_receivers = !receivers_and_activities_.empty();
618 default_->SetVisible(has_receivers);
619 default_->cast_view()->UpdateLabel(receivers_and_activities_);
621 if (detailed_)
622 detailed_->UpdateReceiverList(receivers_and_activities_);
625 void TrayCast::UpdatePrimaryView() {
626 if (HasCastExtension()) {
627 if (default_) {
628 if (is_casting_)
629 default_->ActivateCastView();
630 else
631 default_->ActivateSelectView();
634 if (tray_)
635 tray_->SetVisible(is_casting_);
636 } else {
637 if (default_)
638 default_->SetVisible(false);
639 if (tray_)
640 tray_->SetVisible(false);
644 void TrayCast::OnCastingSessionStartedOrStopped(bool started) {
645 is_casting_ = started;
646 UpdatePrimaryView();
649 void TrayCast::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
650 if (tray_)
651 tray_->UpdateAlignment(alignment);
654 } // namespace ash