blob: e057b7b091b2003c0a0e641d1f0fffc2bd1a6486 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef COMPONENTS_USER_EDUCATION_COMMON_TUTORIAL_TUTORIAL_DESCRIPTION_H_
#define COMPONENTS_USER_EDUCATION_COMMON_TUTORIAL_TUTORIAL_DESCRIPTION_H_
#include <optional>
#include <string>
#include <utility>
#include <variant>
#include <vector>
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_macros.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/common/user_education_metadata.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_specifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
namespace user_education {
// Holds the data required to properly store histograms for a given tutorial.
// Abstract base class because best practice is to statically declare
// histograms and so we need some compile-time polymorphism to actually
// implement the RecordXXX() calls.
//
// Use MakeTutorialHistograms() below to create a concrete instance of this
// class.
class TutorialHistograms {
public:
TutorialHistograms() = default;
TutorialHistograms(const TutorialHistograms& other) = delete;
void operator=(const TutorialHistograms& other) = delete;
virtual ~TutorialHistograms() = default;
// Records whether the tutorial was completed or not.
virtual void RecordComplete(bool value) = 0;
// Records the step on which the tutorial was aborted.
virtual void RecordAbortStep(int step) = 0;
// Records whether, when an IPH offered the tutorial, the user opted into
// seeing the tutorial or not.
virtual void RecordIphLinkClicked(bool value) = 0;
// Records whether, when an IPH offered the tutorial, the user opted into
// seeing the tutorial or not.
virtual void RecordStartedFromWhatsNewPage(bool value) = 0;
// This is used for consistency-checking only.
virtual const std::string& GetTutorialPrefix() const = 0;
};
namespace internal {
constexpr char kTutorialHistogramPrefix[] = "Tutorial.";
template <const char histogram_name[]>
class TutorialHistogramsImpl : public TutorialHistograms {
public:
explicit TutorialHistogramsImpl(int max_steps)
: histogram_name_(histogram_name),
completed_name_(kTutorialHistogramPrefix + histogram_name_ +
".Completion"),
aborted_name_(kTutorialHistogramPrefix + histogram_name_ +
".AbortStep"),
iph_link_clicked_name_(kTutorialHistogramPrefix + histogram_name_ +
".IPHLinkClicked"),
whats_new_page_name_(kTutorialHistogramPrefix + histogram_name_ +
".StartedFromWhatsNewPage"),
max_steps_(max_steps) {}
~TutorialHistogramsImpl() override = default;
protected:
void RecordComplete(bool value) override {
UMA_HISTOGRAM_BOOLEAN(completed_name_, value);
}
void RecordAbortStep(int step) override {
UMA_HISTOGRAM_EXACT_LINEAR(aborted_name_, step, max_steps_);
}
void RecordIphLinkClicked(bool value) override {
UMA_HISTOGRAM_BOOLEAN(iph_link_clicked_name_, value);
}
void RecordStartedFromWhatsNewPage(bool value) override {
UMA_HISTOGRAM_BOOLEAN(whats_new_page_name_, value);
}
const std::string& GetTutorialPrefix() const override {
return histogram_name_;
}
private:
const std::string histogram_name_;
const std::string completed_name_;
const std::string aborted_name_;
const std::string iph_link_clicked_name_;
const std::string whats_new_page_name_;
const int max_steps_;
};
} // namespace internal
// Call to create a tutorial-specific histograms object for use with the
// tutorial. The template parameter should be a reference to a const char[]
// that is a compile-time constant. Also remember to add a matching entry to
// the "TutorialID" variant in histograms.xml corresponding to your tutorial.
//
// Example:
// const char kMyTutorialName[] = "MyTutorial";
// tutorial_descriptions.histograms =
// MakeTutorialHistograms<kMyTutorialName>(
// tutorial_description.steps.size());
template <const char* histogram_name>
std::unique_ptr<TutorialHistograms> MakeTutorialHistograms(int max_steps) {
return std::make_unique<internal::TutorialHistogramsImpl<histogram_name>>(
max_steps);
}
// A class that manages a temporary state associated with the tutorial.
// A new object via the temporary setup callback when a new tutorial is started
// and maintained through the lifetime of the tutorial.
class ScopedTutorialState {
public:
explicit ScopedTutorialState(ui::ElementContext context);
virtual ~ScopedTutorialState();
ui::ElementContext context() const { return context_; }
private:
const ui::ElementContext context_;
};
// A Struct that provides all of the data necessary to construct a Tutorial.
// A Tutorial Description is a list of Steps for a tutorial. Each step has info
// for constructing the InteractionSequence::Step from the
// TutorialDescription::Step.
struct TutorialDescription {
using NameElementsCallback =
base::RepeatingCallback<bool(ui::InteractionSequence*,
ui::TrackedElement*)>;
using NextButtonCallback =
base::RepeatingCallback<void(ui::TrackedElement* current_anchor)>;
using TemporaryStateCallback =
base::RepeatingCallback<std::unique_ptr<ScopedTutorialState>(
ui::ElementContext)>;
TutorialDescription();
TutorialDescription(TutorialDescription&& other) noexcept;
TutorialDescription& operator=(TutorialDescription&& other) noexcept;
~TutorialDescription();
using ContextMode = ui::InteractionSequence::ContextMode;
using ElementSpecifier = ui::ElementSpecifier;
// Callback used to determine if the "then" branch of a conditional should be
// followed. Note that `element` may be null if no matching element exists.
using ConditionalCallback =
base::RepeatingCallback<bool(const ui::TrackedElement* element)>;
class Step {
public:
Step();
Step(const Step& other);
Step& operator=(const Step& other);
~Step();
// returns true iff all of the required parameters exist to display a
// bubble.
bool ShouldShowBubble() const;
Step& AbortIfVisibilityLost(bool must_remain_visible) {
must_remain_visible_ = must_remain_visible;
return *this;
}
Step& AbortIfNotVisible() {
must_be_visible_ = true;
return *this;
}
Step& NameElement(std::string name);
Step& NameElements(NameElementsCallback name_elements_callback) {
name_elements_callback_ = std::move(name_elements_callback);
return *this;
}
Step& InAnyContext() {
context_mode_ = ContextMode::kAny;
return *this;
}
Step& InSameContext() {
context_mode_ = ContextMode::kFromPreviousStep;
return *this;
}
ui::ElementIdentifier element_id() const { return element_.identifier(); }
std::string_view element_name() const { return element_.name(); }
ElementSpecifier element() const { return element_; }
ui::InteractionSequence::StepType step_type() const { return step_type_; }
ui::CustomElementEventType event_type() const { return event_type_; }
int title_text_id() const { return title_text_id_; }
int body_text_id() const { return body_text_id_; }
int screenreader_text_id() const { return screenreader_text_id_; }
HelpBubbleArrow arrow() const { return arrow_; }
std::optional<bool> must_remain_visible() const {
return must_remain_visible_;
}
std::optional<bool> must_be_visible() const { return must_be_visible_; }
bool transition_only_on_event() const { return transition_only_on_event_; }
const NameElementsCallback& name_elements_callback() const {
return name_elements_callback_;
}
ContextMode context_mode() const { return context_mode_; }
const NextButtonCallback& next_button_callback() const {
return next_button_callback_;
}
const HelpBubbleParams::ExtendedProperties& extended_properties() const {
return extended_properties_;
}
ui::InteractionSequence::SubsequenceMode subsequence_mode() const {
return subsequence_mode_;
}
const auto& branches() const { return branches_; }
protected:
Step(ElementSpecifier element,
ui::InteractionSequence::StepType step_type,
HelpBubbleArrow arrow = HelpBubbleArrow::kNone,
ui::CustomElementEventType event_type = ui::CustomElementEventType());
// The element used by interaction sequence to observe and attach a bubble.
ElementSpecifier element_;
// The step type for InteractionSequence::Step.
ui::InteractionSequence::StepType step_type_ =
ui::InteractionSequence::StepType::kShown;
// The event type for the step if `step_type` is kCustomEvent.
ui::CustomElementEventType event_type_;
// The title text to be populated in the bubble.
int title_text_id_ = 0;
// The body text to be populated in the bubble.
int body_text_id_ = 0;
// The screenreader text to be populated in the bubble.
int screenreader_text_id_ = 0;
// The positioning of the bubble arrow.
HelpBubbleArrow arrow_ = HelpBubbleArrow::kNone;
// Should the element remain visible through the entire step, this should be
// set to false for hidden steps and for shown steps that precede hidden
// steps on the same element. if left empty the interaction sequence will
// decide what its value should be based on the generated
// InteractionSequence::StepBuilder
std::optional<bool> must_remain_visible_ = std::nullopt;
// If set, determines whether the element in question must be visible at the
// start of the step. If left empty the interaction sequence will choose a
// reasonable default.
std::optional<bool> must_be_visible_;
// Should the step only be completed when an event like shown or hidden only
// happens during current step. for more information on the implementation
// take a look at transition_only_on_event in InteractionSequence::Step
bool transition_only_on_event_ = false;
// lambda which is called on the start callback of the InteractionSequence
// which provides the interaction sequence and the current element that
// belongs to the step. The intention for this functionality is to name one
// or many elements using the Framework's Specific API finding an element
// and naming it OR using the current element from the sequence as the
// element for naming. The return value is a boolean which controls whether
// the Interaction Sequence should continue or not. If false is returned
// the tutorial will abort
NameElementsCallback name_elements_callback_ = NameElementsCallback();
// Where to search for the step's target element. Default is the context the
// tutorial started in.
ContextMode context_mode_ = ContextMode::kInitial;
// Lambda which is called when the "Next" button is clicked in the help
// bubble associated with this step. Note that a "Next" button won't render:
// 1. if `next_button_callback` is null
// 2. if this step is the last step of a tutorial
NextButtonCallback next_button_callback_ = NextButtonCallback();
// Platform-specific properties that can be set for a bubble step. If an
// extended property evolves to warrant cross-platform support, it should be
// promoted out of extended properties.
HelpBubbleParams::ExtendedProperties extended_properties_;
// Used for if-then-else conditionals.
ui::InteractionSequence::SubsequenceMode subsequence_mode_ =
ui::InteractionSequence::SubsequenceMode::kAtMostOne;
std::vector<std::pair<ConditionalCallback, std::vector<Step>>> branches_;
private:
friend class Tutorial;
};
// TutorialDescription::BubbleStep
// A bubble step is a step which shows a bubble anchored to an element
// This requires that the anchor element be visible, so this is always
// a kShown step.
//
// - A bubble step must be passed an element_id or an element_name
class BubbleStep : public Step {
public:
explicit BubbleStep(ElementSpecifier element_specifier)
: Step(element_specifier, ui::InteractionSequence::StepType::kShown) {}
BubbleStep& SetBubbleTitleText(int title_text) {
title_text_id_ = title_text;
return *this;
}
BubbleStep& SetBubbleBodyText(int body_text_id) {
body_text_id_ = body_text_id;
return *this;
}
BubbleStep& SetBubbleScreenreaderText(int screenreader_text_id) {
screenreader_text_id_ = screenreader_text_id;
return *this;
}
BubbleStep& SetBubbleArrow(HelpBubbleArrow arrow) {
arrow_ = arrow;
return *this;
}
BubbleStep& SetExtendedProperties(
HelpBubbleParams::ExtendedProperties extended_properties) {
extended_properties_ = std::move(extended_properties);
return *this;
}
BubbleStep& AddCustomNextButton(NextButtonCallback next_button_callback) {
next_button_callback_ = std::move(next_button_callback);
return *this;
}
BubbleStep& AddDefaultNextButton();
};
// TutorialDescription::HiddenStep
// A hidden step has no bubble and waits for a UI event to occur on
// a particular element.
//
// - A hidden step must be passed an element_id or an element_name
class HiddenStep : public Step {
public:
// Transition to the next step after a show event occurs
static HiddenStep WaitForShowEvent(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kShown);
step.transition_only_on_event_ = true;
return step;
}
// Transition to the next step after a hide event occurs
static HiddenStep WaitForHideEvent(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kHidden);
step.transition_only_on_event_ = true;
return step;
}
// Transition to the next step if anchor is, or becomes, visible
static HiddenStep WaitForShown(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kShown);
step.transition_only_on_event_ = false;
return step;
}
// Transition to the next step if anchor is, or becomes, hidden
static HiddenStep WaitForHidden(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kHidden);
step.transition_only_on_event_ = false;
return step;
}
// Transition to the next step if anchor is, or becomes, activated
static HiddenStep WaitForActivated(ElementSpecifier element_specifier) {
return HiddenStep(element_specifier,
ui::InteractionSequence::StepType::kActivated);
}
template <typename... Args>
static HiddenStep WaitForOneOf(Args&&... args) {}
protected:
explicit HiddenStep(ElementSpecifier element_specifier,
ui::InteractionSequence::StepType step_type)
: Step(element_specifier, step_type) {}
};
// TutorialDescription::EventStep
// An event step is a special case of a HiddenStep that waits for
// a custom event to be fired programmatically.
//
// - This step must be passed an event_id
// - Additionally, you can also pass an element_id or element_name if
// the event should occur specifically on a given element
class EventStep : public Step {
public:
explicit EventStep(ui::CustomElementEventType event_type)
: Step(ui::ElementIdentifier(),
ui::InteractionSequence::StepType::kCustomEvent,
HelpBubbleArrow::kNone,
event_type) {}
EventStep(ui::CustomElementEventType event_type,
ElementSpecifier element_specifier)
: Step(element_specifier,
ui::InteractionSequence::StepType::kCustomEvent,
HelpBubbleArrow::kNone,
event_type) {}
};
// TutorialDescription::Create<"Prefix">(step1, step2, ...)
//
// Create a tutorial description with the given steps
// This will also generate the histograms with the given prefix
template <const char histogram_name[], typename... Args>
static TutorialDescription Create(Args... steps) {
TutorialDescription description;
description.steps = Steps(std::move(steps)...);
description.histograms =
user_education::MakeTutorialHistograms<histogram_name>(
description.steps.size());
return description;
}
// TutorialDescription::Steps(step1, step2, {step3, step4}, ...)
//
// Turn steps and step vectors into a flattened vector of steps
template <typename... Args>
static std::vector<Step> Steps(Args... steps) {
std::vector<Step> flat_steps;
(AddStep(flat_steps, std::move(steps)), ...);
return flat_steps;
}
// Creates a conditional step. Syntax is:
// ```
// If(element[, condition])
// .Then(steps...)
// [.Else(steps...)]
// ```
//
// The `element` is retrieved from the current context (it will be null if it
// is not present). The `Then()` or branch is conditionally executed based on:
// * The result of calling `condition`, if specified (please note that the
// parameter may be null!)
// * Whether `element` exists, if `condition` is not specified.
//
// If `Else()` is specified, it will be executed if the `Then()` branch is
// not. At most one branch can execute, and if it fails, the tutorial fails.
//
// Because the step does not wait for `element` to become present if it is
// not yet visible, you may want to insert a `HiddenStep` before your
// conditional to ensure the correct state:
// ```
// HiddenStep::WaitForShown(kElementId),
// If(kElementId, should_do_optional_steps)
// .Then(<optional-steps>),
// ```
//
// If you want to wait for one of several elements to be visible (e.g. when
// the browser might show different variations of a WebUI page based on
// viewport size), use `WaitForAnyOf()`:
// ```
// // Wait for either version of the surface to appear:
// WaitForAnyOf(kPageVariation1ElementId)
// .Or(kPageVariation2ElementId),
//
// // Show different Tutorial steps based on which variation appeared:
// If(kPageVariation1ElementId)
// .Then(<then-steps>)
// .Else(<else-steps>),
// ```
//
// Without the WaitForAnyOf, it is possible that neither element would have
// yet become visible, resulting in the condition failing and the Else()
// branch always running.
class If : public Step {
public:
// Executes the `Then()` part of this conditional step if `element`
// satisfies `if_condition`. If `if_condition` is not specified, the
// default is to execute the `Then()` portion if `element` is visible.
explicit If(ElementSpecifier element,
ConditionalCallback if_condition = RunIfPresent())
: Step(element, ui::InteractionSequence::StepType::kSubsequence) {
branches_.emplace_back(std::move(if_condition), std::vector<Step>());
}
// These tutorial `then_steps` are executed if `if_condition` returns true.
// Otherwise they are ignored (and the `Else()` steps will be executed if
// present).
template <typename... Args>
If& Then(Args... then_steps) {
CHECK_EQ(1U, branches_.size());
CHECK(branches_[0].second.empty());
branches_[0].second = Steps(std::move(then_steps)...);
return *this;
}
// The tutorial `else_steps` are executed if `if_condition` returns false.
// This is optional; if not specified and the condition returns false,
// nothing happens.
template <typename... Args>
If& Else(Args... else_steps) {
CHECK_EQ(1U, branches_.size());
branches_.emplace_back(AlwaysRun(), Steps(std::move(else_steps)...));
subsequence_mode_ = ui::InteractionSequence::SubsequenceMode::kExactlyOne;
return *this;
}
// Provides mutable access to the branches in case steps need to be added
// individually.
using Step::branches;
auto& branches() { return branches_; }
};
// Constructs a hidden step that waits for at least one of `first` and any
// additional elements added through `Or()`.
//
// This is usually used before an `If()` to ensure that one of several
// different elements is present. See documentation for `If()` for more
// information.
class WaitForAnyOf : public HiddenStep {
public:
explicit WaitForAnyOf(ElementSpecifier first, bool wait_for_event = false);
WaitForAnyOf& Or(ElementSpecifier element, bool wait_for_event = false);
};
// The list of TutorialDescription steps.
std::vector<Step> steps;
// The histogram data to use. Use MakeTutorialHistograms() above to create a
// value to use, if you want to record specific histograms for this tutorial.
std::unique_ptr<TutorialHistograms> histograms;
// The callback for the tutorial which returns a scoped object which
// manages temporary state that is maintained through the lifetime of the
// tutorial.
TemporaryStateCallback temporary_state_callback;
// The ability for the tutorial to be restarted. In some cases tutorials can
// leave the UI in a state where it can not re-run the tutorial. In these
// cases this flag should be set to false so that the restart tutorial button
// is not displayed.
bool can_be_restarted = false;
// The text ID to use for the complete button at the end of the tutorial.
int complete_button_text_id = IDS_TUTORIAL_CLOSE_TUTORIAL;
// Holds metadata about the tutorial.
Metadata metadata;
private:
static void AddStep(std::vector<Step>& dest, Step step) {
dest.emplace_back(std::move(step));
}
static void AddStep(std::vector<Step>& dest, const std::vector<Step>& src) {
for (auto& step : src) {
dest.emplace_back(step);
}
}
static ConditionalCallback AlwaysRun();
static ConditionalCallback RunIfPresent();
};
} // namespace user_education
#endif // COMPONENTS_USER_EDUCATION_COMMON_TUTORIAL_TUTORIAL_DESCRIPTION_H_