[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 commit1786625b69
. DISABLE_OBJECTIVE_C_STYLE (cherry picked from commit57c0d866fc
) 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:

committed by
Chromium LUCI CQ

parent
e433da7b94
commit
0e64a6b1a4
chrome/browser
apps
guest_view
ui
cocoa
components/remote_cocoa/app_shim
content
app_shim_remote_cocoa
public
ui
base
interaction
menus
views
controls
@ -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;
|
||||
|
Reference in New Issue
Block a user