Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / views / controls / menu / menu_runner_impl_cocoa.mm
blobed13900135bf81f23f540ee2a2a138461036da0c
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"
15 namespace views {
16 namespace internal {
17 namespace {
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]))
30       return item;
31   }
32   return nil;
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(
40     NSWindow* window,
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.
49   if (!checked_item) {
50     rect.size.height = 0;
51     if (base::i18n::IsRTL())
52       rect.origin.x += rect.size.width;
53   } else {
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;
67     } else {
68       rect.origin.x -= kNativeCheckmarkWidth;
69     }
70   }
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];
77   return anchor_view;
80 }  // namespace
82 // static
83 MenuRunnerImplInterface* MenuRunnerImplInterface::Create(
84     ui::MenuModel* menu_model,
85     int32 run_types) {
86   if ((run_types & kNativeRunTypes) != 0 &&
87       (run_types & MenuRunner::IS_NESTED) == 0) {
88     return new MenuRunnerImplCocoa(menu_model);
89   }
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() {
105   if (IsRunning()) {
106     if (delete_after_run_)
107       return;  // We already canceled.
109     delete_after_run_ = true;
110     [menu_controller_ cancel];
111   } else {
112     delete this;
113   }
116 MenuRunner::RunResult MenuRunnerImplCocoa::RunMenuAt(Widget* parent,
117                                                      MenuButton* button,
118                                                      const gfx::Rect& bounds,
119                                                      MenuAnchorPosition anchor,
120                                                      int32 run_types) {
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]
128                      forView:nil];
129   } else if (run_types & MenuRunner::COMBOBOX) {
130     DCHECK(parent);
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
138                             inView:anchor_view];
139     [anchor_view removeFromSuperview];
140   } else {
141     NOTREACHED();
142   }
144   closing_event_time_ = ui::EventTimeForNow();
146   if (delete_after_run_) {
147     delete this;
148     return MenuRunner::MENU_DELETED;
149   }
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
166 }  // namespace views