blob: e98dda7f8534dfa33fee469c601674315d8b0866 [file] [log] [blame]
// Copyright (c) 2012 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 "ui/views/controls/button/menu_button.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/event_utils.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/text_constants.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/menu_button_listener.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/resources/grit/views_resources.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"
using base::TimeTicks;
using base::TimeDelta;
namespace views {
// Default menu offset.
static const int kDefaultMenuOffsetX = -2;
static const int kDefaultMenuOffsetY = -4;
// static
const char MenuButton::kViewClassName[] = "MenuButton";
const int MenuButton::kMenuMarkerPaddingLeft = 3;
const int MenuButton::kMenuMarkerPaddingRight = -1;
////////////////////////////////////////////////////////////////////////////////
//
// MenuButton::PressedLock
//
////////////////////////////////////////////////////////////////////////////////
MenuButton::PressedLock::PressedLock(MenuButton* menu_button)
: PressedLock(menu_button, false) {}
MenuButton::PressedLock::PressedLock(MenuButton* menu_button,
bool is_sibling_menu_show)
: menu_button_(menu_button->weak_factory_.GetWeakPtr()) {
menu_button_->IncrementPressedLocked(is_sibling_menu_show);
}
MenuButton::PressedLock::~PressedLock() {
if (menu_button_.get())
menu_button_->DecrementPressedLocked();
}
////////////////////////////////////////////////////////////////////////////////
//
// MenuButton - constructors, destructors, initialization
//
////////////////////////////////////////////////////////////////////////////////
MenuButton::MenuButton(const base::string16& text,
MenuButtonListener* menu_button_listener,
bool show_menu_marker)
: LabelButton(nullptr, text),
menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY),
listener_(menu_button_listener),
show_menu_marker_(show_menu_marker),
menu_marker_(ui::ResourceBundle::GetSharedInstance()
.GetImageNamed(IDR_MENU_DROPARROW)
.ToImageSkia()),
destroyed_flag_(nullptr),
pressed_lock_count_(0),
increment_pressed_lock_called_(nullptr),
should_disable_after_press_(false),
weak_factory_(this) {
SetHorizontalAlignment(gfx::ALIGN_LEFT);
}
MenuButton::~MenuButton() {
if (destroyed_flag_)
*destroyed_flag_ = true;
}
////////////////////////////////////////////////////////////////////////////////
//
// MenuButton - Public APIs
//
////////////////////////////////////////////////////////////////////////////////
bool MenuButton::Activate(const ui::Event* event) {
if (listener_) {
gfx::Rect lb = GetLocalBounds();
// The position of the menu depends on whether or not the locale is
// right-to-left.
gfx::Point menu_position(lb.right(), lb.bottom());
if (base::i18n::IsRTL())
menu_position.set_x(lb.x());
View::ConvertPointToScreen(this, &menu_position);
if (base::i18n::IsRTL())
menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
else
menu_position.Offset(menu_offset_.x(), menu_offset_.y());
int max_x_coordinate = GetMaximumScreenXCoordinate();
if (max_x_coordinate && max_x_coordinate <= menu_position.x())
menu_position.set_x(max_x_coordinate - 1);
// We're about to show the menu from a mouse press. By showing from the
// mouse press event we block RootView in mouse dispatching. This also
// appears to cause RootView to get a mouse pressed BEFORE the mouse
// release is seen, which means RootView sends us another mouse press no
// matter where the user pressed. To force RootView to recalculate the
// mouse target during the mouse press we explicitly set the mouse handler
// to NULL.
static_cast<internal::RootView*>(GetWidget()->GetRootView())
->SetMouseHandler(nullptr);
bool destroyed = false;
destroyed_flag_ = &destroyed;
DCHECK(increment_pressed_lock_called_ == nullptr);
// Observe if IncrementPressedLocked() was called so we can trigger the
// correct ink drop animations.
bool increment_pressed_lock_called = false;
increment_pressed_lock_called_ = &increment_pressed_lock_called;
// We don't set our state here. It's handled in the MenuController code or
// by our click listener.
listener_->OnMenuButtonClicked(this, menu_position, event);
if (destroyed) {
// The menu was deleted while showing. Don't attempt any processing.
return false;
}
increment_pressed_lock_called_ = nullptr;
destroyed_flag_ = nullptr;
if (!increment_pressed_lock_called && pressed_lock_count_ == 0) {
AnimateInkDrop(InkDropState::ACTION_TRIGGERED,
ui::LocatedEvent::FromIfValid(event));
}
// We must return false here so that the RootView does not get stuck
// sending all mouse pressed events to us instead of the appropriate
// target.
return false;
}
AnimateInkDrop(InkDropState::HIDDEN, ui::LocatedEvent::FromIfValid(event));
return true;
}
bool MenuButton::IsTriggerableEventType(const ui::Event& event) {
if (event.IsMouseEvent()) {
const ui::MouseEvent& mouseev = static_cast<const ui::MouseEvent&>(event);
// Active on left mouse button only, to prevent a menu from being activated
// when a right-click would also activate a context menu.
if (!mouseev.IsOnlyLeftMouseButton())
return false;
// If dragging is supported activate on release, otherwise activate on
// pressed.
ui::EventType active_on =
GetDragOperations(mouseev.location()) == ui::DragDropTypes::DRAG_NONE
? ui::ET_MOUSE_PRESSED
: ui::ET_MOUSE_RELEASED;
return event.type() == active_on;
}
return event.type() == ui::ET_GESTURE_TAP;
}
void MenuButton::OnPaint(gfx::Canvas* canvas) {
LabelButton::OnPaint(canvas);
if (show_menu_marker_)
PaintMenuMarker(canvas);
}
////////////////////////////////////////////////////////////////////////////////
//
// MenuButton - Events
//
////////////////////////////////////////////////////////////////////////////////
gfx::Size MenuButton::GetPreferredSize() const {
gfx::Size prefsize = LabelButton::GetPreferredSize();
if (show_menu_marker_) {
prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft +
kMenuMarkerPaddingRight,
0);
}
return prefsize;
}
const char* MenuButton::GetClassName() const {
return kViewClassName;
}
bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
if (request_focus_on_press())
RequestFocus();
if (state() != STATE_DISABLED && HitTestPoint(event.location()) &&
IsTriggerableEventType(event)) {
if (IsTriggerableEvent(event))
return Activate(&event);
}
return true;
}
void MenuButton::OnMouseReleased(const ui::MouseEvent& event) {
if (state() != STATE_DISABLED && IsTriggerableEvent(event) &&
HitTestPoint(event.location()) && !InDrag()) {
Activate(&event);
} else {
AnimateInkDrop(InkDropState::HIDDEN, &event);
LabelButton::OnMouseReleased(event);
}
}
void MenuButton::OnMouseEntered(const ui::MouseEvent& event) {
if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
LabelButton::OnMouseEntered(event);
}
void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
LabelButton::OnMouseExited(event);
}
void MenuButton::OnMouseMoved(const ui::MouseEvent& event) {
if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
LabelButton::OnMouseMoved(event);
}
void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
if (state() != STATE_DISABLED) {
if (IsTriggerableEvent(*event) && !Activate(event)) {
// When |Activate()| returns |false|, it means the click was handled by
// a button listener and has handled the gesture event. So, there is no
// need to further process the gesture event here. However, if the
// listener didn't run menu code, we should make sure to reset our state.
if (state() == Button::STATE_HOVERED)
SetState(Button::STATE_NORMAL);
return;
}
if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
event->SetHandled();
if (pressed_lock_count_ == 0)
SetState(Button::STATE_HOVERED);
} else if (state() == Button::STATE_HOVERED &&
(event->type() == ui::ET_GESTURE_TAP_CANCEL ||
event->type() == ui::ET_GESTURE_END) &&
pressed_lock_count_ == 0) {
SetState(Button::STATE_NORMAL);
}
}
LabelButton::OnGestureEvent(event);
}
bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
switch (event.key_code()) {
case ui::VKEY_SPACE:
// Alt-space on windows should show the window menu.
if (event.IsAltDown())
break;
case ui::VKEY_RETURN:
case ui::VKEY_UP:
case ui::VKEY_DOWN: {
// WARNING: we may have been deleted by the time Activate returns.
Activate(&event);
// This is to prevent the keyboard event from being dispatched twice. If
// the keyboard event is not handled, we pass it to the default handler
// which dispatches the event back to us causing the menu to get displayed
// again. Return true to prevent this.
return true;
}
default:
break;
}
return false;
}
bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) {
// Override CustomButton's implementation, which presses the button when
// you press space and clicks it when you release space. For a MenuButton
// we always activate the menu on key press.
return false;
}
void MenuButton::GetAccessibleNodeData(ui::AXNodeData* node_data) {
CustomButton::GetAccessibleNodeData(node_data);
node_data->role = ui::AX_ROLE_POP_UP_BUTTON;
node_data->AddStringAttribute(
ui::AX_ATTR_ACTION, l10n_util::GetStringUTF8(IDS_APP_ACCACTION_PRESS));
node_data->AddStateFlag(ui::AX_STATE_HASPOPUP);
}
void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
gfx::Insets insets = GetInsets();
// Using the Views mirroring infrastructure incorrectly flips icon content.
// Instead, manually mirror the position of the down arrow.
gfx::Rect arrow_bounds(width() - insets.right() -
menu_marker_->width() - kMenuMarkerPaddingRight,
height() / 2 - menu_marker_->height() / 2,
menu_marker_->width(),
menu_marker_->height());
arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
}
gfx::Rect MenuButton::GetChildAreaBounds() {
gfx::Size s = size();
if (show_menu_marker_) {
s.set_width(s.width() - menu_marker_->width() - kMenuMarkerPaddingLeft -
kMenuMarkerPaddingRight);
}
return gfx::Rect(s);
}
bool MenuButton::IsTriggerableEvent(const ui::Event& event) {
if (!IsTriggerableEventType(event))
return false;
TimeDelta delta = TimeTicks::Now() - menu_closed_time_;
if (delta.InMilliseconds() < kMinimumMsBetweenButtonClicks)
return false; // Not enough time since the menu closed.
return true;
}
bool MenuButton::ShouldEnterPushedState(const ui::Event& event) {
return IsTriggerableEventType(event);
}
void MenuButton::StateChanged() {
if (pressed_lock_count_ != 0) {
// The button's state was changed while it was supposed to be locked in a
// pressed state. This shouldn't happen, but conceivably could if a caller
// tries to switch from enabled to disabled or vice versa while the button
// is pressed.
if (state() == STATE_NORMAL)
should_disable_after_press_ = false;
else if (state() == STATE_DISABLED)
should_disable_after_press_ = true;
} else {
LabelButton::StateChanged();
}
}
void MenuButton::NotifyClick(const ui::Event& event) {
// We don't forward events to the normal button listener, instead using the
// MenuButtonListener.
Activate(&event);
}
void MenuButton::IncrementPressedLocked(bool snap_ink_drop_to_activated) {
++pressed_lock_count_;
if (increment_pressed_lock_called_)
*increment_pressed_lock_called_ = true;
should_disable_after_press_ = state() == STATE_DISABLED;
if (state() != STATE_PRESSED) {
if (snap_ink_drop_to_activated)
GetInkDrop()->SnapToActivated();
else
AnimateInkDrop(InkDropState::ACTIVATED, nullptr /* event */);
}
SetState(STATE_PRESSED);
}
void MenuButton::DecrementPressedLocked() {
--pressed_lock_count_;
DCHECK_GE(pressed_lock_count_, 0);
// If this was the last lock, manually reset state to the desired state.
if (pressed_lock_count_ == 0) {
menu_closed_time_ = TimeTicks::Now();
ButtonState desired_state = STATE_NORMAL;
if (should_disable_after_press_) {
desired_state = STATE_DISABLED;
should_disable_after_press_ = false;
} else if (ShouldEnterHoveredState()) {
desired_state = STATE_HOVERED;
}
SetState(desired_state);
// The widget may be null during shutdown. If so, it doesn't make sense to
// try to add an ink drop effect.
if (GetWidget() && state() != STATE_PRESSED)
AnimateInkDrop(InkDropState::DEACTIVATED, nullptr /* event */);
}
}
int MenuButton::GetMaximumScreenXCoordinate() {
if (!GetWidget()) {
NOTREACHED();
return 0;
}
gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
return monitor_bounds.right() - 1;
}
} // namespace views