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/slider.h"
6
7#include <algorithm>
8
9#include "base/logging.h"
10#include "base/message_loop/message_loop.h"
11#include "base/strings/stringprintf.h"
12#include "base/strings/utf_string_conversions.h"
13#include "third_party/skia/include/core/SkCanvas.h"
14#include "third_party/skia/include/core/SkColor.h"
15#include "third_party/skia/include/core/SkPaint.h"
16#include "ui/accessibility/ax_view_state.h"
17#include "ui/base/resource/resource_bundle.h"
18#include "ui/events/event.h"
19#include "ui/gfx/animation/slide_animation.h"
20#include "ui/gfx/canvas.h"
21#include "ui/gfx/point.h"
22#include "ui/gfx/rect.h"
23#include "ui/resources/grit/ui_resources.h"
24#include "ui/views/widget/widget.h"
25
26namespace {
27const int kSlideValueChangeDurationMS = 150;
28
29const int kBarImagesActive[] = {
30    IDR_SLIDER_ACTIVE_LEFT,
31    IDR_SLIDER_ACTIVE_CENTER,
32    IDR_SLIDER_PRESSED_CENTER,
33    IDR_SLIDER_PRESSED_RIGHT,
34};
35
36const int kBarImagesDisabled[] = {
37    IDR_SLIDER_DISABLED_LEFT,
38    IDR_SLIDER_DISABLED_CENTER,
39    IDR_SLIDER_DISABLED_CENTER,
40    IDR_SLIDER_DISABLED_RIGHT,
41};
42
43// The image chunks.
44enum BorderElements {
45  LEFT,
46  CENTER_LEFT,
47  CENTER_RIGHT,
48  RIGHT,
49};
50}  // namespace
51
52namespace views {
53
54Slider::Slider(SliderListener* listener, Orientation orientation)
55    : listener_(listener),
56      orientation_(orientation),
57      value_(0.f),
58      keyboard_increment_(0.1f),
59      animating_value_(0.f),
60      value_is_valid_(false),
61      accessibility_events_enabled_(true),
62      focus_border_color_(0),
63      bar_active_images_(kBarImagesActive),
64      bar_disabled_images_(kBarImagesDisabled) {
65  EnableCanvasFlippingForRTLUI(true);
66  SetFocusable(true);
67  UpdateState(true);
68}
69
70Slider::~Slider() {
71}
72
73void Slider::SetValue(float value) {
74  SetValueInternal(value, VALUE_CHANGED_BY_API);
75}
76
77void Slider::SetKeyboardIncrement(float increment) {
78  keyboard_increment_ = increment;
79}
80
81void Slider::SetValueInternal(float value, SliderChangeReason reason) {
82  bool old_value_valid = value_is_valid_;
83
84  value_is_valid_ = true;
85  if (value < 0.0)
86    value = 0.0;
87  else if (value > 1.0)
88    value = 1.0;
89  if (value_ == value)
90    return;
91  float old_value = value_;
92  value_ = value;
93  if (listener_)
94    listener_->SliderValueChanged(this, value_, old_value, reason);
95
96  if (old_value_valid && base::MessageLoop::current()) {
97    // Do not animate when setting the value of the slider for the first time.
98    // There is no message-loop when running tests. So we cannot animate then.
99    animating_value_ = old_value;
100    move_animation_.reset(new gfx::SlideAnimation(this));
101    move_animation_->SetSlideDuration(kSlideValueChangeDurationMS);
102    move_animation_->Show();
103    AnimationProgressed(move_animation_.get());
104  } else {
105    SchedulePaint();
106  }
107  if (accessibility_events_enabled_ && GetWidget()) {
108    NotifyAccessibilityEvent(
109        ui::AX_EVENT_VALUE_CHANGED, true);
110  }
111}
112
113void Slider::PrepareForMove(const gfx::Point& point) {
114  // Try to remember the position of the mouse cursor on the button.
115  gfx::Insets inset = GetInsets();
116  gfx::Rect content = GetContentsBounds();
117  float value = move_animation_.get() && move_animation_->is_animating() ?
118        animating_value_ : value_;
119
120  // For the horizontal orientation.
121  const int thumb_x = value * (content.width() - thumb_->width());
122  const int candidate_x = (base::i18n::IsRTL() ?
123      width() - (point.x() - inset.left()) :
124      point.x() - inset.left()) - thumb_x;
125  if (candidate_x >= 0 && candidate_x < thumb_->width())
126    initial_button_offset_.set_x(candidate_x);
127  else
128    initial_button_offset_.set_x(thumb_->width() / 2);
129
130  // For the vertical orientation.
131  const int thumb_y = (1.0 - value) * (content.height() - thumb_->height());
132  const int candidate_y = point.y() - thumb_y;
133  if (candidate_y >= 0 && candidate_y < thumb_->height())
134    initial_button_offset_.set_y(candidate_y);
135  else
136    initial_button_offset_.set_y(thumb_->height() / 2);
137}
138
139void Slider::MoveButtonTo(const gfx::Point& point) {
140  gfx::Insets inset = GetInsets();
141  // Calculate the value.
142  if (orientation_ == HORIZONTAL) {
143    int amount = base::i18n::IsRTL() ?
144        width() - inset.left() - point.x() - initial_button_offset_.x() :
145        point.x() - inset.left() - initial_button_offset_.x();
146    SetValueInternal(static_cast<float>(amount) /
147                         (width() - inset.width() - thumb_->width()),
148                     VALUE_CHANGED_BY_USER);
149  } else {
150    SetValueInternal(
151        1.0f - static_cast<float>(point.y() - initial_button_offset_.y()) /
152            (height() - thumb_->height()),
153        VALUE_CHANGED_BY_USER);
154  }
155}
156
157void Slider::UpdateState(bool control_on) {
158  ResourceBundle& rb = ResourceBundle::GetSharedInstance();
159  if (control_on) {
160    thumb_ = rb.GetImageNamed(IDR_SLIDER_ACTIVE_THUMB).ToImageSkia();
161    for (int i = 0; i < 4; ++i)
162      images_[i] = rb.GetImageNamed(bar_active_images_[i]).ToImageSkia();
163  } else {
164    thumb_ = rb.GetImageNamed(IDR_SLIDER_DISABLED_THUMB).ToImageSkia();
165    for (int i = 0; i < 4; ++i)
166      images_[i] = rb.GetImageNamed(bar_disabled_images_[i]).ToImageSkia();
167  }
168  bar_height_ = images_[LEFT]->height();
169  SchedulePaint();
170}
171
172void Slider::SetAccessibleName(const base::string16& name) {
173  accessible_name_ = name;
174}
175
176void Slider::OnPaintFocus(gfx::Canvas* canvas) {
177  if (!HasFocus())
178    return;
179
180  if (!focus_border_color_) {
181    canvas->DrawFocusRect(GetLocalBounds());
182  } else if (HasFocus()) {
183    canvas->DrawSolidFocusRect(
184        gfx::Rect(1, 1, width() - 3, height() - 3),
185        focus_border_color_);
186  }
187}
188
189gfx::Size Slider::GetPreferredSize() const {
190  const int kSizeMajor = 200;
191  const int kSizeMinor = 40;
192
193  if (orientation_ == HORIZONTAL)
194    return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor);
195  return gfx::Size(kSizeMinor, std::max(height(), kSizeMajor));
196}
197
198void Slider::OnPaint(gfx::Canvas* canvas) {
199  gfx::Rect content = GetContentsBounds();
200  float value = move_animation_.get() && move_animation_->is_animating() ?
201      animating_value_ : value_;
202  if (orientation_ == HORIZONTAL) {
203    // Paint slider bar with image resources.
204
205    // Inset the slider bar a little bit, so that the left or the right end of
206    // the slider bar will not be exposed under the thumb button when the thumb
207    // button slides to the left most or right most position.
208    const int kBarInsetX = 2;
209    int bar_width = content.width() - kBarInsetX * 2;
210    int bar_cy = content.height() / 2 - bar_height_ / 2;
211
212    int w = content.width() - thumb_->width();
213    int full = value * w;
214    int middle = std::max(full, images_[LEFT]->width());
215
216    canvas->Save();
217    canvas->Translate(gfx::Vector2d(kBarInsetX, bar_cy));
218    canvas->DrawImageInt(*images_[LEFT], 0, 0);
219    canvas->DrawImageInt(*images_[RIGHT],
220                         bar_width - images_[RIGHT]->width(),
221                         0);
222    canvas->TileImageInt(*images_[CENTER_LEFT],
223                         images_[LEFT]->width(),
224                         0,
225                         middle - images_[LEFT]->width(),
226                         bar_height_);
227    canvas->TileImageInt(*images_[CENTER_RIGHT],
228                         middle,
229                         0,
230                         bar_width - middle - images_[RIGHT]->width(),
231                         bar_height_);
232    canvas->Restore();
233
234    // Paint slider thumb.
235    int button_cx = content.x() + full;
236    int thumb_y = content.height() / 2 - thumb_->height() / 2;
237    canvas->DrawImageInt(*thumb_, button_cx, thumb_y);
238  } else {
239    // TODO(jennyz): draw vertical slider bar with resources.
240    // TODO(sad): The painting code should use NativeTheme for various
241    // platforms.
242    const int kButtonRadius = thumb_->width() / 2;
243    const int kLineThickness = bar_height_ / 2;
244    const SkColor kFullColor = SkColorSetARGB(125, 0, 0, 0);
245    const SkColor kEmptyColor = SkColorSetARGB(50, 0, 0, 0);
246
247    int h = content.height() - thumb_->height();
248    int full = value * h;
249    int empty = h - full;
250    int x = content.width() / 2 - kLineThickness / 2;
251    canvas->FillRect(gfx::Rect(x, content.y() + kButtonRadius,
252                               kLineThickness, empty),
253                     kEmptyColor);
254    canvas->FillRect(gfx::Rect(x, content.y() + empty + 2 * kButtonRadius,
255                               kLineThickness, full),
256                     kFullColor);
257
258    // TODO(mtomasz): We draw a thumb here because so far it is the same
259    // for horizontal and vertical orientations. If it is different, then
260    // we will need a separate resource.
261    int button_cy = content.y() + h - full;
262    int thumb_x = content.width() / 2 - thumb_->width() / 2;
263    canvas->DrawImageInt(*thumb_, thumb_x, button_cy);
264  }
265  View::OnPaint(canvas);
266  OnPaintFocus(canvas);
267}
268
269bool Slider::OnMousePressed(const ui::MouseEvent& event) {
270  if (!event.IsOnlyLeftMouseButton())
271    return false;
272  OnSliderDragStarted();
273  PrepareForMove(event.location());
274  MoveButtonTo(event.location());
275  return true;
276}
277
278bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
279  MoveButtonTo(event.location());
280  return true;
281}
282
283void Slider::OnMouseReleased(const ui::MouseEvent& event) {
284  OnSliderDragEnded();
285}
286
287bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
288  if (orientation_ == HORIZONTAL) {
289    if (event.key_code() == ui::VKEY_LEFT) {
290      SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
291      return true;
292    } else if (event.key_code() == ui::VKEY_RIGHT) {
293      SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
294      return true;
295    }
296  } else {
297    if (event.key_code() == ui::VKEY_DOWN) {
298      SetValueInternal(value_ - keyboard_increment_, VALUE_CHANGED_BY_USER);
299      return true;
300    } else if (event.key_code() == ui::VKEY_UP) {
301      SetValueInternal(value_ + keyboard_increment_, VALUE_CHANGED_BY_USER);
302      return true;
303    }
304  }
305  return false;
306}
307
308void Slider::OnFocus() {
309  View::OnFocus();
310  SchedulePaint();
311}
312
313void Slider::OnBlur() {
314  View::OnBlur();
315  SchedulePaint();
316}
317
318void Slider::OnGestureEvent(ui::GestureEvent* event) {
319  switch (event->type()) {
320    // In a multi point gesture only the touch point will generate
321    // an ET_GESTURE_TAP_DOWN event.
322    case ui::ET_GESTURE_TAP_DOWN:
323      OnSliderDragStarted();
324      PrepareForMove(event->location());
325      // Intentional fall through to next case.
326    case ui::ET_GESTURE_SCROLL_BEGIN:
327    case ui::ET_GESTURE_SCROLL_UPDATE:
328      MoveButtonTo(event->location());
329      event->SetHandled();
330      break;
331    case ui::ET_GESTURE_END:
332      MoveButtonTo(event->location());
333      event->SetHandled();
334      if (event->details().touch_points() <= 1)
335        OnSliderDragEnded();
336      break;
337    default:
338      break;
339  }
340}
341
342void Slider::AnimationProgressed(const gfx::Animation* animation) {
343  animating_value_ = animation->CurrentValueBetween(animating_value_, value_);
344  SchedulePaint();
345}
346
347void Slider::GetAccessibleState(ui::AXViewState* state) {
348  state->role = ui::AX_ROLE_SLIDER;
349  state->name = accessible_name_;
350  state->value = base::UTF8ToUTF16(
351      base::StringPrintf("%d%%", static_cast<int>(value_ * 100 + 0.5)));
352}
353
354void Slider::OnSliderDragStarted() {
355  if (listener_)
356    listener_->SliderDragStarted(this);
357}
358
359void Slider::OnSliderDragEnded() {
360  if (listener_)
361    listener_->SliderDragEnded(this);
362}
363
364}  // namespace views
365