| // 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_ |