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/widget/root_view.h"
25 #include "ui/views/widget/widget.h"
27 using base::TimeTicks
;
28 using base::TimeDelta
;
32 // Default menu offset.
33 static const int kDefaultMenuOffsetX
= -2;
34 static const int kDefaultMenuOffsetY
= -4;
37 const char MenuButton::kViewClassName
[] = "MenuButton";
38 const int MenuButton::kMenuMarkerPaddingLeft
= 3;
39 const int MenuButton::kMenuMarkerPaddingRight
= -1;
41 ////////////////////////////////////////////////////////////////////////////////
43 // MenuButton::PressedLock
45 ////////////////////////////////////////////////////////////////////////////////
47 MenuButton::PressedLock::PressedLock(MenuButton
* menu_button
)
48 : menu_button_(menu_button
->weak_factory_
.GetWeakPtr()) {
49 menu_button_
->IncrementPressedLocked();
52 MenuButton::PressedLock::~PressedLock() {
53 if (menu_button_
.get())
54 menu_button_
->DecrementPressedLocked();
57 ////////////////////////////////////////////////////////////////////////////////
59 // MenuButton - constructors, destructors, initialization
61 ////////////////////////////////////////////////////////////////////////////////
63 MenuButton::MenuButton(ButtonListener
* listener
,
64 const base::string16
& text
,
65 MenuButtonListener
* menu_button_listener
,
66 bool show_menu_marker
)
67 : LabelButton(listener
, text
),
68 menu_offset_(kDefaultMenuOffsetX
, kDefaultMenuOffsetY
),
69 listener_(menu_button_listener
),
70 show_menu_marker_(show_menu_marker
),
71 menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
72 IDR_MENU_DROPARROW
).ToImageSkia()),
73 destroyed_flag_(NULL
),
74 pressed_lock_count_(0),
75 should_disable_after_press_(false),
77 SetHorizontalAlignment(gfx::ALIGN_LEFT
);
80 MenuButton::~MenuButton() {
82 *destroyed_flag_
= true;
85 ////////////////////////////////////////////////////////////////////////////////
87 // MenuButton - Public APIs
89 ////////////////////////////////////////////////////////////////////////////////
91 bool MenuButton::Activate() {
92 SetState(STATE_PRESSED
);
94 gfx::Rect lb
= GetLocalBounds();
96 // The position of the menu depends on whether or not the locale is
98 gfx::Point
menu_position(lb
.right(), lb
.bottom());
99 if (base::i18n::IsRTL())
100 menu_position
.set_x(lb
.x());
102 View::ConvertPointToScreen(this, &menu_position
);
103 if (base::i18n::IsRTL())
104 menu_position
.Offset(-menu_offset_
.x(), menu_offset_
.y());
106 menu_position
.Offset(menu_offset_
.x(), menu_offset_
.y());
108 int max_x_coordinate
= GetMaximumScreenXCoordinate();
109 if (max_x_coordinate
&& max_x_coordinate
<= menu_position
.x())
110 menu_position
.set_x(max_x_coordinate
- 1);
112 // We're about to show the menu from a mouse press. By showing from the
113 // mouse press event we block RootView in mouse dispatching. This also
114 // appears to cause RootView to get a mouse pressed BEFORE the mouse
115 // release is seen, which means RootView sends us another mouse press no
116 // matter where the user pressed. To force RootView to recalculate the
117 // mouse target during the mouse press we explicitly set the mouse handler
119 static_cast<internal::RootView
*>(GetWidget()->GetRootView())->
120 SetMouseHandler(NULL
);
122 bool destroyed
= false;
123 destroyed_flag_
= &destroyed
;
125 // We don't set our state here. It's handled in the MenuController code or
126 // by our click listener.
128 listener_
->OnMenuButtonClicked(this, menu_position
);
131 // The menu was deleted while showing. Don't attempt any processing.
135 destroyed_flag_
= NULL
;
137 menu_closed_time_
= TimeTicks::Now();
139 // We must return false here so that the RootView does not get stuck
140 // sending all mouse pressed events to us instead of the appropriate
147 void MenuButton::OnPaint(gfx::Canvas
* canvas
) {
148 LabelButton::OnPaint(canvas
);
150 if (show_menu_marker_
)
151 PaintMenuMarker(canvas
);
154 ////////////////////////////////////////////////////////////////////////////////
156 // MenuButton - Events
158 ////////////////////////////////////////////////////////////////////////////////
160 gfx::Size
MenuButton::GetPreferredSize() const {
161 gfx::Size prefsize
= LabelButton::GetPreferredSize();
162 if (show_menu_marker_
) {
163 prefsize
.Enlarge(menu_marker_
->width() + kMenuMarkerPaddingLeft
+
164 kMenuMarkerPaddingRight
,
170 const char* MenuButton::GetClassName() const {
171 return kViewClassName
;
174 bool MenuButton::OnMousePressed(const ui::MouseEvent
& event
) {
176 if (state() != STATE_DISABLED
&& ShouldEnterPushedState(event
) &&
177 HitTestPoint(event
.location())) {
178 TimeDelta delta
= TimeTicks::Now() - menu_closed_time_
;
179 if (delta
.InMilliseconds() > kMinimumMsBetweenButtonClicks
)
185 void MenuButton::OnMouseReleased(const ui::MouseEvent
& event
) {
186 if (state() != STATE_DISABLED
&& ShouldEnterPushedState(event
) &&
187 HitTestPoint(event
.location()) && !InDrag()) {
190 LabelButton::OnMouseReleased(event
);
194 void MenuButton::OnMouseEntered(const ui::MouseEvent
& event
) {
195 if (pressed_lock_count_
== 0) // Ignore mouse movement if state is locked.
196 LabelButton::OnMouseEntered(event
);
199 void MenuButton::OnMouseExited(const ui::MouseEvent
& event
) {
200 if (pressed_lock_count_
== 0) // Ignore mouse movement if state is locked.
201 LabelButton::OnMouseExited(event
);
204 void MenuButton::OnMouseMoved(const ui::MouseEvent
& event
) {
205 if (pressed_lock_count_
== 0) // Ignore mouse movement if state is locked.
206 LabelButton::OnMouseMoved(event
);
209 void MenuButton::OnGestureEvent(ui::GestureEvent
* event
) {
210 if (state() != STATE_DISABLED
) {
211 if (ShouldEnterPushedState(*event
) && !Activate()) {
212 // When |Activate()| returns |false|, it means that a menu is shown and
213 // has handled the gesture event. So, there is no need to further process
214 // the gesture event here.
217 if (switches::IsTouchFeedbackEnabled()) {
218 if (event
->type() == ui::ET_GESTURE_TAP_DOWN
) {
220 SetState(Button::STATE_HOVERED
);
221 } else if (state() == Button::STATE_HOVERED
&&
222 (event
->type() == ui::ET_GESTURE_TAP_CANCEL
||
223 event
->type() == ui::ET_GESTURE_END
)) {
224 SetState(Button::STATE_NORMAL
);
228 LabelButton::OnGestureEvent(event
);
231 bool MenuButton::OnKeyPressed(const ui::KeyEvent
& event
) {
232 switch (event
.key_code()) {
234 // Alt-space on windows should show the window menu.
235 if (event
.IsAltDown())
237 case ui::VKEY_RETURN
:
239 case ui::VKEY_DOWN
: {
240 // WARNING: we may have been deleted by the time Activate returns.
242 // This is to prevent the keyboard event from being dispatched twice. If
243 // the keyboard event is not handled, we pass it to the default handler
244 // which dispatches the event back to us causing the menu to get displayed
245 // again. Return true to prevent this.
254 bool MenuButton::OnKeyReleased(const ui::KeyEvent
& event
) {
255 // Override CustomButton's implementation, which presses the button when
256 // you press space and clicks it when you release space. For a MenuButton
257 // we always activate the menu on key press.
261 void MenuButton::GetAccessibleState(ui::AXViewState
* state
) {
262 CustomButton::GetAccessibleState(state
);
263 state
->role
= ui::AX_ROLE_POP_UP_BUTTON
;
264 state
->default_action
= l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS
);
265 state
->AddStateFlag(ui::AX_STATE_HASPOPUP
);
268 void MenuButton::PaintMenuMarker(gfx::Canvas
* canvas
) {
269 gfx::Insets insets
= GetInsets();
271 // Using the Views mirroring infrastructure incorrectly flips icon content.
272 // Instead, manually mirror the position of the down arrow.
273 gfx::Rect
arrow_bounds(width() - insets
.right() -
274 menu_marker_
->width() - kMenuMarkerPaddingRight
,
275 height() / 2 - menu_marker_
->height() / 2,
276 menu_marker_
->width(),
277 menu_marker_
->height());
278 arrow_bounds
.set_x(GetMirroredXForRect(arrow_bounds
));
279 canvas
->DrawImageInt(*menu_marker_
, arrow_bounds
.x(), arrow_bounds
.y());
282 gfx::Rect
MenuButton::GetChildAreaBounds() {
283 gfx::Size s
= size();
285 if (show_menu_marker_
) {
286 s
.set_width(s
.width() - menu_marker_
->width() - kMenuMarkerPaddingLeft
-
287 kMenuMarkerPaddingRight
);
293 bool MenuButton::ShouldEnterPushedState(const ui::Event
& event
) {
294 if (event
.IsMouseEvent()) {
295 const ui::MouseEvent
& mouseev
= static_cast<const ui::MouseEvent
&>(event
);
296 // Active on left mouse button only, to prevent a menu from being activated
297 // when a right-click would also activate a context menu.
298 if (!mouseev
.IsOnlyLeftMouseButton())
300 // If dragging is supported activate on release, otherwise activate on
302 ui::EventType active_on
=
303 GetDragOperations(mouseev
.location()) == ui::DragDropTypes::DRAG_NONE
304 ? ui::ET_MOUSE_PRESSED
305 : ui::ET_MOUSE_RELEASED
;
306 return event
.type() == active_on
;
309 return event
.type() == ui::ET_GESTURE_TAP
;
312 void MenuButton::StateChanged() {
313 if (pressed_lock_count_
!= 0) {
314 // The button's state was changed while it was supposed to be locked in a
315 // pressed state. This shouldn't happen, but conceivably could if a caller
316 // tries to switch from enabled to disabled or vice versa while the button
318 if (state() == STATE_NORMAL
)
319 should_disable_after_press_
= false;
320 else if (state() == STATE_DISABLED
)
321 should_disable_after_press_
= true;
325 void MenuButton::IncrementPressedLocked() {
326 ++pressed_lock_count_
;
327 should_disable_after_press_
= state() == STATE_DISABLED
;
328 SetState(STATE_PRESSED
);
331 void MenuButton::DecrementPressedLocked() {
332 --pressed_lock_count_
;
333 DCHECK_GE(pressed_lock_count_
, 0);
335 // If this was the last lock, manually reset state to the desired state.
336 if (pressed_lock_count_
== 0) {
337 ButtonState desired_state
= STATE_NORMAL
;
338 if (should_disable_after_press_
) {
339 desired_state
= STATE_DISABLED
;
340 should_disable_after_press_
= false;
341 } else if (IsMouseHovered()) {
342 desired_state
= STATE_HOVERED
;
344 SetState(desired_state
);
348 int MenuButton::GetMaximumScreenXCoordinate() {
354 gfx::Rect monitor_bounds
= GetWidget()->GetWorkAreaBoundsInScreen();
355 return monitor_bounds
.right() - 1;