910e62b5创建于 1月15日历史提交
// 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_