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