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 "chrome/browser/ui/views/message_center/web_notification_tray.h" 6 7 #include "base/i18n/number_formatting.h" 8 #include "base/prefs/pref_service.h" 9 #include "base/strings/string16.h" 10 #include "base/strings/utf_string_conversions.h" 11 #include "chrome/browser/browser_process.h" 12 #include "chrome/browser/status_icons/status_icon.h" 13 #include "chrome/browser/status_icons/status_icon_menu_model.h" 14 #include "chrome/browser/status_icons/status_tray.h" 15 #include "chrome/common/pref_names.h" 16 #include "chrome/grit/chromium_strings.h" 17 #include "chrome/grit/generated_resources.h" 18 #include "content/public/browser/notification_service.h" 19 #include "grit/theme_resources.h" 20 #include "ui/base/l10n/l10n_util.h" 21 #include "ui/base/resource/resource_bundle.h" 22 #include "ui/gfx/canvas.h" 23 #include "ui/gfx/image/image_skia_operations.h" 24 #include "ui/gfx/rect.h" 25 #include "ui/gfx/screen.h" 26 #include "ui/gfx/size.h" 27 #include "ui/message_center/message_center_tray.h" 28 #include "ui/message_center/message_center_tray_delegate.h" 29 #include "ui/message_center/views/desktop_popup_alignment_delegate.h" 30 #include "ui/message_center/views/message_popup_collection.h" 31 #include "ui/strings/grit/ui_strings.h" 32 #include "ui/views/widget/widget.h" 33 34 namespace { 35 36 // Tray constants 37 const int kScreenEdgePadding = 2; 38 39 // Number of pixels the message center is offset from the mouse. 40 const int kMouseOffset = 5; 41 42 // Menu commands 43 const int kToggleQuietMode = 0; 44 const int kEnableQuietModeHour = 1; 45 const int kEnableQuietModeDay = 2; 46 47 gfx::ImageSkia* GetIcon(int unread_count, bool is_quiet_mode) { 48 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 49 int resource_id = IDR_NOTIFICATION_TRAY_EMPTY; 50 51 if (unread_count) { 52 if (is_quiet_mode) 53 resource_id = IDR_NOTIFICATION_TRAY_DO_NOT_DISTURB_ATTENTION; 54 else 55 resource_id = IDR_NOTIFICATION_TRAY_ATTENTION; 56 } else if (is_quiet_mode) { 57 resource_id = IDR_NOTIFICATION_TRAY_DO_NOT_DISTURB_EMPTY; 58 } 59 60 return rb.GetImageSkiaNamed(resource_id); 61 } 62 63 } // namespace 64 65 namespace message_center { 66 67 namespace internal { 68 69 // Gets the position of the taskbar from the work area bounds. Returns 70 // ALIGNMENT_NONE if position cannot be found. 71 Alignment GetTaskbarAlignment() { 72 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 73 // TODO(dewittj): It's possible GetPrimaryDisplay is wrong. 74 gfx::Rect screen_bounds = screen->GetPrimaryDisplay().bounds(); 75 gfx::Rect work_area = screen->GetPrimaryDisplay().work_area(); 76 work_area.Inset(kScreenEdgePadding, kScreenEdgePadding); 77 78 // Comparing the work area to the screen bounds gives us the location of the 79 // taskbar. If the work area is exactly the same as the screen bounds, 80 // we are unable to locate the taskbar so we say we don't know it's alignment. 81 if (work_area.height() < screen_bounds.height()) { 82 if (work_area.y() > screen_bounds.y()) 83 return ALIGNMENT_TOP; 84 return ALIGNMENT_BOTTOM; 85 } 86 if (work_area.width() < screen_bounds.width()) { 87 if (work_area.x() > screen_bounds.x()) 88 return ALIGNMENT_LEFT; 89 return ALIGNMENT_RIGHT; 90 } 91 92 return ALIGNMENT_NONE; 93 } 94 95 gfx::Point GetClosestCorner(const gfx::Rect& rect, const gfx::Point& query) { 96 gfx::Point center_point = rect.CenterPoint(); 97 gfx::Point rv; 98 99 if (query.x() > center_point.x()) 100 rv.set_x(rect.right()); 101 else 102 rv.set_x(rect.x()); 103 104 if (query.y() > center_point.y()) 105 rv.set_y(rect.bottom()); 106 else 107 rv.set_y(rect.y()); 108 109 return rv; 110 } 111 112 // Gets the corner of the screen where the message center should pop up. 113 Alignment GetAnchorAlignment(const gfx::Rect& work_area, gfx::Point corner) { 114 gfx::Point center = work_area.CenterPoint(); 115 116 Alignment anchor_alignment = 117 center.y() > corner.y() ? ALIGNMENT_TOP : ALIGNMENT_BOTTOM; 118 anchor_alignment = 119 (Alignment)(anchor_alignment | 120 (center.x() > corner.x() ? ALIGNMENT_LEFT : ALIGNMENT_RIGHT)); 121 122 return anchor_alignment; 123 } 124 125 } // namespace internal 126 127 MessageCenterTrayDelegate* CreateMessageCenterTray() { 128 return new WebNotificationTray(g_browser_process->local_state()); 129 } 130 131 WebNotificationTray::WebNotificationTray(PrefService* local_state) 132 : message_center_delegate_(NULL), 133 status_icon_(NULL), 134 status_icon_menu_(NULL), 135 should_update_tray_content_(true) { 136 message_center_tray_.reset( 137 new MessageCenterTray(this, g_browser_process->message_center())); 138 last_quiet_mode_state_ = message_center()->IsQuietMode(); 139 alignment_delegate_.reset(new message_center::DesktopPopupAlignmentDelegate); 140 popup_collection_.reset(new message_center::MessagePopupCollection( 141 NULL, message_center(), message_center_tray_.get(), 142 alignment_delegate_.get())); 143 144 #if defined(OS_WIN) 145 // |local_state| can be NULL in tests. 146 if (local_state) { 147 did_force_tray_visible_.reset(new BooleanPrefMember()); 148 did_force_tray_visible_->Init(prefs::kMessageCenterForcedOnTaskbar, 149 local_state); 150 } 151 #endif 152 title_ = l10n_util::GetStringFUTF16( 153 IDS_MESSAGE_CENTER_FOOTER_WITH_PRODUCT_TITLE, 154 l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)); 155 } 156 157 WebNotificationTray::~WebNotificationTray() { 158 // Reset this early so that delegated events during destruction don't cause 159 // problems. 160 popup_collection_.reset(); 161 message_center_tray_.reset(); 162 DestroyStatusIcon(); 163 } 164 165 message_center::MessageCenter* WebNotificationTray::message_center() { 166 return message_center_tray_->message_center(); 167 } 168 169 bool WebNotificationTray::ShowPopups() { 170 alignment_delegate_->StartObserving(gfx::Screen::GetNativeScreen()); 171 popup_collection_->DoUpdateIfPossible(); 172 return true; 173 } 174 175 void WebNotificationTray::HidePopups() { 176 DCHECK(popup_collection_.get()); 177 popup_collection_->MarkAllPopupsShown(); 178 } 179 180 bool WebNotificationTray::ShowMessageCenter() { 181 message_center_delegate_ = 182 new MessageCenterWidgetDelegate(this, 183 message_center_tray_.get(), 184 false, // settings initally invisible 185 GetPositionInfo(), 186 title_); 187 188 return true; 189 } 190 191 void WebNotificationTray::HideMessageCenter() { 192 if (message_center_delegate_) { 193 views::Widget* widget = message_center_delegate_->GetWidget(); 194 if (widget) 195 widget->Close(); 196 } 197 } 198 199 bool WebNotificationTray::ShowNotifierSettings() { 200 if (message_center_delegate_) { 201 message_center_delegate_->SetSettingsVisible(true); 202 return true; 203 } 204 message_center_delegate_ = 205 new MessageCenterWidgetDelegate(this, 206 message_center_tray_.get(), 207 true, // settings initally visible 208 GetPositionInfo(), 209 title_); 210 211 return true; 212 } 213 214 bool WebNotificationTray::IsContextMenuEnabled() const { 215 // It can always return true because the notifications are invisible if 216 // the context menu shouldn't be enabled, such as in the lock screen. 217 return true; 218 } 219 220 void WebNotificationTray::OnMessageCenterTrayChanged() { 221 if (status_icon_) { 222 bool quiet_mode_state = message_center()->IsQuietMode(); 223 if (last_quiet_mode_state_ != quiet_mode_state) { 224 last_quiet_mode_state_ = quiet_mode_state; 225 226 // Quiet mode has changed, update the quiet mode menu. 227 status_icon_menu_->SetCommandIdChecked(kToggleQuietMode, 228 quiet_mode_state); 229 } 230 } else if (message_center()->NotificationCount() == 0) { 231 // If there's no existing status icon and we still don't have any 232 // notifications to display, nothing needs to be done. 233 return; 234 } 235 236 // See the comments in ash/system/web_notification/web_notification_tray.cc 237 // for why PostTask. 238 should_update_tray_content_ = true; 239 base::MessageLoop::current()->PostTask( 240 FROM_HERE, 241 base::Bind(&WebNotificationTray::UpdateStatusIcon, AsWeakPtr())); 242 } 243 244 void WebNotificationTray::OnStatusIconClicked() { 245 // TODO(dewittj): It's possible GetNativeScreen is wrong for win-aura. 246 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 247 mouse_click_point_ = screen->GetCursorScreenPoint(); 248 message_center_tray_->ToggleMessageCenterBubble(); 249 } 250 251 void WebNotificationTray::ExecuteCommand(int command_id, int event_flags) { 252 if (command_id == kToggleQuietMode) { 253 bool in_quiet_mode = message_center()->IsQuietMode(); 254 message_center()->SetQuietMode(!in_quiet_mode); 255 return; 256 } 257 base::TimeDelta expires_in = command_id == kEnableQuietModeDay 258 ? base::TimeDelta::FromDays(1) 259 : base::TimeDelta::FromHours(1); 260 message_center()->EnterQuietModeWithExpire(expires_in); 261 } 262 263 void WebNotificationTray::UpdateStatusIcon() { 264 if (!should_update_tray_content_) 265 return; 266 should_update_tray_content_ = false; 267 268 int unread_notifications = message_center()->UnreadNotificationCount(); 269 270 base::string16 tool_tip; 271 if (unread_notifications > 0) { 272 base::string16 str_unread_count = base::FormatNumber(unread_notifications); 273 tool_tip = l10n_util::GetStringFUTF16(IDS_MESSAGE_CENTER_TOOLTIP_UNREAD, 274 str_unread_count); 275 } else { 276 tool_tip = l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_TOOLTIP); 277 } 278 279 if (message_center()->GetVisibleNotifications().empty()) { 280 DestroyStatusIcon(); 281 return; 282 } 283 284 gfx::ImageSkia* icon_image = GetIcon( 285 unread_notifications, 286 message_center()->IsQuietMode()); 287 288 if (status_icon_) { 289 status_icon_->SetImage(*icon_image); 290 status_icon_->SetToolTip(tool_tip); 291 return; 292 } 293 294 CreateStatusIcon(*icon_image, tool_tip); 295 } 296 297 void WebNotificationTray::SendHideMessageCenter() { 298 message_center_tray_->HideMessageCenterBubble(); 299 } 300 301 void WebNotificationTray::MarkMessageCenterHidden() { 302 if (message_center_delegate_) { 303 message_center_tray_->MarkMessageCenterHidden(); 304 message_center_delegate_ = NULL; 305 } 306 } 307 308 PositionInfo WebNotificationTray::GetPositionInfo() { 309 PositionInfo pos_info; 310 311 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 312 gfx::Rect work_area = screen->GetPrimaryDisplay().work_area(); 313 work_area.Inset(kScreenEdgePadding, kScreenEdgePadding); 314 315 gfx::Point corner = internal::GetClosestCorner(work_area, mouse_click_point_); 316 317 pos_info.taskbar_alignment = internal::GetTaskbarAlignment(); 318 319 // We assume the taskbar is either at the top or at the bottom if we are not 320 // able to find it. 321 if (pos_info.taskbar_alignment == ALIGNMENT_NONE) { 322 if (mouse_click_point_.y() > corner.y()) 323 pos_info.taskbar_alignment = ALIGNMENT_TOP; 324 else 325 pos_info.taskbar_alignment = ALIGNMENT_BOTTOM; 326 } 327 328 pos_info.message_center_alignment = 329 internal::GetAnchorAlignment(work_area, corner); 330 331 pos_info.inital_anchor_point = corner; 332 pos_info.max_height = work_area.height(); 333 334 if (work_area.Contains(mouse_click_point_)) { 335 // Message center is in the work area. So position it few pixels above the 336 // mouse click point if alignemnt is towards bottom and few pixels below if 337 // alignment is towards top. 338 pos_info.inital_anchor_point.set_y( 339 mouse_click_point_.y() + 340 (pos_info.message_center_alignment & ALIGNMENT_BOTTOM ? -kMouseOffset 341 : kMouseOffset)); 342 343 // Subtract the distance between mouse click point and the closest 344 // (insetted) edge from the max height to show the message center within the 345 // (insetted) work area bounds. Also subtract the offset from the mouse 346 // click point we added earlier. 347 pos_info.max_height -= 348 std::abs(mouse_click_point_.y() - corner.y()) + kMouseOffset; 349 } 350 return pos_info; 351 } 352 353 MessageCenterTray* WebNotificationTray::GetMessageCenterTray() { 354 return message_center_tray_.get(); 355 } 356 357 void WebNotificationTray::CreateStatusIcon(const gfx::ImageSkia& image, 358 const base::string16& tool_tip) { 359 if (status_icon_) 360 return; 361 362 StatusTray* status_tray = g_browser_process->status_tray(); 363 if (!status_tray) 364 return; 365 366 status_icon_ = status_tray->CreateStatusIcon( 367 StatusTray::NOTIFICATION_TRAY_ICON, image, tool_tip); 368 if (!status_icon_) 369 return; 370 371 status_icon_->AddObserver(this); 372 AddQuietModeMenu(status_icon_); 373 } 374 375 void WebNotificationTray::DestroyStatusIcon() { 376 if (!status_icon_) 377 return; 378 379 status_icon_->RemoveObserver(this); 380 StatusTray* status_tray = g_browser_process->status_tray(); 381 if (status_tray) 382 status_tray->RemoveStatusIcon(status_icon_); 383 status_icon_menu_ = NULL; 384 status_icon_ = NULL; 385 } 386 387 void WebNotificationTray::AddQuietModeMenu(StatusIcon* status_icon) { 388 DCHECK(status_icon); 389 390 scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this)); 391 menu->AddCheckItem(kToggleQuietMode, 392 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE)); 393 menu->SetCommandIdChecked(kToggleQuietMode, message_center()->IsQuietMode()); 394 menu->AddItem(kEnableQuietModeHour, 395 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE_1HOUR)); 396 menu->AddItem(kEnableQuietModeDay, 397 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE_1DAY)); 398 399 status_icon_menu_ = menu.get(); 400 status_icon->SetContextMenu(menu.Pass()); 401 } 402 403 MessageCenterWidgetDelegate* 404 WebNotificationTray::GetMessageCenterWidgetDelegateForTest() { 405 return message_center_delegate_; 406 } 407 408 } // namespace message_center 409