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