chromium/ui/views/controls/menu/menu_controller.cc

// 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/menu_controller.h"

#include <algorithm>
#include <set>
#include <utility>

#include "base/callback_list.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/i18n/case_conversion.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/owned_window_anchor.h"
#include "ui/base/ui_base_types.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_rep.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/native_theme/native_theme.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/menu_button.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller_delegate.h"
#include "ui/views/controls/menu/menu_host_root_view.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_pre_target_handler.h"
#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include "ui/views/controls/menu/menu_types.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/drag_utils.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_constants.h"
#include "ui/views/view_tracker.h"
#include "ui/views/view_utils.h"
#include "ui/views/views_delegate.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"

#if BUILDFLAG(IS_WIN)
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/win/internal_constants.h"
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif

#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/aura/window_delegate.h"
#endif

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif

OSExchangeData;

DEFINE_UI_CLASS_PROPERTY_TYPE()

namespace views {

namespace {

enum class MenuPartType {};

// The menu controller manages the AX index attributes inside menu items. This
// property maintains a vector of menu children that were last assigned such
// attributes by MenuController::SetSelectionIndices() so that the controller
// can update them if children change via MenuController::MenuChildrenChanged().
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY()

#if BUILDFLAG(IS_MAC)
bool AcceleratorShouldCancelMenu(const ui::Accelerator& accelerator) {
  // Since AcceleratorShouldCancelMenu() is called quite early in key
  // event handling, it is actually invoked for modifier keys themselves
  // changing. In that case, the key code reflects that the modifier key is
  // being pressed/released. We should never treat those presses as
  // accelerators, so bail out here.
  //
  // Also, we have to check for VKEY_SHIFT here even though we don't check
  // IsShiftDown() - otherwise this sequence of keypresses will dismiss the
  // menu:
  //   Press Cmd
  //   Press Shift
  // Which makes it impossible to use menus that have Cmd-Shift accelerators.
  if (accelerator.key_code() == ui::VKEY_CONTROL ||
      accelerator.key_code() == ui::VKEY_MENU ||  // aka Alt
      accelerator.key_code() == ui::VKEY_COMMAND ||
      accelerator.key_code() == ui::VKEY_SHIFT) {
    return false;
  }

  // Using an accelerator on Mac closes any open menu. Note that Mac behavior is
  // different between context menus (which block use of accelerators) and other
  // types of menus, which close when an accelerator is sent and do repost the
  // accelerator. In MacViews, this happens naturally because context menus are
  // (modal) Cocoa menus and other menus are Views menus, which will go through
  // this code path.
  return accelerator.IsCtrlDown() || accelerator.IsAltDown() ||
         accelerator.IsCmdDown();
}
#endif

bool ShouldIgnoreScreenBoundsForMenus() {}

// The amount of time the mouse should be down before a mouse release is
// considered intentional. This is to prevent spurious mouse releases from
// activating controls, especially when some UI element is revealed under the
// source of the activation (ex. menus showing underneath menu buttons).
base::TimeDelta menu_selection_hold_time =;

// Amount of time from when the drop exits the menu and the menu is hidden.
constexpr int kCloseOnExitTime =;

// If a context menu is invoked by touch, we shift the menu by this offset so
// that the finger does not obscure the menu.
constexpr int kTouchYPadding =;

// The spacing offset for the bubble tip.
constexpr int kBubbleTipSizeLeftRight =;
constexpr int kBubbleTipSizeTopBottom =;

// The maximum distance (in DIPS) that the mouse can be moved before it should
// trigger a mouse menu item activation (regardless of how long the menu has
// been showing).
constexpr float kMaximumLengthMovedToActivate =;

// Time to complete a cycle of the menu item alert animation.
constexpr base::TimeDelta kAlertAnimationThrobDuration =;

// Returns true if the mnemonic of |menu| matches key.
bool MatchesMnemonic(MenuItemView* menu, char16_t key) {}

// Returns true if |menu| doesn't have a mnemonic and first character of the its
// title is |key|.
bool TitleMatchesMnemonic(MenuItemView* menu, char16_t key) {}

// Returns the first descendant of |view| that is hot tracked.
Button* GetFirstHotTrackedView(View* view) {}

MenuPartType GetScrollButtonAt(SubmenuView* source,
                               const gfx::Point& location) {}

// Convenience wrappers for converting between screen coordinates and the
// coordinates of the submenu's root view. CAUTION: The latter is not the same
// as the coordinates of the submenu itself! Be careful which View you are
// treating coordinates as relative to!
gfx::Point ConvertFromScreen(const SubmenuView& submenu,
                             const gfx::Point& location) {}
gfx::Point ConvertToScreen(const SubmenuView& submenu,
                           const gfx::Point& location) {}

template <typename T>
T ConvertLocatedEventForRootView(const SubmenuView& submenu,
                                 const MenuHostRootView& root_view,
                                 const T& event) {}

const SubmenuView& GetRootMenu(const SubmenuView& submenu) {}

gfx::Point GetLocationInRootMenu(const SubmenuView& submenu,
                                 const gfx::Point& location) {}

bool Contains(const SubmenuView& submenu, const gfx::Point& location) {}

// Recurses through the child views of |view| returning the first view starting
// at |pos| that is focusable. Children are considered first to last.
// TODO(crbug.com/41447095): This can also return |view|, which seems
// incorrect.
View* GetFirstFocusableViewForward(View* view,
                                   View::Views::const_iterator pos) {}

// As GetFirstFocusableViewForward(), but children are considered last to first.
View* GetFirstFocusableViewBackward(View* view,
                                    View::Views::const_reverse_iterator pos) {}

// Returns the first child of |start| that is focusable.
View* GetInitialFocusableView(View* start, bool forward) {}

// Returns the next view after |start_at| that is focusable. Returns null if
// there are no focusable children of |ancestor| after |start_at|.
View* GetNextFocusableView(View* ancestor, View* start_at, bool forward) {}

#if BUILDFLAG(IS_WIN)
// Determines the correct coordinates and window to repost |event| to, if it is
// a mouse or touch event.
static void RepostEventImpl(const ui::LocatedEvent* event,
                            const gfx::Point& screen_loc,
                            gfx::NativeView native_view,
                            gfx::NativeWindow window) {
  if (!event->IsMouseEvent() && !event->IsTouchEvent()) {
    // TODO(rbyers): Gesture event repost is tricky to get right
    // crbug.com/170987.
    DCHECK(event->IsGestureEvent());
    return;
  }

  if (!native_view)
    return;

  gfx::Point screen_loc_pixels =
      display::win::ScreenWin::DIPToScreenPoint(screen_loc);
  HWND target_window = ::WindowFromPoint(screen_loc_pixels.ToPOINT());
  // If we don't find a native window for the HWND at the current location,
  // then attempt to find a native window from its parent if one exists.
  // There are HWNDs created outside views, which don't have associated
  // native windows.
  if (!window) {
    HWND parent = ::GetParent(target_window);
    if (parent) {
      aura::WindowTreeHost* host =
          aura::WindowTreeHost::GetForAcceleratedWidget(parent);
      if (host) {
        target_window = parent;
        window = host->window();
      }
    }
  }
  // Convert screen_loc to pixels for the Win32 API's like WindowFromPoint,
  // PostMessage/SendMessage to work correctly. These API's expect the
  // coordinates to be in pixels.
  if (event->IsMouseEvent()) {
    HWND source_window = HWNDForNativeView(native_view);
    if (!target_window || !source_window ||
        GetWindowThreadProcessId(source_window, nullptr) !=
            GetWindowThreadProcessId(target_window, nullptr)) {
      // Even though we have mouse capture, windows generates a mouse event if
      // the other window is in a separate thread. Only repost an event if
      // |target_window| and |source_window| were created on the same thread,
      // else double events can occur and lead to bad behavior.
      return;
    }

    // Determine whether the click was in the client area or not.
    // NOTE: WM_NCHITTEST coordinates are relative to the screen.
    LPARAM coords = MAKELPARAM(screen_loc_pixels.x(), screen_loc_pixels.y());
    LRESULT nc_hit_result = SendMessage(target_window, WM_NCHITTEST, 0, coords);
    const bool client_area = nc_hit_result == HTCLIENT;

    int window_x = screen_loc_pixels.x();
    int window_y = screen_loc_pixels.y();
    if (client_area) {
      POINT pt = {window_x, window_y};
      ScreenToClient(target_window, &pt);
      window_x = pt.x;
      window_y = pt.y;
    }

    WPARAM target = client_area ? event->native_event().wParam
                                : static_cast<WPARAM>(nc_hit_result);
    LPARAM window_coords = MAKELPARAM(window_x, window_y);
    PostMessage(target_window, event->native_event().message, target,
                window_coords);
    return;
  }

  if (!window)
    return;

  aura::Window* root = window->GetRootWindow();
  aura::client::ScreenPositionClient* spc =
      aura::client::GetScreenPositionClient(root);
  if (!spc)
    return;

  gfx::Point root_loc(screen_loc);
  spc->ConvertPointFromScreen(root, &root_loc);

  std::unique_ptr<ui::Event> clone = event->Clone();
  std::unique_ptr<ui::LocatedEvent> located_event(
      static_cast<ui::LocatedEvent*>(clone.release()));
  located_event->set_location(root_loc);
  located_event->set_root_location(root_loc);

  root->GetHost()->dispatcher()->RepostEvent(located_event.get());
}
#endif  // BUILDFLAG(IS_WIN)

}  // namespace

// MenuController:MenuPart ---------------------------------------------------

struct MenuController::MenuPart {};

// MenuScrollTask --------------------------------------------------------------

// MenuScrollTask is used when the SubmenuView does not all fit on screen and
// the mouse is over the scroll up/down buttons. MenuScrollTask schedules
// itself with a RepeatingTimer. When Run is invoked MenuScrollTask scrolls
// appropriately.

class MenuController::MenuScrollTask {};

// MenuController:SelectByCharDetails ----------------------------------------

struct MenuController::SelectByCharDetails {};

// MenuController:State ------------------------------------------------------

MenuController::State::State() = default;

MenuController::State::State(const State& other) = default;

MenuController::State::~State() = default;

// MenuController ------------------------------------------------------------

// static
MenuController* MenuController::active_instance_ =;

// static
MenuController* MenuController::GetActiveInstance() {}

void MenuController::OnWidgetShowStateChanged(Widget* widget) {}

void MenuController::Run(Widget* parent,
                         MenuButtonController* button_controller,
                         MenuItemView* root,
                         const gfx::Rect& anchor_bounds,
                         MenuAnchorPosition position,
                         bool context_menu,
                         bool is_nested_drag,
                         gfx::NativeView native_view_for_gestures) {}

void MenuController::Cancel(ExitType type) {}

void MenuController::AddNestedDelegate(
    internal::MenuControllerDelegate* delegate) {}

bool MenuController::IsCombobox() const {}

bool MenuController::IsEditableCombobox() const {}

bool MenuController::IsReadonlyCombobox() const {}

bool MenuController::IsContextMenu() const {}

void MenuController::SelectItemAndOpenSubmenu(MenuItemView* item) {}

bool MenuController::OnMousePressed(SubmenuView* source,
                                    const ui::MouseEvent& event) {}

bool MenuController::OnMouseDragged(SubmenuView* source,
                                    const ui::MouseEvent& event) {}

void MenuController::OnMouseReleased(SubmenuView* source,
                                     const ui::MouseEvent& event) {}

void MenuController::OnMouseMoved(SubmenuView* source,
                                  const ui::MouseEvent& event) {}

void MenuController::OnMouseEntered(SubmenuView* source,
                                    const ui::MouseEvent& event) {}

bool MenuController::OnMouseWheel(SubmenuView* source,
                                  const ui::MouseWheelEvent& event) {}

void MenuController::OnGestureEvent(SubmenuView* source,
                                    ui::GestureEvent* event) {}

void MenuController::OnTouchEvent(SubmenuView* source, ui::TouchEvent* event) {}

View* MenuController::GetTooltipHandlerForPoint(SubmenuView* source,
                                                const gfx::Point& point) {}

void MenuController::ViewHierarchyChanged(
    SubmenuView* source,
    const ViewHierarchyChangedDetails& details) {}

bool MenuController::GetDropFormats(
    SubmenuView* source,
    int* formats,
    std::set<ui::ClipboardFormatType>* format_types) {}

bool MenuController::AreDropTypesRequired(SubmenuView* source) {}

bool MenuController::CanDrop(SubmenuView* source, const OSExchangeData& data) {}

void MenuController::OnDragEntered(SubmenuView* source,
                                   const ui::DropTargetEvent& event) {}

int MenuController::OnDragUpdated(SubmenuView* source,
                                  const ui::DropTargetEvent& event) {}

void MenuController::OnDragExited(SubmenuView* source) {}

views::View::DropCallback MenuController::GetDropCallback(
    SubmenuView* source,
    const ui::DropTargetEvent& event) {}

void MenuController::OnDragEnteredScrollButton(SubmenuView* source,
                                               bool is_up) {}

void MenuController::OnDragExitedScrollButton(SubmenuView* source) {}

void MenuController::OnDragWillStart() {}

void MenuController::OnDragComplete(bool should_close) {}

ui::PostDispatchAction MenuController::OnWillDispatchKeyEvent(
    ui::KeyEvent* event) {}

void MenuController::UpdateSubmenuSelection(SubmenuView* submenu) {}

void MenuController::OnWidgetDestroying(Widget* widget) {}

bool MenuController::IsCancelAllTimerRunningForTest() {}

void MenuController::ClearStateForTest() {}

// static
void MenuController::TurnOffMenuSelectionHoldForTest() {}

ui::ColorId MenuController::GetSeparatorColorId() const {}

void MenuController::OnMenuItemDestroying(MenuItemView* menu_item) {}

void MenuController::AnimationProgressed(const gfx::Animation* animation) {}

void MenuController::SetSelection(MenuItemView* menu_item,
                                  int selection_types) {}

void MenuController::SetSelectionOnPointerDown(SubmenuView* source,
                                               const ui::LocatedEvent* event) {}

void MenuController::StartDrag(SubmenuView* source,
                               const gfx::Point& location) {}

bool MenuController::OnKeyPressed(const ui::KeyEvent& event) {}

MenuController::MenuController(bool for_drop,
                               internal::MenuControllerDelegate* delegate)
    :{}

MenuController::~MenuController() {}

bool MenuController::SendAcceleratorToHotTrackedView(int event_flags) {}

void MenuController::UpdateInitialLocation(const gfx::Rect& anchor_bounds,
                                           MenuAnchorPosition position,
                                           bool context_menu) {}

// static
MenuAnchorPosition MenuController::AdjustAnchorPositionForRtl(
    MenuAnchorPosition position) {}

void MenuController::Accept(MenuItemView* item, int event_flags) {}

void MenuController::ReallyAccept() {}

bool MenuController::ShowSiblingMenu(SubmenuView* source,
                                     const gfx::Point& mouse_location) {}

bool MenuController::ShowContextMenu(MenuItemView* menu_item,
                                     const gfx::Point& screen_location,
                                     ui::MenuSourceType source_type) {}

void MenuController::CloseAllNestedMenus() {}

MenuItemView* MenuController::GetMenuItemAt(View* source,
                                            const gfx::Point& location) {}

MenuController::MenuPart MenuController::GetMenuPart(
    SubmenuView* source,
    const gfx::Point& source_loc) {}

MenuController::MenuPart MenuController::GetMenuPartByScreenCoordinateUsingMenu(
    MenuItemView* item,
    const gfx::Point& screen_loc) {}

bool MenuController::GetMenuPartByScreenCoordinateImpl(
    SubmenuView* menu,
    const gfx::Point& screen_loc,
    MenuPart* part) {}

MenuHostRootView* MenuController::GetRootView(SubmenuView* submenu,
                                              const gfx::Point& source_loc) {}

bool MenuController::IsLocationOverSubmenuAreaOfActionableSubmenu(
    MenuItemView* item,
    const gfx::Point& screen_loc) const {}

void MenuController::CommitPendingSelection() {}

void MenuController::CloseMenu(MenuItemView* item) {}

void MenuController::OpenMenu(MenuItemView* item) {}

void MenuController::OpenMenuImpl(MenuItemView* item, bool show) {}

void MenuController::MenuChildrenChanged(MenuItemView* item) {}

void MenuController::BuildPathsAndCalculateDiff(
    MenuItemView* old_item,
    MenuItemView* new_item,
    std::vector<MenuItemView*>* old_path,
    std::vector<MenuItemView*>* new_path,
    size_t* first_diff_at) {}

void MenuController::BuildMenuItemPath(MenuItemView* item,
                                       std::vector<MenuItemView*>* path) {}

void MenuController::StartShowTimer() {}

void MenuController::StopShowTimer() {}

void MenuController::StartCancelAllTimer() {}

void MenuController::StopCancelAllTimer() {}

gfx::Rect MenuController::CalculateMenuBounds(
    MenuItemView* item,
    MenuOpenDirection preferred_open_direction,
    MenuOpenDirection* resulting_direction,
    ui::OwnedWindowAnchor* anchor) {}

gfx::Rect MenuController::CalculateBubbleMenuBounds(
    MenuItemView* item,
    MenuOpenDirection preferred_open_direction,
    MenuOpenDirection* resulting_direction,
    ui::OwnedWindowAnchor* anchor) {}

// static
size_t MenuController::MenuDepth(MenuItemView* item) {}

void MenuController::IncrementSelection(
    SelectionIncrementDirectionType direction) {}

void MenuController::SetSelectionIndices(MenuItemView* parent) {}

void MenuController::MoveSelectionToFirstOrLastItem(
    SelectionIncrementDirectionType direction) {}

MenuItemView* MenuController::FindInitialSelectableMenuItem(
    MenuItemView* parent,
    SelectionIncrementDirectionType direction) {}

void MenuController::OpenSubmenuChangeSelectionIfCan() {}

void MenuController::CloseSubmenu() {}

MenuController::SelectByCharDetails MenuController::FindChildForMnemonic(
    MenuItemView* parent,
    char16_t key,
    bool (*match_function)(MenuItemView* menu, char16_t mnemonic)) {}

void MenuController::AcceptOrSelect(MenuItemView* parent,
                                    const SelectByCharDetails& details) {}

void MenuController::SelectByChar(char16_t character) {}

void MenuController::RepostEventAndCancel(SubmenuView* source,
                                          const ui::LocatedEvent* event) {}

void MenuController::SetDropMenuItem(MenuItemView* new_target,
                                     MenuDelegate::DropPosition new_position) {}

void MenuController::UpdateScrolling(const MenuPart& part) {}

void MenuController::StopScrollingViaButton() {}

void MenuController::UpdateActiveMouseView(SubmenuView* event_source,
                                           const ui::MouseEvent& event,
                                           View* target_menu) {}

void MenuController::SendMouseReleaseToActiveView(SubmenuView* event_source,
                                                  const ui::MouseEvent& event) {}

void MenuController::SendMouseCaptureLostToActiveView() {}

void MenuController::SetExitType(ExitType type) {}

void MenuController::ExitMenu() {}

MenuItemView* MenuController::ExitTopMostMenu() {}

void MenuController::HandleMouseLocation(SubmenuView* source,
                                         const gfx::Point& mouse_location) {}

void MenuController::SetInitialHotTrackedView(
    MenuItemView* item,
    SelectionIncrementDirectionType direction) {}

void MenuController::SetNextHotTrackedView(
    MenuItemView* item,
    SelectionIncrementDirectionType direction) {}

void MenuController::SetHotTrackedButton(Button* new_hot_button) {}

bool MenuController::ShouldContinuePrefixSelection() const {}

void MenuController::RegisterAlertedItem(MenuItemView* item) {}

void MenuController::UnregisterAlertedItem(MenuItemView* item) {}

void MenuController::SetAnchorParametersForItem(MenuItemView* item,
                                                const gfx::Point& item_loc,
                                                ui::OwnedWindowAnchor* anchor) {}

base::CallbackListSubscription MenuController::AddAnnotationCallback(
    AnnotationCallback callback) {}

bool MenuController::MaybeForwardToAnnotation(SubmenuView* source,
                                              const ui::LocatedEvent& event) {}

bool MenuController::CanProcessInputEvents() const {}

MenuController::MenuOpenDirection
MenuController::GetChildMenuOpenDirectionAtDepth(size_t depth) const {}

void MenuController::SetChildMenuOpenDirectionAtDepth(
    size_t depth,
    MenuOpenDirection direction) {}

void MenuController::SetMenuRoundedCorners(
    std::optional<gfx::RoundedCornersF> corners) {}

}  // namespace views