0

[Merge M135] Revert "Do popups using modern API"

There is a bug in macOS where the usage of NSMenu.minimumWidth
causes menu items to be prematurely truncated. Given that
this is on Stable, do a clean revert.

Disabling the style warning because this is a revert. The
cleanup code will be re-landed later.

This reverts commit 1786625b69.

DISABLE_OBJECTIVE_C_STYLE

(cherry picked from commit 57c0d866fc)

Fixed: 402800256
Bug: 389067059
Bug: 401443090
Change-Id: I1e06296f7647010a3ca36395b2a42953ec448203
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6347626
Commit-Queue: Keren Zhu <kerenzhu@chromium.org>
Reviewed-by: Keren Zhu <kerenzhu@chromium.org>
Reviewed-by: Leonard Grey <lgrey@chromium.org>
Auto-Submit: Avi Drissman <avi@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1431666}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6354233
Cr-Commit-Position: refs/branch-heads/7049@{#687}
Cr-Branched-From: 2dab7846d0951a552bdc4f350dad497f986e6fed-refs/heads/main@{#1427262}
This commit is contained in:
Avi Drissman
2025-03-14 12:06:00 -07:00
committed by Chromium LUCI CQ
parent e433da7b94
commit 0e64a6b1a4
14 changed files with 383 additions and 310 deletions

@ -109,10 +109,10 @@ class NewSubViewAddedObserver : content::RenderWidgetHostViewCocoaObserver {
const gfx::Rect& view_bounds_in_screen() const { return bounds_; }
private:
void DidAttemptToShowPopup(const gfx::Rect& bounds,
int selected_item) override {
void DidAddSubviewWillBeDismissed(
const gfx::Rect& bounds_in_root_view) override {
did_receive_rect_ = true;
bounds_ = bounds;
bounds_ = bounds_in_root_view;
if (run_loop_)
run_loop_->Quit();
}
@ -126,7 +126,11 @@ class NewSubViewAddedObserver : content::RenderWidgetHostViewCocoaObserver {
class WebViewInteractiveTest : public extensions::PlatformAppBrowserTest {
public:
WebViewInteractiveTest()
: guest_view_(nullptr), embedder_web_contents_(nullptr) {}
: guest_view_(nullptr),
embedder_web_contents_(nullptr),
corner_(gfx::Point()),
mouse_click_result_(false),
first_click_(true) {}
void SetUpCommandLine(base::CommandLine* command_line) override {
extensions::PlatformAppBrowserTest::SetUpCommandLine(command_line);
@ -487,8 +491,8 @@ class WebViewInteractiveTest : public extensions::PlatformAppBrowserTest {
embedder_web_contents_;
gfx::Point corner_;
bool mouse_click_result_ = false;
bool first_click_ = true;
bool mouse_click_result_;
bool first_click_;
};
class WebViewImeInteractiveTest : public WebViewInteractiveTest {

@ -81,7 +81,8 @@ void RenderViewContextMenuMacCocoa::Show() {
initWithParams:MenuControllerParamsForWidget(widget)];
menu_controller_ =
[[MenuControllerCocoa alloc] initWithModel:&menu_model_
delegate:menu_controller_delegate_];
delegate:menu_controller_delegate_
useWithPopUpButtonCell:NO];
NSPoint position =
NSMakePoint(params_.x, NSHeight(parent_view_.bounds) - params_.y);

@ -228,6 +228,8 @@ void StatusIconMac::UpdatePlatformContextMenu(StatusIconMenuModel* model) {
void StatusIconMac::CreateMenu(ui::MenuModel* model) {
DCHECK(model);
menu_ = [[MenuControllerCocoa alloc] initWithModel:model delegate:nil];
menu_ = [[MenuControllerCocoa alloc] initWithModel:model
delegate:nil
useWithPopUpButtonCell:NO];
item().menu = menu_.menu;
}

@ -47,7 +47,7 @@ ContextMenuRunner::ContextMenuRunner(
ContextMenuRunner::~ContextMenuRunner() {
if (menu_controller_) {
CHECK(!menu_controller_.menuOpen);
CHECK(!menu_controller_.isMenuOpen);
}
}
@ -60,7 +60,8 @@ void ContextMenuRunner::ShowMenu(mojom::ContextMenuPtr menu,
initWithParams:std::move(menu->params)];
menu_controller_ =
[[MenuControllerCocoa alloc] initWithModel:menu_model_.get()
delegate:menu_delegate_];
delegate:menu_delegate_
useWithPopUpButtonCell:NO];
if (!target_view) {
target_view = window.contentView;

@ -13,7 +13,6 @@
#include "base/containers/flat_set.h"
#include "content/browser/renderer_host/input/mouse_wheel_rails_filter_mac.h"
#include "content/common/content_export.h"
#include "content/common/render_widget_host_ns_view.mojom.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "third_party/blink/public/mojom/input/input_handler.mojom-shared.h"
@ -50,7 +49,6 @@ struct DidOverscrollParams;
// but that means that the view needs to own the delegate and will dispose of it
// when it's removed from the view system.
// TODO(ccameron): Hide this interface behind RenderWidgetHostNSViewBridge.
CONTENT_EXPORT
@interface RenderWidgetHostViewCocoa
: ToolTipBaseView <CommandDispatcherTarget,
RenderWidgetHostNSViewHostOwner,

@ -9,30 +9,24 @@
#include <vector>
#include "base/functional/callback_forward.h"
#include "content/common/content_export.h"
#include "third_party/blink/public/mojom/choosers/popup_menu.mojom.h"
// WebMenuRunner ---------------------------------------------------------------
// A class for presenting a menu when a HTML select element is clicked, and
// returning the user selection or dismissal without selection. If a menu item
// is selected, MenuDelegate is informed and sets a flag which can be queried
// after the menu has finished running.
CONTENT_EXPORT
// A class for determining whether an item was selected from an HTML select
// control, or if the menu was dismissed without making a selection. If a menu
// item is selected, MenuDelegate is informed and sets a flag which can be
// queried after the menu has finished running.
@interface WebMenuRunner : NSObject
// Initializes the MenuDelegate with a list of items sent from Blink.
// Initializes the MenuDelegate with a list of items sent from WebKit.
- (id)initWithItems:(const std::vector<blink::mojom::MenuItemPtr>&)items
fontSize:(CGFloat)fontSize
rightAligned:(BOOL)rightAligned;
// Returns YES if an item was selected from the menu, NO if the menu was
// dismissed.
@property(readonly) BOOL menuItemWasChosen;
// Returns the index of selected menu item, or its initial value (-1) if no item
// was selected.
@property(readonly) int indexOfSelectedItem;
- (BOOL)menuItemWasChosen;
// Displays and runs a native popup menu.
- (void)runMenuInView:(NSView*)view
@ -44,22 +38,13 @@ CONTENT_EXPORT
// contents of the menu changed so the menu has to be rebuilt). Because this is
// driven by Blink, and in some cases Blink will immediately re-issue the menu,
// this is a synchronous cancellation with no animation. See
// https://crbug.com/41370640.
// https://crbug.com/812260.
- (void)cancelSynchronously;
// Returns the index of selected menu item, or its initial value (-1) if no item
// was selected.
- (int)indexOfSelectedItem;
@end // @interface WebMenuRunner
// The callback for testing. Parameters are the same as on
// -runMenuInView:withBounds:initialIndex:.
using MenuWasRunCallback = base::RepeatingCallback<void(NSView*, NSRect, int)>;
@interface WebMenuRunner (TestingAPI)
// Register a callback to be called if a popup menu is invoked for a specific
// view. If a callback is registered for a view, the menu will not be invoked
// but instead, the callback will be run.
+ (void)registerForTestingMenuRunCallback:(MenuWasRunCallback)callback
forView:(NSView*)view;
+ (void)unregisterForTestingMenuRunCallbackForView:(NSView*)view;
@end
#endif // CONTENT_APP_SHIM_REMOTE_COCOA_WEB_MENU_RUNNER_MAC_H_

@ -4,37 +4,34 @@
#include "content/app_shim_remote_cocoa/web_menu_runner_mac.h"
#include <AppKit/AppKit.h>
#include <Foundation/Foundation.h>
#include <objc/runtime.h>
#include <stddef.h>
#include "base/base64.h"
#include "base/mac/mac_util.h"
#include "base/strings/sys_string_conversions.h"
namespace {
@interface WebMenuRunner (PrivateAPI)
// A key to attach a MenuWasRunCallbackHolder to the NSView*.
static const char kMenuWasRunCallbackKey = 0;
// Worker function used during initialization.
- (void)addItem:(const blink::mojom::MenuItemPtr&)item;
} // namespace
// A callback for the menu controller object to call when an item is selected
// from the menu. This is not called if the menu is dismissed without a
// selection.
- (void)menuItemSelected:(id)sender;
@interface MenuWasRunCallbackHolder : NSObject
@property MenuWasRunCallback callback;
@end
@implementation MenuWasRunCallbackHolder
@synthesize callback = _callback;
@end
@end // WebMenuRunner (PrivateAPI)
@implementation WebMenuRunner {
// The native menu control.
NSMenu* __strong _menu;
// The index of the selected menu item. Set to -1 initially, and then set to
// the index of the selected item if an item was selected.
int _selectedItemIndex;
// A flag set to YES if a menu item was chosen, or NO if the menu was
// dismissed without selecting an item.
BOOL _menuItemWasChosen;
// The index of the selected menu item.
int _index;
// The font size being used for the menu.
CGFloat _fontSize;
@ -49,12 +46,7 @@ static const char kMenuWasRunCallbackKey = 0;
if ((self = [super init])) {
_menu = [[NSMenu alloc] initWithTitle:@""];
_menu.autoenablesItems = NO;
if (rightAligned) {
_menu.userInterfaceLayoutDirection =
NSUserInterfaceLayoutDirectionRightToLeft;
}
_selectedItemIndex = -1;
_index = -1;
_fontSize = fontSize;
_rightAligned = rightAligned;
for (const auto& item : items) {
@ -70,32 +62,25 @@ static const char kMenuWasRunCallbackKey = 0;
return;
}
std::string label = item->label.value_or("");
NSString* title = base::SysUTF8ToNSString(label);
// https://crbug.com/40726719: SysUTF8ToNSString will return nil if the bits
NSString* title = base::SysUTF8ToNSString(item->label.value_or(""));
// https://crbug.com/1140620: SysUTF8ToNSString will return nil if the bits
// that it is passed cannot be turned into a CFString. If this nil value is
// passed to -[NSMenuItem addItemWithTitle:action:keyEquivalent:], Chromium
// passed to -[NSMenuItem addItemWithTitle:action:keyEquivalent], Chromium
// will crash. Therefore, for debugging, if the result is nil, substitute in
// the raw bytes, encoded for safety in base64, to allow for investigation.
if (!title) {
title = base::SysUTF8ToNSString(base::Base64Encode(label));
title = base::SysUTF8ToNSString(base::Base64Encode(*item->label));
}
// TODO(https://crbug.com/389084419): Figure out how to handle
// blink::mojom::MenuItem::Type::kGroup items. This should use the macOS 14+
// support for section headers, but popup menus have to resize themselves to
// match the scale of the page, and there's no good way (currently) to get the
// font used for section header items in order to scale it and set it.
NSMenuItem* menuItem = [_menu addItemWithTitle:title
action:@selector(menuItemSelected:)
keyEquivalent:@""];
if (item->tool_tip.has_value()) {
menuItem.toolTip = base::SysUTF8ToNSString(item->tool_tip.value());
NSString* toolTip = base::SysUTF8ToNSString(item->tool_tip.value());
[menuItem setToolTip:toolTip];
}
menuItem.enabled =
item->enabled && item->type != blink::mojom::MenuItem::Type::kGroup;
menuItem.target = self;
[menuItem setEnabled:(item->enabled &&
item->type != blink::mojom::MenuItem::Type::kGroup)];
[menuItem setTarget:self];
// Set various alignment/language attributes.
NSMutableDictionary* attrs = [[NSMutableDictionary alloc] initWithCapacity:3];
@ -128,59 +113,65 @@ static const char kMenuWasRunCallbackKey = 0;
//
// This is the approach that WebKit uses; see PopupMenuMac::populate():
// https://github.com/search?q=repo%3AWebKit/WebKit%20PopupMenuMac%3A%3Apopulate&type=code
NSCharacterSet* whitespaceSet = NSCharacterSet.whitespaceCharacterSet;
menuItem.title = [title stringByTrimmingCharactersInSet:whitespaceSet];
NSCharacterSet* whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
[menuItem setTitle:[title stringByTrimmingCharactersInSet:whitespaceSet]];
menuItem.tag = _menu.numberOfItems - 1;
[menuItem setTag:[_menu numberOfItems] - 1];
}
// Reflects the result of the user's interaction with the popup menu. If NO, the
// menu was dismissed without the user choosing an item, which can happen if the
// user clicked outside the menu region or hit the escape key. If YES, the user
// selected an item from the menu.
- (BOOL)menuItemWasChosen {
return _selectedItemIndex != -1;
}
- (int)indexOfSelectedItem {
return _selectedItemIndex;
return _menuItemWasChosen;
}
- (void)menuItemSelected:(id)sender {
_selectedItemIndex = [sender tag];
_menuItemWasChosen = YES;
}
- (void)runMenuInView:(NSView*)view
withBounds:(NSRect)bounds
initialIndex:(int)index {
// In a testing situation, make the callback and early-exit.
MenuWasRunCallbackHolder* holder =
objc_getAssociatedObject(view, &kMenuWasRunCallbackKey);
if (holder) {
holder.callback.Run(view, bounds, index);
return;
// Set up the button cell, converting to NSView coordinates. The menu is
// positioned such that the currently selected menu item appears over the
// popup button, which is the expected Mac popup menu behavior.
NSPopUpButtonCell* cell = [[NSPopUpButtonCell alloc] initTextCell:@""
pullsDown:NO];
cell.menu = _menu;
// We use selectItemWithTag below so if the index is out-of-bounds nothing
// bad happens.
[cell selectItemWithTag:index];
if (_rightAligned) {
cell.userInterfaceLayoutDirection =
NSUserInterfaceLayoutDirectionRightToLeft;
_menu.userInterfaceLayoutDirection =
NSUserInterfaceLayoutDirectionRightToLeft;
}
// Add a checkmark to the initial item.
NSMenuItem* item = [_menu itemWithTag:index];
item.state = NSControlStateValueOn;
// When popping up a menu near the Dock, Cocoa restricts the menu
// size to not overlap the Dock, with a scroll arrow. Below a
// certain point this doesn't work. At that point the menu is
// popped up above the element, so that the current item can be
// selected without mouse-tracking selecting a different item
// immediately.
//
// Unfortunately, instead of popping up above the passed |bounds|,
// it pops up above the bounds of the view passed to inView:. Use a
// dummy view to fake this out.
NSView* dummyView = [[NSView alloc] initWithFrame:bounds];
[view addSubview:dummyView];
// Create a rect roughly containing the initial item, and center it in the
// provided bounds.
NSRect initialItemBounds =
NSInsetRect(bounds, /*dX=*/0.0f,
/*dY=*/(NSHeight(bounds) - _fontSize) / 2);
initialItemBounds = NSIntegralRect(initialItemBounds);
// Display the menu, and set a flag if a menu item was chosen.
[cell attachPopUpWithFrame:dummyView.bounds inView:dummyView];
[cell performClickWithFrame:dummyView.bounds inView:dummyView];
// Increase the minimum width of the menu so that we don't end up with a tiny
// menu floating in a sea of the popup widget.
_menu.minimumWidth = NSWidth(initialItemBounds);
[dummyView removeFromSuperview];
// The call to do the popup menu takes the location of the upper-left corner.
// Tweak it to compensate for the overall padding of the menu.
NSPoint initialPoint =
NSMakePoint(NSMinX(initialItemBounds), NSMaxY(initialItemBounds));
initialPoint.x -= 8;
initialPoint.y += 4;
// Do the popup.
[_menu popUpMenuPositioningItem:item atLocation:initialPoint inView:view];
if ([self menuItemWasChosen])
_index = [cell indexOfSelectedItem];
}
- (void)cancelSynchronously {
@ -228,17 +219,8 @@ static const char kMenuWasRunCallbackKey = 0;
}
}
+ (void)registerForTestingMenuRunCallback:(MenuWasRunCallback)callback
forView:(NSView*)view {
MenuWasRunCallbackHolder* holder = [[MenuWasRunCallbackHolder alloc] init];
holder.callback = callback;
objc_setAssociatedObject(view, &kMenuWasRunCallbackKey, holder,
OBJC_ASSOCIATION_RETAIN);
}
+ (void)unregisterForTestingMenuRunCallbackForView:(NSView*)view {
objc_setAssociatedObject(view, &kMenuWasRunCallbackKey, nil,
OBJC_ASSOCIATION_RETAIN);
- (int)indexOfSelectedItem {
return _index;
}
@end // WebMenuRunner

@ -177,6 +177,8 @@ class ShellAddedObserver {
// corresponding to the page.
class RenderWidgetHostViewCocoaObserver {
public:
// The method name for 'didAddSubview'.
static constexpr char kDidAddSubview[] = "didAddSubview:";
static constexpr char kShowDefinitionForAttributedString[] =
"showDefinitionForAttributedString:atPoint:";
@ -200,12 +202,12 @@ class RenderWidgetHostViewCocoaObserver {
virtual ~RenderWidgetHostViewCocoaObserver();
// Called when a popup was attempted to be displayed, conveying the bounds of
// the popup rectangle (in the RenderWidgetHostViewCocoa coordinate system)
// and the initially-selected item. The popup will not actually be triggered.
virtual void DidAttemptToShowPopup(const gfx::Rect& bounds,
int selected_item) {}
// Called when RenderWidgetHostViewCocoa is asked to show definition of
// Called when a new NSView is added as a subview of RWHVCocoa.
// |rect_in_root_view| represents the bounds of the NSView in RWHVCocoa
// coordinates. The view will be dismissed shortly after this call.
virtual void DidAddSubviewWillBeDismissed(
const gfx::Rect& rect_in_root_view) {}
// Called when RenderWidgeHostViewCocoa is asked to show definition of
// |for_word| using Mac's dictionary popup.
virtual void OnShowDefinitionForAttributedString(
const std::string& for_word) {}

@ -9,19 +9,16 @@
#include <memory>
#include "base/apple/foundation_util.h"
#include "base/apple/scoped_objc_class_swizzler.h"
#include "base/functional/bind.h"
#include "base/lazy_instance.h"
#include "base/strings/sys_string_conversions.h"
#import "content/app_shim_remote_cocoa/render_widget_host_view_cocoa.h"
#include "content/app_shim_remote_cocoa/web_menu_runner_mac.h"
#include "content/browser/renderer_host/render_widget_host_view_mac.h"
#include "content/browser/renderer_host/text_input_client_mac.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/mac/attributed_string_type_converters.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "ui/base/mojom/attributed_string.mojom.h"
#include "ui/gfx/geometry/point.h"
@ -31,6 +28,7 @@
// The interface class used to override the implementation of some of
// RenderWidgetHostViewCocoa methods for tests.
@interface RenderWidgetHostViewCocoaSwizzler : NSObject
- (void)didAddSubview:(NSView*)view;
- (void)showDefinitionForAttributedString:(NSAttributedString*)attrString
atPoint:(NSPoint)textBaselineOrigin;
@end
@ -40,6 +38,7 @@ namespace content {
using base::apple::ScopedObjCClassSwizzler;
// static
constexpr char RenderWidgetHostViewCocoaObserver::kDidAddSubview[];
constexpr char
RenderWidgetHostViewCocoaObserver::kShowDefinitionForAttributedString[];
@ -53,33 +52,17 @@ std::map<WebContents*, RenderWidgetHostViewCocoaObserver*>
namespace {
content::RenderWidgetHostViewMac* GetRenderWidgetHostViewMac(
WebContents* contents) {
auto* rwhv_base = static_cast<RenderWidgetHostViewBase*>(
contents->GetRenderWidgetHostView());
if (rwhv_base && !rwhv_base->IsRenderWidgetHostViewChildFrame()) {
return static_cast<RenderWidgetHostViewMac*>(rwhv_base);
}
return nil;
}
RenderWidgetHostViewCocoa* GetRenderWidgetHostViewCocoa(WebContents* contents) {
content::RenderWidgetHostViewMac* rwhv_mac =
GetRenderWidgetHostViewMac(contents);
if (!rwhv_mac) {
return nil;
}
return rwhv_mac->GetInProcessNSView();
}
content::RenderWidgetHostViewMac* GetRenderWidgetHostViewMac(NSObject* object) {
for (auto* contents : WebContentsImpl::GetAllWebContents()) {
content::RenderWidgetHostViewMac* rwhv_mac =
GetRenderWidgetHostViewMac(contents);
if (rwhv_mac && rwhv_mac->GetInProcessNSView() == object) {
return rwhv_mac;
auto* rwhv_base = static_cast<RenderWidgetHostViewBase*>(
contents->GetRenderWidgetHostView());
if (rwhv_base && !rwhv_base->IsRenderWidgetHostViewChildFrame()) {
auto* rwhv_mac = static_cast<RenderWidgetHostViewMac*>(rwhv_base);
if (rwhv_mac->GetInProcessNSView() == object)
return rwhv_mac;
}
}
return nullptr;
}
@ -101,43 +84,30 @@ RenderWidgetHostViewCocoaObserver::GetObserver(WebContents* web_contents) {
RenderWidgetHostViewCocoaObserver::RenderWidgetHostViewCocoaObserver(
WebContents* web_contents)
: web_contents_(web_contents) {
if (rwhvcocoa_swizzlers_.empty()) {
if (rwhvcocoa_swizzlers_.empty())
SetUpSwizzlers();
}
MenuWasRunCallback callback = base::BindRepeating(
[](RenderWidgetHostViewCocoaObserver* observer, NSView* view,
NSRect bounds, int index) {
RenderWidgetHostViewCocoa* rwhv_cocoa =
base::apple::ObjCCast<RenderWidgetHostViewCocoa>(view);
gfx::Rect rect = [rwhv_cocoa flipNSRectToRect:bounds];
observer->DidAttemptToShowPopup(rect, index);
},
this);
[WebMenuRunner registerForTestingMenuRunCallback:callback
forView:GetRenderWidgetHostViewCocoa(
web_contents)];
DCHECK(!observers_.count(web_contents));
observers_[web_contents] = this;
}
RenderWidgetHostViewCocoaObserver::~RenderWidgetHostViewCocoaObserver() {
[WebMenuRunner
unregisterForTestingMenuRunCallbackForView:GetRenderWidgetHostViewCocoa(
web_contents_)];
observers_.erase(web_contents_);
if (observers_.empty()) {
if (observers_.empty())
rwhvcocoa_swizzlers_.clear();
}
}
void RenderWidgetHostViewCocoaObserver::SetUpSwizzlers() {
if (!rwhvcocoa_swizzlers_.empty()) {
if (!rwhvcocoa_swizzlers_.empty())
return;
}
// [RenderWidgetHostViewCocoa didAddSubview:NSView*].
rwhvcocoa_swizzlers_[kDidAddSubview] =
std::make_unique<ScopedObjCClassSwizzler>(
GetRenderWidgetHostViewCocoaClassForTesting(),
[RenderWidgetHostViewCocoaSwizzler class],
NSSelectorFromString(@(kDidAddSubview)));
// [RenderWidgetHostViewCocoa showDefinitionForAttributedString:atPoint].
rwhvcocoa_swizzlers_[kShowDefinitionForAttributedString] =
@ -204,6 +174,46 @@ void GetStringFromRangeForRenderWidget(
} // namespace content
@implementation RenderWidgetHostViewCocoaSwizzler
- (void)didAddSubview:(NSView*)view {
content::RenderWidgetHostViewCocoaObserver::GetSwizzler(
content::RenderWidgetHostViewCocoaObserver::kDidAddSubview)
->InvokeOriginal<void, NSView*>(self, _cmd, view);
content::RenderWidgetHostViewMac* rwhv_mac =
content::GetRenderWidgetHostViewMac(self);
if (!rwhv_mac)
return;
content::RenderWidgetHostViewCocoaObserver* observer =
content::RenderWidgetHostViewCocoaObserver::GetObserver(
rwhv_mac->GetWebContents());
if (!observer)
return;
NSRect bounds_in_cocoa_view =
[view convertRect:view.bounds toView:rwhv_mac->GetInProcessNSView()];
gfx::Rect rect =
[rwhv_mac->GetInProcessNSView() flipNSRectToRect:bounds_in_cocoa_view];
observer->DidAddSubviewWillBeDismissed(rect);
// This override is useful for testing popups. To make sure the run loops end
// after the call it is best to dismiss the popup soon.
NSEvent* dismissal_event =
[NSEvent mouseEventWithType:NSEventTypeLeftMouseDown
location:NSZeroPoint
modifierFlags:0
timestamp:0.0
windowNumber:0
context:nil
eventNumber:0
clickCount:1
pressure:1.0];
[NSApplication.sharedApplication postEvent:dismissal_event atStart:false];
}
- (void)showDefinitionForAttributedString:(NSAttributedString*)attrString
atPoint:(NSPoint)textBaselineOrigin {
@ -218,11 +228,9 @@ void GetStringFromRangeForRenderWidget(
auto* observer = content::RenderWidgetHostViewCocoaObserver::GetObserver(
rwhv_mac->GetWebContents());
if (!observer) {
if (!observer)
return;
}
observer->OnShowDefinitionForAttributedString(
base::SysNSStringToUTF8(attrString.string));
}
@end

@ -38,7 +38,7 @@ ActionResult InteractionTestUtilSimulatorMac::SelectMenuItem(
LOG(ERROR) << "Cannot retrieve MenuControllerCocoa from menu.";
return ActionResult::kFailed;
}
ui::MenuModel* const model = controller.model;
ui::MenuModel* const model = [controller model];
if (!model) {
LOG(ERROR) << "Cannot retrieve MenuModel from controller.";
return ActionResult::kFailed;

@ -33,24 +33,35 @@ COMPONENT_EXPORT(UI_MENUS)
@interface MenuControllerCocoa
: NSObject <NSMenuDelegate, NSUserInterfaceValidations>
- (instancetype)init NS_UNAVAILABLE;
// Note that changing this will have no effect if you use
// |-initWithModel:useWithPopUpButtonCell:| or after the first call to |-menu|.
@property(nonatomic, assign) BOOL useWithPopUpButtonCell;
// NIB-based initializer. This does not create a menu. Clients can set the
// properties of the object and the menu will be created upon the first call to
// |-maybeBuildWithColorProvider:| or |-menu|.
- (instancetype)init;
// Builds a NSMenu from the pre-built model (must not be nil). Changes made
// to the contents of the model after calling this will not be noticed.
// to the contents of the model after calling this will not be noticed. If
// the menu will be displayed by a NSPopUpButtonCell, it needs to be of a
// slightly different form (0th item is empty).
- (instancetype)initWithModel:(ui::MenuModel*)model
delegate:(id<MenuControllerCocoaDelegate>)delegate;
delegate:(id<MenuControllerCocoaDelegate>)delegate
useWithPopUpButtonCell:(BOOL)useWithCell;
// Programmatically close the constructed menu.
- (void)cancel;
@property(readwrite) ui::MenuModel* model;
- (ui::MenuModel*)model;
- (void)setModel:(ui::MenuModel*)model;
// Access to the constructed menu. If the menu has not been built yet it will be
// built on the first call.
@property(readonly) NSMenu* menu;
// Access to the constructed menu if the complex initializer was used. If the
// menu has not bee built yet it will be built on the first call.
- (NSMenu*)menu;
// Whether the menu is currently open.
@property(readonly, getter=isMenuOpen) BOOL menuOpen;
- (BOOL)isMenuOpen;
@end

@ -4,8 +4,6 @@
#import "ui/menus/cocoa/menu_controller.h"
#include <AppKit/AppKit.h>
#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/apple/owned_objc.h"
@ -55,8 +53,9 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
// which allows it to be stored in the representedObject field of an NSMenuItem.
@interface WeakPtrToMenuModelAsNSObject : NSObject
+ (instancetype)weakPtrForModel:(ui::MenuModel*)model;
+ (ui::MenuModel*)menuModelForNSMenuItem:(NSMenuItem*)menuItem;
@property(readonly) ui::MenuModel* menuModel;
+ (ui::MenuModel*)getFrom:(id)instance;
- (instancetype)initWithModel:(ui::MenuModel*)model;
- (ui::MenuModel*)menuModel;
@end
@implementation WeakPtrToMenuModelAsNSObject {
@ -67,10 +66,9 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
return [[WeakPtrToMenuModelAsNSObject alloc] initWithModel:model];
}
+ (ui::MenuModel*)menuModelForNSMenuItem:(NSMenuItem*)menuItem {
return base::apple::ObjCCastStrict<WeakPtrToMenuModelAsNSObject>(
menuItem.representedObject)
.menuModel;
+ (ui::MenuModel*)getFrom:(id)instance {
return [base::apple::ObjCCastStrict<WeakPtrToMenuModelAsNSObject>(instance)
menuModel];
}
- (instancetype)initWithModel:(ui::MenuModel*)model {
@ -116,10 +114,13 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
@implementation MenuControllerCocoa {
base::WeakPtr<ui::MenuModel> _model;
NSMenu* __strong _menu;
BOOL _useWithPopUpButtonCell; // If YES, 0th item is blank
BOOL _isMenuOpen;
id<MenuControllerCocoaDelegate> __weak _delegate;
}
@synthesize useWithPopUpButtonCell = _useWithPopUpButtonCell;
- (ui::MenuModel*)model {
return _model.get();
}
@ -128,11 +129,18 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
_model = model->AsWeakPtr();
}
- (instancetype)init {
self = [super init];
return self;
}
- (instancetype)initWithModel:(ui::MenuModel*)model
delegate:(id<MenuControllerCocoaDelegate>)delegate {
delegate:(id<MenuControllerCocoaDelegate>)delegate
useWithPopUpButtonCell:(BOOL)useWithCell {
if ((self = [super init])) {
_model = model->AsWeakPtr();
_delegate = delegate;
_useWithPopUpButtonCell = useWithCell;
[self maybeBuild];
}
return self;
@ -227,7 +235,9 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
// On the Mac, context menus do not have accelerators except when
// |force_show_accelerator_for_item| is set to true. Consult with the Mac
// team before using the flag!
if (model->GetForceShowAcceleratorForItemAt(index)) {
// Menus constructed for context use have useWithPopUpButtonCell_ set to NO.
if (_useWithPopUpButtonCell ||
model->GetForceShowAcceleratorForItemAt(index)) {
ui::Accelerator accelerator;
if (model->GetAcceleratorAt(index, &accelerator)) {
KeyEquivalentAndModifierMask* equivalent =
@ -254,7 +264,7 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
}
ui::MenuModel* model =
[WeakPtrToMenuModelAsNSObject menuModelForNSMenuItem:menuItem];
[WeakPtrToMenuModelAsNSObject getFrom:menuItem.representedObject];
if (!model) {
return NO;
}
@ -289,7 +299,7 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
NSMenuItem* menuItem = base::apple::ObjCCastStrict<NSMenuItem>(sender);
ui::MenuModel* model =
[WeakPtrToMenuModelAsNSObject menuModelForNSMenuItem:menuItem];
[WeakPtrToMenuModelAsNSObject getFrom:menuItem.representedObject];
DCHECK(model);
const size_t modelIndex = base::checked_cast<size_t>(menuItem.tag);
const ui::ElementIdentifier identifier =
@ -319,6 +329,17 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
if (_delegate) {
[_delegate controllerWillAddMenu:_menu fromModel:_model.get()];
}
// If this is to be used with a NSPopUpButtonCell, add an item at the 0th
// position that's empty. Doing it after the menu has been constructed won't
// complicate creation logic, and since the tags are model indexes, they
// are unaffected by the extra item.
if (_useWithPopUpButtonCell) {
NSMenuItem* blankItem = [[NSMenuItem alloc] initWithTitle:@""
action:nil
keyEquivalent:@""];
[_menu insertItem:blankItem atIndex:0];
}
}
- (NSMenu*)menu {

@ -171,7 +171,8 @@ class OwningDelegate : public Delegate {
: did_delete_(did_delete), model_(this) {
model_.AddItem(1, u"foo");
controller_ = [[WatchedLifetimeMenuController alloc] initWithModel:&model_
delegate:nil];
delegate:nil
useWithPopUpButtonCell:NO];
[controller_ setDeallocCalled:did_dealloc];
}
@ -226,8 +227,9 @@ TEST_F(MenuControllerTest, EmptyMenu) {
Delegate delegate;
SimpleMenuModel model(&delegate);
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(0, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(0, [[menu menu] numberOfItems]);
}
TEST_F(MenuControllerTest, BasicCreation) {
@ -241,17 +243,18 @@ TEST_F(MenuControllerTest, BasicCreation) {
model.AddItem(5, u"five");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(6, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(6, [[menu menu] numberOfItems]);
// Check the title, tag, and represented object are correct for a random
// element.
NSMenuItem* itemTwo = [menu.menu itemAtIndex:2];
NSString* title = itemTwo.title;
NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
NSString* title = [itemTwo title];
EXPECT_EQ(u"three", base::SysNSStringToUTF16(title));
EXPECT_EQ(2, itemTwo.tag);
EXPECT_EQ(2, [itemTwo tag]);
EXPECT_TRUE([menu.menu itemAtIndex:3].separatorItem);
EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
}
TEST_F(MenuControllerTest, Submenus) {
@ -266,29 +269,30 @@ TEST_F(MenuControllerTest, Submenus) {
model.AddItem(6, u"three");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(3, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(3, [[menu menu] numberOfItems]);
// Inspect the submenu to ensure it has correct properties.
NSMenuItem* menuItem = [menu.menu itemAtIndex:1];
EXPECT_TRUE(menuItem.enabled);
NSMenu* submenu = menuItem.submenu;
NSMenuItem* menuItem = [[menu menu] itemAtIndex:1];
EXPECT_TRUE([menuItem isEnabled]);
NSMenu* submenu = [menuItem submenu];
EXPECT_TRUE(submenu);
EXPECT_EQ(3, submenu.numberOfItems);
EXPECT_EQ(3, [submenu numberOfItems]);
// Inspect one of the items to make sure it has the correct model as its
// represented object and the proper tag.
NSMenuItem* submenuItem = [submenu itemAtIndex:1];
NSString* title = submenuItem.title;
NSString* title = [submenuItem title];
EXPECT_EQ(u"sub-two", base::SysNSStringToUTF16(title));
EXPECT_EQ(1, submenuItem.tag);
EXPECT_EQ(1, [submenuItem tag]);
// Make sure the item after the submenu is correct and its represented
// object is back to the top model.
NSMenuItem* item = [menu.menu itemAtIndex:2];
title = item.title;
NSMenuItem* item = [[menu menu] itemAtIndex:2];
title = [item title];
EXPECT_EQ(u"three", base::SysNSStringToUTF16(title));
EXPECT_EQ(2, item.tag);
EXPECT_EQ(2, [item tag]);
}
TEST_F(MenuControllerTest, EmptySubmenu) {
@ -299,15 +303,16 @@ TEST_F(MenuControllerTest, EmptySubmenu) {
model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(2, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(2, [[menu menu] numberOfItems]);
// Inspect the submenu to ensure it has one item labeled "(empty)".
NSMenu* submenu = [menu.menu itemAtIndex:1].submenu;
NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
EXPECT_TRUE(submenu);
EXPECT_EQ(1, submenu.numberOfItems);
EXPECT_EQ(1, [submenu numberOfItems]);
EXPECT_NSEQ(@"(empty)", [submenu itemAtIndex:0].title);
EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);
}
// Tests that an empty menu item, "(empty)", is added to a submenu that contains
@ -325,15 +330,16 @@ TEST_F(MenuControllerTest, EmptySubmenuWhenAllChildItemsAreHidden) {
model.AddSubMenuWithStringId(4, kTestLabelResourceId, &submodel);
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(2, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(2, [[menu menu] numberOfItems]);
// Inspect the submenu to ensure it has one item labeled "(empty)".
NSMenu* submenu = [menu.menu itemAtIndex:1].submenu;
NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
EXPECT_TRUE(submenu);
EXPECT_EQ(1, submenu.numberOfItems);
EXPECT_EQ(1, [submenu numberOfItems]);
EXPECT_NSEQ(@"(empty)", [submenu itemAtIndex:0].title);
EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);
}
// Tests hiding a submenu item. If a submenu item with children is set to
@ -357,27 +363,29 @@ TEST_F(MenuControllerTest, HiddenSubmenu) {
// Create the controller.
MenuControllerCocoa* menu_controller =
[[MenuControllerCocoa alloc] initWithModel:&model delegate:nil];
EXPECT_EQ(2, menu_controller.menu.numberOfItems);
delegate.menu_to_close_ = menu_controller.menu;
[[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(2, [[menu_controller menu] numberOfItems]);
delegate.menu_to_close_ = [menu_controller menu];
// Show the menu.
[NSRunLoop.currentRunLoop performInModes:@[ NSEventTrackingRunLoopMode ]
block:^{
EXPECT_TRUE(menu_controller.menuOpen);
// Ensure that the submenu is hidden.
NSMenuItem* item =
[menu_controller.menu itemAtIndex:1];
EXPECT_TRUE(item.hidden);
}];
[NSRunLoop.currentRunLoop
performInModes:@[ NSEventTrackingRunLoopMode ]
block:^{
EXPECT_TRUE([menu_controller isMenuOpen]);
// Ensure that the submenu is hidden.
NSMenuItem* item = [[menu_controller menu] itemAtIndex:1];
EXPECT_TRUE([item isHidden]);
}];
// Pop open the menu, which will spin an event-tracking run loop.
[NSMenu popUpContextMenu:menu_controller.menu
[NSMenu popUpContextMenu:[menu_controller menu]
withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
NSZeroPoint)
forView:test_window().contentView];
forView:[test_window() contentView]];
EXPECT_FALSE(menu_controller.menuOpen);
EXPECT_FALSE([menu_controller isMenuOpen]);
// Pump the task that notifies the delegate.
base::RunLoop().RunUntilIdle();
@ -407,33 +415,34 @@ TEST_F(MenuControllerTest, DisabledSubmenu) {
// Create the controller.
MenuControllerCocoa* menu_controller =
[[MenuControllerCocoa alloc] initWithModel:&model delegate:nil];
delegate.menu_to_close_ = menu_controller.menu;
[[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil
useWithPopUpButtonCell:NO];
delegate.menu_to_close_ = [menu_controller menu];
// Show the menu.
[NSRunLoop.currentRunLoop performInModes:@[ NSEventTrackingRunLoopMode ]
block:^{
EXPECT_TRUE(menu_controller.menuOpen);
[NSRunLoop.currentRunLoop
performInModes:@[ NSEventTrackingRunLoopMode ]
block:^{
EXPECT_TRUE([menu_controller isMenuOpen]);
// Ensure that the disabled submenu is
// disabled.
NSMenuItem* disabled_item =
[menu_controller.menu itemAtIndex:1];
EXPECT_FALSE(disabled_item.enabled);
// Ensure that the disabled submenu is disabled.
NSMenuItem* disabled_item =
[[menu_controller menu] itemAtIndex:1];
EXPECT_FALSE([disabled_item isEnabled]);
// Ensure that the enabled submenu is
// enabled.
NSMenuItem* enabled_item =
[menu_controller.menu itemAtIndex:2];
EXPECT_TRUE(enabled_item.enabled);
}];
// Ensure that the enabled submenu is enabled.
NSMenuItem* enabled_item =
[[menu_controller menu] itemAtIndex:2];
EXPECT_TRUE([enabled_item isEnabled]);
}];
// Pop open the menu, which will spin an event-tracking run loop.
[NSMenu popUpContextMenu:menu_controller.menu
[NSMenu popUpContextMenu:[menu_controller menu]
withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
NSZeroPoint)
forView:test_window().contentView];
EXPECT_FALSE(menu_controller.menuOpen);
forView:[test_window() contentView]];
EXPECT_FALSE([menu_controller isMenuOpen]);
// Pump the task that notifies the delegate.
base::RunLoop().RunUntilIdle();
@ -441,30 +450,52 @@ TEST_F(MenuControllerTest, DisabledSubmenu) {
EXPECT_TRUE(delegate.did_close_);
}
TEST_F(MenuControllerTest, PopUpButton) {
Delegate delegate;
SimpleMenuModel model(&delegate);
model.AddItem(1, u"one");
model.AddItem(2, u"two");
model.AddItem(3, u"three");
// Menu should have an extra item inserted at position 0 that has an empty
// title.
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil
useWithPopUpButtonCell:YES];
EXPECT_EQ(4, [[menu menu] numberOfItems]);
EXPECT_EQ(std::u16string(),
base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]));
// Make sure the tags are still correct (the index no longer matches the tag).
NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
EXPECT_EQ(1, [itemTwo tag]);
}
TEST_F(MenuControllerTest, Execute) {
Delegate delegate;
SimpleMenuModel model(&delegate);
model.AddItem(1, u"one");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(1, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(1, [[menu menu] numberOfItems]);
// Fake selecting the menu item, we expect the delegate to be told to execute
// a command.
NSMenuItem* item = [menu.menu itemAtIndex:0];
NSMenuItem* item = [[menu menu] itemAtIndex:0];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[item.target performSelector:item.action withObject:item];
[[item target] performSelector:[item action] withObject:item];
#pragma clang diagnostic pop
EXPECT_EQ(1, delegate.execute_count_);
}
void Validate(MenuControllerCocoa* controller, NSMenu* menu) {
for (int i = 0; i < menu.numberOfItems; ++i) {
for (int i = 0; i < [menu numberOfItems]; ++i) {
NSMenuItem* item = [menu itemAtIndex:i];
[controller validateUserInterfaceItem:item];
if (item.hasSubmenu) {
Validate(controller, item.submenu);
if ([item hasSubmenu]) {
Validate(controller, [item submenu]);
}
}
}
@ -479,10 +510,11 @@ TEST_F(MenuControllerTest, Validate) {
model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(3, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(3, [[menu menu] numberOfItems]);
Validate(menu, menu.menu);
Validate(menu, [menu menu]);
}
// Tests that items which have a font set actually use that font.
@ -497,13 +529,34 @@ TEST_F(MenuControllerTest, LabelFontList) {
model.AddItem(2, u"two");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(2, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(2, [[menu menu] numberOfItems]);
Validate(menu, menu.menu);
Validate(menu, [menu menu]);
EXPECT_TRUE([menu.menu itemAtIndex:0].attributedTitle != nil);
EXPECT_TRUE([menu.menu itemAtIndex:1].attributedTitle == nil);
EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
}
TEST_F(MenuControllerTest, DefaultInitializer) {
Delegate delegate;
SimpleMenuModel model(&delegate);
model.AddItem(1, u"one");
model.AddItem(2, u"two");
model.AddItem(3, u"three");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] init];
EXPECT_FALSE([menu menu]);
[menu setModel:&model];
[menu setUseWithPopUpButtonCell:NO];
EXPECT_TRUE([menu menu]);
EXPECT_EQ(3, [[menu menu] numberOfItems]);
// Check immutability.
model.AddItem(4, u"four");
EXPECT_EQ(3, [[menu menu] numberOfItems]);
}
// Test that menus with dynamic labels actually get updated.
@ -517,15 +570,16 @@ TEST_F(MenuControllerTest, Dynamic) {
SimpleMenuModel model(&delegate);
model.AddItem(1, u"foo");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
EXPECT_EQ(1, menu.menu.numberOfItems);
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_EQ(1, [[menu menu] numberOfItems]);
// Validate() simulates opening the menu - the item label/icon should be
// initialized after this so we can validate the menu contents.
Validate(menu, menu.menu);
NSMenuItem* item = [menu.menu itemAtIndex:0];
Validate(menu, [menu menu]);
NSMenuItem* item = [[menu menu] itemAtIndex:0];
// Item should have the "initial" label and no icon.
EXPECT_EQ(initial, base::SysNSStringToUTF16(item.title));
EXPECT_EQ(nil, item.image);
EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
EXPECT_EQ(nil, [item image]);
// Now update the item to have a label of "second" and an icon.
std::u16string second = u"second";
@ -533,15 +587,15 @@ TEST_F(MenuControllerTest, Dynamic) {
const gfx::Image& icon = gfx::test::CreateImage(32, 32);
delegate.SetDynamicIcon(icon);
// Simulate opening the menu and validate that the item label + icon changes.
Validate(menu, menu.menu);
EXPECT_EQ(second, base::SysNSStringToUTF16(item.title));
EXPECT_NE(nil, item.image);
Validate(menu, [menu menu]);
EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
EXPECT_TRUE([item image] != nil);
// Now get rid of the icon and make sure it goes away.
delegate.SetDynamicIcon(gfx::Image());
Validate(menu, menu.menu);
EXPECT_EQ(second, base::SysNSStringToUTF16(item.title));
EXPECT_EQ(nil, item.image);
Validate(menu, [menu menu]);
EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
EXPECT_EQ(nil, [item image]);
}
TEST_F(MenuControllerTest, OpenClose) {
@ -558,25 +612,26 @@ TEST_F(MenuControllerTest, OpenClose) {
// Create the controller.
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
delegate.menu_to_close_ = menu.menu;
delegate:nil
useWithPopUpButtonCell:NO];
delegate.menu_to_close_ = [menu menu];
EXPECT_FALSE(menu.menuOpen);
EXPECT_FALSE([menu isMenuOpen]);
// In the event tracking run loop mode of the menu, verify that the controller
// reports the menu as open.
[NSRunLoop.currentRunLoop performInModes:@[ NSEventTrackingRunLoopMode ]
block:^{
EXPECT_TRUE(menu.menuOpen);
EXPECT_TRUE([menu isMenuOpen]);
}];
// Pop open the menu, which will spin an event-tracking run loop.
[NSMenu popUpContextMenu:menu.menu
[NSMenu popUpContextMenu:[menu menu]
withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
NSZeroPoint)
forView:test_window().contentView];
forView:[test_window() contentView]];
EXPECT_FALSE(menu.menuOpen);
EXPECT_FALSE([menu isMenuOpen]);
// When control returns back to here, the menu will have finished running its
// loop and will have closed itself (see Delegate::OnMenuWillShow).
@ -614,13 +669,13 @@ TEST_F(MenuControllerTest, OwningDelegate) {
// Unretained reference to the controller.
MenuControllerCocoa* controller = delegate->controller();
item = [controller.menu itemAtIndex:0];
item = [[controller menu] itemAtIndex:0];
EXPECT_TRUE(item);
// Simulate opening the menu and selecting an item. Methods are always
// invoked by AppKit in the following order.
[controller menuWillOpen:controller.menu];
[controller menuDidClose:controller.menu];
[controller menuWillOpen:[controller menu]];
[controller menuDidClose:[controller menu]];
}
EXPECT_FALSE(did_dealloc);
EXPECT_FALSE(did_delete);
@ -633,7 +688,7 @@ TEST_F(MenuControllerTest, OwningDelegate) {
@autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[item.target performSelector:item.action withObject:item];
[[item target] performSelector:[item action] withObject:item];
#pragma clang diagnostic pop
}
EXPECT_TRUE(did_dealloc);
@ -650,7 +705,8 @@ TEST_F(MenuControllerTest, InitBuildsMenu) {
model.AddItem(3, u"three");
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
delegate:nil
useWithPopUpButtonCell:YES];
EXPECT_TRUE([menu isMenuBuiltForTesting]);
}
@ -664,10 +720,11 @@ TEST_F(MenuControllerTest, Ampersands) {
model.SetMayHaveMnemonicsAt(1, false);
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
delegate:nil];
delegate:nil
useWithPopUpButtonCell:NO];
EXPECT_NSEQ([menu.menu itemAtIndex:0].title, @"New");
EXPECT_NSEQ([menu.menu itemAtIndex:1].title, @"Gin & Tonic");
EXPECT_NSEQ([[[menu menu] itemAtIndex:0] title], @"New");
EXPECT_NSEQ([[[menu menu] itemAtIndex:1] title], @"Gin & Tonic");
}
} // namespace

@ -81,7 +81,8 @@ void MenuRunnerImplCocoa::RunMenuAt(
menu_delegate_ = [[MenuControllerCocoaDelegateImpl alloc]
initWithParams:MenuControllerParamsForWidget(parent)];
menu_controller_ = [[MenuControllerCocoa alloc] initWithModel:menu_model_
delegate:menu_delegate_];
delegate:menu_delegate_
useWithPopUpButtonCell:NO];
closing_event_time_ = base::TimeTicks();
running_ = true;