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