0

[FreezeCast] Add freeze button in cast subpage

In the case that a casting session may be frozen, add a button for
pausing / resuming, depending on the current state of the session.

Since the pause button and stop button cannot fit together on a line
with the device name, put them on a separate line below the device.
Ensure the stop button will be aligned with the stop buttons of devices
that are not freezable.

Pause: https://screenshot.googleplex.com/5PsydzGks7oh2SJ.png
Resume: https://screenshot.googleplex.com/ALrYGRQBDWpkqnR.png

Bug: b:284516619
Change-Id: I250723afd6c3216f0e1a203c68f6aeded62c8bef
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4571349
Reviewed-by: Takumi Fujimoto <takumif@chromium.org>
Reviewed-by: Evan Stade <estade@chromium.org>
Commit-Queue: Benjamin Zielinski <bzielinski@google.com>
Cr-Commit-Position: refs/heads/main@{#1153372}
This commit is contained in:
Benjamin Zielinski
2023-06-05 18:40:37 +00:00
committed by Chromium LUCI CQ
parent c3301a2ffc
commit 2c2f718aea
9 changed files with 274 additions and 49 deletions

@ -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>

@ -0,0 +1 @@
c8647e60121c1b5a16205154492ba70f83d50d29

@ -0,0 +1 @@
12ca3db87ee67fdfa25fd132901a7f491ef74f29

@ -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",

@ -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

@ -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

@ -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

@ -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;

@ -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