blob: aa5b65124d82eb205f598e964507bf1fd3b681f1 [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_DESCRIPTION_H_
#define COMPONENTS_USER_EDUCATION_COMMON_TUTORIAL_DESCRIPTION_H_
#include <string>
#include <vector>
#include "base/metrics/histogram_macros.h"
#include "components/user_education/common/help_bubble.h"
#include "components/user_education/common/help_bubble_params.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/base/interaction/element_identifier.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;
virtual ~TutorialHistograms() = default;
void operator=(const TutorialHistograms& other) = delete;
// 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 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)>;
TutorialDescription();
~TutorialDescription();
TutorialDescription(TutorialDescription&& other);
TutorialDescription& operator=(TutorialDescription&& other);
using ContextMode = ui::InteractionSequence::ContextMode;
using ElementSpecifier = absl::variant<ui::ElementIdentifier, std::string>;
struct Step {
Step();
explicit Step(
ElementSpecifier element_specifier,
ui::InteractionSequence::StepType step_type_ =
ui::InteractionSequence::StepType::kShown,
ui::CustomElementEventType event_type_ = ui::CustomElementEventType());
Step(int title_text_id_,
int body_text_id_,
ui::InteractionSequence::StepType step_type_,
ui::ElementIdentifier element_id_,
std::string element_name_,
HelpBubbleArrow arrow_,
ui::CustomElementEventType event_type_ = ui::CustomElementEventType(),
absl::optional<bool> must_remain_visible_ = absl::nullopt,
bool transition_only_on_event_ = false,
NameElementsCallback name_elements_callback_ = NameElementsCallback(),
ContextMode step_context = ContextMode::kInitial);
Step(const Step& other);
Step& operator=(const Step& other);
~Step();
// The element used by interaction sequence to observe and attach a bubble.
ui::ElementIdentifier element_id;
// The element, referred to by name, used by the interaction sequence
// to observe and potentially attach a bubble. must be non-empty.
std::string element_name;
// 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 = ui::CustomElementEventType();
// 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 positioning of the bubble arrow.
HelpBubbleArrow arrow = HelpBubbleArrow::kTopRight;
// 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
absl::optional<bool> must_remain_visible = absl::nullopt;
// 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;
// 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.
absl::optional<bool> must_be_visible;
// 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();
// 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(const char name_[]) {
return NameElements(base::BindRepeating(
[](const char name[], ui::InteractionSequence* sequence,
ui::TrackedElement* element) {
sequence->NameElement(element, base::StringPiece(name));
return true;
},
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;
}
};
// 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
struct BubbleStep : public Step {
// TutorialDescription::BubbleStep(element_id_)
// TutorialDescription::BubbleStep(element_name_)
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_) {
body_text_id = body_text_;
return *this;
}
BubbleStep& SetBubbleArrow(HelpBubbleArrow arrow_) {
arrow = arrow_;
return *this;
}
BubbleStep& AddDefaultNextButton() {
return AddCustomNextButton(
base::BindRepeating([](ui::TrackedElement* current_anchor) {
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(
current_anchor, kHelpBubbleNextButtonClickedEvent);
}));
}
BubbleStep& AddCustomNextButton(NextButtonCallback next_button_callback_) {
next_button_callback = std::move(next_button_callback_);
return *this;
}
};
// 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
struct HiddenStep : public Step {
// HiddenStep::WaitForShowEvent(element_id_)
// HiddenStep::WaitForShowEvent(element_name_)
// 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;
}
// HiddenStep::WaitForHideEvent(element_id_)
// HiddenStep::WaitForHideEvent(element_name_)
// 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;
}
// HiddenStep::WaitForActivateEvent(element_id_)
// HiddenStep::WaitForActivateEvent(element_name_)
// Transition to the next step after an activation event occurs
static HiddenStep WaitForActivateEvent(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kActivated);
step.transition_only_on_event = true;
return step;
}
// HiddenStep::WaitForShown(element_id_)
// HiddenStep::WaitForShown(element_name_)
// 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;
}
// HiddenStep::WaitForHidden(element_id_)
// HiddenStep::WaitForHidden(element_name_)
// 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;
}
// HiddenStep::WaitForActivated(element_id_)
// HiddenStep::WaitForActivated(element_name_)
// Transition to the next step if anchor is, or becomes, activated
static HiddenStep WaitForActivated(ElementSpecifier element_specifier) {
HiddenStep step(element_specifier,
ui::InteractionSequence::StepType::kActivated);
step.transition_only_on_event = false;
return step;
}
private:
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
struct EventStep : public Step {
// TutorialDescription::EventStep(event_id_)
explicit EventStep(ui::CustomElementEventType event_type_)
: Step(ui::ElementIdentifier(),
ui::InteractionSequence::StepType::kCustomEvent,
event_type_) {}
// TutorialDescription::EventStep(event_id_, element_id_)
// TutorialDescription::EventStep(event_id_, element_name_)
EventStep(ui::CustomElementEventType event_type_,
ElementSpecifier element_specifier)
: Step(element_specifier,
ui::InteractionSequence::StepType::kCustomEvent,
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(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<TutorialDescription::Step> Steps(Args&&... steps) {
std::vector<TutorialDescription::Step> flat_steps = {};
(AddStep(flat_steps, std::forward<Args>(steps)), ...);
return flat_steps;
}
// 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 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;
private:
static void AddStep(std::vector<Step>& dest, Step step) {
dest.emplace_back(step);
}
static void AddStep(std::vector<Step>& dest, const std::vector<Step>& src) {
for (auto& step : src) {
dest.emplace_back(step);
}
}
};
} // namespace user_education
#endif // COMPONENTS_USER_EDUCATION_COMMON_TUTORIAL_DESCRIPTION_H_