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 "grit/ui_resources.h" 14 #include "third_party/skia/include/core/SkCanvas.h" 15 #include "third_party/skia/include/core/SkColor.h" 16 #include "third_party/skia/include/core/SkPaint.h" 17 #include "ui/accessibility/ax_view_state.h" 18 #include "ui/base/resource/resource_bundle.h" 19 #include "ui/events/event.h" 20 #include "ui/gfx/animation/slide_animation.h" 21 #include "ui/gfx/canvas.h" 22 #include "ui/gfx/point.h" 23 #include "ui/gfx/rect.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 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 70 Slider::~Slider() { 71 } 72 73 void Slider::SetValue(float value) { 74 SetValueInternal(value, VALUE_CHANGED_BY_API); 75 } 76 77 void Slider::SetKeyboardIncrement(float increment) { 78 keyboard_increment_ = increment; 79 } 80 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 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 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 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 172 void Slider::SetAccessibleName(const base::string16& name) { 173 accessible_name_ = name; 174 } 175 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 189 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 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 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 278 bool Slider::OnMouseDragged(const ui::MouseEvent& event) { 279 MoveButtonTo(event.location()); 280 return true; 281 } 282 283 void Slider::OnMouseReleased(const ui::MouseEvent& event) { 284 OnSliderDragEnded(); 285 } 286 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 308 void Slider::OnFocus() { 309 View::OnFocus(); 310 SchedulePaint(); 311 } 312 313 void Slider::OnBlur() { 314 View::OnBlur(); 315 SchedulePaint(); 316 } 317 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 342 void Slider::AnimationProgressed(const gfx::Animation* animation) { 343 animating_value_ = animation->CurrentValueBetween(animating_value_, value_); 344 SchedulePaint(); 345 } 346 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 354 void Slider::OnSliderDragStarted() { 355 if (listener_) 356 listener_->SliderDragStarted(this); 357 } 358 359 void Slider::OnSliderDragEnded() { 360 if (listener_) 361 listener_->SliderDragEnded(this); 362 } 363 364 } // namespace views 365