OLD | NEW |
(Empty) | |
| 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/radial_menu/radial_menu_views.h" |
| 6 |
| 7 #include "base/time.h" |
| 8 #include "base/timer.h" |
| 9 #include "third_party/skia/include/core/SkCanvas.h" |
| 10 #include "third_party/skia/include/core/SkPaint.h" |
| 11 #include "third_party/skia/include/core/SkShader.h" |
| 12 #include "third_party/skia/include/core/SkXfermode.h" |
| 13 #include "third_party/skia/include/effects/SkGradientShader.h" |
| 14 #include "ui/base/animation/linear_animation.h" |
| 15 #include "ui/gfx/rect.h" |
| 16 #include "ui/gfx/screen.h" |
| 17 #include "ui/gfx/image/image_skia.h" |
| 18 #include "ui/views/controls/image_view.h" |
| 19 #include "ui/views/widget/widget.h" |
| 20 |
| 21 namespace views { |
| 22 |
| 23 namespace { |
| 24 |
| 25 // The animation framerate. |
| 26 static const int kFrameRateHz = 60; |
| 27 |
| 28 // The opacity values for menu items dependent on their state. |
| 29 static const float kHiddenOpacity = 0.0; |
| 30 static const float kNormalOpacity = 0.8; |
| 31 static const float kDisabledOpacity = 0.4; |
| 32 static const float kActiveOpacity = 1.0; |
| 33 |
| 34 // The size values for menu items dependent on their state. |
| 35 static const float kHiddenSize = 0.0; |
| 36 static const float kNormalSize = 1.0; |
| 37 static const float kActiveSize = 1.1; |
| 38 |
| 39 // The default stroke width around the wedges. |
| 40 static const int kStrokeWidth = 2; |
| 41 |
| 42 static SkColor kUpperGradient = 0xffe0e0e0; |
| 43 static SkColor kLowerGradient = 0xff808080; |
| 44 static SkColor kStrokeColor = 0xffffffff; |
| 45 static SkColor kTransparentBlack = 0x00000000; |
| 46 |
| 47 // How long to spend moving downwards and fading out after waiting. |
| 48 // (In this case 5 frames will be used for the animation). |
| 49 static const double kDefaultTransitionLengthMS = 5 * 1.0 / kFrameRateHz; |
| 50 static const double kDestructionTransitionLengthMS = 2 * 1.0 / kFrameRateHz; |
| 51 // Within 3 frames we want to change the offset. |
| 52 static const double kOffsetTransitionLengthMS = 3.0 / kFrameRateHz; |
| 53 |
| 54 |
| 55 // A class to animate a parameter over time - allowing overlapping changes. |
| 56 // The animation method is always linear. |
| 57 class ValueAnimator { |
| 58 public: |
| 59 ValueAnimator(double initial_value, double current_time) |
| 60 : start_time_(current_time), |
| 61 current_time_(current_time), |
| 62 end_time_(current_time), |
| 63 start_value_(initial_value), |
| 64 current_value_(initial_value), |
| 65 end_value_(initial_value) { } |
| 66 |
| 67 // Initiate an animation of the parameter - note that it will smoothly |
| 68 // change the value from its current state. |length| is the duration of |
| 69 // the animation in seconds beginning from now + |delay| (in seconds). |
| 70 // The |target_value| is the value which needs to be reached. |
| 71 // Note: animation requests without a real change get ignored. |
| 72 void SetAnimation(double length, double delay, double target_value); |
| 73 |
| 74 // Get the current value of the animator. |
| 75 double GetCurrentValue() { return current_value_; } |
| 76 |
| 77 // Advance to the new time - should get called before GetCurrentValue |
| 78 // gets called. |
| 79 bool AdvanceTime(double current_time); |
| 80 |
| 81 private: |
| 82 // The start, current and end time of the animation. |
| 83 double start_time_; |
| 84 double current_time_; |
| 85 double end_time_; |
| 86 // The start, current and end value of the animation. |
| 87 double start_value_; |
| 88 double current_value_; |
| 89 double end_value_; |
| 90 |
| 91 DISALLOW_COPY_AND_ASSIGN(ValueAnimator); |
| 92 }; |
| 93 |
| 94 void ValueAnimator::SetAnimation( |
| 95 double length, |
| 96 double delay, |
| 97 double target_value) { |
| 98 // Get the current value since we might be in the middle of an animation. |
| 99 start_value_ = current_value_; |
| 100 end_value_ = target_value; |
| 101 start_time_ = current_time_ + delay; |
| 102 end_time_ = current_time_ + delay + length; |
| 103 } |
| 104 |
| 105 bool ValueAnimator::AdvanceTime(double current_time) { |
| 106 current_time_ = current_time; |
| 107 |
| 108 if (current_time_ >= end_time_ && current_value_ == end_value_) |
| 109 return false; |
| 110 |
| 111 double value = start_value_; |
| 112 if (current_time >= end_time_) { |
| 113 value = end_value_; |
| 114 } else if (current_time > start_time_) { |
| 115 value = start_value_ + (end_value_ - start_value_) * |
| 116 (current_time_ - start_time_) / (end_time_ - start_time_); |
| 117 } |
| 118 |
| 119 if (current_value_ == value) |
| 120 return false; |
| 121 |
| 122 current_value_ = value; |
| 123 return true; |
| 124 } |
| 125 |
| 126 // The class which handles a menu item's drawing and animation. |
| 127 class RadialMenuItemVisual : public views::ImageView { |
| 128 public: |
| 129 explicit RadialMenuItemVisual(RadialMenuItem* info, |
| 130 double current_time, |
| 131 gfx::Rect bounds, |
| 132 int inner_radius, |
| 133 int outer_radius, |
| 134 int stroke_width, |
| 135 double center_degree, |
| 136 double span_degrees); |
| 137 ~RadialMenuItemVisual(); |
| 138 |
| 139 // Change the state of the item accordingly. |
| 140 void ShowMenuItem(); |
| 141 void SetMenuItemActive(); |
| 142 void HideMenuItem(); |
| 143 |
| 144 // Update the time. The function will return true if the menu item is |
| 145 // visible. |
| 146 bool AdvanceTime(double current_time); |
| 147 |
| 148 // Returns true if the segment is a separator. |
| 149 bool IsSeparator(); |
| 150 |
| 151 // Returns the orientation of the wedge. |
| 152 double center_degree() { return center_degree_ ;} |
| 153 |
| 154 // Return the type of menu. |
| 155 RadialMenuItem::RadialMenuItemType type() { |
| 156 return presentation_info_->Type(); |
| 157 } |
| 158 |
| 159 // The offset of the wedge to the designated place. |
| 160 void SetSliderOffset(const gfx::Point& offset); |
| 161 |
| 162 // Returns the current fractional size of this element. |
| 163 double RelativeSize(); |
| 164 |
| 165 // Get - set the last known slider position. |
| 166 void GetLastSliderValue(int& count, int& distance) { |
| 167 count = last_slider_count_; |
| 168 distance = last_slider_distance_; |
| 169 } |
| 170 void SetLastSliderValue(int count, int distance) { |
| 171 last_slider_count_ = count; |
| 172 last_slider_distance_ = distance; |
| 173 } |
| 174 |
| 175 // Get the associated window of this item. |
| 176 aura::Window* GetWindow() { |
| 177 if (popup_) |
| 178 return popup_->GetNativeWindow(); |
| 179 return NULL; |
| 180 } |
| 181 |
| 182 private: |
| 183 // Draw the wedge which is underneath the menu. |
| 184 void CreateWedgeImage(); |
| 185 |
| 186 void GetIconBounds( |
| 187 SkRect& bounds, int xMid, int yMid, double radians); |
| 188 |
| 189 // The bounds on the screen for the menu. |
| 190 gfx::Rect bounds_; |
| 191 |
| 192 // The widget for this menu element. If the widget is NULL it will is an |
| 193 // empty space in the menu. |
| 194 views::Widget* popup_; |
| 195 |
| 196 // The passed presentation information from the creator. |
| 197 RadialMenuItem* presentation_info_; |
| 198 |
| 199 // Animated parameters over time. Note: They might change the animation |
| 200 // while animating. |
| 201 scoped_ptr<ValueAnimator> opacity_; |
| 202 scoped_ptr<ValueAnimator> size_; |
| 203 |
| 204 scoped_ptr<ValueAnimator> slider_offset_x_; |
| 205 scoped_ptr<ValueAnimator> slider_offset_y_; |
| 206 |
| 207 // Our bitmap which contains the initial wedge image. |
| 208 SkBitmap* wedge_image_; |
| 209 |
| 210 // The graphical constants to draw the wedge. |
| 211 int inner_radius_; |
| 212 int outer_radius_; |
| 213 int stroke_width_; |
| 214 double center_degree_; |
| 215 double span_degrees_; |
| 216 // To avoid value / rounding instabilities we ignore changes in border areas |
| 217 // by remembering the previous return value. |
| 218 int last_slider_count_; |
| 219 int last_slider_distance_; |
| 220 |
| 221 DISALLOW_COPY_AND_ASSIGN(RadialMenuItemVisual); |
| 222 }; |
| 223 |
| 224 RadialMenuItemVisual::RadialMenuItemVisual( |
| 225 RadialMenuItem* item_info, |
| 226 double current_time, |
| 227 gfx::Rect bounds, |
| 228 int inner_radius, |
| 229 int outer_radius, |
| 230 int stroke_width, |
| 231 double center_degree, |
| 232 double span_degrees) |
| 233 : bounds_(bounds), |
| 234 popup_(NULL), |
| 235 presentation_info_(item_info), |
| 236 opacity_(NULL), |
| 237 size_(NULL), |
| 238 slider_offset_x_(NULL), |
| 239 slider_offset_y_(NULL), |
| 240 wedge_image_(NULL), |
| 241 inner_radius_(inner_radius), |
| 242 outer_radius_(outer_radius), |
| 243 stroke_width_(stroke_width), |
| 244 center_degree_(center_degree), |
| 245 span_degrees_(span_degrees), |
| 246 last_slider_count_(0), |
| 247 last_slider_distance_(0) { |
| 248 // Elements might be empty to show nothing (separators). |
| 249 if (presentation_info_->Type() == RadialMenuItem::RADIAL_BUTTON || |
| 250 presentation_info_->Type() == RadialMenuItem::RADIAL_SLIDER || |
| 251 presentation_info_->Type() == RadialMenuItem::RADIAL_DISABLED) { |
| 252 CreateWedgeImage(); |
| 253 SetImage(gfx::ImageSkia(*wedge_image_)); |
| 254 popup_ = new views::Widget; |
| 255 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP); |
| 256 params.transparent = true; |
| 257 params.accept_events = false; |
| 258 params.parent = NULL; |
| 259 popup_->Init(params); |
| 260 popup_->SetOpacity(0x00); |
| 261 popup_->SetContentsView(this); |
| 262 popup_->Show(); |
| 263 |
| 264 // Set the animation up. |
| 265 size_.reset(new ValueAnimator(0, current_time)); |
| 266 opacity_.reset(new ValueAnimator(kHiddenOpacity, current_time)); |
| 267 slider_offset_x_.reset(new ValueAnimator(0, current_time)); |
| 268 slider_offset_y_.reset(new ValueAnimator(0, current_time)); |
| 269 ShowMenuItem(); |
| 270 } |
| 271 } |
| 272 |
| 273 RadialMenuItemVisual::~RadialMenuItemVisual() { |
| 274 delete presentation_info_; |
| 275 |
| 276 if (!popup_) |
| 277 return; |
| 278 |
| 279 popup_->Close(); |
| 280 popup_ = NULL; |
| 281 |
| 282 delete wedge_image_; |
| 283 } |
| 284 |
| 285 void RadialMenuItemVisual::ShowMenuItem() { |
| 286 if (!popup_) |
| 287 return; |
| 288 |
| 289 size_->SetAnimation(kDefaultTransitionLengthMS, 0.0, kNormalSize); |
| 290 opacity_->SetAnimation(kDefaultTransitionLengthMS, 0.0, |
| 291 presentation_info_->Type() == RadialMenuItem::RADIAL_DISABLED ? |
| 292 kDisabledOpacity : kNormalOpacity); |
| 293 // Set the offset back to the origin. |
| 294 slider_offset_x_->SetAnimation(0, 0.0, 0); |
| 295 slider_offset_y_->SetAnimation(0, 0.0, 0); |
| 296 } |
| 297 |
| 298 void RadialMenuItemVisual::SetMenuItemActive() { |
| 299 if (!popup_) |
| 300 return; |
| 301 |
| 302 // Note: By not remembering that we were already activated the item, |
| 303 // we will re-activate the item several times which will create a non |
| 304 // linear smooth animation. |
| 305 size_->SetAnimation(kDefaultTransitionLengthMS, 0.0, kActiveSize); |
| 306 opacity_->SetAnimation(kDefaultTransitionLengthMS, 0.0, |
| 307 presentation_info_->Type() == RadialMenuItem::RADIAL_DISABLED ? |
| 308 kDisabledOpacity : kActiveOpacity); |
| 309 } |
| 310 |
| 311 void RadialMenuItemVisual::HideMenuItem() { |
| 312 if (!popup_) |
| 313 return; |
| 314 |
| 315 size_->SetAnimation(kDestructionTransitionLengthMS, 0.0, kHiddenSize); |
| 316 opacity_->SetAnimation(kDestructionTransitionLengthMS, 0.0, kHiddenOpacity); |
| 317 } |
| 318 |
| 319 void RadialMenuItemVisual::SetSliderOffset(const gfx::Point& offset) { |
| 320 slider_offset_x_->SetAnimation(kOffsetTransitionLengthMS, 0.0, offset.x()); |
| 321 slider_offset_y_->SetAnimation(kOffsetTransitionLengthMS, 0.0, offset.y()); |
| 322 } |
| 323 |
| 324 bool RadialMenuItemVisual::AdvanceTime(double current_time) { |
| 325 if (!popup_) |
| 326 return false; |
| 327 |
| 328 // Bring the items into view by growing them quickly. |
| 329 int xMid = bounds_.x() + bounds_.width() / 2; |
| 330 int yMid = bounds_.y() + bounds_.height() / 2; |
| 331 int xSize = bounds_.width() / 2; |
| 332 int ySize = bounds_.height() / 2; |
| 333 bool is_visible = false; |
| 334 |
| 335 bool offset_x_changed = slider_offset_x_->AdvanceTime(current_time); |
| 336 bool offset_y_changed = slider_offset_y_->AdvanceTime(current_time); |
| 337 if (size_->AdvanceTime(current_time) || |
| 338 offset_x_changed || offset_y_changed) { |
| 339 double current_size = size_->GetCurrentValue(); |
| 340 // Grow / shrink item from the center point. |
| 341 popup_->SetBounds(gfx::Rect( |
| 342 xMid - xSize * current_size + slider_offset_x_->GetCurrentValue(), |
| 343 yMid - ySize * current_size + slider_offset_y_->GetCurrentValue(), |
| 344 2 * xSize * current_size, 2 * ySize * current_size)); |
| 345 // When we see at least one pixel it is visible. |
| 346 is_visible = 2 * xSize * current_size > 1.0; |
| 347 if (is_visible) { |
| 348 // It is necessary to scale the image to the target size, otherwise |
| 349 // it would only get cropped. |
| 350 SetImageSize( |
| 351 gfx::Size(2 * xSize * current_size, 2 * ySize * current_size)); |
| 352 } |
| 353 } |
| 354 |
| 355 if (opacity_->AdvanceTime(current_time)) { |
| 356 unsigned char opacity = static_cast<unsigned char>( |
| 357 opacity_->GetCurrentValue() * 255.0); |
| 358 popup_->SetOpacity(opacity); |
| 359 } |
| 360 |
| 361 return is_visible; |
| 362 } |
| 363 |
| 364 bool RadialMenuItemVisual::IsSeparator() { |
| 365 return (popup_ == NULL ? true : false); |
| 366 } |
| 367 |
| 368 double RadialMenuItemVisual::RelativeSize() { |
| 369 return size_.get() ? size_->GetCurrentValue() : 1.0; |
| 370 } |
| 371 |
| 372 void RadialMenuItemVisual::CreateWedgeImage() { |
| 373 wedge_image_ = new SkBitmap(); |
| 374 wedge_image_->setConfig(SkBitmap::kARGB_8888_Config, |
| 375 bounds_.width(), |
| 376 bounds_.height()); |
| 377 wedge_image_->allocPixels(); |
| 378 SkCanvas canvas(*wedge_image_); |
| 379 // Clear the bitmap fully transparent. |
| 380 canvas.drawColor(kTransparentBlack); |
| 381 |
| 382 // Draw the wedge. |
| 383 SkRect bounds; |
| 384 // Note: xMid and yMid does not need to be the outer_radius if there is a |
| 385 // smooth shadow involved below the wedges. |
| 386 double radians = (center_degree_ + 90) * M_PI / 180.0; |
| 387 int xMid = bounds_.width() / 2 + sin(radians) * 2 * stroke_width_; |
| 388 int yMid = bounds_.height() / 2 - cos(radians) * 2 * stroke_width_; |
| 389 bounds.set(xMid - outer_radius_, yMid - outer_radius_, |
| 390 xMid + outer_radius_, yMid + outer_radius_); |
| 391 |
| 392 SkPaint paint; |
| 393 paint.setColor(kStrokeColor); |
| 394 paint.setAntiAlias(true); |
| 395 paint.setStyle(SkPaint::kFill_Style); |
| 396 |
| 397 // A gradient background image, First point: top, second point: bottom. |
| 398 const SkPoint pts[] = { { 0, 0 }, { 0, bounds_.height() } }; |
| 399 const SkColor colors[] = { kUpperGradient, kLowerGradient }; |
| 400 SkShader* shader = SkGradientShader::CreateLinear( |
| 401 pts, colors, NULL, 2, SkShader::kClamp_TileMode); |
| 402 paint.setShader(shader); |
| 403 shader->unref(); |
| 404 canvas.drawArc(bounds, |
| 405 center_degree_ - span_degrees_ / 2, |
| 406 span_degrees_, |
| 407 true, |
| 408 paint); |
| 409 |
| 410 paint.setStyle(SkPaint::kStroke_Style); |
| 411 paint.setStrokeWidth(stroke_width_); |
| 412 paint.setShader(NULL); |
| 413 canvas.drawArc(bounds, |
| 414 center_degree_ - span_degrees_ / 2, |
| 415 span_degrees_, |
| 416 true, |
| 417 paint); |
| 418 |
| 419 // Erase the inner portion of the wedge again. |
| 420 if (inner_radius_) { |
| 421 // Erase inner wedge - need to use clear mode to get rid of alpha. |
| 422 paint.setXfermodeMode(SkXfermode::kClear_Mode); |
| 423 paint.setStyle(SkPaint::kStrokeAndFill_Style); |
| 424 paint.setColor(kTransparentBlack); |
| 425 paint.setAntiAlias(false); |
| 426 // Adjust our target area (keep part of the stroke area). |
| 427 // This is necessary to get the antialiased alpha pixels correct. |
| 428 bounds.set(xMid - inner_radius_ + stroke_width_ / 2, |
| 429 yMid - inner_radius_ + stroke_width_ / 2, |
| 430 xMid + inner_radius_ - stroke_width_ / 2, |
| 431 yMid + inner_radius_ - stroke_width_ / 2); |
| 432 // Extend span range to get rid of anti aliased pixels. |
| 433 canvas.drawArc(bounds, |
| 434 center_degree_ - span_degrees_ / 2 - 5, |
| 435 span_degrees_ + 10, |
| 436 true, |
| 437 paint); |
| 438 |
| 439 // To eliminate antialiased pixels in center area, we draw a tiny circle. |
| 440 int clear_radius = 2 * stroke_width_; |
| 441 if (inner_radius_ < clear_radius) |
| 442 clear_radius = inner_radius_; |
| 443 bounds.set(xMid - clear_radius, yMid - clear_radius, |
| 444 xMid + clear_radius, yMid + clear_radius); |
| 445 paint.setStyle(SkPaint::kFill_Style); |
| 446 canvas.drawArc(bounds, 0, 360, true, paint); |
| 447 |
| 448 // Draw the inner stroke to fill the outline. |
| 449 paint.setStyle(SkPaint::kStroke_Style); |
| 450 paint.setXfermodeMode(SkXfermode::kSrcOver_Mode); |
| 451 paint.setColor(kStrokeColor); |
| 452 paint.setAntiAlias(true); |
| 453 bounds.set(xMid - inner_radius_, yMid - inner_radius_, |
| 454 xMid + inner_radius_, yMid + inner_radius_); |
| 455 canvas.drawArc(bounds, |
| 456 center_degree_ - span_degrees_ / 2, |
| 457 span_degrees_, |
| 458 false, |
| 459 paint); |
| 460 } |
| 461 if (presentation_info_->Icon()) { |
| 462 // Draw the action icon into the wedge. |
| 463 SkPaint bitmap_paint; |
| 464 bitmap_paint.setFilterBitmap(true); |
| 465 bitmap_paint.setXfermodeMode(SkXfermode::kSrcOver_Mode); |
| 466 GetIconBounds(bounds, xMid, yMid, radians); |
| 467 canvas.drawBitmapRect( |
| 468 *(presentation_info_->Icon()->bitmap()), NULL, bounds, &bitmap_paint); |
| 469 } |
| 470 }; |
| 471 |
| 472 void RadialMenuItemVisual::GetIconBounds( |
| 473 SkRect& bounds, int xMid, int yMid, double radians) { |
| 474 // Get the mid point for the image for this wedge. |
| 475 int xMidIcon = xMid + sin(radians) * (inner_radius_ + outer_radius_) / 2; |
| 476 int yMidIcon = yMid - cos(radians) * (inner_radius_ + outer_radius_) / 2; |
| 477 |
| 478 // Determine the size of the image. |
| 479 // ___ |
| 480 // Assume we are looking at the wedge as it stands up like this: \_/ |
| 481 // There are two size limiting factors: |
| 482 // 1. The height as given by the inner and outer radius. |
| 483 int size_radius = outer_radius_ - inner_radius_; |
| 484 // 2. The width as given by the wedge "degree span". For simplicity we ignore |
| 485 // the fact that the wedge will be wider at the top then at the bottom. |
| 486 double half_width = sin(span_degrees_ / 2 * M_PI / 180.0); |
| 487 int size_midpoint = static_cast<int>(half_width * 2 * |
| 488 (outer_radius_ + inner_radius_) / 2); |
| 489 // Taking the minimum size we have found. |
| 490 int size = ((size_midpoint < size_radius) ? size_midpoint : size_radius); |
| 491 // Shrink this to three fourths to compensate for the shape and give spacing. |
| 492 size = (3 * size) / 4; |
| 493 int native_width = presentation_info_->Icon()->width(); |
| 494 int native_height = presentation_info_->Icon()->height(); |
| 495 double aspect = static_cast<double>(native_width) / |
| 496 static_cast<double>(native_height); |
| 497 // Finally we do not want to grow the image bigger then 2x its actual size. |
| 498 if (size > 2 * native_width) |
| 499 size = 2 * native_width; |
| 500 if (size > 2 * native_height) |
| 501 size = 2 * native_height; |
| 502 // Calculate the size to use taking the size as the maximum dimension and |
| 503 // using the aspect. |
| 504 int height = size; |
| 505 int width = size; |
| 506 if (aspect >= 1.0) |
| 507 height /= aspect; |
| 508 else |
| 509 width *= aspect; |
| 510 bounds.set(xMidIcon - width / 2, yMidIcon - height / 2, |
| 511 xMidIcon + width / 2, yMidIcon + height / 2); |
| 512 } |
| 513 |
| 514 // The implementation of the radial menu. |
| 515 class RadialMenuImpl : public RadialMenu { |
| 516 public: |
| 517 explicit RadialMenuImpl(const gfx::Point& location, |
| 518 int outer_radius, |
| 519 int inner_radius, |
| 520 double rotation, |
| 521 const RadialMenuItems items, |
| 522 int pointer_activation_area_extension); |
| 523 |
| 524 // Set the item with |index| to be the element which is active |
| 525 // (e.g. get hovered over). |
| 526 void SetActiveItem(int index); |
| 527 |
| 528 // Radial Menu implementation. |
| 529 virtual void CloseAndDelete(bool immediate_close) OVERRIDE; |
| 530 virtual bool HitMenuItemTest( |
| 531 const gfx::Point& location, int* item, int* count) OVERRIDE; |
| 532 virtual aura::Window* GetWindow() OVERRIDE; |
| 533 virtual void set_pointer_activation_area_extension(int expansion) OVERRIDE { |
| 534 pointer_activation_area_extension_ = expansion; |
| 535 } |
| 536 |
| 537 static RadialMenu* CreateRadialMenuInstance( |
| 538 const gfx::Point& location, |
| 539 int outer_radius, |
| 540 int inner_radius, |
| 541 double rotation, |
| 542 const RadialMenuItems items) OVERRIDE; |
| 543 static RadialMenuItem* CreateRadialMenuItemInstance( |
| 544 RadialMenuItem::RadialMenuItemType type, |
| 545 gfx::ImageSkia* icon, |
| 546 bool delete_icon) OVERRIDE; |
| 547 |
| 548 private: |
| 549 ~RadialMenuImpl(); |
| 550 |
| 551 // Updates the menu over time to the correct state. |
| 552 // (e.g. show / hide / hover / ..) |
| 553 void PeriodicAnimateMenu(); |
| 554 |
| 555 // Update the current time relative to the creation time. |
| 556 void UpdateCurrentTime(); |
| 557 |
| 558 // This are all menu items. The item at index 0 is the top most and then |
| 559 // they rotate clockwise from there. |
| 560 std::vector<RadialMenuItemVisual*> items_; |
| 561 |
| 562 // The screen location of the entire menu. |
| 563 gfx::Rect bounds_; |
| 564 |
| 565 // The outer and inner radius of the menu. |
| 566 int outer_radius_; |
| 567 int inner_radius_; |
| 568 int stroke_width_; |
| 569 // The whole menu got rotated by this many degrees. |
| 570 int rotation_; |
| 571 |
| 572 // The timer which is used for our events. |
| 573 base::RepeatingTimer<RadialMenuImpl> timer_; |
| 574 |
| 575 // The timer "interrupt" can be very imprecise. As such the correct time |
| 576 // gets calculated on use. |
| 577 double current_time_; |
| 578 const base::TimeTicks start_time_; |
| 579 |
| 580 // When the menu gets gradually removed from the screen this is true. |
| 581 bool delete_in_progress_; |
| 582 // When the menu deletion is performed this is true. This is put here to |
| 583 // avoid asynchronous animation calls coming in. |
| 584 bool deleting_now_; |
| 585 |
| 586 // If true the pointer needs to go exactly over the button to activate, |
| 587 // otherwise anything over or behind the button is fine. |
| 588 int pointer_activation_area_extension_; |
| 589 |
| 590 // The active item. A negative value means that none is active. |
| 591 int active_item_; |
| 592 |
| 593 DISALLOW_COPY_AND_ASSIGN(RadialMenuImpl); |
| 594 }; |
| 595 |
| 596 RadialMenuImpl::RadialMenuImpl(const gfx::Point& location, |
| 597 int outer_radius, |
| 598 int inner_radius, |
| 599 double rotation, |
| 600 const RadialMenuItems items, |
| 601 int pointer_activation_area_extension) |
| 602 : outer_radius_(outer_radius), |
| 603 inner_radius_(inner_radius), |
| 604 stroke_width_(kStrokeWidth), |
| 605 rotation_(rotation), |
| 606 current_time_(0.0), |
| 607 start_time_(base::TimeTicks::Now()), |
| 608 delete_in_progress_(false), |
| 609 deleting_now_(false), |
| 610 pointer_activation_area_extension_(pointer_activation_area_extension), |
| 611 active_item_(-1) { |
| 612 // We use 3 times the stroke width for: 1x for the stroke itself and 2x |
| 613 // to offset the wedges of each other (so that they don't touch). |
| 614 bounds_ = gfx::Rect(location.x() - outer_radius_ - 3 * stroke_width_, |
| 615 location.y() - outer_radius_ - 3 * stroke_width_, |
| 616 2 * (outer_radius_ + 3 * stroke_width_), |
| 617 2 * (outer_radius_ + 3 * stroke_width_)); |
| 618 // Each item spans this amount of degrees: |
| 619 double circular_span = 360.0 / items.size(); |
| 620 // We rotate the menu negative 90 degrees so that the first item is at the |
| 621 // top. |
| 622 rotation -= 90; |
| 623 // Populate the menu with items. |
| 624 for (size_t i = 0; i < items.size(); i++) { |
| 625 // Update the time since considerable time could have already passed. |
| 626 UpdateCurrentTime(); |
| 627 // Note: The -90 is to rotate the first segment to the top. |
| 628 items_.push_back(new RadialMenuItemVisual(items[i], |
| 629 current_time_, |
| 630 bounds_, |
| 631 inner_radius_, |
| 632 outer_radius_, |
| 633 stroke_width_, |
| 634 i * circular_span + rotation, |
| 635 circular_span)); |
| 636 } |
| 637 |
| 638 // Ping the animation code once to initialize. |
| 639 PeriodicAnimateMenu(); |
| 640 |
| 641 // Kick off our periodic timer to animate the menu. |
| 642 timer_.Start(FROM_HERE, |
| 643 base::TimeDelta::FromMilliseconds(1000 / kFrameRateHz), |
| 644 this, |
| 645 &RadialMenuImpl::PeriodicAnimateMenu); |
| 646 } |
| 647 |
| 648 void RadialMenuImpl::SetActiveItem(int index) { |
| 649 DCHECK(index < static_cast<unsigned char>(items_.size())); |
| 650 if (delete_in_progress_ || index == active_item_) |
| 651 return; |
| 652 |
| 653 if (active_item_ >= 0) |
| 654 items_[active_item_]->ShowMenuItem(); |
| 655 |
| 656 active_item_ = index; |
| 657 |
| 658 if (active_item_ >= 0) |
| 659 items_[active_item_]->SetMenuItemActive(); |
| 660 } |
| 661 |
| 662 void RadialMenuImpl::CloseAndDelete(bool immediate_close) { |
| 663 if (delete_in_progress_) |
| 664 return; |
| 665 |
| 666 delete_in_progress_ = true; |
| 667 |
| 668 if (immediate_close) { |
| 669 delete this; |
| 670 } else { |
| 671 // Fade away all segments of the menu. Once they are gone the menu will |
| 672 // destroy itself. |
| 673 for (size_t i = 0; i < items_.size(); i++) |
| 674 items_[i]->HideMenuItem(); |
| 675 } |
| 676 } |
| 677 |
| 678 bool RadialMenuImpl::HitMenuItemTest( |
| 679 const gfx::Point& location, int* returned_item, int* returned_count) { |
| 680 if (delete_in_progress_) |
| 681 return false; |
| 682 |
| 683 // Get the mid point. |
| 684 double x = bounds_.x() + bounds_.width() / 2; |
| 685 double y = bounds_.y() + bounds_.height() / 2; |
| 686 // The distance of the coordinates. |
| 687 double dx = x - location.x(); |
| 688 double dy = y - location.y(); |
| 689 |
| 690 double distance = sqrt(dx * dx + dy * dy); |
| 691 if (distance < inner_radius_) { |
| 692 SetActiveItem(-1); |
| 693 return false; |
| 694 } |
| 695 int segments = items_.size(); |
| 696 // Get the rotation of the point around the mid point. |
| 697 double degree = atan2(dy, dx) * 180 / M_PI; |
| 698 // The segments are offsetted by three rotations: |
| 699 // - 90 degree rotation to get the first element to the top. |
| 700 // - half a segment width to get to the start of the segment |
| 701 // + the callers rotation |
| 702 double first_start_offset = 90 - 360 / segments / 2 + rotation_; |
| 703 // Offset the retrieved rotation and limit it to positive degrees [0..360]. |
| 704 int adjusted_degree = |
| 705 static_cast<int>(3600 + degree - first_start_offset) % 360; |
| 706 int item = adjusted_degree / (360 / segments); |
| 707 // For normal buttons we want to only activate it when over the button. |
| 708 // For sliders we want to have the width a bit wider (thus two times the |
| 709 // with of a wedge circle). |
| 710 int step_width = (items_[item]->type() == RadialMenuItem::RADIAL_SLIDER) ? |
| 711 (2 * (outer_radius_ - inner_radius_)) : (outer_radius_ - inner_radius_); |
| 712 // We need to adjust the size relative to the size of the wedge. |
| 713 distance /= items_[item]->RelativeSize(); |
| 714 int count = ceil((distance - inner_radius_) / step_width); |
| 715 if (items_[item]->type() == RadialMenuItem::RADIAL_SLIDER) { |
| 716 // TODO(skuhne): Sliders are currently expected to be left / right only. |
| 717 // This can be changed by calculating the distance projected onto the moving |
| 718 // axis. |
| 719 DCHECK((static_cast<int>(items_[item]->center_degree()) + 360) % 180 == 90); |
| 720 if (distance < outer_radius_) |
| 721 items_[item]->SetSliderOffset(gfx::Point(0, 0)); |
| 722 else |
| 723 items_[item]->SetSliderOffset( |
| 724 gfx::Point(-(dx + (dx < 0 ? outer_radius_ : -outer_radius_)), 0)); |
| 725 } |
| 726 if (items_[item]->IsSeparator() || |
| 727 items_[item]->type() == RadialMenuItem::RADIAL_DISABLED || |
| 728 (count > pointer_activation_area_extension_ && |
| 729 items_[item]->type() != RadialMenuItem::RADIAL_SLIDER)) { |
| 730 SetActiveItem(-1); |
| 731 return false; |
| 732 } |
| 733 SetActiveItem(item); |
| 734 // Transfer the found return information. |
| 735 *returned_item = item; |
| 736 if (items_[item]->type() == RadialMenuItem::RADIAL_SLIDER) { |
| 737 int last_distance = 0; |
| 738 int last_count = 0; |
| 739 items_[item]->GetLastSliderValue(last_count, last_distance); |
| 740 if (last_count != count) { |
| 741 // To avoid value jittering we only accept changes with more then half |
| 742 // step delta. |
| 743 if (abs(last_distance - distance) < step_width / 2 && count) |
| 744 count = last_count; |
| 745 else |
| 746 items_[item]->SetLastSliderValue(count, distance); |
| 747 } |
| 748 *returned_count = count; |
| 749 } |
| 750 return true; |
| 751 } |
| 752 |
| 753 RadialMenuImpl::~RadialMenuImpl() { |
| 754 timer_.Stop(); |
| 755 // Delete all menu item related resources. |
| 756 while (items_.size()) { |
| 757 RadialMenuItemVisual* item = items_.back(); |
| 758 items_.pop_back(); |
| 759 delete item; |
| 760 } |
| 761 } |
| 762 |
| 763 void RadialMenuImpl::PeriodicAnimateMenu() { |
| 764 if (deleting_now_) |
| 765 return; |
| 766 |
| 767 // Since the interrupt is not really happening in "the accurate time", the |
| 768 // time needs to get calculated here. |
| 769 UpdateCurrentTime(); |
| 770 |
| 771 // We need to check if the menu is still at least partially visible. |
| 772 // This is required for self destruction when we were asked to shut down. |
| 773 bool is_visible = false; |
| 774 |
| 775 for (size_t i = 0; i < items_.size(); i++) |
| 776 is_visible |= items_[i]->AdvanceTime(current_time_); |
| 777 |
| 778 if (delete_in_progress_ && !is_visible) { |
| 779 deleting_now_ = true; |
| 780 delete this; |
| 781 } |
| 782 } |
| 783 |
| 784 void RadialMenuImpl::UpdateCurrentTime() { |
| 785 current_time_ = (base::TimeTicks::Now() - start_time_).InSecondsF(); |
| 786 } |
| 787 |
| 788 aura::Window* RadialMenuImpl::GetWindow() { |
| 789 for (size_t i = 0; i < items_.size(); i++) { |
| 790 if (items_[i]->GetWindow()) |
| 791 return items_[i]->GetWindow(); |
| 792 } |
| 793 return NULL; |
| 794 } |
| 795 |
| 796 // The class which represents the menu item structure which assembles the menu. |
| 797 class RadialMenuItemImpl: public RadialMenuItem { |
| 798 public: |
| 799 RadialMenuItemImpl( |
| 800 RadialMenuItemType type, gfx::ImageSkia* icon, bool delete_icon); |
| 801 |
| 802 virtual RadialMenuItemType Type() OVERRIDE { return type_; } |
| 803 virtual gfx::ImageSkia* Icon() OVERRIDE { return icon_; } |
| 804 virtual bool DeleteIcon() OVERRIDE {return delete_icon_; } |
| 805 |
| 806 virtual ~RadialMenuItemImpl(); |
| 807 |
| 808 private: |
| 809 // The type of the menu. |
| 810 RadialMenuItemType type_; |
| 811 // The image to use for this item. |
| 812 gfx::ImageSkia* icon_; |
| 813 // If true, the icon should get deleted on destruction. |
| 814 bool delete_icon_; |
| 815 }; |
| 816 |
| 817 RadialMenuItemImpl::RadialMenuItemImpl( |
| 818 RadialMenuItemType type, gfx::ImageSkia* icon, bool delete_icon) |
| 819 : type_(type), |
| 820 icon_(icon), |
| 821 delete_icon_(delete_icon) {} |
| 822 |
| 823 RadialMenuItemImpl::~RadialMenuItemImpl() { |
| 824 if (delete_icon_ && icon_) |
| 825 delete icon_; |
| 826 } |
| 827 |
| 828 } // namespace |
| 829 |
| 830 // static |
| 831 RadialMenu* RadialMenu::CreateRadialMenuInstance( |
| 832 const gfx::Point& location, |
| 833 int outer_radius, |
| 834 int inner_radius, |
| 835 double rotation, |
| 836 const RadialMenuItems items, |
| 837 int pointer_activation_area_extension, |
| 838 bool clip_menu_to_screen) { |
| 839 DCHECK(pointer_activation_area_extension >= 1); |
| 840 int min_radius = outer_radius; |
| 841 if (clip_menu_to_screen) { |
| 842 // Check that the menu stays within the visible screen by resizing if |
| 843 // necessary. |
| 844 gfx::Rect screen = gfx::Screen::GetDisplayNearestPoint(location).bounds(); |
| 845 if (location.x() - screen.x() < min_radius) |
| 846 min_radius = location.x() - screen.x(); |
| 847 if (location.y() - screen.y() < min_radius) |
| 848 min_radius = location.y() - screen.y(); |
| 849 if (screen.right() - location.x() < min_radius) |
| 850 min_radius = screen.right() - location.x(); |
| 851 if (screen.bottom() - location.y() < min_radius) |
| 852 min_radius = screen.bottom() - location.y(); |
| 853 if (min_radius < 48) |
| 854 min_radius = 48; |
| 855 inner_radius = (inner_radius * min_radius) / outer_radius; |
| 856 } |
| 857 |
| 858 // The animation will delete itself when it's finished or when the tab |
| 859 // contents is hidden or destroyed. |
| 860 return new RadialMenuImpl( |
| 861 location, |
| 862 min_radius, |
| 863 inner_radius, |
| 864 rotation, |
| 865 items, |
| 866 pointer_activation_area_extension); |
| 867 } |
| 868 |
| 869 // static |
| 870 RadialMenuItem* RadialMenuItem::CreateRadialMenuItemInstance( |
| 871 RadialMenuItem::RadialMenuItemType type, |
| 872 gfx::ImageSkia* icon, |
| 873 bool delete_icon) { |
| 874 return new RadialMenuItemImpl(type, icon, delete_icon); |
| 875 } |
| 876 |
| 877 } // namespace views |
OLD | NEW |