0

VC UI: Add expand/collapse functionality to return to app panel

Added an expand button to the summary row of the return to app panel, as
well as the expand/collapse functionality.

Screenshot:
https://screenshot.googleplex.com/9t2oyc5Mbj7EyAN
https://screenshot.googleplex.com/3yjFusmXZfd7fhy

Fixed: b:253274147
Change-Id: I8468d07a1154fcaa234cdece1f1d9a6fc5a53dbf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4125184
Commit-Queue: Andre Le <leandre@chromium.org>
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1089774}
This commit is contained in:
Andre Le
2023-01-06 17:30:37 +00:00
committed by Chromium LUCI CQ
parent 404ab4d0b1
commit f6f8e94674
6 changed files with 277 additions and 18 deletions

@ -1454,6 +1454,12 @@ Style notes:
<message name="IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SUMMARY_TEXT" desc="The summary text in the return to app panel of the video conference panel, specifying how many video conferencing apps are in used.">
Used by <ph name="APP_COUNT">$1<ex>2</ex></ph> apps
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP" desc="The tooltip text for the expand button (when in collapsed state) of the return to app panel.">
Show apps
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_HIDE_TOOLTIP" desc="The tooltip text for the expand button (when in expanded state) of the return to app panel.">
Hide apps
</message>
<message name="IDS_ASH_VIDEO_CONFERENCE_BUBBLE_BACKGROUND_BLUR_NAME" desc="Text for name of the background blur effect in the VC bubble.">
Background Blur
</message>

@ -0,0 +1 @@
e4cb430f7ff581525dededabbbb64673d45a6a3c

@ -0,0 +1 @@
4a4454ca86cbfa55580f159db8e67021aca805da

@ -16,8 +16,12 @@
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout.h"
@ -80,18 +84,74 @@ std::u16string GetMediaAppDisplayText(
: media_app->title;
}
// A customized toggle button for the return to app panel, which rotates
// depending on the expand state.
class ReturnToAppExpandButton : public views::ImageButton,
ReturnToAppButton::Observer {
public:
ReturnToAppExpandButton(PressedCallback callback,
ReturnToAppButton* return_to_app_button)
: views::ImageButton(std::move(callback)),
return_to_app_button_(return_to_app_button) {
return_to_app_button_->AddObserver(this);
}
ReturnToAppExpandButton(const ReturnToAppExpandButton&) = delete;
ReturnToAppExpandButton& operator=(const ReturnToAppExpandButton&) = delete;
~ReturnToAppExpandButton() override {
return_to_app_button_->RemoveObserver(this);
}
// views::ImageButton:
void PaintButtonContents(gfx::Canvas* canvas) override {
// Rotate the canvas to rotate the button depending on the panel's expanded
// state.
gfx::ScopedCanvas scoped(canvas);
canvas->Translate(gfx::Vector2d(size().width() / 2, size().height() / 2));
if (!expanded_) {
canvas->sk_canvas()->rotate(180.);
}
gfx::ImageSkia image = GetImageToPaint();
canvas->DrawImageInt(image, -image.width() / 2, -image.height() / 2);
}
private:
// ReturnToAppButton::Observer:
void OnExpandedStateChanged(bool expanded) override {
if (expanded_ == expanded) {
return;
}
expanded_ = expanded;
// Repaint to rotate the button.
SchedulePaint();
}
// Indicates if this button (and also the parent panel) is in the expanded
// state.
bool expanded_ = false;
// Owned by the views hierarchy. Will be destroyed after this view since it is
// the parent.
ReturnToAppButton* const return_to_app_button_;
};
} // namespace
// -----------------------------------------------------------------------------
// ReturnToAppButton:
ReturnToAppButton::ReturnToAppButton(bool is_capturing_camera,
ReturnToAppButton::ReturnToAppButton(ReturnToAppPanel* panel,
bool is_top_row,
bool is_capturing_camera,
bool is_capturing_microphone,
bool is_capturing_screen,
const std::u16string& display_text)
: is_capturing_camera_(is_capturing_camera),
is_capturing_microphone_(is_capturing_microphone),
is_capturing_screen_(is_capturing_screen) {
is_capturing_screen_(is_capturing_screen),
panel_(panel) {
SetLayoutManager(std::make_unique<views::FlexLayout>())
->SetOrientation(views::LayoutOrientation::kHorizontal)
.SetMainAxisAlignment(views::LayoutAlignment::kCenter)
@ -100,10 +160,48 @@ ReturnToAppButton::ReturnToAppButton(bool is_capturing_camera,
gfx::Insets::TLBR(0, kReturnToAppButtonSpacing / 2, 0,
kReturnToAppButtonSpacing / 2));
AddChildView(CreateReturnToAppIconsContainer(
icons_container_ = AddChildView(CreateReturnToAppIconsContainer(
is_capturing_camera, is_capturing_microphone, is_capturing_screen));
label_ = AddChildView(std::make_unique<views::Label>(display_text));
if (is_top_row) {
auto expand_button = std::make_unique<ReturnToAppExpandButton>(
base::BindRepeating(&ReturnToAppButton::OnExpandButtonToggled,
weak_ptr_factory_.GetWeakPtr()),
this);
expand_button->SetImageModel(
views::Button::ButtonState::STATE_NORMAL,
ui::ImageModel::FromVectorIcon(kUnifiedMenuExpandIcon,
cros_tokens::kCrosSysSecondary, 16));
expand_button->SetTooltipText(l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP));
expand_button_ = AddChildView(std::move(expand_button));
}
}
ReturnToAppButton::~ReturnToAppButton() = default;
void ReturnToAppButton::AddObserver(Observer* observer) {
observer_list_.AddObserver(observer);
}
void ReturnToAppButton::RemoveObserver(Observer* observer) {
observer_list_.RemoveObserver(observer);
}
void ReturnToAppButton::OnExpandButtonToggled(const ui::Event& event) {
expanded_ = !expanded_;
for (auto& observer : observer_list_) {
observer.OnExpandedStateChanged(expanded_);
}
icons_container_->SetVisible(!expanded_);
auto tooltip_text_id =
expanded_ ? IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_HIDE_TOOLTIP
: IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP;
expand_button_->SetTooltipText(l10n_util::GetStringUTF16(tooltip_text_id));
}
// -----------------------------------------------------------------------------
@ -128,7 +226,25 @@ ReturnToAppPanel::ReturnToAppPanel() {
cros_tokens::kCrosSysSystemOnBase, kReturnToAppPanelRadius));
}
ReturnToAppPanel::~ReturnToAppPanel() = default;
ReturnToAppPanel::~ReturnToAppPanel() {
// We only need to remove observer in case that there's a summary row
// (multiple apps).
if (summary_row_view_) {
summary_row_view_->RemoveObserver(this);
}
}
void ReturnToAppPanel::OnExpandedStateChanged(bool expanded) {
for (auto* child : children()) {
// Skip the first child since we always show the summary row. Otherwise,
// show the other rows if `expanded` and vice versa.
if (child == children().front()) {
continue;
}
child->SetVisible(expanded);
}
PreferredSizeChanged();
}
void ReturnToAppPanel::AddButtonsToPanel(MediaApps apps) {
if (apps.size() < 1) {
@ -138,9 +254,14 @@ void ReturnToAppPanel::AddButtonsToPanel(MediaApps apps) {
if (apps.size() == 1) {
auto& app = apps.front();
AddChildView(std::make_unique<ReturnToAppButton>(
app->is_capturing_camera, app->is_capturing_microphone,
app->is_capturing_screen, GetMediaAppDisplayText(app)));
auto app_button = std::make_unique<ReturnToAppButton>(
/*panel=*/this,
/*is_top_row=*/true, app->is_capturing_camera,
app->is_capturing_microphone, app->is_capturing_screen,
GetMediaAppDisplayText(app));
app_button->expand_button()->SetVisible(false);
AddChildView(std::move(app_button));
return;
}
@ -150,8 +271,10 @@ void ReturnToAppPanel::AddButtonsToPanel(MediaApps apps) {
for (auto& app : apps) {
AddChildView(std::make_unique<ReturnToAppButton>(
app->is_capturing_camera, app->is_capturing_microphone,
app->is_capturing_screen, GetMediaAppDisplayText(app)));
/*panel=*/this,
/*is_top_row=*/false, app->is_capturing_camera,
app->is_capturing_microphone, app->is_capturing_screen,
GetMediaAppDisplayText(app)));
any_apps_capturing_camera |= app->is_capturing_camera;
any_apps_capturing_microphone |= app->is_capturing_microphone;
@ -162,10 +285,16 @@ void ReturnToAppPanel::AddButtonsToPanel(MediaApps apps) {
IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SUMMARY_TEXT,
static_cast<int>(apps.size()));
AddChildViewAt(std::make_unique<ReturnToAppButton>(
any_apps_capturing_camera, any_apps_capturing_microphone,
any_apps_capturing_screen, summary_text),
0);
summary_row_view_ =
AddChildViewAt(std::make_unique<ReturnToAppButton>(
/*panel=*/this,
/*is_top_row=*/true, any_apps_capturing_camera,
any_apps_capturing_microphone,
any_apps_capturing_screen, summary_text),
0);
summary_row_view_->AddObserver(this);
OnExpandedStateChanged(false);
}
} // namespace ash::video_conference

@ -9,15 +9,24 @@
#include "ash/ash_export.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list_types.h"
#include "chromeos/crosapi/mojom/video_conference.mojom-forward.h"
#include "ui/views/view.h"
namespace ui {
class Event;
} // namespace ui
namespace views {
class ImageButton;
class Label;
class View;
} // namespace views
namespace ash::video_conference {
class ReturnToAppPanel;
using MediaApps = std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr>;
// The "return to app" button that resides within the "return to app" panel,
@ -25,7 +34,20 @@ using MediaApps = std::vector<crosapi::mojom::VideoConferenceMediaAppInfoPtr>;
// button will take users to the app.
class ASH_EXPORT ReturnToAppButton : public views::View {
public:
ReturnToAppButton(bool is_capturing_camera,
class Observer : public base::CheckedObserver {
public:
~Observer() override = default;
// Called when the expanded state is changed.
virtual void OnExpandedStateChanged(bool expanded) = 0;
};
// `is_top_row` specifies if the button is in the top row of `panel`. If the
// button is in the top row, it might represent the only media app running or
// the summary row if there are multiple media apps.
ReturnToAppButton(ReturnToAppPanel* panel,
bool is_top_row,
bool is_capturing_camera,
bool is_capturing_microphone,
bool is_capturing_screen,
const std::u16string& display_text);
@ -33,31 +55,67 @@ class ASH_EXPORT ReturnToAppButton : public views::View {
ReturnToAppButton(const ReturnToAppButton&) = delete;
ReturnToAppButton& operator=(const ReturnToAppButton&) = delete;
~ReturnToAppButton() override = default;
~ReturnToAppButton() override;
// Observer functions.
void AddObserver(Observer* observer);
void RemoveObserver(Observer* observer);
bool is_capturing_camera() const { return is_capturing_camera_; }
bool is_capturing_microphone() const { return is_capturing_microphone_; }
bool is_capturing_screen() const { return is_capturing_screen_; }
bool expanded() const { return expanded_; }
views::Label* label() { return label_; }
views::View* icons_container() { return icons_container_; }
views::ImageButton* expand_button() { return expand_button_; }
private:
FRIEND_TEST_ALL_PREFIXES(ReturnToAppPanelTest, ExpandCollapse);
// Callback for `expand_button_`.
void OnExpandButtonToggled(const ui::Event& event);
// Indicates if the running app is using camera, microphone, or screen
// sharing.
const bool is_capturing_camera_;
const bool is_capturing_microphone_;
const bool is_capturing_screen_;
// Label showing the url or name of the running app. Owned by the views
// hierarchy.
// Registered observers.
base::ObserverList<Observer> observer_list_;
// Indicates if this button (and also the parent panel) is in the expanded
// state. Note that `expanded_` is only meaningful in the case that the button
// is in the top row.
bool expanded_ = false;
// The pointers below are owned by the views hierarchy.
// This panel is the parent view of this button.
ReturnToAppPanel* const panel_;
// Label showing the url or name of the running app.
views::Label* label_ = nullptr;
// The container of icons showing the state of camera/microphone/screen
// capturing of the media app.
views::View* icons_container_ = nullptr;
// The button to toggle expand/collapse the panel. Only available if the
// button is in the top row.
views::ImageButton* expand_button_ = nullptr;
base::WeakPtrFactory<ReturnToAppButton> weak_ptr_factory_{this};
};
// The "return to app" panel that resides in the video conference bubble. The
// user selects from a list of apps that are actively capturing audio/video
// and/or sharing the screen, and the selected app is brought to the top and
// focused.
class ASH_EXPORT ReturnToAppPanel : public views::View {
class ASH_EXPORT ReturnToAppPanel : public views::View,
ReturnToAppButton::Observer {
public:
ReturnToAppPanel();
ReturnToAppPanel(const ReturnToAppPanel&) = delete;
@ -65,9 +123,17 @@ class ASH_EXPORT ReturnToAppPanel : public views::View {
~ReturnToAppPanel() override;
private:
// ReturnToAppButton::Observer:
void OnExpandedStateChanged(bool expanded) override;
// Used by the ctor to add `ReturnToAppButton`(s) to the panel.
void AddButtonsToPanel(MediaApps apps);
// The view at the top of the panel, summarizing the information of all media
// apps. This pointer will be null when there's one or fewer media apps. Owned
// by the views hierarchy.
ReturnToAppButton* summary_row_view_ = nullptr;
base::WeakPtrFactory<ReturnToAppPanel> weak_ptr_factory_{this};
};

@ -12,6 +12,8 @@
#include "base/test/scoped_feature_list.h"
#include "chromeos/crosapi/mojom/video_conference.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/test/test_event.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/label.h"
namespace {
@ -103,6 +105,7 @@ TEST_F(ReturnToAppPanelTest, OneApp) {
auto* app_button =
static_cast<ReturnToAppButton*>(return_to_app_panel->children().front());
EXPECT_FALSE(app_button->expand_button()->GetVisible());
VerifyReturnToAppButtonInfo(app_button, is_capturing_camera,
is_capturing_microphone, is_capturing_screen,
kExpectedGoogleMeetDisplayedUrl);
@ -155,4 +158,57 @@ TEST_F(ReturnToAppPanelTest, MultipleApps) {
/*is_capturing_screen=*/true, u"Zoom");
}
TEST_F(ReturnToAppPanelTest, ExpandCollapse) {
controller()->ClearMediaApps();
controller()->AddMediaApp(crosapi::mojom::VideoConferenceMediaAppInfo::New(
/*id=*/base::UnguessableToken::Create(),
/*last_activity_time=*/base::Time::Now(),
/*is_capturing_camera=*/true, /*is_capturing_microphone=*/false,
/*is_capturing_screen=*/false, /*title=*/u"Google Meet",
/*url=*/GURL(kGoogleMeetTestUrl)));
controller()->AddMediaApp(crosapi::mojom::VideoConferenceMediaAppInfo::New(
/*id=*/base::UnguessableToken::Create(),
/*last_activity_time=*/base::Time::Now(),
/*is_capturing_camera=*/false, /*is_capturing_microphone=*/true,
/*is_capturing_screen=*/true, /*title=*/u"Zoom",
/*url=*/absl::nullopt));
auto return_to_app_panel = std::make_unique<ReturnToAppPanel>();
auto* summary_row =
static_cast<ReturnToAppButton*>(return_to_app_panel->children().front());
EXPECT_TRUE(summary_row->expand_button()->GetVisible());
auto* first_app_row =
static_cast<ReturnToAppButton*>(return_to_app_panel->children()[1]);
auto* second_app_row =
static_cast<ReturnToAppButton*>(return_to_app_panel->children()[2]);
// The panel should be collapsed by default.
EXPECT_FALSE(summary_row->expanded());
// Verify the views in collapsed state:
EXPECT_TRUE(summary_row->icons_container()->GetVisible());
EXPECT_EQ(l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_SHOW_TOOLTIP),
summary_row->expand_button()->GetTooltipText());
EXPECT_FALSE(first_app_row->GetVisible());
EXPECT_FALSE(second_app_row->GetVisible());
// Clicking the expand button should expand the panel.
summary_row->OnExpandButtonToggled(ui::test::TestEvent());
EXPECT_TRUE(summary_row->expanded());
// Verify the views in expanded state:
EXPECT_FALSE(summary_row->icons_container()->GetVisible());
EXPECT_EQ(l10n_util::GetStringUTF16(
IDS_ASH_VIDEO_CONFERENCE_RETURN_TO_APP_HIDE_TOOLTIP),
summary_row->expand_button()->GetTooltipText());
EXPECT_TRUE(first_app_row->GetVisible());
EXPECT_TRUE(second_app_row->GetVisible());
// Click again. Should be in collapsed state.
summary_row->OnExpandButtonToggled(ui::test::TestEvent());
EXPECT_FALSE(summary_row->expanded());
}
} // namespace ash::video_conference