1 // Copyright (c) 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 "ui/message_center/views/notifier_settings_view.h" 6 7 #include <set> 8 #include <string> 9 10 #include "base/strings/string16.h" 11 #include "base/strings/utf_string_conversions.h" 12 #include "grit/ui_resources.h" 13 #include "grit/ui_strings.h" 14 #include "skia/ext/image_operations.h" 15 #include "third_party/skia/include/core/SkColor.h" 16 #include "ui/base/keycodes/keyboard_codes.h" 17 #include "ui/base/l10n/l10n_util.h" 18 #include "ui/base/models/simple_menu_model.h" 19 #include "ui/base/resource/resource_bundle.h" 20 #include "ui/gfx/canvas.h" 21 #include "ui/gfx/image/image.h" 22 #include "ui/gfx/size.h" 23 #include "ui/message_center/message_center_style.h" 24 #include "ui/message_center/views/message_center_view.h" 25 #include "ui/views/background.h" 26 #include "ui/views/border.h" 27 #include "ui/views/controls/button/checkbox.h" 28 #include "ui/views/controls/button/custom_button.h" 29 #include "ui/views/controls/button/label_button_border.h" 30 #include "ui/views/controls/button/menu_button.h" 31 #include "ui/views/controls/image_view.h" 32 #include "ui/views/controls/label.h" 33 #include "ui/views/controls/menu/menu_runner.h" 34 #include "ui/views/controls/scroll_view.h" 35 #include "ui/views/controls/scrollbar/overlay_scroll_bar.h" 36 #include "ui/views/layout/box_layout.h" 37 #include "ui/views/layout/fill_layout.h" 38 #include "ui/views/layout/grid_layout.h" 39 #include "ui/views/widget/widget.h" 40 41 #if defined(USE_AURA) 42 #include "ui/aura/window.h" 43 #endif 44 45 namespace message_center { 46 namespace { 47 const int kButtonPainterInsets = 5; 48 // We really want the margin to be 20px, but various views are padded by 49 // whitespace. 50 const int kDesiredMargin = 20; 51 // The MenuButton has 2px whitespace built-in. 52 const int kMenuButtonInnateMargin = 2; 53 const int kMinimumHorizontalMargin = kDesiredMargin - kMenuButtonInnateMargin; 54 // The EntryViews' leftmost view is a checkbox with 1px whitespace built in, so 55 // the margin for entry views should be one less than the target margin. 56 const int kCheckboxInnateMargin = 1; 57 const int kEntryMargin = kDesiredMargin - kCheckboxInnateMargin; 58 const int kMenuButtonLeftPadding = 12; 59 const int kMenuButtonRightPadding = 13; 60 const int kMenuButtonVerticalPadding = 9; 61 const int kMenuWhitespaceOffset = 2; 62 const int kMinimumWindowHeight = 480; 63 const int kMinimumWindowWidth = 320; 64 const int kSettingsTitleBottomMargin = 12; 65 const int kSettingsTitleTopMargin = 15; 66 const int kSpaceInButtonComponents = 16; 67 const int kTitleVerticalMargin = 1; 68 const int kTitleElementSpacing = 10; 69 const int kEntryHeight = kMinimumWindowHeight / 10; 70 71 // The view to guarantee the 48px height and place the contents at the 72 // middle. It also guarantee the left margin. 73 class EntryView : public views::View { 74 public: 75 EntryView(views::View* contents); 76 virtual ~EntryView(); 77 78 // Overridden from views::View: 79 virtual void Layout() OVERRIDE; 80 virtual gfx::Size GetPreferredSize() OVERRIDE; 81 virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE; 82 virtual void OnFocus() OVERRIDE; 83 virtual void OnPaintFocusBorder(gfx::Canvas* canvas) OVERRIDE; 84 virtual bool OnKeyPressed(const ui::KeyEvent& event) OVERRIDE; 85 virtual bool OnKeyReleased(const ui::KeyEvent& event) OVERRIDE; 86 87 private: 88 DISALLOW_COPY_AND_ASSIGN(EntryView); 89 }; 90 91 EntryView::EntryView(views::View* contents) { 92 AddChildView(contents); 93 } 94 95 EntryView::~EntryView() { 96 } 97 98 void EntryView::Layout() { 99 DCHECK_EQ(1, child_count()); 100 views::View* content = child_at(0); 101 int content_width = width() - kEntryMargin * 2; 102 int content_height = content->GetHeightForWidth(content_width); 103 int y = std::max((height() - content_height) / 2, 0); 104 content->SetBounds(kEntryMargin, y, content_width, content_height); 105 } 106 107 gfx::Size EntryView::GetPreferredSize() { 108 DCHECK_EQ(1, child_count()); 109 gfx::Size size = child_at(0)->GetPreferredSize(); 110 size.SetToMax(gfx::Size(kMinimumWindowWidth, kEntryHeight)); 111 return size; 112 } 113 114 void EntryView::GetAccessibleState(ui::AccessibleViewState* state) { 115 DCHECK_EQ(1, child_count()); 116 child_at(0)->GetAccessibleState(state); 117 } 118 119 void EntryView::OnFocus() { 120 views::View::OnFocus(); 121 ScrollRectToVisible(GetLocalBounds()); 122 } 123 124 void EntryView::OnPaintFocusBorder(gfx::Canvas* canvas) { 125 if (HasFocus() && (focusable() || IsAccessibilityFocusable())) { 126 canvas->DrawRect(gfx::Rect(2, 1, width() - 4, height() - 3), 127 kFocusBorderColor); 128 } 129 } 130 131 bool EntryView::OnKeyPressed(const ui::KeyEvent& event) { 132 return child_at(0)->OnKeyPressed(event); 133 } 134 135 bool EntryView::OnKeyReleased(const ui::KeyEvent& event) { 136 return child_at(0)->OnKeyReleased(event); 137 } 138 139 } // namespace 140 141 // NotifierGroupMenuButtonBorder /////////////////////////////////////////////// 142 //////////////////////////////////////////////////////////////////////////////// 143 class NotifierGroupMenuButtonBorder : public views::TextButtonDefaultBorder { 144 public: 145 NotifierGroupMenuButtonBorder(); 146 147 private: 148 virtual ~NotifierGroupMenuButtonBorder(); 149 }; 150 151 NotifierGroupMenuButtonBorder::NotifierGroupMenuButtonBorder() 152 : views::TextButtonDefaultBorder() { 153 ui::ResourceBundle& rb = ResourceBundle::GetSharedInstance(); 154 155 gfx::Insets insets(kButtonPainterInsets, 156 kButtonPainterInsets, 157 kButtonPainterInsets, 158 kButtonPainterInsets); 159 160 set_normal_painter(views::Painter::CreateImagePainter( 161 *rb.GetImageSkiaNamed(IDR_BUTTON_NORMAL), insets)); 162 set_hot_painter(views::Painter::CreateImagePainter( 163 *rb.GetImageSkiaNamed(IDR_BUTTON_HOVER), insets)); 164 set_pushed_painter(views::Painter::CreateImagePainter( 165 *rb.GetImageSkiaNamed(IDR_BUTTON_PRESSED), insets)); 166 167 SetInsets(gfx::Insets(kMenuButtonVerticalPadding, 168 kMenuButtonLeftPadding, 169 kMenuButtonVerticalPadding, 170 kMenuButtonRightPadding)); 171 } 172 173 NotifierGroupMenuButtonBorder::~NotifierGroupMenuButtonBorder() {} 174 175 // NotifierGroupMenuModel ////////////////////////////////////////////////////// 176 //////////////////////////////////////////////////////////////////////////////// 177 class NotifierGroupMenuModel : public ui::SimpleMenuModel, 178 public ui::SimpleMenuModel::Delegate { 179 public: 180 NotifierGroupMenuModel(NotifierSettingsProvider* notifier_settings_provider); 181 virtual ~NotifierGroupMenuModel(); 182 183 // Overridden from ui::SimpleMenuModel::Delegate: 184 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE; 185 virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE; 186 virtual bool GetAcceleratorForCommandId( 187 int command_id, 188 ui::Accelerator* accelerator) OVERRIDE; 189 virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE; 190 191 private: 192 NotifierSettingsProvider* notifier_settings_provider_; 193 }; 194 195 NotifierGroupMenuModel::NotifierGroupMenuModel( 196 NotifierSettingsProvider* notifier_settings_provider) 197 : ui::SimpleMenuModel(this), 198 notifier_settings_provider_(notifier_settings_provider) { 199 if (!notifier_settings_provider_) 200 return; 201 202 size_t num_menu_items = notifier_settings_provider_->GetNotifierGroupCount(); 203 for (size_t i = 0; i < num_menu_items; ++i) { 204 const NotifierGroup& group = 205 notifier_settings_provider_->GetNotifierGroupAt(i); 206 207 AddCheckItem(i, group.login_info.empty() ? group.name : group.login_info); 208 } 209 } 210 211 NotifierGroupMenuModel::~NotifierGroupMenuModel() {} 212 213 bool NotifierGroupMenuModel::IsCommandIdChecked(int command_id) const { 214 // If there's no provider, assume only one notifier group - the active one. 215 if (!notifier_settings_provider_) 216 return true; 217 218 return notifier_settings_provider_->IsNotifierGroupActiveAt(command_id); 219 } 220 221 bool NotifierGroupMenuModel::IsCommandIdEnabled(int command_id) const { 222 return true; 223 } 224 225 bool NotifierGroupMenuModel::GetAcceleratorForCommandId( 226 int command_id, 227 ui::Accelerator* accelerator) { 228 return false; 229 } 230 231 void NotifierGroupMenuModel::ExecuteCommand(int command_id, int event_flags) { 232 if (!notifier_settings_provider_) 233 return; 234 235 size_t notifier_group_index = static_cast<size_t>(command_id); 236 size_t num_notifier_groups = 237 notifier_settings_provider_->GetNotifierGroupCount(); 238 if (notifier_group_index >= num_notifier_groups) 239 return; 240 241 notifier_settings_provider_->SwitchToNotifierGroup(notifier_group_index); 242 } 243 244 // We do not use views::Checkbox class directly because it doesn't support 245 // showing 'icon'. 246 class NotifierSettingsView::NotifierButton : public views::CustomButton, 247 public views::ButtonListener { 248 public: 249 NotifierButton(Notifier* notifier, views::ButtonListener* listener) 250 : views::CustomButton(listener), 251 notifier_(notifier), 252 icon_view_(NULL), 253 checkbox_(new views::Checkbox(string16())) { 254 DCHECK(notifier); 255 SetLayoutManager(new views::BoxLayout( 256 views::BoxLayout::kHorizontal, 0, 0, kSpaceInButtonComponents)); 257 checkbox_->SetChecked(notifier_->enabled); 258 checkbox_->set_listener(this); 259 checkbox_->set_focusable(false); 260 checkbox_->SetAccessibleName(notifier_->name); 261 AddChildView(checkbox_); 262 UpdateIconImage(notifier_->icon); 263 AddChildView(new views::Label(notifier_->name)); 264 } 265 266 void UpdateIconImage(const gfx::Image& icon) { 267 notifier_->icon = icon; 268 if (icon.IsEmpty()) { 269 delete icon_view_; 270 icon_view_ = NULL; 271 } else { 272 if (!icon_view_) { 273 icon_view_ = new views::ImageView(); 274 AddChildViewAt(icon_view_, 1); 275 } 276 icon_view_->SetImage(icon.ToImageSkia()); 277 icon_view_->SetImageSize(gfx::Size(kSettingsIconSize, kSettingsIconSize)); 278 } 279 Layout(); 280 SchedulePaint(); 281 } 282 283 void SetChecked(bool checked) { 284 checkbox_->SetChecked(checked); 285 notifier_->enabled = checked; 286 } 287 288 bool checked() const { 289 return checkbox_->checked(); 290 } 291 292 const Notifier& notifier() const { 293 return *notifier_.get(); 294 } 295 296 private: 297 // Overridden from views::ButtonListener: 298 virtual void ButtonPressed(views::Button* button, 299 const ui::Event& event) OVERRIDE { 300 DCHECK(button == checkbox_); 301 // The checkbox state has already changed at this point, but we'll update 302 // the state on NotifierSettingsView::ButtonPressed() too, so here change 303 // back to the previous state. 304 checkbox_->SetChecked(!checkbox_->checked()); 305 CustomButton::NotifyClick(event); 306 } 307 308 virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE { 309 static_cast<views::View*>(checkbox_)->GetAccessibleState(state); 310 } 311 312 scoped_ptr<Notifier> notifier_; 313 views::ImageView* icon_view_; 314 views::Checkbox* checkbox_; 315 316 DISALLOW_COPY_AND_ASSIGN(NotifierButton); 317 }; 318 319 NotifierSettingsView::NotifierSettingsView(NotifierSettingsProvider* provider) 320 : title_arrow_(NULL), 321 title_label_(NULL), 322 notifier_group_selector_(NULL), 323 scroller_(NULL), 324 provider_(provider) { 325 // |provider_| may be NULL in tests. 326 if (provider_) 327 provider_->AddObserver(this); 328 329 set_focusable(true); 330 set_focus_border(NULL); 331 set_background(views::Background::CreateSolidBackground( 332 kMessageCenterBackgroundColor)); 333 if (get_use_acceleration_when_possible()) 334 SetPaintToLayer(true); 335 336 gfx::Font title_font = 337 ResourceBundle::GetSharedInstance().GetFont(ResourceBundle::MediumFont); 338 title_label_ = new views::Label( 339 l10n_util::GetStringUTF16(IDS_MESSAGE_CENTER_SETTINGS_BUTTON_LABEL), 340 title_font); 341 title_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT); 342 title_label_->SetMultiLine(true); 343 title_label_->set_border( 344 views::Border::CreateEmptyBorder(kSettingsTitleTopMargin, 345 kDesiredMargin, 346 kSettingsTitleBottomMargin, 347 kDesiredMargin)); 348 349 AddChildView(title_label_); 350 351 scroller_ = new views::ScrollView(); 352 scroller_->SetVerticalScrollBar(new views::OverlayScrollBar(false)); 353 AddChildView(scroller_); 354 355 std::vector<Notifier*> notifiers; 356 if (provider_) 357 provider_->GetNotifierList(¬ifiers); 358 359 UpdateContentsView(notifiers); 360 } 361 362 NotifierSettingsView::~NotifierSettingsView() { 363 // |provider_| may be NULL in tests. 364 if (provider_) 365 provider_->RemoveObserver(this); 366 } 367 368 bool NotifierSettingsView::IsScrollable() { 369 return scroller_->height() < scroller_->contents()->height(); 370 } 371 372 void NotifierSettingsView::UpdateIconImage(const NotifierId& notifier_id, 373 const gfx::Image& icon) { 374 for (std::set<NotifierButton*>::iterator iter = buttons_.begin(); 375 iter != buttons_.end(); ++iter) { 376 if ((*iter)->notifier().notifier_id == notifier_id) { 377 (*iter)->UpdateIconImage(icon); 378 return; 379 } 380 } 381 } 382 383 void NotifierSettingsView::NotifierGroupChanged() { 384 std::vector<Notifier*> notifiers; 385 if (provider_) 386 provider_->GetNotifierList(¬ifiers); 387 388 UpdateContentsView(notifiers); 389 } 390 391 void NotifierSettingsView::UpdateContentsView( 392 const std::vector<Notifier*>& notifiers) { 393 buttons_.clear(); 394 395 views::View* contents_view = new views::View(); 396 contents_view->SetLayoutManager( 397 new views::BoxLayout(views::BoxLayout::kVertical, 0, 0, 0)); 398 399 views::View* contents_title_view = new views::View(); 400 contents_title_view->SetLayoutManager( 401 new views::BoxLayout(views::BoxLayout::kVertical, 402 kMinimumHorizontalMargin, 403 kTitleVerticalMargin, 404 kTitleElementSpacing)); 405 406 bool need_account_switcher = 407 provider_ && provider_->GetNotifierGroupCount() > 1; 408 int top_label_resource_id = 409 need_account_switcher ? IDS_MESSAGE_CENTER_SETTINGS_DESCRIPTION_MULTIUSER 410 : IDS_MESSAGE_CENTER_SETTINGS_DIALOG_DESCRIPTION; 411 412 views::Label* top_label = 413 new views::Label(l10n_util::GetStringUTF16(top_label_resource_id)); 414 415 top_label->SetHorizontalAlignment(gfx::ALIGN_LEFT); 416 top_label->SetMultiLine(true); 417 top_label->set_border(views::Border::CreateEmptyBorder( 418 0, kMenuButtonInnateMargin, 0, kMenuButtonInnateMargin)); 419 contents_title_view->AddChildView(top_label); 420 421 string16 notifier_group_text; 422 if (provider_) { 423 const NotifierGroup& active_group = provider_->GetActiveNotifierGroup(); 424 notifier_group_text = active_group.login_info.empty() 425 ? active_group.name 426 : active_group.login_info; 427 } 428 429 if (need_account_switcher) { 430 notifier_group_selector_ = 431 new views::MenuButton(NULL, notifier_group_text, this, true); 432 notifier_group_selector_->set_border(new NotifierGroupMenuButtonBorder); 433 notifier_group_selector_->set_focus_border(NULL); 434 notifier_group_selector_->set_animate_on_state_change(false); 435 notifier_group_selector_->set_focusable(true); 436 contents_title_view->AddChildView(notifier_group_selector_); 437 } 438 439 contents_view->AddChildView(contents_title_view); 440 441 for (size_t i = 0; i < notifiers.size(); ++i) { 442 NotifierButton* button = new NotifierButton(notifiers[i], this); 443 EntryView* entry = new EntryView(button); 444 entry->set_focusable(true); 445 contents_view->AddChildView(entry); 446 buttons_.insert(button); 447 } 448 449 scroller_->SetContents(contents_view); 450 451 contents_view->SetBoundsRect(gfx::Rect(contents_view->GetPreferredSize())); 452 InvalidateLayout(); 453 } 454 455 void NotifierSettingsView::Layout() { 456 int title_height = title_label_->GetHeightForWidth(width()); 457 title_label_->SetBounds(0, 0, width(), title_height); 458 459 views::View* contents_view = scroller_->contents(); 460 int content_width = width(); 461 int content_height = contents_view->GetHeightForWidth(content_width); 462 if (title_height + content_height > height()) { 463 content_width -= scroller_->GetScrollBarWidth(); 464 content_height = contents_view->GetHeightForWidth(content_width); 465 } 466 contents_view->SetBounds(0, 0, content_width, content_height); 467 scroller_->SetBounds(0, title_height, width(), height() - title_height); 468 } 469 470 gfx::Size NotifierSettingsView::GetMinimumSize() { 471 gfx::Size size(kMinimumWindowWidth, kMinimumWindowHeight); 472 int total_height = title_label_->GetPreferredSize().height() + 473 scroller_->contents()->GetPreferredSize().height(); 474 if (total_height > kMinimumWindowHeight) 475 size.Enlarge(scroller_->GetScrollBarWidth(), 0); 476 return size; 477 } 478 479 gfx::Size NotifierSettingsView::GetPreferredSize() { 480 gfx::Size preferred_size; 481 std::vector<gfx::Size> child_sizes; 482 gfx::Size title_size = title_label_->GetPreferredSize(); 483 gfx::Size content_size = scroller_->contents()->GetPreferredSize(); 484 return gfx::Size(std::max(title_size.width(), content_size.width()), 485 title_size.height() + content_size.height()); 486 } 487 488 bool NotifierSettingsView::OnKeyPressed(const ui::KeyEvent& event) { 489 if (event.key_code() == ui::VKEY_ESCAPE) { 490 GetWidget()->Close(); 491 return true; 492 } 493 494 return scroller_->OnKeyPressed(event); 495 } 496 497 bool NotifierSettingsView::OnMouseWheel(const ui::MouseWheelEvent& event) { 498 return scroller_->OnMouseWheel(event); 499 } 500 501 void NotifierSettingsView::ButtonPressed(views::Button* sender, 502 const ui::Event& event) { 503 if (sender == title_arrow_) { 504 MessageCenterView* center_view = static_cast<MessageCenterView*>(parent()); 505 center_view->SetSettingsVisible(!center_view->settings_visible()); 506 return; 507 } 508 509 std::set<NotifierButton*>::iterator iter = buttons_.find( 510 static_cast<NotifierButton*>(sender)); 511 512 if (iter == buttons_.end()) 513 return; 514 515 (*iter)->SetChecked(!(*iter)->checked()); 516 if (provider_) 517 provider_->SetNotifierEnabled((*iter)->notifier(), (*iter)->checked()); 518 } 519 520 void NotifierSettingsView::OnMenuButtonClicked(views::View* source, 521 const gfx::Point& point) { 522 notifier_group_menu_model_.reset(new NotifierGroupMenuModel(provider_)); 523 notifier_group_menu_runner_.reset( 524 new views::MenuRunner(notifier_group_menu_model_.get())); 525 gfx::Rect menu_anchor = source->GetBoundsInScreen(); 526 menu_anchor.Inset( 527 gfx::Insets(0, kMenuWhitespaceOffset, 0, kMenuWhitespaceOffset)); 528 if (views::MenuRunner::MENU_DELETED == 529 notifier_group_menu_runner_->RunMenuAt(GetWidget(), 530 notifier_group_selector_, 531 menu_anchor, 532 views::MenuItemView::BUBBLE_ABOVE, 533 ui::MENU_SOURCE_MOUSE, 534 views::MenuRunner::CONTEXT_MENU)) 535 return; 536 MessageCenterView* center_view = static_cast<MessageCenterView*>(parent()); 537 center_view->OnSettingsChanged(); 538 } 539 540 } // namespace message_center 541