0

gif_recording: Implement the recording type menu widget

Original CL was reverted [https://chromium-review.googlesource.com/c/chromium/src/+/4100287]
because they needed to revert another CL cleanly.
This is just a pure reland.

This CL adds the widget that contains the recording type selection
menu, which opens when the drop down button in the label view is
pressed.

This CL does not hook up the menu options to the capture mode
controller yet, nor does it handle clicking on those options.
These will be done in a follow-up CL.

Fixed: b:261450790
Test: Manually, Added unit tests.
Change-Id: I78e32cb84a6ee8e55898aa2b0832f7a9d20bd6df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4098820
Commit-Queue: Michele Fan <michelefan@chromium.org>
Reviewed-by: Michele Fan <michelefan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1082571}
This commit is contained in:
Ahmed Fakhry
2022-12-13 18:29:20 +00:00
committed by Chromium LUCI CQ
parent 227cd37171
commit f830c1e3c0
19 changed files with 580 additions and 77 deletions

@ -338,6 +338,8 @@ component("ash") {
"capture_mode/key_item_view.h",
"capture_mode/recording_overlay_controller.cc",
"capture_mode/recording_overlay_controller.h",
"capture_mode/recording_type_menu_view.cc",
"capture_mode/recording_type_menu_view.h",
"capture_mode/stop_recording_button_tray.cc",
"capture_mode/stop_recording_button_tray.h",
"capture_mode/user_nudge_controller.cc",
@ -2777,6 +2779,7 @@ test("ash_unittests") {
"capture_mode/capture_mode_test_util.cc",
"capture_mode/capture_mode_test_util.h",
"capture_mode/capture_mode_unittests.cc",
"capture_mode/gif_recording_unittests.cc",
"child_accounts/parent_access_controller_impl_unittest.cc",
"clipboard/clipboard_history_controller_unittest.cc",
"clipboard/clipboard_history_resource_manager_unittest.cc",

@ -4896,8 +4896,11 @@ Here are some things you can try to get started.
<message name="IDS_ASH_SCREEN_CAPTURE_LABEL_IMAGE_CAPTURE" desc="The capture label message which shows in the middle of the captured region in region image capture mode.">
Capture
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_LABEL_VIDEO_RECORD" desc="The capture label message which shows in the middel of the captured region in region video record mode or shows in the middle of the screen in fullscreen video record mode.">
Record
<message name="IDS_ASH_SCREEN_CAPTURE_LABEL_VIDEO_RECORD" desc="The capture label message which shows in the middel of the captured region in region video recording mode.">
Record video
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_LABEL_GIF_RECORD" desc="The capture label message which shows in the middel of the captured region in region GIF recording mode.">
Record GIF
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_SAVE_TO_DIALOG_TITLE" desc="The title of the folder selection dialog, where users can select a location to save their captured images and screen recordings.">
Select a folder to save to
@ -5115,6 +5118,9 @@ Here are some things you can try to get started.
<message name="IDS_ASH_SCREEN_CAPTURE_SETTINGS_A11Y_TITLE" desc="The label of window hosting the screen capture settings menu that will be read by ChromeVox when prompted for title.">
Screen capture settings
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_RECORDING_TYPE_MENU_A11Y_TITLE" desc="The label of window hosting the recording type drop down menu that will be read by ChromeVox when prompted for title.">
Recording format menu
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_SAVE_TO_GOOGLE_DRIVE" desc="The label of the menu item button for selecting the root of Google Drive to store the captured images and videos.">
Google Drive
</message>

@ -0,0 +1 @@
0a240badf861c44ad33c16f26826936a9a1301ac

@ -1 +1 @@
5cc44477bc0dde6b2231bca438cae4eebcf8bdc3
0a240badf861c44ad33c16f26826936a9a1301ac

@ -0,0 +1 @@
0a240badf861c44ad33c16f26826936a9a1301ac

@ -32,6 +32,7 @@ class CaptureButtonView : public views::View {
~CaptureButtonView() override = default;
views::LabelButton* capture_button() { return capture_button_; }
views::ImageButton* drop_down_button() { return drop_down_button_; }
// Updates the icon and text of `capture_button_`, as well as the visibility
// of the `separator_` and `drop_down_button_` depending on the current type

@ -26,6 +26,7 @@
#include "ui/gfx/geometry/transform.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
@ -144,7 +145,8 @@ class DropToStopRecordingButtonAnimation : public gfx::LinearAnimation {
CaptureLabelView::CaptureLabelView(
CaptureModeSession* capture_mode_session,
base::RepeatingClosure on_capture_button_container_pressed)
base::RepeatingClosure on_capture_button_pressed,
base::RepeatingClosure on_drop_down_button_pressed)
: capture_mode_session_(capture_mode_session) {
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
@ -155,7 +157,8 @@ CaptureLabelView::CaptureLabelView(
layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
capture_button_container_ = AddChildView(std::make_unique<CaptureButtonView>(
std::move(on_capture_button_container_pressed), base::DoNothing()));
std::move(on_capture_button_pressed),
std::move(on_drop_down_button_pressed)));
capture_button_container_->SetPaintToLayer();
capture_button_container_->layer()->SetFillsBoundsOpaquely(false);
capture_button_container_->SetNotifyEnterExitOnChild(true);
@ -179,6 +182,19 @@ bool CaptureLabelView::IsViewInteractable() const {
return capture_button_container_->GetVisible();
}
bool CaptureLabelView::IsPointOnRecordingTypeDropDownButton(
const gfx::Point& screen_location) const {
auto* drop_down_button = capture_button_container_->drop_down_button();
return drop_down_button &&
drop_down_button->GetBoundsInScreen().Contains(screen_location);
}
bool CaptureLabelView::IsRecordingTypeDropDownButtonVisible() const {
auto* drop_down_button = capture_button_container_->drop_down_button();
return capture_button_container_->GetVisible() && drop_down_button &&
drop_down_button->GetVisible();
}
void CaptureLabelView::UpdateIconAndText() {
CaptureModeController* controller = CaptureModeController::Get();
const CaptureModeSource source = controller->source();

@ -35,11 +35,24 @@ class ASH_EXPORT CaptureLabelView
METADATA_HEADER(CaptureLabelView);
CaptureLabelView(CaptureModeSession* capture_mode_session,
base::RepeatingClosure on_capture_button_container_pressed);
base::RepeatingClosure on_capture_button_pressed,
base::RepeatingClosure on_drop_down_button_pressed);
CaptureLabelView(const CaptureLabelView&) = delete;
CaptureLabelView& operator=(const CaptureLabelView&) = delete;
~CaptureLabelView() override;
CaptureButtonView* capture_button_container() {
return capture_button_container_;
}
// Returns true if the given `screen_location` is on the drop down button,
// which when clicked opens the recording type menu.
bool IsPointOnRecordingTypeDropDownButton(
const gfx::Point& screen_location) const;
// Returns true if the recording drop down button is available and visible.
bool IsRecordingTypeDropDownButtonVisible() const;
// Returns true if this view is hosting the capture button instead of just a
// label, and can be interacted with by the user. In this case, this view has
// views that are a11y highlightable.

@ -346,8 +346,12 @@ END_METADATA
// -----------------------------------------------------------------------------
// CaptureModeMenuGroup:
CaptureModeMenuGroup::CaptureModeMenuGroup(Delegate* delegate)
: CaptureModeMenuGroup(delegate, /*menu_header=*/nullptr) {}
CaptureModeMenuGroup::CaptureModeMenuGroup(
Delegate* delegate,
const gfx::Insets& inside_border_insets)
: CaptureModeMenuGroup(delegate,
/*menu_header=*/nullptr,
inside_border_insets) {}
CaptureModeMenuGroup::CaptureModeMenuGroup(Delegate* delegate,
const gfx::VectorIcon& header_icon,
@ -357,7 +361,8 @@ CaptureModeMenuGroup::CaptureModeMenuGroup(Delegate* delegate,
delegate,
std::make_unique<CaptureModeMenuHeader>(header_icon,
std::move(header_label),
managed_by_policy)) {}
managed_by_policy),
kMenuGroupPadding) {}
CaptureModeMenuGroup::~CaptureModeMenuGroup() = default;
@ -468,7 +473,8 @@ std::u16string CaptureModeMenuGroup::GetOptionLabelForTesting(
CaptureModeMenuGroup::CaptureModeMenuGroup(
Delegate* delegate,
std::unique_ptr<CaptureModeMenuHeader> menu_header)
std::unique_ptr<CaptureModeMenuHeader> menu_header,
const gfx::Insets& inside_border_insets)
: delegate_(delegate),
menu_header_(menu_header ? AddChildView(std::move(menu_header))
: nullptr),
@ -477,7 +483,7 @@ CaptureModeMenuGroup::CaptureModeMenuGroup(
options_container_->SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical));
SetLayoutManager(std::make_unique<views::BoxLayout>(
views::BoxLayout::Orientation::kVertical, kMenuGroupPadding,
views::BoxLayout::Orientation::kVertical, inside_border_insets,
kSpaceBetweenMenuItem));
}

@ -48,8 +48,10 @@ class ASH_EXPORT CaptureModeMenuGroup : public views::View {
// This version of the constructor creates a header-less menu group. Note that
// menu groups without headers is not designed for settings that are managed
// by policy.
explicit CaptureModeMenuGroup(Delegate* delegate);
// by policy. The `inside_border_insets` are used as paddings around the menu
// options and items in this group.
CaptureModeMenuGroup(Delegate* delegate,
const gfx::Insets& inside_border_insets);
// If `managed_by_policy` is true, the header of this menu group will show an
// enterprise-managed feature icon next to the `header_label`.
@ -128,7 +130,8 @@ class ASH_EXPORT CaptureModeMenuGroup : public views::View {
// Acts as a common constructor that's called by the above public
// constructors.
CaptureModeMenuGroup(Delegate* delegate,
std::unique_ptr<CaptureModeMenuHeader> menu_header);
std::unique_ptr<CaptureModeMenuHeader> menu_header,
const gfx::Insets& inside_border_insets);
// Returns the option whose ID is |option_id|, and nullptr if no such option
// exists.

@ -21,6 +21,7 @@
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/capture_mode/capture_window_observer.h"
#include "ash/capture_mode/folder_selection_dialog_controller.h"
#include "ash/capture_mode/recording_type_menu_view.h"
#include "ash/capture_mode/user_nudge_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/display/mouse_cursor_event_filter.h"
@ -42,6 +43,7 @@
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "cc/paint/paint_flags.h"
#include "ui/aura/client/aura_constants.h"
@ -853,6 +855,10 @@ void CaptureModeSession::SetSettingsMenuShown(bool shown) {
}
if (!capture_mode_settings_widget_) {
// Close the recording type menu if any. There can be only one menu visible
// at any time.
SetRecordingTypeMenuShown(false);
auto* parent = GetParentContainer(current_root_);
capture_mode_settings_widget_ = std::make_unique<views::Widget>();
MaybeDismissUserNudgeForever();
@ -907,19 +913,21 @@ void CaptureModeSession::StartCountDown(
if (!capture_label_widget_->IsVisible())
capture_label_widget_->Show();
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
label_view->StartCountDown(std::move(countdown_finished_callback));
DCHECK(capture_label_view_);
capture_label_view_->StartCountDown(std::move(countdown_finished_callback));
UpdateCaptureLabelWidgetBounds(CaptureLabelAnimation::kCountdownStart);
UpdateCursor(display::Screen::GetScreen()->GetCursorScreenPoint(),
/*is_touch=*/false);
// Fade out the capture bar, capture settings and capture toast if they exist.
// Fade out the capture bar, capture settings, recording type menu, and the
// capture toast if they exist.
std::vector<ui::Layer*> layers_to_fade_out{
capture_mode_bar_widget_->GetLayer()};
if (capture_mode_settings_widget_)
layers_to_fade_out.push_back(capture_mode_settings_widget_->GetLayer());
if (recording_type_menu_widget_)
layers_to_fade_out.push_back(recording_type_menu_widget_->GetLayer());
if (auto* toast_layer = capture_toast_controller_.MaybeGetToastLayer())
layers_to_fade_out.push_back(toast_layer);
@ -965,9 +973,8 @@ bool CaptureModeSession::IsInCountDownAnimation() const {
if (is_shutting_down_)
return false;
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
return label_view->IsInCountDownAnimation();
DCHECK(capture_label_view_);
return capture_label_view_->IsInCountDownAnimation();
}
void CaptureModeSession::OnCaptureFolderMayHaveChanged() {
@ -1124,10 +1131,12 @@ void CaptureModeSession::OnKeyEvent(ui::KeyEvent* event) {
event->StopPropagation();
*should_update_opacity_ptr = true;
// We only dismiss the settings menu or clear the focus on ESC key if the
// count down is not in progress.
// We only dismiss the settings / recording type menus or clear the focus
// on ESC key if the count down is not in progress.
const bool is_in_count_down = IsInCountDownAnimation();
if (capture_mode_settings_widget_ && !is_in_count_down)
if (recording_type_menu_widget_ && !is_in_count_down)
SetRecordingTypeMenuShown(false);
else if (capture_mode_settings_widget_ && !is_in_count_down)
SetSettingsMenuShown(false);
else if (focus_cycler_->HasFocus() && !is_in_count_down)
focus_cycler_->ClearFocus();
@ -1258,11 +1267,7 @@ void CaptureModeSession::OnDisplayMetricsChanged(
DCHECK_EQ(parent->layer(), layer()->parent());
layer()->SetBounds(parent->bounds());
// We need to update the capture bar bounds first and then settings bounds.
// The sequence matters here since settings bounds depend on capture bar
// bounds.
RefreshBarWidgetBounds();
MaybeUpdateSettingsBounds();
// Only need to update the camera preview's bounds if the capture source is
// `kFullscreen`, since `ClampCaptureRegionToRootWindowSize` will take care of
@ -1350,26 +1355,28 @@ void CaptureModeSession::UpdateCursor(const gfx::Point& location_in_screen,
// If the current mouse event is on capture label button, and capture label
// button can handle the event, show the hand mouse cursor.
DCHECK(capture_label_view_);
const bool is_event_on_capture_button =
capture_label_widget_->GetWindowBoundsInScreen().Contains(
location_in_screen) &&
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView())
->ShouldHandleEvent();
capture_label_view_->ShouldHandleEvent();
if (is_event_on_capture_button) {
cursor_setter_->UpdateCursor(ui::mojom::CursorType::kHand);
return;
}
// As long as the settings menu is open, a pointer cursor should be used as
// long as the cursor is not on top of the capture button, since clicking
// anywhere outside the bounds of either of them (the menu or the clickable
// capture button) will dismiss the menu. Also if the event is on the bar, a
// pointer will also be used, as long as the bar is visible.
// As long as the settings menu, or the recording type menu are open, a
// pointer cursor should be used as long as the cursor is not on top of the
// capture button, since clicking anywhere outside the bounds of either of
// them (the menus or the clickable capture button) will dismiss the menus.
// Also if the event is on the bar, a pointer will also be used, as long as
// the bar is visible.
const bool is_event_on_capture_bar =
capture_mode_bar_widget_->GetLayer()->GetTargetOpacity() &&
capture_mode_bar_widget_->GetWindowBoundsInScreen().Contains(
location_in_screen);
if (capture_mode_settings_widget_ || is_event_on_capture_bar) {
if (capture_mode_settings_widget_ || is_event_on_capture_bar ||
recording_type_menu_widget_) {
cursor_setter_->UpdateCursor(ui::mojom::CursorType::kPointer);
return;
}
@ -1441,6 +1448,8 @@ void CaptureModeSession::MaybeUpdateCaptureUisOpacity(
const bool is_settings_visible = capture_mode_settings_widget_ &&
capture_mode_settings_widget_->IsVisible();
const bool is_recording_type_menu_visible =
recording_type_menu_widget_ && recording_type_menu_widget_->IsVisible();
gfx::Rect capture_region = controller_->user_capture_region();
wm::ConvertRectToScreen(current_root_, &capture_region);
@ -1492,7 +1501,8 @@ void CaptureModeSession::MaybeUpdateCaptureUisOpacity(
}
if (widget == capture_label_widget_.get() &&
(is_cursor_on_top_of_widget || focus_cycler_->CaptureLabelFocused())) {
(is_cursor_on_top_of_widget || focus_cycler_->CaptureLabelFocused() ||
is_recording_type_menu_visible)) {
continue;
}
@ -1519,8 +1529,8 @@ void CaptureModeSession::OnCameraPreviewDragStarted() {
DCHECK(!controller_->is_recording_in_progress());
// If settings menu is shown at the beginning of drag, we should close it.
if (capture_mode_settings_widget_)
SetSettingsMenuShown(false);
SetSettingsMenuShown(false);
SetRecordingTypeMenuShown(false);
// Hide capture UIs while dragging camera preview.
HideAllUis();
@ -1592,6 +1602,8 @@ std::vector<views::Widget*> CaptureModeSession::GetAvailableWidgets() {
result.push_back(capture_mode_bar_widget_.get());
if (capture_label_widget_)
result.push_back(capture_label_widget_.get());
if (recording_type_menu_widget_)
result.push_back(recording_type_menu_widget_.get());
if (capture_mode_settings_widget_)
result.push_back(capture_mode_settings_widget_.get());
if (dimensions_label_widget_)
@ -1660,10 +1672,12 @@ bool CaptureModeSession::CanShowWidget(views::Widget* widget) const {
void CaptureModeSession::RefreshBarWidgetBounds() {
DCHECK(capture_mode_bar_widget_);
// We need to update the capture bar bounds first and then settings bounds.
// The sequence matters here since settings bounds depend on capture bar
// bounds.
capture_mode_bar_widget_->SetBounds(
CaptureModeBarView::GetBounds(current_root_, is_in_projector_mode_));
auto* parent = GetParentContainer(current_root_);
parent->StackChildAtTop(capture_mode_bar_widget_->GetNativeWindow());
MaybeUpdateSettingsBounds();
if (user_nudge_controller_)
user_nudge_controller_->Reposition();
capture_toast_controller_.MaybeRepositionCaptureToast();
@ -1694,6 +1708,11 @@ void CaptureModeSession::DoPerformCapture() {
controller_->PerformCapture(); // `this` can be deleted after this.
}
void CaptureModeSession::OnRecordingTypeDropDownButtonPressed() {
SetRecordingTypeMenuShown(!recording_type_menu_widget_ ||
!recording_type_menu_widget_->IsVisible());
}
gfx::Rect CaptureModeSession::GetSelectedWindowBounds() const {
auto* window = GetSelectedWindow();
return window ? window->bounds() : gfx::Rect();
@ -1723,6 +1742,8 @@ void CaptureModeSession::RefreshStackingOrder() {
widget_in_order.emplace_back(capture_label_widget_.get());
if (capture_mode_bar_widget_)
widget_in_order.emplace_back(capture_mode_bar_widget_.get());
if (recording_type_menu_widget_)
widget_in_order.emplace_back(recording_type_menu_widget_.get());
if (capture_mode_settings_widget_)
widget_in_order.emplace_back(capture_mode_settings_widget_.get());
@ -1945,9 +1966,17 @@ void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
if (ShouldCaptureLabelHandleEvent(event_target))
return;
// Also allow events that target the settings menu (if present) to go through.
if (IsEventTargetedOnSettingsMenu(*event))
// Let the recording type menu handle its events if any.
if (capture_mode_util::IsEventTargetedOnWidget(
*event, recording_type_menu_widget_.get())) {
return;
}
// Also allow events that target the settings menu (if present) to go through.
if (capture_mode_util::IsEventTargetedOnWidget(
*event, capture_mode_settings_widget_.get())) {
return;
}
// Here we know that the event doesn't target the settings menu, so if it's a
// press event, we will use it to dismiss the settings menu, unless it's on
@ -1968,6 +1997,17 @@ void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
SetSettingsMenuShown(/*shown=*/false);
}
// Similar to the above, we want a press event that is outside the recording
// type menu to close it, unless it is on on the drop down menu button if any.
const bool should_close_recording_type_menu =
is_press_event &&
!IsPointOnRecordingTypeDropDownButton(screen_location) &&
recording_type_menu_widget_;
if (should_close_recording_type_menu) {
ignore_located_events_ = true;
SetRecordingTypeMenuShown(false);
}
const bool old_ignore_located_events = ignore_located_events_;
if (ignore_located_events_) {
if (is_release_event)
@ -1975,13 +2015,16 @@ void CaptureModeSession::OnLocatedEvent(ui::LocatedEvent* event,
}
// Events targeting the capture bar should also go through.
if (IsEventTargetedOnCaptureBar(*event))
if (capture_mode_util::IsEventTargetedOnWidget(
*event, capture_mode_bar_widget_.get())) {
return;
}
event->SetHandled();
event->StopPropagation();
if (should_close_settings || old_ignore_located_events) {
if (should_close_settings || old_ignore_located_events ||
should_close_recording_type_menu) {
// Note that these ignored events have already been consumed above.
return;
}
@ -2381,18 +2424,26 @@ void CaptureModeSession::UpdateCaptureLabelWidget(
auto* parent = GetParentContainer(current_root_);
capture_label_widget_->Init(
CreateWidgetParams(parent, gfx::Rect(), "CaptureLabel"));
capture_label_widget_->SetContentsView(std::make_unique<CaptureLabelView>(
this, base::BindRepeating(&CaptureModeSession::DoPerformCapture,
base::Unretained(this))));
capture_label_view_ = capture_label_widget_->SetContentsView(
std::make_unique<CaptureLabelView>(
this,
base::BindRepeating(&CaptureModeSession::DoPerformCapture,
base::Unretained(this)),
base::BindRepeating(
&CaptureModeSession::OnRecordingTypeDropDownButtonPressed,
base::Unretained(this))));
capture_label_widget_->GetNativeWindow()->SetTitle(
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_A11Y_TITLE));
capture_label_widget_->Show();
}
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
label_view->UpdateIconAndText();
// Note that the order here matters. The bounds of the recording type menu
// widget is always relative to the bounds of the `capture_label_widget_`.
// Thus, the latter must be updated before the former. Also, the menu may need
// to close if the `label_view` becomes not interactable.
capture_label_view_->UpdateIconAndText();
UpdateCaptureLabelWidgetBounds(animation_type);
MaybeUpdateRecordingTypeMenu();
focus_cycler_->OnCaptureLabelWidgetUpdated();
}
@ -2475,10 +2526,9 @@ void CaptureModeSession::UpdateCaptureLabelWidgetBounds(
gfx::Rect CaptureModeSession::CalculateCaptureLabelWidgetBounds() {
DCHECK(capture_label_widget_);
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
DCHECK(capture_label_view_);
const gfx::Size preferred_size = label_view->GetPreferredSize();
const gfx::Size preferred_size = capture_label_view_->GetPreferredSize();
const gfx::Rect capture_bar_bounds =
capture_mode_bar_widget_->GetNativeWindow()->bounds();
@ -2581,7 +2631,7 @@ gfx::Rect CaptureModeSession::CalculateCaptureLabelWidgetBounds() {
// away from one of the edges of the selected window.
if (source == CaptureModeSource::kRegion && !is_selecting_region_ &&
!capture_region.IsEmpty()) {
if (label_view->IsInCountDownAnimation()) {
if (capture_label_view_->IsInCountDownAnimation()) {
// If countdown starts, calculate the bounds based on the old capture
// label's position, otherwise, since the countdown label bounds is
// smaller than the label bounds and may fit into the capture region even
@ -2610,9 +2660,8 @@ bool CaptureModeSession::ShouldCaptureLabelHandleEvent(
return false;
}
CaptureLabelView* label_view =
static_cast<CaptureLabelView*>(capture_label_widget_->GetContentsView());
return label_view->ShouldHandleEvent();
DCHECK(capture_label_view_);
return capture_label_view_->ShouldHandleEvent();
}
void CaptureModeSession::MaybeChangeRoot(aura::Window* new_root) {
@ -2654,8 +2703,11 @@ void CaptureModeSession::MaybeChangeRoot(aura::Window* new_root) {
UpdateCaptureRegion(gfx::Rect(), /*is_resizing=*/false, /*by_user=*/false);
UpdateRootWindowDimmers();
MaybeReparentCameraPreviewWidget();
// Changing the root window may require updating the stacking order on the new
// display.
RefreshStackingOrder();
}
void CaptureModeSession::UpdateRootWindowDimmers() {
@ -2808,18 +2860,69 @@ void CaptureModeSession::MaybeUpdateCameraPreviewBounds() {
}
}
bool CaptureModeSession::IsEventTargetedOnCaptureBar(
const ui::LocatedEvent& event) const {
DCHECK(capture_mode_bar_widget_);
auto* target = static_cast<aura::Window*>(event.target());
return capture_mode_bar_widget_->GetNativeWindow()->Contains(target);
void CaptureModeSession::SetRecordingTypeMenuShown(bool shown) {
if (!shown) {
recording_type_menu_widget_.reset();
return;
}
if (!recording_type_menu_widget_) {
DCHECK(features::IsGifRecordingEnabled());
DCHECK(capture_label_widget_);
DCHECK(capture_label_widget_->IsVisible());
// Close the settings widget if any. Only one menu at a time can be visible.
SetSettingsMenuShown(false);
auto* parent = GetParentContainer(current_root_);
recording_type_menu_widget_ = std::make_unique<views::Widget>();
MaybeDismissUserNudgeForever();
capture_toast_controller_.DismissCurrentToastIfAny();
recording_type_menu_widget_->Init(CreateWidgetParams(
parent,
RecordingTypeMenuView::GetIdealScreenBounds(
capture_label_widget_->GetWindowBoundsInScreen()),
"RecordingTypeMenuWidget"));
recording_type_menu_widget_->SetContentsView(
std::make_unique<RecordingTypeMenuView>());
auto* menu_window = recording_type_menu_widget_->GetNativeWindow();
parent->StackChildAtTop(menu_window);
menu_window->SetTitle(l10n_util::GetStringUTF16(
IDS_ASH_SCREEN_CAPTURE_RECORDING_TYPE_MENU_A11Y_TITLE));
}
recording_type_menu_widget_->Show();
}
bool CaptureModeSession::IsEventTargetedOnSettingsMenu(
const ui::LocatedEvent& event) const {
auto* target = static_cast<aura::Window*>(event.target());
return capture_mode_settings_widget_ &&
capture_mode_settings_widget_->GetNativeWindow()->Contains(target);
bool CaptureModeSession::IsPointOnRecordingTypeDropDownButton(
const gfx::Point& screen_location) const {
if (!capture_label_widget_ || !capture_label_widget_->IsVisible())
return false;
DCHECK(capture_label_view_);
return capture_label_view_->IsPointOnRecordingTypeDropDownButton(
screen_location);
}
void CaptureModeSession::MaybeUpdateRecordingTypeMenu() {
if (!recording_type_menu_widget_)
return;
// If the the drop down button becomes hidden, the recording type menu widget
// should also hide.
if (!capture_label_widget_ ||
!capture_label_view_->IsRecordingTypeDropDownButtonVisible()) {
SetRecordingTypeMenuShown(false);
return;
}
recording_type_menu_widget_->SetBounds(
RecordingTypeMenuView::GetIdealScreenBounds(
capture_label_widget_->GetWindowBoundsInScreen(),
recording_type_menu_widget_->GetContentsView()));
}
} // namespace ash

@ -10,6 +10,7 @@
#include "ash/accessibility/magnifier/magnifier_glass.h"
#include "ash/ash_export.h"
#include "ash/capture_mode/capture_label_view.h"
#include "ash/capture_mode/capture_mode_toast_controller.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/folder_selection_dialog_controller.h"
@ -294,6 +295,10 @@ class ASH_EXPORT CaptureModeSession
// record button in the capture label view.
void DoPerformCapture();
// Called when the drop-down button in the `capture_label_widget_` is pressed
// which toggles the recording type menu on and off.
void OnRecordingTypeDropDownButtonPressed();
// Gets the bounds of current window selected for |kWindow| capture source.
gfx::Rect GetSelectedWindowBounds() const;
@ -420,12 +425,19 @@ class ASH_EXPORT CaptureModeSession
// camera preview's bounds and visibility.
void MaybeUpdateCameraPreviewBounds();
// Returns true if the given `event` is targeted on the capture bar.
bool IsEventTargetedOnCaptureBar(const ui::LocatedEvent& event) const;
// Creates or distroys the recording type menu widget based on the given
// `shown` value.
void SetRecordingTypeMenuShown(bool shown);
// Returns true if the given `event` is targeted on the setting menu if it
// exists.
bool IsEventTargetedOnSettingsMenu(const ui::LocatedEvent& event) const;
// Returns true if the given `screen_location` is on the drop down button in
// the `capture_label_widget_` which when clicked opens the recording type
// menu.
bool IsPointOnRecordingTypeDropDownButton(
const gfx::Point& screen_location) const;
// Updates the availability or bounds of the recording type menu widget
// according to the current state.
void MaybeUpdateRecordingTypeMenu();
CaptureModeController* const controller_;
@ -454,6 +466,11 @@ class ASH_EXPORT CaptureModeSession
// starting capturing, the widget will transform into a 3-second countdown
// timer.
views::UniqueWidgetPtr capture_label_widget_;
CaptureLabelView* capture_label_view_ = nullptr;
// Widget that hosts the recording type menu, from which the user can pick the
// desired recording format type.
views::UniqueWidgetPtr recording_type_menu_widget_;
// Magnifier glass used during a region capture session.
MagnifierGlass magnifier_glass_;

@ -4,13 +4,22 @@
#include "ash/capture_mode/capture_mode_session_test_api.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session.h"
namespace ash {
CaptureModeSessionTestApi::CaptureModeSessionTestApi()
: session_(CaptureModeController::Get()->capture_mode_session()) {
DCHECK(CaptureModeController::Get()->IsActive());
DCHECK(session_);
}
CaptureModeSessionTestApi::CaptureModeSessionTestApi(
CaptureModeSession* session)
: session_(session) {}
: session_(session) {
DCHECK(session_);
}
CaptureModeBarView* CaptureModeSessionTestApi::GetCaptureModeBarView() {
return session_->capture_mode_bar_view_;
@ -21,6 +30,10 @@ CaptureModeSessionTestApi::GetCaptureModeSettingsView() {
return session_->capture_mode_settings_view_;
}
CaptureLabelView* CaptureModeSessionTestApi::GetCaptureLabelView() {
return session_->capture_label_view_;
}
views::Widget* CaptureModeSessionTestApi::GetCaptureModeSettingsWidget() {
return session_->capture_mode_settings_widget_.get();
}
@ -29,6 +42,10 @@ views::Widget* CaptureModeSessionTestApi::GetCaptureLabelWidget() {
return session_->capture_label_widget_.get();
}
views::Widget* CaptureModeSessionTestApi::GetRecordingTypeMenuWidget() {
return session_->recording_type_menu_widget_.get();
}
views::Widget* CaptureModeSessionTestApi::GetDimensionsLabelWidget() {
return session_->dimensions_label_widget_.get();
}

@ -9,15 +9,17 @@
namespace ash {
class CaptureModeSession;
class CaptureLabelView;
class CaptureModeBarView;
class CaptureModeSession;
class CaptureModeSettingsView;
class UserNudgeController;
class MagnifierGlass;
class UserNudgeController;
// Wrapper for CaptureModeSession that exposes internal state to test functions.
class CaptureModeSessionTestApi {
public:
CaptureModeSessionTestApi();
explicit CaptureModeSessionTestApi(CaptureModeSession* session);
CaptureModeSessionTestApi(CaptureModeSessionTestApi&) = delete;
@ -28,10 +30,14 @@ class CaptureModeSessionTestApi {
CaptureModeSettingsView* GetCaptureModeSettingsView();
CaptureLabelView* GetCaptureLabelView();
views::Widget* GetCaptureModeSettingsWidget();
views::Widget* GetCaptureLabelWidget();
views::Widget* GetRecordingTypeMenuWidget();
views::Widget* GetDimensionsLabelWidget();
UserNudgeController* GetUserNudgeController();

@ -513,4 +513,10 @@ void MaybeUpdateMicrophonePrivacyIndicator(bool mic_on) {
}
}
bool IsEventTargetedOnWidget(const ui::LocatedEvent& event,
views::Widget* widget) {
auto* target = static_cast<aura::Window*>(event.target());
return widget && widget->GetNativeWindow()->Contains(target);
}
} // namespace ash::capture_mode_util

@ -27,6 +27,7 @@ class Transform;
namespace ui {
class Layer;
class LocatedEvent;
} // namespace ui
namespace views {
@ -189,6 +190,12 @@ ASH_EXPORT std::string GetScreenCaptureNotificationIdForPath(
void MaybeUpdateCameraPrivacyIndicator(bool camera_on);
void MaybeUpdateMicrophonePrivacyIndicator(bool mic_on);
// Returns true if the given located `event` is targeted on a window that is a
// descendant of the given `widget`. Note that `widget` can be provided as null
// if it no longer exists, in this case this function returns false.
bool IsEventTargetedOnWidget(const ui::LocatedEvent& event,
views::Widget* widget);
} // namespace capture_mode_util
} // namespace ash

@ -0,0 +1,164 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/capture_mode/capture_button_view.h"
#include "ash/capture_mode/capture_label_view.h"
#include "ash/capture_mode/capture_mode_bar_view.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session_test_api.h"
#include "ash/capture_mode/capture_mode_test_util.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/capture_mode/capture_mode_test_api.h"
#include "ash/style/icon_button.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/widget/widget.h"
namespace ash {
class GifRecordingTest : public AshTestBase {
public:
GifRecordingTest() : scoped_feature_list_(features::kGifRecording) {}
GifRecordingTest(const GifRecordingTest&) = delete;
GifRecordingTest& operator=(const GifRecordingTest&) = delete;
~GifRecordingTest() override = default;
// AshTestBase:
void SetUp() override {
AshTestBase::SetUp();
CaptureModeController::Get()->SetUserCaptureRegion(gfx::Rect(200, 200),
/*by_user=*/true);
}
CaptureModeController* StartRegionVideoCapture() {
return StartCaptureSession(CaptureModeSource::kRegion,
CaptureModeType::kVideo);
}
CaptureLabelView* GetCaptureLabelView() {
return CaptureModeSessionTestApi().GetCaptureLabelView();
}
views::Widget* GetRecordingTypeMenuWidget() {
return CaptureModeSessionTestApi().GetRecordingTypeMenuWidget();
}
views::Widget* GetSettingsMenuWidget() {
return CaptureModeSessionTestApi().GetCaptureModeSettingsWidget();
}
void ClickOnDropDownButton() {
auto* label_view = GetCaptureLabelView();
ASSERT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());
CaptureButtonView* capture_button_container =
label_view->capture_button_container();
LeftClickOn(capture_button_container->drop_down_button());
}
void ClickOnSettingsButton() {
CaptureModeBarView* bar_view =
CaptureModeSessionTestApi().GetCaptureModeBarView();
LeftClickOn(bar_view->settings_button());
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(GifRecordingTest, DropDownButtonVisibility) {
// With region video recording, the drop down button should be visible.
auto* controller = StartRegionVideoCapture();
auto* label_view = GetCaptureLabelView();
EXPECT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());
// It should hide, once we switch to image recording, but the label view
// should remain interactable.
controller->SetType(CaptureModeType::kImage);
EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
EXPECT_TRUE(label_view->IsViewInteractable());
// Switching to a fullscreen source, the label view becomes no longer
// interactable, and the drop down button remains hidden.
controller->SetSource(CaptureModeSource::kFullscreen);
EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
EXPECT_FALSE(label_view->IsViewInteractable());
// Even when we switch back to video recording.
controller->SetType(CaptureModeType::kVideo);
EXPECT_FALSE(label_view->IsRecordingTypeDropDownButtonVisible());
EXPECT_FALSE(label_view->IsViewInteractable());
// Only region recording in video mode, that the label view is interactable,
// and the button is visible.
controller->SetSource(CaptureModeSource::kRegion);
EXPECT_TRUE(label_view->IsRecordingTypeDropDownButtonVisible());
EXPECT_TRUE(label_view->IsViewInteractable());
}
TEST_F(GifRecordingTest, RecordingTypeMenuCreation) {
// The drop down button acts as a toggle.
StartRegionVideoCapture();
ClickOnDropDownButton();
EXPECT_TRUE(GetRecordingTypeMenuWidget());
ClickOnDropDownButton();
EXPECT_FALSE(GetRecordingTypeMenuWidget());
// The settings menu and the recording type menu are mutually exclusive,
// opening one closes the other.
ClickOnSettingsButton();
EXPECT_TRUE(GetSettingsMenuWidget());
ClickOnDropDownButton();
EXPECT_TRUE(GetRecordingTypeMenuWidget());
EXPECT_FALSE(GetSettingsMenuWidget());
ClickOnSettingsButton();
EXPECT_TRUE(GetSettingsMenuWidget());
EXPECT_FALSE(GetRecordingTypeMenuWidget());
}
TEST_F(GifRecordingTest, EscKeyClosesMenu) {
// Hitting the ESC key closes the recording type menu, but the session remains
// active.
auto* controller = StartRegionVideoCapture();
ClickOnDropDownButton();
EXPECT_TRUE(GetRecordingTypeMenuWidget());
PressAndReleaseKey(ui::VKEY_ESCAPE);
EXPECT_FALSE(GetRecordingTypeMenuWidget());
EXPECT_TRUE(controller->IsActive());
}
TEST_F(GifRecordingTest, EnterKeyHidesMenuAndStartsCountDown) {
StartRegionVideoCapture();
ClickOnDropDownButton();
auto* recording_type_menu = GetRecordingTypeMenuWidget();
EXPECT_TRUE(recording_type_menu);
// Pressing the ENTER key starts the recording count down, at which point, the
// menu remains open but fades out to an opacity of 0.
PressAndReleaseKey(ui::VKEY_RETURN);
EXPECT_TRUE(CaptureModeTestApi().IsInCountDownAnimation());
ASSERT_EQ(recording_type_menu, GetRecordingTypeMenuWidget());
EXPECT_FLOAT_EQ(recording_type_menu->GetLayer()->GetTargetOpacity(), 0);
}
TEST_F(GifRecordingTest, ClickingOutsideClosesMenu) {
auto* controller = StartRegionVideoCapture();
ClickOnDropDownButton();
EXPECT_TRUE(GetRecordingTypeMenuWidget());
// Clicking outside the menu widget should close it, but the region should not
// change.
const auto region = controller->user_capture_region();
auto* generator = GetEventGenerator();
generator->MoveMouseTo(region.bottom_right() + gfx::Vector2d(10, 10));
generator->ClickLeftButton();
EXPECT_FALSE(GetRecordingTypeMenuWidget());
EXPECT_EQ(region, controller->user_capture_region());
}
} // namespace ash

@ -0,0 +1,91 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/capture_mode/recording_type_menu_view.h"
#include "ash/public/cpp/style/color_provider.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/background.h"
namespace ash {
namespace {
// The IDs of the options representing the available recording formats.
enum RecordingTypeOption {
kWebM = 0,
kGif,
};
// The padding around the menu options.
constexpr auto kMenuPadding = gfx::Insets::VH(12, 0);
// The vertical space between the two nearest edges of the capture label widget
// and the recording type menu widget.
constexpr int kYOffsetFromLabelWidget = 8;
constexpr int kMinimumWidth = 184;
constexpr gfx::Size kIdealSize{kMinimumWidth, 96};
constexpr gfx::RoundedCornersF kBorderRadius{12.f};
// Gets the ideal size of the widget hosting the `RecordingTypeMenuView` either
// from the preferred size of `contents_view` (if given), or the default size.
gfx::Size GetIdealSize(views::View* contents_view) {
gfx::Size size =
contents_view ? contents_view->GetPreferredSize() : kIdealSize;
if (size.width() < kMinimumWidth)
size.set_width(kMinimumWidth);
return size;
}
} // namespace
RecordingTypeMenuView::RecordingTypeMenuView()
: CaptureModeMenuGroup(this, kMenuPadding) {
SetPaintToLayer();
SetBackground(views::CreateThemedSolidBackground(kColorAshShieldAndBase80));
layer()->SetFillsBoundsOpaquely(false);
layer()->SetRoundedCornerRadius(kBorderRadius);
layer()->SetBackgroundBlur(ColorProvider::kBackgroundBlurSigma);
layer()->SetBackdropFilterQuality(ColorProvider::kBackgroundBlurQuality);
AddOption(
&kCaptureModeVideoIcon,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_LABEL_VIDEO_RECORD),
RecordingTypeOption::kWebM);
AddOption(&kCaptureGifIcon,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_LABEL_GIF_RECORD),
RecordingTypeOption::kGif);
}
// static
gfx::Rect RecordingTypeMenuView::GetIdealScreenBounds(
const gfx::Rect& capture_label_widget_screen_bounds,
views::View* contents_view) {
const auto size = GetIdealSize(contents_view);
const auto bottom_center = capture_label_widget_screen_bounds.bottom_center();
const int y = bottom_center.y() + kYOffsetFromLabelWidget;
const int x = bottom_center.x() - (size.width() / 2);
return gfx::Rect(gfx::Point(x, y), size);
}
void RecordingTypeMenuView::OnOptionSelected(int option_id) const {}
bool RecordingTypeMenuView::IsOptionChecked(int option_id) const {
return option_id == RecordingTypeOption::kWebM;
}
bool RecordingTypeMenuView::IsOptionEnabled(int option_id) const {
return true;
}
} // namespace ash

@ -0,0 +1,42 @@
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef ASH_CAPTURE_MODE_RECORDING_TYPE_MENU_VIEW_H_
#define ASH_CAPTURE_MODE_RECORDING_TYPE_MENU_VIEW_H_
#include "ash/capture_mode/capture_mode_menu_group.h"
namespace gfx {
class Rect;
} // namespace gfx
namespace ash {
// Defines a view that will be the contents view of the recording type menu
// widget, from which users can pick the desired recording format.
class RecordingTypeMenuView : public CaptureModeMenuGroup,
public CaptureModeMenuGroup::Delegate {
public:
RecordingTypeMenuView();
RecordingTypeMenuView(const RecordingTypeMenuView&) = delete;
RecordingTypeMenuView& operator=(const RecordingTypeMenuView&) = delete;
~RecordingTypeMenuView() override = default;
// Returns the ideal bounds of the widget hosting this view, relative to the
// `capture_label_widget_screen_bounds` which hosts the drop down button that
// opens the recording type menu widget. If `contents_view` is provided, its
// preferred size will be used, otherwise, the default size will be used.
static gfx::Rect GetIdealScreenBounds(
const gfx::Rect& capture_label_widget_screen_bounds,
views::View* contents_view = nullptr);
// CaptureModeMenuGroup::Delegate:
void OnOptionSelected(int option_id) const override;
bool IsOptionChecked(int option_id) const override;
bool IsOptionEnabled(int option_id) const override;
};
} // namespace ash
#endif // ASH_CAPTURE_MODE_RECORDING_TYPE_MENU_VIEW_H_