// 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 {

// 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(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) {
  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;
    }
  }

  // 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 & kNativeRunTypes) != 0 &&
      (run_types & MenuRunner::IS_NESTED) == 0) {
    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;
    [menu_controller_ cancel];
  } else {
    delete this;
  }
}

void MenuRunnerImplCocoa::RunMenuAt(Widget* parent,
                                    MenuButton* button,
                                    const gfx::Rect& bounds,
                                    MenuAnchorPosition anchor,
                                    int32_t run_types) {
  DCHECK(run_types & kNativeRunTypes);
  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_);
    base::scoped_nsobject<NSView> anchor_view(
        CreateMenuAnchorView(window, 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();
  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
