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::PressedLock
44 ////////////////////////////////////////////////////////////////////////////////
46 MenuButton::PressedLock::PressedLock(MenuButton* menu_button)
47 : menu_button_(menu_button->weak_factory_.GetWeakPtr()) {
48 menu_button_->IncrementPressedLocked();
51 MenuButton::PressedLock::~PressedLock() {
52 if (menu_button_.get())
53 menu_button_->DecrementPressedLocked();
56 ////////////////////////////////////////////////////////////////////////////////
58 // MenuButton - constructors, destructors, initialization
60 ////////////////////////////////////////////////////////////////////////////////
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),
75 SetHorizontalAlignment(gfx::ALIGN_LEFT);
78 MenuButton::~MenuButton() {
80 *destroyed_flag_ = true;
83 ////////////////////////////////////////////////////////////////////////////////
85 // MenuButton - Public APIs
87 ////////////////////////////////////////////////////////////////////////////////
89 bool MenuButton::Activate() {
90 SetState(STATE_PRESSED);
92 gfx::Rect lb = GetLocalBounds();
94 // The position of the menu depends on whether or not the locale is
96 gfx::Point menu_position(lb.right(), lb.bottom());
97 if (base::i18n::IsRTL())
98 menu_position.set_x(lb.x());
100 View::ConvertPointToScreen(this, &menu_position);
101 if (base::i18n::IsRTL())
102 menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
104 menu_position.Offset(menu_offset_.x(), menu_offset_.y());
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);
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
117 static_cast<internal::RootView*>(GetWidget()->GetRootView())->
118 SetMouseHandler(NULL);
120 bool destroyed = false;
121 destroyed_flag_ = &destroyed;
123 // We don't set our state here. It's handled in the MenuController code or
124 // by our click listener.
126 listener_->OnMenuButtonClicked(this, menu_position);
129 // The menu was deleted while showing. Don't attempt any processing.
133 destroyed_flag_ = NULL;
135 menu_closed_time_ = TimeTicks::Now();
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
145 void MenuButton::OnPaint(gfx::Canvas* canvas) {
146 LabelButton::OnPaint(canvas);
148 if (show_menu_marker_)
149 PaintMenuMarker(canvas);
152 ////////////////////////////////////////////////////////////////////////////////
154 // MenuButton - Events
156 ////////////////////////////////////////////////////////////////////////////////
158 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,
168 const char* MenuButton::GetClassName() const {
169 return kViewClassName;
172 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
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)
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())) {
198 LabelButton::OnMouseReleased(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);
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);
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);
217 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
218 if (state() != STATE_DISABLED && event->type() == ui::ET_GESTURE_TAP &&
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.
225 LabelButton::OnGestureEvent(event);
228 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
229 switch (event.key_code()) {
231 // Alt-space on windows should show the window menu.
232 if (event.IsAltDown())
234 case ui::VKEY_RETURN:
236 case ui::VKEY_DOWN: {
237 // WARNING: we may have been deleted by the time Activate returns.
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.
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.
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);
265 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
266 gfx::Insets insets = GetInsets();
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());
279 gfx::Rect MenuButton::GetChildAreaBounds() {
280 gfx::Size s = size();
282 if (show_menu_marker_) {
283 s.set_width(s.width() - menu_marker_->width() - kMenuMarkerPaddingLeft -
284 kMenuMarkerPaddingRight);
290 void MenuButton::IncrementPressedLocked() {
291 ++pressed_lock_count_;
292 SetState(STATE_PRESSED);
295 void MenuButton::DecrementPressedLocked() {
296 --pressed_lock_count_;
297 DCHECK_GE(pressed_lock_count_, 0);
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);
308 int MenuButton::GetMaximumScreenXCoordinate() {
314 gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
315 return monitor_bounds.right() - 1;