web_notification_tray.cc revision 868fa2fe829687343ffae624259930155e16dbd8
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/strings/string16.h" 9#include "base/strings/utf_string_conversions.h" 10#include "chrome/browser/browser_process.h" 11#include "chrome/browser/status_icons/status_icon.h" 12#include "chrome/browser/status_icons/status_tray.h" 13#include "chrome/browser/ui/views/message_center/notification_bubble_wrapper.h" 14#include "content/public/browser/user_metrics.h" 15#include "grit/chromium_strings.h" 16#include "grit/theme_resources.h" 17#include "grit/ui_strings.h" 18#include "ui/base/l10n/l10n_util.h" 19#include "ui/base/models/simple_menu_model.h" 20#include "ui/base/resource/resource_bundle.h" 21#include "ui/gfx/canvas.h" 22#include "ui/gfx/image/image_skia_operations.h" 23#include "ui/gfx/rect.h" 24#include "ui/gfx/screen.h" 25#include "ui/gfx/size.h" 26#include "ui/message_center/message_center_tray.h" 27#include "ui/message_center/message_center_tray_delegate.h" 28#include "ui/message_center/views/message_bubble_base.h" 29#include "ui/message_center/views/message_center_bubble.h" 30#include "ui/message_center/views/message_popup_collection.h" 31#include "ui/views/widget/widget.h" 32 33#if defined(OS_WIN) 34#include "ui/base/win/hwnd_util.h" 35#endif 36 37namespace { 38 39// Tray constants 40const int kScreenEdgePadding = 2; 41 42const int kSystemTrayWidth = 16; 43const int kSystemTrayHeight = 16; 44const int kNumberOfSystemTraySprites = 10; 45 46gfx::Rect GetCornerAnchorRect() { 47 // TODO(dewittj): Use the preference to determine which corner to anchor from. 48 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 49 gfx::Rect rect = screen->GetPrimaryDisplay().work_area(); 50 rect.Inset(kScreenEdgePadding, kScreenEdgePadding); 51 return gfx::Rect(rect.bottom_right(), gfx::Size()); 52} 53 54gfx::Point GetClosestCorner(gfx::Rect rect, gfx::Point query) { 55 gfx::Point center_point = rect.CenterPoint(); 56 gfx::Point rv; 57 58 if (query.x() > center_point.x()) 59 rv.set_x(rect.right()); 60 else 61 rv.set_x(rect.x()); 62 63 if (query.y() > center_point.y()) 64 rv.set_y(rect.bottom()); 65 else 66 rv.set_y(rect.y()); 67 68 return rv; 69} 70 71// GetMouseAnchorRect returns a rectangle that has one corner where the mouse 72// clicked, and the opposite corner at the closest corner of the work area 73// (inset by an appropriate margin.) 74gfx::Rect GetMouseAnchorRect(gfx::Point cursor) { 75 // TODO(dewittj): GetNativeScreen could be wrong for Aura. 76 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 77 gfx::Rect work_area = screen->GetPrimaryDisplay().work_area(); 78 work_area.Inset(kScreenEdgePadding, kScreenEdgePadding); 79 gfx::Point corner = GetClosestCorner(work_area, cursor); 80 81 gfx::Rect mouse_anchor_rect(gfx::BoundingRect(cursor, corner)); 82 return mouse_anchor_rect; 83} 84 85gfx::ImageSkia GetIcon(int unread_count) { 86 bool has_unread = unread_count > 0; 87 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 88 if (!has_unread) 89 return *rb.GetImageSkiaNamed(IDR_NOTIFICATION_TRAY_EMPTY); 90 91 // TODO(dewittj): Use scale factors other than 100P. 92 scoped_ptr<gfx::Canvas> canvas( 93 new gfx::Canvas(gfx::Size(kSystemTrayWidth, kSystemTrayHeight), 94 ui::SCALE_FACTOR_100P, 95 false)); 96 97 // Draw the attention-grabbing background image. 98 canvas->DrawImageInt( 99 *rb.GetImageSkiaNamed(IDR_NOTIFICATION_TRAY_ATTENTION), 0, 0); 100 101 // |numbers| is a sprite map with the image of a number from 1-9 and 9+. They 102 // are arranged horizontally, and have a transparent background. 103 gfx::ImageSkia* numbers = rb.GetImageSkiaNamed(IDR_NOTIFICATION_TRAY_NUMBERS); 104 105 // Assume that the last sprite is the catch-all for higher numbers of 106 // notifications. 107 int effective_unread = std::min(unread_count, kNumberOfSystemTraySprites); 108 int x_offset = (effective_unread - 1) * kSystemTrayWidth; 109 110 canvas->DrawImageInt(*numbers, 111 x_offset, 0, kSystemTrayWidth, kSystemTrayHeight, 112 0, 0, kSystemTrayWidth, kSystemTrayHeight, 113 false); 114 return gfx::ImageSkia(canvas->ExtractImageRep()); 115} 116 117} // namespace 118 119using content::UserMetricsAction; 120 121namespace message_center { 122 123MessageCenterTrayDelegate* CreateMessageCenterTray() { 124 return new WebNotificationTray(); 125} 126 127WebNotificationTray::WebNotificationTray() 128 : status_icon_(NULL), 129 message_center_visible_(false), 130 should_update_tray_content_(true) { 131 message_center_tray_.reset( 132 new MessageCenterTray(this, g_browser_process->message_center())); 133 UpdateStatusIcon(); 134} 135 136WebNotificationTray::~WebNotificationTray() { 137 // Reset this early so that delegated events during destruction don't cause 138 // problems. 139 message_center_tray_.reset(); 140 DestroyStatusIcon(); 141} 142 143message_center::MessageCenter* WebNotificationTray::message_center() { 144 return message_center_tray_->message_center(); 145} 146 147bool WebNotificationTray::ShowPopups() { 148 popup_collection_.reset( 149 new message_center::MessagePopupCollection(NULL, message_center())); 150 return true; 151} 152 153void WebNotificationTray::HidePopups() { popup_collection_.reset(); } 154 155bool WebNotificationTray::ShowMessageCenter() { 156 content::RecordAction(UserMetricsAction("Notifications.ShowMessageCenter")); 157 158 // Using MessageBubbleBase instead of MessageCenterBubble to 159 // remove dependence on implicit type conversion 160 scoped_ptr<message_center::MessageBubbleBase> bubble( 161 new message_center::MessageCenterBubble(message_center())); 162 163 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 164 gfx::Rect work_area = screen->GetPrimaryDisplay().work_area(); 165 views::TrayBubbleView::AnchorAlignment alignment = GetAnchorAlignment(); 166 167 int max_height = work_area.height(); 168 169 // If the alignment is left- or right-oriented, the bubble can fill up the 170 // entire vertical height of the screen since the bubble is rendered to the 171 // side of the clicked icon. Otherwise we have to adjust for the arrow's 172 // height. 173 if (alignment == views::TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM || 174 alignment == views::TrayBubbleView::ANCHOR_ALIGNMENT_TOP) { 175 max_height -= 2 * kScreenEdgePadding; 176 177 // If the work area contains the click point, then we know that the icon is 178 // not in the taskbar. Then we need to subtract the distance of the click 179 // point from the edge of the work area so we can see the whole bubble. 180 if (work_area.Contains(mouse_click_point_)) { 181 max_height -= std::min(mouse_click_point_.y() - work_area.y(), 182 work_area.bottom() - mouse_click_point_.y()); 183 } 184 } 185 bubble->SetMaxHeight(max_height); 186 187 message_center_bubble_.reset(new internal::NotificationBubbleWrapper( 188 this, 189 bubble.Pass(), 190 internal::NotificationBubbleWrapper::BUBBLE_TYPE_MESSAGE_CENTER)); 191 return true; 192} 193 194void WebNotificationTray::HideMessageCenter() { 195 message_center_bubble_.reset(); 196} 197 198void WebNotificationTray::UpdatePopups() { 199 // |popup_collection_| receives notification add/remove events and updates 200 // itself, so this method doesn't need to do anything. 201 // TODO(mukai): remove this method (currently this is used by 202 // non-rich-notifications in ChromeOS). 203}; 204 205void WebNotificationTray::OnMessageCenterTrayChanged() { 206 // See the comments in ash/system/web_notification/web_notification_tray.cc 207 // for why PostTask. 208 should_update_tray_content_ = true; 209 base::MessageLoop::current()->PostTask( 210 FROM_HERE, 211 base::Bind(&WebNotificationTray::UpdateStatusIcon, AsWeakPtr())); 212} 213 214gfx::Rect WebNotificationTray::GetMessageCenterAnchor() { 215 return GetMouseAnchorRect(mouse_click_point_); 216} 217 218gfx::Rect WebNotificationTray::GetPopupAnchor() { 219 return GetCornerAnchorRect(); 220} 221 222views::TrayBubbleView::AnchorAlignment 223WebNotificationTray::GetAnchorAlignment() { 224 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 225 // TODO(dewittj): It's possible GetPrimaryDisplay is wrong. 226 gfx::Rect screen_bounds = screen->GetPrimaryDisplay().bounds(); 227 gfx::Rect work_area = screen->GetPrimaryDisplay().work_area(); 228 229 // Comparing the work area to the screen bounds gives us the location of the 230 // taskbar. If the work area is less tall than the screen, assume the taskbar 231 // is on the bottom, and cause the arrow to be displayed on the bottom of the 232 // bubble. Otherwise, cause the arrow to be displayed on the side of the 233 // bubble that the taskbar is on. 234 if (work_area.width() < screen_bounds.width()) { 235 if (work_area.x() > screen_bounds.x()) 236 return views::TrayBubbleView::ANCHOR_ALIGNMENT_LEFT; 237 return views::TrayBubbleView::ANCHOR_ALIGNMENT_RIGHT; 238 } 239 return views::TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM; 240} 241 242gfx::NativeView WebNotificationTray::GetBubbleWindowContainer() { return NULL; } 243 244void WebNotificationTray::UpdateStatusIcon() { 245 if (!should_update_tray_content_) 246 return; 247 should_update_tray_content_ = false; 248 249 int total_notifications = message_center()->NotificationCount(); 250 if (total_notifications == 0) { 251 DestroyStatusIcon(); 252 return; 253 } 254 255 int unread_notifications = message_center()->UnreadNotificationCount(); 256 StatusIcon* status_icon = GetStatusIcon(); 257 if (!status_icon) 258 return; 259 260 status_icon->SetImage(GetIcon(unread_notifications)); 261 262 string16 product_name(l10n_util::GetStringUTF16(IDS_SHORT_PRODUCT_NAME)); 263 if (unread_notifications > 0) { 264 string16 str_unread_count = base::FormatNumber(unread_notifications); 265 status_icon->SetToolTip(l10n_util::GetStringFUTF16( 266 IDS_MESSAGE_CENTER_TOOLTIP_UNREAD, product_name, str_unread_count)); 267 } else { 268 status_icon->SetToolTip( 269 l10n_util::GetStringFUTF16(IDS_MESSAGE_CENTER_TOOLTIP, product_name)); 270 } 271} 272 273void WebNotificationTray::OnStatusIconClicked() { 274 // TODO(dewittj): It's possible GetNativeScreen is wrong for win-aura. 275 gfx::Screen* screen = gfx::Screen::GetNativeScreen(); 276 mouse_click_point_ = screen->GetCursorScreenPoint(); 277 message_center_tray_->ToggleMessageCenterBubble(); 278} 279 280void WebNotificationTray::HideBubbleWithView( 281 const views::TrayBubbleView* bubble_view) { 282 if (message_center_bubble_.get() && 283 bubble_view == message_center_bubble_->bubble_view()) { 284 message_center_tray_->HideMessageCenterBubble(); 285 } 286} 287 288StatusIcon* WebNotificationTray::GetStatusIcon() { 289 if (status_icon_) 290 return status_icon_; 291 292 StatusTray* status_tray = g_browser_process->status_tray(); 293 if (!status_tray) 294 return NULL; 295 296 StatusIcon* status_icon = status_tray->CreateStatusIcon(); 297 if (!status_icon) 298 return NULL; 299 300 status_icon_ = status_icon; 301 status_icon_->AddObserver(this); 302 AddQuietModeMenu(status_icon_); 303 304 return status_icon_; 305} 306 307void WebNotificationTray::DestroyStatusIcon() { 308 if (!status_icon_) 309 return; 310 311 status_icon_->RemoveObserver(this); 312 StatusTray* status_tray = g_browser_process->status_tray(); 313 if (status_tray) 314 status_tray->RemoveStatusIcon(status_icon_); 315 status_icon_ = NULL; 316} 317 318void WebNotificationTray::AddQuietModeMenu(StatusIcon* status_icon) { 319 DCHECK(status_icon); 320 status_icon->SetContextMenu(message_center_tray_->CreateQuietModeMenu()); 321} 322 323message_center::MessageCenterBubble* 324WebNotificationTray::GetMessageCenterBubbleForTest() { 325 if (!message_center_bubble_.get()) 326 return NULL; 327 return static_cast<message_center::MessageCenterBubble*>( 328 message_center_bubble_->bubble()); 329} 330 331} // namespace message_center 332