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/message_view.h" 6 7 #include "grit/ui_resources.h" 8 #include "grit/ui_strings.h" 9 #include "ui/base/accessibility/accessible_view_state.h" 10 #include "ui/base/l10n/l10n_util.h" 11 #include "ui/base/models/simple_menu_model.h" 12 #include "ui/base/resource/resource_bundle.h" 13 #include "ui/compositor/scoped_layer_animation_settings.h" 14 #include "ui/gfx/canvas.h" 15 #include "ui/message_center/message_center.h" 16 #include "ui/message_center/message_center_style.h" 17 #include "ui/message_center/message_center_tray.h" 18 #include "ui/message_center/message_center_util.h" 19 #include "ui/views/context_menu_controller.h" 20 #include "ui/views/controls/button/image_button.h" 21 #include "ui/views/controls/menu/menu_runner.h" 22 #include "ui/views/controls/scroll_view.h" 23 #include "ui/views/shadow_border.h" 24 #include "ui/views/widget/widget.h" 25 26 namespace { 27 28 const int kCloseIconTopPadding = 5; 29 const int kCloseIconRightPadding = 5; 30 const int kExpandIconBottomPadding = 8; 31 const int kExpandIconRightPadding = 11; 32 33 const int kShadowOffset = 1; 34 const int kShadowBlur = 4; 35 36 // Menu constants 37 const int kTogglePermissionCommand = 0; 38 const int kToggleExtensionCommand = 1; 39 const int kShowSettingsCommand = 2; 40 41 // ControlButtons are ImageButtons whose image can be padded within the button. 42 // This allows the creation of buttons like the notification close and expand 43 // buttons whose clickable areas extends beyond their image areas 44 // (<http://crbug.com/168822>) without the need to create and maintain 45 // corresponding resource images with alpha padding. In the future, this class 46 // will also allow for buttons whose touch areas extend beyond their clickable 47 // area (<http://crbug.com/168856>). 48 class ControlButton : public views::ImageButton { 49 public: 50 ControlButton(views::ButtonListener* listener); 51 virtual ~ControlButton(); 52 53 // Overridden from views::ImageButton: 54 virtual gfx::Size GetPreferredSize() OVERRIDE; 55 virtual void OnPaint(gfx::Canvas* canvas) OVERRIDE; 56 virtual void OnFocus() OVERRIDE; 57 virtual void OnPaintFocusBorder(gfx::Canvas* canvas) OVERRIDE; 58 59 // The SetPadding() method also sets the button's image alignment (positive 60 // values yield left/top alignments, negative values yield right/bottom ones, 61 // and zero values center/middle ones). ImageButton::SetImageAlignment() calls 62 // will not affect ControlButton image alignments. 63 void SetPadding(int horizontal_padding, int vertical_padding); 64 65 void SetNormalImage(int resource_id); 66 void SetHoveredImage(int resource_id); 67 void SetPressedImage(int resource_id); 68 69 protected: 70 gfx::Point ComputePaddedImagePaintPosition(const gfx::ImageSkia& image); 71 72 private: 73 gfx::Insets padding_; 74 75 DISALLOW_COPY_AND_ASSIGN(ControlButton); 76 }; 77 78 ControlButton::ControlButton(views::ButtonListener* listener) 79 : views::ImageButton(listener) { 80 set_focusable(true); 81 set_request_focus_on_press(false); 82 } 83 84 ControlButton::~ControlButton() { 85 } 86 87 void ControlButton::SetPadding(int horizontal_padding, int vertical_padding) { 88 padding_.Set(std::max(vertical_padding, 0), 89 std::max(horizontal_padding, 0), 90 std::max(-vertical_padding, 0), 91 std::max(-horizontal_padding, 0)); 92 } 93 94 void ControlButton::SetNormalImage(int resource_id) { 95 SetImage(views::CustomButton::STATE_NORMAL, 96 ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 97 resource_id)); 98 } 99 100 void ControlButton::SetHoveredImage(int resource_id) { 101 SetImage(views::CustomButton::STATE_HOVERED, 102 ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 103 resource_id)); 104 } 105 106 void ControlButton::SetPressedImage(int resource_id) { 107 SetImage(views::CustomButton::STATE_PRESSED, 108 ResourceBundle::GetSharedInstance().GetImageSkiaNamed( 109 resource_id)); 110 } 111 112 gfx::Size ControlButton::GetPreferredSize() { 113 return gfx::Size(message_center::kControlButtonSize, 114 message_center::kControlButtonSize); 115 } 116 117 void ControlButton::OnPaint(gfx::Canvas* canvas) { 118 // This is the same implementation as ImageButton::OnPaint except 119 // that it calls ComputePaddedImagePaintPosition() instead of 120 // ComputeImagePaintPosition(), in effect overriding that private method. 121 View::OnPaint(canvas); 122 gfx::ImageSkia image = GetImageToPaint(); 123 if (!image.isNull()) { 124 gfx::Point position = ComputePaddedImagePaintPosition(image); 125 if (!background_image_.isNull()) 126 canvas->DrawImageInt(background_image_, position.x(), position.y()); 127 canvas->DrawImageInt(image, position.x(), position.y()); 128 if (!overlay_image_.isNull()) 129 canvas->DrawImageInt(overlay_image_, position.x(), position.y()); 130 } 131 OnPaintFocusBorder(canvas); 132 } 133 134 void ControlButton::OnFocus() { 135 views::ImageButton::OnFocus(); 136 ScrollRectToVisible(GetLocalBounds()); 137 } 138 139 void ControlButton::OnPaintFocusBorder(gfx::Canvas* canvas) { 140 if (HasFocus() && (focusable() || IsAccessibilityFocusable())) { 141 canvas->DrawRect(gfx::Rect(2, 1, width() - 4, height() - 3), 142 message_center::kFocusBorderColor); 143 } 144 } 145 146 gfx::Point ControlButton::ComputePaddedImagePaintPosition( 147 const gfx::ImageSkia& image) { 148 gfx::Vector2d offset; 149 gfx::Rect bounds = GetContentsBounds(); 150 bounds.Inset(padding_); 151 152 if (padding_.left() == 0 && padding_.right() == 0) 153 offset.set_x((bounds.width() - image.width()) / 2); // Center align. 154 else if (padding_.right() > 0) 155 offset.set_x(bounds.width() - image.width()); // Right align. 156 157 if (padding_.top() == 0 && padding_.bottom() == 0) 158 offset.set_y((bounds.height() - image.height()) / 2); // Middle align. 159 else if (padding_.bottom() > 0) 160 offset.set_y(bounds.height() - image.height()); // Bottom align. 161 162 return bounds.origin() + offset; 163 } 164 165 // A dropdown menu for notifications. 166 class MenuModel : public ui::SimpleMenuModel, 167 public ui::SimpleMenuModel::Delegate { 168 public: 169 MenuModel(message_center::MessageCenter* message_center, 170 message_center::MessageCenterTray* tray, 171 const std::string& notification_id, 172 const string16& display_source, 173 const std::string& extension_id); 174 virtual ~MenuModel(); 175 176 // Overridden from ui::SimpleMenuModel::Delegate: 177 virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE; 178 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; 179 virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE; 180 virtual bool GetAcceleratorForCommandId( 181 int command_id, 182 ui::Accelerator* accelerator) OVERRIDE; 183 virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE; 184 185 private: 186 message_center::MessageCenter* message_center_; // Weak reference. 187 message_center::MessageCenterTray* tray_; // Weak reference. 188 std::string notification_id_; 189 190 DISALLOW_COPY_AND_ASSIGN(MenuModel); 191 }; 192 193 MenuModel::MenuModel(message_center::MessageCenter* message_center, 194 message_center::MessageCenterTray* tray, 195 const std::string& notification_id, 196 const string16& display_source, 197 const std::string& extension_id) 198 : ui::SimpleMenuModel(this), 199 message_center_(message_center), 200 tray_(tray), 201 notification_id_(notification_id) { 202 // Add 'disable notifications' menu item. 203 if (!extension_id.empty() && !display_source.empty()) { 204 AddItem(kToggleExtensionCommand, 205 l10n_util::GetStringFUTF16(IDS_MESSAGE_CENTER_EXTENSIONS_DISABLE, 206 display_source)); 207 } else if (!display_source.empty()) { 208 AddItem(kTogglePermissionCommand, 209 l10n_util::GetStringFUTF16(IDS_MESSAGE_CENTER_SITE_DISABLE, 210 display_source)); 211 } 212 // Add settings menu item. 213 AddItem(kShowSettingsCommand, 214 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_SETTINGS)); 215 } 216 217 MenuModel::~MenuModel() { 218 } 219 220 bool MenuModel::IsItemForCommandIdDynamic(int command_id) const { 221 return false; 222 } 223 224 bool MenuModel::IsCommandIdChecked(int command_id) const { 225 return false; 226 } 227 228 bool MenuModel::IsCommandIdEnabled(int command_id) const { 229 return true; 230 } 231 232 bool MenuModel::GetAcceleratorForCommandId(int command_id, 233 ui::Accelerator* accelerator) { 234 return false; 235 } 236 237 void MenuModel::ExecuteCommand(int command_id, int event_flags) { 238 switch (command_id) { 239 case kToggleExtensionCommand: 240 message_center_->DisableNotificationsByExtension(notification_id_); 241 break; 242 case kTogglePermissionCommand: 243 message_center_->DisableNotificationsByUrl(notification_id_); 244 break; 245 case kShowSettingsCommand: 246 // |tray_| may be NULL in tests. 247 if (tray_) 248 tray_->ShowNotifierSettingsBubble(); 249 else 250 message_center_->ShowNotificationSettings(notification_id_); 251 break; 252 default: 253 NOTREACHED(); 254 } 255 } 256 257 } // namespace 258 259 namespace message_center { 260 261 class MessageViewContextMenuController : public views::ContextMenuController { 262 public: 263 MessageViewContextMenuController( 264 MessageCenter* message_center, 265 MessageCenterTray* tray, 266 const Notification& notification); 267 virtual ~MessageViewContextMenuController(); 268 269 protected: 270 // Overridden from views::ContextMenuController: 271 virtual void ShowContextMenuForView(views::View* source, 272 const gfx::Point& point, 273 ui::MenuSourceType source_type) OVERRIDE; 274 275 MessageCenter* message_center_; // Weak reference. 276 MessageCenterTray* tray_; // Weak reference. 277 std::string notification_id_; 278 string16 display_source_; 279 std::string extension_id_; 280 }; 281 282 MessageViewContextMenuController::MessageViewContextMenuController( 283 MessageCenter* message_center, 284 MessageCenterTray* tray, 285 const Notification& notification) 286 : message_center_(message_center), 287 tray_(tray), 288 notification_id_(notification.id()), 289 display_source_(notification.display_source()), 290 extension_id_(notification.extension_id()) { 291 } 292 293 MessageViewContextMenuController::~MessageViewContextMenuController() { 294 } 295 296 void MessageViewContextMenuController::ShowContextMenuForView( 297 views::View* source, 298 const gfx::Point& point, 299 ui::MenuSourceType source_type) { 300 MenuModel menu_model(message_center_, tray_, notification_id_, 301 display_source_, extension_id_); 302 if (menu_model.GetItemCount() == 0) 303 return; 304 305 views::MenuRunner menu_runner(&menu_model); 306 307 ignore_result(menu_runner.RunMenuAt( 308 source->GetWidget()->GetTopLevelWidget(), 309 NULL, 310 gfx::Rect(point, gfx::Size()), 311 views::MenuItemView::TOPRIGHT, 312 source_type, 313 views::MenuRunner::HAS_MNEMONICS)); 314 } 315 316 MessageView::MessageView(const Notification& notification, 317 MessageCenter* message_center, 318 MessageCenterTray* tray, 319 bool expanded) 320 : message_center_(message_center), 321 notification_id_(notification.id()), 322 context_menu_controller_(new MessageViewContextMenuController( 323 message_center, tray, notification)), 324 scroller_(NULL), 325 is_expanded_(expanded) { 326 set_focusable(true); 327 set_context_menu_controller(context_menu_controller_.get()); 328 329 ControlButton *close = new ControlButton(this); 330 close->SetPadding(-kCloseIconRightPadding, kCloseIconTopPadding); 331 close->SetNormalImage(IDR_NOTIFICATION_CLOSE); 332 close->SetHoveredImage(IDR_NOTIFICATION_CLOSE_HOVER); 333 close->SetPressedImage(IDR_NOTIFICATION_CLOSE_PRESSED); 334 close->set_owned_by_client(); 335 close->set_animate_on_state_change(false); 336 close->SetAccessibleName(l10n_util::GetStringUTF16( 337 IDS_MESSAGE_CENTER_CLOSE_NOTIFICATION_BUTTON_ACCESSIBLE_NAME)); 338 close_button_.reset(close); 339 340 ControlButton *expand = new ControlButton(this); 341 expand->SetPadding(-kExpandIconRightPadding, -kExpandIconBottomPadding); 342 expand->SetNormalImage(IDR_NOTIFICATION_EXPAND); 343 expand->SetHoveredImage(IDR_NOTIFICATION_EXPAND_HOVER); 344 expand->SetPressedImage(IDR_NOTIFICATION_EXPAND_PRESSED); 345 expand->set_owned_by_client(); 346 expand->set_animate_on_state_change(false); 347 expand->SetAccessibleName(l10n_util::GetStringUTF16( 348 IDS_MESSAGE_CENTER_EXPAND_NOTIFICATION_BUTTON_ACCESSIBLE_NAME)); 349 expand_button_.reset(expand); 350 } 351 352 MessageView::MessageView() { 353 } 354 355 MessageView::~MessageView() { 356 } 357 358 // static 359 gfx::Insets MessageView::GetShadowInsets() { 360 return gfx::Insets(kShadowBlur / 2 - kShadowOffset, 361 kShadowBlur / 2, 362 kShadowBlur / 2 + kShadowOffset, 363 kShadowBlur / 2); 364 } 365 366 void MessageView::CreateShadowBorder() { 367 set_border(new views::ShadowBorder(kShadowBlur, 368 message_center::kShadowColor, 369 kShadowOffset, // Vertical offset. 370 0)); // Horizontal offset. 371 } 372 373 bool MessageView::IsCloseButtonFocused() { 374 views::FocusManager* focus_manager = GetFocusManager(); 375 return focus_manager && focus_manager->GetFocusedView() == close_button(); 376 } 377 378 void MessageView::RequestFocusOnCloseButton() { 379 close_button_->RequestFocus(); 380 } 381 382 void MessageView::GetAccessibleState(ui::AccessibleViewState* state) { 383 state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON; 384 state->name = accessible_name_; 385 } 386 387 bool MessageView::OnMousePressed(const ui::MouseEvent& event) { 388 if (event.IsOnlyLeftMouseButton()) { 389 message_center_->ClickOnNotification(notification_id_); 390 return true; 391 } 392 return false; 393 } 394 395 bool MessageView::OnKeyPressed(const ui::KeyEvent& event) { 396 if (event.flags() != ui::EF_NONE) 397 return false; 398 399 if (event.key_code() == ui::VKEY_RETURN) { 400 message_center_->ClickOnNotification(notification_id_); 401 return true; 402 } else if ((event.key_code() == ui::VKEY_DELETE || 403 event.key_code() == ui::VKEY_BACK)) { 404 message_center_->RemoveNotification(notification_id_, true); // By user. 405 return true; 406 } 407 408 return false; 409 } 410 411 bool MessageView::OnKeyReleased(const ui::KeyEvent& event) { 412 // Space key handling is triggerred at key-release timing. See 413 // ui/views/controls/buttons/custom_button.cc for why. 414 if (event.flags() != ui::EF_NONE || event.flags() != ui::VKEY_SPACE) 415 return false; 416 417 message_center_->ClickOnNotification(notification_id_); 418 return true; 419 } 420 421 void MessageView::OnGestureEvent(ui::GestureEvent* event) { 422 if (event->type() == ui::ET_GESTURE_TAP) { 423 message_center_->ClickOnNotification(notification_id_); 424 event->SetHandled(); 425 return; 426 } 427 428 SlideOutView::OnGestureEvent(event); 429 // Do not return here by checking handled(). SlideOutView calls SetHandled() 430 // even though the scroll gesture doesn't make no (or little) effects on the 431 // slide-out behavior. See http://crbug.com/172991 432 433 if (!event->IsScrollGestureEvent() && !event->IsFlingScrollEvent()) 434 return; 435 436 if (scroller_) 437 scroller_->OnGestureEvent(event); 438 event->SetHandled(); 439 } 440 441 void MessageView::OnPaintFocusBorder(gfx::Canvas* canvas) { 442 if (HasFocus()) { 443 canvas->DrawRect(gfx::Rect(1, 0, width() - 2, height() - 2), 444 message_center::kFocusBorderColor); 445 } 446 } 447 448 void MessageView::ButtonPressed(views::Button* sender, 449 const ui::Event& event) { 450 if (sender == close_button()) { 451 message_center_->RemoveNotification(notification_id_, true); // By user. 452 } else if (sender == expand_button()) { 453 is_expanded_ = true; 454 message_center_->ExpandNotification(notification_id_); 455 } 456 } 457 458 void MessageView::OnSlideOut() { 459 message_center_->RemoveNotification(notification_id_, true); // By user. 460 } 461 462 } // namespace message_center 463