| // 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. |
| |
| #include "ui/base/interaction/element_tracker_mac.h" |
| |
| #include <map> |
| #include <memory> |
| |
| #include "base/containers/contains.h" |
| #include "base/logging.h" |
| #include "base/no_destructor.h" |
| #include "ui/base/interaction/element_identifier.h" |
| |
| namespace ui { |
| |
| DEFINE_FRAMEWORK_SPECIFIC_METADATA(TrackedElementMac) |
| |
| TrackedElementMac::TrackedElementMac(ElementIdentifier identifier, |
| ElementContext context, |
| const gfx::Rect& screen_bounds) |
| : TrackedElement(identifier, context), screen_bounds_(screen_bounds) {} |
| |
| TrackedElementMac::~TrackedElementMac() = default; |
| |
| gfx::Rect TrackedElementMac::GetScreenBounds() const { |
| return screen_bounds_; |
| } |
| |
| // Holds all data regarding elements in a specific NSMenu or its children, and |
| // handles dispatching of ElementTracker events. |
| class ElementTrackerMac::MenuData { |
| public: |
| explicit MenuData(ElementContext context) : context_(context) { |
| DCHECK(context); |
| } |
| |
| ~MenuData() { |
| HideElements(recycle_bin_); |
| LOG_IF(WARNING, !elements_.empty()) |
| << "Destroying menu data before all elements are hidden."; |
| HideElements(elements_); |
| } |
| |
| MenuData(const MenuData& other) = delete; |
| void operator=(const MenuData& other) = delete; |
| |
| ElementContext context() const { return context_; } |
| |
| // Adds an element representing a menu item. The item must not already exist. |
| void AddElement(ElementIdentifier identifier, |
| const gfx::Rect& screen_bounds) { |
| // On show of a new menu or submenu, clear out any menu items waiting to be |
| // garbage-collected, since an activation won't happen. |
| HideElements(recycle_bin_); |
| |
| // Insert an entry for the new element and verify that it is correct. |
| const auto result = |
| elements_.emplace(identifier, std::make_unique<TrackedElementMac>( |
| identifier, context_, screen_bounds)); |
| DCHECK(result.second); |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyElementShown( |
| result.first->second.get()); |
| } |
| |
| // Notifies that the specified element has been activated. Also checks |
| // elements in the recycle bin, in case hide and activate came arrived in the |
| // wrong order. |
| void ActivateElement(ElementIdentifier identifier) { |
| auto it = elements_.find(identifier); |
| if (it == elements_.end()) { |
| it = recycle_bin_.find(identifier); |
| if (it == recycle_bin_.end()) { |
| NOTREACHED() |
| << "Element " << identifier |
| << " had its activation sent after its menu was destroyed. This " |
| "may be due to a race condition with renderer context menus; " |
| "see crbug.com/1418614 for the state of current efforts to " |
| "diagnose and fix the problem."; |
| return; |
| } |
| LOG(WARNING) << "Element " << identifier |
| << " activated after hide signal received."; |
| } |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyElementActivated( |
| it->second.get()); |
| } |
| |
| // Notifies that the element with `identifier` was hidden. Instead of deleting |
| // the element right away, it is instead put in the `recycle_bin_` for |
| // disposal when it is certain no activation event will occur. |
| void HideElement(ElementIdentifier identifier) { |
| const auto it = elements_.find(identifier); |
| if (it == elements_.end()) { |
| if (base::Contains(recycle_bin_, identifier)) { |
| LOG(WARNING) << "Element " << identifier |
| << " hidden multiple times in a row."; |
| } else { |
| NOTREACHED() << "Element " << identifier |
| << " hidden after its menu was destroyed."; |
| } |
| return; |
| } |
| auto result = recycle_bin_.emplace(identifier, std::move(it->second)); |
| DCHECK(result.second); |
| elements_.erase(it); |
| } |
| |
| private: |
| using ElementMap = |
| std::map<ElementIdentifier, std::unique_ptr<TrackedElementMac>>; |
| |
| // Hides all of the elements in `map` and then clears the map. Used to do |
| // final disposal of TrackedElementMac objects owned by this object. |
| void HideElements(ElementMap& map) { |
| for (auto& [identifier, element] : map) { |
| ui::ElementTracker::GetFrameworkDelegate()->NotifyElementHidden( |
| element.get()); |
| } |
| map.clear(); |
| } |
| |
| const ElementContext context_; |
| |
| // Keeps track of all "live" elements being tracked by this object. When they |
| // are hidden, they are moved to `recycle_bin_` to await final disposal. |
| ElementMap elements_; |
| |
| // Because activation and hide events can come in reverse order in some corner |
| // cases (see crbug.com/1432480) elements are kept around between the time |
| // they are notified as hidden and when they are actually destroyed, which is |
| // when the menu is destroyed or when a new submenu opens. |
| ElementMap recycle_bin_; |
| }; |
| |
| // static |
| ElementTrackerMac* ElementTrackerMac::GetInstance() { |
| static base::NoDestructor<ElementTrackerMac> instance; |
| return instance.get(); |
| } |
| |
| void ElementTrackerMac::NotifyMenuWillShow(NSMenu* menu, |
| ElementContext context) { |
| const auto result = root_menu_to_data_.emplace(menu, context); |
| DCHECK(result.second); |
| } |
| |
| void ElementTrackerMac::NotifyMenuDoneShowing(NSMenu* menu) { |
| const auto result = root_menu_to_data_.erase(menu); |
| DCHECK(result); |
| } |
| |
| void ElementTrackerMac::NotifyMenuItemShown(NSMenu* menu, |
| ElementIdentifier identifier, |
| const gfx::Rect& screen_bounds) { |
| const auto it = root_menu_to_data_.find(GetRootMenu(menu)); |
| if (it != root_menu_to_data_.end()) { |
| it->second.AddElement(identifier, screen_bounds); |
| } |
| } |
| |
| void ElementTrackerMac::NotifyMenuItemActivated(NSMenu* menu, |
| ElementIdentifier identifier) { |
| const auto it = root_menu_to_data_.find(GetRootMenu(menu)); |
| if (it != root_menu_to_data_.end()) { |
| it->second.ActivateElement(identifier); |
| } |
| } |
| |
| void ElementTrackerMac::NotifyMenuItemHidden(NSMenu* menu, |
| ElementIdentifier identifier) { |
| const auto it = root_menu_to_data_.find(GetRootMenu(menu)); |
| if (it != root_menu_to_data_.end()) { |
| it->second.HideElement(identifier); |
| } |
| } |
| |
| NSMenu* ElementTrackerMac::GetRootMenuForContext(ElementContext context) { |
| for (auto& [menu, data] : root_menu_to_data_) { |
| if (data.context() == context) { |
| return menu; |
| } |
| } |
| return nullptr; |
| } |
| |
| ElementTrackerMac::ElementTrackerMac() = default; |
| ElementTrackerMac::~ElementTrackerMac() = default; |
| |
| NSMenu* ElementTrackerMac::GetRootMenu(NSMenu* menu) const { |
| while (menu.supermenu) { |
| menu = menu.supermenu; |
| } |
| return menu; |
| } |
| |
| } // namespace ui |