#include "ui/views/interaction/element_tracker_views.h"
#include <algorithm>
#include <list>
#include <map>
#include <memory>
#include <string>
#include "base/auto_reset.h"
#include "base/containers/contains.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/scoped_multi_source_observation.h"
#include "base/scoped_observation.h"
#include "base/types/pass_key.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
namespace views {
TrackedElementViews::TrackedElementViews(View* view,
ui::ElementIdentifier identifier,
ui::ElementContext context)
: TrackedElement(identifier, context), view_(view) {}
TrackedElementViews::~TrackedElementViews() = default;
gfx::Rect TrackedElementViews::GetScreenBounds() const {
return view()->GetBoundsInScreen();
}
gfx::NativeView TrackedElementViews::GetNativeView() const {
if (!view()->GetWidget()) {
return gfx::NativeView();
}
return view()->GetWidget()->GetNativeView();
}
std::string TrackedElementViews::ToString() const {
auto result = TrackedElement::ToString();
result.append(" with view ");
result.append(view()->GetClassName());
return result;
}
DEFINE_FRAMEWORK_SPECIFIC_METADATA(TrackedElementViews)
class ElementTrackerViews::ElementDataViews : public ViewObserver,
public WidgetObserver {
public:
ElementDataViews(ElementTrackerViews* tracker,
ui::ElementIdentifier identifier)
: tracker_(tracker), id_(identifier) {}
~ElementDataViews() override = default;
void AddView(View* view) {
if (base::Contains(view_data_lookup_, view)) {
return;
}
const auto it = view_data_.insert(view_data_.end(),
ViewData(view, GetContextForView(view)));
view_data_lookup_.emplace(view, it);
view_observer_.AddObservation(view);
tracker_->MaybeTrackWidget(view->GetWidget());
UpdateVisible(view);
}
void RemoveView(View* view) {
const auto it = view_data_lookup_.find(view);
if (it == view_data_lookup_.end()) {
return;
}
if (it->second->visible()) {
ui::ElementTracker::GetFrameworkDelegate()->NotifyElementHidden(
it->second->element.get());
}
view_observer_.RemoveObservation(view);
view_data_.erase(it->second);
view_data_lookup_.erase(it);
if (view_data_.empty()) {
tracker_->element_data_.erase(id_);
}
}
TrackedElementViews* GetElementForView(View* view) {
const auto it = view_data_lookup_.find(view);
CHECK(it != view_data_lookup_.end());
return it->second->element.get();
}
void NotifyViewActivated(View* view) {
const auto it = view_data_lookup_.find(view);
CHECK(it != view_data_lookup_.end());
if (it->second->visible()) {
ui::ElementTracker::GetFrameworkDelegate()->NotifyElementActivated(
it->second->element.get());
} else {
DLOG(WARNING)
<< "View " << view->GetClassName()
<< " activated before it was made visible. This probably happened"
" during a test; it should never happen in a live browser.";
}
}
void UpdateViewVisibilityForWidget(Widget* widget, bool visible) {
for (auto& entry : view_data_) {
if (entry.visible() != visible && entry.view->GetWidget() == widget) {
UpdateVisible(entry.view);
}
}
}
View* FindFirstViewInContext(ui::ElementContext context,
bool require_visible) {
for (const ViewData& data : view_data_) {
if (data.context == context && (!require_visible || data.visible())) {
return data.view;
}
}
return nullptr;
}
ViewList FindAllViewsInContext(ui::ElementContext context) {
ViewList result;
for (const ViewData& data : view_data_) {
if (data.context == context) {
result.push_back(data.view);
}
}
return result;
}
ViewList GetAllViews() {
ViewList result;
std::ranges::transform(view_data_lookup_, std::back_inserter(result),
&ViewDataMap::value_type::first);
return result;
}
private:
enum class VisibilityUpdateReason {
kHidden,
kRemovedFromWidget,
kUnspecified,
};
struct ViewData {
explicit ViewData(View* v, ui::ElementContext initial_context)
: view(v), context(initial_context) {}
bool visible() const { return static_cast<bool>(element); }
const raw_ptr<View> view;
bool notifying_hidden = false;
ui::ElementContext context;
std::unique_ptr<TrackedElementViews> element;
};
using ViewDataList = std::list<ViewData>;
using ViewDataMap = std::map<View*, ViewDataList::iterator>;
void OnViewVisibilityChanged(View* observed_view,
View* starting_view,
bool visible) override {
UpdateVisible(observed_view, visible ? VisibilityUpdateReason::kUnspecified
: VisibilityUpdateReason::kHidden);
}
void OnViewAddedToWidget(View* observed_view) override {
tracker_->MaybeTrackWidget(observed_view->GetWidget());
UpdateVisible(observed_view);
}
void OnViewRemovedFromWidget(View* observed_view) override {
UpdateVisible(observed_view, VisibilityUpdateReason::kRemovedFromWidget);
}
void OnViewIsDeleting(View* observed_view) override {
RemoveView(observed_view);
}
bool IsViewVisibleToUser(View* view) {
const Widget* const widget = view->GetWidget();
return widget && !widget->IsClosed() && tracker_->IsWidgetVisible(widget) &&
view->IsDrawn();
}
void UpdateVisible(View* view,
VisibilityUpdateReason update_reason =
VisibilityUpdateReason::kUnspecified) {
const auto it = view_data_lookup_.find(view);
CHECK(it != view_data_lookup_.end());
ViewData& data = *it->second;
const ui::ElementContext old_context = data.context;
data.context = (update_reason == VisibilityUpdateReason::kRemovedFromWidget)
? ui::ElementContext()
: GetContextForView(view);
const bool was_visible = data.visible();
const bool visible = (update_reason != VisibilityUpdateReason::kHidden) &&
data.context && IsViewVisibleToUser(view);
if (visible && !was_visible) {
data.element =
std::make_unique<TrackedElementViews>(view, id_, data.context);
ui::ElementTracker::GetFrameworkDelegate()->NotifyElementShown(
data.element.get());
} else if (!visible && was_visible) {
if (!data.notifying_hidden) {
base::AutoReset<bool> auto_reset(&data.notifying_hidden, true);
ui::ElementTracker::GetFrameworkDelegate()->NotifyElementHidden(
data.element.get());
data.element.reset();
}
} else if (visible) {
CHECK_EQ(data.context, old_context)
<< "We should always get a removed-from-widget notification before "
"an added-to-widget notification, the context should never "
"change while a view is visible.";
}
}
const raw_ptr<ElementTrackerViews> tracker_;
const ui::ElementIdentifier id_;
ViewDataList view_data_;
ViewDataMap view_data_lookup_;
base::ScopedMultiSourceObservation<View, ViewObserver> view_observer_{this};
};
class ElementTrackerViews::WidgetTracker : public WidgetObserver {
public:
WidgetTracker(ElementTrackerViews* tracker, Widget* widget)
: tracker_(tracker), widget_(widget) {
observation_.Observe(widget);
DCHECK(!widget->IsVisible());
}
bool visible() const { return visible_; }
private:
void OnWidgetDestroying(Widget* widget) override { Remove(); }
void OnWidgetVisibilityChanged(Widget* widget, bool visible) override {
auto* const tracker = tracker_.get();
bool needs_update = visible;
if (widget->IsVisible()) {
Remove();
} else if (!visible) {
needs_update = visible_;
visible_ = false;
} else {
visible_ = true;
}
if (needs_update) {
for (auto& [id, data] : tracker->element_data_) {
data.UpdateViewVisibilityForWidget(widget, visible);
}
}
}
void Remove() {
tracker_->widget_trackers_.erase(widget_);
}
const raw_ptr<ElementTrackerViews> tracker_;
const raw_ptr<Widget> widget_;
bool visible_ = false;
base::ScopedObservation<Widget, WidgetObserver> observation_{this};
};
ElementTrackerViews::ElementTrackerViews() = default;
ElementTrackerViews::~ElementTrackerViews() = default;
void ElementTrackerViews::SetContextOverrideCallback(
ContextOverrideCallback callback) {
GetContextOverrideCallback() = callback;
}
ElementTrackerViews* ElementTrackerViews::GetInstance() {
static base::NoDestructor<ElementTrackerViews> instance;
return instance.get();
}
ui::ElementContext ElementTrackerViews::GetContextForView(View* view) {
Widget* const widget = view->GetWidget();
return widget ? GetContextForWidget(widget) : ui::ElementContext();
}
ui::ElementContext ElementTrackerViews::GetContextForWidget(Widget* widget) {
auto* const primary = widget->GetPrimaryWindowWidget();
if (auto& callback = GetContextOverrideCallback()) {
if (ui::ElementContext context = callback.Run(primary)) {
return context;
}
}
return ui::ElementContext(primary, base::PassKey<ElementTrackerViews>());
}
TrackedElementViews* ElementTrackerViews::GetElementForView(
View* view,
bool assign_temporary_id) {
ui::ElementIdentifier identifier = view->GetProperty(kElementIdentifierKey);
if (!identifier) {
if (!assign_temporary_id) {
return nullptr;
}
DCHECK(view->GetWidget());
identifier = ui::ElementTracker::kTemporaryIdentifier;
view->SetProperty(kElementIdentifierKey, identifier);
}
const auto it = element_data_.find(identifier);
if (it == element_data_.end()) {
DCHECK(!assign_temporary_id);
return nullptr;
}
return it->second.GetElementForView(view);
}
const TrackedElementViews* ElementTrackerViews::GetElementForView(
const View* view) const {
return const_cast<ElementTrackerViews*>(this)->GetElementForView(
const_cast<View*>(view), false);
}
View* ElementTrackerViews::GetUniqueView(ui::ElementIdentifier id,
ui::ElementContext context) {
ui::TrackedElement* const element =
ui::ElementTracker::GetElementTracker()->GetUniqueElement(id, context);
return element ? element->AsA<TrackedElementViews>()->view() : nullptr;
}
View* ElementTrackerViews::GetFirstMatchingView(ui::ElementIdentifier id,
ui::ElementContext context,
bool require_visible) {
const auto it = element_data_.find(id);
if (it == element_data_.end()) {
return nullptr;
}
return it->second.FindFirstViewInContext(context, require_visible);
}
ElementTrackerViews::ViewList ElementTrackerViews::GetAllMatchingViews(
ui::ElementIdentifier id,
ui::ElementContext context) {
const auto it = element_data_.find(id);
if (it == element_data_.end()) {
return ViewList();
}
return it->second.FindAllViewsInContext(context);
}
ElementTrackerViews::ViewList
ElementTrackerViews::GetAllMatchingViewsInAnyContext(ui::ElementIdentifier id) {
const auto it = element_data_.find(id);
if (it == element_data_.end()) {
return ViewList();
}
return it->second.GetAllViews();
}
Widget* ElementTrackerViews::GetWidgetForContext(ui::ElementContext context) {
for (auto& [id, data] : element_data_) {
auto* const view =
data.FindFirstViewInContext(context, false);
if (view) {
return view->GetWidget();
}
}
return nullptr;
}
bool ElementTrackerViews::NotifyCustomEvent(
ui::CustomElementEventType event_type,
View* view) {
auto* const element = GetElementForView(view, true);
if (!element) {
return false;
}
ui::ElementTracker::GetFrameworkDelegate()->NotifyCustomEvent(element,
event_type);
return true;
}
void ElementTrackerViews::RegisterView(ui::ElementIdentifier element_id,
View* view) {
const auto [it, added] =
element_data_.try_emplace(element_id, this, element_id);
it->second.AddView(view);
}
void ElementTrackerViews::UnregisterView(ui::ElementIdentifier element_id,
View* view) {
DCHECK(view);
const auto it = element_data_.find(element_id);
CHECK(it != element_data_.end());
it->second.RemoveView(view);
}
void ElementTrackerViews::NotifyViewActivated(ui::ElementIdentifier element_id,
View* view) {
DCHECK(view);
const auto it = element_data_.find(element_id);
CHECK(it != element_data_.end());
it->second.NotifyViewActivated(view);
}
ElementTrackerViews::ContextOverrideCallback&
ElementTrackerViews::GetContextOverrideCallback() {
static base::NoDestructor<ContextOverrideCallback> callback;
return *callback.get();
}
void ElementTrackerViews::MaybeTrackWidget(Widget* widget) {
if (!widget || widget->IsVisible()) {
return;
}
widget_trackers_.try_emplace(widget, this, widget);
}
bool ElementTrackerViews::IsWidgetVisible(const Widget* widget) const {
if (widget->IsVisible()) {
return true;
}
const auto it = widget_trackers_.find(widget);
return it != widget_trackers_.end() && it->second.visible();
}
}