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/events/event.h"
13 #include "ui/events/event_constants.h"
14 #include "ui/gfx/canvas.h"
15 #include "ui/gfx/image/image.h"
16 #include "ui/gfx/screen.h"
17 #include "ui/gfx/text_constants.h"
18 #include "ui/resources/grit/ui_resources.h"
19 #include "ui/strings/grit/ui_strings.h"
20 #include "ui/views/controls/button/button.h"
21 #include "ui/views/controls/button/menu_button_listener.h"
22 #include "ui/views/mouse_constants.h"
23 #include "ui/views/widget/root_view.h"
24 #include "ui/views/widget/widget.h"
26 using base::TimeTicks
;
27 using base::TimeDelta
;
31 // Default menu offset.
32 static const int kDefaultMenuOffsetX
= -2;
33 static const int kDefaultMenuOffsetY
= -4;
36 const char MenuButton::kViewClassName
[] = "MenuButton";
37 const int MenuButton::kMenuMarkerPaddingLeft
= 3;
38 const int MenuButton::kMenuMarkerPaddingRight
= -1;
40 ////////////////////////////////////////////////////////////////////////////////
42 // MenuButton - constructors, destructors, initialization
44 ////////////////////////////////////////////////////////////////////////////////
46 MenuButton::MenuButton(ButtonListener
* listener
,
47 const base::string16
& text
,
48 MenuButtonListener
* menu_button_listener
,
49 bool show_menu_marker
)
50 : LabelButton(listener
, text
),
52 menu_offset_(kDefaultMenuOffsetX
, kDefaultMenuOffsetY
),
53 listener_(menu_button_listener
),
54 show_menu_marker_(show_menu_marker
),
55 menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
56 IDR_MENU_DROPARROW
).ToImageSkia()),
57 destroyed_flag_(NULL
) {
58 SetHorizontalAlignment(gfx::ALIGN_LEFT
);
61 MenuButton::~MenuButton() {
63 *destroyed_flag_
= true;
66 ////////////////////////////////////////////////////////////////////////////////
68 // MenuButton - Public APIs
70 ////////////////////////////////////////////////////////////////////////////////
72 bool MenuButton::Activate() {
73 SetState(STATE_PRESSED
);
75 gfx::Rect lb
= GetLocalBounds();
77 // The position of the menu depends on whether or not the locale is
79 gfx::Point
menu_position(lb
.right(), lb
.bottom());
80 if (base::i18n::IsRTL())
81 menu_position
.set_x(lb
.x());
83 View::ConvertPointToScreen(this, &menu_position
);
84 if (base::i18n::IsRTL())
85 menu_position
.Offset(-menu_offset_
.x(), menu_offset_
.y());
87 menu_position
.Offset(menu_offset_
.x(), menu_offset_
.y());
89 int max_x_coordinate
= GetMaximumScreenXCoordinate();
90 if (max_x_coordinate
&& max_x_coordinate
<= menu_position
.x())
91 menu_position
.set_x(max_x_coordinate
- 1);
93 // We're about to show the menu from a mouse press. By showing from the
94 // mouse press event we block RootView in mouse dispatching. This also
95 // appears to cause RootView to get a mouse pressed BEFORE the mouse
96 // release is seen, which means RootView sends us another mouse press no
97 // matter where the user pressed. To force RootView to recalculate the
98 // mouse target during the mouse press we explicitly set the mouse handler
100 static_cast<internal::RootView
*>(GetWidget()->GetRootView())->
101 SetMouseHandler(NULL
);
103 menu_visible_
= true;
105 bool destroyed
= false;
106 destroyed_flag_
= &destroyed
;
108 listener_
->OnMenuButtonClicked(this, menu_position
);
111 // The menu was deleted while showing. Don't attempt any processing.
115 destroyed_flag_
= NULL
;
117 menu_visible_
= false;
118 menu_closed_time_
= TimeTicks::Now();
120 // Now that the menu has closed, we need to manually reset state to
121 // "normal" since the menu modal loop will have prevented normal
122 // mouse move messages from getting to this View. We set "normal"
123 // and not "hot" because the likelihood is that the mouse is now
124 // somewhere else (user clicked elsewhere on screen to close the menu
125 // or selected an item) and we will inevitably refresh the hot state
126 // in the event the mouse _is_ over the view.
127 SetState(STATE_NORMAL
);
129 // We must return false here so that the RootView does not get stuck
130 // sending all mouse pressed events to us instead of the appropriate
137 void MenuButton::OnPaint(gfx::Canvas
* canvas
) {
138 LabelButton::OnPaint(canvas
);
140 if (show_menu_marker_
)
141 PaintMenuMarker(canvas
);
144 ////////////////////////////////////////////////////////////////////////////////
146 // MenuButton - Events
148 ////////////////////////////////////////////////////////////////////////////////
150 gfx::Size
MenuButton::GetPreferredSize() const {
151 gfx::Size prefsize
= LabelButton::GetPreferredSize();
152 if (show_menu_marker_
) {
153 prefsize
.Enlarge(menu_marker_
->width() + kMenuMarkerPaddingLeft
+
154 kMenuMarkerPaddingRight
,
160 const char* MenuButton::GetClassName() const {
161 return kViewClassName
;
164 bool MenuButton::OnMousePressed(const ui::MouseEvent
& event
) {
166 if (state() != STATE_DISABLED
) {
167 // If we're draggable (GetDragOperations returns a non-zero value), then
168 // don't pop on press, instead wait for release.
169 if (event
.IsOnlyLeftMouseButton() &&
170 HitTestPoint(event
.location()) &&
171 GetDragOperations(event
.location()) == ui::DragDropTypes::DRAG_NONE
) {
172 TimeDelta delta
= TimeTicks::Now() - menu_closed_time_
;
173 if (delta
.InMilliseconds() > kMinimumMsBetweenButtonClicks
)
180 void MenuButton::OnMouseReleased(const ui::MouseEvent
& event
) {
181 // Explicitly test for left mouse button to show the menu. If we tested for
182 // !IsTriggerableEvent it could lead to a situation where we end up showing
183 // the menu and context menu (this would happen if the right button is not
184 // triggerable and there's a context menu).
185 if (GetDragOperations(event
.location()) != ui::DragDropTypes::DRAG_NONE
&&
186 state() != STATE_DISABLED
&& !InDrag() && event
.IsOnlyLeftMouseButton() &&
187 HitTestPoint(event
.location())) {
190 LabelButton::OnMouseReleased(event
);
194 // The reason we override View::OnMouseExited is because we get this event when
195 // we display the menu. If we don't override this method then
196 // BaseButton::OnMouseExited will get the event and will set the button's state
197 // to STATE_NORMAL instead of keeping the state BM_PUSHED. This, in turn, will
198 // cause the button to appear depressed while the menu is displayed.
199 void MenuButton::OnMouseExited(const ui::MouseEvent
& event
) {
200 if ((state_
!= STATE_DISABLED
) && (!menu_visible_
) && (!InDrag())) {
201 SetState(STATE_NORMAL
);
205 void MenuButton::OnGestureEvent(ui::GestureEvent
* event
) {
206 if (state() != STATE_DISABLED
&& event
->type() == ui::ET_GESTURE_TAP
&&
208 // When |Activate()| returns |false|, it means that a menu is shown and
209 // has handled the gesture event. So, there is no need to further process
210 // the gesture event here.
213 LabelButton::OnGestureEvent(event
);
216 bool MenuButton::OnKeyPressed(const ui::KeyEvent
& event
) {
217 switch (event
.key_code()) {
219 // Alt-space on windows should show the window menu.
220 if (event
.IsAltDown())
222 case ui::VKEY_RETURN
:
224 case ui::VKEY_DOWN
: {
225 // WARNING: we may have been deleted by the time Activate returns.
227 // This is to prevent the keyboard event from being dispatched twice. If
228 // the keyboard event is not handled, we pass it to the default handler
229 // which dispatches the event back to us causing the menu to get displayed
230 // again. Return true to prevent this.
239 bool MenuButton::OnKeyReleased(const ui::KeyEvent
& event
) {
240 // Override CustomButton's implementation, which presses the button when
241 // you press space and clicks it when you release space. For a MenuButton
242 // we always activate the menu on key press.
246 void MenuButton::GetAccessibleState(ui::AXViewState
* state
) {
247 CustomButton::GetAccessibleState(state
);
248 state
->role
= ui::AX_ROLE_POP_UP_BUTTON
;
249 state
->default_action
= l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS
);
250 state
->AddStateFlag(ui::AX_STATE_HASPOPUP
);
253 void MenuButton::PaintMenuMarker(gfx::Canvas
* canvas
) {
254 gfx::Insets insets
= GetInsets();
256 // Using the Views mirroring infrastructure incorrectly flips icon content.
257 // Instead, manually mirror the position of the down arrow.
258 gfx::Rect
arrow_bounds(width() - insets
.right() -
259 menu_marker_
->width() - kMenuMarkerPaddingRight
,
260 height() / 2 - menu_marker_
->height() / 2,
261 menu_marker_
->width(),
262 menu_marker_
->height());
263 arrow_bounds
.set_x(GetMirroredXForRect(arrow_bounds
));
264 canvas
->DrawImageInt(*menu_marker_
, arrow_bounds
.x(), arrow_bounds
.y());
267 gfx::Rect
MenuButton::GetChildAreaBounds() {
268 gfx::Size s
= size();
270 if (show_menu_marker_
) {
271 s
.set_width(s
.width() - menu_marker_
->width() - kMenuMarkerPaddingLeft
-
272 kMenuMarkerPaddingRight
);
278 int MenuButton::GetMaximumScreenXCoordinate() {
284 gfx::Rect monitor_bounds
= GetWidget()->GetWorkAreaBoundsInScreen();
285 return monitor_bounds
.right() - 1;