0

SC Education: Add Arm 2, Shortcut Tutorial

Add Arm 2 for Screen Capture Education, an extension of Arm 1's system
nudge that opens a SystemDialogDelegateView when the primary button is
clicked. The dialog widget includes a keyboard shortcut view and a
themed image of the location of the shortcut keys on a keyboard.

Bug: b/302368855
Test: manual + unit

Change-Id: I7b88e984fcd1146611c5f3fe8b47f9585860ed6c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5015111
Reviewed-by: Alex Newcomer <newcomer@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Commit-Queue: Elijah Hewer <hewer@chromium.org>
Reviewed-by: Michele Fan <michelefan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1230184}
This commit is contained in:
Elijah Hewer
2023-11-28 21:11:55 +00:00
committed by Chromium LUCI CQ
parent 976affef18
commit 956f2334de
13 changed files with 240 additions and 11 deletions

@ -6130,6 +6130,12 @@ Here are some things you can try to get started.
<message name="IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_LABEL" desc="The label of the Screen Capture Education shortcut nudge (Arm 1) for using the screenshot accelerator.">
To take a screenshot, press
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_BUTTON" desc="The label of the button in the Screen Capture Education shortcut nudge (Arm 2).">
Show me how
</message>
<message name="IDS_ASH_SCREEN_CAPTURE_EDUCATION_TUTORIAL_TITLE" desc="The title of the tutorial dialog for Screen Capture Education (Arm 2).">
Press the screenshot shortcut
</message>
<!-- Snap Group -->
<message name="IDS_ASH_SNAP_GROUP_CLICK_TO_LOCK_WINDOWS" desc="Click to lock the windows.">

@ -0,0 +1 @@
ee4cc20e712f317e42bda0793dd4973c07ffc4d6

@ -0,0 +1 @@
429ed526bbc9522910969a18a0af4da869a06b7d

@ -4,18 +4,29 @@
#include "ash/capture_mode/capture_mode_education_controller.h"
#include "ash/capture_mode/capture_mode_util.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/ash_view_ids.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/public/cpp/system/anchored_nudge_data.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_id.h"
#include "ash/style/keyboard_shortcut_view.h"
#include "ash/style/system_dialog_delegate_view.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/view.h"
namespace ash {
@ -31,6 +42,12 @@ constexpr char kCaptureModeNudgeId[] = "kCaptureModeNudge";
// Nudge styling values.
constexpr int kShortcutIconSize = 60;
// Tutorial styling values.
constexpr int kRowSpacing = 30;
constexpr int kTitleShortcutSpacing = 8;
constexpr int kImageButtonSpacing = 20;
constexpr int kKeyboardImageWidth = 448;
// Clock that can be overridden for testing.
base::Clock* g_clock_override = nullptr;
@ -42,6 +59,89 @@ base::Time GetTime() {
return g_clock_override ? g_clock_override->Now() : base::Time::Now();
}
AnchoredNudgeData CreateBaseNudgeData(NudgeCatalogName catalog_name) {
AnchoredNudgeData nudge_data(
kCaptureModeNudgeId, catalog_name,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_LABEL));
nudge_data.image_model = ui::ImageModel::FromVectorIcon(
kCaptureModeIcon, kColorAshIconColorPrimary, kShortcutIconSize);
nudge_data.keyboard_codes = {ui::VKEY_CONTROL, ui::VKEY_SHIFT,
ui::VKEY_MEDIA_LAUNCH_APP1};
return nudge_data;
}
// Creates a view containing a keyboard illustration that indicates the
// location of the keys in the screenshot keyboard shortcut.
std::unique_ptr<views::ImageView> CreateImageView() {
auto image_view = std::make_unique<views::ImageView>();
image_view->SetImage(
ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
IDR_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE));
// Rescale the image size to properly take up the width of the container.
auto image_bounds = image_view->GetImageBounds();
const float image_scale =
static_cast<float>(kKeyboardImageWidth) / image_bounds.width();
image_view->SetImageSize(
gfx::Size(kKeyboardImageWidth, image_bounds.height() * image_scale));
image_view->SetHorizontalAlignment(views::ImageViewBase::Alignment::kCenter);
image_view->SetVerticalAlignment(views::ImageViewBase::Alignment::kCenter);
image_view->SetID(VIEW_ID_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE);
return image_view;
}
// Creates a view containing a keyboard shortcut view and a keyboard
// illustration. To be used as the middle content in a
// `SystemDialogDelegateView`.
std::unique_ptr<views::TableLayoutView> CreateContentView() {
// Use a vertical table with two rows, so we can choose which cells to
// left-align.
auto content_view = std::make_unique<views::TableLayoutView>();
content_view->AddColumn(
views::LayoutAlignment::kStretch, views::LayoutAlignment::kStretch,
/*horizontal_resize=*/1.0f, views::TableLayout::ColumnSize::kUsePreferred,
/*fixed_width=*/0, /*min_width=*/0);
content_view->AddRows(1, views::TableLayout::kFixedSize);
content_view->AddPaddingRow(views::TableLayout::kFixedSize, kRowSpacing);
content_view->AddRows(1, views::TableLayout::kFixedSize);
// If the middle content of `SystemDialogDelegateView` has no top margin, it
// will automatically insert a default content padding. We want to avoid this,
// so we will set the margin ourselves.
content_view->SetProperty(
views::kMarginsKey,
gfx::Insets::TLBR(kTitleShortcutSpacing, 0, kImageButtonSpacing, 0));
// The shortcut view should be left-aligned with the title text.
const std::vector<ui::KeyboardCode> key_codes{
ui::VKEY_CONTROL, ui::VKEY_SHIFT, ui::VKEY_MEDIA_LAUNCH_APP1};
auto* shortcut_view = content_view->AddChildView(
std::make_unique<KeyboardShortcutView>(key_codes));
shortcut_view->SetProperty(views::kTableHorizAlignKey,
views::LayoutAlignment::kStart);
// Add the keyboard illustration below the keyboard shortcut.
content_view->AddChildView(CreateImageView());
return content_view;
}
// Creates a `SystemDialogDelegateView` to be used as the `WidgetDelegate` for
// the Arm 2 tutorial widget.
std::unique_ptr<SystemDialogDelegateView> CreateDialogView() {
auto dialog = std::make_unique<SystemDialogDelegateView>();
dialog->SetTitleText(l10n_util::GetStringUTF16(
IDS_ASH_SCREEN_CAPTURE_EDUCATION_TUTORIAL_TITLE));
dialog->SetMiddleContentView(CreateContentView());
dialog->SetMiddleContentAlignment(views::LayoutAlignment::kStretch);
// Override the title margins to be zero, as the space between the title and
// the shortcut view has already been set by the `content_view` margins.
dialog->SetTitleMargins(gfx::Insets());
dialog->SetAcceptButtonVisible(false);
dialog->SetModalType(ui::ModalType::MODAL_TYPE_SYSTEM);
return dialog;
}
} // namespace
CaptureModeEducationController::CaptureModeEducationController() = default;
@ -108,9 +208,17 @@ void CaptureModeEducationController::MaybeShowEducation() {
pref_service->SetTime(prefs::kCaptureModeEducationLastShown, now);
}
// Close any existing forms of education.
AnchoredNudgeManager::Get()->Cancel(kCaptureModeNudgeId);
tutorial_widget_.reset();
if (IsArm1ShortcutNudgeEnabled()) {
ShowShortcutNudge();
}
if (IsArm2ShortcutTutorialEnabled()) {
ShowTutorialNudge();
}
}
// static
@ -124,18 +232,39 @@ void CaptureModeEducationController::ShowShortcutNudge() {
auto* nudge_manager = AnchoredNudgeManager::Get();
nudge_manager->Cancel(kCaptureModeNudgeId);
AnchoredNudgeData nudge_data(
kCaptureModeNudgeId, NudgeCatalogName::kCaptureModeEducationShortcutNudge,
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_LABEL));
AnchoredNudgeData nudge_data =
CreateBaseNudgeData(NudgeCatalogName::kCaptureModeEducationShortcutNudge);
nudge_data.image_model = ui::ImageModel::FromVectorIcon(
kCaptureModeIcon, kColorAshIconColorPrimary, kShortcutIconSize);
nudge_data.keyboard_codes = {ui::VKEY_CONTROL, ui::VKEY_SHIFT,
ui::VKEY_MEDIA_LAUNCH_APP1};
AnchoredNudgeManager::Get()->Show(nudge_data);
}
// TODO(b/302368860): Add a new view to display keyboard shortcuts in the same
// style as the launcher and the new keyboard shortcut app.
nudge_manager->Show(nudge_data);
void CaptureModeEducationController::ShowTutorialNudge() {
AnchoredNudgeData nudge_data = CreateBaseNudgeData(
NudgeCatalogName::kCaptureModeEducationShortcutTutorial);
nudge_data.primary_button_text =
l10n_util::GetStringUTF16(IDS_ASH_SCREEN_CAPTURE_EDUCATION_NUDGE_BUTTON);
nudge_data.primary_button_callback = base::BindRepeating(
&CaptureModeEducationController::OnShowMeHowButtonPressed,
weak_ptr_factory_.GetWeakPtr());
AnchoredNudgeManager::Get()->Show(nudge_data);
}
void CaptureModeEducationController::CreateAndShowTutorialDialog() {
// As we are creating a system modal dialog, it will automatically be parented
// to `kShellWindowId_SystemModalContainer`.
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.delegate = CreateDialogView().release();
params.name = "CaptureModeEducationTutorialWidget";
tutorial_widget_ = std::make_unique<views::Widget>();
tutorial_widget_->Init(std::move(params));
tutorial_widget_->Show();
}
void CaptureModeEducationController::OnShowMeHowButtonPressed() {
CHECK(!tutorial_widget_);
CreateAndShowTutorialDialog();
}
} // namespace ash

@ -7,12 +7,16 @@
#include "ash/ash_export.h"
#include "base/gtest_prod_util.h"
#include "base/memory/weak_ptr.h"
#include "base/time/clock.h"
#include "ui/views/widget/unique_widget_ptr.h"
class PrefRegistrySimple;
namespace ash {
// TODO(hewer): Close tutorial when a capture session is started.
// Controller for showing the different forms of user education for Screen
// Capture entry points. Education is split into three different arms:
// - Arm 1: Shortcut Nudge. A simple system nudge appears with text indicating
@ -52,6 +56,8 @@ class ASH_EXPORT CaptureModeEducationController {
// education based on the enabled arm/feature param.
void MaybeShowEducation();
views::Widget* tutorial_widget_for_test() { return tutorial_widget_.get(); }
private:
friend class CaptureModeEducationControllerTest;
@ -62,8 +68,24 @@ class ASH_EXPORT CaptureModeEducationController {
// take a screenshot.
void ShowShortcutNudge();
// Shows Arm 2, an unanchored system nudge indicating the keyboard shortcut to
// take a screenshot, with a button to open a new tutorial widget.
void ShowTutorialNudge();
// Creates and shows the system dialog displaying the keyboard shortcut and
// illustration for taking a screenshot.
void CreateAndShowTutorialDialog();
// Closes the nudge and shows the tutorial dialog for Arm 2.
void OnShowMeHowButtonPressed();
// If set to true, ignores the 3 times/24 hours show limit for testing.
bool skip_prefs_for_test_ = false;
// The widget that contains the tutorial dialog view for Arm 2.
views::UniqueWidgetPtr tutorial_widget_;
base::WeakPtrFactory<CaptureModeEducationController> weak_ptr_factory_{this};
};
} // namespace ash

@ -36,6 +36,8 @@ constexpr char kNudgeTimeToActionWithin1h[] =
constexpr char kNudgeTimeToActionWithinSession[] =
"Ash.NotifierFramework.Nudge.TimeToAction.WithinSession";
constexpr float kKeyboardImageWidth = 448;
PrefService* GetPrefService() {
return Shell::Get()->session_controller()->GetActivePrefService();
}
@ -316,4 +318,48 @@ TEST_F(CaptureModeEducationShortcutNudgeTest, ShortcutNudgeMetrics) {
NudgeCatalogName::kCaptureModeEducationShortcutNudge, 1);
}
// Test fixture to verify the behaviour of Arm 2, the shortcut tutorial.
class CaptureModeEducationShortcutTutorialTest
: public CaptureModeEducationControllerTest {
public:
CaptureModeEducationShortcutTutorialTest()
: CaptureModeEducationControllerTest("ShortcutTutorial") {}
CaptureModeEducationShortcutTutorialTest(
const CaptureModeEducationShortcutTutorialTest&) = delete;
CaptureModeEducationShortcutTutorialTest& operator=(
const CaptureModeEducationShortcutTutorialTest&) = delete;
~CaptureModeEducationShortcutTutorialTest() override = default;
};
TEST_F(CaptureModeEducationShortcutTutorialTest, DialogShowsOnButtonPressed) {
base::SimpleTestClock test_clock;
CaptureModeEducationControllerTest::SetOverrideClock(&test_clock);
// Advance clock so we aren't at zero time.
test_clock.Advance(base::Hours(25));
// Attempt to use the Windows Snipping Tool (capture bar) shortcut.
PressAndReleaseKey(ui::VKEY_S, ui::EF_COMMAND_DOWN | ui::EF_SHIFT_DOWN);
// Click the "Show me how" button on the nudge.
AnchoredNudgeManagerImpl* nudge_manager =
Shell::Get()->anchored_nudge_manager();
LeftClickOn(nudge_manager->GetNudgePrimaryButtonForTest(kCaptureModeNudgeId));
// The nudge should not be visible.
EXPECT_FALSE(nudge_manager->GetNudgeIfShown(kCaptureModeNudgeId));
// The tutorial widget should be visible.
auto* tutorial_widget = CaptureModeController::Get()
->education_controller()
->tutorial_widget_for_test();
ASSERT_TRUE(tutorial_widget);
// The keyboard image should be scaled to the the correct width.
auto* image_view = tutorial_widget->GetContentsView()->GetViewByID(
VIEW_ID_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE);
ASSERT_TRUE(image_view);
EXPECT_EQ(kKeyboardImageWidth, image_view->width());
}
} // namespace ash

@ -225,7 +225,8 @@ enum class NudgeCatalogName {
kMultitaskMenuClamshell = 20,
kMultitaskMenuTablet = 21,
kCaptureModeEducationShortcutNudge = 22,
kMaxValue = kCaptureModeEducationShortcutNudge
kCaptureModeEducationShortcutTutorial = 23,
kMaxValue = kCaptureModeEducationShortcutTutorial
};
// A living catalog that registers toasts.

@ -101,6 +101,9 @@ enum ViewID {
VIEW_ID_SA_NOTIFICATION_TRAY,
VIEW_ID_SA_MAX = VIEW_ID_SA_NOTIFICATION_TRAY,
// Screen capture:
VIEW_ID_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE,
// System dialog delegate view:
VIEW_ID_STYLE_SYSTEM_DIALOG_DELEGATE_ACCEPT_BUTTON,
VIEW_ID_STYLE_SYSTEM_DIALOG_DELEGATE_CANCEL_BUTTON,

@ -49,6 +49,8 @@
<structure type="lottie" name="IDR_WELCOME_TOUR_DIALOG_IMAGE" file="unscaled_resources/welcome_tour_dialog_image.json" compress="gzip" />
<!-- VC Nudge -->
<structure type="lottie" name="IDR_SPEAK_ON_MUTE_OPT_IN_NUDGE_IMAGE" file="unscaled_resources/speak_on_mute_opt_in_nudge_image.json" compress="gzip" />
<!-- Screen Capture Education -->
<structure type="lottie" name="IDR_SCREEN_CAPTURE_EDUCATION_KEYBOARD_IMAGE" file="unscaled_resources/screen_capture_education_keyboard.json" compress="gzip" />
</structures>
</release>
</grit>

File diff suppressed because one or more lines are too long

@ -328,6 +328,14 @@ void SystemDialogDelegateView::SetMiddleContentAlignment(
SetViewCrossAxisAlignment(contents_[ContentType::kMiddle], alignment);
}
void SystemDialogDelegateView::SetAcceptButtonVisible(bool visible) {
button_container_->accept_button()->SetVisible(visible);
}
void SystemDialogDelegateView::SetTitleMargins(const gfx::Insets& margins) {
SetViewLayoutSpecs(title_, margins);
}
gfx::Size SystemDialogDelegateView::CalculatePreferredSize() const {
auto* host_window = GetDialogHostWindow(GetWidget());
// If the delegate view is not added to a widget or parented to a host window,

@ -126,6 +126,12 @@ class ASH_EXPORT SystemDialogDelegateView : public views::WidgetDelegateView {
void SetTopContentAlignment(views::LayoutAlignment alignment);
void SetMiddleContentAlignment(views::LayoutAlignment alignment);
// If true, hides the accept button in `button_container_`.
void SetAcceptButtonVisible(bool visible);
// Sets the margins for the title label view.
void SetTitleMargins(const gfx::Insets& margins);
// views::WidgetDelegateView:
gfx::Size CalculatePreferredSize() const override;
gfx::Size GetMinimumSize() const override;
@ -204,6 +210,8 @@ VIEW_BUILDER_VIEW_TYPE_PROPERTY(views::View, MiddleContentView)
VIEW_BUILDER_VIEW_TYPE_PROPERTY(views::View, AdditionalViewInButtonRow)
VIEW_BUILDER_PROPERTY(views::LayoutAlignment, TopContentAlignment)
VIEW_BUILDER_PROPERTY(views::LayoutAlignment, MiddleContentAlignment)
VIEW_BUILDER_PROPERTY(bool, AcceptButtonVisible)
VIEW_BUILDER_PROPERTY(const gfx::Insets&, TitleMargins)
VIEW_BUILDER_PROPERTY(ui::ModalType, ModalType)
END_VIEW_BUILDER

@ -1250,6 +1250,7 @@ chromium-metrics-reviews@google.com.
<int value="20" label="Multitask Menu Clamshell"/>
<int value="21" label="Multitask Menu Tablet"/>
<int value="22" label="Capture Mode Education Shortcut Nudge"/>
<int value="23" label="Capture Mode Education Shortcut Tutorial"/>
</enum>
<enum name="OnDeviceToServerSpeechRecognitionFallbackReason">