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
26 namespace {
27 const int kSlideValueChangeDurationMS = 150;
28
29 const int kBarImagesActive[] = {
30 IDR_SLIDER_ACTIVE_LEFT,
31 IDR_SLIDER_ACTIVE_CENTER,
32 IDR_SLIDER_PRESSED_CENTER,
33 IDR_SLIDER_PRESSED_RIGHT,
34 };
35
36 const 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.
44 enum BorderElements {
45 LEFT,
46 CENTER_LEFT,
47 CENTER_RIGHT,
48 RIGHT,
49 };
50 } // namespace
51
52 namespace views {
53
Slider(SliderListener * listener,Orientation orientation)54 Slider::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
~Slider()70 Slider::~Slider() {
71 }
72
SetValue(float value)73 void Slider::SetValue(float value) {
74 SetValueInternal(value, VALUE_CHANGED_BY_API);
75 }
76
SetKeyboardIncrement(float increment)77 void Slider::SetKeyboardIncrement(float increment) {
78 keyboard_increment_ = increment;
79 }
80
SetValueInternal(float value,SliderChangeReason reason)81 void 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
PrepareForMove(const gfx::Point & point)113 void 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
MoveButtonTo(const gfx::Point & point)139 void 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
UpdateState(bool control_on)157 void 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
SetAccessibleName(const base::string16 & name)172 void Slider::SetAccessibleName(const base::string16& name) {
173 accessible_name_ = name;
174 }
175
OnPaintFocus(gfx::Canvas * canvas)176 void 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
GetPreferredSize() const189 gfx::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
OnPaint(gfx::Canvas * canvas)198 void 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
OnMousePressed(const ui::MouseEvent & event)269 bool 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
OnMouseDragged(const ui::MouseEvent & event)278 bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
279 MoveButtonTo(event.location());
280 return true;
281 }
282
OnMouseReleased(const ui::MouseEvent & event)283 void Slider::OnMouseReleased(const ui::MouseEvent& event) {
284 OnSliderDragEnded();
285 }
286
OnKeyPressed(const ui::KeyEvent & event)287 bool 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
OnFocus()308 void Slider::OnFocus() {
309 View::OnFocus();
310 SchedulePaint();
311 }
312
OnBlur()313 void Slider::OnBlur() {
314 View::OnBlur();
315 SchedulePaint();
316 }
317
OnGestureEvent(ui::GestureEvent * event)318 void 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
AnimationProgressed(const gfx::Animation * animation)342 void Slider::AnimationProgressed(const gfx::Animation* animation) {
343 animating_value_ = animation->CurrentValueBetween(animating_value_, value_);
344 SchedulePaint();
345 }
346
GetAccessibleState(ui::AXViewState * state)347 void 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
OnSliderDragStarted()354 void Slider::OnSliderDragStarted() {
355 if (listener_)
356 listener_->SliderDragStarted(this);
357 }
358
OnSliderDragEnded()359 void Slider::OnSliderDragEnded() {
360 if (listener_)
361 listener_->SliderDragEnded(this);
362 }
363
364 } // namespace views
365