Do popups using modern API
Using an NSPopUpButtonCell to do popups is borderline SPI, plus NSPopUpButtonCell plays some super weird games with having a first item that is magic/used for tool tips. Switch to supported API, and do cleanup. This was committed as r1406461 and reverted because the positioning wasn't as good as it could be. This change includes two differences: 1. The change to headless mode was moved out to its own CL, https://chromium-review.googlesource.com/c/chromium/src/+/6187262 2. The positioning of the menu is improved. Fixed: 389067059 Bug: 389084419 Change-Id: Id40b2f20c93c9bf05a40c96af457fcac7e6c1324 Low-Coverage-Reason: HARD_TO_TEST macOS API integration Include-Ci-Only-Tests: true Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6173642 Reviewed-by: Keren Zhu <kerenzhu@chromium.org> Reviewed-by: Leonard Grey <lgrey@chromium.org> Commit-Queue: Avi Drissman <avi@chromium.org> Cr-Commit-Position: refs/heads/main@{#1409709}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
840441bbb4
commit
1786625b69
chrome/browser
apps
guest_view
ui
cocoa
components/remote_cocoa/app_shim
content
app_shim_remote_cocoa
public
ui
base
interaction
menus
views
controls
@ -110,10 +110,10 @@ class NewSubViewAddedObserver : content::RenderWidgetHostViewCocoaObserver {
|
||||
const gfx::Rect& view_bounds_in_screen() const { return bounds_; }
|
||||
|
||||
private:
|
||||
void DidAddSubviewWillBeDismissed(
|
||||
const gfx::Rect& bounds_in_root_view) override {
|
||||
void DidAttemptToShowPopup(const gfx::Rect& bounds,
|
||||
int selected_item) override {
|
||||
did_receive_rect_ = true;
|
||||
bounds_ = bounds_in_root_view;
|
||||
bounds_ = bounds;
|
||||
if (run_loop_)
|
||||
run_loop_->Quit();
|
||||
}
|
||||
@ -127,11 +127,7 @@ class NewSubViewAddedObserver : content::RenderWidgetHostViewCocoaObserver {
|
||||
class WebViewInteractiveTest : public extensions::PlatformAppBrowserTest {
|
||||
public:
|
||||
WebViewInteractiveTest()
|
||||
: guest_view_(nullptr),
|
||||
embedder_web_contents_(nullptr),
|
||||
corner_(gfx::Point()),
|
||||
mouse_click_result_(false),
|
||||
first_click_(true) {}
|
||||
: guest_view_(nullptr), embedder_web_contents_(nullptr) {}
|
||||
|
||||
void SetUpCommandLine(base::CommandLine* command_line) override {
|
||||
extensions::PlatformAppBrowserTest::SetUpCommandLine(command_line);
|
||||
@ -492,8 +488,8 @@ class WebViewInteractiveTest : public extensions::PlatformAppBrowserTest {
|
||||
embedder_web_contents_;
|
||||
|
||||
gfx::Point corner_;
|
||||
bool mouse_click_result_;
|
||||
bool first_click_;
|
||||
bool mouse_click_result_ = false;
|
||||
bool first_click_ = true;
|
||||
};
|
||||
|
||||
class WebViewImeInteractiveTest : public WebViewInteractiveTest {
|
||||
|
@ -81,8 +81,7 @@ void RenderViewContextMenuMacCocoa::Show() {
|
||||
initWithParams:MenuControllerParamsForWidget(widget)];
|
||||
menu_controller_ =
|
||||
[[MenuControllerCocoa alloc] initWithModel:&menu_model_
|
||||
delegate:menu_controller_delegate_
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate:menu_controller_delegate_];
|
||||
|
||||
NSPoint position =
|
||||
NSMakePoint(params_.x, NSHeight(parent_view_.bounds) - params_.y);
|
||||
|
@ -228,8 +228,6 @@ void StatusIconMac::UpdatePlatformContextMenu(StatusIconMenuModel* model) {
|
||||
|
||||
void StatusIconMac::CreateMenu(ui::MenuModel* model) {
|
||||
DCHECK(model);
|
||||
menu_ = [[MenuControllerCocoa alloc] initWithModel:model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
menu_ = [[MenuControllerCocoa alloc] initWithModel:model delegate:nil];
|
||||
item().menu = menu_.menu;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ ContextMenuRunner::ContextMenuRunner(
|
||||
|
||||
ContextMenuRunner::~ContextMenuRunner() {
|
||||
if (menu_controller_) {
|
||||
CHECK(!menu_controller_.isMenuOpen);
|
||||
CHECK(!menu_controller_.menuOpen);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,8 +60,7 @@ void ContextMenuRunner::ShowMenu(mojom::ContextMenuPtr menu,
|
||||
initWithParams:std::move(menu->params)];
|
||||
menu_controller_ =
|
||||
[[MenuControllerCocoa alloc] initWithModel:menu_model_.get()
|
||||
delegate:menu_delegate_
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate:menu_delegate_];
|
||||
|
||||
if (!target_view) {
|
||||
target_view = window.contentView;
|
||||
|
@ -13,6 +13,7 @@
|
||||
|
||||
#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"
|
||||
@ -49,6 +50,7 @@ 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,24 +9,30 @@
|
||||
|
||||
#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 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.
|
||||
|
||||
// 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
|
||||
@interface WebMenuRunner : NSObject
|
||||
|
||||
// Initializes the MenuDelegate with a list of items sent from WebKit.
|
||||
// Initializes the MenuDelegate with a list of items sent from Blink.
|
||||
- (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.
|
||||
- (BOOL)menuItemWasChosen;
|
||||
@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;
|
||||
|
||||
// Displays and runs a native popup menu.
|
||||
- (void)runMenuInView:(NSView*)view
|
||||
@ -38,13 +44,22 @@
|
||||
// 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/812260.
|
||||
// https://crbug.com/41370640.
|
||||
- (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,34 +4,37 @@
|
||||
|
||||
#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"
|
||||
|
||||
@interface WebMenuRunner (PrivateAPI)
|
||||
namespace {
|
||||
|
||||
// Worker function used during initialization.
|
||||
- (void)addItem:(const blink::mojom::MenuItemPtr&)item;
|
||||
// A key to attach a MenuWasRunCallbackHolder to the NSView*.
|
||||
static const char kMenuWasRunCallbackKey = 0;
|
||||
|
||||
// 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;
|
||||
} // namespace
|
||||
|
||||
@end // WebMenuRunner (PrivateAPI)
|
||||
@interface MenuWasRunCallbackHolder : NSObject
|
||||
@property MenuWasRunCallback callback;
|
||||
@end
|
||||
|
||||
@implementation MenuWasRunCallbackHolder
|
||||
@synthesize callback = _callback;
|
||||
@end
|
||||
|
||||
@implementation WebMenuRunner {
|
||||
// The native menu control.
|
||||
NSMenu* __strong _menu;
|
||||
|
||||
// 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 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;
|
||||
|
||||
// The font size being used for the menu.
|
||||
CGFloat _fontSize;
|
||||
@ -46,7 +49,12 @@
|
||||
if ((self = [super init])) {
|
||||
_menu = [[NSMenu alloc] initWithTitle:@""];
|
||||
_menu.autoenablesItems = NO;
|
||||
_index = -1;
|
||||
if (rightAligned) {
|
||||
_menu.userInterfaceLayoutDirection =
|
||||
NSUserInterfaceLayoutDirectionRightToLeft;
|
||||
}
|
||||
|
||||
_selectedItemIndex = -1;
|
||||
_fontSize = fontSize;
|
||||
_rightAligned = rightAligned;
|
||||
for (const auto& item : items) {
|
||||
@ -62,25 +70,32 @@
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* title = base::SysUTF8ToNSString(item->label.value_or(""));
|
||||
// https://crbug.com/1140620: SysUTF8ToNSString will return nil if the bits
|
||||
std::string label = item->label.value_or("");
|
||||
NSString* title = base::SysUTF8ToNSString(label);
|
||||
// https://crbug.com/40726719: 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(*item->label));
|
||||
title = base::SysUTF8ToNSString(base::Base64Encode(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()) {
|
||||
NSString* toolTip = base::SysUTF8ToNSString(item->tool_tip.value());
|
||||
[menuItem setToolTip:toolTip];
|
||||
menuItem.toolTip = base::SysUTF8ToNSString(item->tool_tip.value());
|
||||
}
|
||||
[menuItem setEnabled:(item->enabled &&
|
||||
item->type != blink::mojom::MenuItem::Type::kGroup)];
|
||||
[menuItem setTarget:self];
|
||||
menuItem.enabled =
|
||||
item->enabled && item->type != blink::mojom::MenuItem::Type::kGroup;
|
||||
menuItem.target = self;
|
||||
|
||||
// Set various alignment/language attributes.
|
||||
NSMutableDictionary* attrs = [[NSMutableDictionary alloc] initWithCapacity:3];
|
||||
@ -113,65 +128,59 @@
|
||||
//
|
||||
// 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 setTitle:[title stringByTrimmingCharactersInSet:whitespaceSet]];
|
||||
NSCharacterSet* whitespaceSet = NSCharacterSet.whitespaceCharacterSet;
|
||||
menuItem.title = [title stringByTrimmingCharactersInSet:whitespaceSet];
|
||||
|
||||
[menuItem setTag:[_menu numberOfItems] - 1];
|
||||
menuItem.tag = _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 _menuItemWasChosen;
|
||||
return _selectedItemIndex != -1;
|
||||
}
|
||||
|
||||
- (int)indexOfSelectedItem {
|
||||
return _selectedItemIndex;
|
||||
}
|
||||
|
||||
- (void)menuItemSelected:(id)sender {
|
||||
_menuItemWasChosen = YES;
|
||||
_selectedItemIndex = [sender tag];
|
||||
}
|
||||
|
||||
- (void)runMenuInView:(NSView*)view
|
||||
withBounds:(NSRect)bounds
|
||||
initialIndex:(int)index {
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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];
|
||||
// Add a checkmark to the initial item.
|
||||
NSMenuItem* item = [_menu itemWithTag:index];
|
||||
item.state = NSControlStateValueOn;
|
||||
|
||||
// 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];
|
||||
// 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);
|
||||
|
||||
[dummyView removeFromSuperview];
|
||||
// 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);
|
||||
|
||||
if ([self menuItemWasChosen])
|
||||
_index = [cell indexOfSelectedItem];
|
||||
// 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];
|
||||
}
|
||||
|
||||
- (void)cancelSynchronously {
|
||||
@ -219,8 +228,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (int)indexOfSelectedItem {
|
||||
return _index;
|
||||
+ (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);
|
||||
}
|
||||
|
||||
@end // WebMenuRunner
|
||||
|
@ -177,8 +177,6 @@ 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:";
|
||||
|
||||
@ -202,12 +200,12 @@ class RenderWidgetHostViewCocoaObserver {
|
||||
|
||||
virtual ~RenderWidgetHostViewCocoaObserver();
|
||||
|
||||
// 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
|
||||
// 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
|
||||
// |for_word| using Mac's dictionary popup.
|
||||
virtual void OnShowDefinitionForAttributedString(
|
||||
const std::string& for_word) {}
|
||||
|
@ -9,16 +9,19 @@
|
||||
|
||||
#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"
|
||||
@ -28,7 +31,6 @@
|
||||
// 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
|
||||
@ -38,7 +40,6 @@ namespace content {
|
||||
using base::apple::ScopedObjCClassSwizzler;
|
||||
|
||||
// static
|
||||
constexpr char RenderWidgetHostViewCocoaObserver::kDidAddSubview[];
|
||||
constexpr char
|
||||
RenderWidgetHostViewCocoaObserver::kShowDefinitionForAttributedString[];
|
||||
|
||||
@ -52,17 +53,33 @@ 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()) {
|
||||
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;
|
||||
content::RenderWidgetHostViewMac* rwhv_mac =
|
||||
GetRenderWidgetHostViewMac(contents);
|
||||
if (rwhv_mac && rwhv_mac->GetInProcessNSView() == object) {
|
||||
return rwhv_mac;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@ -84,30 +101,43 @@ 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] =
|
||||
@ -174,46 +204,6 @@ 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 {
|
||||
@ -228,9 +218,11 @@ 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,35 +33,24 @@ COMPONENT_EXPORT(UI_MENUS)
|
||||
@interface MenuControllerCocoa
|
||||
: NSObject <NSMenuDelegate, NSUserInterfaceValidations>
|
||||
|
||||
// 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;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
// 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. If
|
||||
// the menu will be displayed by a NSPopUpButtonCell, it needs to be of a
|
||||
// slightly different form (0th item is empty).
|
||||
// to the contents of the model after calling this will not be noticed.
|
||||
- (instancetype)initWithModel:(ui::MenuModel*)model
|
||||
delegate:(id<MenuControllerCocoaDelegate>)delegate
|
||||
useWithPopUpButtonCell:(BOOL)useWithCell;
|
||||
delegate:(id<MenuControllerCocoaDelegate>)delegate;
|
||||
|
||||
// Programmatically close the constructed menu.
|
||||
- (void)cancel;
|
||||
|
||||
- (ui::MenuModel*)model;
|
||||
- (void)setModel:(ui::MenuModel*)model;
|
||||
@property(readwrite) ui::MenuModel* model;
|
||||
|
||||
// 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;
|
||||
// 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;
|
||||
|
||||
// Whether the menu is currently open.
|
||||
- (BOOL)isMenuOpen;
|
||||
@property(readonly, getter=isMenuOpen) BOOL menuOpen;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
#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"
|
||||
@ -53,9 +55,8 @@ 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*)getFrom:(id)instance;
|
||||
- (instancetype)initWithModel:(ui::MenuModel*)model;
|
||||
- (ui::MenuModel*)menuModel;
|
||||
+ (ui::MenuModel*)menuModelForNSMenuItem:(NSMenuItem*)menuItem;
|
||||
@property(readonly) ui::MenuModel* menuModel;
|
||||
@end
|
||||
|
||||
@implementation WeakPtrToMenuModelAsNSObject {
|
||||
@ -66,9 +67,10 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
|
||||
return [[WeakPtrToMenuModelAsNSObject alloc] initWithModel:model];
|
||||
}
|
||||
|
||||
+ (ui::MenuModel*)getFrom:(id)instance {
|
||||
return [base::apple::ObjCCastStrict<WeakPtrToMenuModelAsNSObject>(instance)
|
||||
menuModel];
|
||||
+ (ui::MenuModel*)menuModelForNSMenuItem:(NSMenuItem*)menuItem {
|
||||
return base::apple::ObjCCastStrict<WeakPtrToMenuModelAsNSObject>(
|
||||
menuItem.representedObject)
|
||||
.menuModel;
|
||||
}
|
||||
|
||||
- (instancetype)initWithModel:(ui::MenuModel*)model {
|
||||
@ -114,13 +116,10 @@ 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();
|
||||
}
|
||||
@ -129,18 +128,11 @@ 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
|
||||
useWithPopUpButtonCell:(BOOL)useWithCell {
|
||||
delegate:(id<MenuControllerCocoaDelegate>)delegate {
|
||||
if ((self = [super init])) {
|
||||
_model = model->AsWeakPtr();
|
||||
_delegate = delegate;
|
||||
_useWithPopUpButtonCell = useWithCell;
|
||||
[self maybeBuild];
|
||||
}
|
||||
return self;
|
||||
@ -235,9 +227,7 @@ 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!
|
||||
// Menus constructed for context use have useWithPopUpButtonCell_ set to NO.
|
||||
if (_useWithPopUpButtonCell ||
|
||||
model->GetForceShowAcceleratorForItemAt(index)) {
|
||||
if (model->GetForceShowAcceleratorForItemAt(index)) {
|
||||
ui::Accelerator accelerator;
|
||||
if (model->GetAcceleratorAt(index, &accelerator)) {
|
||||
KeyEquivalentAndModifierMask* equivalent =
|
||||
@ -264,7 +254,7 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
|
||||
}
|
||||
|
||||
ui::MenuModel* model =
|
||||
[WeakPtrToMenuModelAsNSObject getFrom:menuItem.representedObject];
|
||||
[WeakPtrToMenuModelAsNSObject menuModelForNSMenuItem:menuItem];
|
||||
if (!model) {
|
||||
return NO;
|
||||
}
|
||||
@ -299,7 +289,7 @@ bool MenuHasVisibleItems(const ui::MenuModel* model) {
|
||||
NSMenuItem* menuItem = base::apple::ObjCCastStrict<NSMenuItem>(sender);
|
||||
|
||||
ui::MenuModel* model =
|
||||
[WeakPtrToMenuModelAsNSObject getFrom:menuItem.representedObject];
|
||||
[WeakPtrToMenuModelAsNSObject menuModelForNSMenuItem:menuItem];
|
||||
DCHECK(model);
|
||||
const size_t modelIndex = base::checked_cast<size_t>(menuItem.tag);
|
||||
const ui::ElementIdentifier identifier =
|
||||
@ -329,17 +319,6 @@ 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,8 +171,7 @@ class OwningDelegate : public Delegate {
|
||||
: did_delete_(did_delete), model_(this) {
|
||||
model_.AddItem(1, u"foo");
|
||||
controller_ = [[WatchedLifetimeMenuController alloc] initWithModel:&model_
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate:nil];
|
||||
[controller_ setDeallocCalled:did_dealloc];
|
||||
}
|
||||
|
||||
@ -227,9 +226,8 @@ TEST_F(MenuControllerTest, EmptyMenu) {
|
||||
Delegate delegate;
|
||||
SimpleMenuModel model(&delegate);
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(0, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
EXPECT_EQ(0, menu.menu.numberOfItems);
|
||||
}
|
||||
|
||||
TEST_F(MenuControllerTest, BasicCreation) {
|
||||
@ -243,18 +241,17 @@ TEST_F(MenuControllerTest, BasicCreation) {
|
||||
model.AddItem(5, u"five");
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(6, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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] isSeparatorItem]);
|
||||
EXPECT_TRUE([menu.menu itemAtIndex:3].separatorItem);
|
||||
}
|
||||
|
||||
TEST_F(MenuControllerTest, Submenus) {
|
||||
@ -269,30 +266,29 @@ TEST_F(MenuControllerTest, Submenus) {
|
||||
model.AddItem(6, u"three");
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(3, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
EXPECT_EQ(3, menu.menu.numberOfItems);
|
||||
|
||||
// Inspect the submenu to ensure it has correct properties.
|
||||
NSMenuItem* menuItem = [[menu menu] itemAtIndex:1];
|
||||
EXPECT_TRUE([menuItem isEnabled]);
|
||||
NSMenu* submenu = [menuItem submenu];
|
||||
NSMenuItem* menuItem = [menu.menu itemAtIndex:1];
|
||||
EXPECT_TRUE(menuItem.enabled);
|
||||
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) {
|
||||
@ -303,16 +299,15 @@ TEST_F(MenuControllerTest, EmptySubmenu) {
|
||||
model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(2, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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
|
||||
@ -330,16 +325,15 @@ TEST_F(MenuControllerTest, EmptySubmenuWhenAllChildItemsAreHidden) {
|
||||
model.AddSubMenuWithStringId(4, kTestLabelResourceId, &submodel);
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(2, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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
|
||||
@ -363,29 +357,27 @@ TEST_F(MenuControllerTest, HiddenSubmenu) {
|
||||
|
||||
// Create the controller.
|
||||
MenuControllerCocoa* menu_controller =
|
||||
[[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(2, [[menu_controller menu] numberOfItems]);
|
||||
delegate.menu_to_close_ = [menu_controller menu];
|
||||
[[MenuControllerCocoa alloc] initWithModel:&model delegate:nil];
|
||||
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 isMenuOpen]);
|
||||
// Ensure that the submenu is hidden.
|
||||
NSMenuItem* item = [[menu_controller menu] itemAtIndex:1];
|
||||
EXPECT_TRUE([item isHidden]);
|
||||
}];
|
||||
[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);
|
||||
}];
|
||||
|
||||
// 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 isMenuOpen]);
|
||||
EXPECT_FALSE(menu_controller.menuOpen);
|
||||
|
||||
// Pump the task that notifies the delegate.
|
||||
base::RunLoop().RunUntilIdle();
|
||||
@ -415,34 +407,33 @@ TEST_F(MenuControllerTest, DisabledSubmenu) {
|
||||
|
||||
// Create the controller.
|
||||
MenuControllerCocoa* menu_controller =
|
||||
[[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate.menu_to_close_ = [menu_controller menu];
|
||||
[[MenuControllerCocoa alloc] initWithModel:&model delegate:nil];
|
||||
delegate.menu_to_close_ = menu_controller.menu;
|
||||
|
||||
// Show the menu.
|
||||
[NSRunLoop.currentRunLoop
|
||||
performInModes:@[ NSEventTrackingRunLoopMode ]
|
||||
block:^{
|
||||
EXPECT_TRUE([menu_controller isMenuOpen]);
|
||||
[NSRunLoop.currentRunLoop performInModes:@[ NSEventTrackingRunLoopMode ]
|
||||
block:^{
|
||||
EXPECT_TRUE(menu_controller.menuOpen);
|
||||
|
||||
// Ensure that the disabled submenu is disabled.
|
||||
NSMenuItem* disabled_item =
|
||||
[[menu_controller menu] itemAtIndex:1];
|
||||
EXPECT_FALSE([disabled_item isEnabled]);
|
||||
// Ensure that the disabled submenu is
|
||||
// disabled.
|
||||
NSMenuItem* disabled_item =
|
||||
[menu_controller.menu itemAtIndex:1];
|
||||
EXPECT_FALSE(disabled_item.enabled);
|
||||
|
||||
// Ensure that the enabled submenu is enabled.
|
||||
NSMenuItem* enabled_item =
|
||||
[[menu_controller menu] itemAtIndex:2];
|
||||
EXPECT_TRUE([enabled_item isEnabled]);
|
||||
}];
|
||||
// Ensure that the enabled submenu is
|
||||
// enabled.
|
||||
NSMenuItem* enabled_item =
|
||||
[menu_controller.menu itemAtIndex:2];
|
||||
EXPECT_TRUE(enabled_item.enabled);
|
||||
}];
|
||||
|
||||
// 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 isMenuOpen]);
|
||||
forView:test_window().contentView];
|
||||
EXPECT_FALSE(menu_controller.menuOpen);
|
||||
|
||||
// Pump the task that notifies the delegate.
|
||||
base::RunLoop().RunUntilIdle();
|
||||
@ -450,52 +441,30 @@ 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
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(1, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -510,11 +479,10 @@ TEST_F(MenuControllerTest, Validate) {
|
||||
model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(3, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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.
|
||||
@ -529,34 +497,13 @@ TEST_F(MenuControllerTest, LabelFontList) {
|
||||
model.AddItem(2, u"two");
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(2, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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);
|
||||
}
|
||||
|
||||
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]);
|
||||
EXPECT_TRUE([menu.menu itemAtIndex:0].attributedTitle != nil);
|
||||
EXPECT_TRUE([menu.menu itemAtIndex:1].attributedTitle == nil);
|
||||
}
|
||||
|
||||
// Test that menus with dynamic labels actually get updated.
|
||||
@ -570,16 +517,15 @@ TEST_F(MenuControllerTest, Dynamic) {
|
||||
SimpleMenuModel model(&delegate);
|
||||
model.AddItem(1, u"foo");
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
EXPECT_EQ(1, [[menu menu] numberOfItems]);
|
||||
delegate:nil];
|
||||
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";
|
||||
@ -587,15 +533,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_TRUE([item image] != nil);
|
||||
Validate(menu, menu.menu);
|
||||
EXPECT_EQ(second, base::SysNSStringToUTF16(item.title));
|
||||
EXPECT_NE(nil, item.image);
|
||||
|
||||
// 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) {
|
||||
@ -612,26 +558,25 @@ TEST_F(MenuControllerTest, OpenClose) {
|
||||
|
||||
// Create the controller.
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate.menu_to_close_ = [menu menu];
|
||||
delegate:nil];
|
||||
delegate.menu_to_close_ = menu.menu;
|
||||
|
||||
EXPECT_FALSE([menu isMenuOpen]);
|
||||
EXPECT_FALSE(menu.menuOpen);
|
||||
|
||||
// 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 isMenuOpen]);
|
||||
EXPECT_TRUE(menu.menuOpen);
|
||||
}];
|
||||
|
||||
// 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 isMenuOpen]);
|
||||
EXPECT_FALSE(menu.menuOpen);
|
||||
|
||||
// When control returns back to here, the menu will have finished running its
|
||||
// loop and will have closed itself (see Delegate::OnMenuWillShow).
|
||||
@ -669,13 +614,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);
|
||||
@ -688,7 +633,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);
|
||||
@ -705,8 +650,7 @@ TEST_F(MenuControllerTest, InitBuildsMenu) {
|
||||
model.AddItem(3, u"three");
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:YES];
|
||||
delegate:nil];
|
||||
EXPECT_TRUE([menu isMenuBuiltForTesting]);
|
||||
}
|
||||
|
||||
@ -720,11 +664,10 @@ TEST_F(MenuControllerTest, Ampersands) {
|
||||
model.SetMayHaveMnemonicsAt(1, false);
|
||||
|
||||
MenuControllerCocoa* menu = [[MenuControllerCocoa alloc] initWithModel:&model
|
||||
delegate:nil
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate:nil];
|
||||
|
||||
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,8 +81,7 @@ void MenuRunnerImplCocoa::RunMenuAt(
|
||||
menu_delegate_ = [[MenuControllerCocoaDelegateImpl alloc]
|
||||
initWithParams:MenuControllerParamsForWidget(parent)];
|
||||
menu_controller_ = [[MenuControllerCocoa alloc] initWithModel:menu_model_
|
||||
delegate:menu_delegate_
|
||||
useWithPopUpButtonCell:NO];
|
||||
delegate:menu_delegate_];
|
||||
|
||||
closing_event_time_ = base::TimeTicks();
|
||||
running_ = true;
|
||||
|
Reference in New Issue
Block a user