• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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/accessibility/ax_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/gfx/text_constants.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 - constructors, destructors, initialization
43 //
44 ////////////////////////////////////////////////////////////////////////////////
45 
MenuButton(ButtonListener * listener,const base::string16 & text,MenuButtonListener * menu_button_listener,bool show_menu_marker)46 MenuButton::MenuButton(ButtonListener* listener,
47                        const base::string16& text,
48                        MenuButtonListener* menu_button_listener,
49                        bool show_menu_marker)
50     : LabelButton(listener, text),
51       menu_visible_(false),
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);
59 }
60 
~MenuButton()61 MenuButton::~MenuButton() {
62   if (destroyed_flag_)
63     *destroyed_flag_ = true;
64 }
65 
66 ////////////////////////////////////////////////////////////////////////////////
67 //
68 // MenuButton - Public APIs
69 //
70 ////////////////////////////////////////////////////////////////////////////////
71 
Activate()72 bool MenuButton::Activate() {
73   SetState(STATE_PRESSED);
74   if (listener_) {
75     gfx::Rect lb = GetLocalBounds();
76 
77     // The position of the menu depends on whether or not the locale is
78     // right-to-left.
79     gfx::Point menu_position(lb.right(), lb.bottom());
80     if (base::i18n::IsRTL())
81       menu_position.set_x(lb.x());
82 
83     View::ConvertPointToScreen(this, &menu_position);
84     if (base::i18n::IsRTL())
85       menu_position.Offset(-menu_offset_.x(), menu_offset_.y());
86     else
87       menu_position.Offset(menu_offset_.x(), menu_offset_.y());
88 
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);
92 
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
99     // to NULL.
100     static_cast<internal::RootView*>(GetWidget()->GetRootView())->
101         SetMouseHandler(NULL);
102 
103     menu_visible_ = true;
104 
105     bool destroyed = false;
106     destroyed_flag_ = &destroyed;
107 
108     listener_->OnMenuButtonClicked(this, menu_position);
109 
110     if (destroyed) {
111       // The menu was deleted while showing. Don't attempt any processing.
112       return false;
113     }
114 
115     destroyed_flag_ = NULL;
116 
117     menu_visible_ = false;
118     menu_closed_time_ = TimeTicks::Now();
119 
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);
128 
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
131     // target.
132     return false;
133   }
134   return true;
135 }
136 
OnPaint(gfx::Canvas * canvas)137 void MenuButton::OnPaint(gfx::Canvas* canvas) {
138   LabelButton::OnPaint(canvas);
139 
140   if (show_menu_marker_)
141     PaintMenuMarker(canvas);
142 }
143 
144 ////////////////////////////////////////////////////////////////////////////////
145 //
146 // MenuButton - Events
147 //
148 ////////////////////////////////////////////////////////////////////////////////
149 
GetPreferredSize() const150 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,
155                      0);
156   }
157   return prefsize;
158 }
159 
GetClassName() const160 const char* MenuButton::GetClassName() const {
161   return kViewClassName;
162 }
163 
OnMousePressed(const ui::MouseEvent & event)164 bool MenuButton::OnMousePressed(const ui::MouseEvent& event) {
165   RequestFocus();
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)
174         return Activate();
175     }
176   }
177   return true;
178 }
179 
OnMouseReleased(const ui::MouseEvent & event)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())) {
188     Activate();
189   } else {
190     LabelButton::OnMouseReleased(event);
191   }
192 }
193 
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.
OnMouseExited(const ui::MouseEvent & event)199 void MenuButton::OnMouseExited(const ui::MouseEvent& event) {
200   if ((state_ != STATE_DISABLED) && (!menu_visible_) && (!InDrag())) {
201     SetState(STATE_NORMAL);
202   }
203 }
204 
OnGestureEvent(ui::GestureEvent * event)205 void MenuButton::OnGestureEvent(ui::GestureEvent* event) {
206   if (state() != STATE_DISABLED && event->type() == ui::ET_GESTURE_TAP &&
207       !Activate()) {
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.
211     return;
212   }
213   LabelButton::OnGestureEvent(event);
214 }
215 
OnKeyPressed(const ui::KeyEvent & event)216 bool MenuButton::OnKeyPressed(const ui::KeyEvent& event) {
217   switch (event.key_code()) {
218     case ui::VKEY_SPACE:
219       // Alt-space on windows should show the window menu.
220       if (event.IsAltDown())
221         break;
222     case ui::VKEY_RETURN:
223     case ui::VKEY_UP:
224     case ui::VKEY_DOWN: {
225       // WARNING: we may have been deleted by the time Activate returns.
226       Activate();
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.
231       return true;
232     }
233     default:
234       break;
235   }
236   return false;
237 }
238 
OnKeyReleased(const ui::KeyEvent & event)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.
243   return false;
244 }
245 
GetAccessibleState(ui::AXViewState * state)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);
251 }
252 
PaintMenuMarker(gfx::Canvas * canvas)253 void MenuButton::PaintMenuMarker(gfx::Canvas* canvas) {
254   gfx::Insets insets = GetInsets();
255 
256   // We can not use the views' mirroring infrastructure for mirroring a
257   // MenuButton control (see TextButton::OnPaint() for a detailed explanation
258   // regarding why we can not flip the canvas). Therefore, we need to
259   // manually mirror the position of the down arrow.
260   gfx::Rect arrow_bounds(width() - insets.right() -
261                          menu_marker_->width() - kMenuMarkerPaddingRight,
262                          height() / 2 - menu_marker_->height() / 2,
263                          menu_marker_->width(),
264                          menu_marker_->height());
265   arrow_bounds.set_x(GetMirroredXForRect(arrow_bounds));
266   canvas->DrawImageInt(*menu_marker_, arrow_bounds.x(), arrow_bounds.y());
267 }
268 
GetChildAreaBounds()269 gfx::Rect MenuButton::GetChildAreaBounds() {
270   gfx::Size s = size();
271 
272   if (show_menu_marker_) {
273     s.set_width(s.width() - menu_marker_->width() - kMenuMarkerPaddingLeft -
274                 kMenuMarkerPaddingRight);
275   }
276 
277   return gfx::Rect(s);
278 }
279 
GetMaximumScreenXCoordinate()280 int MenuButton::GetMaximumScreenXCoordinate() {
281   if (!GetWidget()) {
282     NOTREACHED();
283     return 0;
284   }
285 
286   gfx::Rect monitor_bounds = GetWidget()->GetWorkAreaBoundsInScreen();
287   return monitor_bounds.right() - 1;
288 }
289 
290 }  // namespace views
291