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