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