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/custom_button.h" 6 7 #include "ui/accessibility/ax_view_state.h" 8 #include "ui/events/event.h" 9 #include "ui/events/keycodes/keyboard_codes.h" 10 #include "ui/gfx/animation/throb_animation.h" 11 #include "ui/gfx/screen.h" 12 #include "ui/views/controls/button/blue_button.h" 13 #include "ui/views/controls/button/checkbox.h" 14 #include "ui/views/controls/button/image_button.h" 15 #include "ui/views/controls/button/label_button.h" 16 #include "ui/views/controls/button/menu_button.h" 17 #include "ui/views/controls/button/radio_button.h" 18 #include "ui/views/widget/widget.h" 19 20 namespace views { 21 22 // How long the hover animation takes if uninterrupted. 23 static const int kHoverFadeDurationMs = 150; 24 25 // static 26 const char CustomButton::kViewClassName[] = "CustomButton"; 27 28 //////////////////////////////////////////////////////////////////////////////// 29 // CustomButton, public: 30 31 // static 32 const CustomButton* CustomButton::AsCustomButton(const views::View* view) { 33 return AsCustomButton(const_cast<views::View*>(view)); 34 } 35 36 CustomButton* CustomButton::AsCustomButton(views::View* view) { 37 if (view) { 38 const char* classname = view->GetClassName(); 39 if (!strcmp(classname, Checkbox::kViewClassName) || 40 !strcmp(classname, CustomButton::kViewClassName) || 41 !strcmp(classname, ImageButton::kViewClassName) || 42 !strcmp(classname, LabelButton::kViewClassName) || 43 !strcmp(classname, RadioButton::kViewClassName) || 44 !strcmp(classname, MenuButton::kViewClassName)) { 45 return static_cast<CustomButton*>(view); 46 } 47 } 48 return NULL; 49 } 50 51 CustomButton::~CustomButton() { 52 } 53 54 void CustomButton::SetState(ButtonState state) { 55 if (state == state_) 56 return; 57 58 if (animate_on_state_change_ && 59 (!is_throbbing_ || !hover_animation_->is_animating())) { 60 is_throbbing_ = false; 61 if (state_ == STATE_NORMAL && state == STATE_HOVERED) { 62 // Button is hovered from a normal state, start hover animation. 63 hover_animation_->Show(); 64 } else if ((state_ == STATE_HOVERED || state_ == STATE_PRESSED) 65 && state == STATE_NORMAL) { 66 // Button is returning to a normal state from hover, start hover 67 // fade animation. 68 hover_animation_->Hide(); 69 } else { 70 hover_animation_->Stop(); 71 } 72 } 73 74 state_ = state; 75 StateChanged(); 76 if (state_changed_delegate_.get()) 77 state_changed_delegate_->StateChanged(state_); 78 SchedulePaint(); 79 } 80 81 void CustomButton::StartThrobbing(int cycles_til_stop) { 82 is_throbbing_ = true; 83 hover_animation_->StartThrobbing(cycles_til_stop); 84 } 85 86 void CustomButton::StopThrobbing() { 87 if (hover_animation_->is_animating()) { 88 hover_animation_->Stop(); 89 SchedulePaint(); 90 } 91 } 92 93 void CustomButton::SetAnimationDuration(int duration) { 94 hover_animation_->SetSlideDuration(duration); 95 } 96 97 void CustomButton::SetHotTracked(bool is_hot_tracked) { 98 if (state_ != STATE_DISABLED) 99 SetState(is_hot_tracked ? STATE_HOVERED : STATE_NORMAL); 100 101 if (is_hot_tracked) 102 NotifyAccessibilityEvent(ui::AX_EVENT_FOCUS, true); 103 } 104 105 bool CustomButton::IsHotTracked() const { 106 return state_ == STATE_HOVERED; 107 } 108 109 //////////////////////////////////////////////////////////////////////////////// 110 // CustomButton, View overrides: 111 112 void CustomButton::OnEnabledChanged() { 113 if (enabled() ? (state_ != STATE_DISABLED) : (state_ == STATE_DISABLED)) 114 return; 115 116 if (enabled()) 117 SetState(IsMouseHovered() ? STATE_HOVERED : STATE_NORMAL); 118 else 119 SetState(STATE_DISABLED); 120 } 121 122 const char* CustomButton::GetClassName() const { 123 return kViewClassName; 124 } 125 126 bool CustomButton::OnMousePressed(const ui::MouseEvent& event) { 127 if (state_ != STATE_DISABLED) { 128 if (ShouldEnterPushedState(event) && HitTestPoint(event.location())) 129 SetState(STATE_PRESSED); 130 if (request_focus_on_press_) 131 RequestFocus(); 132 } 133 return true; 134 } 135 136 bool CustomButton::OnMouseDragged(const ui::MouseEvent& event) { 137 if (state_ != STATE_DISABLED) { 138 if (HitTestPoint(event.location())) 139 SetState(ShouldEnterPushedState(event) ? STATE_PRESSED : STATE_HOVERED); 140 else 141 SetState(STATE_NORMAL); 142 } 143 return true; 144 } 145 146 void CustomButton::OnMouseReleased(const ui::MouseEvent& event) { 147 if (state_ == STATE_DISABLED) 148 return; 149 150 if (!HitTestPoint(event.location())) { 151 SetState(STATE_NORMAL); 152 return; 153 } 154 155 SetState(STATE_HOVERED); 156 if (IsTriggerableEvent(event)) { 157 NotifyClick(event); 158 // NOTE: We may be deleted at this point (by the listener's notification 159 // handler). 160 } 161 } 162 163 void CustomButton::OnMouseCaptureLost() { 164 // Starting a drag results in a MouseCaptureLost, we need to ignore it. 165 if (state_ != STATE_DISABLED && !InDrag()) 166 SetState(STATE_NORMAL); 167 } 168 169 void CustomButton::OnMouseEntered(const ui::MouseEvent& event) { 170 if (state_ != STATE_DISABLED) 171 SetState(STATE_HOVERED); 172 } 173 174 void CustomButton::OnMouseExited(const ui::MouseEvent& event) { 175 // Starting a drag results in a MouseExited, we need to ignore it. 176 if (state_ != STATE_DISABLED && !InDrag()) 177 SetState(STATE_NORMAL); 178 } 179 180 void CustomButton::OnMouseMoved(const ui::MouseEvent& event) { 181 if (state_ != STATE_DISABLED) 182 SetState(HitTestPoint(event.location()) ? STATE_HOVERED : STATE_NORMAL); 183 } 184 185 bool CustomButton::OnKeyPressed(const ui::KeyEvent& event) { 186 if (state_ == STATE_DISABLED) 187 return false; 188 189 // Space sets button state to pushed. Enter clicks the button. This matches 190 // the Windows native behavior of buttons, where Space clicks the button on 191 // KeyRelease and Enter clicks the button on KeyPressed. 192 if (event.key_code() == ui::VKEY_SPACE) { 193 SetState(STATE_PRESSED); 194 } else if (event.key_code() == ui::VKEY_RETURN) { 195 SetState(STATE_NORMAL); 196 // TODO(beng): remove once NotifyClick takes ui::Event. 197 ui::MouseEvent synthetic_event(ui::ET_MOUSE_RELEASED, 198 gfx::Point(), 199 gfx::Point(), 200 ui::EF_LEFT_MOUSE_BUTTON, 201 ui::EF_LEFT_MOUSE_BUTTON); 202 NotifyClick(synthetic_event); 203 } else { 204 return false; 205 } 206 return true; 207 } 208 209 bool CustomButton::OnKeyReleased(const ui::KeyEvent& event) { 210 if ((state_ == STATE_DISABLED) || (event.key_code() != ui::VKEY_SPACE)) 211 return false; 212 213 SetState(STATE_NORMAL); 214 // TODO(beng): remove once NotifyClick takes ui::Event. 215 ui::MouseEvent synthetic_event(ui::ET_MOUSE_RELEASED, 216 gfx::Point(), 217 gfx::Point(), 218 ui::EF_LEFT_MOUSE_BUTTON, 219 ui::EF_LEFT_MOUSE_BUTTON); 220 NotifyClick(synthetic_event); 221 return true; 222 } 223 224 void CustomButton::OnGestureEvent(ui::GestureEvent* event) { 225 if (state_ == STATE_DISABLED) { 226 Button::OnGestureEvent(event); 227 return; 228 } 229 230 if (event->type() == ui::ET_GESTURE_TAP && IsTriggerableEvent(*event)) { 231 // Set the button state to hot and start the animation fully faded in. The 232 // GESTURE_END event issued immediately after will set the state to 233 // STATE_NORMAL beginning the fade out animation. See 234 // http://crbug.com/131184. 235 SetState(STATE_HOVERED); 236 hover_animation_->Reset(1.0); 237 NotifyClick(*event); 238 event->StopPropagation(); 239 } else if (event->type() == ui::ET_GESTURE_TAP_DOWN && 240 ShouldEnterPushedState(*event)) { 241 SetState(STATE_PRESSED); 242 if (request_focus_on_press_) 243 RequestFocus(); 244 event->StopPropagation(); 245 } else if (event->type() == ui::ET_GESTURE_TAP_CANCEL || 246 event->type() == ui::ET_GESTURE_END) { 247 SetState(STATE_NORMAL); 248 } 249 if (!event->handled()) 250 Button::OnGestureEvent(event); 251 } 252 253 bool CustomButton::AcceleratorPressed(const ui::Accelerator& accelerator) { 254 SetState(STATE_NORMAL); 255 /* 256 ui::KeyEvent key_event(ui::ET_KEY_RELEASED, accelerator.key_code(), 257 accelerator.modifiers()); 258 */ 259 // TODO(beng): remove once NotifyClick takes ui::Event. 260 ui::MouseEvent synthetic_event(ui::ET_MOUSE_RELEASED, 261 gfx::Point(), 262 gfx::Point(), 263 ui::EF_LEFT_MOUSE_BUTTON, 264 ui::EF_LEFT_MOUSE_BUTTON); 265 NotifyClick(synthetic_event); 266 return true; 267 } 268 269 void CustomButton::ShowContextMenu(const gfx::Point& p, 270 ui::MenuSourceType source_type) { 271 if (!context_menu_controller()) 272 return; 273 274 // We're about to show the context menu. Showing the context menu likely means 275 // we won't get a mouse exited and reset state. Reset it now to be sure. 276 if (state_ != STATE_DISABLED) 277 SetState(STATE_NORMAL); 278 View::ShowContextMenu(p, source_type); 279 } 280 281 void CustomButton::OnDragDone() { 282 SetState(STATE_NORMAL); 283 } 284 285 void CustomButton::GetAccessibleState(ui::AXViewState* state) { 286 Button::GetAccessibleState(state); 287 switch (state_) { 288 case STATE_HOVERED: 289 state->AddStateFlag(ui::AX_STATE_HOVERED); 290 break; 291 case STATE_PRESSED: 292 state->AddStateFlag(ui::AX_STATE_PRESSED); 293 break; 294 case STATE_DISABLED: 295 state->AddStateFlag(ui::AX_STATE_DISABLED); 296 break; 297 case STATE_NORMAL: 298 case STATE_COUNT: 299 // No additional accessibility state set for this button state. 300 break; 301 } 302 } 303 304 void CustomButton::VisibilityChanged(View* starting_from, bool visible) { 305 if (state_ == STATE_DISABLED) 306 return; 307 SetState(visible && IsMouseHovered() ? STATE_HOVERED : STATE_NORMAL); 308 } 309 310 //////////////////////////////////////////////////////////////////////////////// 311 // CustomButton, gfx::AnimationDelegate implementation: 312 313 void CustomButton::AnimationProgressed(const gfx::Animation* animation) { 314 SchedulePaint(); 315 } 316 317 //////////////////////////////////////////////////////////////////////////////// 318 // CustomButton, protected: 319 320 CustomButton::CustomButton(ButtonListener* listener) 321 : Button(listener), 322 state_(STATE_NORMAL), 323 animate_on_state_change_(true), 324 is_throbbing_(false), 325 triggerable_event_flags_(ui::EF_LEFT_MOUSE_BUTTON), 326 request_focus_on_press_(true) { 327 hover_animation_.reset(new gfx::ThrobAnimation(this)); 328 hover_animation_->SetSlideDuration(kHoverFadeDurationMs); 329 } 330 331 void CustomButton::StateChanged() { 332 } 333 334 bool CustomButton::IsTriggerableEvent(const ui::Event& event) { 335 return event.type() == ui::ET_GESTURE_TAP_DOWN || 336 event.type() == ui::ET_GESTURE_TAP || 337 (event.IsMouseEvent() && 338 (triggerable_event_flags_ & event.flags()) != 0); 339 } 340 341 bool CustomButton::ShouldEnterPushedState(const ui::Event& event) { 342 return IsTriggerableEvent(event); 343 } 344 345 //////////////////////////////////////////////////////////////////////////////// 346 // CustomButton, View overrides (protected): 347 348 void CustomButton::ViewHierarchyChanged( 349 const ViewHierarchyChangedDetails& details) { 350 if (!details.is_add && state_ != STATE_DISABLED) 351 SetState(STATE_NORMAL); 352 } 353 354 void CustomButton::OnBlur() { 355 if (IsHotTracked()) 356 SetState(STATE_NORMAL); 357 } 358 359 } // namespace views 360