web_notification_tray.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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/desktop_popup_alignment_delegate.h"
31#include "ui/message_center/views/message_popup_collection.h"
32#include "ui/views/widget/widget.h"
33
34namespace {
35
36// Tray constants
37const int kScreenEdgePadding = 2;
38
39// Number of pixels the message center is offset from the mouse.
40const int kMouseOffset = 5;
41
42// Menu commands
43const int kToggleQuietMode = 0;
44const int kEnableQuietModeHour = 1;
45const int kEnableQuietModeDay = 2;
46
47gfx::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
65namespace message_center {
66
67namespace internal {
68
69// Gets the position of the taskbar from the work area bounds. Returns
70// ALIGNMENT_NONE if position cannot be found.
71Alignment 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
95gfx::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.
113Alignment 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
127MessageCenterTrayDelegate* CreateMessageCenterTray() {
128  return new WebNotificationTray(g_browser_process->local_state());
129}
130
131WebNotificationTray::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
157WebNotificationTray::~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
165message_center::MessageCenter* WebNotificationTray::message_center() {
166  return message_center_tray_->message_center();
167}
168
169bool WebNotificationTray::ShowPopups() {
170  alignment_delegate_->StartObserving(gfx::Screen::GetNativeScreen());
171  popup_collection_->DoUpdateIfPossible();
172  return true;
173}
174
175void WebNotificationTray::HidePopups() {
176  DCHECK(popup_collection_.get());
177  popup_collection_->MarkAllPopupsShown();
178}
179
180bool 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
191void WebNotificationTray::HideMessageCenter() {
192  if (message_center_delegate_) {
193    views::Widget* widget = message_center_delegate_->GetWidget();
194    if (widget)
195      widget->Close();
196  }
197}
198
199bool 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
214bool 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
220void 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
244void 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
251void 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
263void 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  gfx::ImageSkia* icon_image = GetIcon(
280      unread_notifications,
281      message_center()->IsQuietMode());
282
283  if (status_icon_) {
284    status_icon_->SetImage(*icon_image);
285    status_icon_->SetToolTip(tool_tip);
286    return;
287  }
288
289  CreateStatusIcon(*icon_image, tool_tip);
290}
291
292void WebNotificationTray::SendHideMessageCenter() {
293  message_center_tray_->HideMessageCenterBubble();
294}
295
296void WebNotificationTray::MarkMessageCenterHidden() {
297  if (message_center_delegate_) {
298    message_center_tray_->MarkMessageCenterHidden();
299    message_center_delegate_ = NULL;
300  }
301}
302
303PositionInfo WebNotificationTray::GetPositionInfo() {
304  PositionInfo pos_info;
305
306  gfx::Screen* screen = gfx::Screen::GetNativeScreen();
307  gfx::Rect work_area = screen->GetPrimaryDisplay().work_area();
308  work_area.Inset(kScreenEdgePadding, kScreenEdgePadding);
309
310  gfx::Point corner = internal::GetClosestCorner(work_area, mouse_click_point_);
311
312  pos_info.taskbar_alignment = internal::GetTaskbarAlignment();
313
314  // We assume the taskbar is either at the top or at the bottom if we are not
315  // able to find it.
316  if (pos_info.taskbar_alignment == ALIGNMENT_NONE) {
317    if (mouse_click_point_.y() > corner.y())
318      pos_info.taskbar_alignment = ALIGNMENT_TOP;
319    else
320      pos_info.taskbar_alignment = ALIGNMENT_BOTTOM;
321  }
322
323  pos_info.message_center_alignment =
324      internal::GetAnchorAlignment(work_area, corner);
325
326  pos_info.inital_anchor_point = corner;
327  pos_info.max_height = work_area.height();
328
329  if (work_area.Contains(mouse_click_point_)) {
330    // Message center is in the work area. So position it few pixels above the
331    // mouse click point if alignemnt is towards bottom and few pixels below if
332    // alignment is towards top.
333    pos_info.inital_anchor_point.set_y(
334        mouse_click_point_.y() +
335        (pos_info.message_center_alignment & ALIGNMENT_BOTTOM ? -kMouseOffset
336                                                              : kMouseOffset));
337
338    // Subtract the distance between mouse click point and the closest
339    // (insetted) edge from the max height to show the message center within the
340    // (insetted) work area bounds. Also subtract the offset from the mouse
341    // click point we added earlier.
342    pos_info.max_height -=
343        std::abs(mouse_click_point_.y() - corner.y()) + kMouseOffset;
344  }
345  return pos_info;
346}
347
348MessageCenterTray* WebNotificationTray::GetMessageCenterTray() {
349  return message_center_tray_.get();
350}
351
352void WebNotificationTray::CreateStatusIcon(const gfx::ImageSkia& image,
353                                           const base::string16& tool_tip) {
354  if (status_icon_)
355    return;
356
357  StatusTray* status_tray = g_browser_process->status_tray();
358  if (!status_tray)
359    return;
360
361  status_icon_ = status_tray->CreateStatusIcon(
362      StatusTray::NOTIFICATION_TRAY_ICON, image, tool_tip);
363  if (!status_icon_)
364    return;
365
366  status_icon_->AddObserver(this);
367  AddQuietModeMenu(status_icon_);
368}
369
370void WebNotificationTray::DestroyStatusIcon() {
371  if (!status_icon_)
372    return;
373
374  status_icon_->RemoveObserver(this);
375  StatusTray* status_tray = g_browser_process->status_tray();
376  if (status_tray)
377    status_tray->RemoveStatusIcon(status_icon_);
378  status_icon_menu_ = NULL;
379  status_icon_ = NULL;
380}
381
382void WebNotificationTray::AddQuietModeMenu(StatusIcon* status_icon) {
383  DCHECK(status_icon);
384
385  scoped_ptr<StatusIconMenuModel> menu(new StatusIconMenuModel(this));
386  menu->AddCheckItem(kToggleQuietMode,
387                     l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE));
388  menu->SetCommandIdChecked(kToggleQuietMode, message_center()->IsQuietMode());
389  menu->AddItem(kEnableQuietModeHour,
390                l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE_1HOUR));
391  menu->AddItem(kEnableQuietModeDay,
392                l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_QUIET_MODE_1DAY));
393
394  status_icon_menu_ = menu.get();
395  status_icon->SetContextMenu(menu.Pass());
396}
397
398MessageCenterWidgetDelegate*
399WebNotificationTray::GetMessageCenterWidgetDelegateForTest() {
400  return message_center_delegate_;
401}
402
403}  // namespace message_center
404