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.

#include "components/user_education/common/tutorial/tutorial.h"

#include <algorithm>
#include <optional>

#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "components/strings/grit/components_strings.h"
#include "components/user_education/common/help_bubble/help_bubble.h"
#include "components/user_education/common/help_bubble/help_bubble_factory.h"
#include "components/user_education/common/help_bubble/help_bubble_factory_registry.h"
#include "components/user_education/common/help_bubble/help_bubble_params.h"
#include "components/user_education/common/tutorial/tutorial_description.h"
#include "components/user_education/common/tutorial/tutorial_service.h"
#include "components/vector_icons/vector_icons.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/interaction/interaction_sequence.h"
#include "ui/base/l10n/l10n_util.h"

namespace user_education {

namespace {

int CountProgress(const std::vector<TutorialDescription::Step>& steps) {
  int result = 0;
  for (const auto& step : steps) {
    if (step.ShouldShowBubble()) {
      ++result;
    } else if (step.step_type() ==
               ui::InteractionSequence::StepType::kSubsequence) {
      CHECK(!step.branches().empty());
      int to_add = 0;
      for (const auto& branch : step.branches()) {
        to_add = std::max(to_add, CountProgress(branch.second));
      }
      result += to_add;
    }
  }
  return result;
}

void CommonStepBuilderSetup(ui::InteractionSequence::StepBuilder& builder,
                            const TutorialDescription::Step& step) {
  builder.SetContext(step.context_mode());

  if (step.element_id()) {
    builder.SetElementID(step.element_id());
  }

  if (!step.element_name().empty()) {
    builder.SetElementName(step.element_name());
  }

  if (step.must_remain_visible().has_value()) {
    builder.SetMustRemainVisible(step.must_remain_visible().value());
  }

  if (step.must_be_visible().has_value()) {
    builder.SetMustBeVisibleAtStart(step.must_be_visible().value());
  }

  builder.SetTransitionOnlyOnEvent(step.transition_only_on_event());
}

// Adds a branch of a conditional to `builder` based on `condition` and `steps`.
void AddStepBuilderSubsequence(
    ui::InteractionSequence::StepBuilder& builder,
    ui::InteractionSequence::SubsequenceCondition condition,
    const std::vector<TutorialDescription::Step>& steps,
    int max_progress,
    int& current_progress,
    bool is_terminal,
    bool can_be_restarted,
    int complete_button_text_id,
    TutorialService* tutorial_service) {
  ui::InteractionSequence::Builder subsequence;
  for (const auto& step : steps) {
    subsequence.AddStep(Tutorial::Builder::BuildFromDescriptionStep(
        step, max_progress, current_progress,
        is_terminal && &step == &steps.back(), can_be_restarted,
        complete_button_text_id, tutorial_service));
  }
  builder.AddSubsequence(std::move(subsequence), std::move(condition));
}

}  // namespace

namespace internal {

// Step Builder provides an interface for constructing an
// InteractionSequence::Step from a TutorialDescription::Step.
// TutorialDescription is used as the basis for the TutorialStepBuilder since
// all parameters of the Description will be needed to create the bubble or
// build the interaction sequence step. In order to use the The
// TutorialStepBuilder should only be used by Tutorial::Builder to construct the
// steps in the tutorial.
class TutorialStepBuilder {
 public:
  explicit TutorialStepBuilder(const TutorialDescription::Step& step,
                               std::optional<std::pair<int, int>> progress,
                               bool is_last_step,
                               bool can_be_restarted,
                               int complete_button_text_id)
      : progress_(progress),
        is_last_step_(is_last_step),
        can_be_restarted_(can_be_restarted),
        complete_button_text_id_(complete_button_text_id),
        step_(step) {
    CHECK_NE(complete_button_text_id_, 0);
  }
  ~TutorialStepBuilder() = default;

  std::unique_ptr<ui::InteractionSequence::Step> Build(
      TutorialService* tutorial_service);

 private:
  ui::InteractionSequence::StepStartCallback BuildStartCallback(
      TutorialService* tutorial_service);

  ui::InteractionSequence::StepStartCallback BuildMaybeShowBubbleCallback(
      TutorialService* tutorial_service);

  ui::InteractionSequence::StepEndCallback BuildHideBubbleCallback(
      TutorialService* tutorial_service);

  const std::optional<std::pair<int, int>> progress_;
  const bool is_last_step_;
  const bool can_be_restarted_;
  const int complete_button_text_id_;
  const TutorialDescription::Step step_;
};

std::unique_ptr<ui::InteractionSequence::Step> TutorialStepBuilder::Build(
    TutorialService* tutorial_service) {
  ui::InteractionSequence::StepBuilder interaction_sequence_step_builder;

  interaction_sequence_step_builder.SetType(step_.step_type(),
                                            step_.event_type());

  CommonStepBuilderSetup(interaction_sequence_step_builder, step_);

  interaction_sequence_step_builder.SetStartCallback(
      BuildStartCallback(tutorial_service));
  interaction_sequence_step_builder.SetEndCallback(
      BuildHideBubbleCallback(tutorial_service));

  return interaction_sequence_step_builder.Build();
}

ui::InteractionSequence::StepStartCallback
TutorialStepBuilder::BuildStartCallback(TutorialService* tutorial_service) {
  // get show bubble callback
  ui::InteractionSequence::StepStartCallback maybe_show_bubble_callback =
      BuildMaybeShowBubbleCallback(tutorial_service);

  return base::BindOnce(
      [](TutorialDescription::NameElementsCallback name_elements_callback,
         ui::InteractionSequence::StepStartCallback maybe_show_bubble_callback,
         ui::InteractionSequence* sequence, ui::TrackedElement* element) {
        if (name_elements_callback) {
          name_elements_callback.Run(sequence, element);
        }
        if (maybe_show_bubble_callback) {
          std::move(maybe_show_bubble_callback).Run(sequence, element);
        }
      },
      step_.name_elements_callback(), std::move(maybe_show_bubble_callback));
}

ui::InteractionSequence::StepStartCallback
TutorialStepBuilder::BuildMaybeShowBubbleCallback(
    TutorialService* tutorial_service) {
  if (!step_.ShouldShowBubble()) {
    return ui::InteractionSequence::StepStartCallback();
  }

  const std::u16string title_text =
      step_.title_text_id() ? l10n_util::GetStringUTF16(step_.title_text_id())
                            : std::u16string();

  const std::u16string body_text =
      step_.body_text_id() ? l10n_util::GetStringUTF16(step_.body_text_id())
                           : std::u16string();

  const std::u16string screenreader_text =
      step_.screenreader_text_id()
          ? l10n_util::GetStringUTF16(step_.screenreader_text_id())
          : std::u16string();

  return base::BindOnce(
      [](TutorialService* tutorial_service, std::u16string title_text_,
         std::u16string body_text_, std::u16string screenreader_text_,
         HelpBubbleArrow arrow_, std::optional<std::pair<int, int>> progress,
         bool is_last_step, bool can_be_restarted, int complete_button_text_id,
         TutorialDescription::NextButtonCallback next_button_callback,
         HelpBubbleParams::ExtendedProperties extended_properties,
         ui::InteractionSequence* sequence, ui::TrackedElement* element) {
        DCHECK(tutorial_service);

        tutorial_service->HideCurrentBubbleIfShowing();

        HelpBubbleParams params;
        params.extended_properties = std::move(extended_properties);
        params.title_text = title_text_;
        params.body_text = body_text_;
        params.screenreader_text = screenreader_text_;
        params.progress = progress;
        params.arrow = arrow_;
        params.timeout = base::TimeDelta();
        params.dismiss_callback = base::BindOnce(
            [](std::optional<int> step_number,
               TutorialService* tutorial_service) {
              tutorial_service->AbortTutorial(step_number);
            },
            progress.has_value() ? std::make_optional(progress.value().first)
                                 : std::nullopt,
            base::Unretained(tutorial_service));

        if (is_last_step) {
          params.body_icon = &vector_icons::kCelebrationIcon;
          params.body_icon_alt_text =
              tutorial_service->GetBodyIconAltText(true);
          params.dismiss_callback = base::BindOnce(
              [](TutorialService* tutorial_service) {
                tutorial_service->CompleteTutorial();
              },
              base::Unretained(tutorial_service));

          if (can_be_restarted) {
            HelpBubbleButtonParams restart_button;
            restart_button.text =
                l10n_util::GetStringUTF16(IDS_TUTORIAL_RESTART_TUTORIAL);
            restart_button.is_default = false;
            restart_button.callback = base::BindOnce(
                [](TutorialService* tutorial_service) {
                  tutorial_service->RestartTutorial();
                },
                base::Unretained(tutorial_service));
            params.buttons.emplace_back(std::move(restart_button));
          }

          HelpBubbleButtonParams complete_button;
          complete_button.text =
              l10n_util::GetStringUTF16(complete_button_text_id);
          complete_button.is_default = true;
          complete_button.callback = base::BindOnce(
              [](TutorialService* tutorial_service) {
                tutorial_service->CompleteTutorial();
              },
              base::Unretained(tutorial_service));
          params.buttons.emplace_back(std::move(complete_button));
        } else if (next_button_callback) {
          HelpBubbleButtonParams next_button;
          next_button.text =
              l10n_util::GetStringUTF16(IDS_TUTORIAL_NEXT_BUTTON);
          next_button.is_default = true;
          next_button.callback = base::BindOnce(
              [](TutorialDescription::NextButtonCallback next_button_callback,
                 ui::TrackedElement* current_anchor) {
                std::move(next_button_callback).Run(current_anchor);
              },
              std::move(next_button_callback), element);
          params.buttons.emplace_back(std::move(next_button));
        }

        params.close_button_alt_text =
            l10n_util::GetStringUTF16(IDS_CLOSE_TUTORIAL);

        std::unique_ptr<HelpBubble> bubble =
            tutorial_service->bubble_factory_registry()->CreateHelpBubble(
                element, std::move(params));
        tutorial_service->SetCurrentBubble(std::move(bubble), is_last_step);
      },
      base::Unretained(tutorial_service), title_text, body_text,
      screenreader_text, step_.arrow(), progress_, is_last_step_,
      can_be_restarted_, complete_button_text_id_, step_.next_button_callback(),
      step_.extended_properties());
}

ui::InteractionSequence::StepEndCallback
TutorialStepBuilder::BuildHideBubbleCallback(
    TutorialService* tutorial_service) {
  return base::BindOnce(
      [](TutorialService* tutorial_service, ui::TrackedElement* element) {},
      base::Unretained(tutorial_service));
}

}  // namespace internal

// static
std::unique_ptr<ui::InteractionSequence::Step>
Tutorial::Builder::BuildFromDescriptionStep(
    const TutorialDescription::Step& step,
    int max_progress,
    int& current_progress,
    bool is_terminal,
    bool can_be_restarted,
    int complete_button_text_id,
    TutorialService* tutorial_service) {
  if (step.step_type() == ui::InteractionSequence::StepType::kSubsequence) {
    CHECK(!step.branches().empty());
    CHECK(!step.branches()[0].second.empty());
    ui::InteractionSequence::StepBuilder builder;
    builder.SetSubsequenceMode(step.subsequence_mode());
    CommonStepBuilderSetup(builder, step);
    const int prev_progress = current_progress;
    for (auto& branch : step.branches()) {
      int branch_progress = prev_progress;
      AddStepBuilderSubsequence(
          builder,
          base::BindOnce(
              [](TutorialDescription::ConditionalCallback callback,
                 const ui::InteractionSequence*,
                 const ui::TrackedElement* el) { return callback.Run(el); },
              std::move(branch.first)),
          std::move(branch.second), max_progress, branch_progress, is_terminal,
          can_be_restarted, complete_button_text_id, tutorial_service);
      current_progress = std::max(current_progress, branch_progress);
    }
    return builder.Build();
  } else {
    std::optional<std::pair<int, int>> progress;
    if (step.ShouldShowBubble()) {
      ++current_progress;
      if (!is_terminal) {
        DCHECK_LE(current_progress, max_progress)
            << "Intermediate/progress steps should never exceed the maximum "
               "progress.";
        // Only show progress indicator if there is more than one step.
        if (max_progress > 1) {
          progress = std::make_pair(current_progress, max_progress);
        }
      } else {
        DCHECK_LE(current_progress, max_progress + 1)
            << "Terminal step should always be immediately after final "
               "progress step.";
        current_progress = max_progress + 1;
      }
    } else {
      DCHECK(!is_terminal)
          << "Hidden step should never be the last step in a sequence.";
    }
    internal::TutorialStepBuilder step_builder(
        step, progress, is_terminal, can_be_restarted, complete_button_text_id);
    return step_builder.Build(tutorial_service);
  }
}

Tutorial::Builder::Builder()
    : builder_(std::make_unique<ui::InteractionSequence::Builder>()) {}
Tutorial::Builder::~Builder() = default;

// static
std::unique_ptr<Tutorial> Tutorial::Builder::BuildFromDescription(
    const TutorialDescription& description,
    TutorialService* tutorial_service,
    ui::ElementContext context) {
  Tutorial::Builder builder;
  builder.SetContext(context);

  // Last step doesn't have a progress counter.
  const int max_progress = CountProgress(description.steps) - 1;
  int current_progress = 0;
  for (const auto& step : description.steps) {
    builder.AddStep(BuildFromDescriptionStep(
        step, max_progress, current_progress,
        &step == &description.steps.back(), description.can_be_restarted,
        description.complete_button_text_id, tutorial_service));
  }
  DCHECK_EQ(current_progress, max_progress + 1);

  // Note that the step number we are using here is not the same as the the
  // InteractionSequence::AbortCallback step (`sequence_step`) which counts all
  // steps; `current_step` in this case is the visual bubble count, which does
  // not count hidden steps.
  builder.SetAbortedCallback(base::BindOnce(
      [](int step_number, TutorialService* tutorial_service,
         const ui::InteractionSequence::AbortedData&) {
        tutorial_service->AbortTutorial(step_number);
      },
      max_progress, tutorial_service));

  return builder.Build();
}

Tutorial::Builder& Tutorial::Builder::AddStep(
    std::unique_ptr<ui::InteractionSequence::Step> step) {
  builder_->AddStep(std::move(step));
  return *this;
}

Tutorial::Builder& Tutorial::Builder::SetAbortedCallback(
    ui::InteractionSequence::AbortedCallback callback) {
  builder_->SetAbortedCallback(std::move(callback));
  return *this;
}

Tutorial::Builder& Tutorial::Builder::SetCompletedCallback(
    ui::InteractionSequence::CompletedCallback callback) {
  builder_->SetCompletedCallback(std::move(callback));
  return *this;
}

Tutorial::Builder& Tutorial::Builder::SetContext(
    ui::ElementContext element_context) {
  builder_->SetContext(element_context);
  return *this;
}

std::unique_ptr<Tutorial> Tutorial::Builder::Build() {
  return base::WrapUnique(new Tutorial(builder_->Build()));
}

Tutorial::Tutorial(
    std::unique_ptr<ui::InteractionSequence> interaction_sequence)
    : interaction_sequence_(std::move(interaction_sequence)) {}
Tutorial::~Tutorial() = default;

void Tutorial::Start() {
  DCHECK(interaction_sequence_);
  if (interaction_sequence_) {
    interaction_sequence_->Start();
  }
}

void Tutorial::Abort() {
  if (interaction_sequence_) {
    interaction_sequence_.reset();
  }
}

void Tutorial::SetState(std::unique_ptr<ScopedTutorialState> tutorial_state) {
  CHECK(tutorial_state.get());
  tutorial_state_ = std::move(tutorial_state);
}

}  // namespace user_education