diff options
Diffstat (limited to 'chromium/ui/base/interaction/interaction_sequence.cc')
-rw-r--r-- | chromium/ui/base/interaction/interaction_sequence.cc | 515 |
1 files changed, 515 insertions, 0 deletions
diff --git a/chromium/ui/base/interaction/interaction_sequence.cc b/chromium/ui/base/interaction/interaction_sequence.cc new file mode 100644 index 00000000000..1980a82c5f4 --- /dev/null +++ b/chromium/ui/base/interaction/interaction_sequence.cc @@ -0,0 +1,515 @@ +// Copyright 2021 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "ui/base/interaction/interaction_sequence.h" + +#include "base/bind.h" +#include "base/logging.h" +#include "base/memory/ptr_util.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "ui/base/interaction/element_tracker.h" + +namespace ui { + +namespace { + +// Runs |callback| if it is valid. +// We have a lot of callbacks that can be null, so calling through this method +// prevents accidentally trying to run a null callback. +template <typename Signature, typename... Args> +void RunIfValid(base::OnceCallback<Signature> callback, Args... args) { + if (callback) + std::move(callback).Run(args...); +} + +// Version of AutoReset that takes a pointer-to-member and a weak reference in +// case the object that owns the value goes away before the AutoReset does. +template <class T, class U> +class SafeAutoReset { + public: + SafeAutoReset(base::WeakPtr<T> ptr, U T::*ref, U new_value) + : ptr_(ptr), ref_(ref), old_value_(ptr.get()->*ref) { + ptr.get()->*ref = new_value; + } + + SafeAutoReset(SafeAutoReset<T, U>&& other) + : ptr_(std::move(other.ptr_)), + ref_(other.ref_), + old_value_(other.old_value_) {} + + SafeAutoReset& operator=(SafeAutoReset<T, U>&& other) { + if (this != &other) { + Reset(); + ptr_ = std::move(other.ptr_); + ref_ = other.ref_; + old_value_ = other.old_value_; + } + return *this; + } + + ~SafeAutoReset() { Reset(); } + + private: + void Reset() { + if (ptr_) + ptr_.get()->*ref_ = old_value_; + } + + base::WeakPtr<T> ptr_; + U T::*ref_ = nullptr; + U old_value_ = U(); +}; + +// Convenience method to create a SafeAutoReset with less boilerplate. +template <class T, class U> +static SafeAutoReset<T, U> MakeSafeAutoReset(base::WeakPtr<T> ptr, + U T::*ref, + U new_value) { + return SafeAutoReset<T, U>(ptr, ref, new_value); +} + +} // anonymous namespace + +InteractionSequence::Step::Step() = default; +InteractionSequence::Step::~Step() = default; + +struct InteractionSequence::Configuration { + Configuration() = default; + ~Configuration() = default; + + std::list<std::unique_ptr<Step>> steps; + ElementContext context; + AbortedCallback aborted_callback; + CompletedCallback completed_callback; +}; + +InteractionSequence::Builder::Builder() + : configuration_(std::make_unique<Configuration>()) {} + +InteractionSequence::Builder::~Builder() { + DCHECK(!configuration_); +} + +InteractionSequence::Builder& InteractionSequence::Builder::SetAbortedCallback( + AbortedCallback callback) { + DCHECK(!configuration_->aborted_callback); + configuration_->aborted_callback = std::move(callback); + return *this; +} + +InteractionSequence::Builder& +InteractionSequence::Builder::SetCompletedCallback(CompletedCallback callback) { + DCHECK(!configuration_->completed_callback); + configuration_->completed_callback = std::move(callback); + return *this; +} + +InteractionSequence::Builder& InteractionSequence::Builder::AddStep( + std::unique_ptr<Step> step) { + DCHECK(step->id); + DCHECK(configuration_->steps.empty() || !step->element) + << " Only the initial step of a sequence may have a pre-set element."; + DCHECK(!step->element || step->must_be_visible) + << " Initial step with associated element must be visible from start."; + step->must_be_visible = + step->must_be_visible.value_or(step->type == StepType::kActivated); + step->must_remain_visible = + step->must_remain_visible.value_or(step->type == StepType::kShown); + DCHECK(step->type != StepType::kHidden || !step->must_remain_visible.value()); + if (!configuration_->context) + configuration_->context = step->context; + else + DCHECK(!step->context || step->context == configuration_->context); + configuration_->steps.emplace_back(std::move(step)); + return *this; +} + +InteractionSequence::Builder& InteractionSequence::Builder::SetContext( + ElementContext context) { + configuration_->context = context; + return *this; +} + +std::unique_ptr<InteractionSequence> InteractionSequence::Builder::Build() { + DCHECK(!configuration_->steps.empty()); + DCHECK(configuration_->context) + << "If no view is provided, Builder::SetContext() must be called."; + return base::WrapUnique(new InteractionSequence(std::move(configuration_))); +} + +InteractionSequence::StepBuilder::StepBuilder() + : step_(std::make_unique<Step>()) {} +InteractionSequence::StepBuilder::~StepBuilder() = default; + +InteractionSequence::StepBuilder& +InteractionSequence::StepBuilder::SetElementID(ElementIdentifier element_id) { + DCHECK(element_id); + step_->id = element_id; + return *this; +} + +InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetContext( + ElementContext context) { + DCHECK(context); + step_->context = context; + return *this; +} + +InteractionSequence::StepBuilder& +InteractionSequence::StepBuilder::SetMustBeVisibleAtStart( + bool must_be_visible) { + step_->must_be_visible = must_be_visible; + return *this; +} + +InteractionSequence::StepBuilder& +InteractionSequence::StepBuilder::SetMustRemainVisible( + bool must_remain_visible) { + step_->must_remain_visible = must_remain_visible; + return *this; +} + +InteractionSequence::StepBuilder& InteractionSequence::StepBuilder::SetType( + StepType step_type) { + step_->type = step_type; + return *this; +} + +InteractionSequence::StepBuilder& +InteractionSequence::StepBuilder::SetStartCallback( + StepCallback start_callback) { + step_->start_callback = std::move(start_callback); + return *this; +} + +InteractionSequence::StepBuilder& +InteractionSequence::StepBuilder::SetEndCallback(StepCallback end_callback) { + step_->end_callback = std::move(end_callback); + return *this; +} + +std::unique_ptr<InteractionSequence::Step> +InteractionSequence::StepBuilder::Build() { + return std::move(step_); +} + +InteractionSequence::InteractionSequence( + std::unique_ptr<Configuration> configuration) + : configuration_(std::move(configuration)) { + TrackedElement* const first_element = next_step()->element; + if (first_element) { + DCHECK(first_element->identifier() == next_step()->id); + DCHECK(first_element->context() == context()); + next_step()->subscription = + ElementTracker::GetElementTracker()->AddElementHiddenCallback( + first_element->identifier(), first_element->context(), + base::BindRepeating(&InteractionSequence::OnElementHidden, + base::Unretained(this))); + } +} + +// static +std::unique_ptr<InteractionSequence::Step> +InteractionSequence::WithInitialElement(TrackedElement* element, + StepCallback start_callback, + StepCallback end_callback) { + StepBuilder step; + step.step_->element = element; + step.SetType(StepType::kShown) + .SetElementID(element->identifier()) + .SetContext(element->context()) + .SetMustBeVisibleAtStart(true) + .SetMustRemainVisible(true) + .SetStartCallback(std::move(start_callback)) + .SetEndCallback(std::move(end_callback)); + return step.Build(); +} + +InteractionSequence::~InteractionSequence() { + // We can abort during a step callback, but we cannot destroy this object. + if (started_) + Abort(); +} + +void InteractionSequence::Start() { + // Ensure we're not already started. + DCHECK(!started_); + started_ = true; + if (missing_first_element_) { + Abort(); + return; + } + StageNextStep(); +} + +void InteractionSequence::OnElementShown(TrackedElement* element) { + DCHECK_EQ(StepType::kShown, next_step()->type); + DCHECK(element->identifier() == next_step()->id); + DoStepTransition(element); +} + +void InteractionSequence::OnElementActivated(TrackedElement* element) { + DCHECK_EQ(StepType::kActivated, next_step()->type); + DCHECK(element->identifier() == next_step()->id); + DoStepTransition(element); +} + +void InteractionSequence::OnElementHidden(TrackedElement* element) { + if (!started_) { + DCHECK_EQ(next_step()->element, element); + missing_first_element_ = true; + next_step()->subscription = ElementTracker::Subscription(); + next_step()->element = nullptr; + return; + } + + if (current_step_->element == element) { + // If the current step is marked as needing to remain visible and we haven't + // seen the triggering event for the next step, abort. + if (current_step_->must_remain_visible.value() && + !activated_during_callback_) { + Abort(); + return; + } + + // This element pointer is no longer valid and we can stop watching. + current_step_->subscription = ElementTracker::Subscription(); + current_step_->element = nullptr; + } + + // If we got a hidden callback and it wasn't to abort the current step, it + // must be because we're waiting on the next step to start. + if (next_step() && next_step()->id == element->identifier() && + next_step()->type == StepType::kHidden) { + DoStepTransition(element); + } +} + +void InteractionSequence::OnElementActivatedDuringStepTransition( + TrackedElement* element) { + if (!next_step()) + return; + + DCHECK(element->identifier() == next_step()->id); + next_step()->element = element; + next_step()->subscription = + ElementTracker::GetElementTracker()->AddElementHiddenCallback( + next_step()->id, context(), + base::BindRepeating( + &InteractionSequence::OnElementHiddenDuringStepTransition, + base::Unretained(this))); + + activated_during_callback_ = true; +} + +void InteractionSequence::OnElementHiddenDuringStepTransition( + TrackedElement* element) { + if (!next_step() || element != next_step()->element) + return; + + next_step()->element = nullptr; + next_step()->subscription = ElementTracker::Subscription(); +} + +void InteractionSequence::DoStepTransition(TrackedElement* element) { + // There are a number of callbacks during this method that could potentially + // result in this InteractionSequence being destructed, so maintain a weak + // pointer we can check to see if we need to bail out early. + base::WeakPtr<InteractionSequence> delete_guard = weak_factory_.GetWeakPtr(); + auto* const tracker = ElementTracker::GetElementTracker(); + { + // This block is non-re-entrant. + DCHECK(!processing_step_); + auto processing = + MakeSafeAutoReset(weak_factory_.GetWeakPtr(), + &InteractionSequence::processing_step_, true); + + // End the current step. + if (current_step_) { + // Unsubscribe from any events during the step-end process. Since the step + // has ended, conditions like "must remain visible" no longer apply. + current_step_->subscription = ElementTracker::Subscription(); + RunIfValid(std::move(current_step_->end_callback), current_step_->element, + current_step_->id, current_step_->type); + if (!delete_guard || AbortedDuringCallback()) + return; + } + + // Set up the new current step. + current_step_ = std::move(configuration_->steps.front()); + configuration_->steps.pop_front(); + DCHECK(!current_step_->element || current_step_->element == element); + current_step_->element = + current_step_->type == StepType::kHidden ? nullptr : element; + if (current_step_->element) { + current_step_->subscription = tracker->AddElementHiddenCallback( + current_step_->id, context(), + base::BindRepeating(&InteractionSequence::OnElementHidden, + base::Unretained(this))); + } else { + current_step_->subscription = ElementTracker::Subscription(); + } + + // Special care must be taken here, because theoretically *anything* could + // happen as a result of this callback. If the next step is a shown or + // hidden step and the element becomes shown or hidden (or it's a step that + // requires the element to be visible and it is not), then the appropriate + // transition (or Abort()) will happen in StageNextStep() below. + // + // If, however, the callback *activates* the next target element, and the + // next element is of type kActivated, then the activation will not + // register unless we explicitly listen for it. But we still don't want to + if (next_step() && next_step()->type == StepType::kActivated) { + next_step()->subscription = tracker->AddElementActivatedCallback( + next_step()->id, context(), + base::BindRepeating( + &InteractionSequence::OnElementActivatedDuringStepTransition, + base::Unretained((this)))); + } + + // Start the step. Like all callbacks, this could abort the sequence, or + // cause `element` to become invalid. Because of this we use the element + // field of the current step from here forward, because we've installed a + // callback above that will null it out if it becomes invalid. + RunIfValid(std::move(current_step_->start_callback), current_step_->element, + current_step_->id, current_step_->type); + if (!delete_guard || AbortedDuringCallback()) + return; + } + + if (configuration_->steps.empty()) { + // Reset anything that might cause state change during the final callback. + // After this, Abort() will have basically no effect, since by the time it + // gets called, both the aborted and step end callbacks will be null. + current_step_->subscription = ElementTracker::Subscription(); + configuration_->aborted_callback.Reset(); + // Last step end callback needs to be run before sequence completed. + // Because the InteractionSequence could conceivably be destroyed during + // one of these callbacks, make local copies of the callbacks and data. + CompletedCallback completed_callback = + std::move(configuration_->completed_callback); + std::unique_ptr<Step> last_step = std::move(current_step_); + RunIfValid(std::move(last_step->end_callback), last_step->element, + last_step->id, last_step->type); + RunIfValid(std::move(completed_callback)); + return; + } + + // Since we're not done, load up the next step. + StageNextStep(); +} + +void InteractionSequence::StageNextStep() { + auto* const tracker = ElementTracker::GetElementTracker(); + + Step* const next = next_step(); + DCHECK(!activated_during_callback_ || next->type == StepType::kActivated); + + // Note that if the target element for the next step was activated and then + // hidden during the previous step transition, `next_element` could be null. + TrackedElement* const next_element = + (activated_during_callback_ || next->element) + ? next->element + : tracker->GetFirstMatchingElement(next->id, context()); + + if (!activated_during_callback_ && next->must_be_visible.value() && + !next_element) { + // Fast forward to the next step before aborting so we get the correct + // information on the failed step in the abort callback. + current_step_ = std::move(configuration_->steps.front()); + configuration_->steps.pop_front(); + // We don't want to call the step-end callback during Abort() since we + // didn't technically start the step. + current_step_->end_callback = StepCallback(); + Abort(); + return; + } + + switch (next_step()->type) { + case StepType::kShown: + if (next_element) { + DoStepTransition(next_element); + } else { + next_step()->subscription = tracker->AddElementShownCallback( + next_step()->id, context(), + base::BindRepeating(&InteractionSequence::OnElementShown, + base::Unretained(this))); + } + break; + case StepType::kHidden: + if (!next_element) { + DoStepTransition(nullptr); + } else { + next_step()->subscription = tracker->AddElementHiddenCallback( + next_step()->id, context(), + base::BindRepeating(&InteractionSequence::OnElementHidden, + base::Unretained(this))); + } + break; + case StepType::kActivated: + if (activated_during_callback_) { + activated_during_callback_ = false; + DoStepTransition(next_element); + } else { + next_step()->subscription = tracker->AddElementActivatedCallback( + next_step()->id, context(), + base::BindRepeating(&InteractionSequence::OnElementActivated, + base::Unretained(this))); + } + break; + } +} + +void InteractionSequence::Abort() { + DCHECK(started_); + configuration_->steps.clear(); + if (current_step_) { + // Stop listening for events; we don't want additional callbacks during + // teardown. + current_step_->subscription = ElementTracker::Subscription(); + // The current step's element could go away during a callback, so hedge our + // bets by using a safe reference. + SafeElementReference element(current_step_->element); + // The entire InteractionSequence could also go away during a callback, so + // save anything we need locally so that we don't have to access any class + // members as we finish terminating the sequence. + std::unique_ptr<Step> last_step = std::move(current_step_); + AbortedCallback aborted_callback = + std::move(configuration_->aborted_callback); + RunIfValid(std::move(last_step->end_callback), element.get(), last_step->id, + last_step->type); + RunIfValid(std::move(aborted_callback), element.get(), last_step->id, + last_step->type); + } else { + // Aborted before any steps were run. Pass default values. + // Note that if the sequence has already been aborted, this is a no-op, the + // callback will already be null. + RunIfValid(std::move(configuration_->aborted_callback), nullptr, + ElementIdentifier(), StepType::kShown); + } +} + +bool InteractionSequence::AbortedDuringCallback() const { + // All step callbacks are sourced from the current step. If the current step + // is null, then the sequence must have aborted (which clears out the current + // step). Completion can only happen after step callbacks are finished + if (current_step_) + return false; + + DCHECK(configuration_->steps.empty()); + DCHECK(!configuration_->aborted_callback); + return true; +} + +InteractionSequence::Step* InteractionSequence::next_step() { + return configuration_->steps.empty() ? nullptr + : configuration_->steps.front().get(); +} + +ElementContext InteractionSequence::context() const { + return configuration_->context; +} + +} // namespace ui |