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 20namespace views { 21 22// How long the hover animation takes if uninterrupted. 23static const int kHoverFadeDurationMs = 150; 24 25// static 26const char CustomButton::kViewClassName[] = "CustomButton"; 27 28//////////////////////////////////////////////////////////////////////////////// 29// CustomButton, public: 30 31// static 32const CustomButton* CustomButton::AsCustomButton(const views::View* view) { 33 return AsCustomButton(const_cast<views::View*>(view)); 34} 35 36CustomButton* 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 51CustomButton::~CustomButton() { 52} 53 54void 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 81void CustomButton::StartThrobbing(int cycles_til_stop) { 82 is_throbbing_ = true; 83 hover_animation_->StartThrobbing(cycles_til_stop); 84} 85 86void CustomButton::StopThrobbing() { 87 if (hover_animation_->is_animating()) { 88 hover_animation_->Stop(); 89 SchedulePaint(); 90 } 91} 92 93void CustomButton::SetAnimationDuration(int duration) { 94 hover_animation_->SetSlideDuration(duration); 95} 96 97void 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 105bool CustomButton::IsHotTracked() const { 106 return state_ == STATE_HOVERED; 107} 108 109//////////////////////////////////////////////////////////////////////////////// 110// CustomButton, View overrides: 111 112void 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 122const char* CustomButton::GetClassName() const { 123 return kViewClassName; 124} 125 126bool 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 136bool 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 146void 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 163void 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 169void CustomButton::OnMouseEntered(const ui::MouseEvent& event) { 170 if (state_ != STATE_DISABLED) 171 SetState(STATE_HOVERED); 172} 173 174void 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 180void CustomButton::OnMouseMoved(const ui::MouseEvent& event) { 181 if (state_ != STATE_DISABLED) 182 SetState(HitTestPoint(event.location()) ? STATE_HOVERED : STATE_NORMAL); 183} 184 185bool 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 209bool 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 224void 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 253bool 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 269void 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 281void CustomButton::OnDragDone() { 282 SetState(STATE_NORMAL); 283} 284 285void 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 304void 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 313void CustomButton::AnimationProgressed(const gfx::Animation* animation) { 314 SchedulePaint(); 315} 316 317//////////////////////////////////////////////////////////////////////////////// 318// CustomButton, protected: 319 320CustomButton::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 331void CustomButton::StateChanged() { 332} 333 334bool 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 341bool CustomButton::ShouldEnterPushedState(const ui::Event& event) { 342 return IsTriggerableEvent(event); 343} 344 345//////////////////////////////////////////////////////////////////////////////// 346// CustomButton, View overrides (protected): 347 348void CustomButton::ViewHierarchyChanged( 349 const ViewHierarchyChangedDetails& details) { 350 if (!details.is_add && state_ != STATE_DISABLED) 351 SetState(STATE_NORMAL); 352} 353 354void CustomButton::OnBlur() { 355 if (IsHotTracked()) 356 SetState(STATE_NORMAL); 357} 358 359} // namespace views 360