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.
4
5 #include "ui/views/controls/button/menu_button.h"
6
7 #include "base/strings/utf_string_conversions.h"
8 #include "grit/ui_resources.h"
9 #include "grit/ui_strings.h"
10 #include "ui/base/accessibility/accessible_view_state.h"
11 #include "ui/base/dragdrop/drag_drop_types.h"
12 #include "ui/base/l10n/l10n_util.h"
13 #include "ui/base/resource/resource_bundle.h"
14 #include "ui/events/event.h"
15 #include "ui/events/event_constants.h"
16 #include "ui/gfx/canvas.h"
17 #include "ui/gfx/image/image.h"
18 #include "ui/gfx/screen.h"
19 #include "ui/views/controls/button/button.h"
20 #include "ui/views/controls/button/menu_button_listener.h"
21 #include "ui/views/mouse_constants.h"
22 #include "ui/views/widget/root_view.h"
23 #include "ui/views/widget/widget.h"
24
25 using base::TimeTicks;
26 using base::TimeDelta;
27
28 namespace views {
29
30 // Default menu offset.
31 static const int kDefaultMenuOffsetX = -2;
32 static const int kDefaultMenuOffsetY = -4;
33
34 // static
35 const char MenuButton::kViewClassName[] = "MenuButton";
36 const int MenuButton::kMenuMarkerPaddingLeft = 3;
37 const int MenuButton::kMenuMarkerPaddingRight = -1;
38
39 ////////////////////////////////////////////////////////////////////////////////
40 //
41 // MenuButton - constructors, destructors, initialization
42 //
43 ////////////////////////////////////////////////////////////////////////////////
44
MenuButton(ButtonListener * listener,const string16 & text,MenuButtonListener * menu_button_listener,bool show_menu_marker)45 MenuButton::MenuButton(ButtonListener* listener,
46 const string16& text,
47 MenuButtonListener* menu_button_listener,
48 bool show_menu_marker)
49 : TextButton(listener, text),
50 menu_visible_(false),
51 menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY),
52 listener_(menu_button_listener),
53 show_menu_marker_(show_menu_marker),
54 menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
55 IDR_MENU_DROPARROW).ToImageSkia()),
56 destroyed_flag_(NULL) {
57 set_alignment(TextButton::ALIGN_LEFT);
58 }
59
~MenuButton()60 MenuButton::~MenuButton() {
61 if (destroyed_flag_)
62 *destroyed_flag_ = true;
63 }
64
65 ////////////////////////////////////////////////////////////////////////////////
66 //
67 // MenuButton - Public APIs
68 //
69 ////////////////////////////////////////////////////////////////////////////////
70
Activate()71 bool MenuButton::Activate() {
72 SetState(STATE_PRESSED);
73 if (listener_) {
74 gfx::Rect lb = GetLocalBounds();
75
76 // The position of the menu depends on whether or not the locale is
77 // right-to-left.
78 gfx::Point menu_position(lb.right(), lb.bottom());
79 if (base::i18n::IsRTL())
80 menu_position.set_x(lb.x());
81
82 View::ConvertPointToScreen(this, &menu_position);
83 if (base::i18n::IsRTL())
84 menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
85 else
86 menu_position.Offset(menu_offset_.x(), menu_offset_.y());
87
88 int max_x_coordinate = GetMaximumScreenXCoordinate();
89 if (max_x_coordinate && max_x_coordinate <= menu_position.x())
90 menu_position.set_x(max_x_coordinate - 1);
91
92 // We're about to show the menu from a mouse press. By showing from the
93 // mouse press event we block RootView in mouse dispatching. This also
94 // appears to cause RootView to get a mouse pressed BEFORE the mouse
95 // release is seen, which means RootView sends us another mouse press no
96 // matter where the user pressed. To force RootView to recalculate the
97 // mouse target during the mouse press we explicitly set the mouse handler
98 // to NULL.
99 static_cast<internal::RootView*>(GetWidget()->GetRootView())->
100 SetMouseHandler(NULL);
101
102 menu_visible_ = true;
103
104 bool destroyed = false;
105 destroyed_flag_ = &destroyed;
106
107 listener_->OnMenuButtonClicked(this, menu_position);
108
109 if (destroyed) {
110 // The menu was deleted while showing. Don't attempt any processing.
111 return false;
112 }
113
114 destroyed_flag_ = NULL;
115
116 menu_visible_ = false;
117 menu_closed_time_ = TimeTicks::Now();
118
119 // Now that the menu has closed, we need to manually reset state to
120 // "normal" since the menu modal loop will have prevented normal
121 // mouse move messages from getting to this View. We set "normal"
122 // and not "hot" because the likelihood is that the mouse is now
123 // somewhere else (user clicked elsewhere on screen to close the menu
124 // or selected an item) and we will inevitably refresh the hot state
125 // in the event the mouse _is_ over the view.
126 SetState(STATE_NORMAL);
127
128 // We must return false here so that the RootView does not get stuck
129 // sending all mouse pressed events to us instead of the appropriate
130 // target.
131 return false;
132 }
133 return true;
134 }
135
PaintButton(gfx::Canvas * canvas,PaintButtonMode mode)136 void MenuButton::PaintButton(gfx::Canvas* canvas, PaintButtonMode mode) {
137 TextButton::PaintButton(canvas, mode);
138
139 if (show_menu_marker_)
140 PaintMenuMarker(canvas);
141 }
142
143 ////////////////////////////////////////////////////////////////////////////////
144 //
145 // MenuButton - Events
146 //
147 ////////////////////////////////////////////////////////////////////////////////
148
GetPreferredSize()149 gfx::Size MenuButton::GetPreferredSize() {
150 gfx::Size prefsize = TextButton::GetPreferredSize();
151 if (show_menu_marker_) {
152 prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft +
153 kMenuMarkerPaddingRight,
154 0);
155 }
156 return prefsize;
157 }
158
GetClassName() const159 const char* MenuButton::GetClassName() const {
160 return kViewClassName;
161 }
162
OnMousePressed(const ui::MouseEvent & event)163 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
164 RequestFocus();
165 if (state() != STATE_DISABLED) {
166 // If we're draggable (GetDragOperations returns a non-zero value), then
167 // don't pop on press, instead wait for release.
168 if (event.IsOnlyLeftMouseButton() &&
169 HitTestPoint(event.location()) &&
170 GetDragOperations(event.location()) == ui::DragDropTypes::DRAG_NONE) {
171 TimeDelta delta = TimeTicks::Now() - menu_closed_time_;
172 if (delta.InMilliseconds() > kMinimumMsBetweenButtonClicks)
173 return Activate();
174 }
175 }
176 return true;
177 }
178
OnMouseReleased(const ui::MouseEvent & event)179 void MenuButton::OnMouseReleased(const ui::MouseEvent& event) {
180 // Explicitly test for left mouse button to show the menu. If we tested for
181 // !IsTriggerableEvent it could lead to a situation where we end up showing
182 // the menu and context menu (this would happen if the right button is not
183 // triggerable and there's a context menu).
184 if (GetDragOperations(event.location()) != ui::DragDropTypes::DRAG_NONE &&
185 state() != STATE_DISABLED && !InDrag() && event.IsOnlyLeftMouseButton() &&
186 HitTestPoint(event.location())) {
187 Activate();
188 } else {
189 TextButton::OnMouseReleased(event);
190 }
191 }
192
193 // The reason we override View::OnMouseExited is because we get this event when
194 // we display the menu. If we don't override this method then
195 // BaseButton::OnMouseExited will get the event and will set the button's state
196 // to STATE_NORMAL instead of keeping the state BM_PUSHED. This, in turn, will
197 // cause the button to appear depressed while the menu is displayed.
OnMouseExited(const ui::MouseEvent & event)198 void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
199 if ((state_ != STATE_DISABLED) && (!menu_visible_) && (!InDrag())) {
200 SetState(STATE_NORMAL);
201 }
202 }
203
OnGestureEvent(ui::GestureEvent * event)204 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
205 if (state() != STATE_DISABLED && event->type() == ui::ET_GESTURE_TAP) {
206 if (Activate())
207 event->StopPropagation();
208 return;
209 }
210 TextButton::OnGestureEvent(event);
211 }
212
OnKeyPressed(const ui::KeyEvent & event)213 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
214 switch (event.key_code()) {
215 case ui::VKEY_SPACE:
216 // Alt-space on windows should show the window menu.
217 if (event.IsAltDown())
218 break;
219 case ui::VKEY_RETURN:
220 case ui::VKEY_UP:
221 case ui::VKEY_DOWN: {
222 // WARNING: we may have been deleted by the time Activate returns.
223 bool ret = Activate();
224 #if defined(USE_AURA)
225 // This is to prevent the keyboard event from being dispatched twice.
226 // The Activate function returns false in most cases. In AURA if the
227 // keyboard event is not handled, we pass it to the default handler
228 // which dispatches the event back to us causing the menu to get
229 // displayed again.
230 ret = true;
231 #endif
232 return ret;
233 }
234 default:
235 break;
236 }
237 return false;
238 }
239
OnKeyReleased(const ui::KeyEvent & event)240 bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) {
241 // Override CustomButton's implementation, which presses the button when
242 // you press space and clicks it when you release space. For a MenuButton
243 // we always activate the menu on key press.
244 return false;
245 }
246
GetAccessibleState(ui::AccessibleViewState * state)247 void MenuButton::GetAccessibleState(ui::AccessibleViewState* state) {
248 CustomButton::GetAccessibleState(state);
249 state->role = ui::AccessibilityTypes::ROLE_BUTTONMENU;
250 state->default_action = l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS);
251 state->state = ui::AccessibilityTypes::STATE_HASPOPUP;
252 }
253
PaintMenuMarker(gfx::Canvas * canvas)254 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
255 gfx::Insets insets = GetInsets();
256
257 // We can not use the views' mirroring infrastructure for mirroring a
258 // MenuButton control (see TextButton::OnPaint() for a detailed explanation
259 // regarding why we can not flip the canvas). Therefore, we need to
260 // manually mirror the position of the down arrow.
261 gfx::Rect arrow_bounds(width() - insets.right() -
262 menu_marker_->width() - kMenuMarkerPaddingRight,
263 height() / 2 - menu_marker_->height() / 2,
264 menu_marker_->width(),
265 menu_marker_->height());
266 arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
267 canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
268 }
269
GetMaximumScreenXCoordinate()270 int MenuButton::GetMaximumScreenXCoordinate() {
271 if (!GetWidget()) {
272 NOTREACHED();
273 return 0;
274 }
275
276 gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
277 return monitor_bounds.right() - 1;
278 }
279
280 } // namespace views
281