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