blob: 6468258248c373a8cf490f751fa51380c1f7b380 [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"
#import "ui/base/cocoa/menu_controller.h"
#include "ui/base/models/menu_model.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 {
// The menu run types that should show a native NSMenu rather than a toolkit-
// views menu. Only supported when the menu is backed by a ui::MenuModel.
const int kNativeRunTypes = MenuRunner::CONTEXT_MENU | MenuRunner::COMBOBOX;
const CGFloat kNativeCheckmarkWidth = 18;
const CGFloat kNativeMenuItemHeight = 18;
// Returns the first item in |menu_controller|'s menu that will be checked.
NSMenuItem* FirstCheckedItem(MenuController* 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) {
NSRect rect = gfx::ScreenRectToNSRect(screen_bounds);
rect.origin = [window convertScreenToBase:rect.origin];
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;
}
}
// 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;
}
} // namespace
// static
MenuRunnerImplInterface* MenuRunnerImplInterface::Create(
ui::MenuModel* menu_model,
int32 run_types) {
if ((run_types & kNativeRunTypes) != 0 &&
(run_types & MenuRunner::IS_NESTED) == 0) {
return new MenuRunnerImplCocoa(menu_model);
}
return new MenuRunnerImplAdapter(menu_model);
}
MenuRunnerImplCocoa::MenuRunnerImplCocoa(ui::MenuModel* menu)
: delete_after_run_(false), closing_event_time_(base::TimeDelta()) {
menu_controller_.reset(
[[MenuController alloc] initWithModel:menu useWithPopUpButtonCell:NO]);
}
bool MenuRunnerImplCocoa::IsRunning() const {
return [menu_controller_ isMenuOpen];
}
void MenuRunnerImplCocoa::Release() {
if (IsRunning()) {
if (delete_after_run_)
return; // We already canceled.
delete_after_run_ = true;
[menu_controller_ cancel];
} else {
delete this;
}
}
MenuRunner::RunResult MenuRunnerImplCocoa::RunMenuAt(Widget* parent,
MenuButton* button,
const gfx::Rect& bounds,
MenuAnchorPosition anchor,
int32 run_types) {
DCHECK(run_types & kNativeRunTypes);
DCHECK(!IsRunning());
DCHECK(parent);
closing_event_time_ = base::TimeDelta();
if (run_types & MenuRunner::CONTEXT_MENU) {
[NSMenu popUpContextMenu:[menu_controller_ menu]
withEvent:[NSApp currentEvent]
forView:parent->GetNativeView()];
} else if (run_types & MenuRunner::COMBOBOX) {
NSMenuItem* checked_item = FirstCheckedItem(menu_controller_);
base::scoped_nsobject<NSView> anchor_view(
CreateMenuAnchorView(parent->GetNativeWindow(), bounds, checked_item));
NSMenu* menu = [menu_controller_ menu];
[menu setMinimumWidth:bounds.width() + kNativeCheckmarkWidth];
[menu popUpMenuPositioningItem:checked_item
atLocation:NSZeroPoint
inView:anchor_view];
[anchor_view removeFromSuperview];
} else {
NOTREACHED();
}
closing_event_time_ = ui::EventTimeForNow();
if (delete_after_run_) {
delete this;
return MenuRunner::MENU_DELETED;
}
return MenuRunner::NORMAL_EXIT;
}
void MenuRunnerImplCocoa::Cancel() {
[menu_controller_ cancel];
}
base::TimeDelta MenuRunnerImplCocoa::GetClosingEventTime() const {
return closing_event_time_;
}
MenuRunnerImplCocoa::~MenuRunnerImplCocoa() {
}
} // namespace internal
} // namespace views