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
25using base::TimeTicks;
26using base::TimeDelta;
27
28namespace views {
29
30// Default menu offset.
31static const int kDefaultMenuOffsetX = -2;
32static const int kDefaultMenuOffsetY = -4;
33
34// static
35const char MenuButton::kViewClassName[] = "MenuButton";
36const int MenuButton::kMenuMarkerPaddingLeft = 3;
37const int MenuButton::kMenuMarkerPaddingRight = -1;
38
39////////////////////////////////////////////////////////////////////////////////
40//
41// MenuButton - constructors, destructors, initialization
42//
43////////////////////////////////////////////////////////////////////////////////
44
45MenuButton::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
60MenuButton::~MenuButton() {
61  if (destroyed_flag_)
62    *destroyed_flag_ = true;
63}
64
65////////////////////////////////////////////////////////////////////////////////
66//
67// MenuButton - Public APIs
68//
69////////////////////////////////////////////////////////////////////////////////
70
71bool 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
136void 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
149gfx::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
159const char* MenuButton::GetClassName() const {
160  return kViewClassName;
161}
162
163bool 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
179void 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.
198void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
199  if ((state_ != STATE_DISABLED) && (!menu_visible_) && (!InDrag())) {
200    SetState(STATE_NORMAL);
201  }
202}
203
204void 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
213bool 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
240bool 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
247void 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
254void 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
270int 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