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