blob: f0ea47108decb138f17be04b0ff54e7dbac71e0a [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ui/views/controls/menu/menu_runner_impl_cocoa.h"
#include "base/mac/sdk_forward_declarations.h"
#import "base/message_loop/message_pump_mac.h"
#import "ui/base/cocoa/cocoa_base_utils.h"
#import "ui/base/cocoa/menu_controller.h"
#include "ui/base/models/menu_model.h"
#include "ui/events/base_event_utils.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/mac/coordinate_conversion.h"
#include "ui/views/controls/menu/menu_runner_impl_adapter.h"
#include "ui/views/widget/widget.h"
namespace views {
namespace internal {
namespace {
const CGFloat kNativeCheckmarkWidth = 18;
const CGFloat kNativeMenuItemHeight = 18;
// Returns the first item in |menu_controller|'s menu that will be checked.
NSMenuItem* FirstCheckedItem(MenuControllerCocoa* menu_controller) {
for (NSMenuItem* item in [[menu_controller menu] itemArray]) {
if ([menu_controller model]->IsItemCheckedAt([item tag]))
return item;
}
return nil;
}
// Places a temporary, hidden NSView at |screen_bounds| within |window|. Used
// with -[NSMenu popUpMenuPositioningItem:atLocation:inView:] to position the
// menu for a combobox. The caller must remove the returned NSView from its
// superview when the menu is closed.
base::scoped_nsobject<NSView> CreateMenuAnchorView(
NSWindow* window,
const gfx::Rect& screen_bounds,
NSMenuItem* checked_item,
CGFloat actual_menu_width,
MenuAnchorPosition position) {
NSRect rect = gfx::ScreenRectToNSRect(screen_bounds);
rect = [window convertRectFromScreen:rect];
rect = [[window contentView] convertRect:rect fromView:nil];
// If there's no checked item (e.g. Combobox::STYLE_ACTION), NSMenu will
// anchor at the top left of the frame. Action buttons should anchor below.
if (!checked_item) {
rect.size.height = 0;
if (base::i18n::IsRTL())
rect.origin.x += rect.size.width;
} else {
// To ensure a consistent anchoring that's vertically centered in the
// bounds, fix the height to be the same as a menu item.
rect.origin.y = NSMidY(rect) - kNativeMenuItemHeight / 2;
rect.size.height = kNativeMenuItemHeight;
if (base::i18n::IsRTL()) {
// The Views menu controller flips the MenuAnchorPosition value from left
// to right in RTL. NSMenu does this automatically: the menu opens to the
// left of the anchor, but AppKit doesn't account for the anchor width.
// So the width needs to be added to anchor at the right of the view.
// Note the checkmark width is not also added - it doesn't quite line up
// the text. A Yosemite NSComboBox doesn't line up in RTL either: just
// adding the width is a good match for the native behavior.
rect.origin.x += rect.size.width;
} else {
rect.origin.x -= kNativeCheckmarkWidth;
}
}
// When the actual menu width is larger than the anchor, right alignment
// should be respected.
if (actual_menu_width > rect.size.width &&
position == views::MENU_ANCHOR_TOPRIGHT && !base::i18n::IsRTL()) {
int width_diff = actual_menu_width - rect.size.width;
rect.origin.x -= width_diff;
}
// A plain NSView will anchor below rather than "over", so use an NSButton.
base::scoped_nsobject<NSView> anchor_view(
[[NSButton alloc] initWithFrame:rect]);
[anchor_view setHidden:YES];
[[window contentView] addSubview:anchor_view];
return anchor_view;
}
// Returns an appropriate event (with a location) suitable for showing a context
// menu. Uses [NSApp currentEvent] if it's a non-nil mouse click event,
// otherwise creates an autoreleased dummy event located at |anchor|.
NSEvent* EventForPositioningContextMenu(const gfx::Rect& anchor,
NSWindow* window) {
NSEvent* event = [NSApp currentEvent];
switch ([event type]) {
case NSLeftMouseDown:
case NSLeftMouseUp:
case NSRightMouseDown:
case NSRightMouseUp:
case NSOtherMouseDown:
case NSOtherMouseUp:
return event;
default:
break;
}
NSPoint location_in_window = ui::ConvertPointFromScreenToWindow(
window, gfx::ScreenPointToNSPoint(anchor.CenterPoint()));
return [NSEvent mouseEventWithType:NSRightMouseDown
location:location_in_window
modifierFlags:0
timestamp:0
windowNumber:[window windowNumber]
context:nil
eventNumber:0
clickCount:1
pressure:0];
}
} // namespace
// static
MenuRunnerImplInterface* MenuRunnerImplInterface::Create(
ui::MenuModel* menu_model,
int32_t run_types,
const base::Closure& on_menu_closed_callback) {
if ((run_types & MenuRunner::CONTEXT_MENU) &&
!(run_types & MenuRunner::IS_NESTED)) {
return new MenuRunnerImplCocoa(menu_model, on_menu_closed_callback);
}
return new MenuRunnerImplAdapter(menu_model, on_menu_closed_callback);
}
MenuRunnerImplCocoa::MenuRunnerImplCocoa(
ui::MenuModel* menu,
const base::Closure& on_menu_closed_callback)
: running_(false),
delete_after_run_(false),
closing_event_time_(base::TimeTicks()),
on_menu_closed_callback_(on_menu_closed_callback) {
menu_controller_.reset([[MenuControllerCocoa alloc] initWithModel:menu
useWithPopUpButtonCell:NO]);
[menu_controller_ setPostItemSelectedAsTask:YES];
}
bool MenuRunnerImplCocoa::IsRunning() const {
return running_;
}
void MenuRunnerImplCocoa::Release() {
if (IsRunning()) {
if (delete_after_run_)
return; // We already canceled.
delete_after_run_ = true;
// Reset |menu_controller_| to ensure it clears itself as a delegate to
// prevent NSMenu attempting to access the weak pointer to the ui::MenuModel
// it holds (which is not owned by |this|). Toolkit-views menus use
// MenuRunnerImpl::empty_delegate_ to handle this case.
menu_controller_.reset();
} else {
delete this;
}
}
void MenuRunnerImplCocoa::RunMenuAt(Widget* parent,
MenuButton* button,
const gfx::Rect& bounds,
MenuAnchorPosition anchor,
int32_t run_types) {
DCHECK(!IsRunning());
DCHECK(parent);
closing_event_time_ = base::TimeTicks();
running_ = true;
// Ensure the UI can update while the menu is fading out.
base::ScopedPumpMessagesInPrivateModes pump_private;
NSWindow* window = parent->GetNativeWindow();
if (run_types & MenuRunner::CONTEXT_MENU) {
[NSMenu popUpContextMenu:[menu_controller_ menu]
withEvent:EventForPositioningContextMenu(bounds, window)
forView:parent->GetNativeView()];
} else if (run_types & MenuRunner::COMBOBOX) {
NSMenuItem* checked_item = FirstCheckedItem(menu_controller_);
NSMenu* menu = [menu_controller_ menu];
base::scoped_nsobject<NSView> anchor_view(CreateMenuAnchorView(
window, bounds, checked_item, menu.size.width, anchor));
[menu setMinimumWidth:bounds.width() + kNativeCheckmarkWidth];
[menu popUpMenuPositioningItem:checked_item
atLocation:NSZeroPoint
inView:anchor_view];
[anchor_view removeFromSuperview];
} else {
NOTREACHED();
}
closing_event_time_ = ui::EventTimeForNow();
running_ = false;
if (delete_after_run_) {
delete this;
return;
}
// Don't invoke the callback if Release() was called, since that usually means
// the owning instance is being destroyed.
if (!on_menu_closed_callback_.is_null())
on_menu_closed_callback_.Run();
}
void MenuRunnerImplCocoa::Cancel() {
[menu_controller_ cancel];
}
base::TimeTicks MenuRunnerImplCocoa::GetClosingEventTime() const {
return closing_event_time_;
}
MenuRunnerImplCocoa::~MenuRunnerImplCocoa() {}
} // namespace internal
} // namespace views