blob: 4b12177bd505733cf5112383be01610575335a7b [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.
#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.";
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