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