| // Copyright 2012 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/views/controls/menu/submenu_view.h" |
| |
| #include <algorithm> |
| #include <numeric> |
| #include <set> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/compiler_specific.h" |
| #include "base/containers/contains.h" |
| #include "base/numerics/safe_conversions.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/models/menu_separator_types.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/image_view.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/controls/menu/menu_separator.h" |
| #include "ui/views/view_utils.h" |
| #include "ui/views/widget/root_view.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // Height of the drop indicator. This should be an even number. |
| constexpr int kDropIndicatorHeight = 2; |
| |
| template <typename MIV, typename V> |
| std::vector<MIV*> GetMenuItemsFromChildren(const View::Views& children) { |
| std::vector<MIV*> menu_items; |
| std::ranges::transform(children, std::back_inserter(menu_items), |
| static_cast<MIV* (*)(V*)>(&AsViewClass<MenuItemView>)); |
| std::erase_if(menu_items, [](MIV* item) { |
| return !item || IsViewClass<EmptyMenuMenuItem>(item); |
| }); |
| return menu_items; |
| } |
| |
| } // namespace |
| |
| SubmenuView::SubmenuView(MenuItemView* parent) : parent_menu_item_(parent) { |
| CHECK(parent_menu_item_); |
| // We'll delete ourselves, otherwise the ScrollView would delete us on close. |
| set_owned_by_client(OwnedByClientPassKey()); |
| |
| // Menus in Chrome are always traversed in a vertical direction. |
| GetViewAccessibility().SetIsVertical(true); |
| GetViewAccessibility().SetRole(ax::mojom::Role::kMenu); |
| } |
| |
| SubmenuView::~SubmenuView() { |
| // The menu may not have been closed yet (it will be hidden, but not |
| // necessarily closed). |
| Close(); |
| } |
| |
| std::vector<MenuItemView*> SubmenuView::GetMenuItems() { |
| return GetMenuItemsFromChildren<MenuItemView, View>(children()); |
| } |
| |
| std::vector<const MenuItemView*> SubmenuView::GetMenuItems() const { |
| return GetMenuItemsFromChildren<const MenuItemView, const View>(children()); |
| } |
| |
| MenuItemView* SubmenuView::GetMenuItemAt(size_t index) { |
| const auto menu_items = GetMenuItems(); |
| CHECK_LT(index, menu_items.size()); |
| return menu_items[index]; |
| } |
| |
| int SubmenuView::GetPreferredItemHeight() const { |
| EmptyMenuMenuItem menu_item(parent_menu_item_); |
| menu_item.set_controller(parent_menu_item_->GetMenuController()); |
| return menu_item.GetPreferredSize({}).height(); |
| } |
| |
| PrefixSelector* SubmenuView::GetPrefixSelector() { |
| return &prefix_selector_; |
| } |
| |
| void SubmenuView::UpdateMenuPartSizes() { |
| const MenuConfig& config = MenuConfig::instance(); |
| |
| const auto get_metrics = [&] { |
| return std::tie(icon_area_width_, label_start_, trailing_padding_); |
| }; |
| const auto old_metrics = get_metrics(); |
| |
| trailing_padding_ = config.item_horizontal_padding + |
| parent_menu_item_->GetItemHorizontalBorder(); |
| const auto& menu_items = GetMenuItems(); |
| if (config.reserve_dedicated_arrow_column && |
| std::ranges::any_of(menu_items, &MenuItemView::HasSubmenu)) { |
| trailing_padding_ += |
| config.arrow_size + |
| (base::Contains(menu_items, MenuItemView::Type::kActionableSubMenu, |
| &MenuItemView::GetType) |
| ? config.actionable_submenu_arrow_to_edge_padding |
| : config.arrow_to_edge_padding); |
| } |
| |
| const auto is_check_or_radio = [](const auto* item) { |
| const auto type = item->GetType(); |
| return type == MenuItemView::Type::kCheckbox || |
| type == MenuItemView::Type::kRadio; |
| }; |
| icon_area_width_ = min_icon_height_ = |
| (config.always_reserve_check_region || |
| std::ranges::any_of(menu_items, is_check_or_radio)) |
| ? kMenuCheckSize |
| : 0; |
| int max_icon_width = 0; |
| if (!menu_items.empty()) { |
| std::vector<int> widths(menu_items.size()); |
| std::ranges::transform( |
| menu_items, widths.begin(), [&](const MenuItemView* item) { |
| const auto icon_size = item->GetIconPreferredSize(); |
| if (icon_size.IsEmpty()) { |
| return 0; |
| } |
| min_icon_height_ = std::max(min_icon_height_, kMenuCheckSize); |
| // If this item has a radio or checkbox, an additional icon will not |
| // affect horizontal alignment of other items. |
| return (config.icons_in_label || !is_check_or_radio(item)) |
| ? icon_size.width() |
| : 0; |
| }); |
| max_icon_width = std::ranges::max(widths); |
| } |
| if (!config.icons_in_label) { |
| icon_area_width_ = std::max(icon_area_width_, max_icon_width); |
| } |
| |
| label_start_ = parent_menu_item_->GetContentStart() + icon_area_width_; |
| if (icon_area_width_) { |
| const auto* const controller = parent_menu_item_->GetMenuController(); |
| label_start_ += (controller && controller->use_ash_system_ui_layout()) |
| ? config.touchable_item_horizontal_padding |
| : config.icon_label_spacing; |
| } |
| |
| if (config.icons_in_label) { |
| icon_area_width_ = max_icon_width; |
| } |
| |
| if (get_metrics() != old_metrics) { |
| InvalidateLayout(); |
| } |
| } |
| |
| void SubmenuView::ChildPreferredSizeChanged(View* child) { |
| if (!resize_open_menu_) { |
| return; |
| } |
| |
| MenuItemView* item = parent_menu_item_; |
| MenuController* controller = item->GetMenuController(); |
| |
| if (controller) { |
| MenuController::MenuOpenDirection dir; |
| ui::OwnedWindowAnchor anchor; |
| gfx::Rect bounds = controller->CalculateMenuBounds( |
| item, MenuController::MenuOpenDirection::kTrailing, &dir, &anchor); |
| Reposition(bounds, anchor); |
| } |
| } |
| |
| void SubmenuView::Layout(PassKey) { |
| // We're in a ScrollView, and need to set our width/height ourselves. |
| if (!parent()) { |
| return; |
| } |
| |
| // Use our current y, unless it means part of the menu isn't visible anymore. |
| const int pref_height = GetPreferredSize(SizeBounds(size())).height(); |
| SetBounds(x(), |
| (pref_height > parent()->height()) |
| ? std::max(parent()->height() - pref_height, y()) |
| : 0, |
| parent()->width(), pref_height); |
| |
| const gfx::Insets insets = GetInsets(); |
| const int x = insets.left(); |
| int y = insets.top(); |
| const int menu_item_width = width() - insets.width(); |
| const int between_item_vertical_padding = |
| MenuConfig::instance().between_item_vertical_padding; |
| bool previous_child_was_lower_separator = false; |
| for (View* child : children()) { |
| if (child->GetVisible()) { |
| const auto* separator = AsViewClass<MenuSeparator>(child); |
| if (y != insets.top() && !previous_child_was_lower_separator && |
| (!separator || separator->GetType() != ui::UPPER_SEPARATOR)) { |
| y += between_item_vertical_padding; |
| } |
| child->SetBounds(x, y, menu_item_width, |
| child->GetHeightForWidth(menu_item_width)); |
| y = child->bounds().bottom(); |
| previous_child_was_lower_separator = |
| separator && separator->GetType() == ui::LOWER_SEPARATOR; |
| } |
| } |
| } |
| |
| gfx::Size SubmenuView::CalculatePreferredSize( |
| const SizeBounds& /*available_size*/) const { |
| if (children().empty()) { |
| return gfx::Size(); |
| } |
| |
| max_minor_text_width_ = 0; |
| // The maximum width of items which contain maybe a label and multiple views. |
| int max_complex_width = 0; |
| // The max. width of items which contain a label and maybe an accelerator. |
| int max_simple_width = 0; |
| // The minimum width of touchable items. |
| int touchable_minimum_width = 0; |
| |
| // We perform the size calculation in two passes. In the first pass, we |
| // calculate the width of the menu. In the second, we calculate the height |
| // using that width. This allows views that have flexible widths to adjust |
| // accordingly. |
| for (const View* child : children()) { |
| if (!child->GetVisible()) { |
| continue; |
| } |
| if (const auto* const menu = AsViewClass<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()); |
| } |
| } |
| const auto& config = MenuConfig::instance(); |
| if (max_minor_text_width_ > 0) { |
| max_minor_text_width_ += config.item_horizontal_padding; |
| } |
| |
| // Finish calculating our optimum width. |
| const 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); |
| } |
| |
| // Then, the height for that width. |
| const int menu_item_width = width - insets.width(); |
| bool previous_child_was_lower_separator = false; |
| const auto get_height = [&](int height, const View* child) { |
| if (!child->GetVisible()) { |
| return height; |
| } |
| const auto* separator = AsViewClass<MenuSeparator>(child); |
| if (height && !previous_child_was_lower_separator && |
| (!separator || separator->GetType() != ui::UPPER_SEPARATOR)) { |
| height += config.between_item_vertical_padding; |
| } |
| previous_child_was_lower_separator = |
| separator && separator->GetType() == ui::LOWER_SEPARATOR; |
| return height + child->GetHeightForWidth(menu_item_width); |
| }; |
| const int height = |
| std::accumulate(children().cbegin(), children().cend(), 0, get_height); |
| |
| return gfx::Size(width, height + insets.height()); |
| } |
| |
| 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()); |
| drop_item_ = nullptr; |
| 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()) { |
| // All menu items are visible, nothing to scroll. |
| return true; |
| } |
| |
| auto i = std::ranges::lower_bound(menu_items, vis_bounds.y(), {}, |
| &MenuItemView::y); |
| if (i == menu_items.cend()) { |
| return true; |
| } |
| |
| // If the first item isn't entirely visible, make it visible, otherwise make |
| // the next/previous one entirely visible. If enough wasn't scrolled to show |
| // any new rows, then just scroll the amount so that smooth scrolling using |
| // the trackpad is possible. |
| 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::EventType::kGestureScrollBegin: |
| scroll_animator_->Stop(); |
| break; |
| case ui::EventType::kGestureScrollUpdate: |
| handled = OnScroll(0, event->details().scroll_y()); |
| break; |
| case ui::EventType::kGestureScrollEnd: |
| break; |
| case ui::EventType::kScrollFlingStart: |
| if (event->details().velocity_y() != 0.0f) { |
| scroll_animator_->Start(0, event->details().velocity_y()); |
| } |
| break; |
| case ui::EventType::kGestureTapDown: |
| case ui::EventType::kScrollFlingCancel: |
| 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(); |
| } |
| |
| std::optional<size_t> SubmenuView::GetSelectedRow() { |
| const auto menu_items = GetMenuItems(); |
| const auto i = std::ranges::find_if(menu_items, &MenuItemView::IsSelected); |
| return (i == menu_items.cend()) ? std::nullopt |
| : std::make_optional(static_cast<size_t>( |
| std::distance(menu_items.cbegin(), i))); |
| } |
| |
| void SubmenuView::SetSelectedRow(std::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); |
| // Force construction of the scroll view container. |
| GetScrollViewContainer(); |
| // Force a layout since our preferred size may not have changed but our |
| // content may have. |
| InvalidateLayout(); |
| |
| MenuHost::InitParams new_init_params = init_params; |
| new_init_params.contents_view = scroll_view_container_.get(); |
| host_->InitMenuHost(new_init_params); |
| } |
| |
| // Only fire kMenuStart when a top level menu is being shown to notify that |
| // menu interaction is about to begin. Note that the ScrollViewContainer |
| // is not exposed as a kMenu, but as a kMenuBar for most platforms and a |
| // kNone on the Mac. See MenuScrollViewContainer::GetAccessibleNodeData. |
| if (!GetMenuItem()->GetParentMenuItem()) { |
| GetScrollViewContainer()->NotifyAccessibilityEventDeprecated( |
| ax::mojom::Event::kMenuStart, true); |
| } |
| // Fire kMenuPopupStart for each menu/submenu that is shown. |
| NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuPopupStart, true); |
| |
| GetMenuItem()->UpdateAccessibleExpandedCollapsedState(); |
| |
| // Announce if the menu/submenu is empty. |
| if (GetRowCount() == 0) { |
| GetViewAccessibility().AnnouncePolitely( |
| l10n_util::GetStringUTF16(IDS_APP_MENU_AX_ANNOUNCE_EMPTY_SUBMENU)); |
| } |
| } |
| |
| void SubmenuView::Reposition(const gfx::Rect& bounds, |
| const ui::OwnedWindowAnchor& anchor) { |
| if (host_) { |
| // Anchor must be updated first. |
| host_->SetMenuHostOwnedWindowAnchor(anchor); |
| host_->SetMenuHostBounds(bounds); |
| } |
| } |
| |
| void SubmenuView::Close() { |
| if (host_) { |
| host_->DestroyMenuHost(); |
| host_ = nullptr; |
| GetMenuItem()->UpdateAccessibleExpandedCollapsedState(); |
| } |
| } |
| |
| void SubmenuView::Hide() { |
| if (host_) { |
| /// -- Fire accessibility events ---- |
| // Both of these must be fired before HideMenuHost(). |
| // Only fire kMenuEnd when a top level menu closes, not for each submenu. |
| // This is sent before kMenuPopupEnd to allow ViewAXPlatformNodeDelegate to |
| // remove its focus override before AXPlatformNodeAuraLinux needs to access |
| // the previously-focused node while handling kMenuPopupEnd. |
| if (!GetMenuItem()->GetParentMenuItem()) { |
| GetScrollViewContainer()->NotifyAccessibilityEventDeprecated( |
| ax::mojom::Event::kMenuEnd, true); |
| GetViewAccessibility().EndPopupFocusOverride(); |
| } |
| // Fire these kMenuPopupEnd for each menu/submenu that closes/hides. |
| if (host_->IsVisible()) { |
| NotifyAccessibilityEventDeprecated(ax::mojom::Event::kMenuPopupEnd, true); |
| } |
| |
| host_->HideMenuHost(); |
| GetMenuItem()->UpdateAccessibleExpandedCollapsedState(); |
| } |
| |
| 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); |
| } |
| |
| const MenuItemView* SubmenuView::GetMenuItem() const { |
| 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 = std::exchange(drop_item_, item); |
| drop_position_ = position; |
| if (!old_drop_item || !item) { |
| // Whether the selection is actually drawn |
| // (`MenuItemView:last_paint_as_selected_`) depends upon whether there is a |
| // drop item. Find the selected item and have it updates its paint as |
| // selected state. |
| for (View* child : children()) { |
| if (auto* child_menu_item = AsViewClass<MenuItemView>(child); |
| child_menu_item && child_menu_item->GetVisible() && |
| child_menu_item->IsSelected()) { |
| child_menu_item->OnDropOrSelectionStatusMayHaveChanged(); |
| // Only one menu item is selected, so no need to continue iterating once |
| // the selected item is found. |
| 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 { |
| return !drop_item_ || (drop_item_ == item && |
| drop_position_ == MenuDelegate::DropPosition::kOn); |
| } |
| |
| MenuScrollViewContainer* SubmenuView::GetScrollViewContainer() { |
| // Perform null checks for scroll_view_container and MenuController since |
| // MenuScrollViewContainer constructor is invoked later, which uses |
| // menucontroller to determine the value of the use_ash_system_ui_layout_ |
| // variable. |
| if (!scroll_view_container_ && !parent_menu_item_->GetMenuController()) { |
| return nullptr; |
| } |
| |
| if (!scroll_view_container_) { |
| scroll_view_container_ = std::make_unique<MenuScrollViewContainer>(this); |
| // Otherwise MenuHost would delete us. |
| scroll_view_container_->set_owned_by_client(OwnedByClientPassKey()); |
| scroll_view_container_->SetBorderColorId(border_color_id_); |
| } |
| return scroll_view_container_.get(); |
| } |
| |
| 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: |
| // Don't render anything for on. |
| 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; |
| |
| // Ensure that we never try to scroll outside the actual child view. |
| // Note: the old code here was effectively: |
| // std::clamp(y, 0, full_bounds.height() - vis_bounds.height() - 1) |
| // but the -1 there prevented fully scrolling to the bottom here. As a |
| // worked example, suppose that: |
| // full_bounds = { x = 0, y = 0, w = 100, h = 1000 } |
| // vis_bounds = { x = 0, y = 450, w = 100, h = 500 } |
| // and dy = 50. It should be the case that the new vis_bounds are: |
| // new_vis_bounds = { x = 0, y = 500, w = 100, h = 500 } |
| // because full_bounds.height() - vis_bounds.height() == 500. Intuitively, |
| // this makes sense - the bottom 500 pixels of this view, starting with y = |
| // 500, are shown. |
| // |
| // With the clamp set to full_bounds.height() - vis_bounds.height() - 1, |
| // this code path instead would produce: |
| // new_vis_bounds = { x = 0, y = 499, w = 100, h = 500 } |
| // so pixels y=499 through y=998 of this view are drawn, and pixel y=999 is |
| // hidden - oops. |
| 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(std::optional<ui::ColorId> color_id) { |
| if (scroll_view_container_) { |
| scroll_view_container_->SetBorderColorId(color_id); |
| } |
| |
| border_color_id_ = color_id; |
| } |
| |
| BEGIN_METADATA(SubmenuView) |
| END_METADATA |
| |
| } // namespace views |