1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "ui/views/controls/menu/menu_runner_impl_cocoa.h"
7 #import "ui/base/cocoa/menu_controller.h"
8 #include "ui/base/models/menu_model.h"
9 #include "ui/events/event_utils.h"
10 #include "ui/gfx/geometry/rect.h"
11 #include "ui/gfx/mac/coordinate_conversion.h"
12 #include "ui/views/controls/menu/menu_runner_impl_adapter.h"
13 #include "ui/views/widget/widget.h"
19 // The menu run types that should show a native NSMenu rather than a toolkit-
20 // views menu. Only supported when the menu is backed by a ui::MenuModel.
21 const int kNativeRunTypes = MenuRunner::CONTEXT_MENU | MenuRunner::COMBOBOX;
23 const CGFloat kNativeCheckmarkWidth = 18;
24 const CGFloat kNativeMenuItemHeight = 18;
26 // Returns the first item in |menu_controller|'s menu that will be checked.
27 NSMenuItem* FirstCheckedItem(MenuController* menu_controller) {
28 for (NSMenuItem* item in [[menu_controller menu] itemArray]) {
29 if ([menu_controller model]->IsItemCheckedAt([item tag]))
35 // Places a temporary, hidden NSView at |screen_bounds| within |window|. Used
36 // with -[NSMenu popUpMenuPositioningItem:atLocation:inView:] to position the
37 // menu for a combobox. The caller must remove the returned NSView from its
38 // superview when the menu is closed.
39 base::scoped_nsobject<NSView> CreateMenuAnchorView(
41 const gfx::Rect& screen_bounds,
42 NSMenuItem* checked_item) {
43 NSRect rect = gfx::ScreenRectToNSRect(screen_bounds);
44 rect.origin = [window convertScreenToBase:rect.origin];
45 rect = [[window contentView] convertRect:rect fromView:nil];
47 // If there's no checked item (e.g. Combobox::STYLE_ACTION), NSMenu will
48 // anchor at the top left of the frame. Action buttons should anchor below.
51 if (base::i18n::IsRTL())
52 rect.origin.x += rect.size.width;
54 // To ensure a consistent anchoring that's vertically centered in the
55 // bounds, fix the height to be the same as a menu item.
56 rect.origin.y = NSMidY(rect) - kNativeMenuItemHeight / 2;
57 rect.size.height = kNativeMenuItemHeight;
58 if (base::i18n::IsRTL()) {
59 // The Views menu controller flips the MenuAnchorPosition value from left
60 // to right in RTL. NSMenu does this automatically: the menu opens to the
61 // left of the anchor, but AppKit doesn't account for the anchor width.
62 // So the width needs to be added to anchor at the right of the view.
63 // Note the checkmark width is not also added - it doesn't quite line up
64 // the text. A Yosemite NSComboBox doesn't line up in RTL either: just
65 // adding the width is a good match for the native behavior.
66 rect.origin.x += rect.size.width;
68 rect.origin.x -= kNativeCheckmarkWidth;
72 // A plain NSView will anchor below rather than "over", so use an NSButton.
73 base::scoped_nsobject<NSView> anchor_view(
74 [[NSButton alloc] initWithFrame:rect]);
75 [anchor_view setHidden:YES];
76 [[window contentView] addSubview:anchor_view];
83 MenuRunnerImplInterface* MenuRunnerImplInterface::Create(
84 ui::MenuModel* menu_model,
86 if ((run_types & kNativeRunTypes) != 0 &&
87 (run_types & MenuRunner::IS_NESTED) == 0) {
88 return new MenuRunnerImplCocoa(menu_model);
91 return new MenuRunnerImplAdapter(menu_model);
94 MenuRunnerImplCocoa::MenuRunnerImplCocoa(ui::MenuModel* menu)
95 : delete_after_run_(false), closing_event_time_(base::TimeDelta()) {
96 menu_controller_.reset(
97 [[MenuController alloc] initWithModel:menu useWithPopUpButtonCell:NO]);
100 bool MenuRunnerImplCocoa::IsRunning() const {
101 return [menu_controller_ isMenuOpen];
104 void MenuRunnerImplCocoa::Release() {
106 if (delete_after_run_)
107 return; // We already canceled.
109 delete_after_run_ = true;
110 [menu_controller_ cancel];
116 MenuRunner::RunResult MenuRunnerImplCocoa::RunMenuAt(Widget* parent,
118 const gfx::Rect& bounds,
119 MenuAnchorPosition anchor,
121 DCHECK(run_types & kNativeRunTypes);
122 DCHECK(!IsRunning());
123 closing_event_time_ = base::TimeDelta();
125 if (run_types & MenuRunner::CONTEXT_MENU) {
126 [NSMenu popUpContextMenu:[menu_controller_ menu]
127 withEvent:[NSApp currentEvent]
129 } else if (run_types & MenuRunner::COMBOBOX) {
131 NSMenuItem* checked_item = FirstCheckedItem(menu_controller_);
132 base::scoped_nsobject<NSView> anchor_view(
133 CreateMenuAnchorView(parent->GetNativeWindow(), bounds, checked_item));
134 NSMenu* menu = [menu_controller_ menu];
135 [menu setMinimumWidth:bounds.width() + kNativeCheckmarkWidth];
136 [menu popUpMenuPositioningItem:checked_item
137 atLocation:NSZeroPoint
139 [anchor_view removeFromSuperview];
144 closing_event_time_ = ui::EventTimeForNow();
146 if (delete_after_run_) {
148 return MenuRunner::MENU_DELETED;
151 return MenuRunner::NORMAL_EXIT;
154 void MenuRunnerImplCocoa::Cancel() {
155 [menu_controller_ cancel];
158 base::TimeDelta MenuRunnerImplCocoa::GetClosingEventTime() const {
159 return closing_event_time_;
162 MenuRunnerImplCocoa::~MenuRunnerImplCocoa() {
165 } // namespace internal