blob: 8331db72c8dabf4f1b5281cd252ebb2626135277 [file] [log] [blame]
// 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.
#ifndef UI_BASE_INTERACTION_ELEMENT_TRACKER_H_
#define UI_BASE_INTERACTION_ELEMENT_TRACKER_H_
#include <map>
#include <memory>
#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 "base/notreached.h"
#include "base/observer_list_types.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/framework_specific_implementation.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) TrackedElement
: public FrameworkSpecificImplementation {
public:
~TrackedElement() override;
ElementIdentifier identifier() const { return identifier_; }
ElementContext context() const { return context_; }
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) 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) ElementTracker
: ElementTrackerFrameworkDelegate {
public:
using Callback = base::RepeatingCallback<void(TrackedElement*)>;
using Subscription = base::CallbackListSubscription;
using ElementList = std::vector<TrackedElement*>;
// 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 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` 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` 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 event of `event_type` is
// generated within `context` by any element.
Subscription AddCustomEventCallback(CustomElementEventType event_type,
ElementContext context,
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);
std::map<LookupKey, ElementData> element_data_;
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) SafeElementReference {
public:
SafeElementReference();
explicit SafeElementReference(TrackedElement* element);
SafeElementReference(SafeElementReference&& other);
SafeElementReference(const SafeElementReference& other);
SafeElementReference& operator=(SafeElementReference&& other);
SafeElementReference& operator=(const SafeElementReference& other);
~SafeElementReference();
TrackedElement* get() { return element_; }
explicit operator bool() const { return element_; }
bool operator!() const { return !element_; }
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.
#define DECLARE_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
DECLARE_ELEMENT_IDENTIFIER_VALUE(EventName)
#define DEFINE_CUSTOM_ELEMENT_EVENT_TYPE(EventName) \
DEFINE_ELEMENT_IDENTIFIER_VALUE(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_LOCAL_ELEMENT_IDENTIFIER_VALUE(EventName)
#endif // UI_BASE_INTERACTION_ELEMENT_TRACKER_H_