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 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 60 MenuButton::~MenuButton() { 61 if (destroyed_flag_) 62 *destroyed_flag_ = true; 63 } 64 65 //////////////////////////////////////////////////////////////////////////////// 66 // 67 // MenuButton - Public APIs 68 // 69 //////////////////////////////////////////////////////////////////////////////// 70 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 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 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 159 const char* MenuButton::GetClassName() const { 160 return kViewClassName; 161 } 162 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 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. 198 void MenuButton::OnMouseExited(const ui::MouseEvent& event) { 199 if ((state_ != STATE_DISABLED) && (!menu_visible_) && (!InDrag())) { 200 SetState(STATE_NORMAL); 201 } 202 } 203 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 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 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 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 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 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