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