1// Copyright (c) 2012 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 "ash/system/web_notification/web_notification_tray.h"
6
7#include "ash/ash_switches.h"
8#include "ash/root_window_controller.h"
9#include "ash/shelf/shelf_layout_manager.h"
10#include "ash/shelf/shelf_layout_manager_observer.h"
11#include "ash/shelf/shelf_widget.h"
12#include "ash/shell.h"
13#include "ash/shell_window_ids.h"
14#include "ash/system/status_area_widget.h"
15#include "ash/system/tray/system_tray.h"
16#include "ash/system/tray/tray_background_view.h"
17#include "ash/system/tray/tray_bubble_wrapper.h"
18#include "ash/system/tray/tray_constants.h"
19#include "ash/system/tray/tray_utils.h"
20#include "ash/system/web_notification/ash_popup_alignment_delegate.h"
21#include "base/auto_reset.h"
22#include "base/i18n/number_formatting.h"
23#include "base/i18n/rtl.h"
24#include "base/strings/utf_string_conversions.h"
25#include "grit/ash_strings.h"
26#include "ui/aura/window.h"
27#include "ui/aura/window_event_dispatcher.h"
28#include "ui/base/l10n/l10n_util.h"
29#include "ui/gfx/screen.h"
30#include "ui/message_center/message_center_style.h"
31#include "ui/message_center/message_center_tray_delegate.h"
32#include "ui/message_center/views/message_bubble_base.h"
33#include "ui/message_center/views/message_center_bubble.h"
34#include "ui/message_center/views/message_popup_collection.h"
35#include "ui/strings/grit/ui_strings.h"
36#include "ui/views/bubble/tray_bubble_view.h"
37#include "ui/views/controls/button/custom_button.h"
38#include "ui/views/controls/image_view.h"
39#include "ui/views/controls/label.h"
40#include "ui/views/controls/menu/menu_runner.h"
41#include "ui/views/layout/fill_layout.h"
42
43#if defined(OS_CHROMEOS)
44
45namespace message_center {
46
47MessageCenterTrayDelegate* CreateMessageCenterTray() {
48  // On Windows+Ash the Tray will not be hosted in ash::Shell.
49  NOTREACHED();
50  return NULL;
51}
52
53}  // namespace message_center
54
55#endif  // defined(OS_CHROMEOS)
56
57namespace ash {
58namespace {
59
60// Menu commands
61const int kToggleQuietMode = 0;
62const int kEnableQuietModeDay = 2;
63
64}
65
66namespace {
67
68const SkColor kWebNotificationColorNoUnread =
69    SkColorSetARGB(128, 255, 255, 255);
70const SkColor kWebNotificationColorWithUnread = SK_ColorWHITE;
71
72}
73
74// Class to initialize and manage the WebNotificationBubble and
75// TrayBubbleWrapper instances for a bubble.
76class WebNotificationBubbleWrapper {
77 public:
78  // Takes ownership of |bubble| and creates |bubble_wrapper_|.
79  WebNotificationBubbleWrapper(WebNotificationTray* tray,
80                               message_center::MessageBubbleBase* bubble) {
81    bubble_.reset(bubble);
82    views::TrayBubbleView::AnchorAlignment anchor_alignment =
83        tray->GetAnchorAlignment();
84    views::TrayBubbleView::InitParams init_params =
85        bubble->GetInitParams(anchor_alignment);
86    views::View* anchor = tray->tray_container();
87    if (anchor_alignment == views::TrayBubbleView::ANCHOR_ALIGNMENT_BOTTOM) {
88      gfx::Point bounds(anchor->width() / 2, 0);
89      views::View::ConvertPointToWidget(anchor, &bounds);
90      init_params.arrow_offset = bounds.x();
91    }
92    views::TrayBubbleView* bubble_view = views::TrayBubbleView::Create(
93        tray->GetBubbleWindowContainer(), anchor, tray, &init_params);
94    bubble_wrapper_.reset(new TrayBubbleWrapper(tray, bubble_view));
95    bubble_view->SetArrowPaintType(views::BubbleBorder::PAINT_NONE);
96    bubble->InitializeContents(bubble_view);
97  }
98
99  message_center::MessageBubbleBase* bubble() const { return bubble_.get(); }
100
101  // Convenience accessors.
102  views::TrayBubbleView* bubble_view() const { return bubble_->bubble_view(); }
103
104 private:
105  scoped_ptr<message_center::MessageBubbleBase> bubble_;
106  scoped_ptr<TrayBubbleWrapper> bubble_wrapper_;
107
108  DISALLOW_COPY_AND_ASSIGN(WebNotificationBubbleWrapper);
109};
110
111class WebNotificationButton : public views::CustomButton {
112 public:
113  WebNotificationButton(views::ButtonListener* listener)
114      : views::CustomButton(listener),
115        is_bubble_visible_(false),
116        unread_count_(0) {
117    SetLayoutManager(new views::FillLayout);
118    unread_label_ = new views::Label();
119    SetupLabelForTray(unread_label_);
120    AddChildView(unread_label_);
121  }
122
123  void SetBubbleVisible(bool visible) {
124    if (visible == is_bubble_visible_)
125      return;
126
127    is_bubble_visible_ = visible;
128    UpdateIconVisibility();
129  }
130
131  void SetUnreadCount(int unread_count) {
132    // base::FormatNumber doesn't convert to arabic numeric characters.
133    // TODO(mukai): use ICU to support conversion for such locales.
134    unread_count_ = unread_count;
135    // TODO(mukai): move NINE_PLUS message to ui_strings, it doesn't need to be
136    // in ash_strings.
137    unread_label_->SetText((unread_count > 9) ?
138        l10n_util::GetStringUTF16(IDS_ASH_NOTIFICATION_UNREAD_COUNT_NINE_PLUS) :
139        base::FormatNumber(unread_count));
140    UpdateIconVisibility();
141  }
142
143 protected:
144  // Overridden from views::ImageButton:
145  virtual gfx::Size GetPreferredSize() const OVERRIDE {
146    return gfx::Size(kShelfItemHeight, kShelfItemHeight);
147  }
148
149  virtual int GetHeightForWidth(int width) const OVERRIDE {
150    return GetPreferredSize().height();
151  }
152
153 private:
154  void UpdateIconVisibility() {
155    unread_label_->SetEnabledColor(
156        (!is_bubble_visible_ && unread_count_ > 0) ?
157        kWebNotificationColorWithUnread : kWebNotificationColorNoUnread);
158    SchedulePaint();
159  }
160
161  bool is_bubble_visible_;
162  int unread_count_;
163
164  views::Label* unread_label_;
165
166  DISALLOW_COPY_AND_ASSIGN(WebNotificationButton);
167};
168
169WebNotificationTray::WebNotificationTray(StatusAreaWidget* status_area_widget)
170    : TrayBackgroundView(status_area_widget),
171      button_(NULL),
172      show_message_center_on_unlock_(false),
173      should_update_tray_content_(false),
174      should_block_shelf_auto_hide_(false) {
175  button_ = new WebNotificationButton(this);
176  button_->set_triggerable_event_flags(
177      ui::EF_LEFT_MOUSE_BUTTON | ui::EF_RIGHT_MOUSE_BUTTON);
178  tray_container()->AddChildView(button_);
179  SetContentsBackground();
180  tray_container()->SetBorder(views::Border::NullBorder());
181  message_center_tray_.reset(new message_center::MessageCenterTray(
182      this,
183      message_center::MessageCenter::Get()));
184  popup_alignment_delegate_.reset(new AshPopupAlignmentDelegate());
185  popup_collection_.reset(new message_center::MessagePopupCollection(
186      ash::Shell::GetContainer(
187          status_area_widget->GetNativeView()->GetRootWindow(),
188          kShellWindowId_StatusContainer),
189      message_center(),
190      message_center_tray_.get(),
191      popup_alignment_delegate_.get()));
192  const gfx::Display& display = Shell::GetScreen()->GetDisplayNearestWindow(
193      status_area_widget->GetNativeView());
194  popup_alignment_delegate_->StartObserving(Shell::GetScreen(), display);
195  OnMessageCenterTrayChanged();
196}
197
198WebNotificationTray::~WebNotificationTray() {
199  // Release any child views that might have back pointers before ~View().
200  message_center_bubble_.reset();
201  popup_alignment_delegate_.reset();
202  popup_collection_.reset();
203}
204
205// Public methods.
206
207bool WebNotificationTray::ShowMessageCenterInternal(bool show_settings) {
208  if (!ShouldShowMessageCenter())
209    return false;
210
211  should_block_shelf_auto_hide_ = true;
212  message_center::MessageCenterBubble* message_center_bubble =
213      new message_center::MessageCenterBubble(
214          message_center(),
215          message_center_tray_.get(),
216          true);
217
218  int max_height = 0;
219  aura::Window* status_area_window = status_area_widget()->GetNativeView();
220  switch (GetShelfLayoutManager()->GetAlignment()) {
221    case SHELF_ALIGNMENT_BOTTOM: {
222      gfx::Rect shelf_bounds = GetShelfLayoutManager()->GetIdealBounds();
223      max_height = shelf_bounds.y();
224      break;
225    }
226    case SHELF_ALIGNMENT_TOP: {
227      aura::Window* root = status_area_window->GetRootWindow();
228      max_height =
229          root->bounds().height() - status_area_window->bounds().height();
230      break;
231    }
232    case SHELF_ALIGNMENT_LEFT:
233    case SHELF_ALIGNMENT_RIGHT: {
234      // Assume that the bottom line of the status area widget and the bubble
235      // are aligned.
236      max_height = status_area_window->GetBoundsInRootWindow().bottom();
237      break;
238    }
239    default:
240      NOTREACHED();
241  }
242
243  message_center_bubble->SetMaxHeight(std::max(0,
244                                               max_height - kTraySpacing));
245  if (show_settings)
246    message_center_bubble->SetSettingsVisible();
247  message_center_bubble_.reset(
248      new WebNotificationBubbleWrapper(this, message_center_bubble));
249
250  status_area_widget()->SetHideSystemNotifications(true);
251  GetShelfLayoutManager()->UpdateAutoHideState();
252  button_->SetBubbleVisible(true);
253  SetDrawBackgroundAsActive(true);
254  return true;
255}
256
257bool WebNotificationTray::ShowMessageCenter() {
258  return ShowMessageCenterInternal(false /* show_settings */);
259}
260
261void WebNotificationTray::HideMessageCenter() {
262  if (!message_center_bubble())
263    return;
264  SetDrawBackgroundAsActive(false);
265  message_center_bubble_.reset();
266  should_block_shelf_auto_hide_ = false;
267  show_message_center_on_unlock_ = false;
268  status_area_widget()->SetHideSystemNotifications(false);
269  GetShelfLayoutManager()->UpdateAutoHideState();
270  button_->SetBubbleVisible(false);
271}
272
273void WebNotificationTray::SetSystemTrayHeight(int height) {
274  popup_alignment_delegate_->SetSystemTrayHeight(height);
275}
276
277bool WebNotificationTray::ShowPopups() {
278  if (message_center_bubble())
279    return false;
280
281  popup_collection_->DoUpdateIfPossible();
282  return true;
283}
284
285void WebNotificationTray::HidePopups() {
286  DCHECK(popup_collection_.get());
287  popup_collection_->MarkAllPopupsShown();
288}
289
290// Private methods.
291
292bool WebNotificationTray::ShouldShowMessageCenter() {
293  return status_area_widget()->login_status() != user::LOGGED_IN_LOCKED &&
294      !(status_area_widget()->system_tray() &&
295        status_area_widget()->system_tray()->HasNotificationBubble());
296}
297
298bool WebNotificationTray::ShouldBlockShelfAutoHide() const {
299  return should_block_shelf_auto_hide_;
300}
301
302bool WebNotificationTray::IsMessageCenterBubbleVisible() const {
303  return (message_center_bubble() &&
304          message_center_bubble()->bubble()->IsVisible());
305}
306
307bool WebNotificationTray::IsMouseInNotificationBubble() const {
308  return false;
309}
310
311void WebNotificationTray::ShowMessageCenterBubble() {
312  if (!IsMessageCenterBubbleVisible())
313    message_center_tray_->ShowMessageCenterBubble();
314}
315
316void WebNotificationTray::UpdateAfterLoginStatusChange(
317    user::LoginStatus login_status) {
318  OnMessageCenterTrayChanged();
319}
320
321void WebNotificationTray::SetShelfAlignment(ShelfAlignment alignment) {
322  if (alignment == shelf_alignment())
323    return;
324  TrayBackgroundView::SetShelfAlignment(alignment);
325  tray_container()->SetBorder(views::Border::NullBorder());
326  // Destroy any existing bubble so that it will be rebuilt correctly.
327  message_center_tray_->HideMessageCenterBubble();
328  message_center_tray_->HidePopupBubble();
329}
330
331void WebNotificationTray::AnchorUpdated() {
332  if (message_center_bubble()) {
333    message_center_bubble()->bubble_view()->UpdateBubble();
334    UpdateBubbleViewArrow(message_center_bubble()->bubble_view());
335  }
336}
337
338base::string16 WebNotificationTray::GetAccessibleNameForTray() {
339  return l10n_util::GetStringUTF16(
340      IDS_MESSAGE_CENTER_ACCESSIBLE_NAME);
341}
342
343void WebNotificationTray::HideBubbleWithView(
344    const views::TrayBubbleView* bubble_view) {
345  if (message_center_bubble() &&
346      bubble_view == message_center_bubble()->bubble_view()) {
347    message_center_tray_->HideMessageCenterBubble();
348  } else if (popup_collection_.get()) {
349    message_center_tray_->HidePopupBubble();
350  }
351}
352
353bool WebNotificationTray::PerformAction(const ui::Event& event) {
354  if (message_center_bubble())
355    message_center_tray_->HideMessageCenterBubble();
356  else
357    message_center_tray_->ShowMessageCenterBubble();
358  return true;
359}
360
361void WebNotificationTray::BubbleViewDestroyed() {
362  if (message_center_bubble())
363    message_center_bubble()->bubble()->BubbleViewDestroyed();
364}
365
366void WebNotificationTray::OnMouseEnteredView() {}
367
368void WebNotificationTray::OnMouseExitedView() {}
369
370base::string16 WebNotificationTray::GetAccessibleNameForBubble() {
371  return GetAccessibleNameForTray();
372}
373
374gfx::Rect WebNotificationTray::GetAnchorRect(
375    views::Widget* anchor_widget,
376    views::TrayBubbleView::AnchorType anchor_type,
377    views::TrayBubbleView::AnchorAlignment anchor_alignment) const {
378  return GetBubbleAnchorRect(anchor_widget, anchor_type, anchor_alignment);
379}
380
381void WebNotificationTray::HideBubble(const views::TrayBubbleView* bubble_view) {
382  HideBubbleWithView(bubble_view);
383}
384
385bool WebNotificationTray::ShowNotifierSettings() {
386  if (message_center_bubble()) {
387    static_cast<message_center::MessageCenterBubble*>(
388        message_center_bubble()->bubble())->SetSettingsVisible();
389    return true;
390  }
391  return ShowMessageCenterInternal(true /* show_settings */);
392}
393
394bool WebNotificationTray::IsContextMenuEnabled() const {
395  user::LoginStatus login_status = status_area_widget()->login_status();
396  bool userAddingRunning = ash::Shell::GetInstance()
397                               ->session_state_delegate()
398                               ->IsInSecondaryLoginScreen();
399
400  return login_status != user::LOGGED_IN_NONE
401      && login_status != user::LOGGED_IN_LOCKED && !userAddingRunning;
402}
403
404message_center::MessageCenterTray* WebNotificationTray::GetMessageCenterTray() {
405  return message_center_tray_.get();
406}
407
408bool WebNotificationTray::IsCommandIdChecked(int command_id) const {
409  if (command_id != kToggleQuietMode)
410    return false;
411  return message_center()->IsQuietMode();
412}
413
414bool WebNotificationTray::IsCommandIdEnabled(int command_id) const {
415  return true;
416}
417
418bool WebNotificationTray::GetAcceleratorForCommandId(
419    int command_id,
420    ui::Accelerator* accelerator) {
421  return false;
422}
423
424void WebNotificationTray::ExecuteCommand(int command_id, int event_flags) {
425  if (command_id == kToggleQuietMode) {
426    bool in_quiet_mode = message_center()->IsQuietMode();
427    message_center()->SetQuietMode(!in_quiet_mode);
428    return;
429  }
430  base::TimeDelta expires_in = command_id == kEnableQuietModeDay ?
431      base::TimeDelta::FromDays(1):
432      base::TimeDelta::FromHours(1);
433  message_center()->EnterQuietModeWithExpire(expires_in);
434}
435
436void WebNotificationTray::ButtonPressed(views::Button* sender,
437                                        const ui::Event& event) {
438  DCHECK_EQ(button_, sender);
439  PerformAction(event);
440}
441
442void WebNotificationTray::OnMessageCenterTrayChanged() {
443  // Do not update the tray contents directly. Multiple change events can happen
444  // consecutively, and calling Update in the middle of those events will show
445  // intermediate unread counts for a moment.
446  should_update_tray_content_ = true;
447  base::MessageLoop::current()->PostTask(
448      FROM_HERE,
449      base::Bind(&WebNotificationTray::UpdateTrayContent, AsWeakPtr()));
450}
451
452void WebNotificationTray::UpdateTrayContent() {
453  if (!should_update_tray_content_)
454    return;
455  should_update_tray_content_ = false;
456
457  message_center::MessageCenter* message_center =
458      message_center_tray_->message_center();
459  button_->SetUnreadCount(message_center->UnreadNotificationCount());
460  if (IsMessageCenterBubbleVisible())
461    button_->SetState(views::CustomButton::STATE_PRESSED);
462  else
463    button_->SetState(views::CustomButton::STATE_NORMAL);
464  bool userAddingRunning = ash::Shell::GetInstance()
465                               ->session_state_delegate()
466                               ->IsInSecondaryLoginScreen();
467
468  SetVisible((status_area_widget()->login_status() != user::LOGGED_IN_NONE) &&
469             (status_area_widget()->login_status() != user::LOGGED_IN_LOCKED) &&
470             !userAddingRunning && (message_center->NotificationCount() > 0));
471  Layout();
472  SchedulePaint();
473}
474
475bool WebNotificationTray::ClickedOutsideBubble() {
476  // Only hide the message center
477  if (!message_center_bubble())
478    return false;
479
480  message_center_tray_->HideMessageCenterBubble();
481  return true;
482}
483
484message_center::MessageCenter* WebNotificationTray::message_center() const {
485  return message_center_tray_->message_center();
486}
487
488// Methods for testing
489
490bool WebNotificationTray::IsPopupVisible() const {
491  return message_center_tray_->popups_visible();
492}
493
494message_center::MessageCenterBubble*
495WebNotificationTray::GetMessageCenterBubbleForTest() {
496  if (!message_center_bubble())
497    return NULL;
498  return static_cast<message_center::MessageCenterBubble*>(
499      message_center_bubble()->bubble());
500}
501
502}  // namespace ash
503