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/system/web_notification/web_notification_tray.h" 6 7 #include "ash/ash_switches.h" 8 #include "ash/root_window_controller.h" 9 #include "ash/shelf/shelf_layout_manager.h" 10 #include "ash/shelf/shelf_layout_manager_observer.h" 11 #include "ash/shelf/shelf_widget.h" 12 #include "ash/shell.h" 13 #include "ash/shell_window_ids.h" 14 #include "ash/system/status_area_widget.h" 15 #include "ash/system/tray/system_tray.h" 16 #include "ash/system/tray/tray_background_view.h" 17 #include "ash/system/tray/tray_bubble_wrapper.h" 18 #include "ash/system/tray/tray_constants.h" 19 #include "ash/system/tray/tray_utils.h" 20 #include "base/auto_reset.h" 21 #include "base/i18n/number_formatting.h" 22 #include "base/i18n/rtl.h" 23 #include "base/strings/utf_string_conversions.h" 24 #include "grit/ash_strings.h" 25 #include "grit/ui_strings.h" 26 #include "ui/aura/root_window.h" 27 #include "ui/aura/window.h" 28 #include "ui/base/l10n/l10n_util.h" 29 #include "ui/gfx/screen.h" 30 #include "ui/message_center/message_center_style.h" 31 #include "ui/message_center/message_center_tray_delegate.h" 32 #include "ui/message_center/message_center_util.h" 33 #include "ui/message_center/views/message_bubble_base.h" 34 #include "ui/message_center/views/message_center_bubble.h" 35 #include "ui/message_center/views/message_popup_collection.h" 36 #include "ui/views/bubble/tray_bubble_view.h" 37 #include "ui/views/controls/button/custom_button.h" 38 #include "ui/views/controls/image_view.h" 39 #include "ui/views/controls/label.h" 40 #include "ui/views/controls/menu/menu_runner.h" 41 #include "ui/views/layout/fill_layout.h" 42 43 #if defined(OS_CHROMEOS) 44 45 namespace message_center { 46 47 MessageCenterTrayDelegate* CreateMessageCenterTray() { 48 // On Windows+Ash the Tray will not be hosted in ash::Shell. 49 NOTREACHED(); 50 return NULL; 51 } 52 53 } // namespace message_center 54 55 #endif // defined(OS_CHROMEOS) 56 57 namespace ash { 58 namespace internal { 59 namespace { 60 61 const int kWebNotificationIconSize = 31; 62 // Height of the art assets used in alternate shelf layout, 63 // see ash/ash_switches.h:UseAlternateShelfLayout. 64 const int kWebNotificationAlternateSize = 38; 65 const SkColor kWebNotificationColorNoUnread = SkColorSetA(SK_ColorWHITE, 128); 66 const SkColor kWebNotificationColorWithUnread = SK_ColorWHITE; 67 68 } 69 70 // Observes the change of work area (including temporary change by auto-hide) 71 // and notifies MessagePopupCollection. 72 class WorkAreaObserver : public ShelfLayoutManagerObserver, 73 public ShellObserver { 74 public: 75 WorkAreaObserver(message_center::MessagePopupCollection* collection, 76 ShelfLayoutManager* shelf); 77 virtual ~WorkAreaObserver(); 78 79 void SetSystemTrayHeight(int height); 80 81 // Overridden from ShellObserver: 82 virtual void OnDisplayWorkAreaInsetsChanged() OVERRIDE; 83 84 // Overridden from ShelfLayoutManagerObserver: 85 virtual void OnAutoHideStateChanged(ShelfAutoHideState new_state) OVERRIDE; 86 87 private: 88 message_center::MessagePopupCollection* collection_; 89 ShelfLayoutManager* shelf_; 90 int system_tray_height_; 91 92 DISALLOW_COPY_AND_ASSIGN(WorkAreaObserver); 93 }; 94 95 WorkAreaObserver::WorkAreaObserver( 96 message_center::MessagePopupCollection* collection, 97 ShelfLayoutManager* shelf) 98 : collection_(collection), 99 shelf_(shelf), 100 system_tray_height_(0) { 101 DCHECK(collection_); 102 shelf_->AddObserver(this); 103 Shell::GetInstance()->AddShellObserver(this); 104 } 105 106 WorkAreaObserver::~WorkAreaObserver() { 107 Shell::GetInstance()->RemoveShellObserver(this); 108 shelf_->RemoveObserver(this); 109 } 110 111 void WorkAreaObserver::SetSystemTrayHeight(int height) { 112 system_tray_height_ = height; 113 if (system_tray_height_ > 0 && ash::switches::UseAlternateShelfLayout()) 114 system_tray_height_ += message_center::kMarginBetweenItems; 115 116 OnAutoHideStateChanged(shelf_->auto_hide_state()); 117 } 118 119 void WorkAreaObserver::OnDisplayWorkAreaInsetsChanged() { 120 collection_->OnDisplayBoundsChanged( 121 Shell::GetScreen()->GetDisplayNearestWindow( 122 shelf_->shelf_widget()->GetNativeView())); 123 } 124 125 void WorkAreaObserver::OnAutoHideStateChanged(ShelfAutoHideState new_state) { 126 gfx::Display display = Shell::GetScreen()->GetDisplayNearestWindow( 127 shelf_->shelf_widget()->GetNativeView()); 128 gfx::Rect work_area = display.work_area(); 129 int width = 0; 130 if (shelf_->auto_hide_behavior() != SHELF_AUTO_HIDE_BEHAVIOR_NEVER) { 131 width = (new_state == SHELF_AUTO_HIDE_HIDDEN) ? 132 ShelfLayoutManager::kAutoHideSize : 133 ShelfLayoutManager::GetPreferredShelfSize(); 134 } 135 switch (shelf_->GetAlignment()) { 136 case SHELF_ALIGNMENT_BOTTOM: 137 work_area.Inset(0, 0, 0, width); 138 if (system_tray_height_ > 0) { 139 work_area.set_height( 140 std::max(0, work_area.height() - system_tray_height_)); 141 } 142 break; 143 case SHELF_ALIGNMENT_LEFT: 144 work_area.Inset(width, 0, 0, 0); 145 // Popups appear on the left bottom only when UI is RTL. 146 if (base::i18n::IsRTL() && system_tray_height_ > 0) { 147 work_area.set_height( 148 std::max(0, work_area.height() - system_tray_height_)); 149 } 150 break; 151 case SHELF_ALIGNMENT_RIGHT: 152 work_area.Inset(0, 0, width, 0); 153 // Popups appear on the right bottom only when UI isn't RTL. 154 if (!base::i18n::IsRTL() && system_tray_height_ > 0) { 155 work_area.set_height( 156 std::max(0, work_area.height() - system_tray_height_)); 157 } 158 break; 159 case SHELF_ALIGNMENT_TOP: 160 work_area.Inset(0, width, 0, 0); 161 if (system_tray_height_ > 0) { 162 work_area.set_y(work_area.y() + system_tray_height_); 163 work_area.set_height( 164 std::max(0, work_area.height() - system_tray_height_)); 165 } 166 break; 167 } 168 collection_->SetDisplayInfo(work_area, display.bounds()); 169 } 170 171 // Class to initialize and manage the WebNotificationBubble and 172 // TrayBubbleWrapper instances for a bubble. 173 class WebNotificationBubbleWrapper { 174 public: 175 // Takes ownership of |bubble| and creates |bubble_wrapper_|. 176 WebNotificationBubbleWrapper(WebNotificationTray* tray, 177 message_center::MessageBubbleBase* bubble) { 178 bubble_.reset(bubble); 179 views::TrayBubbleView::AnchorAlignment anchor_alignment = 180 tray->GetAnchorAlignment(); 181 views::TrayBubbleView::InitParams init_params = 182 bubble->GetInitParams(anchor_alignment); 183 views::View* anchor = tray->tray_container(); 184 if (anchor_alignment == views::TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM) { 185 gfx::Point bounds(anchor->width() / 2, 0); 186 views::View::ConvertPointToWidget(anchor, &bounds); 187 init_params.arrow_offset = bounds.x(); 188 } 189 views::TrayBubbleView* bubble_view = views::TrayBubbleView::Create( 190 tray->GetBubbleWindowContainer(), anchor, tray, &init_params); 191 if (ash::switches::UseAlternateShelfLayout()) 192 bubble_view->SetArrowPaintType(views::BubbleBorder::PAINT_NONE); 193 bubble_wrapper_.reset(new TrayBubbleWrapper(tray, bubble_view)); 194 bubble->InitializeContents(bubble_view); 195 } 196 197 message_center::MessageBubbleBase* bubble() const { return bubble_.get(); } 198 199 // Convenience accessors. 200 views::TrayBubbleView* bubble_view() const { return bubble_->bubble_view(); } 201 202 private: 203 scoped_ptr<message_center::MessageBubbleBase> bubble_; 204 scoped_ptr<internal::TrayBubbleWrapper> bubble_wrapper_; 205 206 DISALLOW_COPY_AND_ASSIGN(WebNotificationBubbleWrapper); 207 }; 208 209 class WebNotificationButton : public views::CustomButton { 210 public: 211 WebNotificationButton(views::ButtonListener* listener) 212 : views::CustomButton(listener), 213 is_bubble_visible_(false), 214 unread_count_(0) { 215 SetLayoutManager(new views::FillLayout); 216 unread_label_ = new views::Label(); 217 SetupLabelForTray(unread_label_); 218 AddChildView(unread_label_); 219 } 220 221 void SetBubbleVisible(bool visible) { 222 if (visible == is_bubble_visible_) 223 return; 224 225 is_bubble_visible_ = visible; 226 UpdateIconVisibility(); 227 } 228 229 void SetUnreadCount(int unread_count) { 230 // base::FormatNumber doesn't convert to arabic numeric characters. 231 // TODO(mukai): use ICU to support conversion for such locales. 232 unread_count_ = unread_count; 233 // TODO(mukai): move NINE_PLUS message to ui_strings, it doesn't need to be 234 // in ash_strings. 235 unread_label_->SetText((unread_count > 9) ? 236 l10n_util::GetStringUTF16(IDS_ASH_NOTIFICATION_UNREAD_COUNT_NINE_PLUS) : 237 base::FormatNumber(unread_count)); 238 UpdateIconVisibility(); 239 } 240 241 protected: 242 // Overridden from views::ImageButton: 243 virtual gfx::Size GetPreferredSize() OVERRIDE { 244 if (ash::switches::UseAlternateShelfLayout()) 245 return gfx::Size(kWebNotificationAlternateSize, 246 kWebNotificationAlternateSize); 247 return gfx::Size(kWebNotificationIconSize, kWebNotificationIconSize); 248 } 249 250 private: 251 void UpdateIconVisibility() { 252 unread_label_->SetEnabledColor( 253 (!is_bubble_visible_ && unread_count_ > 0) ? 254 kWebNotificationColorWithUnread : kWebNotificationColorNoUnread); 255 SchedulePaint(); 256 } 257 258 bool is_bubble_visible_; 259 int unread_count_; 260 261 views::Label* unread_label_; 262 263 DISALLOW_COPY_AND_ASSIGN(WebNotificationButton); 264 }; 265 266 } // namespace internal 267 268 WebNotificationTray::WebNotificationTray( 269 internal::StatusAreaWidget* status_area_widget) 270 : TrayBackgroundView(status_area_widget), 271 button_(NULL), 272 show_message_center_on_unlock_(false), 273 should_update_tray_content_(false), 274 should_block_shelf_auto_hide_(false) { 275 button_ = new internal::WebNotificationButton(this); 276 button_->set_triggerable_event_flags( 277 ui::EF_LEFT_MOUSE_BUTTON | ui::EF_RIGHT_MOUSE_BUTTON); 278 tray_container()->AddChildView(button_); 279 SetContentsBackground(); 280 tray_container()->set_border(NULL); 281 SetVisible(false); 282 message_center_tray_.reset(new message_center::MessageCenterTray( 283 this, 284 message_center::MessageCenter::Get())); 285 OnMessageCenterTrayChanged(); 286 } 287 288 WebNotificationTray::~WebNotificationTray() { 289 // Release any child views that might have back pointers before ~View(). 290 message_center_bubble_.reset(); 291 popup_collection_.reset(); 292 work_area_observer_.reset(); 293 } 294 295 // Public methods. 296 297 bool WebNotificationTray::ShowMessageCenterInternal(bool show_settings) { 298 if (!ShouldShowMessageCenter()) 299 return false; 300 301 should_block_shelf_auto_hide_ = true; 302 message_center::MessageCenterBubble* message_center_bubble = 303 new message_center::MessageCenterBubble( 304 message_center(), 305 message_center_tray_.get(), 306 ash::switches::UseAlternateShelfLayout()); 307 308 int max_height = 0; 309 aura::Window* status_area_window = status_area_widget()->GetNativeView(); 310 switch (GetShelfLayoutManager()->GetAlignment()) { 311 case SHELF_ALIGNMENT_BOTTOM: { 312 gfx::Rect shelf_bounds = GetShelfLayoutManager()->GetIdealBounds(); 313 max_height = shelf_bounds.y(); 314 break; 315 } 316 case SHELF_ALIGNMENT_TOP: { 317 aura::RootWindow* root = status_area_window->GetRootWindow(); 318 max_height = 319 root->bounds().height() - status_area_window->bounds().height(); 320 break; 321 } 322 case SHELF_ALIGNMENT_LEFT: 323 case SHELF_ALIGNMENT_RIGHT: { 324 // Assume that the bottom line of the status area widget and the bubble 325 // are aligned. 326 max_height = status_area_window->GetBoundsInRootWindow().bottom(); 327 break; 328 } 329 default: 330 NOTREACHED(); 331 } 332 333 message_center_bubble->SetMaxHeight(std::max(0, 334 max_height - GetTraySpacing())); 335 if (show_settings) 336 message_center_bubble->SetSettingsVisible(); 337 message_center_bubble_.reset( 338 new internal::WebNotificationBubbleWrapper(this, message_center_bubble)); 339 340 status_area_widget()->SetHideSystemNotifications(true); 341 GetShelfLayoutManager()->UpdateAutoHideState(); 342 button_->SetBubbleVisible(true); 343 return true; 344 } 345 346 bool WebNotificationTray::ShowMessageCenter() { 347 return ShowMessageCenterInternal(false /* show_settings */); 348 } 349 350 void WebNotificationTray::HideMessageCenter() { 351 if (!message_center_bubble()) 352 return; 353 message_center_bubble_.reset(); 354 should_block_shelf_auto_hide_ = false; 355 show_message_center_on_unlock_ = false; 356 status_area_widget()->SetHideSystemNotifications(false); 357 GetShelfLayoutManager()->UpdateAutoHideState(); 358 button_->SetBubbleVisible(false); 359 } 360 361 void WebNotificationTray::SetSystemTrayHeight(int height) { 362 if (!work_area_observer_) 363 return; 364 work_area_observer_->SetSystemTrayHeight(height); 365 } 366 367 bool WebNotificationTray::ShowPopups() { 368 if (status_area_widget()->login_status() == user::LOGGED_IN_LOCKED || 369 message_center_bubble() || 370 !status_area_widget()->ShouldShowWebNotifications()) { 371 return false; 372 } 373 374 popup_collection_.reset(new message_center::MessagePopupCollection( 375 ash::Shell::GetContainer( 376 GetWidget()->GetNativeView()->GetRootWindow(), 377 internal::kShellWindowId_StatusContainer), 378 message_center(), 379 message_center_tray_.get(), 380 ash::switches::UseAlternateShelfLayout())); 381 work_area_observer_.reset(new internal::WorkAreaObserver( 382 popup_collection_.get(), GetShelfLayoutManager())); 383 return true; 384 } 385 386 void WebNotificationTray::HidePopups() { 387 popup_collection_.reset(); 388 work_area_observer_.reset(); 389 } 390 391 // Private methods. 392 393 bool WebNotificationTray::ShouldShowMessageCenter() { 394 return status_area_widget()->login_status() != user::LOGGED_IN_LOCKED && 395 !(status_area_widget()->system_tray() && 396 status_area_widget()->system_tray()->HasNotificationBubble()); 397 } 398 399 void WebNotificationTray::ShowQuietModeMenu(const ui::Event& event) { 400 base::AutoReset<bool> reset(&should_block_shelf_auto_hide_, true); 401 scoped_ptr<ui::MenuModel> menu_model( 402 message_center_tray_->CreateQuietModeMenu()); 403 quiet_mode_menu_runner_.reset(new views::MenuRunner(menu_model.get())); 404 gfx::Point point; 405 views::View::ConvertPointToScreen(this, &point); 406 if (quiet_mode_menu_runner_->RunMenuAt( 407 GetWidget(), 408 NULL, 409 gfx::Rect(point, bounds().size()), 410 views::MenuItemView::BUBBLE_ABOVE, 411 ui::GetMenuSourceTypeForEvent(event), 412 views::MenuRunner::HAS_MNEMONICS) == views::MenuRunner::MENU_DELETED) 413 return; 414 415 quiet_mode_menu_runner_.reset(); 416 GetShelfLayoutManager()->UpdateAutoHideState(); 417 } 418 419 bool WebNotificationTray::ShouldShowQuietModeMenu(const ui::Event& event) { 420 // TODO(mukai): Add keyboard event handler. 421 if (!event.IsMouseEvent()) 422 return false; 423 424 const ui::MouseEvent* mouse_event = 425 static_cast<const ui::MouseEvent*>(&event); 426 427 return mouse_event->IsRightMouseButton(); 428 } 429 430 void WebNotificationTray::UpdateAfterLoginStatusChange( 431 user::LoginStatus login_status) { 432 if (login_status == user::LOGGED_IN_LOCKED) { 433 show_message_center_on_unlock_ = 434 message_center_tray_->HideMessageCenterBubble(); 435 message_center_tray_->HidePopupBubble(); 436 } else { 437 // Only try once to show the message center bubble on login status change, 438 // so always set |show_message_center_on_unlock_| to false. 439 if (show_message_center_on_unlock_) 440 message_center_tray_->ShowMessageCenterBubble(); 441 show_message_center_on_unlock_ = false; 442 } 443 OnMessageCenterTrayChanged(); 444 } 445 446 bool WebNotificationTray::ShouldBlockLauncherAutoHide() const { 447 return should_block_shelf_auto_hide_; 448 } 449 450 bool WebNotificationTray::IsMessageCenterBubbleVisible() const { 451 return (message_center_bubble() && 452 message_center_bubble()->bubble()->IsVisible()); 453 } 454 455 bool WebNotificationTray::IsMouseInNotificationBubble() const { 456 return false; 457 } 458 459 void WebNotificationTray::ShowMessageCenterBubble() { 460 if (!IsMessageCenterBubbleVisible()) 461 message_center_tray_->ShowMessageCenterBubble(); 462 } 463 464 void WebNotificationTray::SetShelfAlignment(ShelfAlignment alignment) { 465 if (alignment == shelf_alignment()) 466 return; 467 internal::TrayBackgroundView::SetShelfAlignment(alignment); 468 tray_container()->set_border(NULL); 469 // Destroy any existing bubble so that it will be rebuilt correctly. 470 message_center_tray_->HideMessageCenterBubble(); 471 message_center_tray_->HidePopupBubble(); 472 } 473 474 void WebNotificationTray::AnchorUpdated() { 475 if (message_center_bubble()) { 476 message_center_bubble()->bubble_view()->UpdateBubble(); 477 UpdateBubbleViewArrow(message_center_bubble()->bubble_view()); 478 } 479 } 480 481 base::string16 WebNotificationTray::GetAccessibleNameForTray() { 482 return l10n_util::GetStringUTF16( 483 IDS_MESSAGE_CENTER_ACCESSIBLE_NAME); 484 } 485 486 void WebNotificationTray::HideBubbleWithView( 487 const views::TrayBubbleView* bubble_view) { 488 if (message_center_bubble() && 489 bubble_view == message_center_bubble()->bubble_view()) { 490 message_center_tray_->HideMessageCenterBubble(); 491 } else if (popup_collection_.get()) { 492 message_center_tray_->HidePopupBubble(); 493 } 494 } 495 496 bool WebNotificationTray::PerformAction(const ui::Event& event) { 497 if (ShouldShowQuietModeMenu(event)) { 498 ShowQuietModeMenu(event); 499 return true; 500 } 501 502 if (message_center_bubble()) 503 message_center_tray_->HideMessageCenterBubble(); 504 else 505 message_center_tray_->ShowMessageCenterBubble(); 506 return true; 507 } 508 509 void WebNotificationTray::BubbleViewDestroyed() { 510 if (message_center_bubble()) 511 message_center_bubble()->bubble()->BubbleViewDestroyed(); 512 } 513 514 void WebNotificationTray::OnMouseEnteredView() {} 515 516 void WebNotificationTray::OnMouseExitedView() {} 517 518 base::string16 WebNotificationTray::GetAccessibleNameForBubble() { 519 return GetAccessibleNameForTray(); 520 } 521 522 gfx::Rect WebNotificationTray::GetAnchorRect( 523 views::Widget* anchor_widget, 524 views::TrayBubbleView::AnchorType anchor_type, 525 views::TrayBubbleView::AnchorAlignment anchor_alignment) { 526 return GetBubbleAnchorRect(anchor_widget, anchor_type, anchor_alignment); 527 } 528 529 void WebNotificationTray::HideBubble(const views::TrayBubbleView* bubble_view) { 530 HideBubbleWithView(bubble_view); 531 } 532 533 bool WebNotificationTray::ShowNotifierSettings() { 534 if (message_center_bubble()) { 535 static_cast<message_center::MessageCenterBubble*>( 536 message_center_bubble()->bubble())->SetSettingsVisible(); 537 return true; 538 } 539 return ShowMessageCenterInternal(true /* show_settings */); 540 } 541 542 message_center::MessageCenterTray* WebNotificationTray::GetMessageCenterTray() { 543 return message_center_tray_.get(); 544 } 545 546 bool WebNotificationTray::IsPressed() { 547 return IsMessageCenterBubbleVisible(); 548 } 549 550 void WebNotificationTray::ButtonPressed(views::Button* sender, 551 const ui::Event& event) { 552 DCHECK_EQ(button_, sender); 553 PerformAction(event); 554 } 555 556 void WebNotificationTray::OnMessageCenterTrayChanged() { 557 // Do not update the tray contents directly. Multiple change events can happen 558 // consecutively, and calling Update in the middle of those events will show 559 // intermediate unread counts for a moment. 560 should_update_tray_content_ = true; 561 base::MessageLoop::current()->PostTask( 562 FROM_HERE, 563 base::Bind(&WebNotificationTray::UpdateTrayContent, AsWeakPtr())); 564 } 565 566 void WebNotificationTray::UpdateTrayContent() { 567 if (!should_update_tray_content_) 568 return; 569 should_update_tray_content_ = false; 570 571 message_center::MessageCenter* message_center = 572 message_center_tray_->message_center(); 573 button_->SetUnreadCount(message_center->UnreadNotificationCount()); 574 if (IsMessageCenterBubbleVisible()) 575 button_->SetState(views::CustomButton::STATE_PRESSED); 576 else 577 button_->SetState(views::CustomButton::STATE_NORMAL); 578 SetVisible((status_area_widget()->login_status() != user::LOGGED_IN_NONE) && 579 (status_area_widget()->login_status() != user::LOGGED_IN_LOCKED) && 580 (message_center->NotificationCount() > 0)); 581 Layout(); 582 SchedulePaint(); 583 } 584 585 bool WebNotificationTray::ClickedOutsideBubble() { 586 // Only hide the message center 587 if (!message_center_bubble()) 588 return false; 589 590 message_center_tray_->HideMessageCenterBubble(); 591 return true; 592 } 593 594 message_center::MessageCenter* WebNotificationTray::message_center() { 595 return message_center_tray_->message_center(); 596 } 597 598 // Methods for testing 599 600 bool WebNotificationTray::IsPopupVisible() const { 601 return message_center_tray_->popups_visible(); 602 } 603 604 message_center::MessageCenterBubble* 605 WebNotificationTray::GetMessageCenterBubbleForTest() { 606 if (!message_center_bubble()) 607 return NULL; 608 return static_cast<message_center::MessageCenterBubble*>( 609 message_center_bubble()->bubble()); 610 } 611 612 } // namespace ash 613