// Copyright 2013 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.

#include "chrome/browser/ui/views/toolbar/toolbar_action_view.h"

#include <string>

#include "base/auto_reset.h"
#include "base/bind.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/grit/generated_resources.h"
#include "content/public/browser/notification_source.h"
#include "grit/theme_resources.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/theme_provider.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/image/image_skia_source.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/animation/button_ink_drop_delegate.h"
#include "ui/views/controls/button/label_button_border.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_model_adapter.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/resources/grit/views_resources.h"

using views::LabelButtonBorder;

namespace {

// Toolbar action buttons have no insets because the badges are drawn right at
// the edge of the view's area. Other badding (such as centering the icon) is
// handled directly by the Image.
const int kBorderInset = 0;

// The callback to call directly before showing the context menu.
ToolbarActionView::ContextMenuCallback* context_menu_callback_for_test =
    nullptr;

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// ToolbarActionView

ToolbarActionView::ToolbarActionView(
    ToolbarActionViewController* view_controller,
    ToolbarActionView::Delegate* delegate)
    : MenuButton(base::string16(), this, false),
      view_controller_(view_controller),
      delegate_(delegate),
      called_register_command_(false),
      wants_to_run_(false),
      menu_(nullptr),
      ink_drop_delegate_(new views::ButtonInkDropDelegate(this, this)),
      weak_factory_(this) {
  set_ink_drop_delegate(ink_drop_delegate_.get());
  set_has_ink_drop_action_on_click(true);
  set_id(VIEW_ID_BROWSER_ACTION);
  view_controller_->SetDelegate(this);
  SetHorizontalAlignment(gfx::ALIGN_CENTER);
  set_drag_controller(delegate_);

  set_context_menu_controller(this);

  // If the button is within a menu, we need to make it focusable in order to
  // have it accessible via keyboard navigation, but it shouldn't request focus
  // (because that would close the menu).
  if (delegate_->ShownInsideMenu()) {
    set_request_focus_on_press(false);
    SetFocusable(true);
  }

  UpdateState();
}

ToolbarActionView::~ToolbarActionView() {
  // Avoid access to a destroyed InkDropDelegate when the |pressed_lock_| is
  // destroyed.
  set_ink_drop_delegate(nullptr);
  view_controller_->SetDelegate(nullptr);
}

void ToolbarActionView::GetAccessibleState(ui::AXViewState* state) {
  views::MenuButton::GetAccessibleState(state);
  state->role = ui::AX_ROLE_BUTTON;
}

scoped_ptr<LabelButtonBorder> ToolbarActionView::CreateDefaultBorder() const {
  scoped_ptr<LabelButtonBorder> border = LabelButton::CreateDefaultBorder();
  border->set_insets(gfx::Insets(kBorderInset, kBorderInset,
                                 kBorderInset, kBorderInset));
  return border;
}

void ToolbarActionView::OnMouseEntered(const ui::MouseEvent& event) {
  delegate_->OnMouseEnteredToolbarActionView();
  views::MenuButton::OnMouseEntered(event);
}

bool ToolbarActionView::IsTriggerableEvent(const ui::Event& event) {
  return views::MenuButton::IsTriggerableEvent(event) &&
         (base::TimeTicks::Now() - popup_closed_time_).InMilliseconds() >
             views::kMinimumMsBetweenButtonClicks;
}

SkColor ToolbarActionView::GetInkDropBaseColor() const {
  if (delegate_->ShownInsideMenu()) {
    return GetNativeTheme()->GetSystemColor(
        ui::NativeTheme::kColorId_HoverMenuItemBackgroundColor);
  }

  return GetThemeProvider()->GetColor(
      ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON);
}

bool ToolbarActionView::ShouldShowInkDropHover() const {
  return !delegate_->ShownInsideMenu() &&
         views::MenuButton::ShouldShowInkDropHover();
}

content::WebContents* ToolbarActionView::GetCurrentWebContents() const {
  return delegate_->GetCurrentWebContents();
}

void ToolbarActionView::UpdateState() {
  content::WebContents* web_contents = GetCurrentWebContents();
  if (SessionTabHelper::IdForTab(web_contents) < 0)
    return;

  if (!view_controller_->IsEnabled(web_contents) &&
      !view_controller_->DisabledClickOpensMenu()) {
      SetState(views::CustomButton::STATE_DISABLED);
  } else if (state() == views::CustomButton::STATE_DISABLED) {
    SetState(views::CustomButton::STATE_NORMAL);
  }

  wants_to_run_ = view_controller_->WantsToRun(web_contents);

  gfx::ImageSkia icon(
      view_controller_->GetIcon(web_contents,
                                GetPreferredSize()).AsImageSkia());

  if (!icon.isNull())
    SetImage(views::Button::STATE_NORMAL, icon);

  SetTooltipText(view_controller_->GetTooltip(web_contents));
  SetAccessibleName(view_controller_->GetAccessibleName(web_contents));

  Layout();  // We need to layout since we may have added an icon as a result.
  SchedulePaint();
}

void ToolbarActionView::OnMenuButtonClicked(views::MenuButton* sender,
                                            const gfx::Point& point,
                                            const ui::Event* event) {
  if (!view_controller_->IsEnabled(GetCurrentWebContents())) {
    // We should only get a button pressed event with a non-enabled action if
    // the left-click behavior should open the menu.
    DCHECK(view_controller_->DisabledClickOpensMenu());
    context_menu_controller()->ShowContextMenuForView(this, point,
                                                      ui::MENU_SOURCE_NONE);
  } else {
    view_controller_->ExecuteAction(true);
  }
}

void ToolbarActionView::OnMenuClosed() {
  menu_runner_.reset();
  menu_ = nullptr;
  view_controller_->OnContextMenuClosed();
  menu_adapter_.reset();
}

gfx::ImageSkia ToolbarActionView::GetIconForTest() {
  return GetImage(views::Button::STATE_NORMAL);
}

void ToolbarActionView::set_context_menu_callback_for_testing(
    base::Callback<void(ToolbarActionView*)>* callback) {
  context_menu_callback_for_test = callback;
}

gfx::Size ToolbarActionView::GetPreferredSize() const {
  return gfx::Size(ToolbarActionsBar::IconWidth(false),
                   ToolbarActionsBar::IconHeight());
}

bool ToolbarActionView::OnMousePressed(const ui::MouseEvent& event) {
  // views::MenuButton actions are only triggered by left mouse clicks.
  if (event.IsOnlyLeftMouseButton() && !pressed_lock_) {
    // TODO(bruthig): The ACTION_PENDING triggering logic should be in
    // MenuButton::OnPressed() however there is a bug with the pressed state
    // logic in MenuButton. See http://crbug.com/567252.
    ink_drop_delegate()->OnAction(views::InkDropState::ACTION_PENDING);
  }
  return MenuButton::OnMousePressed(event);
}

void ToolbarActionView::OnGestureEvent(ui::GestureEvent* event) {
  // While the dropdown menu is showing, the button should not handle gestures.
  if (menu_)
    event->StopPropagation();
  else
    MenuButton::OnGestureEvent(event);
}

void ToolbarActionView::OnDragDone() {
  views::MenuButton::OnDragDone();
  delegate_->OnToolbarActionViewDragDone();
}

void ToolbarActionView::ViewHierarchyChanged(
    const ViewHierarchyChangedDetails& details) {
  if (details.is_add && !called_register_command_ && GetFocusManager()) {
    view_controller_->RegisterCommand();
    called_register_command_ = true;
  }

  MenuButton::ViewHierarchyChanged(details);
}

views::View* ToolbarActionView::GetAsView() {
  return this;
}

views::FocusManager* ToolbarActionView::GetFocusManagerForAccelerator() {
  return GetFocusManager();
}

views::View* ToolbarActionView::GetReferenceViewForPopup() {
  // Browser actions in the overflow menu can still show popups, so we may need
  // a reference view other than this button's parent. If so, use the overflow
  // view.
  return visible() ? this : delegate_->GetOverflowReferenceView();
}

bool ToolbarActionView::IsMenuRunning() const {
  return menu_ != nullptr;
}

void ToolbarActionView::OnPopupShown(bool by_user) {
  // If this was through direct user action, we press the menu button.
  if (by_user) {
    // We set the state of the menu button we're using as a reference view,
    // which is either this or the overflow reference view.
    // This cast is safe because GetReferenceViewForPopup returns either |this|
    // or delegate_->GetOverflowReferenceView(), which returns a MenuButton.
    views::MenuButton* reference_view =
        static_cast<views::MenuButton*>(GetReferenceViewForPopup());
    pressed_lock_.reset(new views::MenuButton::PressedLock(reference_view));
  }
}

void ToolbarActionView::OnPopupClosed() {
  popup_closed_time_ = base::TimeTicks::Now();
  pressed_lock_.reset();  // Unpress the menu button if it was pressed.
}

void ToolbarActionView::ShowContextMenuForView(
    views::View* source,
    const gfx::Point& point,
    ui::MenuSourceType source_type) {
  if (CloseActiveMenuIfNeeded())
    return;

  // Otherwise, no other menu is showing, and we can proceed normally.
  DoShowContextMenu(source_type);
}

void ToolbarActionView::DoShowContextMenu(
    ui::MenuSourceType source_type) {
  ui::MenuModel* context_menu_model = view_controller_->GetContextMenu();
  // It's possible the action doesn't have a context menu.
  if (!context_menu_model)
    return;

  DCHECK(visible());  // We should never show a context menu for a hidden item.

  gfx::Point screen_loc;
  ConvertPointToScreen(this, &screen_loc);

  int run_types = views::MenuRunner::HAS_MNEMONICS |
                  views::MenuRunner::CONTEXT_MENU | views::MenuRunner::ASYNC;
  if (delegate_->ShownInsideMenu())
    run_types |= views::MenuRunner::IS_NESTED;

  // RunMenuAt expects a nested menu to be parented by the same widget as the
  // already visible menu, in this case the Chrome menu.
  views::Widget* parent = delegate_->ShownInsideMenu() ?
      delegate_->GetOverflowReferenceView()->GetWidget() :
      GetWidget();

  // Unretained() is safe here as ToolbarActionView will always outlive the
  // menu. Any action that would lead to the deletion of |this| first triggers
  // the closing of the menu through lost capture.
  menu_adapter_.reset(new views::MenuModelAdapter(
      context_menu_model,
      base::Bind(&ToolbarActionView::OnMenuClosed, base::Unretained(this))));
  menu_ = menu_adapter_->CreateMenu();
  menu_runner_.reset(new views::MenuRunner(menu_, run_types));

  if (context_menu_callback_for_test)
    context_menu_callback_for_test->Run(this);
  ignore_result(
      menu_runner_->RunMenuAt(parent, this, gfx::Rect(screen_loc, size()),
                              views::MENU_ANCHOR_TOPLEFT, source_type));
}

bool ToolbarActionView::CloseActiveMenuIfNeeded() {
  // If this view is shown inside another menu, there's a possibility that there
  // is another context menu showing that we have to close before we can
  // activate a different menu.
  if (delegate_->ShownInsideMenu()) {
    views::MenuController* menu_controller =
        views::MenuController::GetActiveInstance();
    // If this is shown inside a menu, then there should always be an active
    // menu controller.
    DCHECK(menu_controller);
    if (menu_controller->in_nested_run()) {
      // There is another menu showing. Close the outermost menu (since we are
      // shown in the same menu, we don't want to close the whole thing).
      menu_controller->Cancel(views::MenuController::EXIT_OUTERMOST);
      return true;
    }
  }

  return false;
}