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 46 MenuButton::PressedLock::PressedLock(MenuButton* menu_button) 47 : menu_button_(menu_button->weak_factory_.GetWeakPtr()) { 48 menu_button_->IncrementPressedLocked(); 49 } 50 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 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 78 MenuButton::~MenuButton() { 79 if (destroyed_flag_) 80 *destroyed_flag_ = true; 81 } 82 83 //////////////////////////////////////////////////////////////////////////////// 84 // 85 // MenuButton - Public APIs 86 // 87 //////////////////////////////////////////////////////////////////////////////// 88 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 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 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, 163 0); 164 } 165 return prefsize; 166 } 167 168 const char* MenuButton::GetClassName() const { 169 return kViewClassName; 170 } 171 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 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 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 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 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 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 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 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 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 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 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 290 void MenuButton::IncrementPressedLocked() { 291 ++pressed_lock_count_; 292 SetState(STATE_PRESSED); 293 } 294 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 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