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 "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"
25
26 using base::TimeTicks;
27 using base::TimeDelta;
28
29 namespace views {
30
31 // Default menu offset.
32 static const int kDefaultMenuOffsetX = -2;
33 static const int kDefaultMenuOffsetY = -4;
34
35 // static
36 const char MenuButton::kViewClassName[] = "MenuButton";
37 const int MenuButton::kMenuMarkerPaddingLeft = 3;
38 const int MenuButton::kMenuMarkerPaddingRight = -1;
39
40 ////////////////////////////////////////////////////////////////////////////////
41 //
42 // MenuButton::PressedLock
43 //
44 ////////////////////////////////////////////////////////////////////////////////
45
PressedLock(MenuButton * menu_button)46 MenuButton::PressedLock::PressedLock(MenuButton* menu_button)
47 : menu_button_(menu_button->weak_factory_.GetWeakPtr()) {
48 menu_button_->IncrementPressedLocked();
49 }
50
~PressedLock()51 MenuButton::PressedLock::~PressedLock() {
52 if (menu_button_.get())
53 menu_button_->DecrementPressedLocked();
54 }
55
56 ////////////////////////////////////////////////////////////////////////////////
57 //
58 // MenuButton - constructors, destructors, initialization
59 //
60 ////////////////////////////////////////////////////////////////////////////////
61
MenuButton(ButtonListener * listener,const base::string16 & text,MenuButtonListener * menu_button_listener,bool show_menu_marker)62 MenuButton::MenuButton(ButtonListener* listener,
63 const base::string16& text,
64 MenuButtonListener* menu_button_listener,
65 bool show_menu_marker)
66 : LabelButton(listener, text),
67 menu_offset_(kDefaultMenuOffsetX, kDefaultMenuOffsetY),
68 listener_(menu_button_listener),
69 show_menu_marker_(show_menu_marker),
70 menu_marker_(ui::ResourceBundle::GetSharedInstance().GetImageNamed(
71 IDR_MENU_DROPARROW).ToImageSkia()),
72 destroyed_flag_(NULL),
73 pressed_lock_count_(0),
74 weak_factory_(this) {
75 SetHorizontalAlignment(gfx::ALIGN_LEFT);
76 }
77
~MenuButton()78 MenuButton::~MenuButton() {
79 if (destroyed_flag_)
80 *destroyed_flag_ = true;
81 }
82
83 ////////////////////////////////////////////////////////////////////////////////
84 //
85 // MenuButton - Public APIs
86 //
87 ////////////////////////////////////////////////////////////////////////////////
88
Activate()89 bool MenuButton::Activate() {
90 SetState(STATE_PRESSED);
91 if (listener_) {
92 gfx::Rect lb = GetLocalBounds();
93
94 // The position of the menu depends on whether or not the locale is
95 // right-to-left.
96 gfx::Point menu_position(lb.right(), lb.bottom());
97 if (base::i18n::IsRTL())
98 menu_position.set_x(lb.x());
99
100 View::ConvertPointToScreen(this, &menu_position);
101 if (base::i18n::IsRTL())
102 menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
103 else
104 menu_position.Offset(menu_offset_.x(), menu_offset_.y());
105
106 int max_x_coordinate = GetMaximumScreenXCoordinate();
107 if (max_x_coordinate && max_x_coordinate <= menu_position.x())
108 menu_position.set_x(max_x_coordinate - 1);
109
110 // We're about to show the menu from a mouse press. By showing from the
111 // mouse press event we block RootView in mouse dispatching. This also
112 // appears to cause RootView to get a mouse pressed BEFORE the mouse
113 // release is seen, which means RootView sends us another mouse press no
114 // matter where the user pressed. To force RootView to recalculate the
115 // mouse target during the mouse press we explicitly set the mouse handler
116 // to NULL.
117 static_cast<internal::RootView*>(GetWidget()->GetRootView())->
118 SetMouseHandler(NULL);
119
120 bool destroyed = false;
121 destroyed_flag_ = &destroyed;
122
123 // We don't set our state here. It's handled in the MenuController code or
124 // by our click listener.
125
126 listener_->OnMenuButtonClicked(this, menu_position);
127
128 if (destroyed) {
129 // The menu was deleted while showing. Don't attempt any processing.
130 return false;
131 }
132
133 destroyed_flag_ = NULL;
134
135 menu_closed_time_ = TimeTicks::Now();
136
137 // We must return false here so that the RootView does not get stuck
138 // sending all mouse pressed events to us instead of the appropriate
139 // target.
140 return false;
141 }
142 return true;
143 }
144
OnPaint(gfx::Canvas * canvas)145 void MenuButton::OnPaint(gfx::Canvas* canvas) {
146 LabelButton::OnPaint(canvas);
147
148 if (show_menu_marker_)
149 PaintMenuMarker(canvas);
150 }
151
152 ////////////////////////////////////////////////////////////////////////////////
153 //
154 // MenuButton - Events
155 //
156 ////////////////////////////////////////////////////////////////////////////////
157
GetPreferredSize() const158 gfx::Size MenuButton::GetPreferredSize() const {
159 gfx::Size prefsize = LabelButton::GetPreferredSize();
160 if (show_menu_marker_) {
161 prefsize.Enlarge(menu_marker_->width() + kMenuMarkerPaddingLeft +
162 kMenuMarkerPaddingRight,
163 0);
164 }
165 return prefsize;
166 }
167
GetClassName() const168 const char* MenuButton::GetClassName() const {
169 return kViewClassName;
170 }
171
OnMousePressed(const ui::MouseEvent & event)172 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
173 RequestFocus();
174 if (state() != STATE_DISABLED) {
175 // If we're draggable (GetDragOperations returns a non-zero value), then
176 // don't pop on press, instead wait for release.
177 if (event.IsOnlyLeftMouseButton() &&
178 HitTestPoint(event.location()) &&
179 GetDragOperations(event.location()) == ui::DragDropTypes::DRAG_NONE) {
180 TimeDelta delta = TimeTicks::Now() - menu_closed_time_;
181 if (delta.InMilliseconds() > kMinimumMsBetweenButtonClicks)
182 return Activate();
183 }
184 }
185 return true;
186 }
187
OnMouseReleased(const ui::MouseEvent & event)188 void MenuButton::OnMouseReleased(const ui::MouseEvent& event) {
189 // Explicitly test for left mouse button to show the menu. If we tested for
190 // !IsTriggerableEvent it could lead to a situation where we end up showing
191 // the menu and context menu (this would happen if the right button is not
192 // triggerable and there's a context menu).
193 if (GetDragOperations(event.location()) != ui::DragDropTypes::DRAG_NONE &&
194 state() != STATE_DISABLED && !InDrag() && event.IsOnlyLeftMouseButton() &&
195 HitTestPoint(event.location())) {
196 Activate();
197 } else {
198 LabelButton::OnMouseReleased(event);
199 }
200 }
201
OnMouseEntered(const ui::MouseEvent & event)202 void MenuButton::OnMouseEntered(const ui::MouseEvent& event) {
203 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
204 CustomButton::OnMouseEntered(event);
205 }
206
OnMouseExited(const ui::MouseEvent & event)207 void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
208 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
209 CustomButton::OnMouseExited(event);
210 }
211
OnMouseMoved(const ui::MouseEvent & event)212 void MenuButton::OnMouseMoved(const ui::MouseEvent& event) {
213 if (pressed_lock_count_ == 0) // Ignore mouse movement if state is locked.
214 CustomButton::OnMouseMoved(event);
215 }
216
OnGestureEvent(ui::GestureEvent * event)217 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
218 if (state() != STATE_DISABLED && event->type() == ui::ET_GESTURE_TAP &&
219 !Activate()) {
220 // When |Activate()| returns |false|, it means that a menu is shown and
221 // has handled the gesture event. So, there is no need to further process
222 // the gesture event here.
223 return;
224 }
225 LabelButton::OnGestureEvent(event);
226 }
227
OnKeyPressed(const ui::KeyEvent & event)228 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
229 switch (event.key_code()) {
230 case ui::VKEY_SPACE:
231 // Alt-space on windows should show the window menu.
232 if (event.IsAltDown())
233 break;
234 case ui::VKEY_RETURN:
235 case ui::VKEY_UP:
236 case ui::VKEY_DOWN: {
237 // WARNING: we may have been deleted by the time Activate returns.
238 Activate();
239 // This is to prevent the keyboard event from being dispatched twice. If
240 // the keyboard event is not handled, we pass it to the default handler
241 // which dispatches the event back to us causing the menu to get displayed
242 // again. Return true to prevent this.
243 return true;
244 }
245 default:
246 break;
247 }
248 return false;
249 }
250
OnKeyReleased(const ui::KeyEvent & event)251 bool MenuButton::OnKeyReleased(const ui::KeyEvent& event) {
252 // Override CustomButton's implementation, which presses the button when
253 // you press space and clicks it when you release space. For a MenuButton
254 // we always activate the menu on key press.
255 return false;
256 }
257
GetAccessibleState(ui::AXViewState * state)258 void MenuButton::GetAccessibleState(ui::AXViewState* state) {
259 CustomButton::GetAccessibleState(state);
260 state->role = ui::AX_ROLE_POP_UP_BUTTON;
261 state->default_action = l10n_util::GetStringUTF16(IDS_APP_ACCACTION_PRESS);
262 state->AddStateFlag(ui::AX_STATE_HASPOPUP);
263 }
264
PaintMenuMarker(gfx::Canvas * canvas)265 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
266 gfx::Insets insets = GetInsets();
267
268 // Using the Views mirroring infrastructure incorrectly flips icon content.
269 // Instead, manually mirror the position of the down arrow.
270 gfx::Rect arrow_bounds(width() - insets.right() -
271 menu_marker_->width() - kMenuMarkerPaddingRight,
272 height() / 2 - menu_marker_->height() / 2,
273 menu_marker_->width(),
274 menu_marker_->height());
275 arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
276 canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
277 }
278
GetChildAreaBounds()279 gfx::Rect MenuButton::GetChildAreaBounds() {
280 gfx::Size s = size();
281
282 if (show_menu_marker_) {
283 s.set_width(s.width() - menu_marker_->width() - kMenuMarkerPaddingLeft -
284 kMenuMarkerPaddingRight);
285 }
286
287 return gfx::Rect(s);
288 }
289
IncrementPressedLocked()290 void MenuButton::IncrementPressedLocked() {
291 ++pressed_lock_count_;
292 SetState(STATE_PRESSED);
293 }
294
DecrementPressedLocked()295 void MenuButton::DecrementPressedLocked() {
296 --pressed_lock_count_;
297 DCHECK_GE(pressed_lock_count_, 0);
298
299 // If this was the last lock, manually reset state to "normal". We set
300 // "normal" and not "hot" because the likelihood is that the mouse is now
301 // somewhere else (user clicked elsewhere on screen to close the menu or
302 // selected an item) and we will inevitably refresh the hot state in the event
303 // the mouse _is_ over the view.
304 if (pressed_lock_count_ == 0)
305 SetState(STATE_NORMAL);
306 }
307
GetMaximumScreenXCoordinate()308 int MenuButton::GetMaximumScreenXCoordinate() {
309 if (!GetWidget()) {
310 NOTREACHED();
311 return 0;
312 }
313
314 gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
315 return monitor_bounds.right() - 1;
316 }
317
318 } // namespace views
319