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