notification_view.cc revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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 "ui/message_center/views/notification_view.h"
6
7#include "base/command_line.h"
8#include "base/utf_string_conversions.h"
9#include "grit/ui_resources.h"
10#include "ui/base/accessibility/accessible_view_state.h"
11#include "ui/base/resource/resource_bundle.h"
12#include "ui/base/text/text_elider.h"
13#include "ui/gfx/canvas.h"
14#include "ui/gfx/size.h"
15#include "ui/message_center/message_center_constants.h"
16#include "ui/message_center/message_center_switches.h"
17#include "ui/message_center/message_center_util.h"
18#include "ui/message_center/notification.h"
19#include "ui/message_center/notification_change_observer.h"
20#include "ui/message_center/notification_types.h"
21#include "ui/message_center/views/message_simple_view.h"
22#include "ui/native_theme/native_theme.h"
23#include "ui/views/controls/button/image_button.h"
24#include "ui/views/controls/image_view.h"
25#include "ui/views/controls/label.h"
26#include "ui/views/layout/box_layout.h"
27#include "ui/views/layout/fill_layout.h"
28
29namespace {
30
31// Dimensions.
32const int kIconColumnWidth = message_center::kNotificationIconSize;
33const int kLegacyIconSize = 40;
34const int kIconToTextPadding = 16;
35const int kTextTopPadding = 6;
36const int kTextLeftPadding = kIconColumnWidth + kIconToTextPadding;
37const int kTextBottomPadding = 6;
38const int kTextRightPadding = 23;
39const int kItemTitleToMessagePadding = 3;
40const int kButtonHeight = 38;
41const int kButtonHorizontalPadding = 16;
42const int kButtonVecticalPadding = 0;
43const int kButtonIconTopPadding = 11;
44const int kButtonIconToTitlePadding = 16;
45const int kButtonTitleTopPadding = 0;
46
47const size_t kTitleCharacterLimit = 100;
48const size_t kMessageCharacterLimit = 200;
49
50// Notification colors. The text background colors below are used only to keep
51// view::Label from modifying the text color and will not actually be drawn.
52// See view::Label's SetEnabledColor() and SetBackgroundColor() for details.
53const SkColor kBackgroundColor = SkColorSetRGB(255, 255, 255);
54const SkColor kLegacyIconBackgroundColor = SkColorSetRGB(230, 230, 230);
55const SkColor kRegularTextColor = SkColorSetRGB(68, 68, 68);
56const SkColor kRegularTextBackgroundColor = SK_ColorWHITE;
57const SkColor kDimTextColor = SkColorSetRGB(136, 136, 136);
58const SkColor kDimTextBackgroundColor = SK_ColorBLACK;
59const SkColor kButtonSeparatorColor = SkColorSetRGB(234, 234, 234);
60
61// Static.
62views::Background* MakeBackground(SkColor color = kBackgroundColor) {
63  return views::Background::CreateSolidBackground(color);
64}
65
66// Static.
67views::Border* MakeBorder(int top,
68                          int bottom,
69                          int left = kTextLeftPadding,
70                          int right = kTextRightPadding,
71                          SkColor color = 0x00000000) {
72  return (color == 0x00000000) ?
73         views::Border::CreateEmptyBorder(top, left, bottom, right) :
74         views::Border::CreateSolidSidedBorder(top, left, bottom, right, color);
75}
76
77// ContainerView ///////////////////////////////////////////////////////////////
78
79// ContainerViews are vertical BoxLayout views that propagates their childrens'
80// ChildPreferredSizeChanged() and ChildVisibilityChanged() calls.
81class ContainerView : public views::View {
82 public:
83  ContainerView();
84  virtual ~ContainerView();
85
86 protected:
87  virtual void ChildPreferredSizeChanged(View* child) OVERRIDE;
88  virtual void ChildVisibilityChanged(View* child) OVERRIDE;
89
90 private:
91  DISALLOW_COPY_AND_ASSIGN(ContainerView);
92};
93
94ContainerView::ContainerView() {
95  SetLayoutManager(new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0));
96}
97
98ContainerView::~ContainerView() {
99}
100
101void ContainerView::ChildPreferredSizeChanged(View* child) {
102  PreferredSizeChanged();
103}
104
105void ContainerView::ChildVisibilityChanged(View* child) {
106  PreferredSizeChanged();
107}
108
109// ItemView ////////////////////////////////////////////////////////////////////
110
111// ItemViews are responsible for drawing each list notification item's title and
112// message next to each other within a single column.
113class ItemView : public views::View {
114 public:
115  ItemView(const message_center::NotificationItem& item);
116  virtual ~ItemView();
117
118  virtual void SetVisible(bool visible) OVERRIDE;
119
120 private:
121  DISALLOW_COPY_AND_ASSIGN(ItemView);
122};
123
124ItemView::ItemView(const message_center::NotificationItem& item) {
125  SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal,
126                                        0, 0, kItemTitleToMessagePadding));
127
128  views::Label* title = new views::Label(item.title);
129  title->set_collapse_when_hidden(true);
130  title->SetHorizontalAlignment(gfx::ALIGN_LEFT);
131  title->SetElideBehavior(views::Label::ELIDE_AT_END);
132  title->SetEnabledColor(kRegularTextColor);
133  title->SetBackgroundColor(kRegularTextBackgroundColor);
134  AddChildView(title);
135
136  views::Label* message = new views::Label(item.message);
137  message->set_collapse_when_hidden(true);
138  message->SetHorizontalAlignment(gfx::ALIGN_LEFT);
139  message->SetElideBehavior(views::Label::ELIDE_AT_END);
140  message->SetEnabledColor(kDimTextColor);
141  message->SetBackgroundColor(kDimTextBackgroundColor);
142  AddChildView(message);
143
144  PreferredSizeChanged();
145  SchedulePaint();
146}
147
148ItemView::~ItemView() {
149}
150
151void ItemView::SetVisible(bool visible) {
152  views::View::SetVisible(visible);
153  for (int i = 0; i < child_count(); ++i)
154    child_at(i)->SetVisible(visible);
155}
156
157// ProportionalImageView ///////////////////////////////////////////////////////
158
159// ProportionalImageViews center their images to preserve their proportion.
160class ProportionalImageView : public views::View {
161 public:
162  ProportionalImageView(const gfx::ImageSkia& image);
163  virtual ~ProportionalImageView();
164
165  // Overridden from views::View:
166  virtual gfx::Size GetPreferredSize() OVERRIDE;
167  virtual int GetHeightForWidth(int width) OVERRIDE;
168  virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE;
169
170 private:
171  gfx::Size GetImageSizeForWidth(int width);
172
173  gfx::ImageSkia image_;
174
175  DISALLOW_COPY_AND_ASSIGN(ProportionalImageView);
176};
177
178ProportionalImageView::ProportionalImageView(const gfx::ImageSkia& image)
179    : image_(image) {
180}
181
182ProportionalImageView::~ProportionalImageView() {
183}
184
185gfx::Size ProportionalImageView::GetPreferredSize() {
186  gfx::Size size = GetImageSizeForWidth(image_.width());
187  return gfx::Size(size.width() + GetInsets().width(),
188                   size.height() + GetInsets().height());
189}
190
191int ProportionalImageView::GetHeightForWidth(int width) {
192  return GetImageSizeForWidth(width).height();
193}
194
195void ProportionalImageView::OnPaint(gfx::Canvas* canvas) {
196  views::View::OnPaint(canvas);
197
198  gfx::Size draw_size(GetImageSizeForWidth(width()));
199  if (!draw_size.IsEmpty()) {
200    gfx::Rect draw_bounds = GetLocalBounds();
201    draw_bounds.Inset(GetInsets());
202    draw_bounds.ClampToCenteredSize(draw_size);
203
204    gfx::Size image_size(image_.size());
205    if (image_size == draw_size) {
206      canvas->DrawImageInt(image_, draw_bounds.x(), draw_bounds.y());
207    } else {
208      // Resize case
209      SkPaint paint;
210      paint.setFilterBitmap(true);
211      canvas->DrawImageInt(image_, 0, 0,
212                           image_size.width(), image_size.height(),
213                           draw_bounds.x(), draw_bounds.y(),
214                           draw_size.width(), draw_size.height(),
215                           true, paint);
216    }
217  }
218}
219
220gfx::Size ProportionalImageView::GetImageSizeForWidth(int width) {
221  gfx::Size size = visible() ? image_.size() : gfx::Size();
222  if (width > 0 && !size.IsEmpty()) {
223    double proportion = size.height() / (double) size.width();
224    size.SetSize(width, std::max(0.5 + width * proportion, 1.0));
225    if (size.height() > message_center::kNotificationMaximumImageHeight) {
226      int height = message_center::kNotificationMaximumImageHeight;
227      size.SetSize(std::max(0.5 + height / proportion, 1.0), height);
228    }
229  }
230  return size;
231}
232
233// NotificationButton //////////////////////////////////////////////////////////
234
235// NotificationButtons render the action buttons of notifications.
236class NotificationButton : public views::CustomButton {
237 public:
238  NotificationButton(views::ButtonListener* listener);
239  virtual ~NotificationButton();
240
241  void SetIcon(const gfx::ImageSkia& icon);
242  void SetTitle(const string16& title);
243
244  // Overridden from views::View:
245  virtual gfx::Size GetPreferredSize() OVERRIDE;
246  virtual int GetHeightForWidth(int width) OVERRIDE;
247
248 private:
249  views::ImageView* icon_;
250  views::Label* title_;
251};
252
253NotificationButton::NotificationButton(views::ButtonListener* listener)
254    : views::CustomButton(listener),
255      icon_(NULL),
256      title_(NULL) {
257  set_focusable(true);
258  SetLayoutManager(new views::BoxLayout(views::BoxLayout::kHorizontal,
259                                        kButtonHorizontalPadding,
260                                        kButtonVecticalPadding,
261                                        kButtonIconToTitlePadding));
262}
263
264NotificationButton::~NotificationButton() {
265}
266
267void NotificationButton::SetIcon(const gfx::ImageSkia& image) {
268  if (icon_ != NULL)
269    delete icon_;  // This removes the icon from this view's children.
270  if (image.isNull()) {
271    icon_ = NULL;
272  } else {
273    icon_ = new views::ImageView();
274    icon_->SetImageSize(gfx::Size(message_center::kNotificationButtonIconSize,
275                                  message_center::kNotificationButtonIconSize));
276    icon_->SetImage(image);
277    icon_->SetHorizontalAlignment(views::ImageView::LEADING);
278    icon_->SetVerticalAlignment(views::ImageView::LEADING);
279    icon_->set_border(MakeBorder(kButtonIconTopPadding, 0, 0, 0));
280    AddChildViewAt(icon_, 0);
281  }
282}
283
284void NotificationButton::SetTitle(const string16& title) {
285  if (title_ != NULL)
286    delete title_;  // This removes the title from this view's children.
287  if (title.empty()) {
288    title_ = NULL;
289  } else {
290    title_ = new views::Label(title);
291    title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
292    title_->SetElideBehavior(views::Label::ELIDE_AT_END);
293    title_->SetEnabledColor(kRegularTextColor);
294    title_->SetBackgroundColor(kRegularTextBackgroundColor);
295    title_->set_border(MakeBorder(kButtonTitleTopPadding, 0, 0, 0));
296    AddChildView(title_);
297  }
298}
299
300gfx::Size NotificationButton::GetPreferredSize() {
301  return gfx::Size(message_center::kNotificationWidth, kButtonHeight);
302}
303
304int NotificationButton::GetHeightForWidth(int width) {
305  return kButtonHeight;
306}
307
308}  // namespace
309
310namespace message_center {
311
312// NotificationView ////////////////////////////////////////////////////////////
313
314// static
315MessageView* NotificationView::Create(const Notification& notification,
316                                      NotificationChangeObserver* observer,
317                                      bool expanded) {
318  // Use MessageSimpleView for simple notifications unless rich style
319  // notifications are enabled. This preserves the appearance of notifications
320  // created by existing code that uses webkitNotifications.
321  if (!IsRichNotificationEnabled() &&
322      notification.type() == NOTIFICATION_TYPE_SIMPLE)
323    return new MessageSimpleView(notification, observer);
324
325  switch (notification.type()) {
326    case NOTIFICATION_TYPE_BASE_FORMAT:
327    case NOTIFICATION_TYPE_IMAGE:
328    case NOTIFICATION_TYPE_MULTIPLE:
329    case NOTIFICATION_TYPE_SIMPLE:
330      break;
331    default:
332      // If the caller asks for an unrecognized kind of view (entirely possible
333      // if an application is running on an older version of this code that
334      // doesn't have the requested kind of notification template), we'll fall
335      // back to a notification instance that will provide at least basic
336      // functionality.
337      LOG(WARNING) << "Unable to fulfill request for unrecognized "
338                   << "notification type " << notification.type() << ". "
339                   << "Falling back to simple notification type.";
340  }
341
342  // Currently all roads lead to the generic NotificationView.
343  return new NotificationView(notification, observer, expanded);
344}
345
346NotificationView::NotificationView(const Notification& notification,
347                                   NotificationChangeObserver* observer,
348                                   bool expanded)
349    : MessageView(notification, observer, expanded) {
350  // Create the opaque background that's above the view's shadow.
351  background_view_ = new views::View();
352  background_view_->set_background(MakeBackground());
353
354  // Create the top_view_, which collects into a vertical box all content
355  // at the top of the notification (to the right of the icon) except for the
356  // close button.
357  top_view_ = new ContainerView();
358
359  // Create the title view if appropriate.
360  title_view_ = NULL;
361  if (!notification.title().empty()) {
362    title_view_ = new views::Label(
363        MaybeTruncateText( notification.title(), kTitleCharacterLimit));
364    title_view_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
365    if (is_expanded())
366      title_view_->SetMultiLine(true);
367    else
368      title_view_->SetElideBehavior(views::Label::ELIDE_AT_END);
369    title_view_->SetFont(title_view_->font().DeriveFont(2));
370    title_view_->SetEnabledColor(kRegularTextColor);
371    title_view_->SetBackgroundColor(kRegularTextBackgroundColor);
372    title_view_->set_border(MakeBorder(kTextTopPadding, 3));
373    top_view_->AddChildView(title_view_);
374  }
375
376  // Create the message view if appropriate.
377  message_view_ = NULL;
378  if (!notification.message().empty()) {
379    message_view_ = new views::Label(
380        MaybeTruncateText(notification.message(), kMessageCharacterLimit));
381    message_view_->SetVisible(!is_expanded() || !notification.items().size());
382    message_view_->set_collapse_when_hidden(true);
383    message_view_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
384    if (is_expanded())
385      message_view_->SetMultiLine(true);
386    else
387      message_view_->SetElideBehavior(views::Label::ELIDE_AT_END);
388    message_view_->SetEnabledColor(kRegularTextColor);
389    message_view_->SetBackgroundColor(kRegularTextBackgroundColor);
390    message_view_->set_border(MakeBorder(0, 3));
391    top_view_->AddChildView(message_view_);
392  }
393
394  // Create the list item views (up to a maximum).
395  std::vector<NotificationItem> items = notification.items();
396  for (size_t i = 0; i < items.size() && i < kNotificationMaximumItems; ++i) {
397    ItemView* item_view = new ItemView(items[i]);
398    item_view->SetVisible(is_expanded());
399    item_view->set_border(MakeBorder(0, 4));
400    item_views_.push_back(item_view);
401    top_view_->AddChildView(item_view);
402  }
403
404  // Create the notification icon view.
405  if (notification.type() == NOTIFICATION_TYPE_SIMPLE) {
406    views::ImageView* icon_view = new views::ImageView();
407    icon_view->SetImage(notification.icon().AsImageSkia());
408    icon_view->SetImageSize(gfx::Size(kLegacyIconSize, kLegacyIconSize));
409    icon_view->SetHorizontalAlignment(views::ImageView::CENTER);
410    icon_view->SetVerticalAlignment(views::ImageView::CENTER);
411    icon_view->set_background(MakeBackground(kLegacyIconBackgroundColor));
412    icon_view_ = icon_view;
413  } else {
414    icon_view_ = new ProportionalImageView(notification.icon().AsImageSkia());
415  }
416
417  // Create the bottom_view_, which collects into a vertical box all content
418  // below the notification icon except for the expand button.
419  bottom_view_ = new ContainerView();
420
421  // Create the image view if appropriate.
422  image_view_ = NULL;
423  if (!notification.image().IsEmpty()) {
424    image_view_ = new ProportionalImageView(notification.image().AsImageSkia());
425    image_view_->SetVisible(is_expanded());
426    bottom_view_->AddChildView(image_view_);
427  }
428
429  // Create action buttons if appropriate.
430  std::vector<ButtonInfo> buttons = notification.buttons();
431  for (size_t i = 0; i < buttons.size(); ++i) {
432    views::View* separator = new views::ImageView();
433    separator->set_border(MakeBorder(1, 0, 0, 0, kButtonSeparatorColor));
434    bottom_view_->AddChildView(separator);
435    NotificationButton* button = new NotificationButton(this);
436    ButtonInfo button_info = buttons[i];
437    button->SetTitle(button_info.title);
438    button->SetIcon(button_info.icon.AsImageSkia());
439    action_buttons_.push_back(button);
440    bottom_view_->AddChildView(button);
441  }
442
443  // Hide the expand button if appropriate.
444  bool expandable = item_views_.size() || image_view_;
445  expand_button()->SetVisible(expandable && !is_expanded());
446
447  // Put together the different content and control views. Layering those allows
448  // for proper layout logic and it also allows the close and expand buttons to
449  // overlap the content as needed to provide large enough click and touch areas
450  // (<http://crbug.com/168822> and <http://crbug.com/168856>).
451  AddChildView(background_view_);
452  AddChildView(top_view_);
453  AddChildView(icon_view_);
454  AddChildView(bottom_view_);
455  AddChildView(close_button());
456  AddChildView(expand_button());
457}
458
459NotificationView::~NotificationView() {
460}
461
462gfx::Size NotificationView::GetPreferredSize() {
463  int top_width = top_view_->GetPreferredSize().width();
464  int bottom_width = bottom_view_->GetPreferredSize().width();
465  int preferred_width = std::max(top_width, bottom_width) + GetInsets().width();
466  return gfx::Size(preferred_width, GetHeightForWidth(preferred_width));
467}
468
469int NotificationView::GetHeightForWidth(int width) {
470  gfx::Insets insets = GetInsets();
471  int top_height = top_view_->GetHeightForWidth(width - insets.width());
472  int bottom_height = bottom_view_->GetHeightForWidth(width - insets.width());
473  int icon_size = message_center::kNotificationIconSize;
474  return std::max(top_height, icon_size) + bottom_height + insets.height();
475}
476
477void NotificationView::Layout() {
478  gfx::Insets insets = GetInsets();
479  int content_width = width() - insets.width();
480  int content_right = width() - insets.right();
481
482  background_view_->SetBounds(insets.left(), insets.top(),
483                              content_width, height() - insets.height());
484
485  int top_height = top_view_->GetHeightForWidth(content_width);
486  top_view_->SetBounds(insets.left(), insets.top(), content_width, top_height);
487
488  int icon_size = message_center::kNotificationIconSize;
489  icon_view_->SetBounds(insets.left(), insets.top(), icon_size, icon_size);
490
491  int bottom_y = insets.top() + std::max(top_height, icon_size);
492  int bottom_height = bottom_view_->GetHeightForWidth(content_width);
493  bottom_view_->SetBounds(insets.left(), bottom_y,
494                          content_width, bottom_height);
495
496  gfx::Size close_size(close_button()->GetPreferredSize());
497  close_button()->SetBounds(content_right - close_size.width(), insets.top(),
498                            close_size.width(), close_size.height());
499
500  gfx::Size expand_size(expand_button()->GetPreferredSize());
501  int expand_y = bottom_y - expand_size.height();
502  expand_button()->SetBounds(content_right - expand_size.width(), expand_y,
503                             expand_size.width(), expand_size.height());
504}
505
506void NotificationView::ButtonPressed(views::Button* sender,
507                                     const ui::Event& event) {
508  // See if the button pressed was an action button.
509  for (size_t i = 0; i < action_buttons_.size(); ++i) {
510    if (sender == action_buttons_[i]) {
511      observer()->OnButtonClicked(notification_id(), i);
512      return;
513    }
514  }
515
516  // Let the superclass handled anything other than action buttons.
517  MessageView::ButtonPressed(sender, event);
518
519  // Show and hide subviews appropriately on expansion.
520  if (sender == expand_button()) {
521    if (message_view_ && item_views_.size())
522      message_view_->SetVisible(false);
523    for (size_t i = 0; i < item_views_.size(); ++i)
524      item_views_[i]->SetVisible(true);
525    if (image_view_)
526      image_view_->SetVisible(true);
527    expand_button()->SetVisible(false);
528    PreferredSizeChanged();
529    SchedulePaint();
530  }
531}
532
533string16 NotificationView::MaybeTruncateText(const string16& text,
534                                             size_t limit) {
535  // Currently just truncate the text by the total number of characters.
536  // TODO(mukai): add better assumption like number of lines.
537  if (!is_expanded())
538    return text;
539
540  return ui::TruncateString(text, limit);
541}
542
543}  // namespace message_center
544