#include "ui/views/controls/menu/submenu_view.h"
#include <algorithm>
#include <numeric>
#include <set>
#include "base/compiler_specific.h"
#include "base/containers/contains.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/owned_window_anchor.h"
#include "ui/base/ui_base_types.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_host.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
namespace {
constexpr int kDropIndicatorHeight = 2;
}
namespace views {
SubmenuView::SubmenuView(MenuItemView* parent)
: parent_menu_item_(parent),
host_(nullptr),
drop_item_(nullptr),
scroll_view_container_(nullptr),
scroll_animator_(new ScrollAnimator(this)),
prefix_selector_(this, this) {
DCHECK(parent);
set_owned_by_client();
}
SubmenuView::~SubmenuView() {
Close();
delete scroll_view_container_;
}
bool SubmenuView::HasEmptyMenuItemView() const {
return base::Contains(children(), MenuItemView::kEmptyMenuItemViewID,
&View::GetID);
}
bool SubmenuView::HasVisibleChildren() const {
return base::ranges::any_of(GetMenuItems(), [](const MenuItemView* item) {
return item->GetVisible();
});
}
SubmenuView::MenuItems SubmenuView::GetMenuItems() const {
MenuItems menu_items;
for (View* child : children()) {
if (child->GetID() == MenuItemView::kMenuItemViewID)
menu_items.push_back(static_cast<MenuItemView*>(child));
}
return menu_items;
}
MenuItemView* SubmenuView::GetMenuItemAt(size_t index) {
const MenuItems menu_items = GetMenuItems();
DCHECK_LT(index, menu_items.size());
return menu_items[index];
}
PrefixSelector* SubmenuView::GetPrefixSelector() {
return &prefix_selector_;
}
void SubmenuView::ChildPreferredSizeChanged(View* child) {
if (!resize_open_menu_)
return;
MenuItemView* item = parent_menu_item_;
MenuController* controller = item->GetMenuController();
if (controller) {
bool dir;
ui::OwnedWindowAnchor anchor;
gfx::Rect bounds =
controller->CalculateMenuBounds(item, false, &dir, &anchor);
Reposition(bounds, anchor);
}
}
void SubmenuView::Layout() {
if (!parent())
return;
int pref_height = GetPreferredSize().height();
int new_y;
if (pref_height > parent()->height())
new_y = std::max(parent()->height() - pref_height, y());
else
new_y = 0;
SetBounds(x(), new_y, parent()->width(), pref_height);
gfx::Insets insets = GetInsets();
int x = insets.left();
int y = insets.top();
int menu_item_width = width() - insets.width();
for (View* child : children()) {
if (child->GetVisible()) {
int child_height = child->GetHeightForWidth(menu_item_width);
child->SetBounds(x, y, menu_item_width, child_height);
y += child_height;
}
}
}
gfx::Size SubmenuView::CalculatePreferredSize() const {
if (children().empty())
return gfx::Size();
max_minor_text_width_ = 0;
int max_complex_width = 0;
int max_simple_width = 0;
int touchable_minimum_width = 0;
for (const View* child : children()) {
if (!child->GetVisible())
continue;
if (child->GetID() == MenuItemView::kMenuItemViewID) {
const MenuItemView* menu = static_cast<const MenuItemView*>(child);
const MenuItemView::MenuItemDimensions& dimensions =
menu->GetDimensions();
max_simple_width = std::max(max_simple_width, dimensions.standard_width);
max_minor_text_width_ =
std::max(max_minor_text_width_, dimensions.minor_text_width);
max_complex_width =
std::max(max_complex_width,
dimensions.standard_width + dimensions.children_width);
touchable_minimum_width = dimensions.standard_width;
} else {
max_complex_width =
std::max(max_complex_width, child->GetPreferredSize().width());
}
}
if (max_minor_text_width_ > 0)
max_minor_text_width_ += MenuConfig::instance().item_horizontal_padding;
gfx::Insets insets = GetInsets();
int width = std::max(
max_complex_width,
std::max(max_simple_width + max_minor_text_width_ + insets.width(),
minimum_preferred_width_ - 2 * insets.width()));
if (parent_menu_item_->GetMenuController() &&
parent_menu_item_->GetMenuController()->use_ash_system_ui_layout()) {
width = std::max(touchable_minimum_width, width);
}
const int menu_item_width = width - insets.width();
const auto get_height = [menu_item_width](int height, const View* child) {
return height + (child->GetVisible()
? child->GetHeightForWidth(menu_item_width)
: 0);
};
const int height =
std::accumulate(children().cbegin(), children().cend(), 0, get_height);
return gfx::Size(width, height + insets.height());
}
void SubmenuView::GetAccessibleNodeData(ui::AXNodeData* node_data) {
if (parent_menu_item_)
parent_menu_item_->GetAccessibleNodeData(node_data);
node_data->role = ax::mojom::Role::kMenu;
node_data->AddState(ax::mojom::State::kVertical);
}
void SubmenuView::PaintChildren(const PaintInfo& paint_info) {
View::PaintChildren(paint_info);
bool paint_drop_indicator = false;
if (drop_item_) {
switch (drop_position_) {
case MenuDelegate::DropPosition::kNone:
case MenuDelegate::DropPosition::kOn:
break;
case MenuDelegate::DropPosition::kUnknow:
case MenuDelegate::DropPosition::kBefore:
case MenuDelegate::DropPosition::kAfter:
paint_drop_indicator = true;
break;
}
}
if (paint_drop_indicator) {
gfx::Rect bounds = CalculateDropIndicatorBounds(drop_item_, drop_position_);
ui::PaintRecorder recorder(paint_info.context(), size());
const SkColor drop_indicator_color =
GetColorProvider()->GetColor(ui::kColorMenuDropmarker);
recorder.canvas()->FillRect(bounds, drop_indicator_color);
}
}
bool SubmenuView::GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) {
DCHECK(parent_menu_item_->GetMenuController());
return parent_menu_item_->GetMenuController()->GetDropFormats(this, formats,
format_types);
}
bool SubmenuView::AreDropTypesRequired() {
DCHECK(parent_menu_item_->GetMenuController());
return parent_menu_item_->GetMenuController()->AreDropTypesRequired(this);
}
bool SubmenuView::CanDrop(const OSExchangeData& data) {
DCHECK(parent_menu_item_->GetMenuController());
return parent_menu_item_->GetMenuController()->CanDrop(this, data);
}
void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event) {
DCHECK(parent_menu_item_->GetMenuController());
parent_menu_item_->GetMenuController()->OnDragEntered(this, event);
}
int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event) {
DCHECK(parent_menu_item_->GetMenuController());
return parent_menu_item_->GetMenuController()->OnDragUpdated(this, event);
}
void SubmenuView::OnDragExited() {
DCHECK(parent_menu_item_->GetMenuController());
parent_menu_item_->GetMenuController()->OnDragExited(this);
}
views::View::DropCallback SubmenuView::GetDropCallback(
const ui::DropTargetEvent& event) {
DCHECK(parent_menu_item_->GetMenuController());
return parent_menu_item_->GetMenuController()->GetDropCallback(this, event);
}
bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e) {
gfx::Rect vis_bounds = GetVisibleBounds();
const auto menu_items = GetMenuItems();
if (vis_bounds.height() == height() || menu_items.empty()) {
return true;
}
auto i = base::ranges::lower_bound(menu_items, vis_bounds.y(), {},
&MenuItemView::y);
if (i == menu_items.cend())
return true;
int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta);
if (delta == 0)
return OnScroll(0, e.y_offset());
const auto scrolled_to_top = [&vis_bounds](const MenuItemView* item) {
return item->y() == vis_bounds.y();
};
if (i != menu_items.cbegin() && !scrolled_to_top(*i))
--i;
for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
int scroll_target;
if (scroll_up) {
if (scrolled_to_top(*i)) {
if (i == menu_items.cbegin())
break;
--i;
}
scroll_target = (*i)->y();
} else {
const auto next_iter = std::next(i);
if (next_iter == menu_items.cend())
break;
scroll_target = (*next_iter)->y();
if (scrolled_to_top(*i))
i = next_iter;
}
ScrollRectToVisible(
gfx::Rect(gfx::Point(0, scroll_target), vis_bounds.size()));
vis_bounds = GetVisibleBounds();
}
return true;
}
void SubmenuView::OnGestureEvent(ui::GestureEvent* event) {
bool handled = true;
switch (event->type()) {
case ui::ET_GESTURE_SCROLL_BEGIN:
scroll_animator_->Stop();
break;
case ui::ET_GESTURE_SCROLL_UPDATE:
handled = OnScroll(0, event->details().scroll_y());
break;
case ui::ET_GESTURE_SCROLL_END:
break;
case ui::ET_SCROLL_FLING_START:
if (event->details().velocity_y() != 0.0f)
scroll_animator_->Start(0, event->details().velocity_y());
break;
case ui::ET_GESTURE_TAP_DOWN:
case ui::ET_SCROLL_FLING_CANCEL:
if (scroll_animator_->is_scrolling())
scroll_animator_->Stop();
else
handled = false;
break;
default:
handled = false;
break;
}
if (handled)
event->SetHandled();
}
size_t SubmenuView::GetRowCount() {
return GetMenuItems().size();
}
absl::optional<size_t> SubmenuView::GetSelectedRow() {
const auto menu_items = GetMenuItems();
const auto i = base::ranges::find_if(menu_items, &MenuItemView::IsSelected);
return (i == menu_items.cend()) ? absl::nullopt
: absl::make_optional(static_cast<size_t>(
std::distance(menu_items.cbegin(), i)));
}
void SubmenuView::SetSelectedRow(absl::optional<size_t> row) {
parent_menu_item_->GetMenuController()->SetSelection(
GetMenuItemAt(row.value()), MenuController::SELECTION_DEFAULT);
}
std::u16string SubmenuView::GetTextForRow(size_t row) {
return MenuItemView::GetAccessibleNameForMenuItem(
GetMenuItemAt(row)->title(), std::u16string(),
GetMenuItemAt(row)->ShouldShowNewBadge());
}
bool SubmenuView::IsShowing() const {
return host_ && host_->IsMenuHostVisible();
}
void SubmenuView::ShowAt(const MenuHost::InitParams& init_params) {
if (host_) {
host_->SetMenuHostBounds(init_params.bounds);
host_->ShowMenuHost(init_params.do_capture);
} else {
host_ = new MenuHost(this);
GetScrollViewContainer();
InvalidateLayout();
MenuHost::InitParams new_init_params = init_params;
new_init_params.contents_view = scroll_view_container_;
host_->InitMenuHost(new_init_params);
}
if (!GetMenuItem()->GetParentMenuItem()) {
GetScrollViewContainer()->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuStart, true);
}
NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupStart, true);
}
void SubmenuView::Reposition(const gfx::Rect& bounds,
const ui::OwnedWindowAnchor& anchor) {
if (host_) {
host_->SetMenuHostOwnedWindowAnchor(anchor);
host_->SetMenuHostBounds(bounds);
}
}
void SubmenuView::Close() {
if (host_) {
host_->DestroyMenuHost();
host_ = nullptr;
}
}
void SubmenuView::Hide() {
if (host_) {
if (!GetMenuItem()->GetParentMenuItem()) {
GetScrollViewContainer()->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuEnd, true);
GetViewAccessibility().EndPopupFocusOverride();
}
if (host_->IsVisible())
NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupEnd, true);
host_->HideMenuHost();
}
if (scroll_animator_->is_scrolling())
scroll_animator_->Stop();
}
void SubmenuView::ReleaseCapture() {
if (host_)
host_->ReleaseMenuHostCapture();
}
bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
return views::FocusManager::IsTabTraversalKeyEvent(e);
}
MenuItemView* SubmenuView::GetMenuItem() {
return parent_menu_item_;
}
void SubmenuView::SetDropMenuItem(MenuItemView* item,
MenuDelegate::DropPosition position) {
if (drop_item_ == item && drop_position_ == position)
return;
SchedulePaintForDropIndicator(drop_item_, drop_position_);
MenuItemView* old_drop_item = drop_item_;
drop_item_ = item;
drop_position_ = position;
if (!old_drop_item || !item) {
for (View* child : children()) {
if (!child->GetVisible() ||
child->GetID() != MenuItemView::kMenuItemViewID) {
continue;
}
MenuItemView* child_menu_item = static_cast<MenuItemView*>(child);
if (child_menu_item->IsSelected()) {
child_menu_item->OnDropOrSelectionStatusMayHaveChanged();
break;
}
}
} else {
if (old_drop_item && old_drop_item != drop_item_)
old_drop_item->OnDropOrSelectionStatusMayHaveChanged();
if (drop_item_)
drop_item_->OnDropOrSelectionStatusMayHaveChanged();
}
SchedulePaintForDropIndicator(drop_item_, drop_position_);
}
bool SubmenuView::GetShowSelection(const MenuItemView* item) const {
if (drop_item_ == nullptr)
return true;
return (drop_item_ == item &&
drop_position_ == MenuDelegate::DropPosition::kOn);
}
MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() {
if (!scroll_view_container_) {
scroll_view_container_ = new MenuScrollViewContainer(this);
scroll_view_container_->set_owned_by_client();
scroll_view_container_->SetBorderColorId(border_color_id_);
}
return scroll_view_container_;
}
MenuItemView* SubmenuView::GetLastItem() {
const auto menu_items = GetMenuItems();
return menu_items.empty() ? nullptr : menu_items.back();
}
void SubmenuView::MenuHostDestroyed() {
host_ = nullptr;
MenuController* controller = parent_menu_item_->GetMenuController();
if (controller)
controller->Cancel(MenuController::ExitType::kDestroyed);
}
void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
SchedulePaint();
}
void SubmenuView::SchedulePaintForDropIndicator(
MenuItemView* item,
MenuDelegate::DropPosition position) {
if (item == nullptr)
return;
if (position == MenuDelegate::DropPosition::kOn) {
item->SchedulePaint();
} else if (position != MenuDelegate::DropPosition::kNone) {
SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
}
}
gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
MenuItemView* item,
MenuDelegate::DropPosition position) {
DCHECK(position != MenuDelegate::DropPosition::kNone);
gfx::Rect item_bounds = item->bounds();
switch (position) {
case MenuDelegate::DropPosition::kBefore:
item_bounds.Offset(0, -kDropIndicatorHeight / 2);
item_bounds.set_height(kDropIndicatorHeight);
return item_bounds;
case MenuDelegate::DropPosition::kAfter:
item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
item_bounds.set_height(kDropIndicatorHeight);
return item_bounds;
default:
return gfx::Rect();
}
}
bool SubmenuView::OnScroll(float dx, float dy) {
const gfx::Rect& vis_bounds = GetVisibleBounds();
const gfx::Rect& full_bounds = bounds();
int x = vis_bounds.x();
float y_f = vis_bounds.y() - dy - roundoff_error_;
int y = base::ClampRound(y_f);
roundoff_error_ = y - y_f;
y = std::clamp(y, 0, full_bounds.height() - vis_bounds.height());
gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height());
if (new_vis_bounds != vis_bounds) {
ScrollRectToVisible(new_vis_bounds);
return true;
}
return false;
}
void SubmenuView::SetBorderColorId(absl::optional<ui::ColorId> color_id) {
if (scroll_view_container_) {
scroll_view_container_->SetBorderColorId(color_id);
}
border_color_id_ = color_id;
}
BEGIN_METADATA(SubmenuView, View)
END_METADATA
}