blob: fe1e33c721d274b5e30cd827130d13126af55f5c [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.
#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.
Subscription AddCustomEventInAnyContextCallback(
CustomElementEventType event_type,
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_