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