1 // Copyright 2013 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 "ash/shelf/shelf_button.h" 6 7 #include <algorithm> 8 9 #include "ash/ash_constants.h" 10 #include "ash/ash_switches.h" 11 #include "ash/shelf/shelf_button_host.h" 12 #include "ash/shelf/shelf_layout_manager.h" 13 #include "grit/ash_resources.h" 14 #include "skia/ext/image_operations.h" 15 #include "ui/base/accessibility/accessible_view_state.h" 16 #include "ui/base/resource/resource_bundle.h" 17 #include "ui/compositor/layer.h" 18 #include "ui/compositor/scoped_layer_animation_settings.h" 19 #include "ui/events/event_constants.h" 20 #include "ui/gfx/animation/animation_delegate.h" 21 #include "ui/gfx/animation/throb_animation.h" 22 #include "ui/gfx/canvas.h" 23 #include "ui/gfx/image/image.h" 24 #include "ui/gfx/image/image_skia_operations.h" 25 #include "ui/gfx/skbitmap_operations.h" 26 #include "ui/views/controls/image_view.h" 27 28 namespace { 29 30 // Size of the bar. This is along the opposite axis of the shelf. For example, 31 // if the shelf is aligned horizontally then this is the height of the bar. 32 const int kBarSize = 3; 33 const int kIconSize = 32; 34 const int kHopSpacing = 2; 35 const int kIconPad = 8; 36 const int kAlternateIconPad = 5; 37 const int kAlternateIconPadVertical = 6; 38 const int kHopUpMS = 0; 39 const int kHopDownMS = 200; 40 const int kAttentionThrobDurationMS = 800; 41 42 bool ShouldHop(int state) { 43 return state & ash::internal::ShelfButton::STATE_HOVERED || 44 state & ash::internal::ShelfButton::STATE_ACTIVE || 45 state & ash::internal::ShelfButton::STATE_FOCUSED; 46 } 47 48 // Simple AnimationDelegate that owns a single ThrobAnimation instance to 49 // keep all Draw Attention animations in sync. 50 class ShelfButtonAnimation : public gfx::AnimationDelegate { 51 public: 52 class Observer { 53 public: 54 virtual void AnimationProgressed() = 0; 55 56 protected: 57 virtual ~Observer() {} 58 }; 59 60 static ShelfButtonAnimation* GetInstance() { 61 static ShelfButtonAnimation* s_instance = new ShelfButtonAnimation(); 62 return s_instance; 63 } 64 65 void AddObserver(Observer* observer) { 66 observers_.AddObserver(observer); 67 } 68 69 void RemoveObserver(Observer* observer) { 70 observers_.RemoveObserver(observer); 71 if (!observers_.might_have_observers()) 72 animation_.Stop(); 73 } 74 75 int GetAlpha() { 76 return GetThrobAnimation().CurrentValueBetween(0, 255); 77 } 78 79 double GetAnimation() { 80 return GetThrobAnimation().GetCurrentValue(); 81 } 82 83 private: 84 ShelfButtonAnimation() 85 : animation_(this) { 86 animation_.SetThrobDuration(kAttentionThrobDurationMS); 87 animation_.SetTweenType(gfx::Tween::SMOOTH_IN_OUT); 88 } 89 90 virtual ~ShelfButtonAnimation() { 91 } 92 93 gfx::ThrobAnimation& GetThrobAnimation() { 94 if (!animation_.is_animating()) { 95 animation_.Reset(); 96 animation_.StartThrobbing(-1 /*throb indefinitely*/); 97 } 98 return animation_; 99 } 100 101 // gfx::AnimationDelegate 102 virtual void AnimationProgressed(const gfx::Animation* animation) OVERRIDE { 103 if (animation != &animation_) 104 return; 105 if (!animation_.is_animating()) 106 return; 107 FOR_EACH_OBSERVER(Observer, observers_, AnimationProgressed()); 108 } 109 110 gfx::ThrobAnimation animation_; 111 ObserverList<Observer> observers_; 112 113 DISALLOW_COPY_AND_ASSIGN(ShelfButtonAnimation); 114 }; 115 116 } // namespace 117 118 namespace ash { 119 namespace internal { 120 121 //////////////////////////////////////////////////////////////////////////////// 122 // ShelfButton::BarView 123 124 class ShelfButton::BarView : public views::ImageView, 125 public ShelfButtonAnimation::Observer { 126 public: 127 BarView(ShelfButton* host) 128 : host_(host), 129 show_attention_(false) { 130 } 131 132 virtual ~BarView() { 133 if (show_attention_) 134 ShelfButtonAnimation::GetInstance()->RemoveObserver(this); 135 } 136 137 // View 138 virtual bool HitTestRect(const gfx::Rect& rect) const OVERRIDE { 139 // Allow Mouse...() messages to go to the parent view. 140 return false; 141 } 142 143 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE { 144 if (show_attention_) { 145 int alpha = ShelfButtonAnimation::GetInstance()->GetAlpha(); 146 canvas->SaveLayerAlpha(alpha); 147 views::ImageView::OnPaint(canvas); 148 canvas->Restore(); 149 } else { 150 views::ImageView::OnPaint(canvas); 151 } 152 } 153 154 // ShelfButtonAnimation::Observer 155 virtual void AnimationProgressed() OVERRIDE { 156 UpdateBounds(); 157 SchedulePaint(); 158 } 159 160 void SetBarBoundsRect(const gfx::Rect& bounds) { 161 base_bounds_ = bounds; 162 UpdateBounds(); 163 } 164 165 void ShowAttention(bool show) { 166 if (show_attention_ != show) { 167 show_attention_ = show; 168 if (show_attention_) 169 ShelfButtonAnimation::GetInstance()->AddObserver(this); 170 else 171 ShelfButtonAnimation::GetInstance()->RemoveObserver(this); 172 } 173 UpdateBounds(); 174 } 175 176 private: 177 void UpdateBounds() { 178 gfx::Rect bounds = base_bounds_; 179 if (show_attention_) { 180 // Scale from .35 to 1.0 of the total width (which is wider than the 181 // visible width of the image, so the animation "rests" briefly at full 182 // visible width. 183 double animation = ShelfButtonAnimation::GetInstance()->GetAnimation(); 184 double scale = (.35 + .65 * animation); 185 if (host_->shelf_layout_manager()->GetAlignment() == 186 SHELF_ALIGNMENT_BOTTOM) { 187 bounds.set_width(base_bounds_.width() * scale); 188 int x_offset = (base_bounds_.width() - bounds.width()) / 2; 189 bounds.set_x(base_bounds_.x() + x_offset); 190 } else { 191 bounds.set_height(base_bounds_.height() * scale); 192 int y_offset = (base_bounds_.height() - bounds.height()) / 2; 193 bounds.set_y(base_bounds_.y() + y_offset); 194 } 195 } 196 SetBoundsRect(bounds); 197 } 198 199 ShelfButton* host_; 200 bool show_attention_; 201 gfx::Rect base_bounds_; 202 203 DISALLOW_COPY_AND_ASSIGN(BarView); 204 }; 205 206 //////////////////////////////////////////////////////////////////////////////// 207 // ShelfButton::IconView 208 209 ShelfButton::IconView::IconView() : icon_size_(kIconSize) { 210 } 211 212 ShelfButton::IconView::~IconView() { 213 } 214 215 bool ShelfButton::IconView::HitTestRect(const gfx::Rect& rect) const { 216 // Return false so that ShelfButton gets all the mouse events. 217 return false; 218 } 219 220 //////////////////////////////////////////////////////////////////////////////// 221 // ShelfButton 222 223 ShelfButton* ShelfButton::Create(views::ButtonListener* listener, 224 ShelfButtonHost* host, 225 ShelfLayoutManager* shelf_layout_manager) { 226 ShelfButton* button = new ShelfButton(listener, host, shelf_layout_manager); 227 button->Init(); 228 return button; 229 } 230 231 ShelfButton::ShelfButton(views::ButtonListener* listener, 232 ShelfButtonHost* host, 233 ShelfLayoutManager* shelf_layout_manager) 234 : CustomButton(listener), 235 host_(host), 236 icon_view_(NULL), 237 bar_(new BarView(this)), 238 state_(STATE_NORMAL), 239 shelf_layout_manager_(shelf_layout_manager), 240 destroyed_flag_(NULL) { 241 SetAccessibilityFocusable(true); 242 243 const gfx::ShadowValue kShadows[] = { 244 gfx::ShadowValue(gfx::Point(0, 2), 0, SkColorSetARGB(0x1A, 0, 0, 0)), 245 gfx::ShadowValue(gfx::Point(0, 3), 1, SkColorSetARGB(0x1A, 0, 0, 0)), 246 gfx::ShadowValue(gfx::Point(0, 0), 1, SkColorSetARGB(0x54, 0, 0, 0)), 247 }; 248 icon_shadows_.assign(kShadows, kShadows + arraysize(kShadows)); 249 250 AddChildView(bar_); 251 } 252 253 ShelfButton::~ShelfButton() { 254 if (destroyed_flag_) 255 *destroyed_flag_ = true; 256 } 257 258 void ShelfButton::SetShadowedImage(const gfx::ImageSkia& image) { 259 icon_view_->SetImage(gfx::ImageSkiaOperations::CreateImageWithDropShadow( 260 image, icon_shadows_)); 261 } 262 263 void ShelfButton::SetImage(const gfx::ImageSkia& image) { 264 if (image.isNull()) { 265 // TODO: need an empty image. 266 icon_view_->SetImage(image); 267 return; 268 } 269 270 if (icon_view_->icon_size() == 0) { 271 SetShadowedImage(image); 272 return; 273 } 274 275 // Resize the image maintaining our aspect ratio. 276 int pref = icon_view_->icon_size(); 277 float aspect_ratio = 278 static_cast<float>(image.width()) / static_cast<float>(image.height()); 279 int height = pref; 280 int width = static_cast<int>(aspect_ratio * height); 281 if (width > pref) { 282 width = pref; 283 height = static_cast<int>(width / aspect_ratio); 284 } 285 286 if (width == image.width() && height == image.height()) { 287 SetShadowedImage(image); 288 return; 289 } 290 291 SetShadowedImage(gfx::ImageSkiaOperations::CreateResizedImage(image, 292 skia::ImageOperations::RESIZE_BEST, gfx::Size(width, height))); 293 } 294 295 const gfx::ImageSkia& ShelfButton::GetImage() const { 296 return icon_view_->GetImage(); 297 } 298 299 void ShelfButton::AddState(State state) { 300 if (!(state_ & state)) { 301 if (!ash::switches::UseAlternateShelfLayout() && 302 (ShouldHop(state) || !ShouldHop(state_))) { 303 ui::ScopedLayerAnimationSettings scoped_setter( 304 icon_view_->layer()->GetAnimator()); 305 scoped_setter.SetTransitionDuration( 306 base::TimeDelta::FromMilliseconds(kHopUpMS)); 307 } 308 state_ |= state; 309 Layout(); 310 if (state & STATE_ATTENTION) 311 bar_->ShowAttention(true); 312 } 313 } 314 315 void ShelfButton::ClearState(State state) { 316 if (state_ & state) { 317 if (!ash::switches::UseAlternateShelfLayout() && 318 (!ShouldHop(state) || ShouldHop(state_))) { 319 ui::ScopedLayerAnimationSettings scoped_setter( 320 icon_view_->layer()->GetAnimator()); 321 scoped_setter.SetTweenType(gfx::Tween::LINEAR); 322 scoped_setter.SetTransitionDuration( 323 base::TimeDelta::FromMilliseconds(kHopDownMS)); 324 } 325 state_ &= ~state; 326 Layout(); 327 if (state & STATE_ATTENTION) 328 bar_->ShowAttention(false); 329 } 330 } 331 332 gfx::Rect ShelfButton::GetIconBounds() const { 333 return icon_view_->bounds(); 334 } 335 336 void ShelfButton::ShowContextMenu(const gfx::Point& p, 337 ui::MenuSourceType source_type) { 338 if (!context_menu_controller()) 339 return; 340 341 bool destroyed = false; 342 destroyed_flag_ = &destroyed; 343 344 CustomButton::ShowContextMenu(p, source_type); 345 346 if (!destroyed) { 347 destroyed_flag_ = NULL; 348 // The menu will not propagate mouse events while its shown. To address, 349 // the hover state gets cleared once the menu was shown (and this was not 350 // destroyed). 351 ClearState(STATE_HOVERED); 352 } 353 } 354 355 bool ShelfButton::OnMousePressed(const ui::MouseEvent& event) { 356 CustomButton::OnMousePressed(event); 357 host_->PointerPressedOnButton(this, ShelfButtonHost::MOUSE, event); 358 return true; 359 } 360 361 void ShelfButton::OnMouseReleased(const ui::MouseEvent& event) { 362 CustomButton::OnMouseReleased(event); 363 host_->PointerReleasedOnButton(this, ShelfButtonHost::MOUSE, false); 364 } 365 366 void ShelfButton::OnMouseCaptureLost() { 367 ClearState(STATE_HOVERED); 368 host_->PointerReleasedOnButton(this, ShelfButtonHost::MOUSE, true); 369 CustomButton::OnMouseCaptureLost(); 370 } 371 372 bool ShelfButton::OnMouseDragged(const ui::MouseEvent& event) { 373 CustomButton::OnMouseDragged(event); 374 host_->PointerDraggedOnButton(this, ShelfButtonHost::MOUSE, event); 375 return true; 376 } 377 378 void ShelfButton::OnMouseMoved(const ui::MouseEvent& event) { 379 CustomButton::OnMouseMoved(event); 380 host_->MouseMovedOverButton(this); 381 } 382 383 void ShelfButton::OnMouseEntered(const ui::MouseEvent& event) { 384 AddState(STATE_HOVERED); 385 CustomButton::OnMouseEntered(event); 386 host_->MouseEnteredButton(this); 387 } 388 389 void ShelfButton::OnMouseExited(const ui::MouseEvent& event) { 390 ClearState(STATE_HOVERED); 391 CustomButton::OnMouseExited(event); 392 host_->MouseExitedButton(this); 393 } 394 395 void ShelfButton::GetAccessibleState(ui::AccessibleViewState* state) { 396 state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON; 397 state->name = host_->GetAccessibleName(this); 398 } 399 400 void ShelfButton::Layout() { 401 const gfx::Rect button_bounds(GetContentsBounds()); 402 int icon_pad = kIconPad; 403 if (ash::switches::UseAlternateShelfLayout()) { 404 icon_pad = 405 shelf_layout_manager_->GetAlignment() != SHELF_ALIGNMENT_BOTTOM ? 406 kAlternateIconPadVertical : kAlternateIconPad; 407 } 408 int x_offset = shelf_layout_manager_->PrimaryAxisValue(0, icon_pad); 409 int y_offset = shelf_layout_manager_->PrimaryAxisValue(icon_pad, 0); 410 411 int icon_width = std::min(kIconSize, 412 button_bounds.width() - x_offset); 413 int icon_height = std::min(kIconSize, 414 button_bounds.height() - y_offset); 415 416 // If on the left or top 'invert' the inset so the constant gap is on 417 // the interior (towards the center of display) edge of the shelf. 418 if (SHELF_ALIGNMENT_LEFT == shelf_layout_manager_->GetAlignment()) 419 x_offset = button_bounds.width() - (kIconSize + icon_pad); 420 421 if (SHELF_ALIGNMENT_TOP == shelf_layout_manager_->GetAlignment()) 422 y_offset = button_bounds.height() - (kIconSize + icon_pad); 423 424 if (ShouldHop(state_) && !ash::switches::UseAlternateShelfLayout()) { 425 x_offset += shelf_layout_manager_->SelectValueForShelfAlignment( 426 0, kHopSpacing, -kHopSpacing, 0); 427 y_offset += shelf_layout_manager_->SelectValueForShelfAlignment( 428 -kHopSpacing, 0, 0, kHopSpacing); 429 } 430 431 // Center icon with respect to the secondary axis, and ensure 432 // that the icon doesn't occlude the bar highlight. 433 if (shelf_layout_manager_->IsHorizontalAlignment()) { 434 x_offset = std::max(0, button_bounds.width() - icon_width) / 2; 435 if (y_offset + icon_height + kBarSize > button_bounds.height()) 436 icon_height = button_bounds.height() - (y_offset + kBarSize); 437 } else { 438 y_offset = std::max(0, button_bounds.height() - icon_height) / 2; 439 if (x_offset + icon_width + kBarSize > button_bounds.width()) 440 icon_width = button_bounds.width() - (x_offset + kBarSize); 441 } 442 443 icon_view_->SetBoundsRect(gfx::Rect( 444 button_bounds.x() + x_offset, 445 button_bounds.y() + y_offset, 446 icon_width, 447 icon_height)); 448 449 // Icon size has been incorrect when running 450 // PanelLayoutManagerTest.PanelAlignmentSecondDisplay on valgrind bot, see 451 // http://crbug.com/234854. 452 DCHECK_LE(icon_width, kIconSize); 453 DCHECK_LE(icon_height, kIconSize); 454 455 bar_->SetBarBoundsRect(button_bounds); 456 457 UpdateState(); 458 } 459 460 void ShelfButton::ChildPreferredSizeChanged(views::View* child) { 461 Layout(); 462 } 463 464 void ShelfButton::OnFocus() { 465 AddState(STATE_FOCUSED); 466 CustomButton::OnFocus(); 467 } 468 469 void ShelfButton::OnBlur() { 470 ClearState(STATE_FOCUSED); 471 CustomButton::OnBlur(); 472 } 473 474 void ShelfButton::OnPaint(gfx::Canvas* canvas) { 475 CustomButton::OnPaint(canvas); 476 if (HasFocus()) { 477 gfx::Rect paint_bounds(GetLocalBounds()); 478 paint_bounds.Inset(1, 1, 1, 1); 479 canvas->DrawSolidFocusRect(paint_bounds, kFocusBorderColor); 480 } 481 } 482 483 void ShelfButton::OnGestureEvent(ui::GestureEvent* event) { 484 switch (event->type()) { 485 case ui::ET_GESTURE_TAP_DOWN: 486 AddState(STATE_HOVERED); 487 return CustomButton::OnGestureEvent(event); 488 case ui::ET_GESTURE_END: 489 ClearState(STATE_HOVERED); 490 return CustomButton::OnGestureEvent(event); 491 case ui::ET_GESTURE_SCROLL_BEGIN: 492 host_->PointerPressedOnButton(this, ShelfButtonHost::TOUCH, *event); 493 event->SetHandled(); 494 return; 495 case ui::ET_GESTURE_SCROLL_UPDATE: 496 host_->PointerDraggedOnButton(this, ShelfButtonHost::TOUCH, *event); 497 event->SetHandled(); 498 return; 499 case ui::ET_GESTURE_SCROLL_END: 500 case ui::ET_SCROLL_FLING_START: 501 host_->PointerReleasedOnButton(this, ShelfButtonHost::TOUCH, false); 502 event->SetHandled(); 503 return; 504 default: 505 return CustomButton::OnGestureEvent(event); 506 } 507 } 508 509 void ShelfButton::Init() { 510 icon_view_ = CreateIconView(); 511 512 // TODO: refactor the layers so each button doesn't require 2. 513 icon_view_->SetPaintToLayer(true); 514 icon_view_->SetFillsBoundsOpaquely(false); 515 icon_view_->SetHorizontalAlignment(views::ImageView::CENTER); 516 icon_view_->SetVerticalAlignment(views::ImageView::LEADING); 517 518 AddChildView(icon_view_); 519 } 520 521 ShelfButton::IconView* ShelfButton::CreateIconView() { 522 return new IconView; 523 } 524 525 bool ShelfButton::IsShelfHorizontal() const { 526 return shelf_layout_manager_->IsHorizontalAlignment(); 527 } 528 529 void ShelfButton::UpdateState() { 530 UpdateBar(); 531 532 icon_view_->SetHorizontalAlignment( 533 shelf_layout_manager_->PrimaryAxisValue(views::ImageView::CENTER, 534 views::ImageView::LEADING)); 535 icon_view_->SetVerticalAlignment( 536 shelf_layout_manager_->PrimaryAxisValue(views::ImageView::LEADING, 537 views::ImageView::CENTER)); 538 SchedulePaint(); 539 } 540 541 void ShelfButton::UpdateBar() { 542 if (state_ & STATE_HIDDEN) { 543 bar_->SetVisible(false); 544 return; 545 } 546 547 int bar_id = 0; 548 if (ash::switches::UseAlternateShelfLayout()) { 549 if (state_ & STATE_ACTIVE) 550 bar_id = IDR_AURA_LAUNCHER_UNDERLINE_ACTIVE_ALTERNATE; 551 else if (state_ & STATE_RUNNING) 552 bar_id = IDR_AURA_LAUNCHER_UNDERLINE_RUNNING_ALTERNATE; 553 } else { 554 if (state_ & (STATE_ACTIVE | STATE_ATTENTION)) 555 bar_id = IDR_AURA_LAUNCHER_UNDERLINE_ACTIVE; 556 else if (state_ & (STATE_HOVERED | STATE_FOCUSED)) 557 bar_id = IDR_AURA_LAUNCHER_UNDERLINE_HOVER; 558 else 559 bar_id = IDR_AURA_LAUNCHER_UNDERLINE_RUNNING; 560 } 561 562 if (bar_id != 0) { 563 ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 564 const gfx::ImageSkia* image = rb.GetImageNamed(bar_id).ToImageSkia(); 565 if (shelf_layout_manager_->GetAlignment() == SHELF_ALIGNMENT_BOTTOM) { 566 bar_->SetImage(*image); 567 } else { 568 bar_->SetImage(gfx::ImageSkiaOperations::CreateRotatedImage(*image, 569 shelf_layout_manager_->SelectValueForShelfAlignment( 570 SkBitmapOperations::ROTATION_90_CW, 571 SkBitmapOperations::ROTATION_90_CW, 572 SkBitmapOperations::ROTATION_270_CW, 573 SkBitmapOperations::ROTATION_180_CW))); 574 } 575 bar_->SetHorizontalAlignment( 576 shelf_layout_manager_->SelectValueForShelfAlignment( 577 views::ImageView::CENTER, 578 views::ImageView::LEADING, 579 views::ImageView::TRAILING, 580 views::ImageView::CENTER)); 581 bar_->SetVerticalAlignment( 582 shelf_layout_manager_->SelectValueForShelfAlignment( 583 views::ImageView::TRAILING, 584 views::ImageView::CENTER, 585 views::ImageView::CENTER, 586 views::ImageView::LEADING)); 587 bar_->SchedulePaint(); 588 } 589 590 bar_->SetVisible(bar_id != 0 && state_ != STATE_NORMAL); 591 } 592 593 } // namespace internal 594 } // namespace ash 595