Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ui / views / controls / button / menu_button.cc
blobdd7e489b661ddd6de86c0439f376ef024f39a441
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ui/views/controls/button/menu_button.h"
7 #include "base/strings/utf_string_conversions.h"
8 #include "ui/accessibility/ax_view_state.h"
9 #include "ui/base/dragdrop/drag_drop_types.h"
10 #include "ui/base/l10n/l10n_util.h"
11 #include "ui/base/resource/resource_bundle.h"
12 #include "ui/base/ui_base_switches_util.h"
13 #include "ui/events/event.h"
14 #include "ui/events/event_constants.h"
15 #include "ui/gfx/canvas.h"
16 #include "ui/gfx/image/image.h"
17 #include "ui/gfx/screen.h"
18 #include "ui/gfx/text_constants.h"
19 #include "ui/resources/grit/ui_resources.h"
20 #include "ui/strings/grit/ui_strings.h"
21 #include "ui/views/controls/button/button.h"
22 #include "ui/views/controls/button/menu_button_listener.h"
23 #include "ui/views/mouse_constants.h"
24 #include "ui/views/resources/grit/views_resources.h"
25 #include "ui/views/widget/root_view.h"
26 #include "ui/views/widget/widget.h"
28 using base::TimeTicks;
29 using base::TimeDelta;
31 namespace views {
33 // Default menu offset.
34 static const int kDefaultMenuOffsetX = -2;
35 static const int kDefaultMenuOffsetY = -4;
37 // static
38 const char MenuButton::kViewClassName[] = "MenuButton";
39 const int MenuButton::kMenuMarkerPaddingLeft = 3;
40 const int MenuButton::kMenuMarkerPaddingRight = -1;
42 ////////////////////////////////////////////////////////////////////////////////
44 // MenuButton::PressedLock
46 ////////////////////////////////////////////////////////////////////////////////
48 MenuButton::PressedLock::PressedLock(MenuButton* menu_button)
49 : menu_button_(menu_button->weak_factory_.GetWeakPtr()) {
50 menu_button_->IncrementPressedLocked();
53 MenuButton::PressedLock::~PressedLock() {
54 if (menu_button_.get())
55 menu_button_->DecrementPressedLocked();
58 ////////////////////////////////////////////////////////////////////////////////
60 // MenuButton - constructors, destructors, initialization
62 ////////////////////////////////////////////////////////////////////////////////
64 MenuButton::MenuButton(ButtonListener* listener,
65 const base::string16& text,
66 MenuButtonListener* menu_button_listener,
67 bool show_menu_marker)
68 : LabelButton(listener, text),
69 menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY),
70 listener_(menu_button_listener),
71 show_menu_marker_(show_menu_marker),
72 menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
73 IDR_MENU_DROPARROW).ToImageSkia()),
74 destroyed_flag_(NULL),
75 pressed_lock_count_(0),
76 should_disable_after_press_(false),
77 weak_factory_(this) {
78 SetHorizontalAlignment(gfx::ALIGN_LEFT);
81 MenuButton::~MenuButton() {
82 if (destroyed_flag_)
83 *destroyed_flag_ = true;
86 ////////////////////////////////////////////////////////////////////////////////
88 // MenuButton - Public APIs
90 ////////////////////////////////////////////////////////////////////////////////
92 bool MenuButton::Activate() {
93 SetState(STATE_PRESSED);
94 if (listener_) {
95 gfx::Rect lb = GetLocalBounds();
97 // The position of the menu depends on whether or not the locale is
98 // right-to-left.
99 gfx::Point menu_position(lb.right(), lb.bottom());
100 if (base::i18n::IsRTL())
101 menu_position.set_x(lb.x());
103 View::ConvertPointToScreen(this, &menu_position);
104 if (base::i18n::IsRTL())
105 menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
106 else
107 menu_position.Offset(menu_offset_.x(), menu_offset_.y());
109 int max_x_coordinate = GetMaximumScreenXCoordinate();
110 if (max_x_coordinate && max_x_coordinate <= menu_position.x())
111 menu_position.set_x(max_x_coordinate - 1);
113 // We're about to show the menu from a mouse press. By showing from the
114 // mouse press event we block RootView in mouse dispatching. This also
115 // appears to cause RootView to get a mouse pressed BEFORE the mouse
116 // release is seen, which means RootView sends us another mouse press no
117 // matter where the user pressed. To force RootView to recalculate the
118 // mouse target during the mouse press we explicitly set the mouse handler
119 // to NULL.
120 static_cast<internal::RootView*>(GetWidget()->GetRootView())->
121 SetMouseHandler(NULL);
123 bool destroyed = false;
124 destroyed_flag_ = &destroyed;
126 // We don't set our state here. It's handled in the MenuController code or
127 // by our click listener.
129 listener_->OnMenuButtonClicked(this, menu_position);
131 if (destroyed) {
132 // The menu was deleted while showing. Don't attempt any processing.
133 return false;
136 destroyed_flag_ = NULL;
138 menu_closed_time_ = TimeTicks::Now();
140 // We must return false here so that the RootView does not get stuck
141 // sending all mouse pressed events to us instead of the appropriate
142 // target.
143 return false;
145 return true;
148 void MenuButton::OnPaint(gfx::Canvas* canvas) {
149 LabelButton::OnPaint(canvas);
151 if (show_menu_marker_)
152 PaintMenuMarker(canvas);
155 ////////////////////////////////////////////////////////////////////////////////
157 // MenuButton - Events
159 ////////////////////////////////////////////////////////////////////////////////
161 gfx::Size MenuButton::GetPreferredSize() const {
162 gfx::Size prefsize = LabelButton::GetPreferredSize();
163 if (show_menu_marker_) {
164 prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft +
165 kMenuMarkerPaddingRight,
168 return prefsize;
171 const char* MenuButton::GetClassName() const {
172 return kViewClassName;
175 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
176 if (request_focus_on_press())
177 RequestFocus();
178 if (state() != STATE_DISABLED && ShouldEnterPushedState(event) &&
179 HitTestPoint(event.location())) {
180 TimeDelta delta = TimeTicks::Now() - menu_closed_time_;
181 if (delta.InMilliseconds() > kMinimumMsBetweenButtonClicks)
182 return Activate();
184 return true;
187 void MenuButton::OnMouseReleased(const ui::MouseEvent& event) {
188 if (state() != STATE_DISABLED && ShouldEnterPushedState(event) &&
189 HitTestPoint(event.location()) && !InDrag()) {
190 Activate();
191 } else {
192 LabelButton::OnMouseReleased(event);
196 void MenuButton::OnMouseEntered(const ui::MouseEvent& event) {
197 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
198 LabelButton::OnMouseEntered(event);
201 void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
202 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
203 LabelButton::OnMouseExited(event);
206 void MenuButton::OnMouseMoved(const ui::MouseEvent& event) {
207 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
208 LabelButton::OnMouseMoved(event);
211 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
212 if (state() != STATE_DISABLED) {
213 if (ShouldEnterPushedState(*event) && !Activate()) {
214 // When |Activate()| returns |false|, it means that a menu is shown and
215 // has handled the gesture event. So, there is no need to further process
216 // the gesture event here.
217 return;
219 if (switches::IsTouchFeedbackEnabled()) {
220 if (event->type() == ui::ET_GESTURE_TAP_DOWN) {
221 event->SetHandled();
222 SetState(Button::STATE_HOVERED);
223 } else if (state() == Button::STATE_HOVERED &&
224 (event->type() == ui::ET_GESTURE_TAP_CANCEL ||
225 event->type() == ui::ET_GESTURE_END)) {
226 SetState(Button::STATE_NORMAL);
230 LabelButton::OnGestureEvent(event);
233 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
234 switch (event.key_code()) {
235 case ui::VKEY_SPACE:
236 // Alt-space on windows should show the window menu.
237 if (event.IsAltDown())
238 break;
239 case ui::VKEY_RETURN:
240 case ui::VKEY_UP:
241 case ui::VKEY_DOWN: {
242 // WARNING: we may have been deleted by the time Activate returns.
243 Activate();
244 // This is to prevent the keyboard event from being dispatched twice. If
245 // the keyboard event is not handled, we pass it to the default handler
246 // which dispatches the event back to us causing the menu to get displayed
247 // again. Return true to prevent this.
248 return true;
250 default:
251 break;
253 return false;
256 bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) {
257 // Override CustomButton's implementation, which presses the button when
258 // you press space and clicks it when you release space. For a MenuButton
259 // we always activate the menu on key press.
260 return false;
263 void MenuButton::GetAccessibleState(ui::AXViewState* state) {
264 CustomButton::GetAccessibleState(state);
265 state->role = ui::AX_ROLE_POP_UP_BUTTON;
266 state->default_action = l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS);
267 state->AddStateFlag(ui::AX_STATE_HASPOPUP);
270 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
271 gfx::Insets insets = GetInsets();
273 // Using the Views mirroring infrastructure incorrectly flips icon content.
274 // Instead, manually mirror the position of the down arrow.
275 gfx::Rect arrow_bounds(width() - insets.right() -
276 menu_marker_->width() - kMenuMarkerPaddingRight,
277 height() / 2 - menu_marker_->height() / 2,
278 menu_marker_->width(),
279 menu_marker_->height());
280 arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
281 canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
284 gfx::Rect MenuButton::GetChildAreaBounds() {
285 gfx::Size s = size();
287 if (show_menu_marker_) {
288 s.set_width(s.width() - menu_marker_->width() - kMenuMarkerPaddingLeft -
289 kMenuMarkerPaddingRight);
292 return gfx::Rect(s);
295 bool MenuButton::ShouldEnterPushedState(const ui::Event& event) {
296 if (event.IsMouseEvent()) {
297 const ui::MouseEvent& mouseev = static_cast<const ui::MouseEvent&>(event);
298 // Active on left mouse button only, to prevent a menu from being activated
299 // when a right-click would also activate a context menu.
300 if (!mouseev.IsOnlyLeftMouseButton())
301 return false;
302 // If dragging is supported activate on release, otherwise activate on
303 // pressed.
304 ui::EventType active_on =
305 GetDragOperations(mouseev.location()) == ui::DragDropTypes::DRAG_NONE
306 ? ui::ET_MOUSE_PRESSED
307 : ui::ET_MOUSE_RELEASED;
308 return event.type() == active_on;
311 return event.type() == ui::ET_GESTURE_TAP;
314 void MenuButton::StateChanged() {
315 if (pressed_lock_count_ != 0) {
316 // The button's state was changed while it was supposed to be locked in a
317 // pressed state. This shouldn't happen, but conceivably could if a caller
318 // tries to switch from enabled to disabled or vice versa while the button
319 // is pressed.
320 if (state() == STATE_NORMAL)
321 should_disable_after_press_ = false;
322 else if (state() == STATE_DISABLED)
323 should_disable_after_press_ = true;
327 void MenuButton::IncrementPressedLocked() {
328 ++pressed_lock_count_;
329 should_disable_after_press_ = state() == STATE_DISABLED;
330 SetState(STATE_PRESSED);
333 void MenuButton::DecrementPressedLocked() {
334 --pressed_lock_count_;
335 DCHECK_GE(pressed_lock_count_, 0);
337 // If this was the last lock, manually reset state to the desired state.
338 if (pressed_lock_count_ == 0) {
339 ButtonState desired_state = STATE_NORMAL;
340 if (should_disable_after_press_) {
341 desired_state = STATE_DISABLED;
342 should_disable_after_press_ = false;
343 } else if (IsMouseHovered()) {
344 desired_state = STATE_HOVERED;
346 SetState(desired_state);
350 int MenuButton::GetMaximumScreenXCoordinate() {
351 if (!GetWidget()) {
352 NOTREACHED();
353 return 0;
356 gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
357 return monitor_bounds.right() - 1;
360 } // namespace views