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 UI_BASE_INTERACTION_ELEMENT_TRACKER_H_
#define UI_BASE_INTERACTION_ELEMENT_TRACKER_H_

#include <concepts>
#include <list>
#include <map>
#include <memory>
#include <optional>
#include <vector>

#include "base/callback_list.h"
#include "base/component_export.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/framework_specific_implementation.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_ui_types.h"

namespace ui {

// Represents a unique type of event, you may create these as needed using the
// DECLARE_CUSTOM_ELEMENT_EVENT_TYPE() and DEFINE_CUSTOM_ELEMENT_EVENT_TYPE()
// macros (see definitions at the bottom of this file).
//
// For testing purposes, if you need a local event type guaranteed to avoid
// global name collisions, use DEFINE_LOCAL_ELEMENT_EVENT_TYPE() instead.
//
// Currently, custom event types are imlpemented using ElementIdentifier, since
// both have the same API requirements.
using CustomElementEventType = ElementIdentifier;

// Represents a visible UI element in a platform-agnostic manner.
//
// A pointer to this object may be stored after the element becomes visible, but
// is only valid until the "element hidden" event is called for this element;
// see `ElementTracker` below. If you want to hold a pointer that will be valid
// only as long as the element is visible, use a SafeElementReference.
//
// You should derive a class for each UI framework whose elements you wish to
// track. See README.md for information on how to create your own framework
// implementations.
class COMPONENT_EXPORT(UI_BASE_INTERACTION) TrackedElement
    : public FrameworkSpecificImplementation {
 public:
  ~TrackedElement() override;

  ElementIdentifier identifier() const { return identifier_; }
  ElementContext context() const { return context_; }

  // Returns the bounds of the element on the screen, or an empty rect if it
  // cannot be determined.
  //
  // Note: it is not yet necessary to set up a general method for listening to
  // bounds changes, as they are (a) somewhat difficult to track and (b) tend to
  // be handled correctly by most frameworks in terms of element positioning
  // (e.g. anchoring logic for User Education help bubbles). Specific
  // implementations that need to do additional tracking can implement their own
  // methods.
  virtual gfx::Rect GetScreenBounds() const;

  // Returns the native view associated with this element, if any. This view is
  // used as the parent window for anchoring secondary UIs.
  virtual gfx::NativeView GetNativeView() const;

  // FrameworkSpecificImplementation:
  std::string ToString() const override;

 protected:
  TrackedElement(ElementIdentifier identifier, ElementContext context);

 private:
  // The identifier for this element that will be used by ElementTracker to
  // retrieve it.
  const ElementIdentifier identifier_;

  // The context of the element, corresponding to the main window the element is
  // associated with. See the ElementContext documentation in
  // element_identifier.h for more information on how to create appropriate
  // contexts for each UI framework.
  const ElementContext context_;
};

// Provides a delegate for UI framework-specific implementations to notify of
// element tracker events.
//
// An element must be visible before events can be sent for that element;
// NotifyElementHidden() must be called before the element is destroyed or
// changes context or identifier.
class COMPONENT_EXPORT(UI_BASE_INTERACTION) ElementTrackerFrameworkDelegate {
 public:
  virtual void NotifyElementShown(TrackedElement* element) = 0;
  virtual void NotifyElementActivated(TrackedElement* element) = 0;
  virtual void NotifyElementHidden(TrackedElement* element) = 0;
  virtual void NotifyCustomEvent(TrackedElement* element,
                                 CustomElementEventType event_type) = 0;
};

// Tracks elements as they become visible, are activated by the user, and
// eventually become hidden. Tracks only visible elements.
//
// NOT THREAD SAFE. Should only be accessed from the main UI thread.
class COMPONENT_EXPORT(UI_BASE_INTERACTION) ElementTracker
    : ElementTrackerFrameworkDelegate {
 public:
  // Callback that subscribers receive when the specified event occurs.
  // Note that if an element is destroyed in the middle of calling callbacks,
  // some callbacks may not be called and others may be called with a null
  // argument, so please check the validity of the element pointer.
  using Callback = base::RepeatingCallback<void(TrackedElement*)>;
  using Subscription = base::CallbackListSubscription;
  using ElementList = std::vector<TrackedElement*>;
  using Contexts = std::set<ElementContext>;

  // Identifier that should be used by each framework to create a
  // TrackedElement from an element that does not alreayd have an identifier.
  //
  // Currently, the identifier is not removed when the code that needs the
  // element completes, but in the future we may implement a ref-counting
  // system for systems that use a temporary identifier so that it does not
  // persist longer than it is needed.
  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kTemporaryIdentifier);

  // Gets the element tracker to be used by clients to subscribe to and receive
  // events.
  static ElementTracker* GetElementTracker();

  // Gets the delegate to be used by specific UI frameworks to send events.
  static ElementTrackerFrameworkDelegate* GetFrameworkDelegate();

  // Returns either the one element matching the given `id` and `context`, or
  // null if there are none. Will generate an error if there is more than one
  // element with `id` in `context`. Only visible elements are returned.
  //
  // Use when you want to verify that there's only one matching element in the
  // given context.
  TrackedElement* GetUniqueElement(ElementIdentifier id,
                                   ElementContext context);

  // Returns the same result as GetUniqueElement() except that no error is
  // generated if there is more than one matching element.
  //
  // Use when you just need *an* element in the given context, and don't care if
  // there's more than one.
  TrackedElement* GetFirstMatchingElement(ElementIdentifier id,
                                          ElementContext context);

  // Returns an element with identifier `id` from any context, or null if not
  // found. Contexts are not guaranteed to be searched in any particular order.
  TrackedElement* GetElementInAnyContext(ElementIdentifier id);

  // Returns a list of all visible elements with identifier `id` in `context`.
  // The list may be empty.
  ElementList GetAllMatchingElements(ElementIdentifier id,
                                     ElementContext context);

  // Returns all known elements with the given `id`. The context for each can
  // be retrieved from the TrackedElement itself. No order is guaranteed.
  ElementList GetAllMatchingElementsInAnyContext(ElementIdentifier id);

  // Returns whether an element with identifier `id` in `context` is visible.
  bool IsElementVisible(ElementIdentifier id, ElementContext context);

  // Adds a callback that will be called whenever an element with identifier
  // `id` in `context` becomes visible.
  Subscription AddElementShownCallback(ElementIdentifier id,
                                       ElementContext context,
                                       Callback callback);

  // Adds a callback that will be called whenever an element with identifier
  // `id` becomes visible in any context.
  Subscription AddElementShownInAnyContextCallback(ElementIdentifier id,
                                                   Callback callback);

  // Adds a callback that will be called whenever an element with identifier
  // `id` in `context` is activated by the user.
  Subscription AddElementActivatedCallback(ElementIdentifier id,
                                           ElementContext context,
                                           Callback callback);

  // Adds a callback that will be called whenever an element with identifier
  // `id` is activated in any context.
  Subscription AddElementActivatedInAnyContextCallback(ElementIdentifier id,
                                                       Callback callback);

  // Adds a callback that will be called whenever an element with identifier
  // `id` in `context` is hidden.
  //
  // Note: the TrackedElement* passed to the callback may not remain
  // valid after the call, even if the same element object in its UI framework
  // is re-shown (a new TrackedElement may be generated).
  Subscription AddElementHiddenCallback(ElementIdentifier id,
                                        ElementContext context,
                                        Callback callback);

  // Adds a callback that will be called whenever an element with identifier
  // `id` is hidden in any context.
  //
  // Note: the TrackedElement* passed to the callback may not remain
  // valid after the call, even if the same element object in its UI framework
  // is re-shown (a new TrackedElement may be generated).
  Subscription AddElementHiddenInAnyContextCallback(ElementIdentifier id,
                                                    Callback callback);

  // Adds a callback that will be called whenever an event of `event_type` is
  // generated within `context` by any element.
  Subscription AddCustomEventCallback(CustomElementEventType event_type,
                                      ElementContext context,
                                      Callback callback);

  // Adds a callback that will be called whenever an event of `event_type` is
  // generated within any context by any element.
  Subscription AddCustomEventInAnyContextCallback(
      CustomElementEventType event_type,
      Callback callback);

  // Adds a callback that will be called whenever an event of `event_type` is
  // generated within `context` by an element with identifier `id`.
  Subscription AddCustomEventCallback(CustomElementEventType event_type,
                                      ElementIdentifier id,
                                      ElementContext context,
                                      Callback callback);

  // Adds a callback that will be called whenever an event of `event_type` is
  // generated within any context by an element with identifier `id`.
  Subscription AddCustomEventInAnyContextCallback(
      CustomElementEventType event_type,
      ElementIdentifier id,
      Callback callback);

  // Returns all known contexts.
  Contexts GetAllContextsForTesting() const;

  // Returns a list of all elements. Should only be used in a meta-testing
  // context, e.g. for testing the tracker itself, or for getting lists of
  // candidate elements for fuzzing input.
  //
  // If `in_context` is specified, only elements in that context will be
  // returned.
  ElementList GetAllElementsForTesting(
      std::optional<ElementContext> in_context = std::nullopt);

  // Adds a callback when any element is shown.
  Subscription AddAnyElementShownCallbackForTesting(Callback callback);

 private:
  friend class base::NoDestructor<ElementTracker>;
  class ElementData;
  class GarbageCollector;
  using LookupKey = std::pair<ElementIdentifier, ElementContext>;
  FRIEND_TEST_ALL_PREFIXES(ElementTrackerTest, CleanupAfterElementHidden);
  FRIEND_TEST_ALL_PREFIXES(ElementTrackerTest, CleanupAfterCallbacksRemoved);
  FRIEND_TEST_ALL_PREFIXES(ElementTrackerTest, HideDuringShowCallback);

  ElementTracker();
  ~ElementTracker();

  // ElementTrackerFrameworkDelegate:
  void NotifyElementShown(TrackedElement* element) override;
  void NotifyElementActivated(TrackedElement* element) override;
  void NotifyElementHidden(TrackedElement* element) override;
  void NotifyCustomEvent(TrackedElement* element,
                         CustomElementEventType event_type) override;

  ElementData* GetOrAddElementData(ElementIdentifier id,
                                   ElementContext context);

  void MaybeCleanup(ElementData* data);

  // Use a list to keep track of elements we're in the process of sending
  // notifications for; this allows us to zero out the reference in realtime if
  // the element is deleted. We use a list because the individual elements need
  // to be memory-stable.
  std::list<raw_ptr<TrackedElement, CtnExperimental>> notification_elements_;
  std::map<LookupKey, ElementData> element_data_;
  base::RepeatingCallbackList<void(TrackedElement*)>
      any_element_shown_callbacks_;
  std::unique_ptr<GarbageCollector> gc_;
};

// Holds an TrackedElement reference and nulls it out if the element goes
// away. In other words, acts as a weak reference for TrackedElements.
class COMPONENT_EXPORT(UI_BASE_INTERACTION) SafeElementReference {
 public:
  SafeElementReference();
  explicit SafeElementReference(TrackedElement* element);
  SafeElementReference(SafeElementReference&& other);
  SafeElementReference(const SafeElementReference& other);
  SafeElementReference& operator=(TrackedElement* element);
  SafeElementReference& operator=(SafeElementReference&& other);
  SafeElementReference& operator=(const SafeElementReference& other);
  ~SafeElementReference();

  TrackedElement* get() const { return element_; }
  explicit operator bool() const { return element_; }
  bool operator!() const { return !element_; }
  bool operator==(const SafeElementReference& other) const {
    return element_ == other.element_;
  }
  bool operator==(const TrackedElement* other) const {
    return element_ == other;
  }

  // Gets the held element as type T if present, null if not present or not a T.
  template <typename T>
    requires std::derived_from<T, TrackedElement>
  T* get_as() const {
    return element_ ? element_->AsA<T>() : nullptr;
  }

 private:
  void Subscribe();
  void OnElementHidden(TrackedElement* element);

  ElementTracker::Subscription subscription_;
  raw_ptr<TrackedElement> element_ = nullptr;
};

}  // namespace ui

// Macros for declaring custom element event types. Put the DECLARE call in
// your public header file and the DEFINE in corresponding .cc file. For local
// values to be used in tests, use DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE()
// defined below instead.
//
// Note: if you need to use the identifier outside the current component, use
// DECLARE/DEFINE_EXPORTED_... below.
#define DECLARE_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
  DECLARE_ELEMENT_IDENTIFIER_VALUE(EventName)
#define DEFINE_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
  DEFINE_ELEMENT_IDENTIFIER_VALUE(EventName)

// Macros for declaring custom element event types that can be accessed in other
// components. Put the DECLARE call in your public header file and the DEFINE
// call in the corresponding .cc file.
#define DECLARE_EXPORTED_CUSTOM_ELEMENT_EVENT_TYPE(ExportName, EventName) \
  DECLARE_EXPORTED_ELEMENT_IDENTIFIER_VALUE(ExportName, EventName)
#define DEFINE_EXPORTED_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
  DEFINE_EXPORTED_ELEMENT_IDENTIFIER_VALUE(EventName)

// Macros for declaring custom class element event type. Put the DECLARE call in
// your .h file in your class declaration, and the DEFINE in the corresponding
// .cc file.
#define DECLARE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(EventName)
#define DEFINE_CLASS_CUSTOM_ELEMENT_EVENT_TYPE(ClassName, EventName) \
  DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ClassName, EventName)

// This produces a unique, mangled name that can safely be used in macros called
// by tests without having to worry about global name collisions. For production
// code, use DECLARE/DEFINE above instead. You should pass __FILE__ and __LINE__
// for `File`, and `Line`, respectively.
#define DEFINE_MACRO_CUSTOM_ELEMENT_EVENT_TYPE(File, Line, EventName) \
  DEFINE_MACRO_ELEMENT_IDENTIFIER_VALUE(File, Line, EventName)

// This produces a unique, mangled name that can safely be used in tests
// without having to worry about global name collisions. For production code,
// use DECLARE/DEFINE above instead.
#define DEFINE_LOCAL_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
  DEFINE_MACRO_ELEMENT_IDENTIFIER_VALUE(__FILE__, __LINE__, EventName)

#endif  // UI_BASE_INTERACTION_ELEMENT_TRACKER_H_