diff --git a/ash/ash_strings.grd b/ash/ash_strings.grd index 0efb848650ccb..da9dab463e80f 100644 --- a/ash/ash_strings.grd +++ b/ash/ash_strings.grd @@ -606,9 +606,15 @@ Style notes: <message name="IDS_ASH_STATUS_TRAY_CAST_PAUSE" desc="Label for a button in the cast notification to pause the current cast mirroring session to a Chromecast device"> Pause </message> + <message name="IDS_ASH_STATUS_TRAY_CAST_PAUSE_CASTING" desc="Label for a button in the system tray to pause the current cast mirroring session to a Chromecast device"> + Pause casting + </message> <message name="IDS_ASH_STATUS_TRAY_CAST_RESUME" desc="Label for a button in the cast notification to resume a paused cast mirroring session to a Chromecast device"> Resume </message> + <message name="IDS_ASH_STATUS_TRAY_CAST_RESUME_CASTING" desc="Label for a button in the system tray to resume a paused cast mirroring session to a Chromecast device"> + Resume casting + </message> <message name="IDS_ASH_STATUS_TRAY_QUIET_MODE_TOOLTIP" desc="The tooltip text for the status area icon to tell do-not-disturb mode is currently on."> Do Not Disturb is on </message> diff --git a/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_PAUSE_CASTING.png.sha1 b/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_PAUSE_CASTING.png.sha1 new file mode 100644 index 0000000000000..87510852cd70c --- /dev/null +++ b/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_PAUSE_CASTING.png.sha1 @@ -0,0 +1 @@ +c8647e60121c1b5a16205154492ba70f83d50d29 \ No newline at end of file diff --git a/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_RESUME_CASTING.png.sha1 b/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_RESUME_CASTING.png.sha1 new file mode 100644 index 0000000000000..fd0de7fe1c3b4 --- /dev/null +++ b/ash/ash_strings_grd/IDS_ASH_STATUS_TRAY_CAST_RESUME_CASTING.png.sha1 @@ -0,0 +1 @@ +12ca3db87ee67fdfa25fd132901a7f491ef74f29 \ No newline at end of file diff --git a/ash/resources/vector_icons/BUILD.gn b/ash/resources/vector_icons/BUILD.gn index 1861d26fda605..996d54d316d24 100644 --- a/ash/resources/vector_icons/BUILD.gn +++ b/ash/resources/vector_icons/BUILD.gn @@ -269,6 +269,8 @@ aggregate_vector_icons("ash_vector_icons") { "quick_settings_a11y_sticky_keys.icon", "quick_settings_cast.icon", "quick_settings_cast_connected.icon", + "quick_settings_circle_pause.icon", + "quick_settings_circle_play.icon", "quick_settings_circle_stop.icon", "quick_settings_left_arrow.icon", "quick_settings_managed.icon", diff --git a/ash/resources/vector_icons/quick_settings_circle_pause.icon b/ash/resources/vector_icons/quick_settings_circle_pause.icon new file mode 100644 index 0000000000000..e9272e86728f1 --- /dev/null +++ b/ash/resources/vector_icons/quick_settings_circle_pause.icon @@ -0,0 +1,45 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +CANVAS_DIMENSIONS, 20, +MOVE_TO, 7.5f, 13, +H_LINE_TO, 9, +V_LINE_TO, 7, +H_LINE_TO, 7.5f, +V_LINE_TO, 13, +CLOSE, +MOVE_TO, 11, 13, +H_LINE_TO, 12.5f, +V_LINE_TO, 7, +H_LINE_TO, 11, +V_LINE_TO, 13, +CLOSE, +MOVE_TO, 10, 18, +CUBIC_TO, 8.9f, 18, 7.87f, 17.79f, 6.9f, 17.38f, +CUBIC_TO, 5.92f, 16.96f, 5.07f, 16.39f, 4.33f, 15.67f, +CUBIC_TO, 3.61f, 14.93f, 3.04f, 14.08f, 2.63f, 13.1f, +CUBIC_TO, 2.21f, 12.13f, 2, 11.1f, 2, 10, +CUBIC_TO, 2, 8.89f, 2.21f, 7.85f, 2.63f, 6.9f, +CUBIC_TO, 3.04f, 5.92f, 3.61f, 5.08f, 4.33f, 4.35f, +CUBIC_TO, 5.07f, 3.62f, 5.92f, 3.04f, 6.9f, 2.63f, +CUBIC_TO, 7.87f, 2.21f, 8.9f, 2, 10, 2, +CUBIC_TO, 11.11f, 2, 12.15f, 2.21f, 13.1f, 2.63f, +CUBIC_TO, 14.08f, 3.04f, 14.92f, 3.62f, 15.65f, 4.35f, +CUBIC_TO, 16.38f, 5.08f, 16.96f, 5.92f, 17.38f, 6.9f, +CUBIC_TO, 17.79f, 7.85f, 18, 8.89f, 18, 10, +CUBIC_TO, 18, 11.1f, 17.79f, 12.13f, 17.38f, 13.1f, +CUBIC_TO, 16.96f, 14.08f, 16.38f, 14.93f, 15.65f, 15.67f, +CUBIC_TO, 14.92f, 16.39f, 14.08f, 16.96f, 13.1f, 17.38f, +CUBIC_TO, 12.15f, 17.79f, 11.11f, 18, 10, 18, +CLOSE, +MOVE_TO, 10, 16.5f, +CUBIC_TO, 11.81f, 16.5f, 13.34f, 15.87f, 14.6f, 14.6f, +CUBIC_TO, 15.87f, 13.34f, 16.5f, 11.81f, 16.5f, 10, +CUBIC_TO, 16.5f, 8.19f, 15.87f, 6.66f, 14.6f, 5.4f, +CUBIC_TO, 13.34f, 4.13f, 11.81f, 3.5f, 10, 3.5f, +CUBIC_TO, 8.19f, 3.5f, 6.66f, 4.13f, 5.4f, 5.4f, +CUBIC_TO, 4.13f, 6.66f, 3.5f, 8.19f, 3.5f, 10, +CUBIC_TO, 3.5f, 11.81f, 4.13f, 13.34f, 5.4f, 14.6f, +CUBIC_TO, 6.66f, 15.87f, 8.19f, 16.5f, 10, 16.5f, +CLOSE \ No newline at end of file diff --git a/ash/resources/vector_icons/quick_settings_circle_play.icon b/ash/resources/vector_icons/quick_settings_circle_play.icon new file mode 100644 index 0000000000000..aefbbc6bc4f92 --- /dev/null +++ b/ash/resources/vector_icons/quick_settings_circle_play.icon @@ -0,0 +1,38 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +CANVAS_DIMENSIONS, 20, +MOVE_TO, 8, 13.5f, +LINE_TO, 13.5f, 10, +LINE_TO, 8, 6.5f, +V_LINE_TO, 13.5f, +CLOSE, +MOVE_TO, 10, 18, +CUBIC_TO, 8.9f, 18, 7.87f, 17.79f, 6.9f, 17.38f, +CUBIC_TO, 5.92f, 16.96f, 5.07f, 16.39f, 4.33f, 15.67f, +CUBIC_TO, 3.61f, 14.93f, 3.04f, 14.08f, 2.63f, 13.1f, +CUBIC_TO, 2.21f, 12.13f, 2, 11.1f, 2, 10, +CUBIC_TO, 2, 8.89f, 2.21f, 7.85f, 2.63f, 6.9f, +CUBIC_TO, 3.04f, 5.92f, 3.61f, 5.08f, 4.33f, 4.35f, +CUBIC_TO, 5.07f, 3.62f, 5.92f, 3.04f, 6.9f, 2.63f, +CUBIC_TO, 7.87f, 2.21f, 8.9f, 2, 10, 2, +CUBIC_TO, 11.11f, 2, 12.15f, 2.21f, 13.1f, 2.63f, +CUBIC_TO, 14.08f, 3.04f, 14.92f, 3.62f, 15.65f, 4.35f, +CUBIC_TO, 16.38f, 5.08f, 16.96f, 5.92f, 17.38f, 6.9f, +CUBIC_TO, 17.79f, 7.85f, 18, 8.89f, 18, 10, +CUBIC_TO, 18, 11.1f, 17.79f, 12.13f, 17.38f, 13.1f, +CUBIC_TO, 16.96f, 14.08f, 16.38f, 14.93f, 15.65f, 15.67f, +CUBIC_TO, 14.92f, 16.39f, 14.08f, 16.96f, 13.1f, 17.38f, +CUBIC_TO, 12.15f, 17.79f, 11.11f, 18, 10, 18, +CLOSE, +MOVE_TO, 10, 16.5f, +CUBIC_TO, 11.81f, 16.5f, 13.34f, 15.87f, 14.6f, 14.6f, +CUBIC_TO, 15.87f, 13.34f, 16.5f, 11.81f, 16.5f, 10, +CUBIC_TO, 16.5f, 8.19f, 15.87f, 6.66f, 14.6f, 5.4f, +CUBIC_TO, 13.34f, 4.13f, 11.81f, 3.5f, 10, 3.5f, +CUBIC_TO, 8.19f, 3.5f, 6.66f, 4.13f, 5.4f, 5.4f, +CUBIC_TO, 4.13f, 6.66f, 3.5f, 8.19f, 3.5f, 10, +CUBIC_TO, 3.5f, 11.81f, 4.13f, 13.34f, 5.4f, 14.6f, +CUBIC_TO, 6.66f, 15.87f, 8.19f, 16.5f, 10, 16.5f, +CLOSE \ No newline at end of file diff --git a/ash/system/cast/cast_detailed_view.cc b/ash/system/cast/cast_detailed_view.cc index 577511ea630b3..aceda3c36ff92 100644 --- a/ash/system/cast/cast_detailed_view.cc +++ b/ash/system/cast/cast_detailed_view.cc @@ -35,11 +35,15 @@ #include "ui/views/border.h" #include "ui/views/controls/scroll_view.h" #include "ui/views/layout/box_layout.h" +#include "ui/views/view_class_properties.h" namespace ash { namespace { +// Extra spacing to add between cast stop buttons and the edge of the qs tray. +constexpr int kStopButtonExtraMargin = 4; + // Returns the correct vector icon for |icon_type|. Some types may be different // for branded builds. const gfx::VectorIcon& SinkIconTypeToIcon(SinkIconType icon_type) { @@ -65,6 +69,21 @@ const gfx::VectorIcon& SinkIconTypeToIcon(SinkIconType icon_type) { return kSystemMenuCastGenericIcon; } +std::unique_ptr<views::View> MakeButtonContainer() { + std::unique_ptr<views::View> button_container = + std::make_unique<views::View>(); + views::BoxLayout* manager = + button_container->SetLayoutManager(std::make_unique<views::BoxLayout>( + views::BoxLayout::Orientation::kHorizontal)); + manager->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kEnd); + manager->set_between_child_spacing(kTrayPopupLabelRightPadding); + button_container->SetProperty( + views::kMarginsKey, + gfx::Insets::TLBR(0, 0, 0, + kStopButtonExtraMargin + kQsExtraMarginsFromRightEdge)); + return button_container; +} + } // namespace CastDetailedView::CastDetailedView(DetailedViewDelegate* delegate) @@ -85,40 +104,18 @@ void CastDetailedView::CreateItems() { void CastDetailedView::OnDevicesUpdated( const std::vector<SinkAndRoute>& sinks_routes) { + sinks_and_routes_.clear(); // Add/update existing. for (const auto& device : sinks_routes) sinks_and_routes_.insert(std::make_pair(device.sink.id, device)); - // Remove non-existent sinks. Removing an element invalidates all existing - // iterators. - auto iter = sinks_and_routes_.begin(); - while (iter != sinks_and_routes_.end()) { - bool has_receiver = false; - for (auto& receiver : sinks_routes) { - if (iter->first == receiver.sink.id) - has_receiver = true; - } - - if (has_receiver) - ++iter; - else - iter = sinks_and_routes_.erase(iter); - } - // Update UI. UpdateReceiverListFromCachedData(); Layout(); } void CastDetailedView::UpdateReceiverListFromCachedData() { - // Remove all of the existing views. - view_to_sink_map_.clear(); - scroll_content()->RemoveAllChildViews(); - add_access_code_device_ = nullptr; - if (zero_state_view_) { - RemoveChildViewT(zero_state_view_.get()); - zero_state_view_ = nullptr; - } + RemoveAllViews(); // QsRevamp places items in a rounded container. const bool is_qs_revamp_enabled = features::IsQsRevampEnabled(); @@ -130,18 +127,7 @@ void CastDetailedView::UpdateReceiverListFromCachedData() { // Per product requirement, access code receiver should be shown before other // receivers. if (CastConfigController::Get()->AccessCodeCastingEnabled()) { - add_access_code_device_ = AddScrollListItem( - item_container, vector_icons::kKeyboardIcon, - l10n_util::GetStringUTF16( - IDS_ASH_STATUS_TRAY_CAST_ACCESS_CODE_CAST_CONNECT)); - if (chromeos::features::IsJellyEnabled()) { - // `views::ImageView` does not support changing the color, so set the - // image with an updated `ui::ImageModel`. - add_access_code_device_->icon()->SetImage(ui::ImageModel::FromVectorIcon( - vector_icons::kKeyboardIcon, cros_tokens::kCrosSysPrimary)); - add_access_code_device_->text_label()->SetEnabledColorId( - cros_tokens::kCrosSysPrimary); - } + AddAccessCodeCastButton(item_container); } // Add a view for each receiver. @@ -153,20 +139,10 @@ void CastDetailedView::UpdateReceiverListFromCachedData() { base::UTF8ToUTF16(sink.name)); view_to_sink_map_[container] = sink.id; - // Add a stop casting button if this machine ("local source") is casting to - // the device. See also CastNotificationController::OnDevicesUpdated(). + // Add receiver action buttons if this machine ("local source") is casting + // to the device. See also CastNotificationController::OnDevicesUpdated(). if (is_qs_revamp_enabled && !route.id.empty() && route.is_local_source) { - auto button = std::make_unique<PillButton>( - base::BindRepeating(&CastDetailedView::StopCasting, - base::Unretained(this), route.id), - l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP_CASTING), - PillButton::kDefaultWithIconLeading, &kQuickSettingsCircleStopIcon); - button->SetBackgroundColorId(cros_tokens::kCrosSysErrorContainer); - button->SetIconColorId(cros_tokens::kCrosSysError); - button->SetButtonTextColorId(cros_tokens::kCrosSysError); - container->AddRightView( - button.release(), - views::CreateEmptyBorder(gfx::Insets::TLBR(0, 0, 0, 4))); + AddReceiverActionButtons(sink, route, container, item_container); } } @@ -221,6 +197,102 @@ void CastDetailedView::StopCasting(const std::string& route_id) { CloseBubble(); // Deletes `this`. } +void CastDetailedView::FreezePressed(const std::string& route_id, + bool is_frozen) { + DCHECK(features::IsQsRevampEnabled()); + if (is_frozen) { + CastConfigController::Get()->UnfreezeRoute(route_id); + } else { + CastConfigController::Get()->FreezeRoute(route_id); + CloseBubble(); + } +} + +void CastDetailedView::RemoveAllViews() { + view_to_sink_map_.clear(); + sink_extra_views_map_.clear(); + scroll_content()->RemoveAllChildViews(); + add_access_code_device_ = nullptr; + if (zero_state_view_) { + RemoveChildViewT(zero_state_view_.get()); + zero_state_view_ = nullptr; + } +} + +void CastDetailedView::AddAccessCodeCastButton( + views::View* receiver_list_view) { + add_access_code_device_ = + AddScrollListItem(receiver_list_view, vector_icons::kKeyboardIcon, + l10n_util::GetStringUTF16( + IDS_ASH_STATUS_TRAY_CAST_ACCESS_CODE_CAST_CONNECT)); + if (chromeos::features::IsJellyEnabled()) { + // `views::ImageView` does not support changing the color, so set the + // image with an updated `ui::ImageModel`. + add_access_code_device_->icon()->SetImage(ui::ImageModel::FromVectorIcon( + vector_icons::kKeyboardIcon, cros_tokens::kCrosSysPrimary)); + add_access_code_device_->text_label()->SetEnabledColorId( + cros_tokens::kCrosSysPrimary); + } +} + +void CastDetailedView::AddReceiverActionButtons( + const CastSink& sink, + const CastRoute& route, + HoverHighlightView* receiver_view, + views::View* receiver_list_view) { + std::unique_ptr<PillButton> stop_button = CreateStopButton(route); + + // In the case that we want to show a pause/resume button, then we must + // put both buttons on a row below the cast sink. + if (route.freeze_info.can_freeze) { + std::unique_ptr<PillButton> freeze_button = CreateFreezeButton(route); + std::unique_ptr<views::View> button_container = MakeButtonContainer(); + std::vector<views::View*> extra_views; + extra_views.emplace_back( + button_container->AddChildView(std::move(freeze_button))); + extra_views.emplace_back( + button_container->AddChildView(std::move(stop_button))); + sink_extra_views_map_[sink.id] = extra_views; + + // Add the button container directly as a new row in the list of cast + // devices. Since the associated device was just added, the buttons will + // show up correctly below their associated device. + receiver_list_view->AddChildView(std::move(button_container)); + } else { + receiver_view->AddRightView(stop_button.release(), + views::CreateEmptyBorder(gfx::Insets::TLBR( + 0, 0, 0, kStopButtonExtraMargin))); + } +} + +std::unique_ptr<PillButton> CastDetailedView::CreateStopButton( + const CastRoute& route) { + std::unique_ptr<PillButton> stop_button = std::make_unique<PillButton>( + base::BindRepeating(&CastDetailedView::StopCasting, + base::Unretained(this), route.id), + l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_STOP_CASTING), + PillButton::kDefaultWithIconLeading, &kQuickSettingsCircleStopIcon); + stop_button->SetBackgroundColorId(cros_tokens::kCrosSysErrorContainer); + stop_button->SetIconColorId(cros_tokens::kCrosSysError); + stop_button->SetButtonTextColorId(cros_tokens::kCrosSysError); + return stop_button; +} + +std::unique_ptr<PillButton> CastDetailedView::CreateFreezeButton( + const CastRoute& route) { + std::unique_ptr<PillButton> freeze_button = std::make_unique<PillButton>( + base::BindRepeating(&CastDetailedView::FreezePressed, + base::Unretained(this), route.id, + route.freeze_info.is_frozen), + route.freeze_info.is_frozen + ? l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_RESUME_CASTING) + : l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_CAST_PAUSE_CASTING), + PillButton::kSecondaryWithIconLeading, + route.freeze_info.is_frozen ? &kQuickSettingsCirclePlayIcon + : &kQuickSettingsCirclePauseIcon); + return freeze_button; +} + BEGIN_METADATA(CastDetailedView, TrayDetailedView) END_METADATA diff --git a/ash/system/cast/cast_detailed_view.h b/ash/system/cast/cast_detailed_view.h index b8e21e1939ae4..4d1c7522f0d23 100644 --- a/ash/system/cast/cast_detailed_view.h +++ b/ash/system/cast/cast_detailed_view.h @@ -22,6 +22,7 @@ class View; namespace ash { class HoverHighlightView; +class PillButton; // This view displays a list of cast receivers that can be clicked on and casted // to. It is activated by clicking on the chevron inside of @@ -61,12 +62,38 @@ class ASH_EXPORT CastDetailedView : public TrayDetailedView, // Stops casting the route identified by `route_id`. void StopCasting(const std::string& route_id); + // Pauses or resumes the route given by `route_id`. + void FreezePressed(const std::string& route_id, bool is_frozen); + + // Remove all child views from CastDetailedView. + void RemoveAllViews(); + + // Adds a button which allows for adding a device using an access code. + void AddAccessCodeCastButton(views::View* receiver_list_view); + + // Adds buttons associated with a receiver so the user may perform route + // actions like stopping or pausing. + void AddReceiverActionButtons(const CastSink& sink, + const CastRoute& route, + HoverHighlightView* receiver_view, + views::View* receiver_list_view); + + // Creates a stop button which, when pressed, stops the associated `route`. + std::unique_ptr<PillButton> CreateStopButton(const CastRoute& route); + + // Creates a freeze button which, when pressed, pauses / resumes the + // associated `route`. + std::unique_ptr<PillButton> CreateFreezeButton(const CastRoute& route); + // A mapping from the sink id to the receiver/activity data. std::map<std::string, SinkAndRoute> sinks_and_routes_; // A mapping from the view pointer to the associated activity sink id. std::map<views::View*, std::string> view_to_sink_map_; + // A mapping of sink id to the associated extra views. + std::map<std::string, std::vector<views::View*>> sink_extra_views_map_; + // Special list item that, if clicked, launches the access code casting dialog raw_ptr<HoverHighlightView, ExperimentalAsh> add_access_code_device_ = nullptr; diff --git a/ash/system/cast/cast_detailed_view_unittest.cc b/ash/system/cast/cast_detailed_view_unittest.cc index 88e821eb7d06f..fb30dc4780d91 100644 --- a/ash/system/cast/cast_detailed_view_unittest.cc +++ b/ash/system/cast/cast_detailed_view_unittest.cc @@ -57,6 +57,10 @@ class CastDetailedViewTest : public AshTestBase { return views; } + std::vector<views::View*> GetExtraViewsForSink(const std::string& sink_id) { + return detailed_view_->sink_extra_views_map_[sink_id]; + } + views::View* GetZeroStateView() { return detailed_view_->zero_state_view_; } // Adds two simulated cast devices. @@ -226,4 +230,33 @@ TEST_F(CastDetailedViewTest, NoStopCastingButtonForNonLocalSource) { EXPECT_FALSE(row->right_view()); } +TEST_F(CastDetailedViewTest, FreezeButton) { + // Set up a fake sink and route, as if this Chromebook is casting to the + // device. And, the route may be frozen. + std::vector<SinkAndRoute> devices; + SinkAndRoute device; + device.sink.id = "fake_sink_id_1"; + device.sink.name = "Sink Name 1"; + device.sink.sink_icon_type = SinkIconType::kCast; + device.route.id = "fake_route_id_1"; + device.route.title = "Title 1"; + // Simulate a local source (this Chromebook). + device.route.is_local_source = true; + device.route.freeze_info.can_freeze = true; + devices.push_back(device); + OnDevicesUpdated(devices); + + std::vector<views::View*> views = GetExtraViewsForSink("fake_sink_id_1"); + ASSERT_EQ(views.size(), 2u); + auto* freeze_button = views[0]; + EXPECT_TRUE(views::IsViewClass<PillButton>(freeze_button)); + EXPECT_EQ(freeze_button->GetTooltipText(gfx::Point()), u"Pause casting"); + + // Clicking on the button pauses casting. + LeftClickOn(freeze_button); + EXPECT_EQ(cast_config_.freeze_route_count(), 1u); + EXPECT_EQ(cast_config_.freeze_route_route_id(), "fake_route_id_1"); + EXPECT_EQ(delegate_->close_bubble_call_count(), 1u); +} + } // namespace ash