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:

committed by
Chromium LUCI CQ

parent
404ab4d0b1
commit
f6f8e94674
@ -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
|
Reference in New Issue
Block a user